From 6668ed271eaefaa63e686bdfbedaeb7b8e492722 Mon Sep 17 00:00:00 2001 From: Kumar Kartikeya Dwivedi Date: Fri, 4 Sep 2026 10:43:19 +0200 Subject: selftests/bpf: Reject graph kptr use after RCU unlock Add a sleepable verifier test that loads a graph-node local kptr in an explicit RCU read-side critical section, then passes its node to bpf_rbtree_remove() after the section ends. Before the verifier fix, the stale NON_OWN_REF flag makes the node look like a live borrowed reference and the program is accepted. After the fix, the pointer is demoted without NON_OWN_REF and the graph kfunc argument is rejected. Also exercise a graph kptr loaded while a spin lock provides implicit RCU protection. The pointer must be invalidated when the lock is released, which guards the required ordering between non-owning-reference invalidation and RCU demotion. Update the existing fault-protected load test state description. The post-unlock pointer no longer carries NON_OWN_REF, but remains readable because the load is rewritten to use BPF_PROBE_MEM. Signed-off-by: Kumar Kartikeya Dwivedi Link: https://lore.kernel.org/r/20260904084325.52250-7-memxor@gmail.com Signed-off-by: Alexei Starovoitov --- drivers/platform/Kconfig | 24 + drivers/platform/Makefile | 17 + drivers/platform/arm64/Kconfig | 106 + drivers/platform/arm64/Makefile | 12 + drivers/platform/arm64/acer-aspire1-ec.c | 562 + drivers/platform/arm64/huawei-gaokun-ec.c | 827 ++ drivers/platform/arm64/lenovo-thinkpad-t14s.c | 662 + drivers/platform/arm64/lenovo-yoga-c630.c | 257 + drivers/platform/arm64/qcom-hamoa-ec.c | 451 + drivers/platform/chrome/Kconfig | 330 + drivers/platform/chrome/Makefile | 46 + drivers/platform/chrome/chromeos_acpi.c | 286 + drivers/platform/chrome/chromeos_laptop.c | 956 ++ drivers/platform/chrome/chromeos_of_hw_prober.c | 185 + drivers/platform/chrome/chromeos_privacy_screen.c | 150 + drivers/platform/chrome/chromeos_pstore.c | 145 + drivers/platform/chrome/chromeos_tbmc.c | 142 + drivers/platform/chrome/cros_ec.c | 522 + drivers/platform/chrome/cros_ec.h | 30 + drivers/platform/chrome/cros_ec_chardev.c | 505 + drivers/platform/chrome/cros_ec_debugfs.c | 583 + drivers/platform/chrome/cros_ec_i2c.c | 379 + drivers/platform/chrome/cros_ec_ishtp.c | 751 ++ drivers/platform/chrome/cros_ec_lightbar.c | 710 ++ drivers/platform/chrome/cros_ec_lpc.c | 913 ++ drivers/platform/chrome/cros_ec_lpc_mec.c | 221 + drivers/platform/chrome/cros_ec_lpc_mec.h | 84 + drivers/platform/chrome/cros_ec_proto.c | 1172 ++ drivers/platform/chrome/cros_ec_proto_test.c | 2744 +++++ drivers/platform/chrome/cros_ec_proto_test_util.c | 128 + drivers/platform/chrome/cros_ec_proto_test_util.h | 51 + drivers/platform/chrome/cros_ec_rpmsg.c | 305 + drivers/platform/chrome/cros_ec_sensorhub.c | 288 + drivers/platform/chrome/cros_ec_sensorhub_ring.c | 1107 ++ drivers/platform/chrome/cros_ec_sensorhub_trace.h | 123 + drivers/platform/chrome/cros_ec_spi.c | 836 ++ drivers/platform/chrome/cros_ec_sysfs.c | 440 + drivers/platform/chrome/cros_ec_trace.c | 225 + drivers/platform/chrome/cros_ec_trace.h | 83 + drivers/platform/chrome/cros_ec_typec.c | 1474 +++ drivers/platform/chrome/cros_ec_typec.h | 87 + drivers/platform/chrome/cros_ec_uart.c | 357 + drivers/platform/chrome/cros_ec_vbc.c | 154 + drivers/platform/chrome/cros_hps_i2c.c | 162 + drivers/platform/chrome/cros_kbd_led_backlight.c | 240 + drivers/platform/chrome/cros_typec_altmode.c | 374 + drivers/platform/chrome/cros_typec_altmode.h | 51 + drivers/platform/chrome/cros_typec_switch.c | 319 + drivers/platform/chrome/cros_typec_vdm.c | 148 + drivers/platform/chrome/cros_typec_vdm.h | 13 + drivers/platform/chrome/cros_usbpd_logger.c | 268 + drivers/platform/chrome/cros_usbpd_notify.c | 280 + drivers/platform/chrome/wilco_ec/Kconfig | 39 + drivers/platform/chrome/wilco_ec/Makefile | 11 + drivers/platform/chrome/wilco_ec/core.c | 174 + drivers/platform/chrome/wilco_ec/debugfs.c | 286 + drivers/platform/chrome/wilco_ec/event.c | 592 + drivers/platform/chrome/wilco_ec/keyboard_leds.c | 203 + drivers/platform/chrome/wilco_ec/mailbox.c | 221 + drivers/platform/chrome/wilco_ec/properties.c | 135 + drivers/platform/chrome/wilco_ec/sysfs.c | 250 + drivers/platform/chrome/wilco_ec/telemetry.c | 474 + drivers/platform/cznic/Kconfig | 98 + drivers/platform/cznic/Makefile | 11 + drivers/platform/cznic/turris-omnia-mcu-base.c | 419 + drivers/platform/cznic/turris-omnia-mcu-gpio.c | 1123 ++ drivers/platform/cznic/turris-omnia-mcu-keyctl.c | 162 + .../cznic/turris-omnia-mcu-sys-off-wakeup.c | 260 + drivers/platform/cznic/turris-omnia-mcu-trng.c | 94 + drivers/platform/cznic/turris-omnia-mcu-watchdog.c | 130 + drivers/platform/cznic/turris-omnia-mcu.h | 163 + drivers/platform/cznic/turris-signing-key.c | 193 + drivers/platform/goldfish/Kconfig | 20 + drivers/platform/goldfish/Makefile | 5 + drivers/platform/goldfish/goldfish_pipe.c | 947 ++ drivers/platform/goldfish/goldfish_pipe_qemu.h | 98 + drivers/platform/loongarch/Kconfig | 31 + drivers/platform/loongarch/Makefile | 1 + drivers/platform/loongarch/loongson-laptop.c | 633 + drivers/platform/mellanox/Kconfig | 124 + drivers/platform/mellanox/Makefile | 14 + drivers/platform/mellanox/mlx-platform.c | 8661 +++++++++++++ drivers/platform/mellanox/mlxbf-bootctl.c | 1084 ++ drivers/platform/mellanox/mlxbf-bootctl.h | 141 + drivers/platform/mellanox/mlxbf-pmc.c | 2371 ++++ drivers/platform/mellanox/mlxbf-tmfifo-regs.h | 73 + drivers/platform/mellanox/mlxbf-tmfifo.c | 1461 +++ drivers/platform/mellanox/mlxreg-dpu.c | 613 + drivers/platform/mellanox/mlxreg-hotplug.c | 797 ++ drivers/platform/mellanox/mlxreg-io.c | 286 + drivers/platform/mellanox/mlxreg-lc.c | 958 ++ drivers/platform/mellanox/nvsw-sn2201.c | 1371 +++ drivers/platform/mips/Kconfig | 40 + drivers/platform/mips/Makefile | 4 + drivers/platform/mips/cpu_hwmon.c | 173 + drivers/platform/mips/ls2k-reset.c | 53 + drivers/platform/mips/rs780e-acpi.c | 165 + drivers/platform/olpc/Kconfig | 29 + drivers/platform/olpc/Makefile | 6 + drivers/platform/olpc/olpc-ec.c | 492 + drivers/platform/olpc/olpc-xo175-ec.c | 758 ++ drivers/platform/raspberrypi/Kconfig | 52 + drivers/platform/raspberrypi/Makefile | 15 + .../platform/raspberrypi/vchiq-interface/TESTING | 125 + drivers/platform/raspberrypi/vchiq-interface/TODO | 4 + .../raspberrypi/vchiq-interface/vchiq_arm.c | 1476 +++ .../raspberrypi/vchiq-interface/vchiq_bus.c | 112 + .../raspberrypi/vchiq-interface/vchiq_core.c | 4013 ++++++ .../raspberrypi/vchiq-interface/vchiq_debugfs.c | 157 + .../raspberrypi/vchiq-interface/vchiq_dev.c | 1355 ++ .../raspberrypi/vchiq-interface/vchiq_ioctl.h | 112 + drivers/platform/raspberrypi/vchiq-mmal/Kconfig | 7 + drivers/platform/raspberrypi/vchiq-mmal/Makefile | 4 + .../platform/raspberrypi/vchiq-mmal/mmal-common.h | 65 + .../raspberrypi/vchiq-mmal/mmal-encodings.h | 124 + .../raspberrypi/vchiq-mmal/mmal-msg-common.h | 45 + .../raspberrypi/vchiq-mmal/mmal-msg-format.h | 108 + .../raspberrypi/vchiq-mmal/mmal-msg-port.h | 109 + drivers/platform/raspberrypi/vchiq-mmal/mmal-msg.h | 406 + .../raspberrypi/vchiq-mmal/mmal-parameters.h | 752 ++ .../platform/raspberrypi/vchiq-mmal/mmal-vchiq.c | 1949 +++ .../platform/raspberrypi/vchiq-mmal/mmal-vchiq.h | 162 + drivers/platform/surface/Kconfig | 234 + drivers/platform/surface/Makefile | 19 + drivers/platform/surface/aggregator/Kconfig | 69 + drivers/platform/surface/aggregator/Makefile | 14 + drivers/platform/surface/aggregator/bus.c | 527 + drivers/platform/surface/aggregator/bus.h | 24 + drivers/platform/surface/aggregator/controller.c | 2847 +++++ drivers/platform/surface/aggregator/controller.h | 285 + drivers/platform/surface/aggregator/core.c | 894 ++ drivers/platform/surface/aggregator/ssh_msgb.h | 205 + .../platform/surface/aggregator/ssh_packet_layer.c | 2086 ++++ .../platform/surface/aggregator/ssh_packet_layer.h | 190 + drivers/platform/surface/aggregator/ssh_parser.c | 228 + drivers/platform/surface/aggregator/ssh_parser.h | 154 + .../surface/aggregator/ssh_request_layer.c | 1274 ++ .../surface/aggregator/ssh_request_layer.h | 143 + drivers/platform/surface/aggregator/trace.h | 703 ++ drivers/platform/surface/surface3-wmi.c | 286 + drivers/platform/surface/surface3_power.c | 586 + drivers/platform/surface/surface_acpi_notify.c | 884 ++ drivers/platform/surface/surface_aggregator_cdev.c | 808 ++ drivers/platform/surface/surface_aggregator_hub.c | 371 + .../platform/surface/surface_aggregator_registry.c | 592 + .../platform/surface/surface_aggregator_tabletsw.c | 645 + drivers/platform/surface/surface_dtx.c | 1282 ++ drivers/platform/surface/surface_gpe.c | 336 + drivers/platform/surface/surface_hotplug.c | 273 + .../platform/surface/surface_platform_profile.c | 257 + drivers/platform/surface/surfacepro3_button.c | 277 + drivers/platform/wmi/Kconfig | 37 + drivers/platform/wmi/Makefile | 11 + drivers/platform/wmi/core.c | 1612 +++ drivers/platform/wmi/internal.h | 18 + drivers/platform/wmi/marshalling.c | 251 + drivers/platform/wmi/string.c | 92 + drivers/platform/wmi/tests/Kconfig | 27 + drivers/platform/wmi/tests/Makefile | 11 + drivers/platform/wmi/tests/marshalling_kunit.c | 472 + drivers/platform/wmi/tests/string_kunit.c | 296 + drivers/platform/x86/Kconfig | 1078 ++ drivers/platform/x86/Makefile | 172 + drivers/platform/x86/acer-wireless.c | 89 + drivers/platform/x86/acer-wmi.c | 3299 +++++ drivers/platform/x86/acerhdf.c | 771 ++ drivers/platform/x86/adv_swbutton.c | 123 + drivers/platform/x86/amd/Kconfig | 46 + drivers/platform/x86/amd/Makefile | 14 + drivers/platform/x86/amd/amd_isp4.c | 419 + drivers/platform/x86/amd/hfi/Kconfig | 18 + drivers/platform/x86/amd/hfi/Makefile | 7 + drivers/platform/x86/amd/hfi/hfi.c | 548 + drivers/platform/x86/amd/hsmp/Kconfig | 49 + drivers/platform/x86/amd/hsmp/Makefile | 13 + drivers/platform/x86/amd/hsmp/acpi.c | 647 + drivers/platform/x86/amd/hsmp/hsmp.c | 488 + drivers/platform/x86/amd/hsmp/hsmp.h | 74 + drivers/platform/x86/amd/hsmp/hwmon.c | 121 + drivers/platform/x86/amd/hsmp/plat.c | 350 + drivers/platform/x86/amd/pmc/Kconfig | 35 + drivers/platform/x86/amd/pmc/Makefile | 9 + drivers/platform/x86/amd/pmc/mp1_stb.c | 332 + drivers/platform/x86/amd/pmc/mp2_stb.c | 280 + drivers/platform/x86/amd/pmc/pmc-quirks.c | 423 + drivers/platform/x86/amd/pmc/pmc.c | 996 ++ drivers/platform/x86/amd/pmc/pmc.h | 204 + drivers/platform/x86/amd/pmf/Kconfig | 32 + drivers/platform/x86/amd/pmf/Makefile | 10 + drivers/platform/x86/amd/pmf/acpi.c | 646 + drivers/platform/x86/amd/pmf/auto-mode.c | 441 + drivers/platform/x86/amd/pmf/cnqf.c | 467 + drivers/platform/x86/amd/pmf/core.c | 672 + drivers/platform/x86/amd/pmf/pmf.h | 931 ++ drivers/platform/x86/amd/pmf/spc.c | 348 + drivers/platform/x86/amd/pmf/sps.c | 444 + drivers/platform/x86/amd/pmf/tee-if.c | 625 + drivers/platform/x86/amd/wbrf.c | 311 + drivers/platform/x86/amd/x3d_vcache.c | 176 + drivers/platform/x86/amilo-rfkill.c | 179 + drivers/platform/x86/apple-gmux.c | 1039 ++ drivers/platform/x86/asus-armoury.c | 1167 ++ drivers/platform/x86/asus-armoury.h | 2406 ++++ drivers/platform/x86/asus-laptop.c | 1980 +++ drivers/platform/x86/asus-nb-wmi.c | 719 ++ drivers/platform/x86/asus-tf103c-dock.c | 942 ++ drivers/platform/x86/asus-wireless.c | 213 + drivers/platform/x86/asus-wmi.c | 5491 +++++++++ drivers/platform/x86/asus-wmi.h | 89 + drivers/platform/x86/ayaneo-ec.c | 593 + drivers/platform/x86/barco-p50-gpio.c | 463 + drivers/platform/x86/bitland-mifs-wmi.c | 845 ++ drivers/platform/x86/classmate-laptop.c | 1233 ++ drivers/platform/x86/compal-laptop.c | 1113 ++ drivers/platform/x86/dasharo-acpi.c | 360 + drivers/platform/x86/dell/Kconfig | 284 + drivers/platform/x86/dell/Makefile | 31 + drivers/platform/x86/dell/alienware-wmi-base.c | 496 + drivers/platform/x86/dell/alienware-wmi-legacy.c | 95 + drivers/platform/x86/dell/alienware-wmi-wmax.c | 1684 +++ drivers/platform/x86/dell/alienware-wmi.h | 117 + drivers/platform/x86/dell/dcdbas.c | 781 ++ drivers/platform/x86/dell/dcdbas.h | 110 + drivers/platform/x86/dell/dell-dw5826e-reset.c | 93 + drivers/platform/x86/dell/dell-laptop.c | 2614 ++++ drivers/platform/x86/dell/dell-lis3lv02d.c | 260 + drivers/platform/x86/dell/dell-pc.c | 302 + drivers/platform/x86/dell/dell-rbtn.c | 513 + drivers/platform/x86/dell/dell-rbtn.h | 16 + drivers/platform/x86/dell/dell-smbios-base.c | 667 + drivers/platform/x86/dell/dell-smbios-smm.c | 153 + drivers/platform/x86/dell/dell-smbios-wmi.c | 337 + drivers/platform/x86/dell/dell-smbios.h | 116 + drivers/platform/x86/dell/dell-smo8800-ids.h | 27 + drivers/platform/x86/dell/dell-smo8800.c | 178 + drivers/platform/x86/dell/dell-uart-backlight.c | 407 + drivers/platform/x86/dell/dell-wmi-aio.c | 186 + drivers/platform/x86/dell/dell-wmi-base.c | 862 ++ drivers/platform/x86/dell/dell-wmi-ddv.c | 1114 ++ drivers/platform/x86/dell/dell-wmi-descriptor.c | 193 + drivers/platform/x86/dell/dell-wmi-descriptor.h | 25 + drivers/platform/x86/dell/dell-wmi-led.c | 186 + drivers/platform/x86/dell/dell-wmi-privacy.c | 392 + drivers/platform/x86/dell/dell-wmi-privacy.h | 36 + drivers/platform/x86/dell/dell-wmi-sysman/Makefile | 8 + .../x86/dell/dell-wmi-sysman/biosattr-interface.c | 185 + .../x86/dell/dell-wmi-sysman/dell-wmi-sysman.h | 199 + .../x86/dell/dell-wmi-sysman/enum-attributes.c | 244 + .../x86/dell/dell-wmi-sysman/int-attributes.c | 199 + .../x86/dell/dell-wmi-sysman/passobj-attributes.c | 198 + .../dell/dell-wmi-sysman/passwordattr-interface.c | 151 + .../x86/dell/dell-wmi-sysman/string-attributes.c | 177 + drivers/platform/x86/dell/dell-wmi-sysman/sysman.c | 596 + drivers/platform/x86/dell/dell_rbu.c | 683 + drivers/platform/x86/dual_accel_detect.h | 52 + drivers/platform/x86/eeepc-laptop.c | 1522 +++ drivers/platform/x86/eeepc-wmi.c | 222 + drivers/platform/x86/firmware_attributes_class.c | 27 + drivers/platform/x86/firmware_attributes_class.h | 12 + drivers/platform/x86/fujitsu-laptop.c | 1197 ++ drivers/platform/x86/fujitsu-tablet.c | 543 + drivers/platform/x86/gigabyte-wmi.c | 164 + drivers/platform/x86/gpd-pocket-fan.c | 224 + drivers/platform/x86/hdaps.c | 627 + drivers/platform/x86/hp/Kconfig | 81 + drivers/platform/x86/hp/Makefile | 11 + drivers/platform/x86/hp/hp-bioscfg/Makefile | 11 + .../x86/hp/hp-bioscfg/biosattr-interface.c | 312 + drivers/platform/x86/hp/hp-bioscfg/bioscfg.c | 1067 ++ drivers/platform/x86/hp/hp-bioscfg/bioscfg.h | 489 + .../platform/x86/hp/hp-bioscfg/enum-attributes.c | 452 + .../platform/x86/hp/hp-bioscfg/int-attributes.c | 415 + .../x86/hp/hp-bioscfg/order-list-attributes.c | 437 + .../x86/hp/hp-bioscfg/passwdobj-attributes.c | 546 + .../platform/x86/hp/hp-bioscfg/spmobj-attributes.c | 380 + .../platform/x86/hp/hp-bioscfg/string-attributes.c | 391 + .../x86/hp/hp-bioscfg/surestart-attributes.c | 132 + drivers/platform/x86/hp/hp-wmi.c | 2798 +++++ drivers/platform/x86/hp/hp_accel.c | 389 + drivers/platform/x86/hp/tc1100-wmi.c | 263 + drivers/platform/x86/huawei-wmi.c | 904 ++ drivers/platform/x86/ibm_rtl.c | 325 + drivers/platform/x86/inspur_platform_profile.c | 220 + drivers/platform/x86/intel/Kconfig | 253 + drivers/platform/x86/intel/Makefile | 57 + drivers/platform/x86/intel/atomisp2/Kconfig | 43 + drivers/platform/x86/intel/atomisp2/Makefile | 9 + drivers/platform/x86/intel/atomisp2/led.c | 116 + drivers/platform/x86/intel/atomisp2/pm.c | 142 + drivers/platform/x86/intel/bxtwc_tmu.c | 135 + drivers/platform/x86/intel/bytcrc_pwrsrc.c | 255 + drivers/platform/x86/intel/chtdc_ti_pwrbtn.c | 93 + drivers/platform/x86/intel/chtwc_int33fe.c | 448 + drivers/platform/x86/intel/crystal_cove_charger.c | 153 + drivers/platform/x86/intel/ehl_pse_io.c | 85 + drivers/platform/x86/intel/hid.c | 829 ++ drivers/platform/x86/intel/ifs/Kconfig | 12 + drivers/platform/x86/intel/ifs/Makefile | 3 + drivers/platform/x86/intel/ifs/core.c | 158 + drivers/platform/x86/intel/ifs/ifs.h | 417 + drivers/platform/x86/intel/ifs/load.c | 438 + drivers/platform/x86/intel/ifs/runtest.c | 663 + drivers/platform/x86/intel/ifs/sysfs.c | 155 + drivers/platform/x86/intel/int0002_vgpio.c | 293 + drivers/platform/x86/intel/int1092/Kconfig | 14 + drivers/platform/x86/intel/int1092/Makefile | 1 + drivers/platform/x86/intel/int1092/intel_sar.c | 326 + drivers/platform/x86/intel/int1092/intel_sar.h | 86 + drivers/platform/x86/intel/int3472/Kconfig | 31 + drivers/platform/x86/intel/int3472/Makefile | 8 + .../platform/x86/intel/int3472/clk_and_regulator.c | 256 + drivers/platform/x86/intel/int3472/common.c | 90 + drivers/platform/x86/intel/int3472/discrete.c | 570 + .../platform/x86/intel/int3472/discrete_quirks.c | 21 + drivers/platform/x86/intel/int3472/led.c | 63 + drivers/platform/x86/intel/int3472/tps68470.c | 266 + drivers/platform/x86/intel/int3472/tps68470.h | 27 + .../x86/intel/int3472/tps68470_board_data.c | 549 + drivers/platform/x86/intel/ishtp_eclite.c | 703 ++ drivers/platform/x86/intel/mrfld_pwrbtn.c | 106 + drivers/platform/x86/intel/oaktrail.c | 370 + drivers/platform/x86/intel/plr_tpmi.c | 395 + drivers/platform/x86/intel/pmc/Kconfig | 55 + drivers/platform/x86/intel/pmc/Makefile | 17 + drivers/platform/x86/intel/pmc/adl.c | 318 + drivers/platform/x86/intel/pmc/arl.c | 754 ++ drivers/platform/x86/intel/pmc/cnp.c | 283 + drivers/platform/x86/intel/pmc/core.c | 2161 ++++ drivers/platform/x86/intel/pmc/core.h | 662 + drivers/platform/x86/intel/pmc/icl.c | 55 + drivers/platform/x86/intel/pmc/lnl.c | 586 + drivers/platform/x86/intel/pmc/mtl.c | 1011 ++ drivers/platform/x86/intel/pmc/nvl.c | 1539 +++ drivers/platform/x86/intel/pmc/pltdrv.c | 90 + drivers/platform/x86/intel/pmc/ptl.c | 584 + drivers/platform/x86/intel/pmc/pwrm_telemetry.c | 215 + drivers/platform/x86/intel/pmc/spt.c | 160 + drivers/platform/x86/intel/pmc/ssram_telemetry.c | 488 + drivers/platform/x86/intel/pmc/ssram_telemetry.h | 24 + drivers/platform/x86/intel/pmc/tgl.c | 312 + drivers/platform/x86/intel/pmc/wcl.c | 508 + drivers/platform/x86/intel/pmt/Kconfig | 68 + drivers/platform/x86/intel/pmt/Makefile | 16 + drivers/platform/x86/intel/pmt/class.c | 565 + drivers/platform/x86/intel/pmt/class.h | 89 + drivers/platform/x86/intel/pmt/crashlog.c | 621 + drivers/platform/x86/intel/pmt/discovery-kunit.c | 116 + drivers/platform/x86/intel/pmt/discovery.c | 637 + drivers/platform/x86/intel/pmt/features.c | 205 + drivers/platform/x86/intel/pmt/telemetry.c | 449 + drivers/platform/x86/intel/pmt/telemetry.h | 126 + drivers/platform/x86/intel/punit_ipc.c | 301 + drivers/platform/x86/intel/rst.c | 147 + drivers/platform/x86/intel/sdsi.c | 710 ++ drivers/platform/x86/intel/smartconnect.c | 49 + drivers/platform/x86/intel/speed_select_if/Kconfig | 21 + .../platform/x86/intel/speed_select_if/Makefile | 12 + .../x86/intel/speed_select_if/isst_if_common.c | 826 ++ .../x86/intel/speed_select_if/isst_if_common.h | 82 + .../x86/intel/speed_select_if/isst_if_mbox_msr.c | 213 + .../x86/intel/speed_select_if/isst_if_mbox_pci.c | 227 + .../x86/intel/speed_select_if/isst_if_mmio.c | 205 + .../platform/x86/intel/speed_select_if/isst_tpmi.c | 72 + .../x86/intel/speed_select_if/isst_tpmi_core.c | 1870 +++ .../x86/intel/speed_select_if/isst_tpmi_core.h | 18 + drivers/platform/x86/intel/telemetry/Kconfig | 16 + drivers/platform/x86/intel/telemetry/Makefile | 11 + drivers/platform/x86/intel/telemetry/core.c | 273 + drivers/platform/x86/intel/telemetry/debugfs.c | 961 ++ drivers/platform/x86/intel/telemetry/pltdrv.c | 957 ++ drivers/platform/x86/intel/tpmi_power_domains.c | 265 + drivers/platform/x86/intel/tpmi_power_domains.h | 19 + drivers/platform/x86/intel/turbo_max_3.c | 140 + .../platform/x86/intel/uncore-frequency/Kconfig | 25 + .../platform/x86/intel/uncore-frequency/Makefile | 11 + .../uncore-frequency/uncore-frequency-common.c | 366 + .../uncore-frequency/uncore-frequency-common.h | 121 + .../intel/uncore-frequency/uncore-frequency-tpmi.c | 784 ++ .../x86/intel/uncore-frequency/uncore-frequency.c | 330 + drivers/platform/x86/intel/vbtn.c | 397 + drivers/platform/x86/intel/vsec.c | 908 ++ drivers/platform/x86/intel/vsec_tpmi.c | 867 ++ drivers/platform/x86/intel/wmi/Kconfig | 31 + drivers/platform/x86/intel/wmi/Makefile | 9 + drivers/platform/x86/intel/wmi/sbl-fw-update.c | 128 + drivers/platform/x86/intel/wmi/thunderbolt.c | 71 + drivers/platform/x86/intel_ips.c | 1624 +++ drivers/platform/x86/intel_ips.h | 6 + drivers/platform/x86/intel_scu_ipc.c | 705 ++ drivers/platform/x86/intel_scu_ipcutil.c | 152 + drivers/platform/x86/intel_scu_pcidrv.c | 54 + drivers/platform/x86/intel_scu_pltdrv.c | 59 + drivers/platform/x86/intel_scu_wdt.c | 75 + drivers/platform/x86/lenovo/Kconfig | 278 + drivers/platform/x86/lenovo/Makefile | 28 + drivers/platform/x86/lenovo/ideapad-laptop.c | 2574 ++++ drivers/platform/x86/lenovo/ideapad-laptop.h | 22 + drivers/platform/x86/lenovo/think-lmi.c | 1859 +++ drivers/platform/x86/lenovo/think-lmi.h | 140 + drivers/platform/x86/lenovo/thinkpad_acpi.c | 12313 +++++++++++++++++++ drivers/platform/x86/lenovo/wmi-camera.c | 147 + drivers/platform/x86/lenovo/wmi-capdata.c | 948 ++ drivers/platform/x86/lenovo/wmi-capdata.h | 91 + drivers/platform/x86/lenovo/wmi-events.c | 197 + drivers/platform/x86/lenovo/wmi-events.h | 20 + drivers/platform/x86/lenovo/wmi-gamezone.c | 409 + drivers/platform/x86/lenovo/wmi-helpers.c | 224 + drivers/platform/x86/lenovo/wmi-helpers.h | 43 + drivers/platform/x86/lenovo/wmi-hotkey-utilities.c | 224 + drivers/platform/x86/lenovo/wmi-other.c | 1738 +++ drivers/platform/x86/lenovo/ymc.c | 166 + .../x86/lenovo/yoga-tab2-pro-1380-fastcharger.c | 333 + drivers/platform/x86/lenovo/yogabook.c | 575 + drivers/platform/x86/lg-laptop.c | 912 ++ drivers/platform/x86/meegopad_anx7428.c | 150 + drivers/platform/x86/meraki-mx100.c | 345 + drivers/platform/x86/msi-ec.c | 1365 ++ drivers/platform/x86/msi-ec.h | 122 + drivers/platform/x86/msi-laptop.c | 1152 ++ drivers/platform/x86/msi-wmi-platform.c | 494 + drivers/platform/x86/msi-wmi.c | 333 + drivers/platform/x86/mxm-wmi.c | 82 + drivers/platform/x86/nvidia-wmi-ec-backlight.c | 142 + drivers/platform/x86/oxpec.c | 1008 ++ drivers/platform/x86/p2sb.c | 263 + drivers/platform/x86/panasonic-laptop.c | 1141 ++ drivers/platform/x86/pcengines-apuv2.c | 339 + drivers/platform/x86/pmc_atom.c | 607 + drivers/platform/x86/portwell-ec.c | 460 + drivers/platform/x86/quickstart.c | 236 + drivers/platform/x86/redmi-wmi.c | 154 + drivers/platform/x86/samsung-galaxybook.c | 1456 +++ drivers/platform/x86/samsung-laptop.c | 1767 +++ drivers/platform/x86/samsung-q10.c | 157 + drivers/platform/x86/sel3350-platform.c | 319 + drivers/platform/x86/serdev_helpers.h | 88 + drivers/platform/x86/serial-multi-instantiate.c | 445 + drivers/platform/x86/siemens/Kconfig | 64 + drivers/platform/x86/siemens/Makefile | 11 + .../x86/siemens/simatic-ipc-batt-apollolake.c | 52 + .../x86/siemens/simatic-ipc-batt-elkhartlake.c | 52 + .../platform/x86/siemens/simatic-ipc-batt-f7188x.c | 88 + drivers/platform/x86/siemens/simatic-ipc-batt.c | 253 + drivers/platform/x86/siemens/simatic-ipc-batt.h | 20 + drivers/platform/x86/siemens/simatic-ipc.c | 237 + drivers/platform/x86/silicom-platform.c | 996 ++ drivers/platform/x86/sony-laptop.c | 4750 +++++++ drivers/platform/x86/system76_acpi.c | 832 ++ drivers/platform/x86/topstar-laptop.c | 408 + drivers/platform/x86/toshiba-wmi.c | 124 + drivers/platform/x86/toshiba_acpi.c | 3674 ++++++ drivers/platform/x86/toshiba_bluetooth.c | 316 + drivers/platform/x86/toshiba_haps.c | 283 + drivers/platform/x86/touchscreen_dmi.c | 2008 +++ drivers/platform/x86/tuxedo/Kconfig | 8 + drivers/platform/x86/tuxedo/Makefile | 8 + drivers/platform/x86/tuxedo/nb04/Kconfig | 17 + drivers/platform/x86/tuxedo/nb04/Makefile | 10 + drivers/platform/x86/tuxedo/nb04/wmi_ab.c | 923 ++ drivers/platform/x86/tuxedo/nb04/wmi_util.c | 91 + drivers/platform/x86/tuxedo/nb04/wmi_util.h | 109 + drivers/platform/x86/uniwill/Kconfig | 38 + drivers/platform/x86/uniwill/Makefile | 8 + drivers/platform/x86/uniwill/uniwill-acpi.c | 2774 +++++ drivers/platform/x86/uniwill/uniwill-wmi.c | 95 + drivers/platform/x86/uniwill/uniwill-wmi.h | 129 + drivers/platform/x86/uv_sysfs.c | 931 ++ drivers/platform/x86/winmate-fm07-keys.c | 189 + drivers/platform/x86/wireless-hotkey.c | 138 + drivers/platform/x86/wmi-bmof.c | 102 + drivers/platform/x86/x86-android-tablets/Kconfig | 26 + drivers/platform/x86/x86-android-tablets/Makefile | 9 + drivers/platform/x86/x86-android-tablets/acer.c | 247 + drivers/platform/x86/x86-android-tablets/asus.c | 319 + drivers/platform/x86/x86-android-tablets/core.c | 623 + drivers/platform/x86/x86-android-tablets/dmi.c | 233 + drivers/platform/x86/x86-android-tablets/lenovo.c | 1079 ++ drivers/platform/x86/x86-android-tablets/other.c | 883 ++ .../x86/x86-android-tablets/shared-psy-info.c | 186 + .../x86/x86-android-tablets/shared-psy-info.h | 35 + .../x86/x86-android-tablets/vexia_atla10_ec.c | 261 + .../x86/x86-android-tablets/x86-android-tablets.h | 138 + drivers/platform/x86/xiaomi-wmi.c | 96 + drivers/platform/x86/xo1-rfkill.c | 79 + drivers/platform/x86/xo15-ebook.c | 163 + 485 files changed, 237324 insertions(+) create mode 100644 drivers/platform/Kconfig create mode 100644 drivers/platform/Makefile create mode 100644 drivers/platform/arm64/Kconfig create mode 100644 drivers/platform/arm64/Makefile create mode 100644 drivers/platform/arm64/acer-aspire1-ec.c create mode 100644 drivers/platform/arm64/huawei-gaokun-ec.c create mode 100644 drivers/platform/arm64/lenovo-thinkpad-t14s.c create mode 100644 drivers/platform/arm64/lenovo-yoga-c630.c create mode 100644 drivers/platform/arm64/qcom-hamoa-ec.c create mode 100644 drivers/platform/chrome/Kconfig create mode 100644 drivers/platform/chrome/Makefile create mode 100644 drivers/platform/chrome/chromeos_acpi.c create mode 100644 drivers/platform/chrome/chromeos_laptop.c create mode 100644 drivers/platform/chrome/chromeos_of_hw_prober.c create mode 100644 drivers/platform/chrome/chromeos_privacy_screen.c create mode 100644 drivers/platform/chrome/chromeos_pstore.c create mode 100644 drivers/platform/chrome/chromeos_tbmc.c create mode 100644 drivers/platform/chrome/cros_ec.c create mode 100644 drivers/platform/chrome/cros_ec.h create mode 100644 drivers/platform/chrome/cros_ec_chardev.c create mode 100644 drivers/platform/chrome/cros_ec_debugfs.c create mode 100644 drivers/platform/chrome/cros_ec_i2c.c create mode 100644 drivers/platform/chrome/cros_ec_ishtp.c create mode 100644 drivers/platform/chrome/cros_ec_lightbar.c create mode 100644 drivers/platform/chrome/cros_ec_lpc.c create mode 100644 drivers/platform/chrome/cros_ec_lpc_mec.c create mode 100644 drivers/platform/chrome/cros_ec_lpc_mec.h create mode 100644 drivers/platform/chrome/cros_ec_proto.c create mode 100644 drivers/platform/chrome/cros_ec_proto_test.c create mode 100644 drivers/platform/chrome/cros_ec_proto_test_util.c create mode 100644 drivers/platform/chrome/cros_ec_proto_test_util.h create mode 100644 drivers/platform/chrome/cros_ec_rpmsg.c create mode 100644 drivers/platform/chrome/cros_ec_sensorhub.c create mode 100644 drivers/platform/chrome/cros_ec_sensorhub_ring.c create mode 100644 drivers/platform/chrome/cros_ec_sensorhub_trace.h create mode 100644 drivers/platform/chrome/cros_ec_spi.c create mode 100644 drivers/platform/chrome/cros_ec_sysfs.c create mode 100644 drivers/platform/chrome/cros_ec_trace.c create mode 100644 drivers/platform/chrome/cros_ec_trace.h create mode 100644 drivers/platform/chrome/cros_ec_typec.c create mode 100644 drivers/platform/chrome/cros_ec_typec.h create mode 100644 drivers/platform/chrome/cros_ec_uart.c create mode 100644 drivers/platform/chrome/cros_ec_vbc.c create mode 100644 drivers/platform/chrome/cros_hps_i2c.c create mode 100644 drivers/platform/chrome/cros_kbd_led_backlight.c create mode 100644 drivers/platform/chrome/cros_typec_altmode.c create mode 100644 drivers/platform/chrome/cros_typec_altmode.h create mode 100644 drivers/platform/chrome/cros_typec_switch.c create mode 100644 drivers/platform/chrome/cros_typec_vdm.c create mode 100644 drivers/platform/chrome/cros_typec_vdm.h create mode 100644 drivers/platform/chrome/cros_usbpd_logger.c create mode 100644 drivers/platform/chrome/cros_usbpd_notify.c create mode 100644 drivers/platform/chrome/wilco_ec/Kconfig create mode 100644 drivers/platform/chrome/wilco_ec/Makefile create mode 100644 drivers/platform/chrome/wilco_ec/core.c create mode 100644 drivers/platform/chrome/wilco_ec/debugfs.c create mode 100644 drivers/platform/chrome/wilco_ec/event.c create mode 100644 drivers/platform/chrome/wilco_ec/keyboard_leds.c create mode 100644 drivers/platform/chrome/wilco_ec/mailbox.c create mode 100644 drivers/platform/chrome/wilco_ec/properties.c create mode 100644 drivers/platform/chrome/wilco_ec/sysfs.c create mode 100644 drivers/platform/chrome/wilco_ec/telemetry.c create mode 100644 drivers/platform/cznic/Kconfig create mode 100644 drivers/platform/cznic/Makefile create mode 100644 drivers/platform/cznic/turris-omnia-mcu-base.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu-gpio.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu-keyctl.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu-sys-off-wakeup.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu-trng.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu-watchdog.c create mode 100644 drivers/platform/cznic/turris-omnia-mcu.h create mode 100644 drivers/platform/cznic/turris-signing-key.c create mode 100644 drivers/platform/goldfish/Kconfig create mode 100644 drivers/platform/goldfish/Makefile create mode 100644 drivers/platform/goldfish/goldfish_pipe.c create mode 100644 drivers/platform/goldfish/goldfish_pipe_qemu.h create mode 100644 drivers/platform/loongarch/Kconfig create mode 100644 drivers/platform/loongarch/Makefile create mode 100644 drivers/platform/loongarch/loongson-laptop.c create mode 100644 drivers/platform/mellanox/Kconfig create mode 100644 drivers/platform/mellanox/Makefile create mode 100644 drivers/platform/mellanox/mlx-platform.c create mode 100644 drivers/platform/mellanox/mlxbf-bootctl.c create mode 100644 drivers/platform/mellanox/mlxbf-bootctl.h create mode 100644 drivers/platform/mellanox/mlxbf-pmc.c create mode 100644 drivers/platform/mellanox/mlxbf-tmfifo-regs.h create mode 100644 drivers/platform/mellanox/mlxbf-tmfifo.c create mode 100644 drivers/platform/mellanox/mlxreg-dpu.c create mode 100644 drivers/platform/mellanox/mlxreg-hotplug.c create mode 100644 drivers/platform/mellanox/mlxreg-io.c create mode 100644 drivers/platform/mellanox/mlxreg-lc.c create mode 100644 drivers/platform/mellanox/nvsw-sn2201.c create mode 100644 drivers/platform/mips/Kconfig create mode 100644 drivers/platform/mips/Makefile create mode 100644 drivers/platform/mips/cpu_hwmon.c create mode 100644 drivers/platform/mips/ls2k-reset.c create mode 100644 drivers/platform/mips/rs780e-acpi.c create mode 100644 drivers/platform/olpc/Kconfig create mode 100644 drivers/platform/olpc/Makefile create mode 100644 drivers/platform/olpc/olpc-ec.c create mode 100644 drivers/platform/olpc/olpc-xo175-ec.c create mode 100644 drivers/platform/raspberrypi/Kconfig create mode 100644 drivers/platform/raspberrypi/Makefile create mode 100644 drivers/platform/raspberrypi/vchiq-interface/TESTING create mode 100644 drivers/platform/raspberrypi/vchiq-interface/TODO create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_arm.c create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_bus.c create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_core.c create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_debugfs.c create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_dev.c create mode 100644 drivers/platform/raspberrypi/vchiq-interface/vchiq_ioctl.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/Kconfig create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/Makefile create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-common.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-encodings.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-common.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-format.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-port.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-msg.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-parameters.h create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.c create mode 100644 drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.h create mode 100644 drivers/platform/surface/Kconfig create mode 100644 drivers/platform/surface/Makefile create mode 100644 drivers/platform/surface/aggregator/Kconfig create mode 100644 drivers/platform/surface/aggregator/Makefile create mode 100644 drivers/platform/surface/aggregator/bus.c create mode 100644 drivers/platform/surface/aggregator/bus.h create mode 100644 drivers/platform/surface/aggregator/controller.c create mode 100644 drivers/platform/surface/aggregator/controller.h create mode 100644 drivers/platform/surface/aggregator/core.c create mode 100644 drivers/platform/surface/aggregator/ssh_msgb.h create mode 100644 drivers/platform/surface/aggregator/ssh_packet_layer.c create mode 100644 drivers/platform/surface/aggregator/ssh_packet_layer.h create mode 100644 drivers/platform/surface/aggregator/ssh_parser.c create mode 100644 drivers/platform/surface/aggregator/ssh_parser.h create mode 100644 drivers/platform/surface/aggregator/ssh_request_layer.c create mode 100644 drivers/platform/surface/aggregator/ssh_request_layer.h create mode 100644 drivers/platform/surface/aggregator/trace.h create mode 100644 drivers/platform/surface/surface3-wmi.c create mode 100644 drivers/platform/surface/surface3_power.c create mode 100644 drivers/platform/surface/surface_acpi_notify.c create mode 100644 drivers/platform/surface/surface_aggregator_cdev.c create mode 100644 drivers/platform/surface/surface_aggregator_hub.c create mode 100644 drivers/platform/surface/surface_aggregator_registry.c create mode 100644 drivers/platform/surface/surface_aggregator_tabletsw.c create mode 100644 drivers/platform/surface/surface_dtx.c create mode 100644 drivers/platform/surface/surface_gpe.c create mode 100644 drivers/platform/surface/surface_hotplug.c create mode 100644 drivers/platform/surface/surface_platform_profile.c create mode 100644 drivers/platform/surface/surfacepro3_button.c create mode 100644 drivers/platform/wmi/Kconfig create mode 100644 drivers/platform/wmi/Makefile create mode 100644 drivers/platform/wmi/core.c create mode 100644 drivers/platform/wmi/internal.h create mode 100644 drivers/platform/wmi/marshalling.c create mode 100644 drivers/platform/wmi/string.c create mode 100644 drivers/platform/wmi/tests/Kconfig create mode 100644 drivers/platform/wmi/tests/Makefile create mode 100644 drivers/platform/wmi/tests/marshalling_kunit.c create mode 100644 drivers/platform/wmi/tests/string_kunit.c create mode 100644 drivers/platform/x86/Kconfig create mode 100644 drivers/platform/x86/Makefile create mode 100644 drivers/platform/x86/acer-wireless.c create mode 100644 drivers/platform/x86/acer-wmi.c create mode 100644 drivers/platform/x86/acerhdf.c create mode 100644 drivers/platform/x86/adv_swbutton.c create mode 100644 drivers/platform/x86/amd/Kconfig create mode 100644 drivers/platform/x86/amd/Makefile create mode 100644 drivers/platform/x86/amd/amd_isp4.c create mode 100644 drivers/platform/x86/amd/hfi/Kconfig create mode 100644 drivers/platform/x86/amd/hfi/Makefile create mode 100644 drivers/platform/x86/amd/hfi/hfi.c create mode 100644 drivers/platform/x86/amd/hsmp/Kconfig create mode 100644 drivers/platform/x86/amd/hsmp/Makefile create mode 100644 drivers/platform/x86/amd/hsmp/acpi.c create mode 100644 drivers/platform/x86/amd/hsmp/hsmp.c create mode 100644 drivers/platform/x86/amd/hsmp/hsmp.h create mode 100644 drivers/platform/x86/amd/hsmp/hwmon.c create mode 100644 drivers/platform/x86/amd/hsmp/plat.c create mode 100644 drivers/platform/x86/amd/pmc/Kconfig create mode 100644 drivers/platform/x86/amd/pmc/Makefile create mode 100644 drivers/platform/x86/amd/pmc/mp1_stb.c create mode 100644 drivers/platform/x86/amd/pmc/mp2_stb.c create mode 100644 drivers/platform/x86/amd/pmc/pmc-quirks.c create mode 100644 drivers/platform/x86/amd/pmc/pmc.c create mode 100644 drivers/platform/x86/amd/pmc/pmc.h create mode 100644 drivers/platform/x86/amd/pmf/Kconfig create mode 100644 drivers/platform/x86/amd/pmf/Makefile create mode 100644 drivers/platform/x86/amd/pmf/acpi.c create mode 100644 drivers/platform/x86/amd/pmf/auto-mode.c create mode 100644 drivers/platform/x86/amd/pmf/cnqf.c create mode 100644 drivers/platform/x86/amd/pmf/core.c create mode 100644 drivers/platform/x86/amd/pmf/pmf.h create mode 100644 drivers/platform/x86/amd/pmf/spc.c create mode 100644 drivers/platform/x86/amd/pmf/sps.c create mode 100644 drivers/platform/x86/amd/pmf/tee-if.c create mode 100644 drivers/platform/x86/amd/wbrf.c create mode 100644 drivers/platform/x86/amd/x3d_vcache.c create mode 100644 drivers/platform/x86/amilo-rfkill.c create mode 100644 drivers/platform/x86/apple-gmux.c create mode 100644 drivers/platform/x86/asus-armoury.c create mode 100644 drivers/platform/x86/asus-armoury.h create mode 100644 drivers/platform/x86/asus-laptop.c create mode 100644 drivers/platform/x86/asus-nb-wmi.c create mode 100644 drivers/platform/x86/asus-tf103c-dock.c create mode 100644 drivers/platform/x86/asus-wireless.c create mode 100644 drivers/platform/x86/asus-wmi.c create mode 100644 drivers/platform/x86/asus-wmi.h create mode 100644 drivers/platform/x86/ayaneo-ec.c create mode 100644 drivers/platform/x86/barco-p50-gpio.c create mode 100644 drivers/platform/x86/bitland-mifs-wmi.c create mode 100644 drivers/platform/x86/classmate-laptop.c create mode 100644 drivers/platform/x86/compal-laptop.c create mode 100644 drivers/platform/x86/dasharo-acpi.c create mode 100644 drivers/platform/x86/dell/Kconfig create mode 100644 drivers/platform/x86/dell/Makefile create mode 100644 drivers/platform/x86/dell/alienware-wmi-base.c create mode 100644 drivers/platform/x86/dell/alienware-wmi-legacy.c create mode 100644 drivers/platform/x86/dell/alienware-wmi-wmax.c create mode 100644 drivers/platform/x86/dell/alienware-wmi.h create mode 100644 drivers/platform/x86/dell/dcdbas.c create mode 100644 drivers/platform/x86/dell/dcdbas.h create mode 100644 drivers/platform/x86/dell/dell-dw5826e-reset.c create mode 100644 drivers/platform/x86/dell/dell-laptop.c create mode 100644 drivers/platform/x86/dell/dell-lis3lv02d.c create mode 100644 drivers/platform/x86/dell/dell-pc.c create mode 100644 drivers/platform/x86/dell/dell-rbtn.c create mode 100644 drivers/platform/x86/dell/dell-rbtn.h create mode 100644 drivers/platform/x86/dell/dell-smbios-base.c create mode 100644 drivers/platform/x86/dell/dell-smbios-smm.c create mode 100644 drivers/platform/x86/dell/dell-smbios-wmi.c create mode 100644 drivers/platform/x86/dell/dell-smbios.h create mode 100644 drivers/platform/x86/dell/dell-smo8800-ids.h create mode 100644 drivers/platform/x86/dell/dell-smo8800.c create mode 100644 drivers/platform/x86/dell/dell-uart-backlight.c create mode 100644 drivers/platform/x86/dell/dell-wmi-aio.c create mode 100644 drivers/platform/x86/dell/dell-wmi-base.c create mode 100644 drivers/platform/x86/dell/dell-wmi-ddv.c create mode 100644 drivers/platform/x86/dell/dell-wmi-descriptor.c create mode 100644 drivers/platform/x86/dell/dell-wmi-descriptor.h create mode 100644 drivers/platform/x86/dell/dell-wmi-led.c create mode 100644 drivers/platform/x86/dell/dell-wmi-privacy.c create mode 100644 drivers/platform/x86/dell/dell-wmi-privacy.h create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/Makefile create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/biosattr-interface.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/dell-wmi-sysman.h create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/enum-attributes.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/int-attributes.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/passobj-attributes.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/passwordattr-interface.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/string-attributes.c create mode 100644 drivers/platform/x86/dell/dell-wmi-sysman/sysman.c create mode 100644 drivers/platform/x86/dell/dell_rbu.c create mode 100644 drivers/platform/x86/dual_accel_detect.h create mode 100644 drivers/platform/x86/eeepc-laptop.c create mode 100644 drivers/platform/x86/eeepc-wmi.c create mode 100644 drivers/platform/x86/firmware_attributes_class.c create mode 100644 drivers/platform/x86/firmware_attributes_class.h create mode 100644 drivers/platform/x86/fujitsu-laptop.c create mode 100644 drivers/platform/x86/fujitsu-tablet.c create mode 100644 drivers/platform/x86/gigabyte-wmi.c create mode 100644 drivers/platform/x86/gpd-pocket-fan.c create mode 100644 drivers/platform/x86/hdaps.c create mode 100644 drivers/platform/x86/hp/Kconfig create mode 100644 drivers/platform/x86/hp/Makefile create mode 100644 drivers/platform/x86/hp/hp-bioscfg/Makefile create mode 100644 drivers/platform/x86/hp/hp-bioscfg/biosattr-interface.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/bioscfg.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/bioscfg.h create mode 100644 drivers/platform/x86/hp/hp-bioscfg/enum-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/int-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/order-list-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/passwdobj-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/spmobj-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/string-attributes.c create mode 100644 drivers/platform/x86/hp/hp-bioscfg/surestart-attributes.c create mode 100644 drivers/platform/x86/hp/hp-wmi.c create mode 100644 drivers/platform/x86/hp/hp_accel.c create mode 100644 drivers/platform/x86/hp/tc1100-wmi.c create mode 100644 drivers/platform/x86/huawei-wmi.c create mode 100644 drivers/platform/x86/ibm_rtl.c create mode 100644 drivers/platform/x86/inspur_platform_profile.c create mode 100644 drivers/platform/x86/intel/Kconfig create mode 100644 drivers/platform/x86/intel/Makefile create mode 100644 drivers/platform/x86/intel/atomisp2/Kconfig create mode 100644 drivers/platform/x86/intel/atomisp2/Makefile create mode 100644 drivers/platform/x86/intel/atomisp2/led.c create mode 100644 drivers/platform/x86/intel/atomisp2/pm.c create mode 100644 drivers/platform/x86/intel/bxtwc_tmu.c create mode 100644 drivers/platform/x86/intel/bytcrc_pwrsrc.c create mode 100644 drivers/platform/x86/intel/chtdc_ti_pwrbtn.c create mode 100644 drivers/platform/x86/intel/chtwc_int33fe.c create mode 100644 drivers/platform/x86/intel/crystal_cove_charger.c create mode 100644 drivers/platform/x86/intel/ehl_pse_io.c create mode 100644 drivers/platform/x86/intel/hid.c create mode 100644 drivers/platform/x86/intel/ifs/Kconfig create mode 100644 drivers/platform/x86/intel/ifs/Makefile create mode 100644 drivers/platform/x86/intel/ifs/core.c create mode 100644 drivers/platform/x86/intel/ifs/ifs.h create mode 100644 drivers/platform/x86/intel/ifs/load.c create mode 100644 drivers/platform/x86/intel/ifs/runtest.c create mode 100644 drivers/platform/x86/intel/ifs/sysfs.c create mode 100644 drivers/platform/x86/intel/int0002_vgpio.c create mode 100644 drivers/platform/x86/intel/int1092/Kconfig create mode 100644 drivers/platform/x86/intel/int1092/Makefile create mode 100644 drivers/platform/x86/intel/int1092/intel_sar.c create mode 100644 drivers/platform/x86/intel/int1092/intel_sar.h create mode 100644 drivers/platform/x86/intel/int3472/Kconfig create mode 100644 drivers/platform/x86/intel/int3472/Makefile create mode 100644 drivers/platform/x86/intel/int3472/clk_and_regulator.c create mode 100644 drivers/platform/x86/intel/int3472/common.c create mode 100644 drivers/platform/x86/intel/int3472/discrete.c create mode 100644 drivers/platform/x86/intel/int3472/discrete_quirks.c create mode 100644 drivers/platform/x86/intel/int3472/led.c create mode 100644 drivers/platform/x86/intel/int3472/tps68470.c create mode 100644 drivers/platform/x86/intel/int3472/tps68470.h create mode 100644 drivers/platform/x86/intel/int3472/tps68470_board_data.c create mode 100644 drivers/platform/x86/intel/ishtp_eclite.c create mode 100644 drivers/platform/x86/intel/mrfld_pwrbtn.c create mode 100644 drivers/platform/x86/intel/oaktrail.c create mode 100644 drivers/platform/x86/intel/plr_tpmi.c create mode 100644 drivers/platform/x86/intel/pmc/Kconfig create mode 100644 drivers/platform/x86/intel/pmc/Makefile create mode 100644 drivers/platform/x86/intel/pmc/adl.c create mode 100644 drivers/platform/x86/intel/pmc/arl.c create mode 100644 drivers/platform/x86/intel/pmc/cnp.c create mode 100644 drivers/platform/x86/intel/pmc/core.c create mode 100644 drivers/platform/x86/intel/pmc/core.h create mode 100644 drivers/platform/x86/intel/pmc/icl.c create mode 100644 drivers/platform/x86/intel/pmc/lnl.c create mode 100644 drivers/platform/x86/intel/pmc/mtl.c create mode 100644 drivers/platform/x86/intel/pmc/nvl.c create mode 100644 drivers/platform/x86/intel/pmc/pltdrv.c create mode 100644 drivers/platform/x86/intel/pmc/ptl.c create mode 100644 drivers/platform/x86/intel/pmc/pwrm_telemetry.c create mode 100644 drivers/platform/x86/intel/pmc/spt.c create mode 100644 drivers/platform/x86/intel/pmc/ssram_telemetry.c create mode 100644 drivers/platform/x86/intel/pmc/ssram_telemetry.h create mode 100644 drivers/platform/x86/intel/pmc/tgl.c create mode 100644 drivers/platform/x86/intel/pmc/wcl.c create mode 100644 drivers/platform/x86/intel/pmt/Kconfig create mode 100644 drivers/platform/x86/intel/pmt/Makefile create mode 100644 drivers/platform/x86/intel/pmt/class.c create mode 100644 drivers/platform/x86/intel/pmt/class.h create mode 100644 drivers/platform/x86/intel/pmt/crashlog.c create mode 100644 drivers/platform/x86/intel/pmt/discovery-kunit.c create mode 100644 drivers/platform/x86/intel/pmt/discovery.c create mode 100644 drivers/platform/x86/intel/pmt/features.c create mode 100644 drivers/platform/x86/intel/pmt/telemetry.c create mode 100644 drivers/platform/x86/intel/pmt/telemetry.h create mode 100644 drivers/platform/x86/intel/punit_ipc.c create mode 100644 drivers/platform/x86/intel/rst.c create mode 100644 drivers/platform/x86/intel/sdsi.c create mode 100644 drivers/platform/x86/intel/smartconnect.c create mode 100644 drivers/platform/x86/intel/speed_select_if/Kconfig create mode 100644 drivers/platform/x86/intel/speed_select_if/Makefile create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_if_common.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_if_common.h create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_if_mbox_msr.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_if_mbox_pci.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_if_mmio.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_tpmi.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c create mode 100644 drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.h create mode 100644 drivers/platform/x86/intel/telemetry/Kconfig create mode 100644 drivers/platform/x86/intel/telemetry/Makefile create mode 100644 drivers/platform/x86/intel/telemetry/core.c create mode 100644 drivers/platform/x86/intel/telemetry/debugfs.c create mode 100644 drivers/platform/x86/intel/telemetry/pltdrv.c create mode 100644 drivers/platform/x86/intel/tpmi_power_domains.c create mode 100644 drivers/platform/x86/intel/tpmi_power_domains.h create mode 100644 drivers/platform/x86/intel/turbo_max_3.c create mode 100644 drivers/platform/x86/intel/uncore-frequency/Kconfig create mode 100644 drivers/platform/x86/intel/uncore-frequency/Makefile create mode 100644 drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.c create mode 100644 drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.h create mode 100644 drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c create mode 100644 drivers/platform/x86/intel/uncore-frequency/uncore-frequency.c create mode 100644 drivers/platform/x86/intel/vbtn.c create mode 100644 drivers/platform/x86/intel/vsec.c create mode 100644 drivers/platform/x86/intel/vsec_tpmi.c create mode 100644 drivers/platform/x86/intel/wmi/Kconfig create mode 100644 drivers/platform/x86/intel/wmi/Makefile create mode 100644 drivers/platform/x86/intel/wmi/sbl-fw-update.c create mode 100644 drivers/platform/x86/intel/wmi/thunderbolt.c create mode 100644 drivers/platform/x86/intel_ips.c create mode 100644 drivers/platform/x86/intel_ips.h create mode 100644 drivers/platform/x86/intel_scu_ipc.c create mode 100644 drivers/platform/x86/intel_scu_ipcutil.c create mode 100644 drivers/platform/x86/intel_scu_pcidrv.c create mode 100644 drivers/platform/x86/intel_scu_pltdrv.c create mode 100644 drivers/platform/x86/intel_scu_wdt.c create mode 100644 drivers/platform/x86/lenovo/Kconfig create mode 100644 drivers/platform/x86/lenovo/Makefile create mode 100644 drivers/platform/x86/lenovo/ideapad-laptop.c create mode 100644 drivers/platform/x86/lenovo/ideapad-laptop.h create mode 100644 drivers/platform/x86/lenovo/think-lmi.c create mode 100644 drivers/platform/x86/lenovo/think-lmi.h create mode 100644 drivers/platform/x86/lenovo/thinkpad_acpi.c create mode 100644 drivers/platform/x86/lenovo/wmi-camera.c create mode 100644 drivers/platform/x86/lenovo/wmi-capdata.c create mode 100644 drivers/platform/x86/lenovo/wmi-capdata.h create mode 100644 drivers/platform/x86/lenovo/wmi-events.c create mode 100644 drivers/platform/x86/lenovo/wmi-events.h create mode 100644 drivers/platform/x86/lenovo/wmi-gamezone.c create mode 100644 drivers/platform/x86/lenovo/wmi-helpers.c create mode 100644 drivers/platform/x86/lenovo/wmi-helpers.h create mode 100644 drivers/platform/x86/lenovo/wmi-hotkey-utilities.c create mode 100644 drivers/platform/x86/lenovo/wmi-other.c create mode 100644 drivers/platform/x86/lenovo/ymc.c create mode 100644 drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c create mode 100644 drivers/platform/x86/lenovo/yogabook.c create mode 100644 drivers/platform/x86/lg-laptop.c create mode 100644 drivers/platform/x86/meegopad_anx7428.c create mode 100644 drivers/platform/x86/meraki-mx100.c create mode 100644 drivers/platform/x86/msi-ec.c create mode 100644 drivers/platform/x86/msi-ec.h create mode 100644 drivers/platform/x86/msi-laptop.c create mode 100644 drivers/platform/x86/msi-wmi-platform.c create mode 100644 drivers/platform/x86/msi-wmi.c create mode 100644 drivers/platform/x86/mxm-wmi.c create mode 100644 drivers/platform/x86/nvidia-wmi-ec-backlight.c create mode 100644 drivers/platform/x86/oxpec.c create mode 100644 drivers/platform/x86/p2sb.c create mode 100644 drivers/platform/x86/panasonic-laptop.c create mode 100644 drivers/platform/x86/pcengines-apuv2.c create mode 100644 drivers/platform/x86/pmc_atom.c create mode 100644 drivers/platform/x86/portwell-ec.c create mode 100644 drivers/platform/x86/quickstart.c create mode 100644 drivers/platform/x86/redmi-wmi.c create mode 100644 drivers/platform/x86/samsung-galaxybook.c create mode 100644 drivers/platform/x86/samsung-laptop.c create mode 100644 drivers/platform/x86/samsung-q10.c create mode 100644 drivers/platform/x86/sel3350-platform.c create mode 100644 drivers/platform/x86/serdev_helpers.h create mode 100644 drivers/platform/x86/serial-multi-instantiate.c create mode 100644 drivers/platform/x86/siemens/Kconfig create mode 100644 drivers/platform/x86/siemens/Makefile create mode 100644 drivers/platform/x86/siemens/simatic-ipc-batt-apollolake.c create mode 100644 drivers/platform/x86/siemens/simatic-ipc-batt-elkhartlake.c create mode 100644 drivers/platform/x86/siemens/simatic-ipc-batt-f7188x.c create mode 100644 drivers/platform/x86/siemens/simatic-ipc-batt.c create mode 100644 drivers/platform/x86/siemens/simatic-ipc-batt.h create mode 100644 drivers/platform/x86/siemens/simatic-ipc.c create mode 100644 drivers/platform/x86/silicom-platform.c create mode 100644 drivers/platform/x86/sony-laptop.c create mode 100644 drivers/platform/x86/system76_acpi.c create mode 100644 drivers/platform/x86/topstar-laptop.c create mode 100644 drivers/platform/x86/toshiba-wmi.c create mode 100644 drivers/platform/x86/toshiba_acpi.c create mode 100644 drivers/platform/x86/toshiba_bluetooth.c create mode 100644 drivers/platform/x86/toshiba_haps.c create mode 100644 drivers/platform/x86/touchscreen_dmi.c create mode 100644 drivers/platform/x86/tuxedo/Kconfig create mode 100644 drivers/platform/x86/tuxedo/Makefile create mode 100644 drivers/platform/x86/tuxedo/nb04/Kconfig create mode 100644 drivers/platform/x86/tuxedo/nb04/Makefile create mode 100644 drivers/platform/x86/tuxedo/nb04/wmi_ab.c create mode 100644 drivers/platform/x86/tuxedo/nb04/wmi_util.c create mode 100644 drivers/platform/x86/tuxedo/nb04/wmi_util.h create mode 100644 drivers/platform/x86/uniwill/Kconfig create mode 100644 drivers/platform/x86/uniwill/Makefile create mode 100644 drivers/platform/x86/uniwill/uniwill-acpi.c create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.c create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.h create mode 100644 drivers/platform/x86/uv_sysfs.c create mode 100644 drivers/platform/x86/winmate-fm07-keys.c create mode 100644 drivers/platform/x86/wireless-hotkey.c create mode 100644 drivers/platform/x86/wmi-bmof.c create mode 100644 drivers/platform/x86/x86-android-tablets/Kconfig create mode 100644 drivers/platform/x86/x86-android-tablets/Makefile create mode 100644 drivers/platform/x86/x86-android-tablets/acer.c create mode 100644 drivers/platform/x86/x86-android-tablets/asus.c create mode 100644 drivers/platform/x86/x86-android-tablets/core.c create mode 100644 drivers/platform/x86/x86-android-tablets/dmi.c create mode 100644 drivers/platform/x86/x86-android-tablets/lenovo.c create mode 100644 drivers/platform/x86/x86-android-tablets/other.c create mode 100644 drivers/platform/x86/x86-android-tablets/shared-psy-info.c create mode 100644 drivers/platform/x86/x86-android-tablets/shared-psy-info.h create mode 100644 drivers/platform/x86/x86-android-tablets/vexia_atla10_ec.c create mode 100644 drivers/platform/x86/x86-android-tablets/x86-android-tablets.h create mode 100644 drivers/platform/x86/xiaomi-wmi.c create mode 100644 drivers/platform/x86/xo1-rfkill.c create mode 100644 drivers/platform/x86/xo15-ebook.c (limited to 'drivers/platform') diff --git a/drivers/platform/Kconfig b/drivers/platform/Kconfig new file mode 100644 index 000000000..312788f24 --- /dev/null +++ b/drivers/platform/Kconfig @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: GPL-2.0-only +source "drivers/platform/mips/Kconfig" + +source "drivers/platform/loongarch/Kconfig" + +source "drivers/platform/goldfish/Kconfig" + +source "drivers/platform/chrome/Kconfig" + +source "drivers/platform/cznic/Kconfig" + +source "drivers/platform/mellanox/Kconfig" + +source "drivers/platform/olpc/Kconfig" + +source "drivers/platform/surface/Kconfig" + +source "drivers/platform/x86/Kconfig" + +source "drivers/platform/arm64/Kconfig" + +source "drivers/platform/raspberrypi/Kconfig" + +source "drivers/platform/wmi/Kconfig" diff --git a/drivers/platform/Makefile b/drivers/platform/Makefile new file mode 100644 index 000000000..fa322e7f8 --- /dev/null +++ b/drivers/platform/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform +# + +obj-$(CONFIG_X86) += x86/ +obj-$(CONFIG_LOONGARCH) += loongarch/ +obj-$(CONFIG_MELLANOX_PLATFORM) += mellanox/ +obj-$(CONFIG_MIPS) += mips/ +obj-$(CONFIG_OLPC_EC) += olpc/ +obj-$(CONFIG_GOLDFISH) += goldfish/ +obj-$(CONFIG_CHROME_PLATFORMS) += chrome/ +obj-$(CONFIG_CZNIC_PLATFORMS) += cznic/ +obj-$(CONFIG_SURFACE_PLATFORMS) += surface/ +obj-$(CONFIG_ARM64_PLATFORM_DEVICES) += arm64/ +obj-$(CONFIG_BCM2835_VCHIQ) += raspberrypi/ +obj-$(CONFIG_ACPI_WMI) += wmi/ diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig new file mode 100644 index 000000000..e32e01b2a --- /dev/null +++ b/drivers/platform/arm64/Kconfig @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EC-like Drivers for aarch64 based devices. +# + +menuconfig ARM64_PLATFORM_DEVICES + bool "ARM64 Platform-Specific Device Drivers" + depends on ARM64 || COMPILE_TEST + default ARM64 + help + Say Y here to get to see options for platform-specific device drivers + for arm64 based devices, primarily EC-like device drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if ARM64_PLATFORM_DEVICES + +config EC_ACER_ASPIRE1 + tristate "Acer Aspire 1 Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on DRM + depends on POWER_SUPPLY + depends on INPUT + help + Say Y here to enable the EC driver for the (Snapdragon-based) + Acer Aspire 1 laptop. The EC handles battery and charging + monitoring as well as some misc functions like the lid sensor + and USB Type-C DP HPD events. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + +config EC_HUAWEI_GAOKUN + tristate "Huawei Matebook E Go Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on INPUT + depends on HWMON + select AUXILIARY_BUS + + help + Say Y here to enable the EC driver for the Huawei Matebook E Go + which is a sc8280xp-based 2-in-1 tablet. The driver handles battery + (information, charge control) and USB Type-C DP HPD events as well + as some misc functions like the lid sensor and temperature sensors, + etc. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + + Say M or Y here to include this support. + +config EC_LENOVO_YOGA_C630 + tristate "Lenovo Yoga C630 Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + select AUXILIARY_BUS + help + Driver for the Embedded Controller in the Qualcomm Snapdragon-based + Lenovo Yoga C630, which provides battery and power adapter + information. + + This driver provides battery and AC status support for the mentioned + laptop where this information is not properly exposed via the + standard ACPI devices. + + Say M or Y here to include this support. + +config EC_LENOVO_THINKPAD_T14S + tristate "Lenovo Thinkpad T14s Embedded Controller driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on INPUT + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + select SND_CTL_LED if SND + help + Driver for the Embedded Controller in the Qualcomm Snapdragon-based + Lenovo Thinkpad T14s, which provides access to keyboard backlight + and status LEDs. + + This driver provides support for the mentioned laptop where this + information is not properly exposed via the standard Qualcomm + devices. + + Say M or Y here to include this support. + +config EC_QCOM_HAMOA + tristate "Embedded Controller driver for Qualcomm Hamoa/Glymur reference devices" + depends on ARCH_QCOM || COMPILE_TEST + depends on I2C + depends on THERMAL || THERMAL=n + help + Say M or Y here to enable the Embedded Controller driver for Qualcomm + Snapdragon-based Hamoa/Glymur reference devices. The driver handles fan + control, temperature sensors, access to EC state changes and supports + reporting suspend entry/exit to the EC. + + This driver currently supports Hamoa/Purwa/Glymur reference devices. + +endif # ARM64_PLATFORM_DEVICES diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile new file mode 100644 index 000000000..7681be4a4 --- /dev/null +++ b/drivers/platform/arm64/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for linux/drivers/platform/arm64 +# +# This dir should only include drivers for EC-like devices. +# + +obj-$(CONFIG_EC_ACER_ASPIRE1) += acer-aspire1-ec.o +obj-$(CONFIG_EC_HUAWEI_GAOKUN) += huawei-gaokun-ec.o +obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o +obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o +obj-$(CONFIG_EC_QCOM_HAMOA) += qcom-hamoa-ec.o diff --git a/drivers/platform/arm64/acer-aspire1-ec.c b/drivers/platform/arm64/acer-aspire1-ec.c new file mode 100644 index 000000000..08d0b155a --- /dev/null +++ b/drivers/platform/arm64/acer-aspire1-ec.c @@ -0,0 +1,562 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024, Nikita Travkin */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MILLI_TO_MICRO 1000 + +#define ASPIRE_EC_EVENT 0x05 + +#define ASPIRE_EC_EVENT_WATCHDOG 0x20 +#define ASPIRE_EC_EVENT_KBD_BKL_ON 0x57 +#define ASPIRE_EC_EVENT_KBD_BKL_OFF 0x58 +#define ASPIRE_EC_EVENT_LID_CLOSE 0x9b +#define ASPIRE_EC_EVENT_LID_OPEN 0x9c +#define ASPIRE_EC_EVENT_BKL_UNBLANKED 0x9d +#define ASPIRE_EC_EVENT_BKL_BLANKED 0x9e +#define ASPIRE_EC_EVENT_FG_INF_CHG 0x85 +#define ASPIRE_EC_EVENT_FG_STA_CHG 0xc6 +#define ASPIRE_EC_EVENT_HPD_DIS 0xa3 +#define ASPIRE_EC_EVENT_HPD_CON 0xa4 + +#define ASPIRE_EC_FG_DYNAMIC 0x07 +#define ASPIRE_EC_FG_STATIC 0x08 + +#define ASPIRE_EC_FG_FLAG_PRESENT BIT(0) +#define ASPIRE_EC_FG_FLAG_FULL BIT(1) +#define ASPIRE_EC_FG_FLAG_DISCHARGING BIT(2) +#define ASPIRE_EC_FG_FLAG_CHARGING BIT(3) + +#define ASPIRE_EC_RAM_READ 0x20 +#define ASPIRE_EC_RAM_WRITE 0x21 + +#define ASPIRE_EC_RAM_WATCHDOG 0x19 +#define ASPIRE_EC_WATCHDOG_BIT BIT(6) + +#define ASPIRE_EC_RAM_KBD_MODE 0x43 + +#define ASPIRE_EC_RAM_KBD_FN_EN BIT(0) +#define ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP BIT(5) +#define ASPIRE_EC_RAM_KBD_ALWAYS_SET BIT(6) +#define ASPIRE_EC_RAM_KBD_NUM_LAYER_EN BIT(7) + +#define ASPIRE_EC_RAM_KBD_MODE_2 0x60 + +#define ASPIRE_EC_RAM_KBD_MEDIA_NOTIFY BIT(3) + +#define ASPIRE_EC_RAM_HPD_STATUS 0xf4 +#define ASPIRE_EC_HPD_CONNECTED 0x03 + +#define ASPIRE_EC_RAM_LID_STATUS 0x4c +#define ASPIRE_EC_LID_OPEN BIT(6) + +#define ASPIRE_EC_RAM_ADP 0x40 +#define ASPIRE_EC_AC_STATUS BIT(0) + +struct aspire_ec { + struct i2c_client *client; + struct power_supply *bat_psy; + struct power_supply *adp_psy; + struct input_dev *idev; + + bool bridge_configured; + struct drm_bridge bridge; + struct work_struct work; +}; + +static int aspire_ec_ram_read(struct i2c_client *client, u8 off, u8 *data, u8 data_len) +{ + i2c_smbus_write_byte_data(client, ASPIRE_EC_RAM_READ, off); + i2c_smbus_read_i2c_block_data(client, ASPIRE_EC_RAM_READ, data_len, data); + return 0; +} + +static int aspire_ec_ram_write(struct i2c_client *client, u8 off, u8 data) +{ + u8 tmp[2] = {off, data}; + + i2c_smbus_write_i2c_block_data(client, ASPIRE_EC_RAM_WRITE, sizeof(tmp), tmp); + return 0; +} + +static irqreturn_t aspire_ec_irq_handler(int irq, void *data) +{ + struct aspire_ec *ec = data; + int id; + u8 tmp; + + /* + * The original ACPI firmware actually has a small sleep in the handler. + * + * It seems like in most cases it's not needed but when the device + * just exits suspend, our i2c driver has a brief time where data + * transfer is not possible yet. So this delay allows us to suppress + * quite a bunch of spurious error messages in dmesg. Thus it's kept. + */ + usleep_range(15000, 30000); + + id = i2c_smbus_read_byte_data(ec->client, ASPIRE_EC_EVENT); + if (id < 0) { + dev_err(&ec->client->dev, "Failed to read event id: %pe\n", ERR_PTR(id)); + return IRQ_HANDLED; + } + + switch (id) { + case 0x0: /* No event */ + break; + + case ASPIRE_EC_EVENT_WATCHDOG: + /* + * Here acpi responds to the event and clears some bit. + * Notify (\_SB.I2C3.BAT1, 0x81) // Information Change + * Notify (\_SB.I2C3.ADP1, 0x80) // Status Change + */ + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_WATCHDOG, &tmp, sizeof(tmp)); + tmp &= ~ASPIRE_EC_WATCHDOG_BIT; + aspire_ec_ram_write(ec->client, ASPIRE_EC_RAM_WATCHDOG, tmp); + break; + + case ASPIRE_EC_EVENT_LID_CLOSE: + /* Notify (\_SB.LID0, 0x80) // Status Change */ + input_report_switch(ec->idev, SW_LID, 1); + input_sync(ec->idev); + break; + + case ASPIRE_EC_EVENT_LID_OPEN: + /* Notify (\_SB.LID0, 0x80) // Status Change */ + input_report_switch(ec->idev, SW_LID, 0); + input_sync(ec->idev); + break; + + case ASPIRE_EC_EVENT_FG_INF_CHG: + /* Notify (\_SB.I2C3.BAT1, 0x81) // Information Change */ + fallthrough; + case ASPIRE_EC_EVENT_FG_STA_CHG: + /* Notify (\_SB.I2C3.BAT1, 0x80) // Status Change */ + power_supply_changed(ec->bat_psy); + power_supply_changed(ec->adp_psy); + break; + + case ASPIRE_EC_EVENT_HPD_DIS: + if (ec->bridge_configured) + drm_bridge_hpd_notify(&ec->bridge, connector_status_disconnected); + break; + + case ASPIRE_EC_EVENT_HPD_CON: + if (ec->bridge_configured) + drm_bridge_hpd_notify(&ec->bridge, connector_status_connected); + break; + + case ASPIRE_EC_EVENT_BKL_BLANKED: + case ASPIRE_EC_EVENT_BKL_UNBLANKED: + /* Display backlight blanked on FN+F6. No action needed. */ + break; + + case ASPIRE_EC_EVENT_KBD_BKL_ON: + case ASPIRE_EC_EVENT_KBD_BKL_OFF: + /* + * There is a keyboard backlight connector on Aspire 1 that is + * controlled by FN+F8. There is no kb backlight on the device though. + * Seems like this is used on other devices like Acer Spin 7. + * No action needed. + */ + break; + + default: + dev_warn(&ec->client->dev, "Unknown event id=0x%x\n", id); + } + + return IRQ_HANDLED; +} + +/* + * Power supply. + */ + +struct aspire_ec_bat_psy_static_data { + u8 unk1; + u8 flags; + __le16 unk2; + __le16 voltage_design; + __le16 capacity_full; + __le16 unk3; + __le16 serial; + u8 model_id; + u8 vendor_id; +} __packed; + +static const char * const aspire_ec_bat_psy_battery_model[] = { + "AP18C4K", + "AP18C8K", + "AP19B8K", + "AP16M4J", + "AP16M5J", +}; + +static const char * const aspire_ec_bat_psy_battery_vendor[] = { + "SANYO", + "SONY", + "PANASONIC", + "SAMSUNG", + "SIMPLO", + "MOTOROLA", + "CELXPERT", + "LGC", + "GETAC", + "MURATA", +}; + +struct aspire_ec_bat_psy_dynamic_data { + u8 unk1; + u8 flags; + u8 unk2; + __le16 capacity_now; + __le16 voltage_now; + __le16 current_now; + __le16 unk3; + __le16 unk4; +} __packed; + +static int aspire_ec_bat_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct aspire_ec *ec = power_supply_get_drvdata(psy); + struct aspire_ec_bat_psy_static_data sdat; + struct aspire_ec_bat_psy_dynamic_data ddat; + int str_index = 0; + + i2c_smbus_read_i2c_block_data(ec->client, ASPIRE_EC_FG_STATIC, sizeof(sdat), (u8 *)&sdat); + i2c_smbus_read_i2c_block_data(ec->client, ASPIRE_EC_FG_DYNAMIC, sizeof(ddat), (u8 *)&ddat); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + if (ddat.flags & ASPIRE_EC_FG_FLAG_CHARGING) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (ddat.flags & ASPIRE_EC_FG_FLAG_DISCHARGING) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (ddat.flags & ASPIRE_EC_FG_FLAG_FULL) + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = get_unaligned_le16(&ddat.voltage_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = le16_to_cpu(sdat.voltage_design) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = get_unaligned_le16(&ddat.capacity_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = le16_to_cpu(sdat.capacity_full) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = get_unaligned_le16(&ddat.capacity_now) * 100; + val->intval /= le16_to_cpu(sdat.capacity_full); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = (s16)get_unaligned_le16(&ddat.current_now) * MILLI_TO_MICRO; + break; + + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(ddat.flags & ASPIRE_EC_FG_FLAG_PRESENT); + break; + + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + str_index = sdat.model_id - 1; + + if (str_index >= 0 && str_index < ARRAY_SIZE(aspire_ec_bat_psy_battery_model)) + val->strval = aspire_ec_bat_psy_battery_model[str_index]; + else + val->strval = "Unknown"; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + str_index = sdat.vendor_id - 3; /* ACPI uses 3 as an offset here. */ + + if (str_index >= 0 && str_index < ARRAY_SIZE(aspire_ec_bat_psy_battery_vendor)) + val->strval = aspire_ec_bat_psy_battery_vendor[str_index]; + else + val->strval = "Unknown"; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property aspire_ec_bat_psy_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static const struct power_supply_desc aspire_ec_bat_psy_desc = { + .name = "aspire-ec-bat", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = aspire_ec_bat_psy_get_property, + .properties = aspire_ec_bat_psy_props, + .num_properties = ARRAY_SIZE(aspire_ec_bat_psy_props), +}; + +static int aspire_ec_adp_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct aspire_ec *ec = power_supply_get_drvdata(psy); + u8 tmp; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_ADP, &tmp, sizeof(tmp)); + val->intval = !!(tmp & ASPIRE_EC_AC_STATUS); + break; + + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property aspire_ec_adp_psy_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc aspire_ec_adp_psy_desc = { + .name = "aspire-ec-adp", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = aspire_ec_adp_psy_get_property, + .properties = aspire_ec_adp_psy_props, + .num_properties = ARRAY_SIZE(aspire_ec_adp_psy_props), +}; + +/* + * USB-C DP Alt mode HPD. + */ + +static int aspire_ec_bridge_attach(struct drm_bridge *bridge, struct drm_encoder *encoder, + enum drm_bridge_attach_flags flags) +{ + return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; +} + +static void aspire_ec_bridge_update_hpd_work(struct work_struct *work) +{ + struct aspire_ec *ec = container_of(work, struct aspire_ec, work); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_HPD_STATUS, &tmp, sizeof(tmp)); + if (tmp == ASPIRE_EC_HPD_CONNECTED) + drm_bridge_hpd_notify(&ec->bridge, connector_status_connected); + else + drm_bridge_hpd_notify(&ec->bridge, connector_status_disconnected); +} + +static void aspire_ec_bridge_hpd_enable(struct drm_bridge *bridge) +{ + struct aspire_ec *ec = container_of(bridge, struct aspire_ec, bridge); + + schedule_work(&ec->work); +} + +static const struct drm_bridge_funcs aspire_ec_bridge_funcs = { + .hpd_enable = aspire_ec_bridge_hpd_enable, + .attach = aspire_ec_bridge_attach, +}; + +/* + * Sysfs attributes. + */ + +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_KBD_MODE, &tmp, sizeof(tmp)); + + return sysfs_emit(buf, "%u\n", !(tmp & ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP)); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + bool state; + int ret; + + ret = kstrtobool(buf, &state); + if (ret) + return ret; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_KBD_MODE, &tmp, sizeof(tmp)); + + if (state) + tmp &= ~ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + else + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + + aspire_ec_ram_write(ec->client, ASPIRE_EC_RAM_KBD_MODE, tmp); + + return count; +} + +static DEVICE_ATTR_RW(fn_lock); + +static struct attribute *aspire_ec_attrs[] = { + &dev_attr_fn_lock.attr, + NULL +}; +ATTRIBUTE_GROUPS(aspire_ec); + +static int aspire_ec_probe(struct i2c_client *client) +{ + struct power_supply_config psy_cfg = {0}; + struct device *dev = &client->dev; + struct fwnode_handle *fwnode; + struct aspire_ec *ec; + int ret; + u8 tmp; + + ec = devm_drm_bridge_alloc(dev, struct aspire_ec, bridge, &aspire_ec_bridge_funcs); + if (IS_ERR(ec)) + return PTR_ERR(ec); + + ec->client = client; + i2c_set_clientdata(client, ec); + + /* Battery status reports */ + psy_cfg.drv_data = ec; + ec->bat_psy = devm_power_supply_register(dev, &aspire_ec_bat_psy_desc, &psy_cfg); + if (IS_ERR(ec->bat_psy)) + return dev_err_probe(dev, PTR_ERR(ec->bat_psy), + "Failed to register battery power supply\n"); + + ec->adp_psy = devm_power_supply_register(dev, &aspire_ec_adp_psy_desc, &psy_cfg); + if (IS_ERR(ec->adp_psy)) + return dev_err_probe(dev, PTR_ERR(ec->adp_psy), + "Failed to register AC power supply\n"); + + /* Lid switch */ + ec->idev = devm_input_allocate_device(dev); + if (!ec->idev) + return -ENOMEM; + + ec->idev->name = "aspire-ec"; + ec->idev->phys = "aspire-ec/input0"; + input_set_capability(ec->idev, EV_SW, SW_LID); + + ret = input_register_device(ec->idev); + if (ret) + return dev_err_probe(dev, ret, "Input device register failed\n"); + + /* Enable the keyboard fn keys */ + tmp = ASPIRE_EC_RAM_KBD_FN_EN | ASPIRE_EC_RAM_KBD_ALWAYS_SET; + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_ON_TOP; + aspire_ec_ram_write(client, ASPIRE_EC_RAM_KBD_MODE, tmp); + + aspire_ec_ram_read(client, ASPIRE_EC_RAM_KBD_MODE_2, &tmp, sizeof(tmp)); + tmp |= ASPIRE_EC_RAM_KBD_MEDIA_NOTIFY; + aspire_ec_ram_write(client, ASPIRE_EC_RAM_KBD_MODE_2, tmp); + + /* External Type-C display attach reports */ + fwnode = device_get_named_child_node(dev, "connector"); + if (fwnode) { + INIT_WORK(&ec->work, aspire_ec_bridge_update_hpd_work); + ec->bridge.of_node = to_of_node(fwnode); + ec->bridge.ops = DRM_BRIDGE_OP_HPD; + ec->bridge.type = DRM_MODE_CONNECTOR_USB; + + ret = devm_drm_bridge_add(dev, &ec->bridge); + if (ret) { + fwnode_handle_put(fwnode); + return dev_err_probe(dev, ret, "Failed to register drm bridge\n"); + } + + ec->bridge_configured = true; + } + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + aspire_ec_irq_handler, IRQF_ONESHOT, + dev_name(dev), ec); + if (ret) + return dev_err_probe(dev, ret, "Failed to request irq\n"); + + return 0; +} + +static int aspire_ec_resume(struct device *dev) +{ + struct aspire_ec *ec = i2c_get_clientdata(to_i2c_client(dev)); + u8 tmp; + + aspire_ec_ram_read(ec->client, ASPIRE_EC_RAM_LID_STATUS, &tmp, sizeof(tmp)); + input_report_switch(ec->idev, SW_LID, !!(tmp & ASPIRE_EC_LID_OPEN)); + input_sync(ec->idev); + + return 0; +} + +static const struct i2c_device_id aspire_ec_id[] = { + { .name = "aspire1-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, aspire_ec_id); + +static const struct of_device_id aspire_ec_of_match[] = { + { .compatible = "acer,aspire1-ec", }, + { } +}; +MODULE_DEVICE_TABLE(of, aspire_ec_of_match); + +static DEFINE_SIMPLE_DEV_PM_OPS(aspire_ec_pm_ops, NULL, aspire_ec_resume); + +static struct i2c_driver aspire_ec_driver = { + .driver = { + .name = "aspire-ec", + .of_match_table = aspire_ec_of_match, + .pm = pm_sleep_ptr(&aspire_ec_pm_ops), + .dev_groups = aspire_ec_groups, + }, + .probe = aspire_ec_probe, + .id_table = aspire_ec_id, +}; +module_i2c_driver(aspire_ec_driver); + +MODULE_DESCRIPTION("Acer Aspire 1 embedded controller"); +MODULE_AUTHOR("Nikita Travkin "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/huawei-gaokun-ec.c b/drivers/platform/arm64/huawei-gaokun-ec.c new file mode 100644 index 000000000..80a8ba8b8 --- /dev/null +++ b/drivers/platform/arm64/huawei-gaokun-ec.c @@ -0,0 +1,827 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * huawei-gaokun-ec - An EC driver for HUAWEI Matebook E Go + * + * Copyright (C) 2024-2025 Pengyu Luo + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EC_EVENT 0x06 + +/* Also can be found in ACPI specification 12.3 */ +#define EC_READ 0x80 +#define EC_WRITE 0x81 +#define EC_BURST 0x82 +#define EC_QUERY 0x84 + +#define EC_FN_LOCK_ON 0x5A +#define EC_FN_LOCK_OFF 0x55 +#define EC_FN_LOCK_READ 0x6B +#define EC_FN_LOCK_WRITE 0x6C + +#define EC_EVENT_LID 0x81 + +#define EC_LID_STATE 0x80 +#define EC_LID_OPEN BIT(1) + +#define EC_TEMP_REG 0x61 + +#define EC_STANDBY_REG 0xB2 +#define EC_STANDBY_ENTER 0xDB +#define EC_STANDBY_EXIT 0xEB + +enum gaokun_ec_smart_charge_cmd { + SMART_CHARGE_DATA_WRITE = 0xE3, + SMART_CHARGE_DATA_READ, + SMART_CHARGE_ENABLE_WRITE, + SMART_CHARGE_ENABLE_READ, +}; + +enum gaokun_ec_ucsi_cmd { + UCSI_REG_WRITE = 0xD2, + UCSI_REG_READ, + UCSI_DATA_WRITE, + UCSI_DATA_READ, +}; + +#define UCSI_REG_SIZE 7 + +/* + * For tx, command sequences are arranged as + * {master_cmd, slave_cmd, data_len, data_seq} + */ +#define REQ_HDR_SIZE 3 +#define INPUT_SIZE_OFFSET 2 +#define REQ_LEN(req) (REQ_HDR_SIZE + (req)[INPUT_SIZE_OFFSET]) + +/* + * For rx, data sequences are arranged as + * {status, data_len(unreliable), data_seq} + */ +#define RESP_HDR_SIZE 2 + +#define MKREQ(REG0, REG1, SIZE, ...) \ +{ \ + REG0, REG1, SIZE, \ + /* ## will remove comma when SIZE is 0 */ \ + ## __VA_ARGS__, \ + /* make sure len(pkt[3:]) >= SIZE */ \ + [3 + (SIZE)] = 0, \ +} + +#define MKRESP(SIZE) \ +{ \ + [RESP_HDR_SIZE + (SIZE) - 1] = 0, \ +} + +/* Possible size 1, 4, 20, 24. Most of the time, the size is 1. */ +static inline void refill_req(u8 *dest, const u8 *src, size_t size) +{ + memcpy(dest + REQ_HDR_SIZE, src, size); +} + +static inline void refill_req_byte(u8 *dest, const u8 *src) +{ + dest[REQ_HDR_SIZE] = *src; +} + +/* Possible size 1, 2, 4, 7, 20. Most of the time, the size is 1. */ +static inline void extr_resp(u8 *dest, const u8 *src, size_t size) +{ + memcpy(dest, src + RESP_HDR_SIZE, size); +} + +static inline void extr_resp_byte(u8 *dest, const u8 *src) +{ + *dest = src[RESP_HDR_SIZE]; +} + +static inline void *extr_resp_shallow(const u8 *src) +{ + return (void *)(src + RESP_HDR_SIZE); +} + +struct gaokun_ec { + struct i2c_client *client; + struct mutex lock; /* EC transaction lock */ + struct blocking_notifier_head notifier_list; + struct device *hwmon_dev; + struct input_dev *idev; + bool suspended; +}; + +static int gaokun_ec_request(struct gaokun_ec *ec, const u8 *req, + size_t resp_len, u8 *resp) +{ + struct i2c_client *client = ec->client; + struct i2c_msg msgs[] = { + { + .addr = client->addr, + .flags = client->flags, + .len = REQ_LEN(req), + .buf = (void *)req, + }, { + .addr = client->addr, + .flags = client->flags | I2C_M_RD, + .len = resp_len, + .buf = resp, + }, + }; + int ret; + + guard(mutex)(&ec->lock); + ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs)); + if (ret != ARRAY_SIZE(msgs)) { + dev_err(&client->dev, "I2C transfer error %d\n", ret); + goto out_after_break; + } + + ret = *resp; + if (ret) + dev_err(&client->dev, "EC transaction error %d\n", ret); + +out_after_break: + usleep_range(2000, 2500); /* have a break, ACPI did this */ + + return ret; +} + +/* -------------------------------------------------------------------------- */ +/* Common API */ + +/** + * gaokun_ec_read - Read from EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * @resp_len: The size to read + * @resp: The buffer to store response sequence + * + * This function is used to read data after writing a magic sequence to EC. + * All EC operations depend on this function. + * + * Huawei uses magic sequences everywhere to complete various functions, all + * these sequences are passed to ECCD(a ACPI method which is quiet similar + * to gaokun_ec_request), there is no good abstraction to generalize these + * sequences, so just wrap it for now. Almost all magic sequences are kept + * in this file. + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_read(struct gaokun_ec *ec, const u8 *req, + size_t resp_len, u8 *resp) +{ + return gaokun_ec_request(ec, req, resp_len, resp); +} +EXPORT_SYMBOL_GPL(gaokun_ec_read); + +/** + * gaokun_ec_write - Write to EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * + * This function has no big difference from gaokun_ec_read. When caller care + * only write status and no actual data are returned, then use it. + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_write(struct gaokun_ec *ec, const u8 *req) +{ + u8 ec_resp[] = MKRESP(0); + + return gaokun_ec_request(ec, req, sizeof(ec_resp), ec_resp); +} +EXPORT_SYMBOL_GPL(gaokun_ec_write); + +int gaokun_ec_read_byte(struct gaokun_ec *ec, const u8 *req, u8 *byte) +{ + int ret; + u8 ec_resp[] = MKRESP(sizeof(*byte)); + + ret = gaokun_ec_read(ec, req, sizeof(ec_resp), ec_resp); + extr_resp_byte(byte, ec_resp); + + return ret; +} +EXPORT_SYMBOL_GPL(gaokun_ec_read_byte); + +/** + * gaokun_ec_register_notify - Register a notifier callback for EC events. + * @ec: The gaokun_ec structure + * @nb: Notifier block pointer to register + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_register_notify(struct gaokun_ec *ec, struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(gaokun_ec_register_notify); + +/** + * gaokun_ec_unregister_notify - Unregister notifier callback for EC events. + * @ec: The gaokun_ec structure + * @nb: Notifier block pointer to unregister + * + * Unregister a notifier callback that was previously registered with + * gaokun_ec_register_notify(). + */ +void gaokun_ec_unregister_notify(struct gaokun_ec *ec, struct notifier_block *nb) +{ + blocking_notifier_chain_unregister(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(gaokun_ec_unregister_notify); + +/* -------------------------------------------------------------------------- */ +/* API for PSY */ + +/** + * gaokun_ec_psy_multi_read - Read contiguous registers + * @ec: The gaokun_ec structure + * @reg: The start register + * @resp_len: The number of registers to be read + * @resp: The buffer to store response sequence + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_multi_read(struct gaokun_ec *ec, u8 reg, + size_t resp_len, u8 *resp) +{ + u8 ec_req[] = MKREQ(0x02, EC_READ, 1, 0); + u8 ec_resp[] = MKRESP(1); + int i, ret; + + for (i = 0; i < resp_len; ++i, reg++) { + refill_req_byte(ec_req, ®); + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + extr_resp_byte(&resp[i], ec_resp); + } + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_multi_read); + +/* Smart charge */ + +/** + * gaokun_ec_psy_get_smart_charge - Get smart charge data from EC + * @ec: The gaokun_ec structure + * @resp: The buffer to store response sequence (mode, delay, start, end) + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_get_smart_charge(struct gaokun_ec *ec, + u8 resp[GAOKUN_SMART_CHARGE_DATA_SIZE]) +{ + /* GBCM */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_DATA_READ, 0); + u8 ec_resp[] = MKRESP(GAOKUN_SMART_CHARGE_DATA_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp(resp, ec_resp, GAOKUN_SMART_CHARGE_DATA_SIZE); + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_get_smart_charge); + +static inline bool validate_battery_threshold_range(u8 start, u8 end) +{ + return end != 0 && start <= end && end <= 100; +} + +/** + * gaokun_ec_psy_set_smart_charge - Set smart charge data + * @ec: The gaokun_ec structure + * @req: The sequence to request (mode, delay, start, end) + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_set_smart_charge(struct gaokun_ec *ec, + const u8 req[GAOKUN_SMART_CHARGE_DATA_SIZE]) +{ + /* SBCM */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_DATA_WRITE, + GAOKUN_SMART_CHARGE_DATA_SIZE); + + if (!validate_battery_threshold_range(req[2], req[3])) + return -EINVAL; + + refill_req(ec_req, req, GAOKUN_SMART_CHARGE_DATA_SIZE); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_set_smart_charge); + +/* Smart charge enable */ + +/** + * gaokun_ec_psy_get_smart_charge_enable - Get smart charge state + * @ec: The gaokun_ec structure + * @on: The state + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_get_smart_charge_enable(struct gaokun_ec *ec, bool *on) +{ + /* GBAC */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_ENABLE_READ, 0); + u8 state; + int ret; + + ret = gaokun_ec_read_byte(ec, ec_req, &state); + if (ret) + return ret; + + *on = !!state; + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_get_smart_charge_enable); + +/** + * gaokun_ec_psy_set_smart_charge_enable - Set smart charge state + * @ec: The gaokun_ec structure + * @on: The state + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_psy_set_smart_charge_enable(struct gaokun_ec *ec, bool on) +{ + /* SBAC */ + u8 ec_req[] = MKREQ(0x02, SMART_CHARGE_ENABLE_WRITE, 1, on); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_psy_set_smart_charge_enable); + +/* -------------------------------------------------------------------------- */ +/* API for UCSI */ + +/** + * gaokun_ec_ucsi_read - Read UCSI data from EC + * @ec: The gaokun_ec structure + * @resp: The buffer to store response sequence + * + * Read CCI and MSGI (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_read(struct gaokun_ec *ec, + u8 resp[GAOKUN_UCSI_READ_SIZE]) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_DATA_READ, 0); + u8 ec_resp[] = MKRESP(GAOKUN_UCSI_READ_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp(resp, ec_resp, GAOKUN_UCSI_READ_SIZE); + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_read); + +/** + * gaokun_ec_ucsi_write - Write UCSI data to EC + * @ec: The gaokun_ec structure + * @req: The sequence to request + * + * Write CTRL and MSGO (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_write(struct gaokun_ec *ec, + const u8 req[GAOKUN_UCSI_WRITE_SIZE]) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_DATA_WRITE, GAOKUN_UCSI_WRITE_SIZE); + + refill_req(ec_req, req, GAOKUN_UCSI_WRITE_SIZE); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_write); + +/** + * gaokun_ec_ucsi_get_reg - Get UCSI register from EC + * @ec: The gaokun_ec structure + * @ureg: The gaokun ucsi register + * + * Get UCSI register data (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_get_reg(struct gaokun_ec *ec, struct gaokun_ucsi_reg *ureg) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_REG_READ, 0); + u8 ec_resp[] = MKRESP(UCSI_REG_SIZE); + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + extr_resp((u8 *)ureg, ec_resp, UCSI_REG_SIZE); + + return 0; +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_get_reg); + +/** + * gaokun_ec_ucsi_pan_ack - Ack pin assignment notifications from EC + * @ec: The gaokun_ec structure + * @port_id: The port id receiving and handling the notifications + * + * Ack pin assignment notifications (used by UCSI subdriver). + * + * Return: 0 on success or negative error code. + */ +int gaokun_ec_ucsi_pan_ack(struct gaokun_ec *ec, int port_id) +{ + u8 ec_req[] = MKREQ(0x03, UCSI_REG_WRITE, 1); + u8 data = 1 << port_id; + + if (port_id == GAOKUN_UCSI_NO_PORT_UPDATE) + data = 0; + + refill_req_byte(ec_req, &data); + + return gaokun_ec_write(ec, ec_req); +} +EXPORT_SYMBOL_GPL(gaokun_ec_ucsi_pan_ack); + +/* -------------------------------------------------------------------------- */ +/* EC Sysfs */ + +/* Fn lock */ +static int gaokun_ec_get_fn_lock(struct gaokun_ec *ec, bool *on) +{ + /* GFRS */ + u8 ec_req[] = MKREQ(0x02, EC_FN_LOCK_READ, 0); + int ret; + u8 state; + + ret = gaokun_ec_read_byte(ec, ec_req, &state); + if (ret) + return ret; + + if (state == EC_FN_LOCK_ON) + *on = true; + else if (state == EC_FN_LOCK_OFF) + *on = false; + else + return -EIO; + + return 0; +} + +static int gaokun_ec_set_fn_lock(struct gaokun_ec *ec, bool on) +{ + /* SFRS */ + u8 ec_req[] = MKREQ(0x02, EC_FN_LOCK_WRITE, 1, + on ? EC_FN_LOCK_ON : EC_FN_LOCK_OFF); + + return gaokun_ec_write(ec, ec_req); +} + +static ssize_t fn_lock_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + bool on; + int ret; + + ret = gaokun_ec_get_fn_lock(ec, &on); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", on); +} + +static ssize_t fn_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + bool on; + int ret; + + if (kstrtobool(buf, &on)) + return -EINVAL; + + ret = gaokun_ec_set_fn_lock(ec, on); + if (ret) + return ret; + + return size; +} + +static DEVICE_ATTR_RW(fn_lock); + +static struct attribute *gaokun_ec_attrs[] = { + &dev_attr_fn_lock.attr, + NULL, +}; +ATTRIBUTE_GROUPS(gaokun_ec); + +/* -------------------------------------------------------------------------- */ +/* Thermal Zone HwMon */ + +/* Range from 0 to 0x2C, partially valid */ +static const u8 temp_reg[] = { + 0x05, 0x07, 0x08, 0x0E, 0x0F, 0x12, 0x15, 0x1E, + 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, + 0x27, 0x28, 0x29, 0x2A +}; + +static int gaokun_ec_get_temp(struct gaokun_ec *ec, u8 idx, long *temp) +{ + /* GTMP */ + u8 ec_req[] = MKREQ(0x02, EC_TEMP_REG, 1, temp_reg[idx]); + u8 ec_resp[] = MKRESP(sizeof(__le16)); + __le16 *tmp; + int ret; + + ret = gaokun_ec_read(ec, ec_req, sizeof(ec_resp), ec_resp); + if (ret) + return ret; + + tmp = (__le16 *)extr_resp_shallow(ec_resp); + *temp = le16_to_cpu(*tmp) * 100; /* convert to HwMon's unit */ + + return 0; +} + +static umode_t +gaokun_ec_hwmon_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return type == hwmon_temp ? 0444 : 0; +} + +static int +gaokun_ec_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + + if (type == hwmon_temp) + return gaokun_ec_get_temp(ec, channel, val); + + return -EINVAL; +} + +static const struct hwmon_ops gaokun_ec_hwmon_ops = { + .is_visible = gaokun_ec_hwmon_is_visible, + .read = gaokun_ec_hwmon_read, +}; + +static u32 gaokun_ec_temp_config[] = { + [0 ... ARRAY_SIZE(temp_reg) - 1] = HWMON_T_INPUT, + 0 +}; + +static const struct hwmon_channel_info gaokun_ec_temp = { + .type = hwmon_temp, + .config = gaokun_ec_temp_config, +}; + +static const struct hwmon_channel_info * const gaokun_ec_hwmon_info[] = { + &gaokun_ec_temp, + NULL +}; + +static const struct hwmon_chip_info gaokun_ec_hwmon_chip_info = { + .ops = &gaokun_ec_hwmon_ops, + .info = gaokun_ec_hwmon_info, +}; + +/* -------------------------------------------------------------------------- */ +/* Modern Standby */ + +static int gaokun_ec_suspend(struct device *dev) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + u8 ec_req[] = MKREQ(0x02, EC_STANDBY_REG, 1, EC_STANDBY_ENTER); + int ret; + + if (ec->suspended) + return 0; + + ret = gaokun_ec_write(ec, ec_req); + if (ret) + return ret; + + ec->suspended = true; + + return 0; +} + +static int gaokun_ec_resume(struct device *dev) +{ + struct gaokun_ec *ec = dev_get_drvdata(dev); + u8 ec_req[] = MKREQ(0x02, EC_STANDBY_REG, 1, EC_STANDBY_EXIT); + int ret; + int i; + + if (!ec->suspended) + return 0; + + for (i = 0; i < 3; ++i) { + ret = gaokun_ec_write(ec, ec_req); + if (ret == 0) + break; + + msleep(100); /* EC need time to resume */ + } + + ec->suspended = false; + + return 0; +} + +static void gaokun_aux_release(struct device *dev) +{ + struct auxiliary_device *adev = to_auxiliary_dev(dev); + + of_node_put(dev->of_node); + kfree(adev); +} + +static void gaokun_aux_remove(void *data) +{ + struct auxiliary_device *adev = data; + + auxiliary_device_delete(adev); + auxiliary_device_uninit(adev); +} + +static int gaokun_aux_init(struct device *parent, const char *name, + struct gaokun_ec *ec) +{ + struct auxiliary_device *adev; + int ret; + + adev = kzalloc_obj(*adev); + if (!adev) + return -ENOMEM; + + adev->name = name; + adev->id = 0; + adev->dev.parent = parent; + adev->dev.release = gaokun_aux_release; + adev->dev.platform_data = ec; + /* Allow aux devices to access parent's DT nodes directly */ + device_set_of_node_from_dev(&adev->dev, parent); + + ret = auxiliary_device_init(adev); + if (ret) { + of_node_put(adev->dev.of_node); + kfree(adev); + return ret; + } + + ret = auxiliary_device_add(adev); + if (ret) { + auxiliary_device_uninit(adev); + return ret; + } + + return devm_add_action_or_reset(parent, gaokun_aux_remove, adev); +} + +/* -------------------------------------------------------------------------- */ +/* EC */ + +static irqreturn_t gaokun_ec_irq_handler(int irq, void *data) +{ + struct gaokun_ec *ec = data; + u8 ec_req[] = MKREQ(EC_EVENT, EC_QUERY, 0); + u8 status, id; + int ret; + + ret = gaokun_ec_read_byte(ec, ec_req, &id); + if (ret) + return IRQ_HANDLED; + + switch (id) { + case 0x0: /* No event */ + break; + + case EC_EVENT_LID: + gaokun_ec_psy_read_byte(ec, EC_LID_STATE, &status); + status &= EC_LID_OPEN; + input_report_switch(ec->idev, SW_LID, !status); + input_sync(ec->idev); + break; + + default: + blocking_notifier_call_chain(&ec->notifier_list, id, ec); + } + + return IRQ_HANDLED; +} + +static int gaokun_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct gaokun_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ret = devm_mutex_init(dev, &ec->lock); + if (ret) + return ret; + + ec->client = client; + i2c_set_clientdata(client, ec); + BLOCKING_INIT_NOTIFIER_HEAD(&ec->notifier_list); + + /* Lid switch */ + ec->idev = devm_input_allocate_device(dev); + if (!ec->idev) + return -ENOMEM; + + ec->idev->name = "LID"; + ec->idev->phys = "gaokun-ec/input0"; + input_set_capability(ec->idev, EV_SW, SW_LID); + + ret = input_register_device(ec->idev); + if (ret) + return dev_err_probe(dev, ret, "Failed to register input device\n"); + + ret = gaokun_aux_init(dev, GAOKUN_DEV_PSY, ec); + if (ret) + return ret; + + ret = gaokun_aux_init(dev, GAOKUN_DEV_UCSI, ec); + if (ret) + return ret; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + gaokun_ec_irq_handler, IRQF_ONESHOT, + dev_name(dev), ec); + if (ret) + return dev_err_probe(dev, ret, "Failed to request IRQ\n"); + + ec->hwmon_dev = devm_hwmon_device_register_with_info(dev, "gaokun_ec_hwmon", + ec, &gaokun_ec_hwmon_chip_info, NULL); + if (IS_ERR(ec->hwmon_dev)) + return dev_err_probe(dev, PTR_ERR(ec->hwmon_dev), + "Failed to register hwmon device\n"); + + return 0; +} + +static const struct i2c_device_id gaokun_ec_id[] = { + { .name = "gaokun-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, gaokun_ec_id); + +static const struct of_device_id gaokun_ec_of_match[] = { + { .compatible = "huawei,gaokun3-ec", }, + { } +}; +MODULE_DEVICE_TABLE(of, gaokun_ec_of_match); + +static const struct dev_pm_ops gaokun_ec_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(gaokun_ec_suspend, gaokun_ec_resume) +}; + +static struct i2c_driver gaokun_ec_driver = { + .driver = { + .name = "gaokun-ec", + .of_match_table = gaokun_ec_of_match, + .pm = &gaokun_ec_pm_ops, + .dev_groups = gaokun_ec_groups, + }, + .probe = gaokun_ec_probe, + .id_table = gaokun_ec_id, +}; +module_i2c_driver(gaokun_ec_driver); + +MODULE_DESCRIPTION("HUAWEI Matebook E Go EC driver"); +MODULE_AUTHOR("Pengyu Luo "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c new file mode 100644 index 000000000..e7acb66b7 --- /dev/null +++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2025, Sebastian Reichel + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define T14S_EC_CMD_ECRD 0x02 +#define T14S_EC_CMD_ECWR 0x03 +#define T14S_EC_CMD_EVT 0xf0 + +#define T14S_EC_REG_LED 0x0c +#define T14S_EC_REG_KBD_BL1 0x0d +#define T14S_EC_REG_MODERN_STANDBY 0xe0 +#define T14S_EC_MODERN_STANDBY_ENTRY BIT(1) +#define T14S_EC_MODERN_STANDBY_EXIT BIT(0) +#define T14S_EC_REG_KBD_BL2 0xe1 +#define T14S_EC_KBD_BL1_MASK GENMASK_U8(7, 6) +#define T14S_EC_KBD_BL2_MASK GENMASK_U8(3, 2) +#define T14S_EC_REG_AUD 0x30 +#define T14S_EC_MIC_MUTE_LED BIT(5) +#define T14S_EC_SPK_MUTE_LED BIT(6) + +#define T14S_EC_EVT_NONE 0x00 +#define T14S_EC_EVT_KEY_FN_4 0x13 +#define T14S_EC_EVT_KEY_FN_F7 0x16 +#define T14S_EC_EVT_KEY_FN_SPACE 0x1f +#define T14S_EC_EVT_KEY_TP_DOUBLE_TAP 0x20 +#define T14S_EC_EVT_AC_CONNECTED 0x26 +#define T14S_EC_EVT_AC_DISCONNECTED 0x27 +#define T14S_EC_EVT_KEY_POWER 0x28 +#define T14S_EC_EVT_LID_OPEN 0x2a +#define T14S_EC_EVT_LID_CLOSED 0x2b +#define T14S_EC_EVT_THERMAL_TZ40 0x5c +#define T14S_EC_EVT_THERMAL_TZ42 0x5d +#define T14S_EC_EVT_THERMAL_TZ39 0x5e +#define T14S_EC_EVT_KEY_FN_F12 0x62 +#define T14S_EC_EVT_KEY_FN_TAB 0x63 +#define T14S_EC_EVT_KEY_FN_F8 0x64 +#define T14S_EC_EVT_KEY_FN_F10 0x65 +#define T14S_EC_EVT_KEY_FN_F4 0x6a +#define T14S_EC_EVT_KEY_FN_D 0x6b +#define T14S_EC_EVT_KEY_FN_T 0x6c +#define T14S_EC_EVT_KEY_FN_H 0x6d +#define T14S_EC_EVT_KEY_FN_M 0x6e +#define T14S_EC_EVT_KEY_FN_L 0x6f +#define T14S_EC_EVT_KEY_FN_RIGHT_SHIFT 0x71 +#define T14S_EC_EVT_KEY_FN_ESC 0x74 +#define T14S_EC_EVT_KEY_FN_N 0x79 +#define T14S_EC_EVT_KEY_FN_F11 0x7a +#define T14S_EC_EVT_KEY_FN_G 0x7e + +/* Hardware LED blink rate is 1 Hz (500ms off, 500ms on) */ +#define T14S_EC_BLINK_RATE_ON_OFF_MS 500 + +/* + * Add a virtual offset on all key event codes for sparse keymap handling, + * since the sparse keymap infrastructure does not map some raw key event + * codes used by the EC. For example 0x16 (T14S_EC_EVT_KEY_FN_F7) is mapped + * to KEY_MUTE if no offset is applied. + */ +#define T14S_EC_KEY_EVT_OFFSET 0x1000 +#define T14S_EC_KEY_ENTRY(key, value) \ + { KE_KEY, T14S_EC_KEY_EVT_OFFSET + T14S_EC_EVT_KEY_##key, { value } } + +enum t14s_ec_led_status_t { + T14S_EC_LED_OFF = 0x00, + T14S_EC_LED_ON = 0x80, + T14S_EC_LED_BLINK = 0xc0, +}; + +struct t14s_ec_led_classdev { + struct led_classdev led_classdev; + int led; + enum t14s_ec_led_status_t cache; + struct t14s_ec *ec; +}; + +struct t14s_ec { + struct regmap *regmap; + struct device *dev; + struct t14s_ec_led_classdev led_pwr_btn; + struct t14s_ec_led_classdev led_chrg_orange; + struct t14s_ec_led_classdev led_chrg_white; + struct t14s_ec_led_classdev led_lid_logo_dot; + struct led_classdev kbd_backlight; + struct led_classdev led_mic_mute; + struct led_classdev led_spk_mute; + struct input_dev *inputdev; +}; + +static const struct regmap_config t14s_ec_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xff, +}; + +static int t14s_ec_write(void *context, unsigned int reg, + unsigned int val) +{ + struct t14s_ec *ec = context; + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[5] = {T14S_EC_CMD_ECWR, reg, 0x00, 0x01, val}; + int ret; + + ret = i2c_master_send(client, buf, sizeof(buf)); + if (ret < 0) + return ret; + + fsleep(10000); + return 0; +} + +static int t14s_ec_read(void *context, unsigned int reg, + unsigned int *val) +{ + struct t14s_ec *ec = context; + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[4] = {T14S_EC_CMD_ECRD, reg, 0x00, 0x01}; + struct i2c_msg request, response; + u8 result; + int ret; + + request.addr = client->addr; + request.flags = I2C_M_STOP; + request.len = sizeof(buf); + request.buf = buf; + response.addr = client->addr; + response.flags = I2C_M_RD; + response.len = 1; + response.buf = &result; + + i2c_lock_bus(client->adapter, I2C_LOCK_SEGMENT); + + ret = __i2c_transfer(client->adapter, &request, 1); + if (ret < 0) + goto out; + + ret = __i2c_transfer(client->adapter, &response, 1); + if (ret < 0) + goto out; + + *val = result; + ret = 0; + +out: + i2c_unlock_bus(client->adapter, I2C_LOCK_SEGMENT); + fsleep(10000); + return ret; +} + +static const struct regmap_bus t14s_ec_regmap_bus = { + .reg_write = t14s_ec_write, + .reg_read = t14s_ec_read, +}; + +static int t14s_ec_read_evt(struct t14s_ec *ec, u8 *val) +{ + struct i2c_client *client = to_i2c_client(ec->dev); + u8 buf[4] = {T14S_EC_CMD_EVT, 0x00, 0x00, 0x01}; + struct i2c_msg request, response; + int ret; + + request.addr = client->addr; + request.flags = I2C_M_STOP; + request.len = sizeof(buf); + request.buf = buf; + response.addr = client->addr; + response.flags = I2C_M_RD; + response.len = 1; + response.buf = val; + + i2c_lock_bus(client->adapter, I2C_LOCK_SEGMENT); + + ret = __i2c_transfer(client->adapter, &request, 1); + if (ret < 0) + goto out; + + ret = __i2c_transfer(client->adapter, &response, 1); + if (ret < 0) + goto out; + + fsleep(10000); + + ret = 0; + +out: + i2c_unlock_bus(client->adapter, I2C_LOCK_SEGMENT); + return ret; +} + +static void t14s_ec_write_sequence(struct t14s_ec *ec, u8 reg, u8 val, u8 cnt) +{ + int i; + + for (i = 0; i < cnt; i++) + regmap_write(ec->regmap, reg, val); +} + +static int t14s_led_set_status(struct t14s_ec *ec, + struct t14s_ec_led_classdev *led, + const enum t14s_ec_led_status_t ledstatus) +{ + int ret; + + ret = regmap_write(ec->regmap, T14S_EC_REG_LED, + led->led | ledstatus); + if (ret < 0) + return ret; + + led->cache = ledstatus; + return 0; +} + +static int t14s_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec_led_classdev *led = container_of(led_cdev, + struct t14s_ec_led_classdev, led_classdev); + enum t14s_ec_led_status_t new_state; + + if (brightness == LED_OFF) + new_state = T14S_EC_LED_OFF; + else if (led->cache == T14S_EC_LED_BLINK) + new_state = T14S_EC_LED_BLINK; + else + new_state = T14S_EC_LED_ON; + + return t14s_led_set_status(led->ec, led, new_state); +} + +static int t14s_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct t14s_ec_led_classdev *led = container_of(led_cdev, + struct t14s_ec_led_classdev, led_classdev); + + if (*delay_on == 0 && *delay_off == 0) { + /* Userspace does not provide a blink rate; we can choose it */ + *delay_on = T14S_EC_BLINK_RATE_ON_OFF_MS; + *delay_off = T14S_EC_BLINK_RATE_ON_OFF_MS; + } else if ((*delay_on != T14S_EC_BLINK_RATE_ON_OFF_MS) || + (*delay_off != T14S_EC_BLINK_RATE_ON_OFF_MS)) + return -EINVAL; + + return t14s_led_set_status(led->ec, led, T14S_EC_LED_BLINK); +} + +static int t14s_init_led(struct t14s_ec *ec, struct t14s_ec_led_classdev *led, + u8 id, const char *name) +{ + led->led_classdev.name = name; + led->led_classdev.flags = LED_RETAIN_AT_SHUTDOWN; + led->led_classdev.max_brightness = 1; + led->led_classdev.brightness_set_blocking = t14s_led_brightness_set; + led->led_classdev.blink_set = t14s_led_blink_set; + led->ec = ec; + led->led = id; + + return devm_led_classdev_register(ec->dev, &led->led_classdev); +} + +static int t14s_leds_probe(struct t14s_ec *ec) +{ + int ret; + + ret = t14s_init_led(ec, &ec->led_pwr_btn, 0, "platform::power"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_chrg_orange, 1, + "platform:amber:battery-charging"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_chrg_white, 2, + "platform:white:battery-full"); + if (ret) + return ret; + + ret = t14s_init_led(ec, &ec->led_lid_logo_dot, 10, + "platform::lid_logo_dot"); + if (ret) + return ret; + + return 0; +} + +static int t14s_kbd_bl_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + kbd_backlight); + int ret; + u8 val; + + val = FIELD_PREP(T14S_EC_KBD_BL1_MASK, brightness); + ret = regmap_update_bits(ec->regmap, T14S_EC_REG_KBD_BL1, + T14S_EC_KBD_BL1_MASK, val); + if (ret < 0) + return ret; + + val = FIELD_PREP(T14S_EC_KBD_BL2_MASK, brightness); + ret = regmap_update_bits(ec->regmap, T14S_EC_REG_KBD_BL2, + T14S_EC_KBD_BL2_MASK, val); + if (ret < 0) + return ret; + + return 0; +} + +static enum led_brightness t14s_kbd_bl_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + kbd_backlight); + unsigned int val; + int ret; + + ret = regmap_read(ec->regmap, T14S_EC_REG_KBD_BL1, &val); + if (ret < 0) + return ret; + + return FIELD_GET(T14S_EC_KBD_BL1_MASK, val); +} + +static void t14s_kbd_bl_update(struct t14s_ec *ec) +{ + enum led_brightness brightness = t14s_kbd_bl_get(&ec->kbd_backlight); + + led_classdev_notify_brightness_hw_changed(&ec->kbd_backlight, brightness); +} + +static int t14s_kbd_backlight_probe(struct t14s_ec *ec) +{ + ec->kbd_backlight.name = "platform::kbd_backlight"; + ec->kbd_backlight.flags = LED_BRIGHT_HW_CHANGED; + ec->kbd_backlight.max_brightness = 2; + ec->kbd_backlight.brightness_set_blocking = t14s_kbd_bl_set; + ec->kbd_backlight.brightness_get = t14s_kbd_bl_get; + + return devm_led_classdev_register(ec->dev, &ec->kbd_backlight); +} + +static enum led_brightness t14s_audio_led_get(struct t14s_ec *ec, u8 led_bit) +{ + unsigned int val; + int ret; + + ret = regmap_read(ec->regmap, T14S_EC_REG_AUD, &val); + if (ret < 0) + return ret; + + return !!(val & led_bit) ? LED_ON : LED_OFF; +} + +static enum led_brightness t14s_audio_led_set(struct t14s_ec *ec, + u8 led_mask, + enum led_brightness brightness) +{ + return regmap_assign_bits(ec->regmap, T14S_EC_REG_AUD, led_mask, brightness > 0); +} + +static enum led_brightness t14s_mic_mute_led_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_mic_mute); + + return t14s_audio_led_get(ec, T14S_EC_MIC_MUTE_LED); +} + +static int t14s_mic_mute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_mic_mute); + + return t14s_audio_led_set(ec, T14S_EC_MIC_MUTE_LED, brightness); +} + +static enum led_brightness t14s_spk_mute_led_get(struct led_classdev *led_cdev) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_spk_mute); + + return t14s_audio_led_get(ec, T14S_EC_SPK_MUTE_LED); +} + +static int t14s_spk_mute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct t14s_ec *ec = container_of(led_cdev, struct t14s_ec, + led_spk_mute); + + return t14s_audio_led_set(ec, T14S_EC_SPK_MUTE_LED, brightness); +} + +static int t14s_kbd_audio_led_probe(struct t14s_ec *ec) +{ + int ret; + + ec->led_mic_mute.name = "platform::micmute"; + ec->led_mic_mute.max_brightness = 1; + ec->led_mic_mute.default_trigger = "audio-micmute"; + ec->led_mic_mute.brightness_set_blocking = t14s_mic_mute_led_set; + ec->led_mic_mute.brightness_get = t14s_mic_mute_led_get; + + ec->led_spk_mute.name = "platform::mute"; + ec->led_spk_mute.max_brightness = 1; + ec->led_spk_mute.default_trigger = "audio-mute"; + ec->led_spk_mute.brightness_set_blocking = t14s_spk_mute_led_set; + ec->led_spk_mute.brightness_get = t14s_spk_mute_led_get; + + ret = devm_led_classdev_register(ec->dev, &ec->led_mic_mute); + if (ret) + return ret; + + return devm_led_classdev_register(ec->dev, &ec->led_spk_mute); +} + +static const struct key_entry t14s_keymap[] = { + T14S_EC_KEY_ENTRY(FN_4, KEY_SLEEP), + T14S_EC_KEY_ENTRY(FN_N, KEY_VENDOR), + T14S_EC_KEY_ENTRY(FN_F4, KEY_MICMUTE), + T14S_EC_KEY_ENTRY(FN_F7, KEY_SWITCHVIDEOMODE), + T14S_EC_KEY_ENTRY(FN_F8, KEY_PERFORMANCE), + T14S_EC_KEY_ENTRY(FN_F10, KEY_SELECTIVE_SCREENSHOT), + T14S_EC_KEY_ENTRY(FN_F11, KEY_LINK_PHONE), + T14S_EC_KEY_ENTRY(FN_F12, KEY_BOOKMARKS), + T14S_EC_KEY_ENTRY(FN_SPACE, KEY_KBDILLUMTOGGLE), + T14S_EC_KEY_ENTRY(FN_ESC, KEY_FN_ESC), + T14S_EC_KEY_ENTRY(FN_TAB, KEY_ZOOM), + T14S_EC_KEY_ENTRY(FN_RIGHT_SHIFT, KEY_FN_RIGHT_SHIFT), + T14S_EC_KEY_ENTRY(TP_DOUBLE_TAP, KEY_PROG4), + { KE_END } +}; + +static int t14s_input_probe(struct t14s_ec *ec) +{ + int ret; + + ec->inputdev = devm_input_allocate_device(ec->dev); + if (!ec->inputdev) + return -ENOMEM; + + ec->inputdev->name = "ThinkPad Extra Buttons"; + ec->inputdev->phys = "thinkpad/input0"; + ec->inputdev->id.bustype = BUS_HOST; + ec->inputdev->dev.parent = ec->dev; + + ret = sparse_keymap_setup(ec->inputdev, t14s_keymap, NULL); + if (ret) + return ret; + + return input_register_device(ec->inputdev); +} + +static irqreturn_t t14s_ec_irq_handler(int irq, void *data) +{ + struct t14s_ec *ec = data; + int ret; + u8 val; + + ret = t14s_ec_read_evt(ec, &val); + if (ret < 0) { + dev_err(ec->dev, "Failed to read event\n"); + return IRQ_HANDLED; + } + + switch (val) { + case T14S_EC_EVT_NONE: + break; + case T14S_EC_EVT_KEY_FN_SPACE: + t14s_kbd_bl_update(ec); + fallthrough; + case T14S_EC_EVT_KEY_FN_F4: + case T14S_EC_EVT_KEY_FN_F7: + case T14S_EC_EVT_KEY_FN_4: + case T14S_EC_EVT_KEY_FN_F8: + case T14S_EC_EVT_KEY_FN_F12: + case T14S_EC_EVT_KEY_FN_TAB: + case T14S_EC_EVT_KEY_FN_F10: + case T14S_EC_EVT_KEY_FN_N: + case T14S_EC_EVT_KEY_FN_F11: + case T14S_EC_EVT_KEY_FN_ESC: + case T14S_EC_EVT_KEY_FN_RIGHT_SHIFT: + case T14S_EC_EVT_KEY_TP_DOUBLE_TAP: + sparse_keymap_report_event(ec->inputdev, + T14S_EC_KEY_EVT_OFFSET + val, 1, true); + break; + case T14S_EC_EVT_AC_CONNECTED: + dev_dbg(ec->dev, "AC connected\n"); + break; + case T14S_EC_EVT_AC_DISCONNECTED: + dev_dbg(ec->dev, "AC disconnected\n"); + break; + case T14S_EC_EVT_KEY_POWER: + dev_dbg(ec->dev, "power button\n"); + break; + case T14S_EC_EVT_LID_OPEN: + dev_dbg(ec->dev, "LID open\n"); + break; + case T14S_EC_EVT_LID_CLOSED: + dev_dbg(ec->dev, "LID closed\n"); + break; + case T14S_EC_EVT_THERMAL_TZ40: + dev_dbg(ec->dev, "Thermal Zone 40 Status Change Event (CPU/GPU)\n"); + break; + case T14S_EC_EVT_THERMAL_TZ42: + dev_dbg(ec->dev, "Thermal Zone 42 Status Change Event (Battery)\n"); + break; + case T14S_EC_EVT_THERMAL_TZ39: + dev_dbg(ec->dev, "Thermal Zone 39 Status Change Event (CPU/GPU)\n"); + break; + case T14S_EC_EVT_KEY_FN_G: + dev_dbg(ec->dev, "FN + G - toggle double-tapping\n"); + break; + case T14S_EC_EVT_KEY_FN_L: + dev_dbg(ec->dev, "FN + L - low performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_M: + dev_dbg(ec->dev, "FN + M - medium performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_H: + dev_dbg(ec->dev, "FN + H - high performance mode\n"); + break; + case T14S_EC_EVT_KEY_FN_T: + dev_dbg(ec->dev, "FN + T - toggle intelligent cooling mode\n"); + break; + case T14S_EC_EVT_KEY_FN_D: + dev_dbg(ec->dev, "FN + D - toggle privacy guard mode\n"); + break; + default: + dev_info(ec->dev, "Unknown EC event: 0x%02x\n", val); + break; + } + + return IRQ_HANDLED; +} + +static int t14s_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct t14s_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ec->dev = dev; + i2c_set_clientdata(client, ec); + + ec->regmap = devm_regmap_init(dev, &t14s_ec_regmap_bus, + ec, &t14s_ec_regmap_config); + if (IS_ERR(ec->regmap)) + return dev_err_probe(dev, PTR_ERR(ec->regmap), + "Failed to init regmap\n"); + + ret = t14s_leds_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_kbd_backlight_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_kbd_audio_led_probe(ec); + if (ret < 0) + return ret; + + ret = t14s_input_probe(ec); + if (ret < 0) + return ret; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + t14s_ec_irq_handler, + IRQF_ONESHOT, dev_name(dev), ec); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to get IRQ\n"); + + /* + * Disable wakeup support by default, because the driver currently does + * not support masking any events and the laptop should not wake up when + * the LID is closed. + */ + device_wakeup_disable(dev); + + return 0; +} + +static int t14s_ec_suspend(struct device *dev) +{ + struct t14s_ec *ec = dev_get_drvdata(dev); + + led_classdev_suspend(&ec->kbd_backlight); + + t14s_ec_write_sequence(ec, T14S_EC_REG_MODERN_STANDBY, + T14S_EC_MODERN_STANDBY_ENTRY, 3); + + return 0; +} + +static int t14s_ec_resume(struct device *dev) +{ + struct t14s_ec *ec = dev_get_drvdata(dev); + + t14s_ec_write_sequence(ec, T14S_EC_REG_MODERN_STANDBY, + T14S_EC_MODERN_STANDBY_EXIT, 3); + + led_classdev_resume(&ec->kbd_backlight); + + return 0; +} + +static const struct of_device_id t14s_ec_of_match[] = { + { .compatible = "lenovo,thinkpad-t14s-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, t14s_ec_of_match); + +static const struct i2c_device_id t14s_ec_i2c_id_table[] = { + { .name = "thinkpad-t14s-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, t14s_ec_i2c_id_table); + +static const struct dev_pm_ops t14s_ec_pm_ops = { + SYSTEM_SLEEP_PM_OPS(t14s_ec_suspend, t14s_ec_resume) +}; + +static struct i2c_driver t14s_ec_i2c_driver = { + .driver = { + .name = "thinkpad-t14s-ec", + .of_match_table = t14s_ec_of_match, + .pm = &t14s_ec_pm_ops, + }, + .probe = t14s_ec_probe, + .id_table = t14s_ec_i2c_id_table, +}; +module_i2c_driver(t14s_ec_i2c_driver); + +MODULE_AUTHOR("Sebastian Reichel "); +MODULE_DESCRIPTION("Lenovo Thinkpad T14s Embedded Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/lenovo-yoga-c630.c b/drivers/platform/arm64/lenovo-yoga-c630.c new file mode 100644 index 000000000..a8600a977 --- /dev/null +++ b/drivers/platform/arm64/lenovo-yoga-c630.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2022-2024, Linaro Ltd + * Authors: + * Bjorn Andersson + * Dmitry Baryshkov + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LENOVO_EC_RESPONSE_REG 0x01 +#define LENOVO_EC_REQUEST_REG 0x02 + +#define LENOVO_EC_UCSI_WRITE 0x20 +#define LENOVO_EC_UCSI_READ 0x21 + +#define LENOVO_EC_READ_REG 0xb0 +#define LENOVO_EC_REQUEST_NEXT_EVENT 0x84 + +#define LENOVO_EC_UCSI_VERSION 0x20 + +struct yoga_c630_ec { + struct i2c_client *client; + struct mutex lock; + struct blocking_notifier_head notifier_list; +}; + +static int yoga_c630_ec_request(struct yoga_c630_ec *ec, u8 *req, size_t req_len, + u8 *resp, size_t resp_len) +{ + int ret; + + lockdep_assert_held(&ec->lock); + + ret = i2c_smbus_write_i2c_block_data(ec->client, LENOVO_EC_REQUEST_REG, + req_len, req); + if (ret < 0) + return ret; + + return i2c_smbus_read_i2c_block_data(ec->client, LENOVO_EC_RESPONSE_REG, + resp_len, resp); +} + +int yoga_c630_ec_read8(struct yoga_c630_ec *ec, u8 addr) +{ + u8 req[2] = { LENOVO_EC_READ_REG, }; + int ret; + u8 val; + + guard(mutex)(&ec->lock); + + req[1] = addr; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &val, 1); + if (ret < 0) + return ret; + + return val; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_read8); + +int yoga_c630_ec_read16(struct yoga_c630_ec *ec, u8 addr) +{ + u8 req[2] = { LENOVO_EC_READ_REG, }; + int ret; + u8 msb; + u8 lsb; + + /* don't overflow the address */ + if (addr == 0xff) + return -EINVAL; + + guard(mutex)(&ec->lock); + + req[1] = addr; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &lsb, 1); + if (ret < 0) + return ret; + + req[1] = addr + 1; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &msb, 1); + if (ret < 0) + return ret; + + return msb << 8 | lsb; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_read16); + +u16 yoga_c630_ec_ucsi_get_version(struct yoga_c630_ec *ec) +{ + u8 req[3] = { 0xb3, 0xf2, }; + int ret; + u8 msb; + u8 lsb; + + guard(mutex)(&ec->lock); + + req[2] = LENOVO_EC_UCSI_VERSION; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &lsb, 1); + if (ret < 0) + return ret; + + req[2] = LENOVO_EC_UCSI_VERSION + 1; + ret = yoga_c630_ec_request(ec, req, sizeof(req), &msb, 1); + if (ret < 0) + return ret; + + return msb << 8 | lsb; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_get_version); + +int yoga_c630_ec_ucsi_write(struct yoga_c630_ec *ec, + const u8 req[YOGA_C630_UCSI_WRITE_SIZE]) +{ + int ret; + + mutex_lock(&ec->lock); + ret = i2c_smbus_write_i2c_block_data(ec->client, LENOVO_EC_UCSI_WRITE, + YOGA_C630_UCSI_WRITE_SIZE, req); + mutex_unlock(&ec->lock); + + return ret < 0 ? ret : 0; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_write); + +int yoga_c630_ec_ucsi_read(struct yoga_c630_ec *ec, + u8 resp[YOGA_C630_UCSI_READ_SIZE]) +{ + int ret; + + mutex_lock(&ec->lock); + ret = i2c_smbus_read_i2c_block_data(ec->client, LENOVO_EC_UCSI_READ, + YOGA_C630_UCSI_READ_SIZE, resp); + mutex_unlock(&ec->lock); + + return ret < 0 ? ret : 0; +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_ucsi_read); + +static irqreturn_t yoga_c630_ec_thread_intr(int irq, void *data) +{ + u8 req[] = { LENOVO_EC_REQUEST_NEXT_EVENT }; + struct yoga_c630_ec *ec = data; + u8 event; + int ret; + + mutex_lock(&ec->lock); + ret = yoga_c630_ec_request(ec, req, sizeof(req), &event, 1); + mutex_unlock(&ec->lock); + if (ret < 0) + return IRQ_HANDLED; + + blocking_notifier_call_chain(&ec->notifier_list, event, ec); + + return IRQ_HANDLED; +} + +/** + * yoga_c630_ec_register_notify - Register a notifier callback for EC events. + * @ec: Yoga C630 EC + * @nb: Notifier block pointer to register + * + * Return: 0 on success or negative error code. + */ +int yoga_c630_ec_register_notify(struct yoga_c630_ec *ec, struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_register_notify); + +/** + * yoga_c630_ec_unregister_notify - Unregister notifier callback for EC events. + * @ec: Yoga C630 EC + * @nb: Notifier block pointer to unregister + * + * Unregister a notifier callback that was previously registered with + * yoga_c630_ec_register_notify(). + */ +void yoga_c630_ec_unregister_notify(struct yoga_c630_ec *ec, struct notifier_block *nb) +{ + blocking_notifier_chain_unregister(&ec->notifier_list, nb); +} +EXPORT_SYMBOL_GPL(yoga_c630_ec_unregister_notify); + +static int yoga_c630_aux_init(struct device *parent, const char *name, + struct yoga_c630_ec *ec) +{ + struct auxiliary_device *adev; + + adev = devm_auxiliary_device_create(parent, name, ec); + if (!adev) + return -ENODEV; + + return 0; +} + +static int yoga_c630_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct yoga_c630_ec *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + mutex_init(&ec->lock); + ec->client = client; + BLOCKING_INIT_NOTIFIER_HEAD(&ec->notifier_list); + + ret = devm_request_threaded_irq(dev, client->irq, + NULL, yoga_c630_ec_thread_intr, + IRQF_ONESHOT, "yoga_c630_ec", ec); + if (ret < 0) + return dev_err_probe(dev, ret, "unable to request irq\n"); + + ret = yoga_c630_aux_init(dev, YOGA_C630_DEV_PSY, ec); + if (ret) + return ret; + + return yoga_c630_aux_init(dev, YOGA_C630_DEV_UCSI, ec); +} + + +static const struct of_device_id yoga_c630_ec_of_match[] = { + { .compatible = "lenovo,yoga-c630-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, yoga_c630_ec_of_match); + +static const struct i2c_device_id yoga_c630_ec_i2c_id_table[] = { + { .name = "yoga-c630-ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, yoga_c630_ec_i2c_id_table); + +static struct i2c_driver yoga_c630_ec_i2c_driver = { + .driver = { + .name = "yoga-c630-ec", + .of_match_table = yoga_c630_ec_of_match + }, + .probe = yoga_c630_ec_probe, + .id_table = yoga_c630_ec_i2c_id_table, +}; +module_i2c_driver(yoga_c630_ec_i2c_driver); + +MODULE_DESCRIPTION("Lenovo Yoga C630 Embedded Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/arm64/qcom-hamoa-ec.c b/drivers/platform/arm64/qcom-hamoa-ec.c new file mode 100644 index 000000000..5ca7308c6 --- /dev/null +++ b/drivers/platform/arm64/qcom-hamoa-ec.c @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2024 Maya Matuszczyk + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EC_SCI_EVT_READ_CMD 0x05 +#define EC_FW_VERSION_CMD 0x0e +#define EC_MODERN_STANDBY_CMD 0x23 +#define EC_FAN_DBG_CONTROL_CMD 0x30 +#define EC_SCI_EVT_CONTROL_CMD 0x35 +#define EC_THERMAL_CAP_CMD 0x42 + +#define EC_FW_VERSION_RESP_LEN 4 +#define EC_THERMAL_CAP_RESP_LEN 3 +#define EC_FAN_DEBUG_CMD_LEN 6 +#define EC_FAN_SPEED_DATA_SIZE 4 + +#define EC_MODERN_STANDBY_ENTER 0x01 +#define EC_MODERN_STANDBY_EXIT 0x00 + +#define EC_FAN_DEBUG_MODE_OFF 0 +#define EC_FAN_DEBUG_MODE_ON BIT(0) +#define EC_FAN_ON BIT(1) +#define EC_FAN_DEBUG_TYPE_PWM BIT(2) +#define EC_MAX_FAN_CNT 2 +#define EC_FAN_NAME_SIZE 20 +#define EC_FAN_MAX_PWM 255 + +enum qcom_ec_sci_events { + EC_FAN1_STATUS_CHANGE_EVT = 0x30, + EC_FAN2_STATUS_CHANGE_EVT, + EC_FAN1_SPEED_CHANGE_EVT, + EC_FAN2_SPEED_CHANGE_EVT, + EC_NEW_LUT_SET_EVT, + EC_FAN_PROFILE_SWITCH_EVT, + EC_THERMISTOR_1_THRESHOLD_CROSS_EVT, + EC_THERMISTOR_2_THRESHOLD_CROSS_EVT, + EC_THERMISTOR_3_THRESHOLD_CROSS_EVT, + /* Reserved: 0x39 - 0x3c/0x3f */ + EC_RECOVERED_FROM_RESET_EVT = 0x3d, +}; + +struct qcom_ec_version { + u8 main_version; + u8 sub_version; + u8 test_version; +}; + +struct qcom_ec_thermal_cap { +#define EC_THERMAL_FAN_CNT(x) (FIELD_GET(GENMASK(1, 0), (x))) +#define EC_THERMAL_FAN_TYPE(x) (FIELD_GET(GENMASK(4, 2), (x))) +#define EC_THERMAL_THERMISTOR_MASK(x) (FIELD_GET(GENMASK(7, 0), (x))) + u8 fan_cnt; + u8 fan_type; + u8 thermistor_mask; +}; + +struct qcom_ec_cooling_dev { + struct thermal_cooling_device *cdev; + struct device *parent_dev; + u8 fan_id; + u8 state; +}; + +struct qcom_ec { + struct qcom_ec_cooling_dev *ec_cdev; + struct qcom_ec_thermal_cap thermal_cap; + struct qcom_ec_version version; + struct i2c_client *client; +}; + +static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, u8 resp_len, u8 *resp) +{ + int ret; + + ret = i2c_smbus_read_i2c_block_data(ec->client, cmd, resp_len, resp); + if (ret < 0) + return ret; + else if (ret == 0 || ret == 0xff) + return -EOPNOTSUPP; + + if (resp[0] >= resp_len) + return -EINVAL; + + return 0; +} + +/* + * EC Device Firmware Version: + * + * Read Response: + * ---------------------------------------------------------------------- + * | Offset | Name | Description | + * ---------------------------------------------------------------------- + * | 0x00 | Byte count | Number of bytes in response | + * | | | (excluding byte count) | + * ---------------------------------------------------------------------- + * | 0x01 | Test-version | Test-version of EC firmware | + * ---------------------------------------------------------------------- + * | 0x02 | Sub-version | Sub-version of EC firmware | + * ---------------------------------------------------------------------- + * | 0x03 | Main-version | Main-version of EC firmware | + * ---------------------------------------------------------------------- + * + */ +static int qcom_ec_read_fw_version(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct qcom_ec *ec = i2c_get_clientdata(client); + struct qcom_ec_version *version = &ec->version; + u8 resp[EC_FW_VERSION_RESP_LEN]; + int ret; + + ret = qcom_ec_read(ec, EC_FW_VERSION_CMD, EC_FW_VERSION_RESP_LEN, resp); + if (ret < 0) + return ret; + + version->main_version = resp[3]; + version->sub_version = resp[2]; + version->test_version = resp[1]; + + dev_dbg(dev, "EC Version %d.%d.%d\n", + version->main_version, version->sub_version, version->test_version); + + return 0; +} + +/* + * EC Device Thermal Capabilities: + * + * Read Response: + * ------------------------------------------------------------------------------ + * | Offset | Name | Description | + * ------------------------------------------------------------------------------ + * | 0x00 | Byte count | Number of bytes in response | + * | | | (excluding byte count) | + * ------------------------------------------------------------------------------ + * | 0x02 (LSB) | EC Thermal | Bit 0-1: Number of fans | + * | 0x03 | Capabilities | Bit 2-4: Type of fan | + * | | | Bit 5-6: Reserved | + * | | | Bit 7: Data Valid/Invalid | + * | | | (Valid - 1, Invalid - 0) | + * | | | Bit 8-15: Thermistor 0 - 7 presence | + * | | | (1 present, 0 absent) | + * ------------------------------------------------------------------------------ + * + */ +static int qcom_ec_thermal_capabilities(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct qcom_ec *ec = i2c_get_clientdata(client); + struct qcom_ec_thermal_cap *cap = &ec->thermal_cap; + u8 resp[EC_THERMAL_CAP_RESP_LEN]; + int ret; + + ret = qcom_ec_read(ec, EC_THERMAL_CAP_CMD, EC_THERMAL_CAP_RESP_LEN, resp); + if (ret < 0) + return ret; + + cap->fan_cnt = min(EC_MAX_FAN_CNT, EC_THERMAL_FAN_CNT(resp[1])); + cap->fan_type = EC_THERMAL_FAN_TYPE(resp[1]); + cap->thermistor_mask = EC_THERMAL_THERMISTOR_MASK(resp[2]); + + dev_dbg(dev, "Fan count: %d Fan Type: %d Thermistor Mask: %x\n", + cap->fan_cnt, cap->fan_type, cap->thermistor_mask); + + return 0; +} + +static irqreturn_t qcom_ec_irq(int irq, void *data) +{ + struct qcom_ec *ec = data; + struct device *dev = &ec->client->dev; + int val; + + val = i2c_smbus_read_byte_data(ec->client, EC_SCI_EVT_READ_CMD); + if (val < 0) { + dev_err_ratelimited(dev, "Failed to read EC SCI Event: %d\n", val); + return IRQ_HANDLED; + } + + switch (val) { + case EC_FAN1_STATUS_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan1 status changed\n"); + break; + case EC_FAN2_STATUS_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan2 status changed\n"); + break; + case EC_FAN1_SPEED_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan1 speed crossed low/high trip point\n"); + break; + case EC_FAN2_SPEED_CHANGE_EVT: + dev_dbg_ratelimited(dev, "Fan2 speed crossed low/high trip point\n"); + break; + case EC_NEW_LUT_SET_EVT: + dev_dbg_ratelimited(dev, "New LUT set\n"); + break; + case EC_FAN_PROFILE_SWITCH_EVT: + dev_dbg_ratelimited(dev, "FAN Profile switched\n"); + break; + case EC_THERMISTOR_1_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 1 threshold crossed\n"); + break; + case EC_THERMISTOR_2_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 2 threshold crossed\n"); + break; + case EC_THERMISTOR_3_THRESHOLD_CROSS_EVT: + dev_dbg_ratelimited(dev, "Thermistor 3 threshold crossed\n"); + break; + case EC_RECOVERED_FROM_RESET_EVT: + dev_dbg_ratelimited(dev, "EC recovered from reset\n"); + break; + default: + dev_notice_ratelimited(dev, "Unknown EC event: %d\n", val); + break; + } + + return IRQ_HANDLED; +} + +static int qcom_ec_sci_evt_control(struct device *dev, bool enable) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_SCI_EVT_CONTROL_CMD, enable ? 1 : 0); +} + +static int qcom_ec_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) +{ + *state = EC_FAN_MAX_PWM; + + return 0; +} + +static int qcom_ec_fan_get_cur_state(struct thermal_cooling_device *cdev, unsigned long *state) +{ + struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; + + *state = ec_cdev->state; + + return 0; +} + +/* + * Fan Debug control command: + * + * Command Payload: + * -------------------------------------------------------------------------------------- + * | Offset | Name | Description | + * -------------------------------------------------------------------------------------- + * | 0x00 | Command | Fan control command | + * -------------------------------------------------------------------------------------- + * | 0x01 | Fan ID | 0x1 : Fan 1 | + * | | | 0x2 : Fan 2 | + * -------------------------------------------------------------------------------------- + * | 0x02 | Byte count = 4| Size of data to set fan speed | + * -------------------------------------------------------------------------------------- + * | 0x03 | Mode | Bit 0: Debug Mode On/Off (0 - OFF, 1 - ON ) | + * | | | Bit 1: Fan On/Off (0 - Off, 1 - ON) | + * | | | Bit 2: Debug Type (0 - RPM, 1 - PWM) | + * -------------------------------------------------------------------------------------- + * | 0x04 (LSB) | Speed in RPM | RPM value, if mode selected is RPM | + * | 0x05 | | | + * -------------------------------------------------------------------------------------- + * | 0x06 | Speed in PWM | PWM value, if mode selected is PWM (0 - 255) | + * ______________________________________________________________________________________ + * + */ +static int qcom_ec_fan_debug_mode_off(struct qcom_ec_cooling_dev *ec_cdev) +{ + struct device *dev = ec_cdev->parent_dev; + struct i2c_client *client = to_i2c_client(dev); + u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, + EC_FAN_DEBUG_MODE_OFF, 0, 0, 0 }; + int ret; + + ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, + sizeof(request), request); + if (ret) { + dev_err(dev, "Failed to turn off fan%d debug mode: %d\n", + ec_cdev->fan_id, ret); + } + + return ret; +} + +static int qcom_ec_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) +{ + struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; + struct device *dev = ec_cdev->parent_dev; + struct i2c_client *client = to_i2c_client(dev); + u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, + EC_FAN_DEBUG_MODE_ON | EC_FAN_ON | EC_FAN_DEBUG_TYPE_PWM, + 0, 0, state }; + int ret; + + ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, + sizeof(request), request); + if (ret) { + dev_err(dev, "Failed to set fan pwm: %d\n", ret); + return ret; + } + + ec_cdev->state = state; + + return 0; +} + +static const struct thermal_cooling_device_ops qcom_ec_thermal_ops = { + .get_max_state = qcom_ec_fan_get_max_state, + .get_cur_state = qcom_ec_fan_get_cur_state, + .set_cur_state = qcom_ec_fan_set_cur_state, +}; + +static int qcom_ec_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, + EC_MODERN_STANDBY_EXIT); +} + +static int qcom_ec_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, + EC_MODERN_STANDBY_ENTER); +} + +static int qcom_ec_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct qcom_ec *ec; + unsigned int i; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + ec->client = client; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, qcom_ec_irq, + IRQF_ONESHOT, "qcom_ec", ec); + if (ret < 0) + return ret; + + i2c_set_clientdata(client, ec); + + ret = qcom_ec_read_fw_version(dev); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read EC firmware version\n"); + + ret = qcom_ec_sci_evt_control(dev, true); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to enable SCI events\n"); + + ret = qcom_ec_thermal_capabilities(dev); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read thermal capabilities\n"); + + if (ec->thermal_cap.fan_cnt == 0) { + dev_warn(dev, FW_BUG "Failed to get fan count, firmware update required\n"); + return 0; + } + + ec->ec_cdev = devm_kcalloc(dev, ec->thermal_cap.fan_cnt, sizeof(*ec->ec_cdev), GFP_KERNEL); + if (!ec->ec_cdev) + return -ENOMEM; + + for (i = 0; i < ec->thermal_cap.fan_cnt; i++) { + struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; + char name[EC_FAN_NAME_SIZE]; + + scnprintf(name, sizeof(name), "qcom_ec_fan_%u", i); + ec_cdev->fan_id = i + 1; + ec_cdev->parent_dev = dev; + + ec_cdev->cdev = devm_thermal_of_child_cooling_device_register(dev, NULL, name, ec_cdev, + &qcom_ec_thermal_ops); + if (IS_ERR(ec_cdev->cdev)) { + return dev_err_probe(dev, PTR_ERR(ec_cdev->cdev), + "Failed to register fan%d cooling device\n", i); + } + } + + return 0; +} + +static void qcom_ec_remove(struct i2c_client *client) +{ + struct qcom_ec *ec = i2c_get_clientdata(client); + struct device *dev = &client->dev; + int ret; + + ret = qcom_ec_sci_evt_control(dev, false); + if (ret < 0) + dev_err(dev, "Failed to disable SCI events: %d\n", ret); + + for (int i = 0; i < ec->thermal_cap.fan_cnt; i++) { + struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; + + qcom_ec_fan_debug_mode_off(ec_cdev); + } +} + +static const struct of_device_id qcom_ec_of_match[] = { + { .compatible = "qcom,hamoa-crd-ec" }, + {} +}; +MODULE_DEVICE_TABLE(of, qcom_ec_of_match); + +static const struct i2c_device_id qcom_ec_i2c_id_table[] = { + { "qcom-hamoa-ec", }, + {} +}; +MODULE_DEVICE_TABLE(i2c, qcom_ec_i2c_id_table); + +static DEFINE_SIMPLE_DEV_PM_OPS(qcom_ec_pm_ops, + qcom_ec_suspend, + qcom_ec_resume); + +static struct i2c_driver qcom_ec_i2c_driver = { + .driver = { + .name = "qcom-hamoa-ec", + .of_match_table = qcom_ec_of_match, + .pm = &qcom_ec_pm_ops, + }, + .probe = qcom_ec_probe, + .remove = qcom_ec_remove, + .id_table = qcom_ec_i2c_id_table, +}; +module_i2c_driver(qcom_ec_i2c_driver); + +MODULE_DESCRIPTION("QCOM Hamoa Embedded Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/Kconfig b/drivers/platform/chrome/Kconfig new file mode 100644 index 000000000..ca2e84420 --- /dev/null +++ b/drivers/platform/chrome/Kconfig @@ -0,0 +1,330 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Platform support for Chrome OS hardware (Chromebooks and Chromeboxes) +# + +menuconfig CHROME_PLATFORMS + bool "Platform support for Chrome hardware" + depends on X86 || ARM || ARM64 || COMPILE_TEST + depends on !CPU_BIG_ENDIAN || COMPILE_TEST + help + Say Y here to get to see options for platform support for + various Chromebooks and Chromeboxes. This option alone does + not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if CHROME_PLATFORMS + +config CHROMEOS_ACPI + tristate "ChromeOS specific ACPI extensions" + depends on ACPI + help + This driver provides the firmware interface for the services + exported through the ChromeOS interfaces when using ChromeOS + ACPI firmware. + + If you have an ACPI-compatible Chromebook, say Y or M here. + The module will be called chromeos_acpi. + +config CHROMEOS_LAPTOP + tristate "Chrome OS Laptop" + depends on I2C && DMI && X86 + help + This driver instantiates i2c and smbus devices such as + light sensors and touchpads. + + If you have a supported Chromebook, choose Y or M here. + The module will be called chromeos_laptop. + +config CHROMEOS_PSTORE + tristate "Chrome OS pstore support" + depends on X86 + help + This module instantiates the persistent storage on x86 ChromeOS + devices. It can be used to store away console logs and crash + information across reboots. + + The range of memory used is 0xf00000-0x1000000, traditionally + the memory used to back VGA controller memory. + + If you have a supported Chromebook, choose Y or M here. + The module will be called chromeos_pstore. + +config CHROMEOS_TBMC + tristate "ChromeOS Tablet Switch Controller" + depends on ACPI + depends on INPUT + help + This option adds a driver for the tablet switch on + select Chrome OS systems. + + To compile this driver as a module, choose M here: the + module will be called chromeos_tbmc. + +config CHROMEOS_OF_HW_PROBER + tristate "ChromeOS Device Tree Hardware Prober" + depends on OF + depends on I2C + select OF_DYNAMIC + default OF + help + This option enables the device tree hardware prober for ChromeOS + devices. The driver will probe the correct component variant in + devices that have multiple drop-in options for one component. + +config CROS_EC + tristate "ChromeOS Embedded Controller" + select CROS_EC_PROTO + help + If you say Y here you get support for the ChromeOS Embedded + Controller (EC) providing keyboard, battery and power services. + You also need to enable the driver for the bus you are using. The + protocol for talking to the EC is defined by the bus driver. + + To compile this driver as a module, choose M here: the + module will be called cros_ec. + +config CROS_EC_I2C + tristate "ChromeOS Embedded Controller (I2C)" + depends on CROS_EC && I2C + + help + If you say Y here, you get support for talking to the ChromeOS + EC through an I2C bus. This uses a simple byte-level protocol with + a checksum. Failing accesses will be retried three times to + improve reliability. + +config CROS_EC_RPMSG + tristate "ChromeOS Embedded Controller (rpmsg)" + depends on CROS_EC && RPMSG && OF + help + If you say Y here, you get support for talking to the ChromeOS EC + through rpmsg. This uses a simple byte-level protocol with a + checksum. Also since there's no addition EC-to-host interrupt, this + use a byte in message to distinguish host event from host command. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_rpmsg. + +config CROS_EC_ISHTP + tristate "ChromeOS Embedded Controller (ISHTP)" + depends on CROS_EC + depends on INTEL_ISH_HID + help + If you say Y here, you get support for talking to the ChromeOS EC + firmware running on Intel Integrated Sensor Hub (ISH), using the + ISH Transport protocol (ISH-TP). This uses a simple byte-level + protocol with a checksum. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_ishtp. + +config CROS_EC_SPI + tristate "ChromeOS Embedded Controller (SPI)" + depends on CROS_EC && SPI + + help + If you say Y here, you get support for talking to the ChromeOS EC + through a SPI bus, using a byte-level protocol. Since the EC's + response time cannot be guaranteed, we support ignoring + 'pre-amble' bytes before the response actually starts. + +config CROS_EC_UART + tristate "ChromeOS Embedded Controller (UART)" + depends on CROS_EC && ACPI && SERIAL_DEV_BUS + help + If you say Y here, you get support for talking to the ChromeOS EC + through a UART, using a byte-level protocol. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_uart. + +config CROS_EC_LPC + tristate "ChromeOS Embedded Controller (LPC)" + depends on CROS_EC && ACPI && (X86 || COMPILE_TEST) + depends on HAS_IOPORT + help + If you say Y here, you get support for talking to the ChromeOS EC + over an LPC bus, including the LPC Microchip EC (MEC) variant. + This uses a simple byte-level protocol with a checksum. This is + used for userspace access only. The kernel typically has its own + communication methods. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_lpcs. + +config CROS_EC_PROTO + tristate + help + ChromeOS EC communication protocol helpers. + +config CROS_KBD_LED_BACKLIGHT + tristate "Backlight LED support for Chrome OS keyboards" + depends on LEDS_CLASS + depends on MFD_CROS_EC_DEV || (MFD_CROS_EC_DEV=n && ACPI) + help + This option enables support for the keyboard backlight LEDs on + select Chrome OS systems. + + To compile this driver as a module, choose M here: the + module will be called cros_kbd_led_backlight. + +config CROS_EC_CHARDEV + tristate "ChromeOS EC miscdevice" + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV + help + This driver adds file operations support to talk with the + ChromeOS EC from userspace via a character device. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_chardev. + +config CROS_EC_LIGHTBAR + tristate "Chromebook Pixel's lightbar support" + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV + help + This option exposes the Chromebook Pixel's lightbar to + userspace. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_lightbar. + +config CROS_EC_VBC + tristate "ChromeOS EC vboot context support" + depends on MFD_CROS_EC_DEV && OF + default MFD_CROS_EC_DEV + help + This option exposes the ChromeOS EC vboot context nvram to + userspace. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_vbc. + +config CROS_EC_DEBUGFS + tristate "Export ChromeOS EC internals in DebugFS" + depends on MFD_CROS_EC_DEV && DEBUG_FS + default MFD_CROS_EC_DEV + help + This option exposes the ChromeOS EC device internals to + userspace. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_debugfs. + +config CROS_EC_SENSORHUB + tristate "ChromeOS EC MEMS Sensor Hub" + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV + help + Allow loading IIO sensors. This driver is loaded by MFD and will in + turn query the EC and register the sensors. + It also spreads the sensor data coming from the EC to the IIO sensor + object. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_sensorhub. + +config CROS_EC_SYSFS + tristate "ChromeOS EC control and information through sysfs" + depends on MFD_CROS_EC_DEV && SYSFS + default MFD_CROS_EC_DEV + help + This option exposes some sysfs attributes to control and get + information from ChromeOS EC. + + To compile this driver as a module, choose M here: the + module will be called cros_ec_sysfs. + +config CROS_EC_TYPEC_ALTMODES + bool + help + Selectable symbol to enable altmodes. + +config CROS_EC_TYPEC + tristate "ChromeOS EC Type-C Connector Control" + depends on MFD_CROS_EC_DEV && TYPEC + depends on CROS_USBPD_NOTIFY + depends on USB_ROLE_SWITCH + default MFD_CROS_EC_DEV + select CROS_EC_TYPEC_ALTMODES if TYPEC_DP_ALTMODE + select CROS_EC_TYPEC_ALTMODES if TYPEC_TBT_ALTMODE + help + If you say Y here, you get support for accessing Type C connector + information from the Chrome OS EC. + + To compile this driver as a module, choose M here: the module will be + called cros-ec-typec. + +config CROS_HPS_I2C + tristate "ChromeOS HPS device" + depends on HID && I2C && PM + help + Say Y here if you want to enable support for the ChromeOS + human presence sensor (HPS), attached via I2C. The driver supports a + sensor connected to the I2C bus and exposes it as a character device. + To save power, the sensor is automatically powered down when no + clients are accessing it. + +config CROS_USBPD_LOGGER + tristate "Logging driver for USB PD charger" + depends on CHARGER_CROS_USBPD + default y + select RTC_LIB + help + This option enables support for logging event data for the USB PD charger + available in the Embedded Controller on ChromeOS systems. + + To compile this driver as a module, choose M here: the + module will be called cros_usbpd_logger. + +config CROS_USBPD_NOTIFY + tristate "ChromeOS Type-C power delivery event notifier" + depends on MFD_CROS_EC_DEV + default MFD_CROS_EC_DEV + help + If you say Y here, you get support for Type-C PD event notifications + from the ChromeOS EC. On ACPI platforms this driver will bind to the + GOOG0003 ACPI device, and on platforms which don't have this device it + will get initialized on ECs which support the feature + EC_FEATURE_USB_PD. + + To compile this driver as a module, choose M here: the + module will be called cros_usbpd_notify. + +config CHROMEOS_PRIVACY_SCREEN + tristate "ChromeOS Privacy Screen support" + depends on ACPI + depends on DRM + select DRM_PRIVACY_SCREEN + help + This driver provides the support needed for the in-built electronic + privacy screen that is present on some ChromeOS devices. When enabled, + this should probably always be built into the kernel to avoid or + minimize drm probe deferral. + +config CROS_TYPEC_SWITCH + tristate "ChromeOS EC Type-C Switch Control" + depends on MFD_CROS_EC_DEV && TYPEC && ACPI + default MFD_CROS_EC_DEV + help + If you say Y here, you get support for configuring the ChromeOS EC Type-C + muxes and retimers. + + To compile this driver as a module, choose M here: the module will be + called cros_typec_switch. + +source "drivers/platform/chrome/wilco_ec/Kconfig" + +# Kunit test cases +config CROS_KUNIT_EC_PROTO_TEST + tristate "Kunit tests for ChromeOS EC protocol" if !KUNIT_ALL_TESTS + depends on KUNIT && CROS_EC + default KUNIT_ALL_TESTS + select CROS_EC_PROTO + help + Kunit tests for ChromeOS EC protocol. + +endif # CHROMEOS_PLATFORMS diff --git a/drivers/platform/chrome/Makefile b/drivers/platform/chrome/Makefile new file mode 100644 index 000000000..b981a1bb5 --- /dev/null +++ b/drivers/platform/chrome/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0 + +# tell define_trace.h where to find the cros ec trace header +CFLAGS_cros_ec_trace.o:= -I$(src) +CFLAGS_cros_ec_sensorhub_ring.o:= -I$(src) + +obj-$(CONFIG_CHROMEOS_ACPI) += chromeos_acpi.o +obj-$(CONFIG_CHROMEOS_LAPTOP) += chromeos_laptop.o +obj-$(CONFIG_CHROMEOS_OF_HW_PROBER) += chromeos_of_hw_prober.o +obj-$(CONFIG_CHROMEOS_PRIVACY_SCREEN) += chromeos_privacy_screen.o +obj-$(CONFIG_CHROMEOS_PSTORE) += chromeos_pstore.o +obj-$(CONFIG_CHROMEOS_TBMC) += chromeos_tbmc.o +obj-$(CONFIG_CROS_EC) += cros_ec.o +obj-$(CONFIG_CROS_EC_I2C) += cros_ec_i2c.o +obj-$(CONFIG_CROS_EC_ISHTP) += cros_ec_ishtp.o +obj-$(CONFIG_CROS_TYPEC_SWITCH) += cros_typec_switch.o +obj-$(CONFIG_CROS_EC_RPMSG) += cros_ec_rpmsg.o +obj-$(CONFIG_CROS_EC_SPI) += cros_ec_spi.o +obj-$(CONFIG_CROS_EC_UART) += cros_ec_uart.o +cros_ec_lpcs-objs := cros_ec_lpc.o cros_ec_lpc_mec.o +cros-ec-typec-objs := cros_ec_typec.o cros_typec_vdm.o +ifneq ($(CONFIG_CROS_EC_TYPEC_ALTMODES),) + cros-ec-typec-objs += cros_typec_altmode.o +endif +obj-$(CONFIG_CROS_EC_TYPEC) += cros-ec-typec.o + +obj-$(CONFIG_CROS_EC_LPC) += cros_ec_lpcs.o +cros-ec-proto-objs := cros_ec_proto.o cros_ec_trace.o +obj-$(CONFIG_CROS_EC_PROTO) += cros-ec-proto.o +obj-$(CONFIG_CROS_KBD_LED_BACKLIGHT) += cros_kbd_led_backlight.o +obj-$(CONFIG_CROS_EC_CHARDEV) += cros_ec_chardev.o +obj-$(CONFIG_CROS_EC_LIGHTBAR) += cros_ec_lightbar.o +obj-$(CONFIG_CROS_EC_VBC) += cros_ec_vbc.o +obj-$(CONFIG_CROS_EC_DEBUGFS) += cros_ec_debugfs.o +cros-ec-sensorhub-objs := cros_ec_sensorhub.o cros_ec_sensorhub_ring.o +obj-$(CONFIG_CROS_EC_SENSORHUB) += cros-ec-sensorhub.o +obj-$(CONFIG_CROS_EC_SYSFS) += cros_ec_sysfs.o +obj-$(CONFIG_CROS_HPS_I2C) += cros_hps_i2c.o +obj-$(CONFIG_CROS_USBPD_LOGGER) += cros_usbpd_logger.o +obj-$(CONFIG_CROS_USBPD_NOTIFY) += cros_usbpd_notify.o + +obj-$(CONFIG_WILCO_EC) += wilco_ec/ + +# Kunit test cases +obj-$(CONFIG_CROS_KUNIT_EC_PROTO_TEST) += cros_kunit_proto_test.o +cros_kunit_proto_test-objs := cros_ec_proto_test_util.o cros_ec_proto_test.o diff --git a/drivers/platform/chrome/chromeos_acpi.c b/drivers/platform/chrome/chromeos_acpi.c new file mode 100644 index 000000000..e6e6dcfc7 --- /dev/null +++ b/drivers/platform/chrome/chromeos_acpi.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ChromeOS specific ACPI extensions + * + * Copyright 2022 Google LLC + * + * This driver attaches to the ChromeOS ACPI device and then exports the + * values reported by the ACPI in a sysfs directory. All values are + * presented in the string form (numbers as decimal values) and can be + * accessed as the contents of the appropriate read only files in the + * sysfs directory tree. + */ +#include +#include +#include +#include +#include + +#define ACPI_ATTR_NAME_LEN 4 + +#define DEV_ATTR(_var, _name) \ + static struct device_attribute dev_attr_##_var = \ + __ATTR(_name, 0444, chromeos_first_level_attr_show, NULL); + +#define GPIO_ATTR_GROUP(_group, _name, _num) \ + static umode_t attr_is_visible_gpio_##_num(struct kobject *kobj, \ + struct attribute *attr, int n) \ + { \ + if (_num < chromeos_acpi_gpio_groups) \ + return attr->mode; \ + return 0; \ + } \ + static ssize_t chromeos_attr_show_gpio_##_num(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + char name[ACPI_ATTR_NAME_LEN + 1]; \ + int ret, num; \ + \ + ret = parse_attr_name(attr->attr.name, name, &num); \ + if (ret) \ + return ret; \ + return chromeos_acpi_evaluate_method(dev, _num, num, name, buf); \ + } \ + static struct device_attribute dev_attr_0_##_group = \ + __ATTR(GPIO.0, 0444, chromeos_attr_show_gpio_##_num, NULL); \ + static struct device_attribute dev_attr_1_##_group = \ + __ATTR(GPIO.1, 0444, chromeos_attr_show_gpio_##_num, NULL); \ + static struct device_attribute dev_attr_2_##_group = \ + __ATTR(GPIO.2, 0444, chromeos_attr_show_gpio_##_num, NULL); \ + static struct device_attribute dev_attr_3_##_group = \ + __ATTR(GPIO.3, 0444, chromeos_attr_show_gpio_##_num, NULL); \ + \ + static struct attribute *attrs_##_group[] = { \ + &dev_attr_0_##_group.attr, \ + &dev_attr_1_##_group.attr, \ + &dev_attr_2_##_group.attr, \ + &dev_attr_3_##_group.attr, \ + NULL \ + }; \ + static const struct attribute_group attr_group_##_group = { \ + .name = _name, \ + .is_visible = attr_is_visible_gpio_##_num, \ + .attrs = attrs_##_group, \ + }; + +static unsigned int chromeos_acpi_gpio_groups; + +/* Parse the ACPI package and return the data related to that attribute */ +static int chromeos_acpi_handle_package(struct device *dev, union acpi_object *obj, + int pkg_num, int sub_pkg_num, char *name, char *buf) +{ + union acpi_object *element = obj->package.elements; + + if (pkg_num >= obj->package.count) + return -EINVAL; + element += pkg_num; + + if (element->type == ACPI_TYPE_PACKAGE) { + if (sub_pkg_num >= element->package.count) + return -EINVAL; + /* select sub element inside this package */ + element = element->package.elements; + element += sub_pkg_num; + } + + switch (element->type) { + case ACPI_TYPE_INTEGER: + return sysfs_emit(buf, "%d\n", (int)element->integer.value); + case ACPI_TYPE_STRING: + return sysfs_emit(buf, "%s\n", element->string.pointer); + case ACPI_TYPE_BUFFER: + { + int i, r, at, room_left; + const int byte_per_line = 16; + + at = 0; + room_left = PAGE_SIZE - 1; + for (i = 0; i < element->buffer.length && room_left; i += byte_per_line) { + r = hex_dump_to_buffer(element->buffer.pointer + i, + element->buffer.length - i, + byte_per_line, 1, buf + at, room_left, + false); + if (r > room_left) + goto truncating; + at += r; + room_left -= r; + + r = sysfs_emit_at(buf, at, "\n"); + if (!r) + goto truncating; + at += r; + room_left -= r; + } + + buf[at] = 0; + return at; +truncating: + dev_info_once(dev, "truncating sysfs content for %s\n", name); + sysfs_emit_at(buf, PAGE_SIZE - 4, "..\n"); + return PAGE_SIZE - 1; + } + default: + dev_err(dev, "element type %d not supported\n", element->type); + return -EINVAL; + } +} + +static int chromeos_acpi_evaluate_method(struct device *dev, int pkg_num, int sub_pkg_num, + char *name, char *buf) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + int ret = -EINVAL; + + status = acpi_evaluate_object(ACPI_HANDLE(dev), name, NULL, &output); + if (ACPI_FAILURE(status)) { + dev_err(dev, "failed to retrieve %s. %s\n", name, acpi_format_exception(status)); + return ret; + } + + if (((union acpi_object *)output.pointer)->type == ACPI_TYPE_PACKAGE) + ret = chromeos_acpi_handle_package(dev, output.pointer, pkg_num, sub_pkg_num, + name, buf); + + kfree(output.pointer); + return ret; +} + +static int parse_attr_name(const char *name, char *attr_name, int *attr_num) +{ + int ret; + + ret = strscpy(attr_name, name, ACPI_ATTR_NAME_LEN + 1); + if (ret == -E2BIG) + return kstrtoint(&name[ACPI_ATTR_NAME_LEN + 1], 0, attr_num); + return 0; +} + +static ssize_t chromeos_first_level_attr_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + char attr_name[ACPI_ATTR_NAME_LEN + 1]; + int ret, attr_num = 0; + + ret = parse_attr_name(attr->attr.name, attr_name, &attr_num); + if (ret) + return ret; + return chromeos_acpi_evaluate_method(dev, attr_num, 0, attr_name, buf); +} + +static unsigned int get_gpio_pkg_num(struct device *dev) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + unsigned int count = 0; + char *name = "GPIO"; + + status = acpi_evaluate_object(ACPI_HANDLE(dev), name, NULL, &output); + if (ACPI_FAILURE(status)) { + dev_err(dev, "failed to retrieve %s. %s\n", name, acpi_format_exception(status)); + return count; + } + + obj = output.pointer; + + if (obj->type == ACPI_TYPE_PACKAGE) + count = obj->package.count; + + kfree(output.pointer); + return count; +} + +DEV_ATTR(binf2, BINF.2) +DEV_ATTR(binf3, BINF.3) +DEV_ATTR(chsw, CHSW) +DEV_ATTR(fmap, FMAP) +DEV_ATTR(frid, FRID) +DEV_ATTR(fwid, FWID) +DEV_ATTR(hwid, HWID) +DEV_ATTR(meck, MECK) +DEV_ATTR(vbnv0, VBNV.0) +DEV_ATTR(vbnv1, VBNV.1) +DEV_ATTR(vdat, VDAT) + +static struct attribute *first_level_attrs[] = { + &dev_attr_binf2.attr, + &dev_attr_binf3.attr, + &dev_attr_chsw.attr, + &dev_attr_fmap.attr, + &dev_attr_frid.attr, + &dev_attr_fwid.attr, + &dev_attr_hwid.attr, + &dev_attr_meck.attr, + &dev_attr_vbnv0.attr, + &dev_attr_vbnv1.attr, + &dev_attr_vdat.attr, + NULL +}; + +static const struct attribute_group first_level_attr_group = { + .attrs = first_level_attrs, +}; + +/* + * Every platform can have a different number of GPIO attribute groups. + * Define upper limit groups. At run time, the platform decides to show + * the present number of groups only, others are hidden. + */ +GPIO_ATTR_GROUP(gpio0, "GPIO.0", 0) +GPIO_ATTR_GROUP(gpio1, "GPIO.1", 1) +GPIO_ATTR_GROUP(gpio2, "GPIO.2", 2) +GPIO_ATTR_GROUP(gpio3, "GPIO.3", 3) +GPIO_ATTR_GROUP(gpio4, "GPIO.4", 4) +GPIO_ATTR_GROUP(gpio5, "GPIO.5", 5) +GPIO_ATTR_GROUP(gpio6, "GPIO.6", 6) +GPIO_ATTR_GROUP(gpio7, "GPIO.7", 7) + +static const struct attribute_group *chromeos_acpi_all_groups[] = { + &first_level_attr_group, + &attr_group_gpio0, + &attr_group_gpio1, + &attr_group_gpio2, + &attr_group_gpio3, + &attr_group_gpio4, + &attr_group_gpio5, + &attr_group_gpio6, + &attr_group_gpio7, + NULL +}; + +static int chromeos_acpi_device_probe(struct platform_device *pdev) +{ + chromeos_acpi_gpio_groups = get_gpio_pkg_num(&pdev->dev); + + /* + * If the platform has more GPIO attribute groups than the number of + * groups this driver supports, give out a warning message. + */ + if (chromeos_acpi_gpio_groups > ARRAY_SIZE(chromeos_acpi_all_groups) - 2) + dev_warn(&pdev->dev, "Only %zu GPIO attr groups supported by the driver out of total %u.\n", + ARRAY_SIZE(chromeos_acpi_all_groups) - 2, chromeos_acpi_gpio_groups); + return 0; +} + +static const struct acpi_device_id chromeos_device_ids[] = { + { "GGL0001", 0 }, + { "GOOG0016", 0 }, + {} +}; +MODULE_DEVICE_TABLE(acpi, chromeos_device_ids); + +static struct platform_driver chromeos_acpi_device_driver = { + .probe = chromeos_acpi_device_probe, + .driver = { + .name = KBUILD_MODNAME, + .dev_groups = chromeos_acpi_all_groups, + .acpi_match_table = chromeos_device_ids, + } +}; +module_platform_driver(chromeos_acpi_device_driver); + +MODULE_AUTHOR("Muhammad Usama Anjum "); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS specific ACPI extensions"); diff --git a/drivers/platform/chrome/chromeos_laptop.c b/drivers/platform/chrome/chromeos_laptop.c new file mode 100644 index 000000000..c3c4cb670 --- /dev/null +++ b/drivers/platform/chrome/chromeos_laptop.c @@ -0,0 +1,956 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Driver to instantiate Chromebook i2c/smbus devices. +// +// Copyright (C) 2012 Google, Inc. +// Author: Benson Leung + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ATMEL_TP_I2C_ADDR 0x4b +#define ATMEL_TP_I2C_BL_ADDR 0x25 +#define ATMEL_TS_I2C_ADDR 0x4a +#define ATMEL_TS_I2C_BL_ADDR 0x26 +#define CYAPA_TP_I2C_ADDR 0x67 +#define ELAN_TP_I2C_ADDR 0x15 +#define ISL_ALS_I2C_ADDR 0x44 +#define TAOS_ALS_I2C_ADDR 0x29 + +static const char *i2c_adapter_names[] = { + "SMBus I801 adapter", + "i915 gmbus vga", + "i915 gmbus panel", + "Synopsys DesignWare I2C adapter", +}; + +/* Keep this enum consistent with i2c_adapter_names */ +enum i2c_adapter_type { + I2C_ADAPTER_SMBUS = 0, + I2C_ADAPTER_VGADDC, + I2C_ADAPTER_PANEL, + I2C_ADAPTER_DESIGNWARE, +}; + +struct i2c_peripheral { + struct i2c_board_info board_info; + unsigned short alt_addr; + + const char *dmi_name; + unsigned long irqflags; + struct resource irq_resource; + + enum i2c_adapter_type type; + u32 pci_devid; + + const struct property_entry *properties; + + struct i2c_client *client; +}; + +struct acpi_peripheral { + char hid[ACPI_ID_LEN]; + struct software_node swnode; + struct i2c_client *client; +}; + +struct chromeos_laptop { + /* + * Note that we can't mark this pointer as const because + * i2c_new_scanned_device() changes passed in I2C board info, so. + */ + struct i2c_peripheral *i2c_peripherals; + unsigned int num_i2c_peripherals; + + struct acpi_peripheral *acpi_peripherals; + unsigned int num_acpi_peripherals; +}; + +static const struct chromeos_laptop *cros_laptop; + +static struct i2c_client * +chromes_laptop_instantiate_i2c_device(struct i2c_adapter *adapter, + struct i2c_board_info *info, + unsigned short alt_addr) +{ + const unsigned short addr_list[] = { info->addr, I2C_CLIENT_END }; + struct i2c_client *client; + + /* + * Add the i2c device. If we can't detect it at the primary + * address we scan secondary addresses. In any case the client + * structure gets assigned primary address. + */ + client = i2c_new_scanned_device(adapter, info, addr_list, NULL); + if (IS_ERR(client) && alt_addr) { + struct i2c_board_info dummy_info = { + I2C_BOARD_INFO("dummy", info->addr), + }; + const unsigned short alt_addr_list[] = { + alt_addr, I2C_CLIENT_END + }; + struct i2c_client *dummy; + + dummy = i2c_new_scanned_device(adapter, &dummy_info, + alt_addr_list, NULL); + if (!IS_ERR(dummy)) { + pr_debug("%d-%02x is probed at %02x\n", + adapter->nr, info->addr, dummy->addr); + i2c_unregister_device(dummy); + client = i2c_new_client_device(adapter, info); + } + } + + if (IS_ERR(client)) { + client = NULL; + pr_debug("failed to register device %d-%02x\n", + adapter->nr, info->addr); + } else { + pr_debug("added i2c device %d-%02x\n", + adapter->nr, info->addr); + } + + return client; +} + +static bool chromeos_laptop_match_adapter_devid(struct device *dev, u32 devid) +{ + struct pci_dev *pdev; + + if (!dev_is_pci(dev)) + return false; + + pdev = to_pci_dev(dev); + return devid == pci_dev_id(pdev); +} + +static void chromeos_laptop_check_adapter(struct i2c_adapter *adapter) +{ + struct i2c_peripheral *i2c_dev; + int i; + + for (i = 0; i < cros_laptop->num_i2c_peripherals; i++) { + i2c_dev = &cros_laptop->i2c_peripherals[i]; + + /* Skip devices already created */ + if (i2c_dev->client) + continue; + + if (strncmp(adapter->name, i2c_adapter_names[i2c_dev->type], + strlen(i2c_adapter_names[i2c_dev->type]))) + continue; + + if (i2c_dev->pci_devid && + !chromeos_laptop_match_adapter_devid(adapter->dev.parent, + i2c_dev->pci_devid)) { + continue; + } + + i2c_dev->client = + chromes_laptop_instantiate_i2c_device(adapter, + &i2c_dev->board_info, + i2c_dev->alt_addr); + } +} + +static bool chromeos_laptop_adjust_client(struct i2c_client *client) +{ + struct acpi_peripheral *acpi_dev; + struct acpi_device_id acpi_ids[2] = { }; + int i; + int error; + + if (!has_acpi_companion(&client->dev)) + return false; + + for (i = 0; i < cros_laptop->num_acpi_peripherals; i++) { + acpi_dev = &cros_laptop->acpi_peripherals[i]; + + memcpy(acpi_ids[0].id, acpi_dev->hid, ACPI_ID_LEN); + + if (acpi_match_device(acpi_ids, &client->dev)) { + error = device_add_software_node(&client->dev, &acpi_dev->swnode); + if (error) { + dev_err(&client->dev, + "failed to add properties: %d\n", + error); + break; + } + + acpi_dev->client = client; + + return true; + } + } + + return false; +} + +static void chromeos_laptop_detach_i2c_client(struct i2c_client *client) +{ + struct acpi_peripheral *acpi_dev; + struct i2c_peripheral *i2c_dev; + int i; + + if (has_acpi_companion(&client->dev)) + for (i = 0; i < cros_laptop->num_acpi_peripherals; i++) { + acpi_dev = &cros_laptop->acpi_peripherals[i]; + + if (acpi_dev->client == client) { + acpi_dev->client = NULL; + return; + } + } + else + for (i = 0; i < cros_laptop->num_i2c_peripherals; i++) { + i2c_dev = &cros_laptop->i2c_peripherals[i]; + + if (i2c_dev->client == client) { + i2c_dev->client = NULL; + return; + } + } +} + +static int chromeos_laptop_i2c_notifier_call(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct device *dev = data; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + if (dev->type == &i2c_adapter_type) + chromeos_laptop_check_adapter(to_i2c_adapter(dev)); + else if (dev->type == &i2c_client_type) + chromeos_laptop_adjust_client(to_i2c_client(dev)); + break; + + case BUS_NOTIFY_REMOVED_DEVICE: + if (dev->type == &i2c_client_type) + chromeos_laptop_detach_i2c_client(to_i2c_client(dev)); + break; + } + + return 0; +} + +static struct notifier_block chromeos_laptop_i2c_notifier = { + .notifier_call = chromeos_laptop_i2c_notifier_call, +}; + +#define DECLARE_CROS_LAPTOP(_name) \ +static const struct chromeos_laptop _name __initconst = { \ + .i2c_peripherals = _name##_peripherals, \ + .num_i2c_peripherals = ARRAY_SIZE(_name##_peripherals), \ +} + +#define DECLARE_ACPI_CROS_LAPTOP(_name) \ +static const struct chromeos_laptop _name __initconst = { \ + .acpi_peripherals = _name##_peripherals, \ + .num_acpi_peripherals = ARRAY_SIZE(_name##_peripherals), \ +} + +static struct i2c_peripheral samsung_series_5_550_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_SMBUS, + }, + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("isl29018", ISL_ALS_I2C_ADDR), + }, + .dmi_name = "lightsensor", + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(samsung_series_5_550); + +static struct i2c_peripheral samsung_series_5_peripherals[] __initdata = { + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("tsl2583", TAOS_ALS_I2C_ADDR), + }, + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(samsung_series_5); + +static const int chromebook_pixel_tp_keys[] __initconst = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + BTN_LEFT +}; + +static const struct property_entry +chromebook_pixel_trackpad_props[] __initconst = { + PROPERTY_ENTRY_STRING("compatible", "atmel,maxtouch"), + PROPERTY_ENTRY_U32_ARRAY("linux,gpio-keymap", chromebook_pixel_tp_keys), + { } +}; + +static const struct property_entry +chromebook_atmel_touchscreen_props[] __initconst = { + PROPERTY_ENTRY_STRING("compatible", "atmel,maxtouch"), + { } +}; + +static struct i2c_peripheral chromebook_pixel_peripherals[] __initdata = { + /* Touch Screen. */ + { + .board_info = { + I2C_BOARD_INFO("atmel_mxt_ts", + ATMEL_TS_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "touchscreen", + .irqflags = IRQF_TRIGGER_FALLING, + .type = I2C_ADAPTER_PANEL, + .alt_addr = ATMEL_TS_I2C_BL_ADDR, + .properties = chromebook_atmel_touchscreen_props, + }, + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("atmel_mxt_tp", + ATMEL_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .irqflags = IRQF_TRIGGER_FALLING, + .type = I2C_ADAPTER_VGADDC, + .alt_addr = ATMEL_TP_I2C_BL_ADDR, + .properties = chromebook_pixel_trackpad_props, + }, + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("isl29018", ISL_ALS_I2C_ADDR), + }, + .dmi_name = "lightsensor", + .type = I2C_ADAPTER_PANEL, + }, +}; +DECLARE_CROS_LAPTOP(chromebook_pixel); + +static struct i2c_peripheral hp_chromebook_14_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + }, +}; +DECLARE_CROS_LAPTOP(hp_chromebook_14); + +static struct i2c_peripheral dell_chromebook_11_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + }, + /* Elan Touchpad option. */ + { + .board_info = { + I2C_BOARD_INFO("elan_i2c", ELAN_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + }, +}; +DECLARE_CROS_LAPTOP(dell_chromebook_11); + +static struct i2c_peripheral toshiba_cb35_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + }, +}; +DECLARE_CROS_LAPTOP(toshiba_cb35); + +static struct i2c_peripheral acer_c7_chromebook_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(acer_c7_chromebook); + +static struct i2c_peripheral acer_ac700_peripherals[] __initdata = { + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("tsl2583", TAOS_ALS_I2C_ADDR), + }, + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(acer_ac700); + +static struct i2c_peripheral acer_c720_peripherals[] __initdata = { + /* Touchscreen. */ + { + .board_info = { + I2C_BOARD_INFO("atmel_mxt_ts", + ATMEL_TS_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "touchscreen", + .irqflags = IRQF_TRIGGER_FALLING, + .type = I2C_ADAPTER_DESIGNWARE, + .pci_devid = PCI_DEVID(0, PCI_DEVFN(0x15, 0x2)), + .alt_addr = ATMEL_TS_I2C_BL_ADDR, + .properties = chromebook_atmel_touchscreen_props, + }, + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + .pci_devid = PCI_DEVID(0, PCI_DEVFN(0x15, 0x1)), + }, + /* Elan Touchpad option. */ + { + .board_info = { + I2C_BOARD_INFO("elan_i2c", ELAN_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_DESIGNWARE, + .pci_devid = PCI_DEVID(0, PCI_DEVFN(0x15, 0x1)), + }, + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("isl29018", ISL_ALS_I2C_ADDR), + }, + .dmi_name = "lightsensor", + .type = I2C_ADAPTER_DESIGNWARE, + .pci_devid = PCI_DEVID(0, PCI_DEVFN(0x15, 0x2)), + }, +}; +DECLARE_CROS_LAPTOP(acer_c720); + +static struct i2c_peripheral +hp_pavilion_14_chromebook_peripherals[] __initdata = { + /* Touchpad. */ + { + .board_info = { + I2C_BOARD_INFO("cyapa", CYAPA_TP_I2C_ADDR), + .flags = I2C_CLIENT_WAKE, + }, + .dmi_name = "trackpad", + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(hp_pavilion_14_chromebook); + +static struct i2c_peripheral cr48_peripherals[] __initdata = { + /* Light Sensor. */ + { + .board_info = { + I2C_BOARD_INFO("tsl2563", TAOS_ALS_I2C_ADDR), + }, + .type = I2C_ADAPTER_SMBUS, + }, +}; +DECLARE_CROS_LAPTOP(cr48); + +static const u32 samus_touchpad_buttons[] __initconst = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + BTN_LEFT +}; + +static const struct property_entry samus_trackpad_props[] __initconst = { + PROPERTY_ENTRY_STRING("compatible", "atmel,maxtouch"), + PROPERTY_ENTRY_U32_ARRAY("linux,gpio-keymap", samus_touchpad_buttons), + { } +}; + +static struct acpi_peripheral samus_peripherals[] __initdata = { + /* Touchpad */ + { + .hid = "ATML0000", + .swnode = { + .properties = samus_trackpad_props, + }, + }, + /* Touchsceen */ + { + .hid = "ATML0001", + .swnode = { + .properties = chromebook_atmel_touchscreen_props, + }, + }, +}; +DECLARE_ACPI_CROS_LAPTOP(samus); + +static struct acpi_peripheral generic_atmel_peripherals[] __initdata = { + /* Touchpad */ + { + .hid = "ATML0000", + .swnode = { + .properties = chromebook_pixel_trackpad_props, + }, + }, + /* Touchsceen */ + { + .hid = "ATML0001", + .swnode = { + .properties = chromebook_atmel_touchscreen_props, + }, + }, +}; +DECLARE_ACPI_CROS_LAPTOP(generic_atmel); + +static const struct dmi_system_id chromeos_laptop_dmi_table[] __initconst = { + { + .ident = "Samsung Series 5 550", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG"), + DMI_MATCH(DMI_PRODUCT_NAME, "Lumpy"), + }, + .driver_data = (void *)&samsung_series_5_550, + }, + { + .ident = "Samsung Series 5", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Alex"), + }, + .driver_data = (void *)&samsung_series_5, + }, + { + .ident = "Chromebook Pixel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Link"), + }, + .driver_data = (void *)&chromebook_pixel, + }, + { + .ident = "Wolf", + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_PRODUCT_NAME, "Wolf"), + }, + .driver_data = (void *)&dell_chromebook_11, + }, + { + .ident = "HP Chromebook 14", + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_PRODUCT_NAME, "Falco"), + }, + .driver_data = (void *)&hp_chromebook_14, + }, + { + .ident = "Toshiba CB35", + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_PRODUCT_NAME, "Leon"), + }, + .driver_data = (void *)&toshiba_cb35, + }, + { + .ident = "Acer C7 Chromebook", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Parrot"), + }, + .driver_data = (void *)&acer_c7_chromebook, + }, + { + .ident = "Acer AC700", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "ZGB"), + }, + .driver_data = (void *)&acer_ac700, + }, + { + .ident = "Acer C720", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Peppy"), + }, + .driver_data = (void *)&acer_c720, + }, + { + .ident = "HP Pavilion 14 Chromebook", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Butterfly"), + }, + .driver_data = (void *)&hp_pavilion_14_chromebook, + }, + { + .ident = "Cr-48", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Mario"), + }, + .driver_data = (void *)&cr48, + }, + /* Devices with peripherals incompletely described in ACPI */ + { + .ident = "Chromebook Pro", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Google"), + DMI_MATCH(DMI_PRODUCT_NAME, "Caroline"), + }, + .driver_data = (void *)&samus, + }, + { + .ident = "Google Pixel 2 (2015)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Samus"), + }, + .driver_data = (void *)&samus, + }, + { + .ident = "Samsung Chromebook 3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Celes"), + }, + .driver_data = (void *)&samus, + }, + { + /* + * Other Chromebooks with Atmel touch controllers: + * - Winky (touchpad) + * - Clapper, Expresso, Rambi, Glimmer (touchscreen) + */ + .ident = "Other Chromebook", + .matches = { + /* + * This will match all Google devices, not only devices + * with Atmel, but we will validate that the device + * actually has matching peripherals. + */ + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + }, + .driver_data = (void *)&generic_atmel, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, chromeos_laptop_dmi_table); + +static int __init chromeos_laptop_scan_peripherals(struct device *dev, void *data) +{ + int error; + + if (dev->type == &i2c_adapter_type) { + chromeos_laptop_check_adapter(to_i2c_adapter(dev)); + } else if (dev->type == &i2c_client_type) { + if (chromeos_laptop_adjust_client(to_i2c_client(dev))) { + /* + * Now that we have needed properties re-trigger + * driver probe in case driver was initialized + * earlier and probe failed. + */ + error = device_attach(dev); + if (error < 0) + dev_warn(dev, + "%s: device_attach() failed: %d\n", + __func__, error); + } + } + + return 0; +} + +static int __init chromeos_laptop_get_irq_from_dmi(const char *dmi_name) +{ + const struct dmi_device *dmi_dev; + const struct dmi_dev_onboard *dev_data; + + dmi_dev = dmi_find_device(DMI_DEV_TYPE_DEV_ONBOARD, dmi_name, NULL); + if (!dmi_dev) { + pr_err("failed to find DMI device '%s'\n", dmi_name); + return -ENOENT; + } + + dev_data = dmi_dev->device_data; + if (!dev_data) { + pr_err("failed to get data from DMI for '%s'\n", dmi_name); + return -EINVAL; + } + + return dev_data->instance; +} + +static int __init chromeos_laptop_setup_irq(struct i2c_peripheral *i2c_dev) +{ + int irq; + + if (i2c_dev->dmi_name) { + irq = chromeos_laptop_get_irq_from_dmi(i2c_dev->dmi_name); + if (irq < 0) + return irq; + + i2c_dev->irq_resource = DEFINE_RES_IRQ(irq); + i2c_dev->irq_resource.flags |= i2c_dev->irqflags; + + i2c_dev->board_info.resources = &i2c_dev->irq_resource; + i2c_dev->board_info.num_resources = 1; + } + + return 0; +} + +static int __init +chromeos_laptop_prepare_i2c_peripherals(struct chromeos_laptop *cros_laptop, + const struct chromeos_laptop *src) +{ + struct i2c_peripheral *i2c_peripherals; + struct i2c_peripheral *i2c_dev; + struct i2c_board_info *info; + int i; + int error; + + if (!src->num_i2c_peripherals) + return 0; + + i2c_peripherals = kmemdup_array(src->i2c_peripherals, + src->num_i2c_peripherals, + sizeof(*i2c_peripherals), GFP_KERNEL); + if (!i2c_peripherals) + return -ENOMEM; + + for (i = 0; i < src->num_i2c_peripherals; i++) { + i2c_dev = &i2c_peripherals[i]; + info = &i2c_dev->board_info; + + error = chromeos_laptop_setup_irq(i2c_dev); + if (error) + goto err_out; + + /* Create primary fwnode for the device - copies everything */ + if (i2c_dev->properties) { + info->fwnode = fwnode_create_software_node(i2c_dev->properties, NULL); + if (IS_ERR(info->fwnode)) { + error = PTR_ERR(info->fwnode); + goto err_out; + } + } + } + + cros_laptop->i2c_peripherals = i2c_peripherals; + cros_laptop->num_i2c_peripherals = src->num_i2c_peripherals; + + return 0; + +err_out: + while (--i >= 0) { + i2c_dev = &i2c_peripherals[i]; + info = &i2c_dev->board_info; + fwnode_remove_software_node(info->fwnode); + } + kfree(i2c_peripherals); + return error; +} + +static int __init +chromeos_laptop_prepare_acpi_peripherals(struct chromeos_laptop *cros_laptop, + const struct chromeos_laptop *src) +{ + struct acpi_peripheral *acpi_peripherals; + struct acpi_peripheral *acpi_dev; + const struct acpi_peripheral *src_dev; + int n_peripherals = 0; + int i; + int error; + + for (i = 0; i < src->num_acpi_peripherals; i++) { + if (acpi_dev_present(src->acpi_peripherals[i].hid, NULL, -1)) + n_peripherals++; + } + + if (!n_peripherals) + return 0; + + acpi_peripherals = kzalloc_objs(*src->acpi_peripherals, n_peripherals); + if (!acpi_peripherals) + return -ENOMEM; + + acpi_dev = acpi_peripherals; + for (i = 0; i < src->num_acpi_peripherals; i++) { + src_dev = &src->acpi_peripherals[i]; + if (!acpi_dev_present(src_dev->hid, NULL, -1)) + continue; + + *acpi_dev = *src_dev; + + /* We need to deep-copy properties */ + if (src_dev->swnode.properties) { + acpi_dev->swnode.properties = + property_entries_dup(src_dev->swnode.properties); + if (IS_ERR(acpi_dev->swnode.properties)) { + error = PTR_ERR(acpi_dev->swnode.properties); + goto err_out; + } + } + + acpi_dev++; + } + + cros_laptop->acpi_peripherals = acpi_peripherals; + cros_laptop->num_acpi_peripherals = n_peripherals; + + return 0; + +err_out: + while (--i >= 0) { + acpi_dev = &acpi_peripherals[i]; + if (!IS_ERR_OR_NULL(acpi_dev->swnode.properties)) + property_entries_free(acpi_dev->swnode.properties); + } + + kfree(acpi_peripherals); + return error; +} + +static void chromeos_laptop_destroy(const struct chromeos_laptop *cros_laptop) +{ + const struct acpi_peripheral *acpi_dev; + struct i2c_peripheral *i2c_dev; + int i; + + for (i = 0; i < cros_laptop->num_i2c_peripherals; i++) { + i2c_dev = &cros_laptop->i2c_peripherals[i]; + i2c_unregister_device(i2c_dev->client); + } + + for (i = 0; i < cros_laptop->num_acpi_peripherals; i++) { + acpi_dev = &cros_laptop->acpi_peripherals[i]; + + if (acpi_dev->client) + device_remove_software_node(&acpi_dev->client->dev); + + property_entries_free(acpi_dev->swnode.properties); + } + + kfree(cros_laptop->i2c_peripherals); + kfree(cros_laptop->acpi_peripherals); + kfree(cros_laptop); +} + +static struct chromeos_laptop * __init +chromeos_laptop_prepare(const struct chromeos_laptop *src) +{ + struct chromeos_laptop *cros_laptop; + int error; + + cros_laptop = kzalloc_obj(*cros_laptop); + if (!cros_laptop) + return ERR_PTR(-ENOMEM); + + error = chromeos_laptop_prepare_i2c_peripherals(cros_laptop, src); + if (!error) + error = chromeos_laptop_prepare_acpi_peripherals(cros_laptop, + src); + + if (error) { + chromeos_laptop_destroy(cros_laptop); + return ERR_PTR(error); + } + + return cros_laptop; +} + +static int __init chromeos_laptop_init(void) +{ + const struct dmi_system_id *dmi_id; + int error; + + dmi_id = dmi_first_match(chromeos_laptop_dmi_table); + if (!dmi_id) { + pr_debug("unsupported system\n"); + return -ENODEV; + } + + pr_debug("DMI Matched %s\n", dmi_id->ident); + + cros_laptop = chromeos_laptop_prepare((void *)dmi_id->driver_data); + if (IS_ERR(cros_laptop)) + return PTR_ERR(cros_laptop); + + if (!cros_laptop->num_i2c_peripherals && + !cros_laptop->num_acpi_peripherals) { + pr_debug("no relevant devices detected\n"); + error = -ENODEV; + goto err_destroy_cros_laptop; + } + + error = bus_register_notifier(&i2c_bus_type, + &chromeos_laptop_i2c_notifier); + if (error) { + pr_err("failed to register i2c bus notifier: %d\n", + error); + goto err_destroy_cros_laptop; + } + + /* + * Scan adapters that have been registered and clients that have + * been created before we installed the notifier to make sure + * we do not miss any devices. + */ + i2c_for_each_dev(NULL, chromeos_laptop_scan_peripherals); + + return 0; + +err_destroy_cros_laptop: + chromeos_laptop_destroy(cros_laptop); + return error; +} + +static void __exit chromeos_laptop_exit(void) +{ + bus_unregister_notifier(&i2c_bus_type, &chromeos_laptop_i2c_notifier); + chromeos_laptop_destroy(cros_laptop); +} + +module_init(chromeos_laptop_init); +module_exit(chromeos_laptop_exit); + +MODULE_DESCRIPTION("Chrome OS Laptop driver"); +MODULE_AUTHOR("Benson Leung "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/chromeos_of_hw_prober.c b/drivers/platform/chrome/chromeos_of_hw_prober.c new file mode 100644 index 000000000..54d894161 --- /dev/null +++ b/drivers/platform/chrome/chromeos_of_hw_prober.c @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ChromeOS Device Tree Hardware Prober + * + * Copyright (c) 2024 Google LLC + */ + +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "chromeos_of_hw_prober" + +/** + * struct hw_prober_entry - Holds an entry for the hardware prober + * + * @compatible: compatible string to match against the machine + * @prober: prober function to call when machine matches + * @data: extra data for the prober function + */ +struct hw_prober_entry { + const char *compatible; + int (*prober)(struct device *dev, const void *data); + const void *data; +}; + +struct chromeos_i2c_probe_data { + const struct i2c_of_probe_cfg *cfg; + const struct i2c_of_probe_simple_opts *opts; +}; + +static int chromeos_i2c_component_prober(struct device *dev, const void *_data) +{ + const struct chromeos_i2c_probe_data *data = _data; + struct i2c_of_probe_simple_ctx ctx = { + .opts = data->opts, + }; + + return i2c_of_probe_component(dev, data->cfg, &ctx); +} + +#define DEFINE_CHROMEOS_I2C_PROBE_CFG_SIMPLE_BY_TYPE(_type) \ + static const struct i2c_of_probe_cfg chromeos_i2c_probe_simple_ ## _type ## _cfg = { \ + .type = #_type, \ + .ops = &i2c_of_probe_simple_ops, \ + } + +#define DEFINE_CHROMEOS_I2C_PROBE_DATA_DUMB_BY_TYPE(_type) \ + static const struct chromeos_i2c_probe_data chromeos_i2c_probe_dumb_ ## _type = { \ + .cfg = &(const struct i2c_of_probe_cfg) { \ + .type = #_type, \ + }, \ + } + +DEFINE_CHROMEOS_I2C_PROBE_DATA_DUMB_BY_TYPE(touchscreen); +DEFINE_CHROMEOS_I2C_PROBE_DATA_DUMB_BY_TYPE(trackpad); + +DEFINE_CHROMEOS_I2C_PROBE_CFG_SIMPLE_BY_TYPE(touchscreen); +DEFINE_CHROMEOS_I2C_PROBE_CFG_SIMPLE_BY_TYPE(trackpad); + +static const struct chromeos_i2c_probe_data chromeos_i2c_probe_hana_trackpad = { + .cfg = &chromeos_i2c_probe_simple_trackpad_cfg, + .opts = &(const struct i2c_of_probe_simple_opts) { + .res_node_compatible = "elan,ekth3000", + .supply_name = "vcc", + /* + * ELAN trackpad needs 2 ms for H/W init and 100 ms for F/W init. + * Synaptics trackpad needs 100 ms. + */ + .post_power_on_delay_ms = 110, + }, +}; + +static const struct chromeos_i2c_probe_data chromeos_i2c_probe_squirtle_touchscreen = { + .cfg = &chromeos_i2c_probe_simple_touchscreen_cfg, + .opts = &(const struct i2c_of_probe_simple_opts) { + .res_node_compatible = "elan,ekth6a12nay", + .supply_name = "vcc33", + .gpio_name = "reset", + .post_power_on_delay_ms = 10, + .post_gpio_config_delay_ms = 300, + }, +}; + +static const struct hw_prober_entry hw_prober_platforms[] = { + { + .compatible = "google,hana", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_dumb_touchscreen, + }, { + .compatible = "google,hana", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_hana_trackpad, + }, { + .compatible = "google,spherion", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_dumb_trackpad, + }, { + .compatible = "google,squirtle", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_dumb_trackpad, + }, { + .compatible = "google,squirtle", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_squirtle_touchscreen, + }, { + .compatible = "google,steelix", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_dumb_trackpad, + }, { + .compatible = "google,voltorb", + .prober = chromeos_i2c_component_prober, + .data = &chromeos_i2c_probe_dumb_trackpad, + }, +}; + +static int chromeos_of_hw_prober_probe(struct platform_device *pdev) +{ + for (size_t i = 0; i < ARRAY_SIZE(hw_prober_platforms); i++) { + int ret; + + if (!of_machine_is_compatible(hw_prober_platforms[i].compatible)) + continue; + + ret = hw_prober_platforms[i].prober(&pdev->dev, hw_prober_platforms[i].data); + /* Ignore unrecoverable errors and keep going through other probers */ + if (ret == -EPROBE_DEFER) + return ret; + } + + return 0; +} + +static struct platform_driver chromeos_of_hw_prober_driver = { + .probe = chromeos_of_hw_prober_probe, + .driver = { + .name = DRV_NAME, + }, +}; + +static struct platform_device *chromeos_of_hw_prober_pdev; + +static int chromeos_of_hw_prober_driver_init(void) +{ + size_t i; + int ret; + + for (i = 0; i < ARRAY_SIZE(hw_prober_platforms); i++) + if (of_machine_is_compatible(hw_prober_platforms[i].compatible)) + break; + if (i == ARRAY_SIZE(hw_prober_platforms)) + return -ENODEV; + + ret = platform_driver_register(&chromeos_of_hw_prober_driver); + if (ret) + return ret; + + chromeos_of_hw_prober_pdev = + platform_device_register_simple(DRV_NAME, PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(chromeos_of_hw_prober_pdev)) + goto err; + + return 0; + +err: + platform_driver_unregister(&chromeos_of_hw_prober_driver); + + return PTR_ERR(chromeos_of_hw_prober_pdev); +} +module_init(chromeos_of_hw_prober_driver_init); + +static void chromeos_of_hw_prober_driver_exit(void) +{ + platform_device_unregister(chromeos_of_hw_prober_pdev); + platform_driver_unregister(&chromeos_of_hw_prober_driver); +} +module_exit(chromeos_of_hw_prober_driver_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS device tree hardware prober"); +MODULE_IMPORT_NS("I2C_OF_PROBER"); diff --git a/drivers/platform/chrome/chromeos_privacy_screen.c b/drivers/platform/chrome/chromeos_privacy_screen.c new file mode 100644 index 000000000..407b04207 --- /dev/null +++ b/drivers/platform/chrome/chromeos_privacy_screen.c @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * ChromeOS Privacy Screen support + * + * Copyright (C) 2022 Google LLC + * + * This is the Chromeos privacy screen provider, present on certain chromebooks, + * represented by a GOOG0010 device in the ACPI. This ACPI device, if present, + * will cause the i915 drm driver to probe defer until this driver registers + * the privacy-screen. + */ + +#include +#include +#include + +/* + * The DSM (Device Specific Method) constants below are the agreed API with + * the firmware team, on how to control privacy screen using ACPI methods. + */ +#define PRIV_SCRN_DSM_REVID 1 /* DSM version */ +#define PRIV_SCRN_DSM_FN_GET_STATUS 1 /* Get privacy screen status */ +#define PRIV_SCRN_DSM_FN_ENABLE 2 /* Enable privacy screen */ +#define PRIV_SCRN_DSM_FN_DISABLE 3 /* Disable privacy screen */ + +static const guid_t chromeos_privacy_screen_dsm_guid = + GUID_INIT(0xc7033113, 0x8720, 0x4ceb, + 0x90, 0x90, 0x9d, 0x52, 0xb3, 0xe5, 0x2d, 0x73); + +static void +chromeos_privacy_screen_get_hw_state(struct drm_privacy_screen + *drm_privacy_screen) +{ + union acpi_object *obj; + struct device *privacy_screen = + drm_privacy_screen_get_drvdata(drm_privacy_screen); + acpi_handle handle = ACPI_HANDLE(privacy_screen); + + obj = acpi_evaluate_dsm(handle, &chromeos_privacy_screen_dsm_guid, + PRIV_SCRN_DSM_REVID, + PRIV_SCRN_DSM_FN_GET_STATUS, NULL); + if (!obj) { + dev_err(privacy_screen, + "_DSM failed to get privacy-screen state\n"); + return; + } + + if (obj->type != ACPI_TYPE_INTEGER) + dev_err(privacy_screen, + "Bad _DSM to get privacy-screen state\n"); + else if (obj->integer.value == 1) + drm_privacy_screen->hw_state = drm_privacy_screen->sw_state = + PRIVACY_SCREEN_ENABLED; + else + drm_privacy_screen->hw_state = drm_privacy_screen->sw_state = + PRIVACY_SCREEN_DISABLED; + + ACPI_FREE(obj); +} + +static int +chromeos_privacy_screen_set_sw_state(struct drm_privacy_screen + *drm_privacy_screen, + enum drm_privacy_screen_status state) +{ + union acpi_object *obj = NULL; + struct device *privacy_screen = + drm_privacy_screen_get_drvdata(drm_privacy_screen); + acpi_handle handle = ACPI_HANDLE(privacy_screen); + + if (state == PRIVACY_SCREEN_DISABLED) { + obj = acpi_evaluate_dsm(handle, + &chromeos_privacy_screen_dsm_guid, + PRIV_SCRN_DSM_REVID, + PRIV_SCRN_DSM_FN_DISABLE, NULL); + } else if (state == PRIVACY_SCREEN_ENABLED) { + obj = acpi_evaluate_dsm(handle, + &chromeos_privacy_screen_dsm_guid, + PRIV_SCRN_DSM_REVID, + PRIV_SCRN_DSM_FN_ENABLE, NULL); + } else { + dev_err(privacy_screen, + "Bad attempt to set privacy-screen status to %u\n", + state); + return -EINVAL; + } + + if (!obj) { + dev_err(privacy_screen, + "_DSM failed to set privacy-screen state\n"); + return -EIO; + } + + drm_privacy_screen->hw_state = drm_privacy_screen->sw_state = state; + ACPI_FREE(obj); + return 0; +} + +static const struct drm_privacy_screen_ops chromeos_privacy_screen_ops = { + .get_hw_state = chromeos_privacy_screen_get_hw_state, + .set_sw_state = chromeos_privacy_screen_set_sw_state, +}; + +static int chromeos_privacy_screen_probe(struct platform_device *pdev) +{ + if (!ACPI_COMPANION(&pdev->dev)) + return -ENODEV; + + struct drm_privacy_screen *drm_privacy_screen = + drm_privacy_screen_register(&pdev->dev, + &chromeos_privacy_screen_ops, + &pdev->dev); + + if (IS_ERR(drm_privacy_screen)) { + dev_err(&pdev->dev, "Error registering privacy-screen\n"); + return PTR_ERR(drm_privacy_screen); + } + + platform_set_drvdata(pdev, drm_privacy_screen); + dev_info(&pdev->dev, "registered privacy-screen '%s'\n", + dev_name(&drm_privacy_screen->dev)); + + return 0; +} + +static void chromeos_privacy_screen_remove(struct platform_device *pdev) +{ + drm_privacy_screen_unregister(platform_get_drvdata(pdev)); +} + +static const struct acpi_device_id chromeos_privacy_screen_device_ids[] = { + {"GOOG0010", 0}, /* Google's electronic privacy screen for eDP-1 */ + {} +}; +MODULE_DEVICE_TABLE(acpi, chromeos_privacy_screen_device_ids); + +static struct platform_driver chromeos_privacy_screen_driver = { + .probe = chromeos_privacy_screen_probe, + .remove = chromeos_privacy_screen_remove, + .driver = { + .name = "chromeos_privacy_screen_driver", + .acpi_match_table = chromeos_privacy_screen_device_ids, + }, +}; + +module_platform_driver(chromeos_privacy_screen_driver); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ChromeOS ACPI Privacy Screen driver"); +MODULE_AUTHOR("Rajat Jain "); diff --git a/drivers/platform/chrome/chromeos_pstore.c b/drivers/platform/chrome/chromeos_pstore.c new file mode 100644 index 000000000..a6eed9950 --- /dev/null +++ b/drivers/platform/chrome/chromeos_pstore.c @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0 +// Driver to instantiate Chromebook ramoops device. +// +// Copyright (C) 2013 Google, Inc. + +#include +#include +#include +#include +#include + +static int ecc_size; +module_param(ecc_size, int, 0400); +MODULE_PARM_DESC(ecc_size, "ECC parity data size in bytes. A positive value enables ECC for the ramoops region."); + +static const struct dmi_system_id chromeos_pstore_dmi_table[] __initconst = { + { + /* + * Today all Chromebooks/boxes ship with Google_* as version and + * coreboot as bios vendor. No other systems with this + * combination are known to date. + */ + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_BIOS_VERSION, "Google_"), + }, + }, + { + /* x86-alex, the first Samsung Chromebook. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_PRODUCT_NAME, "Alex"), + }, + }, + { + /* x86-mario, the Cr-48 pilot device from Google. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "IEC"), + DMI_MATCH(DMI_PRODUCT_NAME, "Mario"), + }, + }, + { + /* x86-zgb, the first Acer Chromebook. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ACER"), + DMI_MATCH(DMI_PRODUCT_NAME, "ZGB"), + }, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, chromeos_pstore_dmi_table); + +/* + * On x86 chromebooks/boxes, the firmware will keep the legacy VGA memory + * range untouched across reboots, so we use that to store our pstore + * contents for panic logs, etc. + */ +static struct ramoops_platform_data chromeos_ramoops_data = { + .mem_size = 0x100000, + .mem_address = 0xf00000, + .record_size = 0x40000, + .console_size = 0x20000, + .ftrace_size = 0x20000, + .pmsg_size = 0x20000, + .max_reason = KMSG_DUMP_OOPS, +}; + +static struct platform_device chromeos_ramoops = { + .name = "ramoops", + .dev = { + .platform_data = &chromeos_ramoops_data, + }, +}; + +#ifdef CONFIG_ACPI +static const struct acpi_device_id cros_ramoops_acpi_match[] = { + { "GOOG9999", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, cros_ramoops_acpi_match); + +static struct platform_driver chromeos_ramoops_acpi = { + .driver = { + .name = "chromeos_pstore", + .acpi_match_table = ACPI_PTR(cros_ramoops_acpi_match), + }, +}; + +static int __init chromeos_probe_acpi(struct platform_device *pdev) +{ + struct resource *res; + resource_size_t len; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENOMEM; + + len = resource_size(res); + if (!res->start || !len) + return -ENOMEM; + + pr_info("chromeos ramoops using acpi device.\n"); + + chromeos_ramoops_data.mem_size = len; + chromeos_ramoops_data.mem_address = res->start; + + return 0; +} + +static bool __init chromeos_check_acpi(void) +{ + if (!platform_driver_probe(&chromeos_ramoops_acpi, chromeos_probe_acpi)) + return true; + return false; +} +#else +static inline bool chromeos_check_acpi(void) { return false; } +#endif + +static int __init chromeos_pstore_init(void) +{ + bool acpi_dev_found; + + if (ecc_size > 0) + chromeos_ramoops_data.ecc_info.ecc_size = ecc_size; + + /* First check ACPI for non-hardcoded values from firmware. */ + acpi_dev_found = chromeos_check_acpi(); + + if (acpi_dev_found || dmi_check_system(chromeos_pstore_dmi_table)) + return platform_device_register(&chromeos_ramoops); + + return -ENODEV; +} + +static void __exit chromeos_pstore_exit(void) +{ + platform_device_unregister(&chromeos_ramoops); +} + +module_init(chromeos_pstore_init); +module_exit(chromeos_pstore_exit); + +MODULE_DESCRIPTION("ChromeOS pstore module"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/chrome/chromeos_tbmc.c b/drivers/platform/chrome/chromeos_tbmc.c new file mode 100644 index 000000000..fd756761a --- /dev/null +++ b/drivers/platform/chrome/chromeos_tbmc.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0 +// Driver to detect Tablet Mode for ChromeOS convertible. +// +// Copyright (C) 2017 Google, Inc. +// Author: Gwendal Grignou +// +// On Chromebook using ACPI, this device listens for notification +// from GOOG0006 and issue method TBMC to retrieve the status. +// +// GOOG0006 issues the notification when it receives EC_HOST_EVENT_MODE_CHANGE +// from the EC. +// Method TBMC reads EC_ACPI_MEM_DEVICE_ORIENTATION byte from the shared +// memory region. + +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "chromeos_tbmc" +#define ACPI_DRV_NAME "GOOG0006" + +static int chromeos_tbmc_query_switch(struct acpi_device *adev, + struct input_dev *idev) +{ + unsigned long long state; + acpi_status status; + + status = acpi_evaluate_integer(adev->handle, "TBMC", NULL, &state); + if (ACPI_FAILURE(status)) + return -ENODEV; + + /* input layer checks if event is redundant */ + input_report_switch(idev, SW_TABLET_MODE, state); + input_sync(idev); + + return 0; +} + +static __maybe_unused int chromeos_tbmc_resume(struct device *dev) +{ + return chromeos_tbmc_query_switch(ACPI_COMPANION(dev), dev_get_drvdata(dev)); +} + +static void chromeos_tbmc_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + + acpi_pm_wakeup_event(dev); + switch (event) { + case 0x80: + chromeos_tbmc_query_switch(ACPI_COMPANION(dev), dev_get_drvdata(dev)); + break; + default: + dev_err(dev, "Unexpected event: 0x%08X\n", event); + } +} + +static int chromeos_tbmc_open(struct input_dev *idev) +{ + struct acpi_device *adev = input_get_drvdata(idev); + + return chromeos_tbmc_query_switch(adev, idev); +} + +static int chromeos_tbmc_probe(struct platform_device *pdev) +{ + struct input_dev *idev; + struct device *dev = &pdev->dev; + struct acpi_device *adev; + int ret; + + adev = ACPI_COMPANION(dev); + if (!adev) + return -ENODEV; + + idev = devm_input_allocate_device(dev); + if (!idev) + return -ENOMEM; + + idev->name = "Tablet Mode Switch"; + idev->phys = acpi_device_hid(adev); + + idev->id.bustype = BUS_HOST; + idev->id.version = 1; + idev->id.product = 0; + idev->open = chromeos_tbmc_open; + + input_set_drvdata(idev, adev); + platform_set_drvdata(pdev, idev); + + input_set_capability(idev, EV_SW, SW_TABLET_MODE); + ret = input_register_device(idev); + if (ret) { + dev_err(dev, "cannot register input device\n"); + return ret; + } + device_init_wakeup(dev, true); + + ret = acpi_dev_install_notify_handler(adev, ACPI_DEVICE_NOTIFY, + chromeos_tbmc_notify, dev); + if (ret) { + dev_err(dev, "cannot install ACPI notify handler\n"); + device_init_wakeup(dev, false); + return ret; + } + + return 0; +} + +static void chromeos_tbmc_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, chromeos_tbmc_notify); + device_init_wakeup(&pdev->dev, false); +} + +static const struct acpi_device_id chromeos_tbmc_acpi_device_ids[] = { + { ACPI_DRV_NAME, 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, chromeos_tbmc_acpi_device_ids); + +static SIMPLE_DEV_PM_OPS(chromeos_tbmc_pm_ops, NULL, + chromeos_tbmc_resume); + +static struct platform_driver chromeos_tbmc_driver = { + .probe = chromeos_tbmc_probe, + .remove = chromeos_tbmc_remove, + .driver = { + .name = DRV_NAME, + .acpi_match_table = chromeos_tbmc_acpi_device_ids, + .pm = &chromeos_tbmc_pm_ops, + }, +}; + +module_platform_driver(chromeos_tbmc_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ChromeOS ACPI tablet switch driver"); diff --git a/drivers/platform/chrome/cros_ec.c b/drivers/platform/chrome/cros_ec.c new file mode 100644 index 000000000..5d0f5a918 --- /dev/null +++ b/drivers/platform/chrome/cros_ec.c @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ChromeOS EC multi-function device + * + * Copyright (C) 2012 Google, Inc + * + * The ChromeOS EC multi function device is used to mux all the requests + * to the EC device for its multiple features: keyboard controller, + * battery charging and regulator control, firmware update. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +static struct cros_ec_platform ec_p = { + .ec_name = CROS_EC_DEV_NAME, + .cmd_offset = EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_EC_INDEX), +}; + +static struct cros_ec_platform pd_p = { + .ec_name = CROS_EC_DEV_PD_NAME, + .cmd_offset = EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX), +}; + +static void cros_ec_device_free(void *data) +{ + struct cros_ec_device *ec_dev = data; + + mutex_destroy(&ec_dev->lock); + lockdep_unregister_key(&ec_dev->lockdep_key); +} + +struct cros_ec_device *cros_ec_device_alloc(struct device *dev) +{ + struct cros_ec_device *ec_dev; + + ec_dev = devm_kzalloc(dev, sizeof(*ec_dev), GFP_KERNEL); + if (!ec_dev) + return NULL; + + ec_dev->din_size = sizeof(struct ec_host_response) + + sizeof(struct ec_response_get_protocol_info) + + EC_MAX_RESPONSE_OVERHEAD; + ec_dev->dout_size = sizeof(struct ec_host_request) + + sizeof(struct ec_params_rwsig_action) + + EC_MAX_REQUEST_OVERHEAD; + + ec_dev->din = devm_kzalloc(dev, ec_dev->din_size, GFP_KERNEL); + if (!ec_dev->din) + return NULL; + + ec_dev->dout = devm_kzalloc(dev, ec_dev->dout_size, GFP_KERNEL); + if (!ec_dev->dout) + return NULL; + + ec_dev->dev = dev; + ec_dev->max_response = sizeof(struct ec_response_get_protocol_info); + ec_dev->max_request = sizeof(struct ec_params_rwsig_action); + ec_dev->suspend_timeout_ms = EC_HOST_SLEEP_TIMEOUT_DEFAULT; + + BLOCKING_INIT_NOTIFIER_HEAD(&ec_dev->event_notifier); + BLOCKING_INIT_NOTIFIER_HEAD(&ec_dev->panic_notifier); + + lockdep_register_key(&ec_dev->lockdep_key); + mutex_init(&ec_dev->lock); + lockdep_set_class(&ec_dev->lock, &ec_dev->lockdep_key); + + if (devm_add_action_or_reset(dev, cros_ec_device_free, ec_dev)) + return NULL; + + return ec_dev; +} +EXPORT_SYMBOL(cros_ec_device_alloc); + +/** + * cros_ec_irq_handler() - top half part of the interrupt handler + * @irq: IRQ id + * @data: (ec_dev) Device with events to process. + * + * Return: Wakeup the bottom half + */ +static irqreturn_t cros_ec_irq_handler(int irq, void *data) +{ + struct cros_ec_device *ec_dev = data; + + ec_dev->last_event_time = cros_ec_get_time_ns(); + + return IRQ_WAKE_THREAD; +} + +/** + * cros_ec_handle_event() - process and forward pending events on EC + * @ec_dev: Device with events to process. + * + * Call this function in a loop when the kernel is notified that the EC has + * pending events. + * + * Return: true if more events are still pending and this function should be + * called again. + */ +static bool cros_ec_handle_event(struct cros_ec_device *ec_dev) +{ + bool wake_event; + bool ec_has_more_events; + int ret; + + ret = cros_ec_get_next_event(ec_dev, &wake_event, &ec_has_more_events); + + /* + * Signal only if wake host events or any interrupt if + * cros_ec_get_next_event() returned an error (default value for + * wake_event is true) + */ + if (wake_event && device_may_wakeup(ec_dev->dev)) + pm_wakeup_event(ec_dev->dev, 0); + + if (ret > 0) + blocking_notifier_call_chain(&ec_dev->event_notifier, + 0, ec_dev); + + return ec_has_more_events; +} + +/** + * cros_ec_irq_thread() - bottom half part of the interrupt handler + * @irq: IRQ id + * @data: (ec_dev) Device with events to process. + * + * Return: Interrupt handled. + */ +irqreturn_t cros_ec_irq_thread(int irq, void *data) +{ + struct cros_ec_device *ec_dev = data; + bool ec_has_more_events; + + do { + ec_has_more_events = cros_ec_handle_event(ec_dev); + } while (ec_has_more_events); + + return IRQ_HANDLED; +} +EXPORT_SYMBOL(cros_ec_irq_thread); + +static int cros_ec_sleep_event(struct cros_ec_device *ec_dev, u8 sleep_event) +{ + int ret; + TRAILING_OVERLAP(struct cros_ec_command, msg, data, + union { + struct ec_params_host_sleep_event req0; + struct ec_params_host_sleep_event_v1 req1; + struct ec_response_host_sleep_event_v1 resp1; + } u; + ) __packed buf; + + memset(&buf, 0, sizeof(buf)); + + if (ec_dev->host_sleep_v1) { + buf.u.req1.sleep_event = sleep_event; + buf.u.req1.suspend_params.sleep_timeout_ms = + ec_dev->suspend_timeout_ms; + + buf.msg.outsize = sizeof(buf.u.req1); + if ((sleep_event == HOST_SLEEP_EVENT_S3_RESUME) || + (sleep_event == HOST_SLEEP_EVENT_S0IX_RESUME)) + buf.msg.insize = sizeof(buf.u.resp1); + + buf.msg.version = 1; + + } else { + buf.u.req0.sleep_event = sleep_event; + buf.msg.outsize = sizeof(buf.u.req0); + } + + buf.msg.command = EC_CMD_HOST_SLEEP_EVENT; + + ret = cros_ec_cmd_xfer_status(ec_dev, &buf.msg); + /* Report failure to transition to system wide suspend with a warning. */ + if (ret >= 0 && ec_dev->host_sleep_v1 && + (sleep_event == HOST_SLEEP_EVENT_S0IX_RESUME || + sleep_event == HOST_SLEEP_EVENT_S3_RESUME)) { + ec_dev->last_resume_result = + buf.u.resp1.resume_response.sleep_transitions; + + WARN_ONCE(buf.u.resp1.resume_response.sleep_transitions & + EC_HOST_RESUME_SLEEP_TIMEOUT, + "EC detected sleep transition timeout. Total sleep transitions: %d", + buf.u.resp1.resume_response.sleep_transitions & + EC_HOST_RESUME_SLEEP_TRANSITIONS_MASK); + } + + return ret; +} + +static int cros_ec_ready_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct cros_ec_device *ec_dev = container_of(nb, struct cros_ec_device, + notifier_ready); + u32 host_event = cros_ec_get_host_event(ec_dev); + + if (host_event & EC_HOST_EVENT_MASK(EC_HOST_EVENT_INTERFACE_READY)) { + mutex_lock(&ec_dev->lock); + cros_ec_query_all(ec_dev); + mutex_unlock(&ec_dev->lock); + return NOTIFY_OK; + } + + return NOTIFY_DONE; +} + +/** + * cros_ec_register() - Register a new ChromeOS EC, using the provided info. + * @ec_dev: Device to register. + * + * Before calling this, allocate a pointer to a new device and then fill + * in all the fields up to the --private-- marker. + * + * Return: 0 on success or negative error code. + */ +int cros_ec_register(struct cros_ec_device *ec_dev) +{ + struct device *dev = ec_dev->dev; + int err; + + /* Send RWSIG continue to jump to RW for devices using RWSIG. */ + err = cros_ec_rwsig_continue(ec_dev); + if (err) + dev_info(dev, "Failed to continue RWSIG: %d\n", err); + + err = cros_ec_query_all(ec_dev); + if (err) { + dev_err(dev, "Cannot identify the EC: error %d\n", err); + goto exit; + } + + if (ec_dev->irq > 0) { + err = devm_request_threaded_irq(dev, ec_dev->irq, + cros_ec_irq_handler, + cros_ec_irq_thread, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + "chromeos-ec", ec_dev); + if (err) + goto exit; + } + + /* Register a platform device for the main EC instance */ + ec_dev->ec = platform_device_register_data(ec_dev->dev, "cros-ec-dev", + PLATFORM_DEVID_AUTO, &ec_p, + sizeof(struct cros_ec_platform)); + if (IS_ERR(ec_dev->ec)) { + dev_err(ec_dev->dev, + "Failed to create CrOS EC platform device\n"); + err = PTR_ERR(ec_dev->ec); + goto exit; + } + + if (ec_dev->max_passthru) { + /* + * Register a platform device for the PD behind the main EC. + * We make the following assumptions: + * - behind an EC, we have a pd + * - only one device added. + * - the EC is responsive at init time (it is not true for a + * sensor hub). + */ + ec_dev->pd = platform_device_register_data(ec_dev->dev, + "cros-ec-dev", + PLATFORM_DEVID_AUTO, &pd_p, + sizeof(struct cros_ec_platform)); + if (IS_ERR(ec_dev->pd)) { + dev_err(ec_dev->dev, + "Failed to create CrOS PD platform device\n"); + err = PTR_ERR(ec_dev->pd); + goto exit; + } + } + + if (IS_ENABLED(CONFIG_OF) && dev->of_node) { + err = devm_of_platform_populate(dev); + if (err) { + dev_err(dev, "Failed to register sub-devices\n"); + goto exit; + } + } + + /* + * Clear sleep event - this will fail harmlessly on platforms that + * don't implement the sleep event host command. + */ + err = cros_ec_sleep_event(ec_dev, 0); + if (err < 0) + dev_dbg(ec_dev->dev, "Error %d clearing sleep event to ec\n", + err); + + if (ec_dev->mkbp_event_supported) { + /* + * Register the notifier for EC_HOST_EVENT_INTERFACE_READY + * event. + */ + ec_dev->notifier_ready.notifier_call = cros_ec_ready_event; + err = blocking_notifier_chain_register(&ec_dev->event_notifier, + &ec_dev->notifier_ready); + if (err) + goto exit; + } + + scoped_guard(mutex, &ec_dev->lock) + ec_dev->registered = true; + + dev_info(dev, "Chrome EC device registered\n"); + + /* + * Unlock EC that may be waiting for AP to process MKBP events. + * If the AP takes to long to answer, the EC would stop sending events. + */ + if (ec_dev->mkbp_event_supported) + cros_ec_irq_thread(0, ec_dev); + + return 0; +exit: + platform_device_unregister(ec_dev->ec); + platform_device_unregister(ec_dev->pd); + return err; +} +EXPORT_SYMBOL(cros_ec_register); + +/** + * cros_ec_unregister() - Remove a ChromeOS EC. + * @ec_dev: Device to unregister. + * + * Call this to deregister a ChromeOS EC, then clean up any private data. + * + * Return: 0 on success or negative error code. + */ +void cros_ec_unregister(struct cros_ec_device *ec_dev) +{ + scoped_guard(mutex, &ec_dev->lock) + ec_dev->registered = false; + + if (ec_dev->mkbp_event_supported) + blocking_notifier_chain_unregister(&ec_dev->event_notifier, + &ec_dev->notifier_ready); + platform_device_unregister(ec_dev->pd); + platform_device_unregister(ec_dev->ec); +} +EXPORT_SYMBOL(cros_ec_unregister); + +#ifdef CONFIG_PM_SLEEP +static void cros_ec_send_suspend_event(struct cros_ec_device *ec_dev) +{ + int ret; + u8 sleep_event; + + sleep_event = (!IS_ENABLED(CONFIG_ACPI) || pm_suspend_via_firmware()) ? + HOST_SLEEP_EVENT_S3_SUSPEND : + HOST_SLEEP_EVENT_S0IX_SUSPEND; + + ret = cros_ec_sleep_event(ec_dev, sleep_event); + if (ret < 0) + dev_dbg(ec_dev->dev, "Error %d sending suspend event to ec\n", + ret); +} + +/** + * cros_ec_suspend_prepare() - Handle a suspend prepare operation for the ChromeOS EC device. + * @ec_dev: Device to suspend. + * + * This can be called by drivers to handle a suspend prepare stage of suspend. + * + * Return: 0 always. + */ +int cros_ec_suspend_prepare(struct cros_ec_device *ec_dev) +{ + cros_ec_send_suspend_event(ec_dev); + return 0; +} +EXPORT_SYMBOL(cros_ec_suspend_prepare); + +static void cros_ec_disable_irq(struct cros_ec_device *ec_dev) +{ + struct device *dev = ec_dev->dev; + if (device_may_wakeup(dev)) + ec_dev->wake_enabled = !enable_irq_wake(ec_dev->irq); + else + ec_dev->wake_enabled = false; + + disable_irq(ec_dev->irq); + ec_dev->suspended = true; +} + +/** + * cros_ec_suspend_late() - Handle a suspend late operation for the ChromeOS EC device. + * @ec_dev: Device to suspend. + * + * This can be called by drivers to handle a suspend late stage of suspend. + * + * Return: 0 always. + */ +int cros_ec_suspend_late(struct cros_ec_device *ec_dev) +{ + cros_ec_disable_irq(ec_dev); + return 0; +} +EXPORT_SYMBOL(cros_ec_suspend_late); + +/** + * cros_ec_suspend() - Handle a suspend operation for the ChromeOS EC device. + * @ec_dev: Device to suspend. + * + * This can be called by drivers to handle a suspend event. + * + * Return: 0 always. + */ +int cros_ec_suspend(struct cros_ec_device *ec_dev) +{ + cros_ec_suspend_prepare(ec_dev); + cros_ec_suspend_late(ec_dev); + return 0; +} +EXPORT_SYMBOL(cros_ec_suspend); + +static void cros_ec_report_events_during_suspend(struct cros_ec_device *ec_dev) +{ + bool wake_event; + + while (ec_dev->mkbp_event_supported && + cros_ec_get_next_event(ec_dev, &wake_event, NULL) > 0) { + blocking_notifier_call_chain(&ec_dev->event_notifier, + 1, ec_dev); + + if (wake_event && device_may_wakeup(ec_dev->dev)) + pm_wakeup_event(ec_dev->dev, 0); + } +} + +static void cros_ec_send_resume_event(struct cros_ec_device *ec_dev) +{ + int ret; + u8 sleep_event; + + sleep_event = (!IS_ENABLED(CONFIG_ACPI) || pm_suspend_via_firmware()) ? + HOST_SLEEP_EVENT_S3_RESUME : + HOST_SLEEP_EVENT_S0IX_RESUME; + + ret = cros_ec_sleep_event(ec_dev, sleep_event); + if (ret < 0) + dev_dbg(ec_dev->dev, "Error %d sending resume event to ec\n", + ret); +} + +/** + * cros_ec_resume_complete() - Handle a resume complete operation for the ChromeOS EC device. + * @ec_dev: Device to resume. + * + * This can be called by drivers to handle a resume complete stage of resume. + */ +void cros_ec_resume_complete(struct cros_ec_device *ec_dev) +{ + cros_ec_send_resume_event(ec_dev); + + /* + * Let the mfd devices know about events that occur during + * suspend. This way the clients know what to do with them. + */ + cros_ec_report_events_during_suspend(ec_dev); +} +EXPORT_SYMBOL(cros_ec_resume_complete); + +static void cros_ec_enable_irq(struct cros_ec_device *ec_dev) +{ + ec_dev->suspended = false; + enable_irq(ec_dev->irq); + + if (ec_dev->wake_enabled) + disable_irq_wake(ec_dev->irq); +} + +/** + * cros_ec_resume_early() - Handle a resume early operation for the ChromeOS EC device. + * @ec_dev: Device to resume. + * + * This can be called by drivers to handle a resume early stage of resume. + * + * Return: 0 always. + */ +int cros_ec_resume_early(struct cros_ec_device *ec_dev) +{ + cros_ec_enable_irq(ec_dev); + return 0; +} +EXPORT_SYMBOL(cros_ec_resume_early); + +/** + * cros_ec_resume() - Handle a resume operation for the ChromeOS EC device. + * @ec_dev: Device to resume. + * + * This can be called by drivers to handle a resume event. + * + * Return: 0 always. + */ +int cros_ec_resume(struct cros_ec_device *ec_dev) +{ + cros_ec_resume_early(ec_dev); + cros_ec_resume_complete(ec_dev); + return 0; +} +EXPORT_SYMBOL(cros_ec_resume); + +#endif + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS EC core driver"); diff --git a/drivers/platform/chrome/cros_ec.h b/drivers/platform/chrome/cros_ec.h new file mode 100644 index 000000000..cd4643bc5 --- /dev/null +++ b/drivers/platform/chrome/cros_ec.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * ChromeOS Embedded Controller core interface. + * + * Copyright (C) 2020 Google LLC + */ + +#ifndef __CROS_EC_H +#define __CROS_EC_H + +#include + +struct cros_ec_device; +struct device; + +struct cros_ec_device *cros_ec_device_alloc(struct device *dev); + +int cros_ec_register(struct cros_ec_device *ec_dev); +void cros_ec_unregister(struct cros_ec_device *ec_dev); + +int cros_ec_suspend(struct cros_ec_device *ec_dev); +int cros_ec_suspend_late(struct cros_ec_device *ec_dev); +int cros_ec_suspend_prepare(struct cros_ec_device *ec_dev); +int cros_ec_resume(struct cros_ec_device *ec_dev); +int cros_ec_resume_early(struct cros_ec_device *ec_dev); +void cros_ec_resume_complete(struct cros_ec_device *ec_dev); + +irqreturn_t cros_ec_irq_thread(int irq, void *data); + +#endif /* __CROS_EC_H */ diff --git a/drivers/platform/chrome/cros_ec_chardev.c b/drivers/platform/chrome/cros_ec_chardev.c new file mode 100644 index 000000000..18763947a --- /dev/null +++ b/drivers/platform/chrome/cros_ec_chardev.c @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Miscellaneous character driver for ChromeOS Embedded Controller + * + * Copyright 2014 Google, Inc. + * Copyright 2019 Google LLC + * + * This file is a rework and part of the code is ported from + * drivers/mfd/cros_ec_dev.c that was originally written by + * Bill Richardson. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-chardev" + +/* Arbitrary bounded size for the event queue */ +#define CROS_MAX_EVENT_LEN PAGE_SIZE + +/* + * Platform device driver data. + */ +struct chardev_pdata { + struct miscdevice misc; + struct kref kref; + struct rw_semaphore ec_dev_sem; + struct cros_ec_device *ec_dev; + u16 cmd_offset; + struct blocking_notifier_head subscribers; + struct notifier_block relay; +}; + +static void chardev_pdata_release(struct kref *kref) +{ + struct chardev_pdata *pdata = container_of(kref, typeof(*pdata), kref); + + kfree(pdata); +} + +static int cros_ec_chardev_relay_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct chardev_pdata *pdata = container_of(nb, typeof(*pdata), relay); + + blocking_notifier_call_chain(&pdata->subscribers, queued_during_suspend, + _notify); + return NOTIFY_OK; +} + +struct chardev_priv { + struct notifier_block notifier; + wait_queue_head_t wait_event; + unsigned long event_mask; + struct list_head events; + size_t event_len; + struct chardev_pdata *pdata; +}; + +struct ec_event { + struct list_head node; + size_t size; + u8 event_type; + u8 data[]; +}; + +static int ec_get_version(struct chardev_priv *priv, char *str, int maxlen) +{ + static const char * const current_image_name[] = { + "unknown", "read-only", "read-write", "invalid", + }; + struct ec_response_get_version *resp; + struct cros_ec_command *msg; + int ret; + + msg = kzalloc(sizeof(*msg) + sizeof(*resp), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_GET_VERSION + priv->pdata->cmd_offset; + msg->insize = sizeof(*resp); + + ret = cros_ec_cmd_xfer_status(priv->pdata->ec_dev, msg); + if (ret < 0) { + snprintf(str, maxlen, + "Unknown EC version, returned error: %d\n", + msg->result); + goto exit; + } + + resp = (struct ec_response_get_version *)msg->data; + if (resp->current_image >= ARRAY_SIZE(current_image_name)) + resp->current_image = 3; /* invalid */ + + snprintf(str, maxlen, "%s\n%s\n%s\n%s\n", CROS_EC_DEV_VERSION, + resp->version_string_ro, resp->version_string_rw, + current_image_name[resp->current_image]); + + ret = 0; +exit: + kfree(msg); + return ret; +} + +static int cros_ec_chardev_mkbp_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct chardev_priv *priv = container_of(nb, struct chardev_priv, + notifier); + struct cros_ec_device *ec_dev; + struct ec_event *event; + unsigned long event_bit; + int total_size; + + guard(rwsem_read)(&priv->pdata->ec_dev_sem); + if (!priv->pdata->ec_dev) + return NOTIFY_DONE; + ec_dev = priv->pdata->ec_dev; + + event_bit = 1 << ec_dev->event_data.event_type; + total_size = sizeof(*event) + ec_dev->event_size; + + if (!(event_bit & priv->event_mask) || + (priv->event_len + total_size) > CROS_MAX_EVENT_LEN) + return NOTIFY_DONE; + + event = kzalloc(total_size, GFP_KERNEL); + if (!event) + return NOTIFY_DONE; + + event->size = ec_dev->event_size; + event->event_type = ec_dev->event_data.event_type; + memcpy(event->data, &ec_dev->event_data.data, ec_dev->event_size); + + spin_lock(&priv->wait_event.lock); + list_add_tail(&event->node, &priv->events); + priv->event_len += total_size; + wake_up_locked(&priv->wait_event); + spin_unlock(&priv->wait_event.lock); + + return NOTIFY_OK; +} + +static struct ec_event *cros_ec_chardev_fetch_event(struct chardev_priv *priv, + bool fetch, bool block) +{ + struct ec_event *event; + int err; + + spin_lock(&priv->wait_event.lock); + if (!block && list_empty(&priv->events)) { + event = ERR_PTR(-EWOULDBLOCK); + goto out; + } + + if (!fetch) { + event = NULL; + goto out; + } + + err = wait_event_interruptible_locked(priv->wait_event, + !list_empty(&priv->events)); + if (err) { + event = ERR_PTR(err); + goto out; + } + + event = list_first_entry(&priv->events, struct ec_event, node); + list_del(&event->node); + priv->event_len -= sizeof(*event) + event->size; + +out: + spin_unlock(&priv->wait_event.lock); + return event; +} + +/* + * Device file ops + */ +static int cros_ec_chardev_open(struct inode *inode, struct file *filp) +{ + struct miscdevice *mdev = filp->private_data; + struct chardev_pdata *pdata = container_of(mdev, typeof(*pdata), misc); + struct chardev_priv *priv; + int ret; + + priv = kzalloc_obj(*priv); + if (!priv) + return -ENOMEM; + + priv->pdata = pdata; + kref_get(&pdata->kref); + filp->private_data = priv; + INIT_LIST_HEAD(&priv->events); + init_waitqueue_head(&priv->wait_event); + nonseekable_open(inode, filp); + + priv->notifier.notifier_call = cros_ec_chardev_mkbp_event; + ret = blocking_notifier_chain_register(&pdata->subscribers, + &priv->notifier); + if (ret) { + dev_err(pdata->ec_dev->dev, + "failed to register event notifier\n"); + kref_put(&priv->pdata->kref, chardev_pdata_release); + kfree(priv); + } + + return ret; +} + +static __poll_t cros_ec_chardev_poll(struct file *filp, poll_table *wait) +{ + struct chardev_priv *priv = filp->private_data; + + guard(rwsem_read)(&priv->pdata->ec_dev_sem); + if (!priv->pdata->ec_dev) + return EPOLLHUP; + + poll_wait(filp, &priv->wait_event, wait); + + if (list_empty(&priv->events)) + return 0; + + return EPOLLIN | EPOLLRDNORM; +} + +static ssize_t cros_ec_chardev_read(struct file *filp, char __user *buffer, + size_t length, loff_t *offset) +{ + char msg[sizeof(struct ec_response_get_version) + + sizeof(CROS_EC_DEV_VERSION)]; + struct chardev_priv *priv = filp->private_data; + size_t count; + int ret; + + guard(rwsem_read)(&priv->pdata->ec_dev_sem); + if (!priv->pdata->ec_dev) + return -ENODEV; + + if (priv->event_mask) { /* queued MKBP event */ + struct ec_event *event; + + event = cros_ec_chardev_fetch_event(priv, length != 0, + !(filp->f_flags & O_NONBLOCK)); + if (IS_ERR(event)) + return PTR_ERR(event); + /* + * length == 0 is special - no IO is done but we check + * for error conditions. + */ + if (length == 0) + return 0; + + /* The event is 1 byte of type plus the payload */ + count = min(length, event->size + 1); + ret = copy_to_user(buffer, &event->event_type, count); + kfree(event); + if (ret) /* the copy failed */ + return -EFAULT; + *offset = count; + return count; + } + + /* + * Legacy behavior if no event mask is defined + */ + if (*offset != 0) + return 0; + + ret = ec_get_version(priv, msg, sizeof(msg)); + if (ret) + return ret; + + count = min(length, strlen(msg)); + + if (copy_to_user(buffer, msg, count)) + return -EFAULT; + + *offset = count; + return count; +} + +static int cros_ec_chardev_release(struct inode *inode, struct file *filp) +{ + struct chardev_priv *priv = filp->private_data; + struct ec_event *event, *e; + + blocking_notifier_chain_unregister(&priv->pdata->subscribers, + &priv->notifier); + kref_put(&priv->pdata->kref, chardev_pdata_release); + + list_for_each_entry_safe(event, e, &priv->events, node) { + list_del(&event->node); + kfree(event); + } + kfree(priv); + + return 0; +} + +/* + * Ioctls + */ +static long cros_ec_chardev_ioctl_xcmd(struct chardev_priv *priv, void __user *arg) +{ + struct cros_ec_command *s_cmd; + struct cros_ec_command u_cmd; + long ret; + + if (copy_from_user(&u_cmd, arg, sizeof(u_cmd))) + return -EFAULT; + + if (u_cmd.outsize > EC_MAX_MSG_BYTES || + u_cmd.insize > EC_MAX_MSG_BYTES) + return -EINVAL; + + s_cmd = kzalloc(sizeof(*s_cmd) + max(u_cmd.outsize, u_cmd.insize), + GFP_KERNEL); + if (!s_cmd) + return -ENOMEM; + + if (copy_from_user(s_cmd, arg, sizeof(*s_cmd) + u_cmd.outsize)) { + ret = -EFAULT; + goto exit; + } + + if (u_cmd.outsize != s_cmd->outsize || + u_cmd.insize != s_cmd->insize) { + ret = -EINVAL; + goto exit; + } + + s_cmd->command += priv->pdata->cmd_offset; + ret = cros_ec_cmd_xfer(priv->pdata->ec_dev, s_cmd); + /* Only copy data to userland if data was received. */ + if (ret < 0) + goto exit; + + if (copy_to_user(arg, s_cmd, sizeof(*s_cmd) + s_cmd->insize)) + ret = -EFAULT; +exit: + kfree(s_cmd); + return ret; +} + +static long cros_ec_chardev_ioctl_readmem(struct chardev_priv *priv, void __user *arg) +{ + struct cros_ec_device *ec_dev = priv->pdata->ec_dev; + struct cros_ec_readmem s_mem = { }; + long num; + + /* Not every platform supports direct reads */ + if (!ec_dev->cmd_readmem) + return -ENOTTY; + + if (copy_from_user(&s_mem, arg, sizeof(s_mem))) + return -EFAULT; + + if (s_mem.bytes > sizeof(s_mem.buffer)) + return -EINVAL; + + num = ec_dev->cmd_readmem(ec_dev, s_mem.offset, s_mem.bytes, + s_mem.buffer); + if (num <= 0) + return num; + + if (copy_to_user((void __user *)arg, &s_mem, sizeof(s_mem))) + return -EFAULT; + + return num; +} + +static long cros_ec_chardev_ioctl(struct file *filp, unsigned int cmd, + unsigned long arg) +{ + struct chardev_priv *priv = filp->private_data; + + guard(rwsem_read)(&priv->pdata->ec_dev_sem); + if (!priv->pdata->ec_dev) + return -ENODEV; + + if (_IOC_TYPE(cmd) != CROS_EC_DEV_IOC) + return -ENOTTY; + + switch (cmd) { + case CROS_EC_DEV_IOCXCMD: + return cros_ec_chardev_ioctl_xcmd(priv, (void __user *)arg); + case CROS_EC_DEV_IOCRDMEM: + return cros_ec_chardev_ioctl_readmem(priv, (void __user *)arg); + case CROS_EC_DEV_IOCEVENTMASK: + priv->event_mask = arg; + return 0; + } + + return -ENOTTY; +} + +static const struct file_operations chardev_fops = { + .open = cros_ec_chardev_open, + .poll = cros_ec_chardev_poll, + .read = cros_ec_chardev_read, + .release = cros_ec_chardev_release, + .unlocked_ioctl = cros_ec_chardev_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = cros_ec_chardev_ioctl, +#endif +}; + +static int cros_ec_chardev_probe(struct platform_device *pdev) +{ + struct cros_ec_dev *ec = dev_get_drvdata(pdev->dev.parent); + struct cros_ec_platform *ec_platform = dev_get_platdata(ec->dev); + struct chardev_pdata *pdata; + int ret; + + pdata = kzalloc_obj(*pdata); + if (!pdata) + return -ENOMEM; + + platform_set_drvdata(pdev, pdata); + kref_init(&pdata->kref); + init_rwsem(&pdata->ec_dev_sem); + pdata->ec_dev = ec->ec_dev; + pdata->cmd_offset = ec->cmd_offset; + BLOCKING_INIT_NOTIFIER_HEAD(&pdata->subscribers); + pdata->relay.notifier_call = cros_ec_chardev_relay_event; + ret = blocking_notifier_chain_register(&pdata->ec_dev->event_notifier, + &pdata->relay); + if (ret) { + dev_err(&pdev->dev, "failed to register event notifier\n"); + goto err_put_pdata; + } + + pdata->misc.minor = MISC_DYNAMIC_MINOR; + pdata->misc.fops = &chardev_fops; + pdata->misc.name = ec_platform->ec_name; + pdata->misc.parent = pdev->dev.parent; + + ret = misc_register(&pdata->misc); + if (ret) { + dev_err(&pdev->dev, "failed to register misc device\n"); + goto err_unregister_notifier; + } + + return 0; +err_unregister_notifier: + blocking_notifier_chain_unregister(&pdata->ec_dev->event_notifier, + &pdata->relay); +err_put_pdata: + kref_put(&pdata->kref, chardev_pdata_release); + return ret; +} + +static void cros_ec_chardev_remove(struct platform_device *pdev) +{ + struct chardev_pdata *pdata = platform_get_drvdata(pdev); + struct cros_ec_device *ec_dev = pdata->ec_dev; + + /* stop new fops from being created */ + misc_deregister(&pdata->misc); + /* stop existing fops from running */ + scoped_guard(rwsem_write, &pdata->ec_dev_sem) + pdata->ec_dev = NULL; + + blocking_notifier_chain_unregister(&ec_dev->event_notifier, + &pdata->relay); + kref_put(&pdata->kref, chardev_pdata_release); +} + +static const struct platform_device_id cros_ec_chardev_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_chardev_id); + +static struct platform_driver cros_ec_chardev_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = cros_ec_chardev_probe, + .remove = cros_ec_chardev_remove, + .id_table = cros_ec_chardev_id, +}; + +module_platform_driver(cros_ec_chardev_driver); + +MODULE_AUTHOR("Enric Balletbo i Serra "); +MODULE_DESCRIPTION("ChromeOS EC Miscellaneous Character Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_ec_debugfs.c b/drivers/platform/chrome/cros_ec_debugfs.c new file mode 100644 index 000000000..c3e40ad7e --- /dev/null +++ b/drivers/platform/chrome/cros_ec_debugfs.c @@ -0,0 +1,583 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Debug logs for the ChromeOS EC +// +// Copyright (C) 2015 Google, Inc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-debugfs" + +#define LOG_SHIFT 14 +#define LOG_SIZE (1 << LOG_SHIFT) +#define LOG_POLL_SEC 10 + +#define CIRC_ADD(idx, size, value) (((idx) + (value)) & ((size) - 1)) + +static unsigned int log_poll_period_ms = LOG_POLL_SEC * MSEC_PER_SEC; +module_param(log_poll_period_ms, uint, 0644); +MODULE_PARM_DESC(log_poll_period_ms, "EC log polling period(ms)"); + +/* waitqueue for log readers */ +static DECLARE_WAIT_QUEUE_HEAD(cros_ec_debugfs_log_wq); + +/** + * struct cros_ec_debugfs - EC debugging information. + * + * @ec: EC device this debugfs information belongs to + * @dir: dentry for debugfs files + * @log_buffer: circular buffer for console log information + * @read_msg: preallocated EC command and buffer to read console log + * @log_mutex: mutex to protect circular buffer + * @log_poll_work: recurring task to poll EC for new console log data + * @panicinfo_blob: panicinfo debugfs blob + * @notifier_panic: notifier_block to let kernel to flush buffered log + * when EC panic + */ +struct cros_ec_debugfs { + struct cros_ec_dev *ec; + struct dentry *dir; + /* EC log */ + struct circ_buf log_buffer; + struct cros_ec_command *read_msg; + struct mutex log_mutex; + struct delayed_work log_poll_work; + /* EC panicinfo */ + struct debugfs_blob_wrapper panicinfo_blob; + struct notifier_block notifier_panic; +}; + +/* + * We need to make sure that the EC log buffer on the UART is large enough, + * so that it is unlikely enough to overlow within log_poll_period_ms. + */ +static void cros_ec_console_log_work(struct work_struct *__work) +{ + struct cros_ec_debugfs *debug_info = + container_of(to_delayed_work(__work), + struct cros_ec_debugfs, + log_poll_work); + struct cros_ec_dev *ec = debug_info->ec; + struct circ_buf *cb = &debug_info->log_buffer; + struct cros_ec_command snapshot_msg = { + .command = EC_CMD_CONSOLE_SNAPSHOT + ec->cmd_offset, + }; + + struct ec_params_console_read_v1 *read_params = + (struct ec_params_console_read_v1 *)debug_info->read_msg->data; + uint8_t *ec_buffer = (uint8_t *)debug_info->read_msg->data; + int idx; + int buf_space; + int ret; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, &snapshot_msg); + if (ret < 0) + goto resched; + + /* Loop until we have read everything, or there's an error. */ + mutex_lock(&debug_info->log_mutex); + buf_space = CIRC_SPACE(cb->head, cb->tail, LOG_SIZE); + + while (1) { + if (!buf_space) { + dev_info_once(ec->dev, + "Some logs may have been dropped...\n"); + break; + } + + memset(read_params, '\0', sizeof(*read_params)); + read_params->subcmd = CONSOLE_READ_RECENT; + ret = cros_ec_cmd_xfer_status(ec->ec_dev, + debug_info->read_msg); + if (ret < 0) + break; + + /* If the buffer is empty, we're done here. */ + if (ret == 0 || ec_buffer[0] == '\0') + break; + + idx = 0; + while (idx < ret && ec_buffer[idx] != '\0' && buf_space > 0) { + cb->buf[cb->head] = ec_buffer[idx]; + cb->head = CIRC_ADD(cb->head, LOG_SIZE, 1); + idx++; + buf_space--; + } + + wake_up(&cros_ec_debugfs_log_wq); + } + + mutex_unlock(&debug_info->log_mutex); + +resched: + schedule_delayed_work(&debug_info->log_poll_work, + msecs_to_jiffies(log_poll_period_ms)); +} + +static int cros_ec_console_log_open(struct inode *inode, struct file *file) +{ + file->private_data = inode->i_private; + + return stream_open(inode, file); +} + +static ssize_t cros_ec_console_log_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + struct cros_ec_debugfs *debug_info = file->private_data; + struct circ_buf *cb = &debug_info->log_buffer; + ssize_t ret; + + mutex_lock(&debug_info->log_mutex); + + while (!CIRC_CNT(cb->head, cb->tail, LOG_SIZE)) { + if (file->f_flags & O_NONBLOCK) { + ret = -EAGAIN; + goto error; + } + + mutex_unlock(&debug_info->log_mutex); + + ret = wait_event_interruptible(cros_ec_debugfs_log_wq, + CIRC_CNT(cb->head, cb->tail, LOG_SIZE)); + if (ret < 0) + return ret; + + mutex_lock(&debug_info->log_mutex); + } + + /* Only copy until the end of the circular buffer, and let userspace + * retry to get the rest of the data. + */ + ret = min_t(size_t, CIRC_CNT_TO_END(cb->head, cb->tail, LOG_SIZE), + count); + + if (copy_to_user(buf, cb->buf + cb->tail, ret)) { + ret = -EFAULT; + goto error; + } + + cb->tail = CIRC_ADD(cb->tail, LOG_SIZE, ret); + +error: + mutex_unlock(&debug_info->log_mutex); + return ret; +} + +static __poll_t cros_ec_console_log_poll(struct file *file, + poll_table *wait) +{ + struct cros_ec_debugfs *debug_info = file->private_data; + __poll_t mask = 0; + + poll_wait(file, &cros_ec_debugfs_log_wq, wait); + + mutex_lock(&debug_info->log_mutex); + if (CIRC_CNT(debug_info->log_buffer.head, + debug_info->log_buffer.tail, + LOG_SIZE)) + mask |= EPOLLIN | EPOLLRDNORM; + mutex_unlock(&debug_info->log_mutex); + + return mask; +} + +static int cros_ec_console_log_release(struct inode *inode, struct file *file) +{ + return 0; +} + +static ssize_t cros_ec_pdinfo_read(struct file *file, + char __user *user_buf, + size_t count, + loff_t *ppos) +{ + char read_buf[EC_USB_PD_MAX_PORTS * 40], *p = read_buf; + struct cros_ec_debugfs *debug_info = file->private_data; + struct cros_ec_device *ec_dev = debug_info->ec->ec_dev; + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + MAX(sizeof(struct ec_response_usb_pd_control_v1), + sizeof(struct ec_params_usb_pd_control))); + struct ec_response_usb_pd_control_v1 *resp = + (struct ec_response_usb_pd_control_v1 *)msg->data; + struct ec_params_usb_pd_control *params = + (struct ec_params_usb_pd_control *)msg->data; + int i; + + msg->command = EC_CMD_USB_PD_CONTROL; + msg->version = 1; + msg->insize = sizeof(*resp); + msg->outsize = sizeof(*params); + + /* + * Read status from all PD ports until failure, typically caused + * by attempting to read status on a port that doesn't exist. + */ + for (i = 0; i < EC_USB_PD_MAX_PORTS; ++i) { + params->port = i; + params->role = 0; + params->mux = 0; + params->swap = 0; + + if (cros_ec_cmd_xfer_status(ec_dev, msg) < 0) + break; + + p += scnprintf(p, sizeof(read_buf) + read_buf - p, + "p%d: %s en:%.2x role:%.2x pol:%.2x\n", i, + resp->state, resp->enabled, resp->role, + resp->polarity); + } + + return simple_read_from_buffer(user_buf, count, ppos, + read_buf, p - read_buf); +} + +static bool cros_ec_uptime_is_supported(struct cros_ec_device *ec_dev) +{ + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_response_uptime_info)); + int ret; + + msg->command = EC_CMD_GET_UPTIME_INFO; + msg->insize = sizeof(struct ec_response_uptime_info); + + ret = cros_ec_cmd_xfer_status(ec_dev, msg); + if (ret == -EPROTO && msg->result == EC_RES_INVALID_COMMAND) + return false; + + /* Other errors maybe a transient error, do not rule about support. */ + return true; +} + +static ssize_t cros_ec_uptime_read(struct file *file, char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct cros_ec_debugfs *debug_info = file->private_data; + struct cros_ec_device *ec_dev = debug_info->ec->ec_dev; + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_response_uptime_info)); + struct ec_response_uptime_info *resp = + (struct ec_response_uptime_info *)msg->data; + char read_buf[32]; + int ret; + + msg->command = EC_CMD_GET_UPTIME_INFO; + msg->insize = sizeof(*resp); + + ret = cros_ec_cmd_xfer_status(ec_dev, msg); + if (ret < 0) + return ret; + + ret = scnprintf(read_buf, sizeof(read_buf), "%u\n", + resp->time_since_ec_boot_ms); + + return simple_read_from_buffer(user_buf, count, ppos, read_buf, ret); +} + +static const struct file_operations cros_ec_console_log_fops = { + .owner = THIS_MODULE, + .open = cros_ec_console_log_open, + .read = cros_ec_console_log_read, + .poll = cros_ec_console_log_poll, + .release = cros_ec_console_log_release, +}; + +static const struct file_operations cros_ec_pdinfo_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cros_ec_pdinfo_read, + .llseek = default_llseek, +}; + +static const struct file_operations cros_ec_uptime_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cros_ec_uptime_read, + .llseek = default_llseek, +}; + +static int ec_read_version_supported(struct cros_ec_dev *ec) +{ + struct ec_params_get_cmd_versions_v1 *params; + struct ec_response_get_cmd_versions *response; + int ret; + + struct cros_ec_command *msg; + + msg = kzalloc(sizeof(*msg) + max(sizeof(*params), sizeof(*response)), + GFP_KERNEL); + if (!msg) + return 0; + + msg->version = 1; + msg->command = EC_CMD_GET_CMD_VERSIONS + ec->cmd_offset; + msg->outsize = sizeof(*params); + msg->insize = sizeof(*response); + + params = (struct ec_params_get_cmd_versions_v1 *)msg->data; + params->cmd = EC_CMD_CONSOLE_READ; + response = (struct ec_response_get_cmd_versions *)msg->data; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg) >= 0 && + response->version_mask & EC_VER_MASK(1); + + kfree(msg); + + return ret; +} + +static int cros_ec_create_console_log(struct cros_ec_debugfs *debug_info) +{ + struct cros_ec_dev *ec = debug_info->ec; + char *buf; + int read_params_size; + int read_response_size; + + /* + * If the console log feature is not supported return silently and + * don't create the console_log entry. + */ + if (!ec_read_version_supported(ec)) + return 0; + + buf = devm_kzalloc(ec->dev, LOG_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + read_params_size = sizeof(struct ec_params_console_read_v1); + read_response_size = ec->ec_dev->max_response; + debug_info->read_msg = devm_kzalloc(ec->dev, + sizeof(*debug_info->read_msg) + + max(read_params_size, read_response_size), GFP_KERNEL); + if (!debug_info->read_msg) + return -ENOMEM; + + debug_info->read_msg->version = 1; + debug_info->read_msg->command = EC_CMD_CONSOLE_READ + ec->cmd_offset; + debug_info->read_msg->outsize = read_params_size; + debug_info->read_msg->insize = read_response_size; + + debug_info->log_buffer.buf = buf; + debug_info->log_buffer.head = 0; + debug_info->log_buffer.tail = 0; + + mutex_init(&debug_info->log_mutex); + + debugfs_create_file("console_log", S_IFREG | 0444, debug_info->dir, + debug_info, &cros_ec_console_log_fops); + + INIT_DELAYED_WORK(&debug_info->log_poll_work, + cros_ec_console_log_work); + schedule_delayed_work(&debug_info->log_poll_work, 0); + + return 0; +} + +static void cros_ec_cleanup_console_log(struct cros_ec_debugfs *debug_info) +{ + if (debug_info->log_buffer.buf) { + cancel_delayed_work_sync(&debug_info->log_poll_work); + mutex_destroy(&debug_info->log_mutex); + } +} + +/* + * Returns the size of the panicinfo data fetched from the EC + */ +static int cros_ec_get_panicinfo(struct cros_ec_device *ec_dev, uint8_t *data, + int data_size) +{ + int ret; + struct cros_ec_command *msg; + + if (!data || data_size <= 0 || data_size > ec_dev->max_response) + return -EINVAL; + + msg = kzalloc(sizeof(*msg) + data_size, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_GET_PANIC_INFO; + msg->insize = data_size; + + ret = cros_ec_cmd_xfer_status(ec_dev, msg); + if (ret < 0) + goto free; + + memcpy(data, msg->data, data_size); + +free: + kfree(msg); + return ret; +} + +static int cros_ec_create_panicinfo(struct cros_ec_debugfs *debug_info) +{ + struct cros_ec_device *ec_dev = debug_info->ec->ec_dev; + int ret; + void *data; + + data = devm_kzalloc(debug_info->ec->dev, ec_dev->max_response, + GFP_KERNEL); + if (!data) + return -ENOMEM; + + ret = cros_ec_get_panicinfo(ec_dev, data, ec_dev->max_response); + if (ret < 0) { + ret = 0; + goto free; + } + + /* No panic data */ + if (ret == 0) + goto free; + + debug_info->panicinfo_blob.data = data; + debug_info->panicinfo_blob.size = ret; + + debugfs_create_blob("panicinfo", 0444, debug_info->dir, + &debug_info->panicinfo_blob); + + return 0; + +free: + devm_kfree(debug_info->ec->dev, data); + return ret; +} + +static int cros_ec_debugfs_panic_event(struct notifier_block *nb, + unsigned long queued_during_suspend, void *_notify) +{ + struct cros_ec_debugfs *debug_info = + container_of(nb, struct cros_ec_debugfs, notifier_panic); + + if (debug_info->log_buffer.buf) { + /* Force log poll work to run immediately */ + mod_delayed_work(debug_info->log_poll_work.wq, &debug_info->log_poll_work, 0); + /* Block until log poll work finishes */ + flush_delayed_work(&debug_info->log_poll_work); + } + + return NOTIFY_DONE; +} + +static int cros_ec_debugfs_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec = dev_get_drvdata(pd->dev.parent); + struct cros_ec_platform *ec_platform = dev_get_platdata(ec->dev); + const char *name = ec_platform->ec_name; + struct cros_ec_debugfs *debug_info; + int ret; + + debug_info = devm_kzalloc(ec->dev, sizeof(*debug_info), GFP_KERNEL); + if (!debug_info) + return -ENOMEM; + + debug_info->ec = ec; + debug_info->dir = debugfs_create_dir(name, NULL); + + ret = cros_ec_create_panicinfo(debug_info); + if (ret) + goto remove_debugfs; + + ret = cros_ec_create_console_log(debug_info); + if (ret) + goto remove_debugfs; + + debugfs_create_file("pdinfo", 0444, debug_info->dir, debug_info, + &cros_ec_pdinfo_fops); + + if (cros_ec_uptime_is_supported(ec->ec_dev)) + debugfs_create_file("uptime", 0444, debug_info->dir, debug_info, + &cros_ec_uptime_fops); + + debugfs_create_x32("last_resume_result", 0444, debug_info->dir, + &ec->ec_dev->last_resume_result); + + debugfs_create_u16("suspend_timeout_ms", 0664, debug_info->dir, + &ec->ec_dev->suspend_timeout_ms); + + debug_info->notifier_panic.notifier_call = cros_ec_debugfs_panic_event; + ret = blocking_notifier_chain_register(&ec->ec_dev->panic_notifier, + &debug_info->notifier_panic); + if (ret) + goto cleanup_console_log; + + ec->debug_info = debug_info; + + dev_set_drvdata(&pd->dev, ec); + + return 0; + +cleanup_console_log: + cros_ec_cleanup_console_log(debug_info); +remove_debugfs: + debugfs_remove_recursive(debug_info->dir); + return ret; +} + +static void cros_ec_debugfs_remove(struct platform_device *pd) +{ + struct cros_ec_dev *ec = dev_get_drvdata(pd->dev.parent); + + blocking_notifier_chain_unregister(&ec->ec_dev->panic_notifier, + &ec->debug_info->notifier_panic); + debugfs_remove_recursive(ec->debug_info->dir); + cros_ec_cleanup_console_log(ec->debug_info); +} + +static int __maybe_unused cros_ec_debugfs_suspend(struct device *dev) +{ + struct cros_ec_dev *ec = dev_get_drvdata(dev); + + if (ec->debug_info->log_buffer.buf) + cancel_delayed_work_sync(&ec->debug_info->log_poll_work); + + return 0; +} + +static int __maybe_unused cros_ec_debugfs_resume(struct device *dev) +{ + struct cros_ec_dev *ec = dev_get_drvdata(dev); + + if (ec->debug_info->log_buffer.buf) + schedule_delayed_work(&ec->debug_info->log_poll_work, 0); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(cros_ec_debugfs_pm_ops, + cros_ec_debugfs_suspend, cros_ec_debugfs_resume); + +static const struct platform_device_id cros_ec_debugfs_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_debugfs_id); + +static struct platform_driver cros_ec_debugfs_driver = { + .driver = { + .name = DRV_NAME, + .pm = &cros_ec_debugfs_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .probe = cros_ec_debugfs_probe, + .remove = cros_ec_debugfs_remove, + .id_table = cros_ec_debugfs_id, +}; + +module_platform_driver(cros_ec_debugfs_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Debug logs for ChromeOS EC"); diff --git a/drivers/platform/chrome/cros_ec_i2c.c b/drivers/platform/chrome/cros_ec_i2c.c new file mode 100644 index 000000000..2f46be4a2 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_i2c.c @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: GPL-2.0 +// I2C interface for ChromeOS Embedded Controller +// +// Copyright (C) 2012 Google, Inc + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +/* + * Request format for protocol v3 + * byte 0 0xda (EC_COMMAND_PROTOCOL_3) + * byte 1-8 struct ec_host_request + * byte 10- response data + */ +struct ec_host_request_i2c { + /* Always 0xda to backward compatible with v2 struct */ + uint8_t command_protocol; + struct ec_host_request ec_request; +} __packed; + + +/* + * Response format for protocol v3 + * byte 0 result code + * byte 1 packet_length + * byte 2-9 struct ec_host_response + * byte 10- response data + */ +struct ec_host_response_i2c { + uint8_t result; + uint8_t packet_length; + struct ec_host_response ec_response; +} __packed; + +static inline struct cros_ec_device *to_ec_dev(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + + return i2c_get_clientdata(client); +} + +static int cros_ec_pkt_xfer_i2c(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + struct i2c_client *client = ec_dev->priv; + int ret = -ENOMEM; + int i; + int packet_len; + u8 *out_buf = NULL; + u8 *in_buf = NULL; + u8 sum; + struct i2c_msg i2c_msg[2]; + struct ec_host_response *ec_response; + struct ec_host_request_i2c *ec_request_i2c; + struct ec_host_response_i2c *ec_response_i2c; + int request_header_size = sizeof(struct ec_host_request_i2c); + int response_header_size = sizeof(struct ec_host_response_i2c); + + i2c_msg[0].addr = client->addr; + i2c_msg[0].flags = 0; + i2c_msg[1].addr = client->addr; + i2c_msg[1].flags = I2C_M_RD; + + packet_len = msg->insize + response_header_size; + if (packet_len > ec_dev->din_size) { + ret = -EINVAL; + goto done; + } + in_buf = ec_dev->din; + i2c_msg[1].len = packet_len; + i2c_msg[1].buf = (char *) in_buf; + + packet_len = msg->outsize + request_header_size; + if (packet_len > ec_dev->dout_size) { + ret = -EINVAL; + goto done; + } + out_buf = ec_dev->dout; + i2c_msg[0].len = packet_len; + i2c_msg[0].buf = (char *) out_buf; + + /* create request data */ + ec_request_i2c = (struct ec_host_request_i2c *) out_buf; + ec_request_i2c->command_protocol = EC_COMMAND_PROTOCOL_3; + + ec_dev->dout++; + ret = cros_ec_prepare_tx(ec_dev, msg); + if (ret < 0) + goto done; + ec_dev->dout--; + + /* send command to EC and read answer */ + ret = i2c_transfer(client->adapter, i2c_msg, 2); + if (ret < 0) { + dev_dbg(ec_dev->dev, "i2c transfer failed: %d\n", ret); + goto done; + } else if (ret != 2) { + dev_err(ec_dev->dev, "failed to get response: %d\n", ret); + ret = -EIO; + goto done; + } + + ec_response_i2c = (struct ec_host_response_i2c *) in_buf; + msg->result = ec_response_i2c->result; + ec_response = &ec_response_i2c->ec_response; + + switch (msg->result) { + case EC_RES_SUCCESS: + break; + case EC_RES_IN_PROGRESS: + ret = -EAGAIN; + dev_dbg(ec_dev->dev, "command 0x%02x in progress\n", + msg->command); + goto done; + + default: + dev_dbg(ec_dev->dev, "command 0x%02x returned %d\n", + msg->command, msg->result); + /* + * When we send v3 request to v2 ec, ec won't recognize the + * 0xda (EC_COMMAND_PROTOCOL_3) and will return with status + * EC_RES_INVALID_COMMAND with zero data length. + * + * In case of invalid command for v3 protocol the data length + * will be at least sizeof(struct ec_host_response) + */ + if (ec_response_i2c->result == EC_RES_INVALID_COMMAND && + ec_response_i2c->packet_length == 0) { + ret = -EPROTONOSUPPORT; + goto done; + } + } + + if (ec_response_i2c->packet_length < sizeof(struct ec_host_response)) { + dev_err(ec_dev->dev, + "response of %u bytes too short; not a full header\n", + ec_response_i2c->packet_length); + ret = -EBADMSG; + goto done; + } + + if (msg->insize < ec_response->data_len) { + dev_err(ec_dev->dev, + "response data size is too large: expected %u, got %u\n", + msg->insize, + ec_response->data_len); + ret = -EMSGSIZE; + goto done; + } + + /* copy response packet payload and compute checksum */ + sum = 0; + for (i = 0; i < sizeof(struct ec_host_response); i++) + sum += ((u8 *)ec_response)[i]; + + memcpy(msg->data, + in_buf + response_header_size, + ec_response->data_len); + for (i = 0; i < ec_response->data_len; i++) + sum += msg->data[i]; + + /* All bytes should sum to zero */ + if (sum) { + dev_err(ec_dev->dev, "bad packet checksum\n"); + ret = -EBADMSG; + goto done; + } + + ret = ec_response->data_len; + +done: + if (msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +static int cros_ec_cmd_xfer_i2c(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + struct i2c_client *client = ec_dev->priv; + int ret = -ENOMEM; + int i; + int len; + int packet_len; + u8 *out_buf = NULL; + u8 *in_buf = NULL; + u8 sum; + struct i2c_msg i2c_msg[2]; + + i2c_msg[0].addr = client->addr; + i2c_msg[0].flags = 0; + i2c_msg[1].addr = client->addr; + i2c_msg[1].flags = I2C_M_RD; + + /* + * allocate larger packet (one byte for checksum, one byte for + * length, and one for result code) + */ + packet_len = msg->insize + 3; + in_buf = kzalloc(packet_len, GFP_KERNEL); + if (!in_buf) + goto done; + i2c_msg[1].len = packet_len; + i2c_msg[1].buf = (char *)in_buf; + + /* + * allocate larger packet (one byte for checksum, one for + * command code, one for length, and one for command version) + */ + packet_len = msg->outsize + 4; + out_buf = kzalloc(packet_len, GFP_KERNEL); + if (!out_buf) + goto done; + i2c_msg[0].len = packet_len; + i2c_msg[0].buf = (char *)out_buf; + + out_buf[0] = EC_CMD_VERSION0 + msg->version; + out_buf[1] = msg->command; + out_buf[2] = msg->outsize; + + /* copy message payload and compute checksum */ + sum = out_buf[0] + out_buf[1] + out_buf[2]; + for (i = 0; i < msg->outsize; i++) { + out_buf[3 + i] = msg->data[i]; + sum += out_buf[3 + i]; + } + out_buf[3 + msg->outsize] = sum; + + /* send command to EC and read answer */ + ret = i2c_transfer(client->adapter, i2c_msg, 2); + if (ret < 0) { + dev_err(ec_dev->dev, "i2c transfer failed: %d\n", ret); + goto done; + } else if (ret != 2) { + dev_err(ec_dev->dev, "failed to get response: %d\n", ret); + ret = -EIO; + goto done; + } + + /* check response error code */ + msg->result = i2c_msg[1].buf[0]; + ret = cros_ec_check_result(ec_dev, msg); + if (ret) + goto done; + + len = in_buf[1]; + if (len > msg->insize) { + dev_err(ec_dev->dev, "packet too long (%d bytes, expected %d)", + len, msg->insize); + ret = -ENOSPC; + goto done; + } + + /* copy response packet payload and compute checksum */ + sum = in_buf[0] + in_buf[1]; + for (i = 0; i < len; i++) { + msg->data[i] = in_buf[2 + i]; + sum += in_buf[2 + i]; + } + dev_dbg(ec_dev->dev, "packet: %*ph, sum = %02x\n", + i2c_msg[1].len, in_buf, sum); + if (sum != in_buf[2 + len]) { + dev_err(ec_dev->dev, "bad packet checksum\n"); + ret = -EBADMSG; + goto done; + } + + ret = len; +done: + kfree(in_buf); + kfree(out_buf); + if (msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +static int cros_ec_i2c_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct cros_ec_device *ec_dev; + int err; + + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + i2c_set_clientdata(client, ec_dev); + ec_dev->priv = client; + ec_dev->irq = client->irq; + ec_dev->cmd_xfer = cros_ec_cmd_xfer_i2c; + ec_dev->pkt_xfer = cros_ec_pkt_xfer_i2c; + ec_dev->phys_name = client->adapter->name; + + err = cros_ec_register(ec_dev); + if (err) { + dev_err(dev, "cannot register EC\n"); + return err; + } + + return 0; +} + +static void cros_ec_i2c_remove(struct i2c_client *client) +{ + struct cros_ec_device *ec_dev = i2c_get_clientdata(client); + + cros_ec_unregister(ec_dev); +} + +#ifdef CONFIG_PM_SLEEP +static int cros_ec_i2c_suspend(struct device *dev) +{ + struct cros_ec_device *ec_dev = to_ec_dev(dev); + + return cros_ec_suspend(ec_dev); +} + +static int cros_ec_i2c_resume(struct device *dev) +{ + struct cros_ec_device *ec_dev = to_ec_dev(dev); + + return cros_ec_resume(ec_dev); +} +#endif + +static const struct dev_pm_ops cros_ec_i2c_pm_ops = { + SET_LATE_SYSTEM_SLEEP_PM_OPS(cros_ec_i2c_suspend, cros_ec_i2c_resume) +}; + +#ifdef CONFIG_OF +static const struct of_device_id cros_ec_i2c_of_match[] = { + { .compatible = "google,cros-ec-i2c", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, cros_ec_i2c_of_match); +#endif + +static const struct i2c_device_id cros_ec_i2c_id[] = { + { .name = "cros-ec-i2c" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, cros_ec_i2c_id); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id cros_ec_i2c_acpi_id[] = { + { "GOOG0008", 0 }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(acpi, cros_ec_i2c_acpi_id); +#endif + +static struct i2c_driver cros_ec_driver = { + .driver = { + .name = "cros-ec-i2c", + .acpi_match_table = ACPI_PTR(cros_ec_i2c_acpi_id), + .of_match_table = of_match_ptr(cros_ec_i2c_of_match), + .pm = &cros_ec_i2c_pm_ops, + }, + .probe = cros_ec_i2c_probe, + .remove = cros_ec_i2c_remove, + .id_table = cros_ec_i2c_id, +}; + +module_i2c_driver(cros_ec_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("I2C interface for ChromeOS Embedded Controller"); diff --git a/drivers/platform/chrome/cros_ec_ishtp.c b/drivers/platform/chrome/cros_ec_ishtp.c new file mode 100644 index 000000000..3766cef81 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_ishtp.c @@ -0,0 +1,751 @@ +// SPDX-License-Identifier: GPL-2.0 +// ISHTP interface for ChromeOS Embedded Controller +// +// Copyright (c) 2019, Intel Corporation. +// +// ISHTP client driver for talking to the Chrome OS EC firmware running +// on Intel Integrated Sensor Hub (ISH) using the ISH Transport protocol +// (ISH-TP). + +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +/* + * ISH TX/RX ring buffer pool size + * + * The AP->ISH messages and corresponding ISH->AP responses are + * serialized. We need 1 TX and 1 RX buffer for these. + * + * The MKBP ISH->AP events are serialized. We need one additional RX + * buffer for them. + */ +#define CROS_ISH_CL_TX_RING_SIZE 8 +#define CROS_ISH_CL_RX_RING_SIZE 8 + +/* ISH CrOS EC Host Commands */ +enum cros_ec_ish_channel { + CROS_EC_COMMAND = 1, /* AP->ISH message */ + CROS_MKBP_EVENT = 2, /* ISH->AP events */ +}; + +/* + * ISH firmware timeout for 1 message send failure is 1Hz, and the + * firmware will retry 2 times, so 3Hz is used for timeout. + */ +#define ISHTP_SEND_TIMEOUT (3 * HZ) + +/* ISH Transport CrOS EC ISH client unique GUID */ +static const struct ishtp_device_id cros_ec_ishtp_id_table[] = { + { .guid = GUID_INIT(0x7b7154d0, 0x56f4, 0x4bdc, + 0xb0, 0xd8, 0x9e, 0x7c, 0xda, 0xe0, 0xd6, 0xa0), }, + { } +}; +MODULE_DEVICE_TABLE(ishtp, cros_ec_ishtp_id_table); + +struct header { + u8 channel; + u8 status; + u8 token; + u8 reserved; +} __packed; + +struct cros_ish_out_msg { + struct header hdr; + struct ec_host_request ec_request; +} __packed; + +struct cros_ish_in_msg { + struct header hdr; + struct ec_host_response ec_response; +} __packed; + +#define IN_MSG_EC_RESPONSE_PREAMBLE \ + offsetof(struct cros_ish_in_msg, ec_response) + +#define OUT_MSG_EC_REQUEST_PREAMBLE \ + offsetof(struct cros_ish_out_msg, ec_request) + +#define cl_data_to_dev(client_data) ishtp_device((client_data)->cl_device) + +/* + * The Read-Write Semaphore is used to prevent message TX or RX while + * the ishtp client is being initialized or undergoing reset. + * + * The readers are the kernel function calls responsible for IA->ISH + * and ISH->AP messaging. + * + * The writers are .reset() and .probe() function. + */ +static DECLARE_RWSEM(init_lock); + +/** + * struct response_info - Encapsulate firmware response related + * information for passing between function ish_send() and + * process_recv() callback. + * + * @data: Copy the data received from firmware here. + * @max_size: Max size allocated for the @data buffer. If the received + * data exceeds this value, we log an error. + * @size: Actual size of data received from firmware. + * @error: 0 for success, negative error code for a failure in process_recv(). + * @token: Expected token for response that we are waiting on. + * @received: Set to true on receiving a valid firmware response to host command + * @wait_queue: Wait queue for host to wait for firmware response. + */ +struct response_info { + void *data; + size_t max_size; + size_t size; + int error; + u8 token; + bool received; + wait_queue_head_t wait_queue; +}; + +/** + * struct ishtp_cl_data - Encapsulate per ISH TP Client. + * + * @cros_ish_cl: ISHTP firmware client instance. + * @cl_device: ISHTP client device instance. + * @response: Response info passing between ish_send() and process_recv(). + * @work_ishtp_reset: Work queue reset handling. + * @work_ec_evt: Work queue for EC events. + * @ec_dev: CrOS EC MFD device. + * + * This structure is used to store per client data. + */ +struct ishtp_cl_data { + struct ishtp_cl *cros_ish_cl; + struct ishtp_cl_device *cl_device; + + /* + * Used for passing firmware response information between + * ish_send() and process_recv() callback. + */ + struct response_info response; + + struct work_struct work_ishtp_reset; + struct work_struct work_ec_evt; + struct cros_ec_device *ec_dev; +}; + +/** + * ish_evt_handler - ISH to AP event handler + * @work: Work struct + */ +static void ish_evt_handler(struct work_struct *work) +{ + struct ishtp_cl_data *client_data = + container_of(work, struct ishtp_cl_data, work_ec_evt); + + cros_ec_irq_thread(0, client_data->ec_dev); +} + +/** + * ish_send() - Send message from host to firmware + * + * @client_data: Client data instance + * @out_msg: Message buffer to be sent to firmware + * @out_size: Size of out going message + * @in_msg: Message buffer where the incoming data is copied. This buffer + * is allocated by calling + * @in_size: Max size of incoming message + * + * Return: Number of bytes copied in the in_msg on success, negative + * error code on failure. + */ +static int ish_send(struct ishtp_cl_data *client_data, + u8 *out_msg, size_t out_size, + u8 *in_msg, size_t in_size) +{ + static u8 next_token; + int rv; + struct header *out_hdr = (struct header *)out_msg; + struct ishtp_cl *cros_ish_cl = client_data->cros_ish_cl; + + dev_dbg(cl_data_to_dev(client_data), + "%s: channel=%02u status=%02u\n", + __func__, out_hdr->channel, out_hdr->status); + + /* Setup for incoming response */ + client_data->response.data = in_msg; + client_data->response.max_size = in_size; + client_data->response.error = 0; + client_data->response.token = next_token++; + client_data->response.received = false; + + out_hdr->token = client_data->response.token; + + rv = ishtp_cl_send(cros_ish_cl, out_msg, out_size); + if (rv) { + dev_err(cl_data_to_dev(client_data), + "ishtp_cl_send error %d\n", rv); + return rv; + } + + wait_event_interruptible_timeout(client_data->response.wait_queue, + client_data->response.received, + ISHTP_SEND_TIMEOUT); + if (!client_data->response.received) { + dev_err(cl_data_to_dev(client_data), + "Timed out for response to host message\n"); + return -ETIMEDOUT; + } + + if (client_data->response.error < 0) + return client_data->response.error; + + return client_data->response.size; +} + +/** + * process_recv() - Received and parse incoming packet + * @cros_ish_cl: Client instance to get stats + * @rb_in_proc: Host interface message buffer + * @timestamp: Timestamp of when parent callback started + * + * Parse the incoming packet. If it is a response packet then it will + * update per instance flags and wake up the caller waiting to for the + * response. If it is an event packet then it will schedule event work. + */ +static void process_recv(struct ishtp_cl *cros_ish_cl, + struct ishtp_cl_rb *rb_in_proc, ktime_t timestamp) +{ + size_t data_len = rb_in_proc->buf_idx; + struct ishtp_cl_data *client_data = + ishtp_get_client_data(cros_ish_cl); + struct device *dev = cl_data_to_dev(client_data); + struct cros_ish_in_msg *in_msg = + (struct cros_ish_in_msg *)rb_in_proc->buffer.data; + + /* Proceed only if reset or init is not in progress */ + if (!down_read_trylock(&init_lock)) { + /* Free the buffer */ + ishtp_cl_io_rb_recycle(rb_in_proc); + dev_warn(dev, + "Host is not ready to receive incoming messages\n"); + return; + } + + /* + * All firmware messages contain a header. Check the buffer size + * before accessing elements inside. + */ + if (!rb_in_proc->buffer.data) { + dev_warn(dev, "rb_in_proc->buffer.data returned null"); + client_data->response.error = -EBADMSG; + goto end_error; + } + + if (data_len < sizeof(struct header)) { + dev_err(dev, "data size %zu is less than header %zu\n", + data_len, sizeof(struct header)); + client_data->response.error = -EMSGSIZE; + goto end_error; + } + + dev_dbg(dev, "channel=%02u status=%02u\n", + in_msg->hdr.channel, in_msg->hdr.status); + + switch (in_msg->hdr.channel) { + case CROS_EC_COMMAND: + if (client_data->response.received) { + dev_err(dev, + "Previous firmware message not yet processed\n"); + goto end_error; + } + + if (client_data->response.token != in_msg->hdr.token) { + dev_err_ratelimited(dev, + "Dropping old response token %d\n", + in_msg->hdr.token); + goto end_error; + } + + /* Sanity check */ + if (!client_data->response.data) { + dev_err(dev, + "Receiving buffer is null. Should be allocated by calling function\n"); + client_data->response.error = -EINVAL; + goto error_wake_up; + } + + if (data_len > client_data->response.max_size) { + dev_err(dev, + "Received buffer size %zu is larger than allocated buffer %zu\n", + data_len, client_data->response.max_size); + client_data->response.error = -EMSGSIZE; + goto error_wake_up; + } + + if (in_msg->hdr.status) { + dev_err(dev, "firmware returned status %d\n", + in_msg->hdr.status); + client_data->response.error = -EIO; + goto error_wake_up; + } + + /* Update the actual received buffer size */ + client_data->response.size = data_len; + + /* + * Copy the buffer received in firmware response for the + * calling thread. + */ + memcpy(client_data->response.data, + rb_in_proc->buffer.data, data_len); + +error_wake_up: + /* Free the buffer since we copied data or didn't need it */ + ishtp_cl_io_rb_recycle(rb_in_proc); + rb_in_proc = NULL; + + /* Set flag before waking up the caller */ + client_data->response.received = true; + + /* Wake the calling thread */ + wake_up_interruptible(&client_data->response.wait_queue); + + break; + + case CROS_MKBP_EVENT: + /* Free the buffer. This is just an event without data */ + ishtp_cl_io_rb_recycle(rb_in_proc); + rb_in_proc = NULL; + /* + * Set timestamp from beginning of function since we actually + * got an incoming MKBP event + */ + client_data->ec_dev->last_event_time = timestamp; + schedule_work(&client_data->work_ec_evt); + + break; + + default: + dev_err(dev, "Invalid channel=%02d\n", in_msg->hdr.channel); + } + +end_error: + /* Free the buffer if we already haven't */ + if (rb_in_proc) + ishtp_cl_io_rb_recycle(rb_in_proc); + + up_read(&init_lock); +} + +/** + * ish_event_cb() - bus driver callback for incoming message + * @cl_device: ISHTP client device for which this message is targeted. + * + * Remove the packet from the list and process the message by calling + * process_recv. + */ +static void ish_event_cb(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl_rb *rb_in_proc; + struct ishtp_cl *cros_ish_cl = ishtp_get_drvdata(cl_device); + ktime_t timestamp; + + /* + * Take timestamp as close to hardware interrupt as possible for sensor + * timestamps. + */ + timestamp = cros_ec_get_time_ns(); + + while ((rb_in_proc = ishtp_cl_rx_get_rb(cros_ish_cl)) != NULL) { + /* Decide what to do with received data */ + process_recv(cros_ish_cl, rb_in_proc, timestamp); + } +} + +/** + * cros_ish_init() - Init function for ISHTP client + * @cros_ish_cl: ISHTP client instance + * @reset: true if called from reset handler + * + * This function complete the initializtion of the client. + * + * Return: 0 for success, negative error code for failure. + */ +static int cros_ish_init(struct ishtp_cl *cros_ish_cl, bool reset) +{ + int rv; + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + + rv = ishtp_cl_establish_connection(cros_ish_cl, + &cros_ec_ishtp_id_table[0].guid, + CROS_ISH_CL_TX_RING_SIZE, + CROS_ISH_CL_RX_RING_SIZE, + reset); + if (rv) { + dev_err(cl_data_to_dev(client_data), + "client connect fail\n"); + goto err_cl_disconnect; + } + + ishtp_register_event_cb(client_data->cl_device, ish_event_cb); + return 0; + +err_cl_disconnect: + ishtp_cl_destroy_connection(cros_ish_cl, reset); + return rv; +} + +/** + * cros_ish_deinit() - Deinit function for ISHTP client + * @cros_ish_cl: ISHTP client instance + * + * Unlink and free cros_ec client + */ +static void cros_ish_deinit(struct ishtp_cl *cros_ish_cl) +{ + ishtp_cl_destroy_connection(cros_ish_cl, false); + + /* Disband and free all Tx and Rx client-level rings */ + ishtp_cl_free(cros_ish_cl); +} + +/** + * prepare_cros_ec_rx() - Check & prepare receive buffer + * @ec_dev: CrOS EC MFD device. + * @in_msg: Incoming message buffer + * @msg: cros_ec command used to send & receive data + * + * Return: 0 for success, negative error code for failure. + * + * Check the received buffer. Convert to cros_ec_command format. + */ +static int prepare_cros_ec_rx(struct cros_ec_device *ec_dev, + const struct cros_ish_in_msg *in_msg, + struct cros_ec_command *msg) +{ + u8 sum = 0; + int i, rv, offset; + + /* Check response error code */ + msg->result = in_msg->ec_response.result; + rv = cros_ec_check_result(ec_dev, msg); + if (rv < 0) + return rv; + + if (in_msg->ec_response.data_len > msg->insize) { + dev_err(ec_dev->dev, "Packet too long (%d bytes, expected %d)", + in_msg->ec_response.data_len, msg->insize); + return -ENOSPC; + } + + /* Copy response packet payload and compute checksum */ + for (i = 0; i < sizeof(struct ec_host_response); i++) + sum += ((u8 *)in_msg)[IN_MSG_EC_RESPONSE_PREAMBLE + i]; + + offset = sizeof(struct cros_ish_in_msg); + for (i = 0; i < in_msg->ec_response.data_len; i++) + sum += msg->data[i] = ((u8 *)in_msg)[offset + i]; + + if (sum) { + dev_dbg(ec_dev->dev, "Bad received packet checksum %d\n", sum); + return -EBADMSG; + } + + return 0; +} + +static int cros_ec_pkt_xfer_ish(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + int rv; + struct ishtp_cl *cros_ish_cl = ec_dev->priv; + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + struct device *dev = cl_data_to_dev(client_data); + struct cros_ish_in_msg *in_msg = (struct cros_ish_in_msg *)ec_dev->din; + struct cros_ish_out_msg *out_msg = + (struct cros_ish_out_msg *)ec_dev->dout; + size_t in_size = sizeof(struct cros_ish_in_msg) + msg->insize; + size_t out_size = sizeof(struct cros_ish_out_msg) + msg->outsize; + + /* Sanity checks */ + if (in_size > ec_dev->din_size) { + dev_err(dev, + "Incoming payload size %zu is too large for ec_dev->din_size %d\n", + in_size, ec_dev->din_size); + return -EMSGSIZE; + } + + if (out_size > ec_dev->dout_size) { + dev_err(dev, + "Outgoing payload size %zu is too large for ec_dev->dout_size %d\n", + out_size, ec_dev->dout_size); + return -EMSGSIZE; + } + + /* Proceed only if reset-init is not in progress */ + if (!down_read_trylock(&init_lock)) { + dev_warn(dev, + "Host is not ready to send messages to ISH. Try again\n"); + return -EAGAIN; + } + + /* Prepare the package to be sent over ISH TP */ + out_msg->hdr.channel = CROS_EC_COMMAND; + out_msg->hdr.status = 0; + + ec_dev->dout += OUT_MSG_EC_REQUEST_PREAMBLE; + rv = cros_ec_prepare_tx(ec_dev, msg); + if (rv < 0) + goto end_error; + ec_dev->dout -= OUT_MSG_EC_REQUEST_PREAMBLE; + + dev_dbg(dev, + "out_msg: struct_ver=0x%x checksum=0x%x command=0x%x command_ver=0x%x data_len=0x%x\n", + out_msg->ec_request.struct_version, + out_msg->ec_request.checksum, + out_msg->ec_request.command, + out_msg->ec_request.command_version, + out_msg->ec_request.data_len); + + /* Send command to ISH EC firmware and read response */ + rv = ish_send(client_data, + (u8 *)out_msg, out_size, + (u8 *)in_msg, in_size); + if (rv < 0) + goto end_error; + + rv = prepare_cros_ec_rx(ec_dev, in_msg, msg); + if (rv) + goto end_error; + + rv = in_msg->ec_response.data_len; + + dev_dbg(dev, + "in_msg: struct_ver=0x%x checksum=0x%x result=0x%x data_len=0x%x\n", + in_msg->ec_response.struct_version, + in_msg->ec_response.checksum, + in_msg->ec_response.result, + in_msg->ec_response.data_len); + +end_error: + if (msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + up_read(&init_lock); + + return rv; +} + +static int cros_ec_dev_init(struct ishtp_cl_data *client_data) +{ + struct cros_ec_device *ec_dev; + struct device *dev = cl_data_to_dev(client_data); + + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + client_data->ec_dev = ec_dev; + dev->driver_data = ec_dev; + + ec_dev->priv = client_data->cros_ish_cl; + ec_dev->cmd_xfer = NULL; + ec_dev->pkt_xfer = cros_ec_pkt_xfer_ish; + ec_dev->phys_name = dev_name(dev); + + return cros_ec_register(ec_dev); +} + +static void reset_handler(struct work_struct *work) +{ + int rv; + struct device *dev; + struct ishtp_cl *cros_ish_cl; + struct ishtp_cl_data *client_data = + container_of(work, struct ishtp_cl_data, work_ishtp_reset); + + /* Lock for reset to complete */ + down_write(&init_lock); + + cros_ish_cl = client_data->cros_ish_cl; + + ishtp_cl_destroy_connection(cros_ish_cl, true); + + rv = cros_ish_init(cros_ish_cl, true); + if (rv) { + dev_err(cl_data_to_dev(client_data), "Reset Failed\n"); + up_write(&init_lock); + return; + } + + /* Refresh ec_dev device pointers */ + client_data->ec_dev->priv = client_data->cros_ish_cl; + dev = cl_data_to_dev(client_data); + dev->driver_data = client_data->ec_dev; + + dev_info(cl_data_to_dev(client_data), "Chrome EC ISH reset done\n"); + + up_write(&init_lock); +} + +/** + * cros_ec_ishtp_probe() - ISHTP client driver probe callback + * @cl_device: ISHTP client device instance + * + * Return: 0 for success, negative error code for failure. + */ +static int cros_ec_ishtp_probe(struct ishtp_cl_device *cl_device) +{ + int rv; + struct ishtp_cl *cros_ish_cl; + struct ishtp_cl_data *client_data = + devm_kzalloc(ishtp_device(cl_device), + sizeof(*client_data), GFP_KERNEL); + if (!client_data) + return -ENOMEM; + + /* Lock for initialization to complete */ + down_write(&init_lock); + + cros_ish_cl = ishtp_cl_allocate(cl_device); + if (!cros_ish_cl) { + rv = -ENOMEM; + goto end_ishtp_cl_alloc_error; + } + + ishtp_set_drvdata(cl_device, cros_ish_cl); + ishtp_set_client_data(cros_ish_cl, client_data); + client_data->cros_ish_cl = cros_ish_cl; + client_data->cl_device = cl_device; + + init_waitqueue_head(&client_data->response.wait_queue); + + INIT_WORK(&client_data->work_ishtp_reset, + reset_handler); + INIT_WORK(&client_data->work_ec_evt, + ish_evt_handler); + + rv = cros_ish_init(cros_ish_cl, false); + if (rv) + goto end_ishtp_cl_init_error; + + ishtp_get_device(cl_device); + + up_write(&init_lock); + + /* Register croc_ec_dev mfd */ + rv = cros_ec_dev_init(client_data); + if (rv) { + down_write(&init_lock); + goto end_cros_ec_dev_init_error; + } + + return 0; + +end_cros_ec_dev_init_error: + ishtp_cl_destroy_connection(cros_ish_cl, false); + ishtp_put_device(cl_device); +end_ishtp_cl_init_error: + ishtp_cl_free(cros_ish_cl); +end_ishtp_cl_alloc_error: + up_write(&init_lock); + return rv; +} + +/** + * cros_ec_ishtp_remove() - ISHTP client driver remove callback + * @cl_device: ISHTP client device instance + * + * Return: 0 + */ +static void cros_ec_ishtp_remove(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *cros_ish_cl = ishtp_get_drvdata(cl_device); + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + + cancel_work_sync(&client_data->work_ishtp_reset); + cancel_work_sync(&client_data->work_ec_evt); + cros_ec_unregister(client_data->ec_dev); + cros_ish_deinit(cros_ish_cl); + ishtp_put_device(cl_device); +} + +/** + * cros_ec_ishtp_reset() - ISHTP client driver reset callback + * @cl_device: ISHTP client device instance + * + * Return: 0 + */ +static int cros_ec_ishtp_reset(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *cros_ish_cl = ishtp_get_drvdata(cl_device); + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + + schedule_work(&client_data->work_ishtp_reset); + + return 0; +} + +/** + * cros_ec_ishtp_suspend() - ISHTP client driver suspend callback + * @device: device instance + * + * Return: 0 for success, negative error code for failure. + */ +static int __maybe_unused cros_ec_ishtp_suspend(struct device *device) +{ + struct ishtp_cl_device *cl_device = ishtp_dev_to_cl_device(device); + struct ishtp_cl *cros_ish_cl = ishtp_get_drvdata(cl_device); + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + + return cros_ec_suspend(client_data->ec_dev); +} + +/** + * cros_ec_ishtp_resume() - ISHTP client driver resume callback + * @device: device instance + * + * Return: 0 for success, negative error code for failure. + */ +static int __maybe_unused cros_ec_ishtp_resume(struct device *device) +{ + struct ishtp_cl_device *cl_device = ishtp_dev_to_cl_device(device); + struct ishtp_cl *cros_ish_cl = ishtp_get_drvdata(cl_device); + struct ishtp_cl_data *client_data = ishtp_get_client_data(cros_ish_cl); + + return cros_ec_resume(client_data->ec_dev); +} + +static SIMPLE_DEV_PM_OPS(cros_ec_ishtp_pm_ops, cros_ec_ishtp_suspend, + cros_ec_ishtp_resume); + +static struct ishtp_cl_driver cros_ec_ishtp_driver = { + .name = "cros_ec_ishtp", + .id = cros_ec_ishtp_id_table, + .probe = cros_ec_ishtp_probe, + .remove = cros_ec_ishtp_remove, + .reset = cros_ec_ishtp_reset, + .driver = { + .pm = &cros_ec_ishtp_pm_ops, + }, +}; + +static int __init cros_ec_ishtp_mod_init(void) +{ + return ishtp_cl_driver_register(&cros_ec_ishtp_driver, THIS_MODULE); +} + +static void __exit cros_ec_ishtp_mod_exit(void) +{ + ishtp_cl_driver_unregister(&cros_ec_ishtp_driver); +} + +module_init(cros_ec_ishtp_mod_init); +module_exit(cros_ec_ishtp_mod_exit); + +MODULE_DESCRIPTION("ChromeOS EC ISHTP Client Driver"); +MODULE_AUTHOR("Rushikesh S Kadam "); + +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/chrome/cros_ec_lightbar.c b/drivers/platform/chrome/cros_ec_lightbar.c new file mode 100644 index 000000000..96dde19b1 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_lightbar.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Expose the Chromebook Pixel lightbar to userspace +// +// Copyright (C) 2014 Google, Inc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-lightbar" + +/* Rate-limit the lightbar interface to prevent DoS. */ +static unsigned long lb_interval_jiffies = 50 * HZ / 1000; + +/* + * Whether or not we have given userspace control of the lightbar. + * If this is true, we won't do anything during suspend/resume. + */ +static bool userspace_control; + +/* + * Whether or not the lightbar supports the manual suspend commands. + * The Pixel 2013 (Link) does not while all other devices with a + * lightbar do. + */ +static bool has_manual_suspend; + +/* + * Lightbar version + */ +static int lb_version; + +static ssize_t interval_msec_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + unsigned long msec = lb_interval_jiffies * 1000 / HZ; + + return sysfs_emit(buf, "%lu\n", msec); +} + +static ssize_t interval_msec_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long msec; + + if (kstrtoul(buf, 0, &msec)) + return -EINVAL; + + lb_interval_jiffies = msec * HZ / 1000; + + return count; +} + +static DEFINE_MUTEX(lb_mutex); +/* Return 0 if able to throttle correctly, error otherwise */ +static int lb_throttle(void) +{ + static unsigned long last_access; + unsigned long now, next_timeslot; + long delay; + int ret = 0; + + mutex_lock(&lb_mutex); + + now = jiffies; + next_timeslot = last_access + lb_interval_jiffies; + + if (time_before(now, next_timeslot)) { + delay = (long)(next_timeslot) - (long)now; + set_current_state(TASK_INTERRUPTIBLE); + if (schedule_timeout(delay) > 0) { + /* interrupted - just abort */ + ret = -EINTR; + goto out; + } + now = jiffies; + } + + last_access = now; +out: + mutex_unlock(&lb_mutex); + + return ret; +} + +static struct cros_ec_command *alloc_lightbar_cmd_msg(struct cros_ec_dev *ec) +{ + int len = max(ec->ec_dev->max_response, ec->ec_dev->max_request); + struct cros_ec_command *msg; + + msg = kmalloc(sizeof(*msg) + len, GFP_KERNEL); + if (!msg) + return NULL; + + msg->version = 0; + msg->command = EC_CMD_LIGHTBAR_CMD + ec->cmd_offset; + /* + * Default sizes for regular commands. + * Can be set smaller to optimize transfer, + * larger when sending large light sequences. + */ + msg->outsize = sizeof(struct ec_params_lightbar); + msg->insize = sizeof(struct ec_response_lightbar); + + return msg; +} + +static int get_lightbar_version(struct cros_ec_dev *ec, + uint32_t *ver_ptr, uint32_t *flg_ptr) +{ + struct ec_params_lightbar *param; + struct ec_response_lightbar *resp; + struct cros_ec_command *msg; + int ret; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return 0; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_VERSION; + msg->outsize = sizeof(param->cmd); + msg->insize = sizeof(resp->version); + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0 && ret != -EINVAL) { + ret = 0; + goto exit; + } + + switch (msg->result) { + case EC_RES_INVALID_PARAM: + /* Pixel had no version command. */ + if (ver_ptr) + *ver_ptr = 0; + if (flg_ptr) + *flg_ptr = 0; + ret = 1; + goto exit; + + case EC_RES_SUCCESS: + resp = (struct ec_response_lightbar *)msg->data; + + /* Future devices w/lightbars should implement this command */ + if (ver_ptr) + *ver_ptr = resp->version.num; + if (flg_ptr) + *flg_ptr = resp->version.flags; + ret = 1; + goto exit; + } + + /* Anything else (ie, EC_RES_INVALID_COMMAND) - no lightbar */ + ret = 0; +exit: + kfree(msg); + return ret; +} + +static ssize_t version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + uint32_t version = 0, flags = 0; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + int ret; + + ret = lb_throttle(); + if (ret) + return ret; + + /* This should always succeed, because we check during init. */ + if (!get_lightbar_version(ec, &version, &flags)) + return -EIO; + + return sysfs_emit(buf, "%d %d\n", version, flags); +} + +static ssize_t num_segments_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ec_params_lightbar *param; + struct ec_response_lightbar *resp; + struct cros_ec_command *msg; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + uint32_t num = 0; + int ret; + + ret = lb_throttle(); + if (ret) + return ret; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_GET_PARAMS_V3; + msg->outsize = sizeof(param->cmd); + msg->insize = sizeof(resp->get_params_v3); + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0 && ret != -EINVAL) + goto exit; + + if (msg->result == EC_RES_SUCCESS) { + resp = (struct ec_response_lightbar *)msg->data; + num = resp->get_params_v3.reported_led_num; + } + + /* + * Anything else (ie, EC_RES_INVALID_COMMAND) - no direct control over + * LEDs, return that no leds are supported. + */ + ret = sysfs_emit(buf, "%u\n", num); +exit: + kfree(msg); + return ret; +} + +static ssize_t brightness_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + int ret; + unsigned int val; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + if (kstrtouint(buf, 0, &val)) + return -EINVAL; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_SET_BRIGHTNESS; + param->set_brightness.num = val; + ret = lb_throttle(); + if (ret) + goto exit; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + + ret = count; +exit: + kfree(msg); + return ret; +} + + +/* + * We expect numbers, and we'll keep reading until we find them, skipping over + * any whitespace (sysfs guarantees that the input is null-terminated). Every + * four numbers are sent to the lightbar as . We fail at the first + * parsing error, if we don't parse any numbers, or if we have numbers left + * over. + */ +static ssize_t led_rgb_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + unsigned int val[4]; + int ret, i = 0, j = 0, ok = 0; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + do { + /* Skip any whitespace */ + while (*buf && isspace(*buf)) + buf++; + + if (!*buf) + break; + + ret = sscanf(buf, "%i", &val[i++]); + if (ret == 0) + goto exit; + + if (i == 4) { + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_SET_RGB; + param->set_rgb.led = val[0]; + param->set_rgb.red = val[1]; + param->set_rgb.green = val[2]; + param->set_rgb.blue = val[3]; + /* + * Throttle only the first of every four transactions, + * so that the user can update all four LEDs at once. + */ + if ((j++ % 4) == 0) { + ret = lb_throttle(); + if (ret) + goto exit; + } + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + + i = 0; + ok = 1; + } + + /* Skip over the number we just read */ + while (*buf && !isspace(*buf)) + buf++; + + } while (*buf); + +exit: + kfree(msg); + return (ok && i == 0) ? count : -EINVAL; +} + +static char const *seqname[] = { + "ERROR", "S5", "S3", "S0", "S5S3", "S3S0", + "S0S3", "S3S5", "STOP", "RUN", "KONAMI", + "TAP", "PROGRAM", +}; + +static ssize_t sequence_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ec_params_lightbar *param; + struct ec_response_lightbar *resp; + struct cros_ec_command *msg; + int ret; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_GET_SEQ; + ret = lb_throttle(); + if (ret) + goto exit; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + ret = sysfs_emit(buf, "XFER / EC ERROR %d / %d\n", ret, msg->result); + goto exit; + } + + resp = (struct ec_response_lightbar *)msg->data; + if (resp->get_seq.num >= ARRAY_SIZE(seqname)) + ret = sysfs_emit(buf, "%d\n", resp->get_seq.num); + else + ret = sysfs_emit(buf, "%s\n", seqname[resp->get_seq.num]); + +exit: + kfree(msg); + return ret; +} + +static int lb_send_empty_cmd(struct cros_ec_dev *ec, uint8_t cmd) +{ + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + int ret; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = cmd; + + ret = lb_throttle(); + if (ret) + goto error; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto error; + + ret = 0; +error: + kfree(msg); + + return ret; +} + +static int lb_manual_suspend_ctrl(struct cros_ec_dev *ec, uint8_t enable) +{ + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + int ret; + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + + param->cmd = LIGHTBAR_CMD_MANUAL_SUSPEND_CTRL; + param->manual_suspend_ctrl.enable = enable; + + ret = lb_throttle(); + if (ret) + goto error; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto error; + + ret = 0; +error: + kfree(msg); + + return ret; +} + +static ssize_t sequence_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + unsigned int num; + int ret, len; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + for (len = 0; len < count; len++) + if (!isalnum(buf[len])) + break; + + for (num = 0; num < ARRAY_SIZE(seqname); num++) + if (!strncasecmp(seqname[num], buf, len)) + break; + + if (num >= ARRAY_SIZE(seqname)) { + ret = kstrtouint(buf, 0, &num); + if (ret) + return ret; + } + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_lightbar *)msg->data; + param->cmd = LIGHTBAR_CMD_SEQ; + param->seq.num = num; + msg->outsize = offsetof(typeof(*param), seq) + sizeof(param->seq); + msg->insize = 0; + ret = lb_throttle(); + if (ret) + goto exit; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + + ret = count; +exit: + kfree(msg); + return ret; +} + +static ssize_t program_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + size_t extra_bytes, max_size; + struct ec_params_lightbar *param; + struct cros_ec_command *msg; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + int ret; + + /* + * We might need to reject the program for size reasons. The EC + * enforces a maximum program size, but we also don't want to try + * and send a program that is too big for the protocol. In order + * to ensure the latter, we also need to ensure we have extra bytes + * to represent the rest of the packet. + * With V3, larger program can be sent, limited only by the EC. + * Only the protocol limit the payload size. + */ + if (lb_version < 3) { + extra_bytes = sizeof(*param) - sizeof(param->set_program.data); + max_size = min(EC_LB_PROG_LEN, ec->ec_dev->max_request - extra_bytes); + if (count > max_size) { + dev_err(dev, "Program is %zu bytes, too long to send (max: %zu)", + count, max_size); + + return -EINVAL; + } + } else { + /* + * Bound the payload strictly by the maximum value the structural + * size field can natively support. + */ + extra_bytes = offsetof(typeof(*param), set_program_ex) + + sizeof(param->set_program_ex); + max_size = min_t(size_t, ec->ec_dev->max_request - extra_bytes, + type_max(typeof(param->set_program_ex.size))); + } + + msg = alloc_lightbar_cmd_msg(ec); + if (!msg) + return -ENOMEM; + + ret = lb_throttle(); + if (ret) + goto exit; + param = (struct ec_params_lightbar *)msg->data; + msg->insize = 0; + + if (lb_version < 3) { + dev_info(dev, "Copying %zu byte program to EC", count); + + param->cmd = LIGHTBAR_CMD_SET_PROGRAM; + + param->set_program.size = count; + memcpy(param->set_program.data, buf, count); + + /* + * We need to set the message size manually or else it will use + * EC_LB_PROG_LEN. This might be too long, and the program + * is unlikely to use all of the space. + */ + msg->outsize = count + extra_bytes; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + } else { + size_t offset = 0; + size_t payload = 0; + + param->cmd = LIGHTBAR_CMD_SET_PROGRAM_EX; + while (offset < count) { + payload = min(max_size, count - offset); + param->set_program_ex.offset = offset; + param->set_program_ex.size = payload; + memcpy(param->set_program_ex.data, &buf[offset], payload); + msg->outsize = payload + extra_bytes; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + offset += payload; + } + } + ret = count; +exit: + kfree(msg); + + return ret; +} + +static ssize_t userspace_control_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", userspace_control); +} + +static ssize_t userspace_control_store(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + userspace_control = enable; + + return count; +} + +/* Module initialization */ + +static DEVICE_ATTR_RW(interval_msec); +static DEVICE_ATTR_RO(num_segments); +static DEVICE_ATTR_RO(version); +static DEVICE_ATTR_WO(brightness); +static DEVICE_ATTR_WO(led_rgb); +static DEVICE_ATTR_RW(sequence); +static DEVICE_ATTR_WO(program); +static DEVICE_ATTR_RW(userspace_control); + +static struct attribute *__lb_cmds_attrs[] = { + &dev_attr_interval_msec.attr, + &dev_attr_num_segments.attr, + &dev_attr_version.attr, + &dev_attr_brightness.attr, + &dev_attr_led_rgb.attr, + &dev_attr_sequence.attr, + &dev_attr_program.attr, + &dev_attr_userspace_control.attr, + NULL, +}; + +static const struct attribute_group cros_ec_lightbar_attr_group = { + .name = "lightbar", + .attrs = __lb_cmds_attrs, +}; + +static int cros_ec_lightbar_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + struct cros_ec_platform *pdata = dev_get_platdata(ec_dev->dev); + struct device *dev = &pd->dev; + int ret; + + /* + * Only instantiate the lightbar if the EC name is 'cros_ec'. Other EC + * devices like 'cros_pd' doesn't have a lightbar. + */ + if (strcmp(pdata->ec_name, CROS_EC_DEV_NAME) != 0) + return -ENODEV; + + /* + * Ask then for the lightbar version, if it's 0 then the 'cros_ec' + * doesn't have a lightbar. + */ + if (!get_lightbar_version(ec_dev, &lb_version, NULL)) + return -ENODEV; + + /* Take control of the lightbar from the EC. */ + has_manual_suspend = (lb_manual_suspend_ctrl(ec_dev, 1) != -EINVAL); + + ret = sysfs_create_group(&ec_dev->class_dev.kobj, + &cros_ec_lightbar_attr_group); + if (ret < 0) + dev_err(dev, "failed to create %s attributes. err=%d\n", + cros_ec_lightbar_attr_group.name, ret); + + return ret; +} + +static void cros_ec_lightbar_remove(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + + sysfs_remove_group(&ec_dev->class_dev.kobj, + &cros_ec_lightbar_attr_group); + + /* Let the EC take over the lightbar again. */ + if (has_manual_suspend) + lb_manual_suspend_ctrl(ec_dev, 0); +} + +static int __maybe_unused cros_ec_lightbar_resume(struct device *dev) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); + + if (userspace_control || !has_manual_suspend) + return 0; + + return lb_send_empty_cmd(ec_dev, LIGHTBAR_CMD_RESUME); +} + +static int __maybe_unused cros_ec_lightbar_suspend(struct device *dev) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); + + if (userspace_control || !has_manual_suspend) + return 0; + + return lb_send_empty_cmd(ec_dev, LIGHTBAR_CMD_SUSPEND); +} + +static SIMPLE_DEV_PM_OPS(cros_ec_lightbar_pm_ops, + cros_ec_lightbar_suspend, cros_ec_lightbar_resume); + +static const struct platform_device_id cros_ec_lightbar_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_lightbar_id); + +static struct platform_driver cros_ec_lightbar_driver = { + .driver = { + .name = DRV_NAME, + .pm = &cros_ec_lightbar_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .probe = cros_ec_lightbar_probe, + .remove = cros_ec_lightbar_remove, + .id_table = cros_ec_lightbar_id, +}; + +module_platform_driver(cros_ec_lightbar_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Expose the Chromebook Pixel's lightbar to userspace"); diff --git a/drivers/platform/chrome/cros_ec_lpc.c b/drivers/platform/chrome/cros_ec_lpc.c new file mode 100644 index 000000000..78cfff80c --- /dev/null +++ b/drivers/platform/chrome/cros_ec_lpc.c @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: GPL-2.0 +// LPC interface for ChromeOS Embedded Controller +// +// Copyright (C) 2012-2015 Google, Inc +// +// This driver uses the ChromeOS EC byte-level message-based protocol for +// communicating the keyboard state (which keys are pressed) from a keyboard EC +// to the AP over some bus (such as i2c, lpc, spi). The EC does debouncing, +// but everything else (including deghosting) is done here. The main +// motivation for this is to keep the EC firmware as simple as possible, since +// it cannot be easily upgraded and EC flash/IRAM space is relatively +// expensive. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" +#include "cros_ec_lpc_mec.h" + +#define DRV_NAME "cros_ec_lpcs" +#define ACPI_DRV_NAME "GOOG0004" +#define FRMW_ACPI_DRV_NAME "FRMWC004" + +/* True if ACPI device is present */ +static bool cros_ec_lpc_acpi_device_found; + +/* + * Indicates that lpc_driver_data.quirk_mmio_memory_base should + * be used as the base port for EC mapped memory. + */ +#define CROS_EC_LPC_QUIRK_REMAP_MEMORY BIT(0) +/* + * Indicates that lpc_driver_data.quirk_acpi_id should be used to find + * the ACPI device. + */ +#define CROS_EC_LPC_QUIRK_ACPI_ID BIT(1) +/* + * Indicates that lpc_driver_data.quirk_aml_mutex_name should be used + * to find an AML mutex to protect access to Microchip EC. + */ +#define CROS_EC_LPC_QUIRK_AML_MUTEX BIT(2) + +/** + * struct lpc_driver_data - driver data attached to a DMI device ID to indicate + * hardware quirks. + * @quirks: a bitfield composed of quirks from CROS_EC_LPC_QUIRK_* + * @quirk_mmio_memory_base: The first I/O port addressing EC mapped memory (used + * when quirk ...REMAP_MEMORY is set.) + * @quirk_acpi_id: An ACPI HID to be used to find the ACPI device. + * @quirk_aml_mutex_name: The name of an AML mutex to be used to protect access + * to Microchip EC. + */ +struct lpc_driver_data { + u32 quirks; + u16 quirk_mmio_memory_base; + const char *quirk_acpi_id; + const char *quirk_aml_mutex_name; +}; + +/** + * struct cros_ec_lpc - LPC device-specific data + * @mmio_memory_base: The first I/O port addressing EC mapped memory. + * @base: For EC supporting memory mapping, base address of the mapped region. + * @mem32: Information about the memory mapped register region, if present. + * @read: Copy length bytes from EC address offset into buffer dest. + * Returns a negative error code on error, or the 8-bit checksum + * of all bytes read. + * @write: Copy length bytes from buffer msg into EC address offset. + * Returns a negative error code on error, or the 8-bit checksum + * of all bytes written. + */ +struct cros_ec_lpc { + u16 mmio_memory_base; + void __iomem *base; + struct acpi_resource_fixed_memory32 mem32; + int (*read)(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, u8 *dest); + int (*write)(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, const u8 *msg); +}; + +/* + * A generic instance of the read function of struct lpc_driver_ops, used for + * the LPC EC. + */ +static int cros_ec_lpc_read_bytes(struct cros_ec_lpc *_, unsigned int offset, unsigned int length, + u8 *dest) +{ + u8 sum = 0; + int i; + + for (i = 0; i < length; ++i) { + dest[i] = inb(offset + i); + sum += dest[i]; + } + + /* Return checksum of all bytes read */ + return sum; +} + +/* + * A generic instance of the write function of struct lpc_driver_ops, used for + * the LPC EC. + */ +static int cros_ec_lpc_write_bytes(struct cros_ec_lpc *_, unsigned int offset, unsigned int length, + const u8 *msg) +{ + u8 sum = 0; + int i; + + for (i = 0; i < length; ++i) { + outb(msg[i], offset + i); + sum += msg[i]; + } + + /* Return checksum of all bytes written */ + return sum; +} + +/* + * An instance of the read function of struct lpc_driver_ops, used for the + * MEC variant of LPC EC. + */ +static int cros_ec_lpc_mec_read_bytes(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, u8 *dest) +{ + int in_range = cros_ec_lpc_mec_in_range(offset, length); + + if (in_range < 0) + return in_range; + + return in_range ? + cros_ec_lpc_io_bytes_mec(MEC_IO_READ, + offset - EC_HOST_CMD_REGION0, + length, dest) : + cros_ec_lpc_read_bytes(ec_lpc, offset, length, dest); +} + +/* + * An instance of the write function of struct lpc_driver_ops, used for the + * MEC variant of LPC EC. + */ +static int cros_ec_lpc_mec_write_bytes(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, const u8 *msg) +{ + int in_range = cros_ec_lpc_mec_in_range(offset, length); + + if (in_range < 0) + return in_range; + + return in_range ? + cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, + offset - EC_HOST_CMD_REGION0, + length, (u8 *)msg) : + cros_ec_lpc_write_bytes(ec_lpc, offset, length, msg); +} + +static int cros_ec_lpc_direct_read(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, u8 *dest) +{ + int sum = 0; + int i; + + if (offset < EC_HOST_CMD_REGION0 || offset > EC_LPC_ADDR_MEMMAP + + EC_MEMMAP_SIZE) { + return cros_ec_lpc_read_bytes(ec_lpc, offset, length, dest); + } + + for (i = 0; i < length; ++i) { + dest[i] = readb(ec_lpc->base + offset - EC_HOST_CMD_REGION0 + i); + sum += dest[i]; + } + + /* Return checksum of all bytes read */ + return sum; +} + +static int cros_ec_lpc_direct_write(struct cros_ec_lpc *ec_lpc, unsigned int offset, + unsigned int length, const u8 *msg) +{ + int sum = 0; + int i; + + if (offset < EC_HOST_CMD_REGION0 || offset > EC_LPC_ADDR_MEMMAP + + EC_MEMMAP_SIZE) { + return cros_ec_lpc_write_bytes(ec_lpc, offset, length, msg); + } + + for (i = 0; i < length; ++i) { + writeb(msg[i], ec_lpc->base + offset - EC_HOST_CMD_REGION0 + i); + sum += msg[i]; + } + + /* Return checksum of all bytes written */ + return sum; +} + +static int ec_response_timed_out(struct cros_ec_lpc *ec_lpc) +{ + unsigned long one_second = jiffies + HZ; + u8 data; + int ret; + + usleep_range(200, 300); + do { + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_CMD, 1, &data); + if (ret < 0) + return ret; + if (!(data & EC_LPC_STATUS_BUSY_MASK)) + return 0; + usleep_range(100, 200); + } while (time_before(jiffies, one_second)); + + return 1; +} + +static int cros_ec_pkt_xfer_lpc(struct cros_ec_device *ec, + struct cros_ec_command *msg) +{ + struct cros_ec_lpc *ec_lpc = ec->priv; + struct ec_host_response response; + u8 sum; + int ret = 0; + u8 *dout; + + ret = cros_ec_prepare_tx(ec, msg); + if (ret < 0) + goto done; + + /* Write buffer */ + ret = ec_lpc->write(ec_lpc, EC_LPC_ADDR_HOST_PACKET, ret, ec->dout); + if (ret < 0) + goto done; + + /* Here we go */ + sum = EC_COMMAND_PROTOCOL_3; + ret = ec_lpc->write(ec_lpc, EC_LPC_ADDR_HOST_CMD, 1, &sum); + if (ret < 0) + goto done; + + ret = ec_response_timed_out(ec_lpc); + if (ret < 0) + goto done; + if (ret) { + dev_warn(ec->dev, "EC response timed out\n"); + ret = -EIO; + goto done; + } + + /* Check result */ + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_DATA, 1, &sum); + if (ret < 0) + goto done; + msg->result = ret; + ret = cros_ec_check_result(ec, msg); + if (ret) + goto done; + + /* Read back response */ + dout = (u8 *)&response; + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_PACKET, sizeof(response), + dout); + if (ret < 0) + goto done; + sum = ret; + + msg->result = response.result; + + if (response.data_len > msg->insize) { + dev_err(ec->dev, + "packet too long (%d bytes, expected %d)", + response.data_len, msg->insize); + ret = -EMSGSIZE; + goto done; + } + + /* Read response and process checksum */ + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_PACKET + + sizeof(response), response.data_len, + msg->data); + if (ret < 0) + goto done; + sum += ret; + + if (sum) { + dev_err(ec->dev, + "bad packet checksum %02x\n", + response.checksum); + ret = -EBADMSG; + goto done; + } + + /* Return actual amount of data received */ + ret = response.data_len; +done: + return ret; +} + +static int cros_ec_cmd_xfer_lpc(struct cros_ec_device *ec, + struct cros_ec_command *msg) +{ + struct cros_ec_lpc *ec_lpc = ec->priv; + struct ec_lpc_host_args args; + u8 sum; + int ret = 0; + + if (msg->outsize > EC_PROTO2_MAX_PARAM_SIZE || + msg->insize > EC_PROTO2_MAX_PARAM_SIZE) { + dev_err(ec->dev, + "invalid buffer sizes (out %d, in %d)\n", + msg->outsize, msg->insize); + return -EINVAL; + } + + /* Now actually send the command to the EC and get the result */ + args.flags = EC_HOST_ARGS_FLAG_FROM_HOST; + args.command_version = msg->version; + args.data_size = msg->outsize; + + /* Initialize checksum */ + sum = msg->command + args.flags + args.command_version + args.data_size; + + /* Copy data and update checksum */ + ret = ec_lpc->write(ec_lpc, EC_LPC_ADDR_HOST_PARAM, msg->outsize, + msg->data); + if (ret < 0) + goto done; + sum += ret; + + /* Finalize checksum and write args */ + args.checksum = sum; + ret = ec_lpc->write(ec_lpc, EC_LPC_ADDR_HOST_ARGS, sizeof(args), + (u8 *)&args); + if (ret < 0) + goto done; + + /* Here we go */ + sum = msg->command; + ret = ec_lpc->write(ec_lpc, EC_LPC_ADDR_HOST_CMD, 1, &sum); + if (ret < 0) + goto done; + + ret = ec_response_timed_out(ec_lpc); + if (ret < 0) + goto done; + if (ret) { + dev_warn(ec->dev, "EC response timed out\n"); + ret = -EIO; + goto done; + } + + /* Check result */ + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_DATA, 1, &sum); + if (ret < 0) + goto done; + msg->result = ret; + ret = cros_ec_check_result(ec, msg); + if (ret) + goto done; + + /* Read back args */ + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_ARGS, sizeof(args), (u8 *)&args); + if (ret < 0) + goto done; + + if (args.data_size > msg->insize) { + dev_err(ec->dev, + "packet too long (%d bytes, expected %d)", + args.data_size, msg->insize); + ret = -ENOSPC; + goto done; + } + + /* Start calculating response checksum */ + sum = msg->command + args.flags + args.command_version + args.data_size; + + /* Read response and update checksum */ + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_HOST_PARAM, args.data_size, + msg->data); + if (ret < 0) + goto done; + sum += ret; + + /* Verify checksum */ + if (args.checksum != sum) { + dev_err(ec->dev, + "bad packet checksum, expected %02x, got %02x\n", + args.checksum, sum); + ret = -EBADMSG; + goto done; + } + + /* Return actual amount of data received */ + ret = args.data_size; +done: + return ret; +} + +/* Returns num bytes read, or negative on error. Doesn't need locking. */ +static int cros_ec_lpc_readmem(struct cros_ec_device *ec, unsigned int offset, + unsigned int bytes, void *dest) +{ + struct cros_ec_lpc *ec_lpc = ec->priv; + int i = offset; + char *s = dest; + int cnt = 0; + int ret; + + if (offset >= EC_MEMMAP_SIZE - bytes) + return -EINVAL; + + /* fixed length */ + if (bytes) { + ret = ec_lpc->read(ec_lpc, ec_lpc->mmio_memory_base + offset, bytes, s); + if (ret < 0) + return ret; + return bytes; + } + + /* string */ + for (; i < EC_MEMMAP_SIZE; i++, s++) { + ret = ec_lpc->read(ec_lpc, ec_lpc->mmio_memory_base + i, 1, s); + if (ret < 0) + return ret; + cnt++; + if (!*s) + break; + } + + return cnt; +} + +static void cros_ec_lpc_acpi_notify(acpi_handle device, u32 value, void *data) +{ + static const char *env[] = { "ERROR=PANIC", NULL }; + struct cros_ec_device *ec_dev = data; + bool ec_has_more_events; + int ret; + + ec_dev->last_event_time = cros_ec_get_time_ns(); + + if (value == ACPI_NOTIFY_CROS_EC_PANIC) { + dev_emerg(ec_dev->dev, "CrOS EC Panic Reported. Shutdown is imminent!"); + blocking_notifier_call_chain(&ec_dev->panic_notifier, 0, ec_dev); + kobject_uevent_env(&ec_dev->dev->kobj, KOBJ_CHANGE, (char **)env); + /* Begin orderly shutdown. EC will force reset after a short period. */ + __hw_protection_trigger("CrOS EC Panic", -1, HWPROT_ACT_SHUTDOWN); + /* Do not query for other events after a panic is reported */ + return; + } + + if (value == ACPI_NOTIFY_CROS_EC_MKBP && ec_dev->mkbp_event_supported) + do { + ret = cros_ec_get_next_event(ec_dev, NULL, + &ec_has_more_events); + if (ret > 0) + blocking_notifier_call_chain( + &ec_dev->event_notifier, 0, + ec_dev); + } while (ec_has_more_events); + + if (value == ACPI_NOTIFY_DEVICE_WAKE) + pm_system_wakeup(); +} + +static acpi_status cros_ec_lpc_parse_device(acpi_handle handle, u32 level, + void *context, void **retval) +{ + *(struct acpi_device **)context = acpi_fetch_acpi_dev(handle); + return AE_CTRL_TERMINATE; +} + +static struct acpi_device *cros_ec_lpc_get_device(const char *id) +{ + struct acpi_device *adev = NULL; + acpi_status status = acpi_get_devices(id, cros_ec_lpc_parse_device, + &adev, NULL); + if (ACPI_FAILURE(status)) { + pr_warn(DRV_NAME ": Looking for %s failed\n", id); + return NULL; + } + + return adev; +} + +static acpi_status cros_ec_lpc_resources(struct acpi_resource *res, void *data) +{ + struct cros_ec_lpc *ec_lpc = data; + + switch (res->type) { + case ACPI_RESOURCE_TYPE_FIXED_MEMORY32: + ec_lpc->mem32 = res->data.fixed_memory32; + break; + default: + break; + } + return AE_OK; +} + +static int cros_ec_lpc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct acpi_device *adev; + acpi_status status; + struct cros_ec_device *ec_dev; + struct cros_ec_lpc *ec_lpc; + const struct lpc_driver_data *driver_data; + u8 buf[2] = {}; + int irq, ret; + u32 quirks; + + ec_lpc = devm_kzalloc(dev, sizeof(*ec_lpc), GFP_KERNEL); + if (!ec_lpc) + return -ENOMEM; + + ec_lpc->mmio_memory_base = EC_LPC_ADDR_MEMMAP; + + driver_data = platform_get_drvdata(pdev); + if (!driver_data) + driver_data = acpi_device_get_match_data(dev); + + if (driver_data) { + quirks = driver_data->quirks; + + if (quirks) + dev_info(dev, "loaded with quirks %8.08x\n", quirks); + + if (quirks & CROS_EC_LPC_QUIRK_REMAP_MEMORY) + ec_lpc->mmio_memory_base = driver_data->quirk_mmio_memory_base; + + if (quirks & CROS_EC_LPC_QUIRK_ACPI_ID) { + adev = cros_ec_lpc_get_device(driver_data->quirk_acpi_id); + if (!adev) { + dev_err(dev, "failed to get ACPI device '%s'", + driver_data->quirk_acpi_id); + return -ENODEV; + } + ACPI_COMPANION_SET(dev, adev); + } + + if (quirks & CROS_EC_LPC_QUIRK_AML_MUTEX) { + const char *name = driver_data->quirk_aml_mutex_name; + ret = cros_ec_lpc_mec_acpi_mutex(ACPI_COMPANION(dev), name); + if (ret) { + dev_err(dev, "failed to get AML mutex '%s'", name); + return ret; + } + dev_info(dev, "got AML mutex '%s'", name); + } + } + adev = ACPI_COMPANION(dev); + if (adev) { + /* + * Retrieve the resource information in the CRS register, if available. + */ + status = acpi_walk_resources(adev->handle, METHOD_NAME__CRS, + cros_ec_lpc_resources, ec_lpc); + if (ACPI_SUCCESS(status) && ec_lpc->mem32.address_length) { + ec_lpc->base = devm_ioremap(dev, + ec_lpc->mem32.address, + ec_lpc->mem32.address_length); + if (!ec_lpc->base) + return -EINVAL; + + ec_lpc->read = cros_ec_lpc_direct_read; + ec_lpc->write = cros_ec_lpc_direct_write; + } + } + if (!ec_lpc->read) { + /* + * The Framework Laptop (and possibly other non-ChromeOS devices) + * only exposes the eight I/O ports that are required for the Microchip EC. + * Requesting a larger reservation will fail. + */ + if (!devm_request_region(dev, EC_HOST_CMD_REGION0, + EC_HOST_CMD_MEC_REGION_SIZE, dev_name(dev))) { + dev_err(dev, "couldn't reserve MEC region\n"); + return -EBUSY; + } + + cros_ec_lpc_mec_init(EC_HOST_CMD_REGION0, + EC_LPC_ADDR_MEMMAP + EC_MEMMAP_SIZE); + + /* + * Read the mapped ID twice, the first one is assuming the + * EC is a Microchip Embedded Controller (MEC) variant, if the + * protocol fails, fallback to the non MEC variant and try to + * read again the ID. + */ + ec_lpc->read = cros_ec_lpc_mec_read_bytes; + ec_lpc->write = cros_ec_lpc_mec_write_bytes; + } + ret = ec_lpc->read(ec_lpc, EC_LPC_ADDR_MEMMAP + EC_MEMMAP_ID, 2, buf); + if (ret < 0) + return ret; + if (buf[0] != 'E' || buf[1] != 'C') { + if (!devm_request_region(dev, ec_lpc->mmio_memory_base, EC_MEMMAP_SIZE, + dev_name(dev))) { + dev_err(dev, "couldn't reserve memmap region\n"); + return -EBUSY; + } + + /* Re-assign read/write operations for the non MEC variant */ + ec_lpc->read = cros_ec_lpc_read_bytes; + ec_lpc->write = cros_ec_lpc_write_bytes; + ret = ec_lpc->read(ec_lpc, ec_lpc->mmio_memory_base + EC_MEMMAP_ID, 2, + buf); + if (ret < 0) + return ret; + if (buf[0] != 'E' || buf[1] != 'C') { + dev_err(dev, "EC ID not detected\n"); + return -ENODEV; + } + + /* Reserve the remaining I/O ports required by the non-MEC protocol. */ + if (!devm_request_region(dev, EC_HOST_CMD_REGION0 + EC_HOST_CMD_MEC_REGION_SIZE, + EC_HOST_CMD_REGION_SIZE - EC_HOST_CMD_MEC_REGION_SIZE, + dev_name(dev))) { + dev_err(dev, "couldn't reserve remainder of region0\n"); + return -EBUSY; + } + if (!devm_request_region(dev, EC_HOST_CMD_REGION1, + EC_HOST_CMD_REGION_SIZE, dev_name(dev))) { + dev_err(dev, "couldn't reserve region1\n"); + return -EBUSY; + } + } + + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + platform_set_drvdata(pdev, ec_dev); + ec_dev->phys_name = dev_name(dev); + ec_dev->cmd_xfer = cros_ec_cmd_xfer_lpc; + ec_dev->pkt_xfer = cros_ec_pkt_xfer_lpc; + ec_dev->cmd_readmem = cros_ec_lpc_readmem; + ec_dev->priv = ec_lpc; + + /* + * Some boards do not have an IRQ allotted for cros_ec_lpc, + * which makes ENXIO an expected (and safe) scenario. + */ + irq = platform_get_irq_optional(pdev, 0); + if (irq > 0) + ec_dev->irq = irq; + else if (irq != -ENXIO) { + dev_err(dev, "couldn't retrieve IRQ number (%d)\n", irq); + return irq; + } + + ret = cros_ec_register(ec_dev); + if (ret) { + dev_err(dev, "couldn't register ec_dev (%d)\n", ret); + return ret; + } + + /* + * Connect a notify handler to process MKBP messages if we have a + * companion ACPI device. + */ + if (adev) { + status = acpi_install_notify_handler(adev->handle, + ACPI_ALL_NOTIFY, + cros_ec_lpc_acpi_notify, + ec_dev); + if (ACPI_FAILURE(status)) + dev_warn(dev, "Failed to register notifier %08x\n", + status); + } + + return 0; +} + +static void cros_ec_lpc_remove(struct platform_device *pdev) +{ + struct cros_ec_device *ec_dev = platform_get_drvdata(pdev); + struct acpi_device *adev; + + adev = ACPI_COMPANION(&pdev->dev); + if (adev) + acpi_remove_notify_handler(adev->handle, ACPI_ALL_NOTIFY, + cros_ec_lpc_acpi_notify); + + cros_ec_unregister(ec_dev); +} + +static const struct lpc_driver_data framework_laptop_npcx_lpc_driver_data __initconst = { + .quirks = CROS_EC_LPC_QUIRK_REMAP_MEMORY, + .quirk_mmio_memory_base = 0xE00, +}; + +static const struct lpc_driver_data framework_laptop_mec_lpc_driver_data __initconst = { + .quirks = CROS_EC_LPC_QUIRK_ACPI_ID|CROS_EC_LPC_QUIRK_AML_MUTEX, + .quirk_acpi_id = "PNP0C09", + .quirk_aml_mutex_name = "ECMT", +}; + +static const struct acpi_device_id cros_ec_lpc_acpi_device_ids[] = { + { ACPI_DRV_NAME, 0 }, + { FRMW_ACPI_DRV_NAME, (kernel_ulong_t)&framework_laptop_npcx_lpc_driver_data }, + { } +}; +MODULE_DEVICE_TABLE(acpi, cros_ec_lpc_acpi_device_ids); + +static const struct dmi_system_id cros_ec_lpc_dmi_table[] __initconst = { + { + /* + * Today all Chromebooks/boxes ship with Google_* as version and + * coreboot as bios vendor. No other systems with this + * combination are known to date. + */ + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_BIOS_VERSION, "Google_"), + }, + }, + { + /* + * If the box is running custom coreboot firmware then the + * DMI BIOS version string will not be matched by "Google_", + * but the system vendor string will still be matched by + * "GOOGLE". + */ + .matches = { + DMI_MATCH(DMI_BIOS_VENDOR, "coreboot"), + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + }, + }, + { + /* x86-link, the Chromebook Pixel. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Link"), + }, + }, + { + /* x86-samus, the Chromebook Pixel 2. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Samus"), + }, + }, + { + /* x86-peppy, the Acer C720 Chromebook. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Peppy"), + }, + }, + { + /* x86-glimmer, the Lenovo Thinkpad Yoga 11e. */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GOOGLE"), + DMI_MATCH(DMI_PRODUCT_NAME, "Glimmer"), + }, + }, + /* A small number of non-Chromebook/box machines also use the ChromeOS EC */ + { + /* Framework Laptop (11th Gen Intel Core) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Laptop"), + }, + .driver_data = (void *)&framework_laptop_mec_lpc_driver_data, + }, + { + /* Framework Laptop (12th Gen Intel Core) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Laptop (12th Gen Intel Core)"), + }, + .driver_data = (void *)&framework_laptop_mec_lpc_driver_data, + }, + { + /* Framework Laptop (13th Gen Intel Core) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Laptop (13th Gen Intel Core)"), + }, + .driver_data = (void *)&framework_laptop_mec_lpc_driver_data, + }, + { + /* + * All remaining Framework Laptop models (13 AMD Ryzen, 16 AMD + * Ryzen, Intel Core Ultra) + */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "Laptop"), + }, + .driver_data = (void *)&framework_laptop_npcx_lpc_driver_data, + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(dmi, cros_ec_lpc_dmi_table); + +#ifdef CONFIG_PM_SLEEP +static int cros_ec_lpc_prepare(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + return cros_ec_suspend_prepare(ec_dev); +} + +static void cros_ec_lpc_complete(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + cros_ec_resume_complete(ec_dev); +} + +static int cros_ec_lpc_suspend_late(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_suspend_late(ec_dev); +} + +static int cros_ec_lpc_resume_early(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_resume_early(ec_dev); +} +#endif + +static const struct dev_pm_ops cros_ec_lpc_pm_ops = { +#ifdef CONFIG_PM_SLEEP + .prepare = cros_ec_lpc_prepare, + .complete = cros_ec_lpc_complete, +#endif + SET_LATE_SYSTEM_SLEEP_PM_OPS(cros_ec_lpc_suspend_late, cros_ec_lpc_resume_early) +}; + +static struct platform_driver cros_ec_lpc_driver = { + .driver = { + .name = DRV_NAME, + .acpi_match_table = cros_ec_lpc_acpi_device_ids, + .pm = &cros_ec_lpc_pm_ops, + /* + * ACPI child devices may probe before us, and they racily + * check our drvdata pointer. Force synchronous probe until + * those races are resolved. + */ + .probe_type = PROBE_FORCE_SYNCHRONOUS, + }, + .probe = cros_ec_lpc_probe, + .remove = cros_ec_lpc_remove, +}; + +static struct platform_device cros_ec_lpc_device = { + .name = DRV_NAME +}; + +static int __init cros_ec_lpc_init(void) +{ + int ret; + const struct dmi_system_id *dmi_match; + + cros_ec_lpc_acpi_device_found = !!cros_ec_lpc_get_device(ACPI_DRV_NAME) || + !!cros_ec_lpc_get_device(FRMW_ACPI_DRV_NAME); + + dmi_match = dmi_first_match(cros_ec_lpc_dmi_table); + + if (!cros_ec_lpc_acpi_device_found && !dmi_match) { + pr_err(DRV_NAME ": unsupported system.\n"); + return -ENODEV; + } + + /* Register the driver */ + ret = platform_driver_register(&cros_ec_lpc_driver); + if (ret) { + pr_err(DRV_NAME ": can't register driver: %d\n", ret); + return ret; + } + + if (!cros_ec_lpc_acpi_device_found) { + /* Pass the DMI match's driver data down to the platform device */ + platform_set_drvdata(&cros_ec_lpc_device, dmi_match->driver_data); + + /* Register the device, and it'll get hooked up automatically */ + ret = platform_device_register(&cros_ec_lpc_device); + if (ret) { + pr_err(DRV_NAME ": can't register device: %d\n", ret); + platform_driver_unregister(&cros_ec_lpc_driver); + } + } + + return ret; +} + +static void __exit cros_ec_lpc_exit(void) +{ + if (!cros_ec_lpc_acpi_device_found) + platform_device_unregister(&cros_ec_lpc_device); + platform_driver_unregister(&cros_ec_lpc_driver); +} + +module_init(cros_ec_lpc_init); +module_exit(cros_ec_lpc_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS EC LPC driver"); diff --git a/drivers/platform/chrome/cros_ec_lpc_mec.c b/drivers/platform/chrome/cros_ec_lpc_mec.c new file mode 100644 index 000000000..a56584171 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_lpc_mec.c @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0 +// LPC variant I/O for Microchip EC +// +// Copyright (C) 2016 Google, Inc + +#include +#include +#include +#include + +#include "cros_ec_lpc_mec.h" + +#define ACPI_LOCK_DELAY_MS 500 + +/* + * This mutex must be held while accessing the EMI unit. We can't rely on the + * EC mutex because memmap data may be accessed without it being held. + */ +static DEFINE_MUTEX(io_mutex); +/* + * An alternative mutex to be used when the ACPI AML code may also + * access memmap data. When set, this mutex is used in preference to + * io_mutex. + */ +static acpi_handle aml_mutex; + +static u16 mec_emi_base, mec_emi_end; + +/** + * cros_ec_lpc_mec_lock() - Acquire mutex for EMI + * + * @return: Negative error code, or zero for success + */ +static int cros_ec_lpc_mec_lock(void) +{ + bool success; + + if (!aml_mutex) { + mutex_lock(&io_mutex); + return 0; + } + + success = ACPI_SUCCESS(acpi_acquire_mutex(aml_mutex, + NULL, ACPI_LOCK_DELAY_MS)); + if (!success) + return -EBUSY; + + return 0; +} + +/** + * cros_ec_lpc_mec_unlock() - Release mutex for EMI + * + * @return: Negative error code, or zero for success + */ +static int cros_ec_lpc_mec_unlock(void) +{ + bool success; + + if (!aml_mutex) { + mutex_unlock(&io_mutex); + return 0; + } + + success = ACPI_SUCCESS(acpi_release_mutex(aml_mutex, NULL)); + if (!success) + return -EBUSY; + + return 0; +} + +/** + * cros_ec_lpc_mec_emi_write_address() - Initialize EMI at a given address. + * + * @addr: Starting read / write address + * @access_type: Type of access, typically 32-bit auto-increment + */ +static void cros_ec_lpc_mec_emi_write_address(u16 addr, + enum cros_ec_lpc_mec_emi_access_mode access_type) +{ + outb((addr & 0xfc) | access_type, MEC_EMI_EC_ADDRESS_B0(mec_emi_base)); + outb((addr >> 8) & 0x7f, MEC_EMI_EC_ADDRESS_B1(mec_emi_base)); +} + +/** + * cros_ec_lpc_mec_in_range() - Determine if addresses are in MEC EMI range. + * + * @offset: Address offset + * @length: Number of bytes to check + * + * Return: 1 if in range, 0 if not, and -EINVAL on failure + * such as the mec range not being initialized + */ +int cros_ec_lpc_mec_in_range(unsigned int offset, unsigned int length) +{ + if (WARN_ON(mec_emi_base == 0 || mec_emi_end == 0)) + return -EINVAL; + + if (offset >= mec_emi_base && offset < mec_emi_end) { + if (WARN_ON(offset + length - 1 >= mec_emi_end)) + return -EINVAL; + return 1; + } + + if (WARN_ON(offset + length > mec_emi_base && offset < mec_emi_end)) + return -EINVAL; + + return 0; +} + +/** + * cros_ec_lpc_io_bytes_mec() - Read / write bytes to MEC EMI port. + * + * @io_type: MEC_IO_READ or MEC_IO_WRITE, depending on request + * @offset: Base read / write address + * @length: Number of bytes to read / write + * @buf: Destination / source buffer + * + * @return: A negative error code on error, or 8-bit checksum of all + * bytes read / written + */ +int cros_ec_lpc_io_bytes_mec(enum cros_ec_lpc_mec_io_type io_type, + unsigned int offset, unsigned int length, + u8 *buf) +{ + int i = 0; + int io_addr; + u8 sum = 0; + enum cros_ec_lpc_mec_emi_access_mode access, new_access; + int ret; + + if (length == 0) + return 0; + + /* Return checksum of 0 if window is not initialized */ + WARN_ON(mec_emi_base == 0 || mec_emi_end == 0); + if (mec_emi_base == 0 || mec_emi_end == 0) + return 0; + + /* + * Long access cannot be used on misaligned data since reading B0 loads + * the data register and writing B3 flushes. + */ + if (offset & 0x3 || length < 4) + access = ACCESS_TYPE_BYTE; + else + access = ACCESS_TYPE_LONG_AUTO_INCREMENT; + + ret = cros_ec_lpc_mec_lock(); + if (ret) + return ret; + + /* Initialize I/O at desired address */ + cros_ec_lpc_mec_emi_write_address(offset, access); + + /* Skip bytes in case of misaligned offset */ + io_addr = MEC_EMI_EC_DATA_B0(mec_emi_base) + (offset & 0x3); + while (i < length) { + while (io_addr <= MEC_EMI_EC_DATA_B3(mec_emi_base)) { + if (io_type == MEC_IO_READ) + buf[i] = inb(io_addr++); + else + outb(buf[i], io_addr++); + + sum += buf[i++]; + offset++; + + /* Extra bounds check in case of misaligned length */ + if (i == length) + goto done; + } + + /* + * Use long auto-increment access except for misaligned write, + * since writing B3 triggers the flush. + */ + if (length - i < 4 && io_type == MEC_IO_WRITE) + new_access = ACCESS_TYPE_BYTE; + else + new_access = ACCESS_TYPE_LONG_AUTO_INCREMENT; + + if (new_access != access || + access != ACCESS_TYPE_LONG_AUTO_INCREMENT) { + access = new_access; + cros_ec_lpc_mec_emi_write_address(offset, access); + } + + /* Access [B0, B3] on each loop pass */ + io_addr = MEC_EMI_EC_DATA_B0(mec_emi_base); + } + +done: + ret = cros_ec_lpc_mec_unlock(); + if (ret) + return ret; + + return sum; +} +EXPORT_SYMBOL(cros_ec_lpc_io_bytes_mec); + +void cros_ec_lpc_mec_init(unsigned int base, unsigned int end) +{ + mec_emi_base = base; + mec_emi_end = end; +} +EXPORT_SYMBOL(cros_ec_lpc_mec_init); + +int cros_ec_lpc_mec_acpi_mutex(struct acpi_device *adev, const char *pathname) +{ + int status; + + if (!adev) + return -ENOENT; + + status = acpi_get_handle(adev->handle, pathname, &aml_mutex); + if (ACPI_FAILURE(status)) + return -ENOENT; + + return 0; +} +EXPORT_SYMBOL(cros_ec_lpc_mec_acpi_mutex); diff --git a/drivers/platform/chrome/cros_ec_lpc_mec.h b/drivers/platform/chrome/cros_ec_lpc_mec.h new file mode 100644 index 000000000..69f9d8786 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_lpc_mec.h @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * LPC variant I/O for Microchip EC + * + * Copyright (C) 2016 Google, Inc + */ + +#ifndef __CROS_EC_LPC_MEC_H +#define __CROS_EC_LPC_MEC_H + +#include + +enum cros_ec_lpc_mec_emi_access_mode { + /* 8-bit access */ + ACCESS_TYPE_BYTE = 0x0, + /* 16-bit access */ + ACCESS_TYPE_WORD = 0x1, + /* 32-bit access */ + ACCESS_TYPE_LONG = 0x2, + /* + * 32-bit access, read or write of MEC_EMI_EC_DATA_B3 causes the + * EC data register to be incremented. + */ + ACCESS_TYPE_LONG_AUTO_INCREMENT = 0x3, +}; + +enum cros_ec_lpc_mec_io_type { + MEC_IO_READ, + MEC_IO_WRITE, +}; + +/* EMI registers are relative to base */ +#define MEC_EMI_HOST_TO_EC(MEC_EMI_BASE) ((MEC_EMI_BASE) + 0) +#define MEC_EMI_EC_TO_HOST(MEC_EMI_BASE) ((MEC_EMI_BASE) + 1) +#define MEC_EMI_EC_ADDRESS_B0(MEC_EMI_BASE) ((MEC_EMI_BASE) + 2) +#define MEC_EMI_EC_ADDRESS_B1(MEC_EMI_BASE) ((MEC_EMI_BASE) + 3) +#define MEC_EMI_EC_DATA_B0(MEC_EMI_BASE) ((MEC_EMI_BASE) + 4) +#define MEC_EMI_EC_DATA_B1(MEC_EMI_BASE) ((MEC_EMI_BASE) + 5) +#define MEC_EMI_EC_DATA_B2(MEC_EMI_BASE) ((MEC_EMI_BASE) + 6) +#define MEC_EMI_EC_DATA_B3(MEC_EMI_BASE) ((MEC_EMI_BASE) + 7) + +/** + * cros_ec_lpc_mec_init() - Initialize MEC I/O. + * + * @base: MEC EMI Base address + * @end: MEC EMI End address + */ +void cros_ec_lpc_mec_init(unsigned int base, unsigned int end); + +/** + * cros_ec_lpc_mec_acpi_mutex() - Find and set ACPI mutex for MEC + * + * @adev: Parent ACPI device + * @pathname: Name of AML mutex + * @return: Negative error code, or zero for success + */ +int cros_ec_lpc_mec_acpi_mutex(struct acpi_device *adev, const char *pathname); + +/** + * cros_ec_lpc_mec_in_range() - Determine if addresses are in MEC EMI range. + * + * @offset: Address offset + * @length: Number of bytes to check + * + * Return: 1 if in range, 0 if not, and -EINVAL on failure + * such as the mec range not being initialized + */ +int cros_ec_lpc_mec_in_range(unsigned int offset, unsigned int length); + +/** + * cros_ec_lpc_io_bytes_mec - Read / write bytes to MEC EMI port + * + * @io_type: MEC_IO_READ or MEC_IO_WRITE, depending on request + * @offset: Base read / write address + * @length: Number of bytes to read / write + * @buf: Destination / source buffer + * + * @return: A negative error code on error, or 8-bit checksum of all + * bytes read / written + */ +int cros_ec_lpc_io_bytes_mec(enum cros_ec_lpc_mec_io_type io_type, + unsigned int offset, unsigned int length, u8 *buf); + +#endif /* __CROS_EC_LPC_MEC_H */ diff --git a/drivers/platform/chrome/cros_ec_proto.c b/drivers/platform/chrome/cros_ec_proto.c new file mode 100644 index 000000000..1d8d9168e --- /dev/null +++ b/drivers/platform/chrome/cros_ec_proto.c @@ -0,0 +1,1172 @@ +// SPDX-License-Identifier: GPL-2.0 +// ChromeOS EC communication protocol helper functions +// +// Copyright (C) 2015 Google, Inc + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec_trace.h" + +#define EC_COMMAND_RETRIES 50 +#define RWSIG_CONTINUE_RETRIES 8 +#define RWSIG_CONTINUE_MAX_ERRORS_IN_ROW 3 + +static const int cros_ec_error_map[] = { + [EC_RES_INVALID_COMMAND] = -EOPNOTSUPP, + [EC_RES_ERROR] = -EIO, + [EC_RES_INVALID_PARAM] = -EINVAL, + [EC_RES_ACCESS_DENIED] = -EACCES, + [EC_RES_INVALID_RESPONSE] = -EPROTO, + [EC_RES_INVALID_VERSION] = -ENOPROTOOPT, + [EC_RES_INVALID_CHECKSUM] = -EBADMSG, + [EC_RES_IN_PROGRESS] = -EINPROGRESS, + [EC_RES_UNAVAILABLE] = -ENODATA, + [EC_RES_TIMEOUT] = -ETIMEDOUT, + [EC_RES_OVERFLOW] = -EOVERFLOW, + [EC_RES_INVALID_HEADER] = -EBADR, + [EC_RES_REQUEST_TRUNCATED] = -EBADR, + [EC_RES_RESPONSE_TOO_BIG] = -EFBIG, + [EC_RES_BUS_ERROR] = -EFAULT, + [EC_RES_BUSY] = -EBUSY, + [EC_RES_INVALID_HEADER_VERSION] = -EBADMSG, + [EC_RES_INVALID_HEADER_CRC] = -EBADMSG, + [EC_RES_INVALID_DATA_CRC] = -EBADMSG, + [EC_RES_DUP_UNAVAILABLE] = -ENODATA, +}; + +static int cros_ec_map_error(uint32_t result) +{ + int ret = 0; + + if (result != EC_RES_SUCCESS) { + if (result < ARRAY_SIZE(cros_ec_error_map) && cros_ec_error_map[result]) + ret = cros_ec_error_map[result]; + else + ret = -EPROTO; + } + + return ret; +} + +static int prepare_tx(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + struct ec_host_request *request; + u8 *out; + int i; + u8 csum = 0; + + if (msg->outsize + sizeof(*request) > ec_dev->dout_size) + return -EINVAL; + + out = ec_dev->dout; + request = (struct ec_host_request *)out; + request->struct_version = EC_HOST_REQUEST_VERSION; + request->checksum = 0; + request->command = msg->command; + request->command_version = msg->version; + request->reserved = 0; + request->data_len = msg->outsize; + + for (i = 0; i < sizeof(*request); i++) + csum += out[i]; + + /* Copy data and update checksum */ + memcpy(out + sizeof(*request), msg->data, msg->outsize); + for (i = 0; i < msg->outsize; i++) + csum += msg->data[i]; + + request->checksum = -csum; + + return sizeof(*request) + msg->outsize; +} + +static int prepare_tx_legacy(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + u8 *out; + u8 csum; + int i; + + if (msg->outsize > EC_PROTO2_MAX_PARAM_SIZE) + return -EINVAL; + + out = ec_dev->dout; + out[0] = EC_CMD_VERSION0 + msg->version; + out[1] = msg->command; + out[2] = msg->outsize; + csum = out[0] + out[1] + out[2]; + for (i = 0; i < msg->outsize; i++) + csum += out[EC_MSG_TX_HEADER_BYTES + i] = msg->data[i]; + out[EC_MSG_TX_HEADER_BYTES + msg->outsize] = csum; + + return EC_MSG_TX_PROTO_BYTES + msg->outsize; +} + +static int cros_ec_xfer_command(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + int ret; + int (*xfer_fxn)(struct cros_ec_device *ec, struct cros_ec_command *msg); + + if (ec_dev->proto_version > 2) + xfer_fxn = ec_dev->pkt_xfer; + else + xfer_fxn = ec_dev->cmd_xfer; + + if (!xfer_fxn) { + /* + * This error can happen if a communication error happened and + * the EC is trying to use protocol v2, on an underlying + * communication mechanism that does not support v2. + */ + dev_err_once(ec_dev->dev, "missing EC transfer API, cannot send command\n"); + return -EIO; + } + + trace_cros_ec_request_start(msg); + ret = (*xfer_fxn)(ec_dev, msg); + trace_cros_ec_request_done(msg, ret); + + return ret; +} + +static int cros_ec_wait_until_complete(struct cros_ec_device *ec_dev, uint32_t *result) +{ + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_response_get_comms_status)); + struct ec_response_get_comms_status *status = + (struct ec_response_get_comms_status *)msg->data; + int ret = 0, i; + + msg->version = 0; + msg->command = EC_CMD_GET_COMMS_STATUS; + msg->insize = sizeof(*status); + msg->outsize = 0; + + /* Query the EC's status until it's no longer busy or we encounter an error. */ + for (i = 0; i < EC_COMMAND_RETRIES; ++i) { + usleep_range(10000, 11000); + + ret = cros_ec_xfer_command(ec_dev, msg); + if (ret == -EAGAIN) + continue; + if (ret < 0) + return ret; + + *result = msg->result; + if (msg->result != EC_RES_SUCCESS) + return ret; + + if (ret == 0) { + ret = -EPROTO; + break; + } + + if (!(status->flags & EC_COMMS_STATUS_PROCESSING)) + return ret; + } + + if (i >= EC_COMMAND_RETRIES) + ret = -EAGAIN; + + return ret; +} + +static int cros_ec_send_command(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + int ret = cros_ec_xfer_command(ec_dev, msg); + + if (msg->result == EC_RES_IN_PROGRESS) + ret = cros_ec_wait_until_complete(ec_dev, &msg->result); + + return ret; +} + +/** + * cros_ec_prepare_tx() - Prepare an outgoing message in the output buffer. + * @ec_dev: Device to register. + * @msg: Message to write. + * + * This is used by all ChromeOS EC drivers to prepare the outgoing message + * according to different protocol versions. + * + * Return: number of prepared bytes on success or negative error code. + */ +int cros_ec_prepare_tx(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + if (ec_dev->proto_version > 2) + return prepare_tx(ec_dev, msg); + + return prepare_tx_legacy(ec_dev, msg); +} +EXPORT_SYMBOL(cros_ec_prepare_tx); + +/** + * cros_ec_check_result() - Check ec_msg->result. + * @ec_dev: EC device. + * @msg: Message to check. + * + * This is used by ChromeOS EC drivers to check the ec_msg->result for + * EC_RES_IN_PROGRESS and to warn about them. + * + * The function should not check for furthermore error codes. Otherwise, + * it would break the ABI. + * + * Return: -EAGAIN if ec_msg->result == EC_RES_IN_PROGRESS. Otherwise, 0. + */ +int cros_ec_check_result(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + switch (msg->result) { + case EC_RES_SUCCESS: + return 0; + case EC_RES_IN_PROGRESS: + dev_dbg(ec_dev->dev, "command 0x%02x in progress\n", + msg->command); + return -EAGAIN; + default: + dev_dbg(ec_dev->dev, "command 0x%02x returned %d\n", + msg->command, msg->result); + return 0; + } +} +EXPORT_SYMBOL(cros_ec_check_result); + +/** + * cros_ec_get_host_event_wake_mask + * + * Get the mask of host events that cause wake from suspend. + * + * @ec_dev: EC device to call + * @mask: result when function returns 0. + * + * LOCKING: + * the caller has ec_dev->lock mutex, or the caller knows there is + * no other command in progress. + */ +static int cros_ec_get_host_event_wake_mask(struct cros_ec_device *ec_dev, uint32_t *mask) +{ + struct cros_ec_command *msg; + struct ec_response_host_event_mask *r; + int ret, mapped; + + msg = kzalloc(sizeof(*msg) + sizeof(*r), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_HOST_EVENT_GET_WAKE_MASK; + msg->insize = sizeof(*r); + + ret = cros_ec_send_command(ec_dev, msg); + if (ret < 0) + goto exit; + + mapped = cros_ec_map_error(msg->result); + if (mapped) { + ret = mapped; + goto exit; + } + + if (ret == 0) { + ret = -EPROTO; + goto exit; + } + + r = (struct ec_response_host_event_mask *)msg->data; + *mask = r->mask; + ret = 0; +exit: + kfree(msg); + return ret; +} + +int cros_ec_rwsig_continue(struct cros_ec_device *ec_dev) +{ + struct cros_ec_command *msg; + struct ec_params_rwsig_action *rwsig_action; + int ret = 0; + int error_count = 0; + + ec_dev->proto_version = 3; + + msg = kmalloc(sizeof(*msg) + sizeof(*rwsig_action), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->version = 0; + msg->command = EC_CMD_RWSIG_ACTION; + msg->insize = 0; + msg->outsize = sizeof(*rwsig_action); + + rwsig_action = (struct ec_params_rwsig_action *)msg->data; + rwsig_action->action = RWSIG_ACTION_CONTINUE; + + for (int i = 0; i < RWSIG_CONTINUE_RETRIES; i++) { + ret = cros_ec_send_command(ec_dev, msg); + + if (ret < 0) { + if (++error_count >= RWSIG_CONTINUE_MAX_ERRORS_IN_ROW) + break; + } else if (msg->result == EC_RES_INVALID_COMMAND) { + /* + * If EC_RES_INVALID_COMMAND is retured, it means RWSIG + * is not supported or EC is already in RW, so there is + * nothing left to do. + */ + break; + } else if (msg->result != EC_RES_SUCCESS) { + /* Unexpected command error. */ + ret = cros_ec_map_error(msg->result); + break; + } else { + /* + * The EC_CMD_RWSIG_ACTION succeed. Send the command + * more times, to make sure EC is in RW. A following + * command can timeout, because EC may need some time to + * initialize after jump to RW. + */ + error_count = 0; + } + + if (ret != -ETIMEDOUT) + usleep_range(90000, 100000); + } + + kfree(msg); + + return ret; +} +EXPORT_SYMBOL(cros_ec_rwsig_continue); + +static int cros_ec_get_proto_info(struct cros_ec_device *ec_dev, int devidx) +{ + struct cros_ec_command *msg; + struct ec_response_get_protocol_info *info; + int ret, mapped; + + ec_dev->proto_version = 3; + if (devidx > 0) + ec_dev->max_passthru = 0; + + msg = kzalloc(sizeof(*msg) + sizeof(*info), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_PASSTHRU_OFFSET(devidx) | EC_CMD_GET_PROTOCOL_INFO; + msg->insize = sizeof(*info); + + ret = cros_ec_send_command(ec_dev, msg); + + if (ret < 0) { + dev_dbg(ec_dev->dev, + "failed to check for EC[%d] protocol version: %d\n", + devidx, ret); + goto exit; + } + + mapped = cros_ec_map_error(msg->result); + if (mapped) { + ret = mapped; + goto exit; + } + + if (ret == 0) { + ret = -EPROTO; + goto exit; + } + + info = (struct ec_response_get_protocol_info *)msg->data; + + switch (devidx) { + case CROS_EC_DEV_EC_INDEX: + ec_dev->max_request = info->max_request_packet_size - + sizeof(struct ec_host_request); + ec_dev->max_response = info->max_response_packet_size - + sizeof(struct ec_host_response); + ec_dev->proto_version = min(EC_HOST_REQUEST_VERSION, + fls(info->protocol_versions) - 1); + ec_dev->din_size = info->max_response_packet_size + EC_MAX_RESPONSE_OVERHEAD; + ec_dev->dout_size = info->max_request_packet_size + EC_MAX_REQUEST_OVERHEAD; + + dev_dbg(ec_dev->dev, "using proto v%u\n", ec_dev->proto_version); + break; + case CROS_EC_DEV_PD_INDEX: + ec_dev->max_passthru = info->max_request_packet_size - + sizeof(struct ec_host_request); + + dev_dbg(ec_dev->dev, "found PD chip\n"); + break; + default: + dev_dbg(ec_dev->dev, "unknown passthru index: %d\n", devidx); + break; + } + + ret = 0; +exit: + kfree(msg); + return ret; +} + +static int cros_ec_get_proto_info_legacy(struct cros_ec_device *ec_dev) +{ + struct cros_ec_command *msg; + struct ec_params_hello *params; + struct ec_response_hello *response; + int ret, mapped; + + ec_dev->proto_version = 2; + + msg = kzalloc(sizeof(*msg) + max(sizeof(*params), sizeof(*response)), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_HELLO; + msg->insize = sizeof(*response); + msg->outsize = sizeof(*params); + + params = (struct ec_params_hello *)msg->data; + params->in_data = 0xa0b0c0d0; + + ret = cros_ec_send_command(ec_dev, msg); + if (ret < 0) { + dev_dbg(ec_dev->dev, "EC failed to respond to v2 hello: %d\n", ret); + goto exit; + } + + mapped = cros_ec_map_error(msg->result); + if (mapped) { + ret = mapped; + dev_err(ec_dev->dev, "EC responded to v2 hello with error: %d\n", msg->result); + goto exit; + } + + if (ret == 0) { + ret = -EPROTO; + goto exit; + } + + response = (struct ec_response_hello *)msg->data; + if (response->out_data != 0xa1b2c3d4) { + dev_err(ec_dev->dev, + "EC responded to v2 hello with bad result: %u\n", + response->out_data); + ret = -EBADMSG; + goto exit; + } + + ec_dev->max_request = EC_PROTO2_MAX_PARAM_SIZE; + ec_dev->max_response = EC_PROTO2_MAX_PARAM_SIZE; + ec_dev->max_passthru = 0; + ec_dev->pkt_xfer = NULL; + ec_dev->din_size = EC_PROTO2_MSG_BYTES; + ec_dev->dout_size = EC_PROTO2_MSG_BYTES; + + dev_dbg(ec_dev->dev, "falling back to proto v2\n"); + ret = 0; +exit: + kfree(msg); + return ret; +} + +/** + * cros_ec_get_host_command_version_mask + * + * Get the version mask of a given command. + * + * @ec_dev: EC device to call + * @cmd: command to get the version of. + * @mask: result when function returns 0. + * + * @return 0 on success, error code otherwise + * + * LOCKING: + * the caller has ec_dev->lock mutex or the caller knows there is + * no other command in progress. + */ +static int cros_ec_get_host_command_version_mask(struct cros_ec_device *ec_dev, u16 cmd, u32 *mask) +{ + struct ec_params_get_cmd_versions *pver; + struct ec_response_get_cmd_versions *rver; + struct cros_ec_command *msg; + int ret, mapped; + + msg = kmalloc(sizeof(*msg) + max(sizeof(*rver), sizeof(*pver)), + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->version = 0; + msg->command = EC_CMD_GET_CMD_VERSIONS; + msg->insize = sizeof(*rver); + msg->outsize = sizeof(*pver); + + pver = (struct ec_params_get_cmd_versions *)msg->data; + pver->cmd = cmd; + + ret = cros_ec_send_command(ec_dev, msg); + if (ret < 0) + goto exit; + + mapped = cros_ec_map_error(msg->result); + if (mapped) { + ret = mapped; + goto exit; + } + + if (ret == 0) { + ret = -EPROTO; + goto exit; + } + + rver = (struct ec_response_get_cmd_versions *)msg->data; + *mask = rver->version_mask; + ret = 0; +exit: + kfree(msg); + return ret; +} + +/** + * cros_ec_query_all() - Query the protocol version supported by the + * ChromeOS EC. + * @ec_dev: Device to register. + * + * Return: 0 on success or negative error code. + */ +int cros_ec_query_all(struct cros_ec_device *ec_dev) +{ + struct device *dev = ec_dev->dev; + u32 ver_mask; + int ret; + + /* First try sending with proto v3. */ + if (!cros_ec_get_proto_info(ec_dev, CROS_EC_DEV_EC_INDEX)) { + /* Check for PD. */ + cros_ec_get_proto_info(ec_dev, CROS_EC_DEV_PD_INDEX); + } else { + /* Try querying with a v2 hello message. */ + ret = cros_ec_get_proto_info_legacy(ec_dev); + if (ret) { + /* + * It's possible for a test to occur too early when + * the EC isn't listening. If this happens, we'll + * test later when the first command is run. + */ + ec_dev->proto_version = EC_PROTO_VERSION_UNKNOWN; + dev_dbg(ec_dev->dev, "EC query failed: %d\n", ret); + return ret; + } + } + + devm_kfree(dev, ec_dev->din); + devm_kfree(dev, ec_dev->dout); + + ec_dev->din = devm_kzalloc(dev, ec_dev->din_size, GFP_KERNEL); + if (!ec_dev->din) { + ret = -ENOMEM; + goto exit; + } + + ec_dev->dout = devm_kzalloc(dev, ec_dev->dout_size, GFP_KERNEL); + if (!ec_dev->dout) { + devm_kfree(dev, ec_dev->din); + ret = -ENOMEM; + goto exit; + } + + /* Probe if MKBP event is supported */ + ret = cros_ec_get_host_command_version_mask(ec_dev, EC_CMD_GET_NEXT_EVENT, &ver_mask); + if (ret < 0 || ver_mask == 0) { + ec_dev->mkbp_event_supported = 0; + } else { + ec_dev->mkbp_event_supported = fls(ver_mask); + + dev_dbg(ec_dev->dev, "MKBP support version %u\n", ec_dev->mkbp_event_supported - 1); + } + + /* Probe if host sleep v1 is supported for S0ix failure detection. */ + ret = cros_ec_get_host_command_version_mask(ec_dev, EC_CMD_HOST_SLEEP_EVENT, &ver_mask); + ec_dev->host_sleep_v1 = (ret == 0 && (ver_mask & EC_VER_MASK(1))); + + /* Get host event wake mask. */ + ret = cros_ec_get_host_event_wake_mask(ec_dev, &ec_dev->host_event_wake_mask); + if (ret < 0) { + /* + * If the EC doesn't support EC_CMD_HOST_EVENT_GET_WAKE_MASK, + * use a reasonable default. Note that we ignore various + * battery, AC status, and power-state events, because (a) + * those can be quite common (e.g., when sitting at full + * charge, on AC) and (b) these are not actionable wake events; + * if anything, we'd like to continue suspending (to save + * power), not wake up. + */ + ec_dev->host_event_wake_mask = U32_MAX & + ~(EC_HOST_EVENT_MASK(EC_HOST_EVENT_LID_CLOSED) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_AC_DISCONNECTED) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_LOW) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_CRITICAL) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_PD_MCU) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_STATUS)); + /* + * Old ECs may not support this command. Complain about all + * other errors. + */ + if (ret != -EOPNOTSUPP) + dev_err(ec_dev->dev, + "failed to retrieve wake mask: %d\n", ret); + } + + ret = 0; + +exit: + return ret; +} +EXPORT_SYMBOL(cros_ec_query_all); + +/** + * cros_ec_cmd_xfer() - Send a command to the ChromeOS EC. + * @ec_dev: EC device. + * @msg: Message to write. + * + * Call this to send a command to the ChromeOS EC. This should be used instead + * of calling the EC's cmd_xfer() callback directly. This function does not + * convert EC command execution error codes to Linux error codes. Most + * in-kernel users will want to use cros_ec_cmd_xfer_status() instead since + * that function implements the conversion. + * + * Return: + * >0 - EC command was executed successfully. The return value is the number + * of bytes returned by the EC (excluding the header). + * =0 - EC communication was successful. EC command execution results are + * reported in msg->result. The result will be EC_RES_SUCCESS if the + * command was executed successfully or report an EC command execution + * error. + * <0 - EC communication error. Return value is the Linux error code. + */ +int cros_ec_cmd_xfer(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + int ret; + + mutex_lock(&ec_dev->lock); + if (ec_dev->proto_version == EC_PROTO_VERSION_UNKNOWN) { + ret = cros_ec_query_all(ec_dev); + if (ret) { + dev_err(ec_dev->dev, + "EC version unknown and query failed; aborting command\n"); + mutex_unlock(&ec_dev->lock); + return ret; + } + } + + if (msg->insize > ec_dev->max_response) { + dev_dbg(ec_dev->dev, "clamping message receive buffer\n"); + msg->insize = ec_dev->max_response; + } + + if (msg->command < EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX)) { + if (msg->outsize > ec_dev->max_request) { + dev_err(ec_dev->dev, + "request of size %u is too big (max: %u)\n", + msg->outsize, + ec_dev->max_request); + mutex_unlock(&ec_dev->lock); + return -EMSGSIZE; + } + } else { + if (msg->outsize > ec_dev->max_passthru) { + dev_err(ec_dev->dev, + "passthru rq of size %u is too big (max: %u)\n", + msg->outsize, + ec_dev->max_passthru); + mutex_unlock(&ec_dev->lock); + return -EMSGSIZE; + } + } + + ret = cros_ec_send_command(ec_dev, msg); + mutex_unlock(&ec_dev->lock); + + return ret; +} +EXPORT_SYMBOL(cros_ec_cmd_xfer); + +/** + * cros_ec_cmd_xfer_status() - Send a command to the ChromeOS EC. + * @ec_dev: EC device. + * @msg: Message to write. + * + * Call this to send a command to the ChromeOS EC. This should be used instead of calling the EC's + * cmd_xfer() callback directly. It returns success status only if both the command was transmitted + * successfully and the EC replied with success status. + * + * Return: + * >=0 - The number of bytes transferred. + * <0 - Linux error code + */ +int cros_ec_cmd_xfer_status(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg) +{ + int ret, mapped; + + ret = cros_ec_cmd_xfer(ec_dev, msg); + if (ret < 0) + return ret; + + mapped = cros_ec_map_error(msg->result); + if (mapped) { + dev_dbg(ec_dev->dev, "Command result (err: %d [%d])\n", + msg->result, mapped); + ret = mapped; + } + + return ret; +} +EXPORT_SYMBOL(cros_ec_cmd_xfer_status); + +static int get_next_event_xfer(struct cros_ec_device *ec_dev, + struct cros_ec_command *msg, + struct ec_response_get_next_event_v3 *event, + int version, uint32_t size) +{ + int ret; + + msg->version = version; + msg->command = EC_CMD_GET_NEXT_EVENT; + msg->insize = size; + msg->outsize = 0; + + ret = cros_ec_cmd_xfer_status(ec_dev, msg); + if (ret > 0) { + ec_dev->event_size = ret - 1; + ec_dev->event_data = *event; + } + + return ret; +} + +static int get_next_event(struct cros_ec_device *ec_dev) +{ + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_response_get_next_event_v3)); + struct ec_response_get_next_event_v3 *event = + (struct ec_response_get_next_event_v3 *)msg->data; + int cmd_version = ec_dev->mkbp_event_supported - 1; + u32 size; + + if (ec_dev->suspended) { + dev_dbg(ec_dev->dev, "Device suspended.\n"); + return -EHOSTDOWN; + } + + if (cmd_version == 0) { + size = sizeof(struct ec_response_get_next_event); + } else if (cmd_version < 3) { + size = sizeof(struct ec_response_get_next_event_v1); + } else { + /* + * The max version we support is v3. So, we speak v3 even if the + * EC says it supports v4+. + */ + cmd_version = 3; + size = sizeof(struct ec_response_get_next_event_v3); + } + + return get_next_event_xfer(ec_dev, msg, event, cmd_version, size); +} + +static int get_keyboard_state_event(struct cros_ec_device *ec_dev) +{ + u8 buffer[sizeof(struct cros_ec_command) + + sizeof(ec_dev->event_data.data)]; + struct cros_ec_command *msg = (struct cros_ec_command *)&buffer; + + msg->version = 0; + msg->command = EC_CMD_MKBP_STATE; + msg->insize = sizeof(ec_dev->event_data.data); + msg->outsize = 0; + + ec_dev->event_size = cros_ec_cmd_xfer_status(ec_dev, msg); + ec_dev->event_data.event_type = EC_MKBP_EVENT_KEY_MATRIX; + memcpy(&ec_dev->event_data.data, msg->data, + sizeof(ec_dev->event_data.data)); + + return ec_dev->event_size; +} + +/** + * cros_ec_get_next_event() - Fetch next event from the ChromeOS EC. + * @ec_dev: Device to fetch event from. + * @wake_event: Pointer to a bool set to true upon return if the event might be + * treated as a wake event. Ignored if null. + * @has_more_events: Pointer to bool set to true if more than one event is + * pending. + * Some EC will set this flag to indicate cros_ec_get_next_event() + * can be called multiple times in a row. + * It is an optimization to prevent issuing a EC command for + * nothing or wait for another interrupt from the EC to process + * the next message. + * Ignored if null. + * + * Return: negative error code on errors; 0 for no data; or else number of + * bytes received (i.e., an event was retrieved successfully). Event types are + * written out to @ec_dev->event_data.event_type on success. + */ +int cros_ec_get_next_event(struct cros_ec_device *ec_dev, + bool *wake_event, + bool *has_more_events) +{ + u8 event_type; + u32 host_event; + int ret; + u32 ver_mask; + + /* + * Default value for wake_event. + * Wake up on keyboard event, wake up for spurious interrupt or link + * error to the EC. + */ + if (wake_event) + *wake_event = true; + + /* + * Default value for has_more_events. + * EC will raise another interrupt if AP does not process all events + * anyway. + */ + if (has_more_events) + *has_more_events = false; + + if (!ec_dev->mkbp_event_supported) + return get_keyboard_state_event(ec_dev); + + ret = get_next_event(ec_dev); + /* + * -ENOPROTOOPT is returned when EC returns EC_RES_INVALID_VERSION. + * This can occur when EC based device (e.g. Fingerprint MCU) jumps to + * the RO image which doesn't support newer version of the command. In + * this case we will attempt to update maximum supported version of the + * EC_CMD_GET_NEXT_EVENT. + */ + if (ret == -ENOPROTOOPT) { + dev_dbg(ec_dev->dev, + "GET_NEXT_EVENT returned invalid version error.\n"); + mutex_lock(&ec_dev->lock); + ret = cros_ec_get_host_command_version_mask(ec_dev, + EC_CMD_GET_NEXT_EVENT, + &ver_mask); + mutex_unlock(&ec_dev->lock); + if (ret < 0 || ver_mask == 0) + /* + * Do not change the MKBP supported version if we can't + * obtain supported version correctly. Please note that + * calling EC_CMD_GET_NEXT_EVENT returned + * EC_RES_INVALID_VERSION which means that the command + * is present. + */ + return -ENOPROTOOPT; + + ec_dev->mkbp_event_supported = fls(ver_mask); + dev_dbg(ec_dev->dev, "MKBP support version changed to %u\n", + ec_dev->mkbp_event_supported - 1); + + /* Try to get next event with new MKBP support version set. */ + ret = get_next_event(ec_dev); + } + + if (ret <= 0) + return ret; + + if (has_more_events) + *has_more_events = ec_dev->event_data.event_type & + EC_MKBP_HAS_MORE_EVENTS; + ec_dev->event_data.event_type &= EC_MKBP_EVENT_TYPE_MASK; + + if (wake_event) { + event_type = ec_dev->event_data.event_type; + host_event = cros_ec_get_host_event(ec_dev); + + /* + * Sensor events need to be parsed by the sensor sub-device. + * Defer them, and don't report the wakeup here. + */ + if (event_type == EC_MKBP_EVENT_SENSOR_FIFO) { + *wake_event = false; + } else if (host_event) { + /* rtc_update_irq() already handles wakeup events. */ + if (host_event & EC_HOST_EVENT_MASK(EC_HOST_EVENT_RTC)) + *wake_event = false; + /* Masked host-events should not count as wake events. */ + if (!(host_event & ec_dev->host_event_wake_mask)) + *wake_event = false; + } + } + + return ret; +} +EXPORT_SYMBOL(cros_ec_get_next_event); + +/** + * cros_ec_get_host_event() - Return a mask of event set by the ChromeOS EC. + * @ec_dev: Device to fetch event from. + * + * When MKBP is supported, when the EC raises an interrupt, we collect the + * events raised and call the functions in the ec notifier. This function + * is a helper to know which events are raised. + * + * Return: 0 on error or non-zero bitmask of one or more EC_HOST_EVENT_*. + */ +u32 cros_ec_get_host_event(struct cros_ec_device *ec_dev) +{ + u32 host_event; + + if (!ec_dev->mkbp_event_supported) + return 0; + + if (ec_dev->event_data.event_type != EC_MKBP_EVENT_HOST_EVENT) + return 0; + + if (ec_dev->event_size != sizeof(host_event)) { + dev_warn(ec_dev->dev, "Invalid host event size\n"); + return 0; + } + + host_event = get_unaligned_le32(&ec_dev->event_data.data.host_event); + + return host_event; +} +EXPORT_SYMBOL(cros_ec_get_host_event); + +/** + * cros_ec_check_features() - Test for the presence of EC features + * + * @ec: EC device, does not have to be connected directly to the AP, + * can be daisy chained through another device. + * @feature: One of ec_feature_code bit. + * + * Call this function to test whether the ChromeOS EC supports a feature. + * + * Return: true if supported, false if not (or if an error was encountered). + */ +bool cros_ec_check_features(struct cros_ec_dev *ec, int feature) +{ + struct ec_response_get_features *features = &ec->features; + int ret; + + if (features->flags[0] == -1U && features->flags[1] == -1U) { + /* features bitmap not read yet */ + ret = cros_ec_cmd(ec->ec_dev, 0, EC_CMD_GET_FEATURES + ec->cmd_offset, + NULL, 0, features, sizeof(*features)); + if (ret < 0) { + dev_warn(ec->dev, "cannot get EC features: %d\n", ret); + memset(features, 0, sizeof(*features)); + } + + dev_dbg(ec->dev, "EC features %08x %08x\n", + features->flags[0], features->flags[1]); + } + + return !!(features->flags[feature / 32] & EC_FEATURE_MASK_0(feature)); +} +EXPORT_SYMBOL_GPL(cros_ec_check_features); + +/** + * cros_ec_get_sensor_count() - Return the number of MEMS sensors supported. + * + * @ec: EC device, does not have to be connected directly to the AP, + * can be daisy chained through another device. + * Return: < 0 in case of error. + */ +int cros_ec_get_sensor_count(struct cros_ec_dev *ec) +{ + /* + * Issue a command to get the number of sensor reported. + * If not supported, check for legacy mode. + */ + int ret, sensor_count; + struct ec_params_motion_sense *params; + struct ec_response_motion_sense *resp; + struct cros_ec_command *msg; + struct cros_ec_device *ec_dev = ec->ec_dev; + u8 status; + + msg = kzalloc(sizeof(*msg) + max(sizeof(*params), sizeof(*resp)), + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->version = 1; + msg->command = EC_CMD_MOTION_SENSE_CMD + ec->cmd_offset; + msg->outsize = sizeof(*params); + msg->insize = sizeof(*resp); + + params = (struct ec_params_motion_sense *)msg->data; + params->cmd = MOTIONSENSE_CMD_DUMP; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + sensor_count = ret; + } else { + resp = (struct ec_response_motion_sense *)msg->data; + sensor_count = resp->dump.sensor_count; + } + kfree(msg); + + /* + * Check legacy mode: Let's find out if sensors are accessible + * via LPC interface. + */ + if (sensor_count < 0 && ec->cmd_offset == 0 && ec_dev->cmd_readmem) { + ret = ec_dev->cmd_readmem(ec_dev, EC_MEMMAP_ACC_STATUS, + 1, &status); + if (ret >= 0 && + (status & EC_MEMMAP_ACC_STATUS_PRESENCE_BIT)) { + /* + * We have 2 sensors, one in the lid, one in the base. + */ + sensor_count = 2; + } else { + /* + * EC uses LPC interface and no sensors are presented. + */ + sensor_count = 0; + } + } + return sensor_count; +} +EXPORT_SYMBOL_GPL(cros_ec_get_sensor_count); + +/** + * cros_ec_cmd - Send a command to the EC. + * + * @ec_dev: EC device + * @version: EC command version + * @command: EC command + * @outdata: EC command output data + * @outsize: Size of outdata + * @indata: EC command input data + * @insize: Size of indata + * + * Return: >= 0 on success, negative error number on failure. + */ +int cros_ec_cmd(struct cros_ec_device *ec_dev, + unsigned int version, + int command, + const void *outdata, + size_t outsize, + void *indata, + size_t insize) +{ + struct cros_ec_command *msg; + int ret; + + msg = kzalloc(sizeof(*msg) + max(insize, outsize), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->version = version; + msg->command = command; + msg->outsize = outsize; + msg->insize = insize; + + if (outsize) + memcpy(msg->data, outdata, outsize); + + ret = cros_ec_cmd_xfer_status(ec_dev, msg); + if (ret < 0) + goto error; + + if (insize) + memcpy(indata, msg->data, insize); +error: + kfree(msg); + return ret; +} +EXPORT_SYMBOL_GPL(cros_ec_cmd); + +/** + * cros_ec_cmd_readmem - Read from EC memory. + * + * @ec_dev: EC device + * @offset: Is within EC_LPC_ADDR_MEMMAP region. + * @size: Number of bytes to read. + * @dest: EC command output data + * + * Return: >= 0 on success, negative error number on failure. + */ +int cros_ec_cmd_readmem(struct cros_ec_device *ec_dev, u8 offset, u8 size, void *dest) +{ + struct ec_params_read_memmap params = {}; + + if (!size) + return -EINVAL; + + if (ec_dev->cmd_readmem) + return ec_dev->cmd_readmem(ec_dev, offset, size, dest); + + params.offset = offset; + params.size = size; + return cros_ec_cmd(ec_dev, 0, EC_CMD_READ_MEMMAP, + ¶ms, sizeof(params), dest, size); +} +EXPORT_SYMBOL_GPL(cros_ec_cmd_readmem); + +/** + * cros_ec_get_cmd_versions - Get supported version mask. + * + * @ec_dev: EC device + * @cmd: Command to test + * + * Return: version mask on success, negative error number on failure. + */ +int cros_ec_get_cmd_versions(struct cros_ec_device *ec_dev, u16 cmd) +{ + struct ec_params_get_cmd_versions req_v0; + struct ec_params_get_cmd_versions_v1 req_v1; + struct ec_response_get_cmd_versions resp; + int ret; + + if (cmd <= U8_MAX) { + req_v0.cmd = cmd; + ret = cros_ec_cmd(ec_dev, 0, EC_CMD_GET_CMD_VERSIONS, + &req_v0, sizeof(req_v0), &resp, sizeof(resp)); + } else { + req_v1.cmd = cmd; + ret = cros_ec_cmd(ec_dev, 1, EC_CMD_GET_CMD_VERSIONS, + &req_v1, sizeof(req_v1), &resp, sizeof(resp)); + } + + if (ret == -EINVAL) + return 0; /* Command not implemented */ + else if (ret < 0) + return ret; + else + return resp.version_mask; +} +EXPORT_SYMBOL_GPL(cros_ec_get_cmd_versions); + +/** + * cros_ec_device_registered - Return if the ec_dev is registered. + * + * @ec_dev: EC device + * + * Return: true if registered. Otherwise, false. + */ +bool cros_ec_device_registered(struct cros_ec_device *ec_dev) +{ + guard(mutex)(&ec_dev->lock); + return ec_dev->registered; +} +EXPORT_SYMBOL_GPL(cros_ec_device_registered); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS EC communication protocol helpers"); diff --git a/drivers/platform/chrome/cros_ec_proto_test.c b/drivers/platform/chrome/cros_ec_proto_test.c new file mode 100644 index 000000000..3f281996a --- /dev/null +++ b/drivers/platform/chrome/cros_ec_proto_test.c @@ -0,0 +1,2744 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Kunit tests for ChromeOS Embedded Controller protocol. + */ + +#include + +#include + +#include +#include + +#include "cros_ec.h" +#include "cros_ec_proto_test_util.h" + +#define BUFSIZE 512 + +struct cros_ec_proto_test_priv { + struct cros_ec_device ec_dev; + u8 dout[BUFSIZE]; + u8 din[BUFSIZE]; + struct cros_ec_command *msg; + u8 _msg[BUFSIZE]; +}; + +static void cros_ec_proto_test_prepare_tx_legacy_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct cros_ec_command *msg = priv->msg; + int ret, i; + u8 csum; + + ec_dev->proto_version = 2; + + msg->command = EC_CMD_HELLO; + msg->outsize = EC_PROTO2_MAX_PARAM_SIZE; + msg->data[0] = 0xde; + msg->data[1] = 0xad; + msg->data[2] = 0xbe; + msg->data[3] = 0xef; + + ret = cros_ec_prepare_tx(ec_dev, msg); + + KUNIT_EXPECT_EQ(test, ret, EC_MSG_TX_PROTO_BYTES + EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, ec_dev->dout[0], EC_CMD_VERSION0); + KUNIT_EXPECT_EQ(test, ec_dev->dout[1], EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, ec_dev->dout[2], EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, EC_MSG_TX_HEADER_BYTES, 3); + KUNIT_EXPECT_EQ(test, ec_dev->dout[EC_MSG_TX_HEADER_BYTES + 0], 0xde); + KUNIT_EXPECT_EQ(test, ec_dev->dout[EC_MSG_TX_HEADER_BYTES + 1], 0xad); + KUNIT_EXPECT_EQ(test, ec_dev->dout[EC_MSG_TX_HEADER_BYTES + 2], 0xbe); + KUNIT_EXPECT_EQ(test, ec_dev->dout[EC_MSG_TX_HEADER_BYTES + 3], 0xef); + for (i = 4; i < EC_PROTO2_MAX_PARAM_SIZE; ++i) + KUNIT_EXPECT_EQ(test, ec_dev->dout[EC_MSG_TX_HEADER_BYTES + i], 0); + + csum = EC_CMD_VERSION0; + csum += EC_CMD_HELLO; + csum += EC_PROTO2_MAX_PARAM_SIZE; + csum += 0xde; + csum += 0xad; + csum += 0xbe; + csum += 0xef; + KUNIT_EXPECT_EQ(test, + ec_dev->dout[EC_MSG_TX_HEADER_BYTES + EC_PROTO2_MAX_PARAM_SIZE], + csum); +} + +static void cros_ec_proto_test_prepare_tx_legacy_bad_msg_outsize(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct cros_ec_command *msg = priv->msg; + int ret; + + ec_dev->proto_version = 2; + + msg->outsize = EC_PROTO2_MAX_PARAM_SIZE + 1; + + ret = cros_ec_prepare_tx(ec_dev, msg); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +static void cros_ec_proto_test_prepare_tx_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct cros_ec_command *msg = priv->msg; + struct ec_host_request *request = (struct ec_host_request *)ec_dev->dout; + int ret, i; + u8 csum; + + msg->command = EC_CMD_HELLO; + msg->outsize = 0x88; + msg->data[0] = 0xde; + msg->data[1] = 0xad; + msg->data[2] = 0xbe; + msg->data[3] = 0xef; + + ret = cros_ec_prepare_tx(ec_dev, msg); + + KUNIT_EXPECT_EQ(test, ret, sizeof(*request) + 0x88); + + KUNIT_EXPECT_EQ(test, request->struct_version, EC_HOST_REQUEST_VERSION); + KUNIT_EXPECT_EQ(test, request->command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, request->command_version, 0); + KUNIT_EXPECT_EQ(test, request->data_len, 0x88); + KUNIT_EXPECT_EQ(test, ec_dev->dout[sizeof(*request) + 0], 0xde); + KUNIT_EXPECT_EQ(test, ec_dev->dout[sizeof(*request) + 1], 0xad); + KUNIT_EXPECT_EQ(test, ec_dev->dout[sizeof(*request) + 2], 0xbe); + KUNIT_EXPECT_EQ(test, ec_dev->dout[sizeof(*request) + 3], 0xef); + for (i = 4; i < 0x88; ++i) + KUNIT_EXPECT_EQ(test, ec_dev->dout[sizeof(*request) + i], 0); + + csum = EC_HOST_REQUEST_VERSION; + csum += EC_CMD_HELLO; + csum += 0x88; + csum += 0xde; + csum += 0xad; + csum += 0xbe; + csum += 0xef; + KUNIT_EXPECT_EQ(test, request->checksum, (u8)-csum); +} + +static void cros_ec_proto_test_prepare_tx_bad_msg_outsize(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct cros_ec_command *msg = priv->msg; + int ret; + + msg->outsize = ec_dev->dout_size - sizeof(struct ec_host_request) + 1; + + ret = cros_ec_prepare_tx(ec_dev, msg); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +static void cros_ec_proto_test_check_result(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct cros_ec_command *msg = priv->msg; + int ret, i; + static enum ec_status status[] = { + EC_RES_SUCCESS, + EC_RES_INVALID_COMMAND, + EC_RES_ERROR, + EC_RES_INVALID_PARAM, + EC_RES_ACCESS_DENIED, + EC_RES_INVALID_RESPONSE, + EC_RES_INVALID_VERSION, + EC_RES_INVALID_CHECKSUM, + EC_RES_UNAVAILABLE, + EC_RES_TIMEOUT, + EC_RES_OVERFLOW, + EC_RES_INVALID_HEADER, + EC_RES_REQUEST_TRUNCATED, + EC_RES_RESPONSE_TOO_BIG, + EC_RES_BUS_ERROR, + EC_RES_BUSY, + EC_RES_INVALID_HEADER_VERSION, + EC_RES_INVALID_HEADER_CRC, + EC_RES_INVALID_DATA_CRC, + EC_RES_DUP_UNAVAILABLE, + }; + + for (i = 0; i < ARRAY_SIZE(status); ++i) { + msg->result = status[i]; + ret = cros_ec_check_result(ec_dev, msg); + KUNIT_EXPECT_EQ(test, ret, 0); + } + + msg->result = EC_RES_IN_PROGRESS; + ret = cros_ec_check_result(ec_dev, msg); + KUNIT_EXPECT_EQ(test, ret, -EAGAIN); +} + +static void cros_ec_proto_test_query_all_pretest(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + + /* + * cros_ec_query_all() will free din and dout and allocate them again to fit the usage by + * calling devm_kfree() and devm_kzalloc(). Set them to NULL as they aren't managed by + * ec_dev->dev but allocated statically in struct cros_ec_proto_test_priv + * (see cros_ec_proto_test_init()). + */ + ec_dev->din = NULL; + ec_dev->dout = NULL; +} + +static void cros_ec_proto_test_query_all_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->protocol_versions = BIT(3) | BIT(2); + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbf; + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_response_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_cmd_versions *)mock->o_data; + data->version_mask = BIT(6) | BIT(5); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + struct ec_response_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_cmd_versions *)mock->o_data; + data->version_mask = BIT(1); + } + + /* For cros_ec_get_host_event_wake_mask(). */ + { + struct ec_response_host_event_mask *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_host_event_mask *)mock->o_data; + data->mask = 0xbeef; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + KUNIT_EXPECT_EQ(test, ec_dev->max_request, 0xbe - sizeof(struct ec_host_request)); + KUNIT_EXPECT_EQ(test, ec_dev->max_response, 0xef - sizeof(struct ec_host_response)); + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, 3); + KUNIT_EXPECT_EQ(test, ec_dev->din_size, 0xef + EC_MAX_RESPONSE_OVERHEAD); + KUNIT_EXPECT_EQ(test, ec_dev->dout_size, 0xbe + EC_MAX_REQUEST_OVERHEAD); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + KUNIT_EXPECT_EQ(test, ec_dev->max_passthru, 0xbf - sizeof(struct ec_host_request)); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_params_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_get_cmd_versions *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, EC_CMD_GET_NEXT_EVENT); + + KUNIT_EXPECT_EQ(test, ec_dev->mkbp_event_supported, 7); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + struct ec_params_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_get_cmd_versions *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, EC_CMD_HOST_SLEEP_EVENT); + + KUNIT_EXPECT_TRUE(test, ec_dev->host_sleep_v1); + } + + /* For cros_ec_get_host_event_wake_mask(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HOST_EVENT_GET_WAKE_MASK); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_host_event_mask)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + KUNIT_EXPECT_EQ(test, ec_dev->host_event_wake_mask, 0xbeef); + } +} + +static void cros_ec_proto_test_query_all_no_pd_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->max_passthru = 0xbf; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + KUNIT_EXPECT_EQ(test, ec_dev->max_passthru, 0); + } +} + +static void cros_ec_proto_test_query_all_no_pd_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->max_passthru = 0xbf; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + KUNIT_EXPECT_EQ(test, ec_dev->max_passthru, 0); + } +} + +static void cros_ec_proto_test_query_all_legacy_normal_v3_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + struct ec_response_hello *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_hello *)mock->o_data; + data->out_data = 0xa1b2c3d4; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + struct ec_params_hello *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_hello *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->in_data, 0xa0b0c0d0); + + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, 2); + KUNIT_EXPECT_EQ(test, ec_dev->max_request, EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, ec_dev->max_response, EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, ec_dev->max_passthru, 0); + KUNIT_EXPECT_PTR_EQ(test, ec_dev->pkt_xfer, NULL); + KUNIT_EXPECT_EQ(test, ec_dev->din_size, EC_PROTO2_MSG_BYTES); + KUNIT_EXPECT_EQ(test, ec_dev->dout_size, EC_PROTO2_MSG_BYTES); + } +} + +static void cros_ec_proto_test_query_all_legacy_normal_v3_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + struct ec_response_hello *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_hello *)mock->o_data; + data->out_data = 0xa1b2c3d4; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + struct ec_params_hello *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_hello *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->in_data, 0xa0b0c0d0); + + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, 2); + KUNIT_EXPECT_EQ(test, ec_dev->max_request, EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, ec_dev->max_response, EC_PROTO2_MAX_PARAM_SIZE); + KUNIT_EXPECT_EQ(test, ec_dev->max_passthru, 0); + KUNIT_EXPECT_PTR_EQ(test, ec_dev->pkt_xfer, NULL); + KUNIT_EXPECT_EQ(test, ec_dev->din_size, EC_PROTO2_MSG_BYTES); + KUNIT_EXPECT_EQ(test, ec_dev->dout_size, EC_PROTO2_MSG_BYTES); + } +} + +static void cros_ec_proto_test_query_all_legacy_xfer_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, -EIO, EC_RES_SUCCESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, EC_PROTO_VERSION_UNKNOWN); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_hello)); + } +} + +static void cros_ec_proto_test_query_all_legacy_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP); + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, EC_PROTO_VERSION_UNKNOWN); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_hello)); + } +} + +static void cros_ec_proto_test_query_all_legacy_data_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + struct ec_response_hello *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_hello *)mock->o_data; + data->out_data = 0xbeefbfbf; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, -EBADMSG); + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, EC_PROTO_VERSION_UNKNOWN); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_hello)); + } +} + +static void cros_ec_proto_test_query_all_legacy_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, -EPROTO); + KUNIT_EXPECT_EQ(test, ec_dev->proto_version, EC_PROTO_VERSION_UNKNOWN); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info_legacy(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_hello)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_hello)); + } +} + +static void cros_ec_proto_test_query_all_no_mkbp(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->mkbp_event_supported = 0xbf; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_response_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_cmd_versions *)mock->o_data; + data->version_mask = 0; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_params_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_get_cmd_versions *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, EC_CMD_GET_NEXT_EVENT); + + KUNIT_EXPECT_EQ(test, ec_dev->mkbp_event_supported, 0); + } +} + +static void cros_ec_proto_test_query_all_no_mkbp_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->mkbp_event_supported = 0xbf; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_params_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_get_cmd_versions *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, EC_CMD_GET_NEXT_EVENT); + + KUNIT_EXPECT_EQ(test, ec_dev->mkbp_event_supported, 0); + } +} + +static void cros_ec_proto_test_query_all_no_mkbp_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->mkbp_event_supported = 0xbf; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_params_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_get_cmd_versions *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, EC_CMD_GET_NEXT_EVENT); + + KUNIT_EXPECT_EQ(test, ec_dev->mkbp_event_supported, 0); + } +} + +static void cros_ec_proto_test_query_all_no_host_sleep(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->host_sleep_v1 = true; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + struct ec_response_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_cmd_versions *)mock->o_data; + data->version_mask = 0; + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + + KUNIT_EXPECT_FALSE(test, ec_dev->host_sleep_v1); + } +} + +static void cros_ec_proto_test_query_all_no_host_sleep_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->host_sleep_v1 = true; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + struct ec_response_get_cmd_versions *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* In order to pollute next cros_ec_get_host_command_version_mask(). */ + data = (struct ec_response_get_cmd_versions *)mock->o_data; + data->version_mask = 0xbeef; + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + + KUNIT_EXPECT_FALSE(test, ec_dev->host_sleep_v1); + } +} + +static void cros_ec_proto_test_query_all_default_wake_mask_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->host_event_wake_mask = U32_MAX; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_event_wake_mask(). */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For cros_ec_get_host_event_wake_mask(). */ + { + u32 mask; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HOST_EVENT_GET_WAKE_MASK); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_host_event_mask)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + mask = ec_dev->host_event_wake_mask; + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_LID_CLOSED), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_AC_DISCONNECTED), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_LOW), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_CRITICAL), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_PD_MCU), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_STATUS), 0); + } +} + +static void cros_ec_proto_test_query_all_default_wake_mask_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + + /* Set some garbage bytes. */ + ec_dev->host_event_wake_mask = U32_MAX; + + /* For cros_ec_get_proto_info() without passthru. */ + { + struct ec_response_get_protocol_info *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + /* + * Although it doesn't check the value, provides valid sizes so that + * cros_ec_query_all() allocates din and dout correctly. + */ + data = (struct ec_response_get_protocol_info *)mock->o_data; + data->max_request_packet_size = 0xbe; + data->max_response_packet_size = 0xef; + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For get_host_event_wake_mask(). */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + cros_ec_proto_test_query_all_pretest(test); + ret = cros_ec_query_all(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* For cros_ec_get_proto_info() without passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_proto_info() with passthru. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, + EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) | + EC_CMD_GET_PROTOCOL_INFO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_protocol_info)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + /* For cros_ec_get_host_command_version_mask() for MKBP. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For cros_ec_get_host_command_version_mask() for host sleep v1. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_CMD_VERSIONS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_cmd_versions)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(struct ec_params_get_cmd_versions)); + } + + /* For get_host_event_wake_mask(). */ + { + u32 mask; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HOST_EVENT_GET_WAKE_MASK); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_host_event_mask)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + + mask = ec_dev->host_event_wake_mask; + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_LID_CLOSED), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_AC_DISCONNECTED), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_LOW), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_CRITICAL), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_PD_MCU), 0); + KUNIT_EXPECT_EQ(test, mask & EC_HOST_EVENT_MASK(EC_HOST_EVENT_BATTERY_STATUS), 0); + } +} + +static void cros_ec_proto_test_cmd_xfer_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + DEFINE_RAW_FLEX(struct cros_ec_command, buf, data, 0x100); + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->max_passthru = 0xdd; + + buf->version = 0; + buf->command = EC_CMD_HELLO; + buf->insize = 4; + buf->outsize = 2; + buf->data[0] = 0x55; + buf->data[1] = 0xaa; + + { + u8 *data; + + mock = cros_kunit_ec_xfer_mock_add(test, 4); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (u8 *)mock->o_data; + data[0] = 0xaa; + data[1] = 0x55; + data[2] = 0xcc; + data[3] = 0x33; + } + + ret = cros_ec_cmd_xfer(ec_dev, buf); + KUNIT_EXPECT_EQ(test, ret, 4); + + { + u8 *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, 4); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 2); + + data = (u8 *)mock->i_data; + KUNIT_EXPECT_EQ(test, data[0], 0x55); + KUNIT_EXPECT_EQ(test, data[1], 0xaa); + + KUNIT_EXPECT_EQ(test, buf->data[0], 0xaa); + KUNIT_EXPECT_EQ(test, buf->data[1], 0x55); + KUNIT_EXPECT_EQ(test, buf->data[2], 0xcc); + KUNIT_EXPECT_EQ(test, buf->data[3], 0x33); + } +} + +static void cros_ec_proto_test_cmd_xfer_excess_msg_insize(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + DEFINE_RAW_FLEX(struct cros_ec_command, buf, data, 0x100); + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->max_passthru = 0xdd; + + buf->version = 0; + buf->command = EC_CMD_HELLO; + buf->insize = 0xee + 1; + buf->outsize = 2; + + { + mock = cros_kunit_ec_xfer_mock_add(test, 0xcc); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer(ec_dev, buf); + KUNIT_EXPECT_EQ(test, ret, 0xcc); + + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_HELLO); + KUNIT_EXPECT_EQ(test, mock->msg.insize, 0xee); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 2); + } +} + +static void cros_ec_proto_test_cmd_xfer_excess_msg_outsize_without_passthru(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + DEFINE_RAW_FLEX(struct cros_ec_command, buf, data, 0x100); + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->max_passthru = 0xdd; + + buf->version = 0; + buf->command = EC_CMD_HELLO; + buf->insize = 4; + buf->outsize = 0xff + 1; + + ret = cros_ec_cmd_xfer(ec_dev, buf); + KUNIT_EXPECT_EQ(test, ret, -EMSGSIZE); +} + +static void cros_ec_proto_test_cmd_xfer_excess_msg_outsize_with_passthru(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + DEFINE_RAW_FLEX(struct cros_ec_command, buf, data, 0x100); + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->max_passthru = 0xdd; + + buf->version = 0; + buf->command = EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX) + EC_CMD_HELLO; + buf->insize = 4; + buf->outsize = 0xdd + 1; + + ret = cros_ec_cmd_xfer(ec_dev, buf); + KUNIT_EXPECT_EQ(test, ret, -EMSGSIZE); +} + +static void cros_ec_proto_test_cmd_xfer_protocol_v3_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->proto_version = 3; + ec_dev->cmd_xfer = cros_kunit_ec_cmd_xfer_mock; + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, 0); + + KUNIT_EXPECT_EQ(test, cros_kunit_ec_cmd_xfer_mock_called, 0); + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 1); +} + +static void cros_ec_proto_test_cmd_xfer_protocol_v3_no_op(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->proto_version = 3; + ec_dev->cmd_xfer = cros_kunit_ec_cmd_xfer_mock; + ec_dev->pkt_xfer = NULL; + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EIO); +} + +static void cros_ec_proto_test_cmd_xfer_protocol_v2_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->proto_version = 2; + ec_dev->cmd_xfer = cros_kunit_ec_cmd_xfer_mock; + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, 0); + + KUNIT_EXPECT_EQ(test, cros_kunit_ec_cmd_xfer_mock_called, 1); + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 0); +} + +static void cros_ec_proto_test_cmd_xfer_protocol_v2_no_op(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->proto_version = 2; + ec_dev->cmd_xfer = NULL; + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EIO); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS. */ + { + struct ec_response_get_comms_status *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_comms_status *)mock->o_data; + data->flags = 0; + } + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, sizeof(struct ec_response_get_comms_status)); + + KUNIT_EXPECT_EQ(test, msg.result, EC_RES_SUCCESS); + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_COMMS_STATUS); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_comms_status)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } + + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 2); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_retries_eagain(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS EC_COMMAND_RETRIES times. */ + cros_kunit_ec_xfer_mock_default_ret = -EAGAIN; + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EAGAIN); + + /* For EC_CMD_GET_COMMS_STATUS EC_COMMAND_RETRIES times. */ + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 51); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_retries_status_processing(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS EC_COMMAND_RETRIES times. */ + { + struct ec_response_get_comms_status *data; + int i; + + for (i = 0; i < 50; ++i) { + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_comms_status *)mock->o_data; + data->flags |= EC_COMMS_STATUS_PROCESSING; + } + } + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EAGAIN); + + /* For EC_CMD_GET_COMMS_STATUS EC_COMMAND_RETRIES times. */ + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 51); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_xfer_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, -EIO, EC_RES_SUCCESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EIO); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_INVALID_COMMAND, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, 0); + + KUNIT_EXPECT_EQ(test, msg.result, EC_RES_INVALID_COMMAND); + + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 2); +} + +static void cros_ec_proto_test_cmd_xfer_in_progress_return0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + ec_dev->pkt_xfer = cros_kunit_ec_pkt_xfer_mock; + + /* For the first host command to return EC_RES_IN_PROGRESS. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, EC_RES_IN_PROGRESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For EC_CMD_GET_COMMS_STATUS. */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EPROTO); + + KUNIT_EXPECT_EQ(test, cros_kunit_ec_pkt_xfer_mock_called, 2); +} + +static void cros_ec_proto_test_cmd_xfer_status_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + /* For cros_ec_cmd_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_add(test, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer_status(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, 0); +} + +static void cros_ec_proto_test_cmd_xfer_status_xfer_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + struct cros_ec_command msg; + + memset(&msg, 0, sizeof(msg)); + + /* For cros_ec_cmd_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, -EPROTO, EC_RES_SUCCESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_cmd_xfer_status(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, -EPROTO); +} + +static void cros_ec_proto_test_cmd_xfer_status_return_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret, i; + struct cros_ec_command msg; + static const int map[] = { + [EC_RES_SUCCESS] = 0, + [EC_RES_INVALID_COMMAND] = -EOPNOTSUPP, + [EC_RES_ERROR] = -EIO, + [EC_RES_INVALID_PARAM] = -EINVAL, + [EC_RES_ACCESS_DENIED] = -EACCES, + [EC_RES_INVALID_RESPONSE] = -EPROTO, + [EC_RES_INVALID_VERSION] = -ENOPROTOOPT, + [EC_RES_INVALID_CHECKSUM] = -EBADMSG, + /* + * EC_RES_IN_PROGRESS is special because cros_ec_send_command() has extra logic to + * handle it. Note that default cros_kunit_ec_xfer_mock_default_ret == 0 thus + * cros_ec_xfer_command() in cros_ec_wait_until_complete() returns 0. As a result, + * it returns -EPROTO without calling cros_ec_map_error(). + */ + [EC_RES_IN_PROGRESS] = -EPROTO, + [EC_RES_UNAVAILABLE] = -ENODATA, + [EC_RES_TIMEOUT] = -ETIMEDOUT, + [EC_RES_OVERFLOW] = -EOVERFLOW, + [EC_RES_INVALID_HEADER] = -EBADR, + [EC_RES_REQUEST_TRUNCATED] = -EBADR, + [EC_RES_RESPONSE_TOO_BIG] = -EFBIG, + [EC_RES_BUS_ERROR] = -EFAULT, + [EC_RES_BUSY] = -EBUSY, + [EC_RES_INVALID_HEADER_VERSION] = -EBADMSG, + [EC_RES_INVALID_HEADER_CRC] = -EBADMSG, + [EC_RES_INVALID_DATA_CRC] = -EBADMSG, + [EC_RES_DUP_UNAVAILABLE] = -ENODATA, + }; + + memset(&msg, 0, sizeof(msg)); + + for (i = 0; i < ARRAY_SIZE(map); ++i) { + mock = cros_kunit_ec_xfer_mock_addx(test, 0, i, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + ret = cros_ec_cmd_xfer_status(ec_dev, &msg); + KUNIT_EXPECT_EQ(test, ret, map[i]); + } +} + +static void cros_ec_proto_test_get_next_event_no_mkbp_event(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + bool wake_event, more_events; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->mkbp_event_supported = 0; + + /* Set some garbage bytes. */ + wake_event = false; + more_events = true; + + /* For get_keyboard_state_event(). */ + { + union ec_response_get_next_data_v3 *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (union ec_response_get_next_data_v3 *)mock->o_data; + data->host_event = 0xbeef; + } + + ret = cros_ec_get_next_event(ec_dev, &wake_event, &more_events); + KUNIT_EXPECT_EQ(test, ret, sizeof(union ec_response_get_next_data_v3)); + + KUNIT_EXPECT_EQ(test, ec_dev->event_data.event_type, EC_MKBP_EVENT_KEY_MATRIX); + KUNIT_EXPECT_EQ(test, ec_dev->event_data.data.host_event, 0xbeef); + + KUNIT_EXPECT_TRUE(test, wake_event); + KUNIT_EXPECT_FALSE(test, more_events); + + /* For get_keyboard_state_event(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_MKBP_STATE); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(union ec_response_get_next_data_v3)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_next_event_mkbp_event_ec_suspended(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + + ec_dev->mkbp_event_supported = 1; + ec_dev->suspended = true; + + ret = cros_ec_get_next_event(ec_dev, NULL, NULL); + KUNIT_EXPECT_EQ(test, ret, -EHOSTDOWN); +} + +static void cros_ec_proto_test_get_next_event_mkbp_event_version0(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + bool wake_event, more_events; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->mkbp_event_supported = 1; + + /* Set some garbage bytes. */ + wake_event = true; + more_events = false; + + /* For get_next_event_xfer(). */ + { + struct ec_response_get_next_event *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_next_event *)mock->o_data; + data->event_type = EC_MKBP_EVENT_SENSOR_FIFO | EC_MKBP_HAS_MORE_EVENTS; + data->data.sysrq = 0xbeef; + } + + ret = cros_ec_get_next_event(ec_dev, &wake_event, &more_events); + KUNIT_EXPECT_EQ(test, ret, sizeof(struct ec_response_get_next_event)); + + KUNIT_EXPECT_EQ(test, ec_dev->event_data.event_type, EC_MKBP_EVENT_SENSOR_FIFO); + KUNIT_EXPECT_EQ(test, ec_dev->event_data.data.sysrq, 0xbeef); + + KUNIT_EXPECT_FALSE(test, wake_event); + KUNIT_EXPECT_TRUE(test, more_events); + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_NEXT_EVENT); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_get_next_event)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_next_event_mkbp_event_version2(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + bool wake_event, more_events; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->mkbp_event_supported = 3; + + /* Set some garbage bytes. */ + wake_event = false; + more_events = true; + + /* For get_next_event_xfer(). */ + { + struct ec_response_get_next_event_v1 *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_next_event_v1 *)mock->o_data; + data->event_type = EC_MKBP_EVENT_FINGERPRINT; + data->data.sysrq = 0xbeef; + } + + ret = cros_ec_get_next_event(ec_dev, &wake_event, &more_events); + KUNIT_EXPECT_EQ(test, ret, sizeof(struct ec_response_get_next_event_v1)); + + KUNIT_EXPECT_EQ(test, ec_dev->event_data.event_type, EC_MKBP_EVENT_FINGERPRINT); + KUNIT_EXPECT_EQ(test, ec_dev->event_data.data.sysrq, 0xbeef); + + KUNIT_EXPECT_TRUE(test, wake_event); + KUNIT_EXPECT_FALSE(test, more_events); + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 2); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_NEXT_EVENT); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_next_event_v1)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_next_event_mkbp_event_host_event_rtc(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + bool wake_event; + struct ec_response_get_next_event_v1 *data; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->mkbp_event_supported = 3; + ec_dev->host_event_wake_mask = U32_MAX; + + /* Set some garbage bytes. */ + wake_event = true; + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_add(test, + sizeof(data->event_type) + + sizeof(data->data.host_event)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_next_event_v1 *)mock->o_data; + data->event_type = EC_MKBP_EVENT_HOST_EVENT; + put_unaligned_le32(EC_HOST_EVENT_MASK(EC_HOST_EVENT_RTC), &data->data.host_event); + } + + ret = cros_ec_get_next_event(ec_dev, &wake_event, NULL); + KUNIT_EXPECT_EQ(test, ret, sizeof(data->event_type) + sizeof(data->data.host_event)); + + KUNIT_EXPECT_EQ(test, ec_dev->event_data.event_type, EC_MKBP_EVENT_HOST_EVENT); + + KUNIT_EXPECT_FALSE(test, wake_event); + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 2); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_NEXT_EVENT); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_next_event_v1)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_next_event_mkbp_event_host_event_masked(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + bool wake_event; + struct ec_response_get_next_event_v1 *data; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->mkbp_event_supported = 3; + ec_dev->host_event_wake_mask = U32_MAX & ~EC_HOST_EVENT_MASK(EC_HOST_EVENT_AC_DISCONNECTED); + + /* Set some garbage bytes. */ + wake_event = true; + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_add(test, + sizeof(data->event_type) + + sizeof(data->data.host_event)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_next_event_v1 *)mock->o_data; + data->event_type = EC_MKBP_EVENT_HOST_EVENT; + put_unaligned_le32(EC_HOST_EVENT_MASK(EC_HOST_EVENT_AC_DISCONNECTED), + &data->data.host_event); + } + + ret = cros_ec_get_next_event(ec_dev, &wake_event, NULL); + KUNIT_EXPECT_EQ(test, ret, sizeof(data->event_type) + sizeof(data->data.host_event)); + + KUNIT_EXPECT_EQ(test, ec_dev->event_data.event_type, EC_MKBP_EVENT_HOST_EVENT); + + KUNIT_EXPECT_FALSE(test, wake_event); + + /* For get_next_event_xfer(). */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 2); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_NEXT_EVENT); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_get_next_event_v1)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_host_event_no_mkbp_event(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + + ec_dev->mkbp_event_supported = 0; + + ret = cros_ec_get_host_event(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); +} + +static void cros_ec_proto_test_get_host_event_not_host_event(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + + ec_dev->mkbp_event_supported = 1; + ec_dev->event_data.event_type = EC_MKBP_EVENT_FINGERPRINT; + + ret = cros_ec_get_host_event(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); +} + +static void cros_ec_proto_test_get_host_event_wrong_event_size(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + + ec_dev->mkbp_event_supported = 1; + ec_dev->event_data.event_type = EC_MKBP_EVENT_HOST_EVENT; + ec_dev->event_size = 0xff; + + ret = cros_ec_get_host_event(ec_dev); + KUNIT_EXPECT_EQ(test, ret, 0); +} + +static void cros_ec_proto_test_get_host_event_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + int ret; + + ec_dev->mkbp_event_supported = 1; + ec_dev->event_data.event_type = EC_MKBP_EVENT_HOST_EVENT; + ec_dev->event_size = sizeof(ec_dev->event_data.data.host_event); + put_unaligned_le32(EC_HOST_EVENT_MASK(EC_HOST_EVENT_RTC), + &ec_dev->event_data.data.host_event); + + ret = cros_ec_get_host_event(ec_dev); + KUNIT_EXPECT_EQ(test, ret, EC_HOST_EVENT_MASK(EC_HOST_EVENT_RTC)); +} + +static void cros_ec_proto_test_check_features_cached(struct kunit *test) +{ + int ret, i; + static struct cros_ec_dev ec; + + ec.features.flags[0] = EC_FEATURE_MASK_0(EC_FEATURE_FINGERPRINT); + ec.features.flags[1] = EC_FEATURE_MASK_0(EC_FEATURE_SCP); + + for (i = 0; i < EC_FEATURE_TYPEC_MUX_REQUIRE_AP_ACK; ++i) { + ret = cros_ec_check_features(&ec, i); + switch (i) { + case EC_FEATURE_FINGERPRINT: + case EC_FEATURE_SCP: + KUNIT_EXPECT_TRUE(test, ret); + break; + default: + KUNIT_EXPECT_FALSE(test, ret); + break; + } + } +} + +static void cros_ec_proto_test_check_features_not_cached(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret, i; + static struct cros_ec_dev ec; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec.ec_dev = ec_dev; + ec.dev = ec_dev->dev; + ec.cmd_offset = 0; + ec.features.flags[0] = -1; + ec.features.flags[1] = -1; + + /* For EC_CMD_GET_FEATURES. */ + { + struct ec_response_get_features *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_get_features *)mock->o_data; + data->flags[0] = EC_FEATURE_MASK_0(EC_FEATURE_FINGERPRINT); + data->flags[1] = EC_FEATURE_MASK_0(EC_FEATURE_SCP); + } + + for (i = 0; i < EC_FEATURE_TYPEC_MUX_REQUIRE_AP_ACK; ++i) { + ret = cros_ec_check_features(&ec, i); + switch (i) { + case EC_FEATURE_FINGERPRINT: + case EC_FEATURE_SCP: + KUNIT_EXPECT_TRUE(test, ret); + break; + default: + KUNIT_EXPECT_FALSE(test, ret); + break; + } + } + + /* For EC_CMD_GET_FEATURES. */ + { + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_GET_FEATURES); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_get_features)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, 0); + } +} + +static void cros_ec_proto_test_get_sensor_count_normal(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + static struct cros_ec_dev ec; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec.ec_dev = ec_dev; + ec.dev = ec_dev->dev; + ec.cmd_offset = 0; + + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + struct ec_response_motion_sense *data; + + mock = cros_kunit_ec_xfer_mock_add(test, sizeof(*data)); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (struct ec_response_motion_sense *)mock->o_data; + data->dump.sensor_count = 0xbf; + } + + ret = cros_ec_get_sensor_count(&ec); + KUNIT_EXPECT_EQ(test, ret, 0xbf); + + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + struct ec_params_motion_sense *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 1); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_MOTION_SENSE_CMD); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_motion_sense)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_motion_sense *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, MOTIONSENSE_CMD_DUMP); + } +} + +static void cros_ec_proto_test_get_sensor_count_xfer_error(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + static struct cros_ec_dev ec; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec.ec_dev = ec_dev; + ec.dev = ec_dev->dev; + ec.cmd_offset = 0; + + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, -EPROTO, EC_RES_SUCCESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + ret = cros_ec_get_sensor_count(&ec); + KUNIT_EXPECT_EQ(test, ret, -EPROTO); + + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + struct ec_params_motion_sense *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 1); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_MOTION_SENSE_CMD); + KUNIT_EXPECT_EQ(test, mock->msg.insize, sizeof(struct ec_response_motion_sense)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_motion_sense *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, MOTIONSENSE_CMD_DUMP); + } +} + +static void cros_ec_proto_test_get_sensor_count_legacy(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret, i; + static struct cros_ec_dev ec; + struct { + u8 readmem_data; + int expected_result; + } test_data[] = { + { 0, 0 }, + { EC_MEMMAP_ACC_STATUS_PRESENCE_BIT, 2 }, + }; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + ec_dev->cmd_readmem = cros_kunit_readmem_mock; + ec.ec_dev = ec_dev; + ec.dev = ec_dev->dev; + ec.cmd_offset = 0; + + for (i = 0; i < ARRAY_SIZE(test_data); ++i) { + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + mock = cros_kunit_ec_xfer_mock_addx(test, -EPROTO, EC_RES_SUCCESS, 0); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + } + + /* For readmem. */ + { + cros_kunit_readmem_mock_data = kunit_kzalloc(test, 1, GFP_KERNEL); + KUNIT_ASSERT_PTR_NE(test, cros_kunit_readmem_mock_data, NULL); + cros_kunit_readmem_mock_data[0] = test_data[i].readmem_data; + + cros_kunit_ec_xfer_mock_default_ret = 1; + } + + ret = cros_ec_get_sensor_count(&ec); + KUNIT_EXPECT_EQ(test, ret, test_data[i].expected_result); + + /* For EC_CMD_MOTION_SENSE_CMD. */ + { + struct ec_params_motion_sense *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 1); + KUNIT_EXPECT_EQ(test, mock->msg.command, EC_CMD_MOTION_SENSE_CMD); + KUNIT_EXPECT_EQ(test, mock->msg.insize, + sizeof(struct ec_response_motion_sense)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, sizeof(*data)); + + data = (struct ec_params_motion_sense *)mock->i_data; + KUNIT_EXPECT_EQ(test, data->cmd, MOTIONSENSE_CMD_DUMP); + } + + /* For readmem. */ + { + KUNIT_EXPECT_EQ(test, cros_kunit_readmem_mock_offset, EC_MEMMAP_ACC_STATUS); + } + } +} + +static void cros_ec_proto_test_ec_cmd(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + struct ec_xfer_mock *mock; + int ret; + u8 out[3], in[2]; + + ec_dev->max_request = 0xff; + ec_dev->max_response = 0xee; + + out[0] = 0xdd; + out[1] = 0xcc; + out[2] = 0xbb; + + { + u8 *data; + + mock = cros_kunit_ec_xfer_mock_add(test, 2); + KUNIT_ASSERT_PTR_NE(test, mock, NULL); + + data = (u8 *)mock->o_data; + data[0] = 0xaa; + data[1] = 0x99; + } + + ret = cros_ec_cmd(ec_dev, 0x88, 0x77, out, ARRAY_SIZE(out), in, ARRAY_SIZE(in)); + KUNIT_EXPECT_EQ(test, ret, 2); + + { + u8 *data; + + mock = cros_kunit_ec_xfer_mock_next(); + KUNIT_EXPECT_PTR_NE(test, mock, NULL); + + KUNIT_EXPECT_EQ(test, mock->msg.version, 0x88); + KUNIT_EXPECT_EQ(test, mock->msg.command, 0x77); + KUNIT_EXPECT_EQ(test, mock->msg.insize, ARRAY_SIZE(in)); + KUNIT_EXPECT_EQ(test, mock->msg.outsize, ARRAY_SIZE(out)); + + data = (u8 *)mock->i_data; + KUNIT_EXPECT_EQ(test, data[0], 0xdd); + KUNIT_EXPECT_EQ(test, data[1], 0xcc); + KUNIT_EXPECT_EQ(test, data[2], 0xbb); + } +} + +static void cros_ec_proto_test_release(struct device *dev) +{ +} + +static int cros_ec_proto_test_init(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv; + struct cros_ec_device *ec_dev; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + test->priv = priv; + + ec_dev = &priv->ec_dev; + ec_dev->dout = (u8 *)priv->dout; + ec_dev->dout_size = ARRAY_SIZE(priv->dout); + ec_dev->din = (u8 *)priv->din; + ec_dev->din_size = ARRAY_SIZE(priv->din); + ec_dev->proto_version = EC_HOST_REQUEST_VERSION; + ec_dev->dev = kunit_kzalloc(test, sizeof(*ec_dev->dev), GFP_KERNEL); + if (!ec_dev->dev) + return -ENOMEM; + device_initialize(ec_dev->dev); + dev_set_name(ec_dev->dev, "cros_ec_proto_test"); + ec_dev->dev->release = cros_ec_proto_test_release; + ec_dev->cmd_xfer = cros_kunit_ec_xfer_mock; + ec_dev->pkt_xfer = cros_kunit_ec_xfer_mock; + mutex_init(&ec_dev->lock); + + priv->msg = (struct cros_ec_command *)priv->_msg; + + cros_kunit_mock_reset(); + + return 0; +} + +static void cros_ec_proto_test_exit(struct kunit *test) +{ + struct cros_ec_proto_test_priv *priv = test->priv; + struct cros_ec_device *ec_dev = &priv->ec_dev; + + put_device(ec_dev->dev); +} + +static struct kunit_case cros_ec_proto_test_cases[] = { + KUNIT_CASE(cros_ec_proto_test_prepare_tx_legacy_normal), + KUNIT_CASE(cros_ec_proto_test_prepare_tx_legacy_bad_msg_outsize), + KUNIT_CASE(cros_ec_proto_test_prepare_tx_normal), + KUNIT_CASE(cros_ec_proto_test_prepare_tx_bad_msg_outsize), + KUNIT_CASE(cros_ec_proto_test_check_result), + KUNIT_CASE(cros_ec_proto_test_query_all_normal), + KUNIT_CASE(cros_ec_proto_test_query_all_no_pd_return_error), + KUNIT_CASE(cros_ec_proto_test_query_all_no_pd_return0), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_normal_v3_return_error), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_normal_v3_return0), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_xfer_error), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_return_error), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_data_error), + KUNIT_CASE(cros_ec_proto_test_query_all_legacy_return0), + KUNIT_CASE(cros_ec_proto_test_query_all_no_mkbp), + KUNIT_CASE(cros_ec_proto_test_query_all_no_mkbp_return_error), + KUNIT_CASE(cros_ec_proto_test_query_all_no_mkbp_return0), + KUNIT_CASE(cros_ec_proto_test_query_all_no_host_sleep), + KUNIT_CASE(cros_ec_proto_test_query_all_no_host_sleep_return0), + KUNIT_CASE(cros_ec_proto_test_query_all_default_wake_mask_return_error), + KUNIT_CASE(cros_ec_proto_test_query_all_default_wake_mask_return0), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_normal), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_excess_msg_insize), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_excess_msg_outsize_without_passthru), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_excess_msg_outsize_with_passthru), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_protocol_v3_normal), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_protocol_v3_no_op), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_protocol_v2_normal), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_protocol_v2_no_op), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_normal), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_retries_eagain), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_retries_status_processing), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_xfer_error), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_return_error), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_in_progress_return0), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_status_normal), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_status_xfer_error), + KUNIT_CASE(cros_ec_proto_test_cmd_xfer_status_return_error), + KUNIT_CASE(cros_ec_proto_test_get_next_event_no_mkbp_event), + KUNIT_CASE(cros_ec_proto_test_get_next_event_mkbp_event_ec_suspended), + KUNIT_CASE(cros_ec_proto_test_get_next_event_mkbp_event_version0), + KUNIT_CASE(cros_ec_proto_test_get_next_event_mkbp_event_version2), + KUNIT_CASE(cros_ec_proto_test_get_next_event_mkbp_event_host_event_rtc), + KUNIT_CASE(cros_ec_proto_test_get_next_event_mkbp_event_host_event_masked), + KUNIT_CASE(cros_ec_proto_test_get_host_event_no_mkbp_event), + KUNIT_CASE(cros_ec_proto_test_get_host_event_not_host_event), + KUNIT_CASE(cros_ec_proto_test_get_host_event_wrong_event_size), + KUNIT_CASE(cros_ec_proto_test_get_host_event_normal), + KUNIT_CASE(cros_ec_proto_test_check_features_cached), + KUNIT_CASE(cros_ec_proto_test_check_features_not_cached), + KUNIT_CASE(cros_ec_proto_test_get_sensor_count_normal), + KUNIT_CASE(cros_ec_proto_test_get_sensor_count_xfer_error), + KUNIT_CASE(cros_ec_proto_test_get_sensor_count_legacy), + KUNIT_CASE(cros_ec_proto_test_ec_cmd), + {} +}; + +static struct kunit_suite cros_ec_proto_test_suite = { + .name = "cros_ec_proto_test", + .init = cros_ec_proto_test_init, + .exit = cros_ec_proto_test_exit, + .test_cases = cros_ec_proto_test_cases, +}; + +kunit_test_suite(cros_ec_proto_test_suite); + +MODULE_DESCRIPTION("Kunit tests for ChromeOS Embedded Controller protocol"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_ec_proto_test_util.c b/drivers/platform/chrome/cros_ec_proto_test_util.c new file mode 100644 index 000000000..65d328bcd --- /dev/null +++ b/drivers/platform/chrome/cros_ec_proto_test_util.c @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CrOS Kunit tests utilities. + */ + +#include + +#include +#include +#include +#include + +#include "cros_ec.h" +#include "cros_ec_proto_test_util.h" + +int cros_kunit_ec_xfer_mock_default_result; +int cros_kunit_ec_xfer_mock_default_ret; +int cros_kunit_ec_cmd_xfer_mock_called; +int cros_kunit_ec_pkt_xfer_mock_called; + +static struct list_head cros_kunit_ec_xfer_mock_in; +static struct list_head cros_kunit_ec_xfer_mock_out; + +int cros_kunit_ec_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + struct ec_xfer_mock *mock; + + mock = list_first_entry_or_null(&cros_kunit_ec_xfer_mock_in, struct ec_xfer_mock, list); + if (!mock) { + msg->result = cros_kunit_ec_xfer_mock_default_result; + return cros_kunit_ec_xfer_mock_default_ret; + } + + list_del(&mock->list); + + memcpy(&mock->msg, msg, sizeof(*msg)); + if (msg->outsize) { + mock->i_data = kunit_kzalloc(mock->test, msg->outsize, GFP_KERNEL); + if (mock->i_data) + memcpy(mock->i_data, msg->data, msg->outsize); + } + + msg->result = mock->result; + if (msg->insize) + memcpy(msg->data, mock->o_data, min(msg->insize, mock->o_data_len)); + + list_add_tail(&mock->list, &cros_kunit_ec_xfer_mock_out); + + return mock->ret; +} + +int cros_kunit_ec_cmd_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + ++cros_kunit_ec_cmd_xfer_mock_called; + return cros_kunit_ec_xfer_mock(ec_dev, msg); +} + +int cros_kunit_ec_pkt_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg) +{ + ++cros_kunit_ec_pkt_xfer_mock_called; + return cros_kunit_ec_xfer_mock(ec_dev, msg); +} + +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_add(struct kunit *test, size_t size) +{ + return cros_kunit_ec_xfer_mock_addx(test, size, EC_RES_SUCCESS, size); +} + +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_addx(struct kunit *test, + int ret, int result, size_t size) +{ + struct ec_xfer_mock *mock; + + mock = kunit_kzalloc(test, sizeof(*mock), GFP_KERNEL); + if (!mock) + return NULL; + + list_add_tail(&mock->list, &cros_kunit_ec_xfer_mock_in); + mock->test = test; + + mock->ret = ret; + mock->result = result; + mock->o_data = kunit_kzalloc(test, size, GFP_KERNEL); + if (!mock->o_data) + return NULL; + mock->o_data_len = size; + + return mock; +} + +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_next(void) +{ + struct ec_xfer_mock *mock; + + mock = list_first_entry_or_null(&cros_kunit_ec_xfer_mock_out, struct ec_xfer_mock, list); + if (mock) + list_del(&mock->list); + + return mock; +} + +int cros_kunit_readmem_mock_offset; +u8 *cros_kunit_readmem_mock_data; +int cros_kunit_readmem_mock_ret; + +int cros_kunit_readmem_mock(struct cros_ec_device *ec_dev, unsigned int offset, + unsigned int bytes, void *dest) +{ + cros_kunit_readmem_mock_offset = offset; + + memcpy(dest, cros_kunit_readmem_mock_data, bytes); + + return cros_kunit_readmem_mock_ret; +} + +void cros_kunit_mock_reset(void) +{ + cros_kunit_ec_xfer_mock_default_result = 0; + cros_kunit_ec_xfer_mock_default_ret = 0; + cros_kunit_ec_cmd_xfer_mock_called = 0; + cros_kunit_ec_pkt_xfer_mock_called = 0; + INIT_LIST_HEAD(&cros_kunit_ec_xfer_mock_in); + INIT_LIST_HEAD(&cros_kunit_ec_xfer_mock_out); + + cros_kunit_readmem_mock_offset = 0; + cros_kunit_readmem_mock_data = NULL; + cros_kunit_readmem_mock_ret = 0; +} diff --git a/drivers/platform/chrome/cros_ec_proto_test_util.h b/drivers/platform/chrome/cros_ec_proto_test_util.h new file mode 100644 index 000000000..b17239f05 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_proto_test_util.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * CrOS Kunit tests utilities. + */ + +#ifndef _CROS_KUNIT_UTIL_H_ +#define _CROS_KUNIT_UTIL_H_ + +#include + +struct ec_xfer_mock { + struct list_head list; + struct kunit *test; + + /* input */ + void *i_data; + + /* output */ + int ret; + int result; + void *o_data; + u32 o_data_len; + + /* input */ + /* Must be last -ends in a flexible-array member. */ + struct cros_ec_command msg; +}; + +extern int cros_kunit_ec_xfer_mock_default_result; +extern int cros_kunit_ec_xfer_mock_default_ret; +extern int cros_kunit_ec_cmd_xfer_mock_called; +extern int cros_kunit_ec_pkt_xfer_mock_called; + +int cros_kunit_ec_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg); +int cros_kunit_ec_cmd_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg); +int cros_kunit_ec_pkt_xfer_mock(struct cros_ec_device *ec_dev, struct cros_ec_command *msg); +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_add(struct kunit *test, size_t size); +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_addx(struct kunit *test, + int ret, int result, size_t size); +struct ec_xfer_mock *cros_kunit_ec_xfer_mock_next(void); + +extern int cros_kunit_readmem_mock_offset; +extern u8 *cros_kunit_readmem_mock_data; +extern int cros_kunit_readmem_mock_ret; + +int cros_kunit_readmem_mock(struct cros_ec_device *ec_dev, unsigned int offset, + unsigned int bytes, void *dest); + +void cros_kunit_mock_reset(void); + +#endif diff --git a/drivers/platform/chrome/cros_ec_rpmsg.c b/drivers/platform/chrome/cros_ec_rpmsg.c new file mode 100644 index 000000000..09bd9e494 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_rpmsg.c @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright 2018 Google LLC. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +#define EC_MSG_TIMEOUT_MS 200 +#define HOST_COMMAND_MARK 1 +#define HOST_EVENT_MARK 2 + +/** + * struct cros_ec_rpmsg_response - rpmsg message format from from EC. + * + * @type: The type of message, should be either HOST_COMMAND_MARK or + * HOST_EVENT_MARK, representing that the message is a response to + * host command, or a host event. + * @data: ec_host_response for host command. + */ +struct cros_ec_rpmsg_response { + u8 type; + u8 data[] __aligned(4); +}; + +/** + * struct cros_ec_rpmsg - information about a EC over rpmsg. + * + * @rpdev: rpmsg device we are connected to + * @xfer_ack: completion for host command transfer. + * @host_event_work: Work struct for pending host event. + * @ept: The rpmsg endpoint of this channel. + * @has_pending_host_event: Boolean used to check if there is a pending event. + * @probe_done: Flag to indicate that probe is done. + */ +struct cros_ec_rpmsg { + struct rpmsg_device *rpdev; + struct completion xfer_ack; + struct work_struct host_event_work; + struct rpmsg_endpoint *ept; + bool has_pending_host_event; + bool probe_done; +}; + +/** + * cros_ec_cmd_xfer_rpmsg - Transfer a message over rpmsg and receive the reply + * + * @ec_dev: ChromeOS EC device + * @ec_msg: Message to transfer + * + * This is only used for old EC proto version, and is not supported for this + * driver. + * + * Return: -EINVAL + */ +static int cros_ec_cmd_xfer_rpmsg(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + return -EINVAL; +} + +/** + * cros_ec_pkt_xfer_rpmsg - Transfer a packet over rpmsg and receive the reply + * + * @ec_dev: ChromeOS EC device + * @ec_msg: Message to transfer + * + * Return: number of bytes of the reply on success or negative error code. + */ +static int cros_ec_pkt_xfer_rpmsg(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + struct cros_ec_rpmsg *ec_rpmsg = ec_dev->priv; + struct ec_host_response *response; + unsigned long timeout; + int len; + int ret; + u8 sum; + int i; + + ec_msg->result = 0; + len = cros_ec_prepare_tx(ec_dev, ec_msg); + if (len < 0) + return len; + dev_dbg(ec_dev->dev, "prepared, len=%d\n", len); + + reinit_completion(&ec_rpmsg->xfer_ack); + ret = rpmsg_send(ec_rpmsg->ept, ec_dev->dout, len); + if (ret) { + dev_err(ec_dev->dev, "rpmsg send failed\n"); + return ret; + } + + timeout = msecs_to_jiffies(EC_MSG_TIMEOUT_MS); + ret = wait_for_completion_timeout(&ec_rpmsg->xfer_ack, timeout); + if (!ret) { + dev_err(ec_dev->dev, "rpmsg send timeout\n"); + return -EIO; + } + + /* check response error code */ + response = (struct ec_host_response *)ec_dev->din; + ec_msg->result = response->result; + + ret = cros_ec_check_result(ec_dev, ec_msg); + if (ret) + goto exit; + + if (response->data_len > ec_msg->insize) { + dev_err(ec_dev->dev, "packet too long (%d bytes, expected %d)", + response->data_len, ec_msg->insize); + ret = -EMSGSIZE; + goto exit; + } + + /* copy response packet payload and compute checksum */ + memcpy(ec_msg->data, ec_dev->din + sizeof(*response), + response->data_len); + + sum = 0; + for (i = 0; i < sizeof(*response) + response->data_len; i++) + sum += ec_dev->din[i]; + + if (sum) { + dev_err(ec_dev->dev, "bad packet checksum, calculated %x\n", + sum); + ret = -EBADMSG; + goto exit; + } + + ret = response->data_len; +exit: + if (ec_msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +static void +cros_ec_rpmsg_host_event_function(struct work_struct *host_event_work) +{ + struct cros_ec_rpmsg *ec_rpmsg = container_of(host_event_work, + struct cros_ec_rpmsg, + host_event_work); + + cros_ec_irq_thread(0, dev_get_drvdata(&ec_rpmsg->rpdev->dev)); +} + +static int cros_ec_rpmsg_callback(struct rpmsg_device *rpdev, void *data, + int len, void *priv, u32 src) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(&rpdev->dev); + struct cros_ec_rpmsg *ec_rpmsg = ec_dev->priv; + struct cros_ec_rpmsg_response *resp; + + if (!len) { + dev_warn(ec_dev->dev, "rpmsg received empty response"); + return -EINVAL; + } + + resp = data; + len -= offsetof(struct cros_ec_rpmsg_response, data); + if (resp->type == HOST_COMMAND_MARK) { + if (len > ec_dev->din_size) { + dev_warn(ec_dev->dev, + "received length %d > din_size %d, truncating", + len, ec_dev->din_size); + len = ec_dev->din_size; + } + + memcpy(ec_dev->din, resp->data, len); + complete(&ec_rpmsg->xfer_ack); + } else if (resp->type == HOST_EVENT_MARK) { + /* + * If the host event is sent before cros_ec_register is + * finished, queue the host event. + */ + if (ec_rpmsg->probe_done) + schedule_work(&ec_rpmsg->host_event_work); + else + ec_rpmsg->has_pending_host_event = true; + } else { + dev_warn(ec_dev->dev, "rpmsg received invalid type = %d", + resp->type); + return -EINVAL; + } + + return 0; +} + +static struct rpmsg_endpoint * +cros_ec_rpmsg_create_ept(struct rpmsg_device *rpdev) +{ + struct rpmsg_channel_info chinfo = {}; + + strscpy(chinfo.name, rpdev->id.name, RPMSG_NAME_SIZE); + chinfo.src = rpdev->src; + chinfo.dst = RPMSG_ADDR_ANY; + + return rpmsg_create_ept(rpdev, cros_ec_rpmsg_callback, NULL, chinfo); +} + +static int cros_ec_rpmsg_probe(struct rpmsg_device *rpdev) +{ + struct device *dev = &rpdev->dev; + struct cros_ec_rpmsg *ec_rpmsg; + struct cros_ec_device *ec_dev; + int ret; + + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + ec_rpmsg = devm_kzalloc(dev, sizeof(*ec_rpmsg), GFP_KERNEL); + if (!ec_rpmsg) + return -ENOMEM; + + ec_dev->priv = ec_rpmsg; + ec_dev->cmd_xfer = cros_ec_cmd_xfer_rpmsg; + ec_dev->pkt_xfer = cros_ec_pkt_xfer_rpmsg; + ec_dev->phys_name = dev_name(&rpdev->dev); + dev_set_drvdata(dev, ec_dev); + + ec_rpmsg->rpdev = rpdev; + init_completion(&ec_rpmsg->xfer_ack); + INIT_WORK(&ec_rpmsg->host_event_work, + cros_ec_rpmsg_host_event_function); + + ec_rpmsg->ept = cros_ec_rpmsg_create_ept(rpdev); + if (!ec_rpmsg->ept) + return -ENOMEM; + + ret = cros_ec_register(ec_dev); + if (ret < 0) { + rpmsg_destroy_ept(ec_rpmsg->ept); + cancel_work_sync(&ec_rpmsg->host_event_work); + return ret; + } + + ec_rpmsg->probe_done = true; + + if (ec_rpmsg->has_pending_host_event) + schedule_work(&ec_rpmsg->host_event_work); + + return 0; +} + +static void cros_ec_rpmsg_remove(struct rpmsg_device *rpdev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(&rpdev->dev); + struct cros_ec_rpmsg *ec_rpmsg = ec_dev->priv; + + cros_ec_unregister(ec_dev); + rpmsg_destroy_ept(ec_rpmsg->ept); + cancel_work_sync(&ec_rpmsg->host_event_work); +} + +#ifdef CONFIG_PM_SLEEP +static int cros_ec_rpmsg_suspend(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_suspend(ec_dev); +} + +static int cros_ec_rpmsg_resume(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_resume(ec_dev); +} +#endif + +static SIMPLE_DEV_PM_OPS(cros_ec_rpmsg_pm_ops, cros_ec_rpmsg_suspend, + cros_ec_rpmsg_resume); + +static const struct of_device_id cros_ec_rpmsg_of_match[] = { + { .compatible = "google,cros-ec-rpmsg", }, + { } +}; +MODULE_DEVICE_TABLE(of, cros_ec_rpmsg_of_match); + +static struct rpmsg_driver cros_ec_driver_rpmsg = { + .drv = { + .name = "cros-ec-rpmsg", + .of_match_table = cros_ec_rpmsg_of_match, + .pm = &cros_ec_rpmsg_pm_ops, + }, + .probe = cros_ec_rpmsg_probe, + .remove = cros_ec_rpmsg_remove, +}; + +module_rpmsg_driver(cros_ec_driver_rpmsg); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ChromeOS EC multi function device (rpmsg)"); diff --git a/drivers/platform/chrome/cros_ec_sensorhub.c b/drivers/platform/chrome/cros_ec_sensorhub.c new file mode 100644 index 000000000..a8395ab0a --- /dev/null +++ b/drivers/platform/chrome/cros_ec_sensorhub.c @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Sensor HUB driver that discovers sensors behind a ChromeOS Embedded + * Controller. + * + * Copyright 2019 Google LLC + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-sensorhub" +#define CROS_EC_CMD_INFO_RETRIES 50 + +static void cros_ec_sensorhub_free_sensor(void *arg) +{ + struct platform_device *pdev = arg; + + platform_device_unregister(pdev); +} + +static int cros_ec_sensorhub_allocate_sensor(struct device *parent, + char *sensor_name, + int sensor_num) +{ + struct cros_ec_sensor_platform sensor_platforms = { + .sensor_num = sensor_num, + }; + struct platform_device *pdev; + + pdev = platform_device_register_data(parent, sensor_name, + PLATFORM_DEVID_AUTO, + &sensor_platforms, + sizeof(sensor_platforms)); + if (IS_ERR(pdev)) + return PTR_ERR(pdev); + + return devm_add_action_or_reset(parent, + cros_ec_sensorhub_free_sensor, + pdev); +} + +static int cros_ec_sensorhub_register(struct device *dev, + struct cros_ec_sensorhub *sensorhub) +{ + int sensor_type[MOTIONSENSE_TYPE_MAX] = { 0 }; + struct cros_ec_command *msg = sensorhub->msg; + struct cros_ec_dev *ec = sensorhub->ec; + int ret, i, retries; + char *name; + + + msg->version = 1; + msg->insize = sizeof(struct ec_response_motion_sense); + msg->outsize = sizeof(struct ec_params_motion_sense); + + for (i = 0; i < sensorhub->sensor_num; i++) { + sensorhub->params->cmd = MOTIONSENSE_CMD_INFO; + sensorhub->params->info.sensor_num = i; + + retries = CROS_EC_CMD_INFO_RETRIES; + do { + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret == -EBUSY) { + /* The EC is still busy initializing sensors. */ + usleep_range(5000, 6000); + retries--; + } + } while (ret == -EBUSY && retries); + + if (ret < 0) { + dev_err(dev, "no info for EC sensor %d : %d/%d\n", + i, ret, msg->result); + continue; + } + if (retries < CROS_EC_CMD_INFO_RETRIES) { + dev_warn(dev, "%d retries needed to bring up sensor %d\n", + CROS_EC_CMD_INFO_RETRIES - retries, i); + } + + switch (sensorhub->resp->info.type) { + case MOTIONSENSE_TYPE_ACCEL: + name = "cros-ec-accel"; + break; + case MOTIONSENSE_TYPE_BARO: + name = "cros-ec-baro"; + break; + case MOTIONSENSE_TYPE_GYRO: + name = "cros-ec-gyro"; + break; + case MOTIONSENSE_TYPE_MAG: + name = "cros-ec-mag"; + break; + case MOTIONSENSE_TYPE_PROX: + name = "cros-ec-prox"; + break; + case MOTIONSENSE_TYPE_LIGHT: + name = "cros-ec-light"; + break; + case MOTIONSENSE_TYPE_ACTIVITY: + name = "cros-ec-activity"; + break; + default: + dev_warn(dev, "unknown type %d\n", + sensorhub->resp->info.type); + continue; + } + + ret = cros_ec_sensorhub_allocate_sensor(dev, name, i); + if (ret) + return ret; + + sensor_type[sensorhub->resp->info.type]++; + } + + if (sensor_type[MOTIONSENSE_TYPE_ACCEL] >= 2) { + ec->has_kb_wake_angle = true; + if (ec->group && sysfs_update_group(&ec->class_dev.kobj, + ec->group)) + dev_warn(dev, "Unable to update sysfs"); + } + + if (cros_ec_check_features(ec, + EC_FEATURE_REFINED_TABLET_MODE_HYSTERESIS)) { + ret = cros_ec_sensorhub_allocate_sensor(dev, + "cros-ec-lid-angle", + 0); + if (ret) + return ret; + } + + return 0; +} + +static int cros_ec_sensorhub_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cros_ec_dev *ec = dev_get_drvdata(dev->parent); + struct cros_ec_sensorhub *data; + struct cros_ec_command *msg; + int ret, i, sensor_num; + + msg = devm_kzalloc(dev, sizeof(struct cros_ec_command) + + max((u16)sizeof(struct ec_params_motion_sense), + ec->ec_dev->max_response), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->command = EC_CMD_MOTION_SENSE_CMD + ec->cmd_offset; + + data = devm_kzalloc(dev, sizeof(struct cros_ec_sensorhub), GFP_KERNEL); + if (!data) + return -ENOMEM; + + mutex_init(&data->cmd_lock); + + data->dev = dev; + data->ec = ec; + data->msg = msg; + data->params = (struct ec_params_motion_sense *)msg->data; + data->resp = (struct ec_response_motion_sense *)msg->data; + + dev_set_drvdata(dev, data); + + /* Check whether this EC is a sensor hub. */ + if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE)) { + sensor_num = cros_ec_get_sensor_count(ec); + if (sensor_num < 0) { + dev_err(dev, + "Unable to retrieve sensor information (err:%d)\n", + sensor_num); + return sensor_num; + } + if (sensor_num == 0) { + dev_err(dev, "Zero sensors reported.\n"); + return -EINVAL; + } + data->sensor_num = sensor_num; + + /* + * Prepare the ring handler before enumering the + * sensors. + */ + if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE_FIFO)) { + ret = cros_ec_sensorhub_ring_allocate(data); + if (ret) + return ret; + } + + /* Enumerate the sensors.*/ + ret = cros_ec_sensorhub_register(dev, data); + if (ret) + return ret; + + /* + * When the EC does not have a FIFO, the sensors will query + * their data themselves via sysfs or a software trigger. + */ + if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE_FIFO)) { + ret = cros_ec_sensorhub_ring_add(data); + if (ret) + return ret; + /* + * The msg and its data is not under the control of the + * ring handler. + */ + return devm_add_action_or_reset(dev, + cros_ec_sensorhub_ring_remove, + data); + } + + } else { + /* + * If the device has sensors but does not claim to + * be a sensor hub, we are in legacy mode. + */ + data->sensor_num = 2; + for (i = 0; i < data->sensor_num; i++) { + ret = cros_ec_sensorhub_allocate_sensor(dev, + "cros-ec-accel-legacy", i); + if (ret) + return ret; + } + } + + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +/* + * When the EC is suspending, we must stop sending interrupt, + * we may use the same interrupt line for waking up the device. + * Tell the EC to stop sending non-interrupt event on the iio ring. + */ +static int cros_ec_sensorhub_suspend(struct device *dev) +{ + struct cros_ec_sensorhub *sensorhub = dev_get_drvdata(dev); + struct cros_ec_dev *ec = sensorhub->ec; + + if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE_FIFO)) + return cros_ec_sensorhub_ring_fifo_enable(sensorhub, false); + return 0; +} + +static int cros_ec_sensorhub_resume(struct device *dev) +{ + struct cros_ec_sensorhub *sensorhub = dev_get_drvdata(dev); + struct cros_ec_dev *ec = sensorhub->ec; + + if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE_FIFO)) + return cros_ec_sensorhub_ring_fifo_enable(sensorhub, true); + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(cros_ec_sensorhub_pm_ops, + cros_ec_sensorhub_suspend, + cros_ec_sensorhub_resume); + +static const struct platform_device_id cros_ec_sensorhub_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_sensorhub_id); + +static struct platform_driver cros_ec_sensorhub_driver = { + .driver = { + .name = DRV_NAME, + .pm = &cros_ec_sensorhub_pm_ops, + }, + .probe = cros_ec_sensorhub_probe, + .id_table = cros_ec_sensorhub_id, +}; + +module_platform_driver(cros_ec_sensorhub_driver); + +MODULE_AUTHOR("Gwendal Grignou "); +MODULE_DESCRIPTION("ChromeOS EC MEMS Sensor Hub Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_ec_sensorhub_ring.c b/drivers/platform/chrome/cros_ec_sensorhub_ring.c new file mode 100644 index 000000000..d92b60213 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_sensorhub_ring.c @@ -0,0 +1,1107 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for Chrome OS EC Sensor hub FIFO. + * + * Copyright 2020 Google LLC + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CREATE_TRACE_POINTS +#include "cros_ec_sensorhub_trace.h" + +/* Precision of fixed point for the m values from the filter */ +#define M_PRECISION BIT(23) + +/* Only activate the filter once we have at least this many elements. */ +#define TS_HISTORY_THRESHOLD 8 + +/* + * If we don't have any history entries for this long, empty the filter to + * make sure there are no big discontinuities. + */ +#define TS_HISTORY_BORED_US 500000 + +/* To measure by how much the filter is overshooting, if it happens. */ +#define FUTURE_TS_ANALYTICS_COUNT_MAX 100 + +static inline int +cros_sensorhub_send_sample(struct cros_ec_sensorhub *sensorhub, + struct cros_ec_sensors_ring_sample *sample) +{ + cros_ec_sensorhub_push_data_cb_t cb; + int id = sample->sensor_id; + struct iio_dev *indio_dev; + + if (id >= sensorhub->sensor_num) + return -EINVAL; + + cb = sensorhub->push_data[id].push_data_cb; + if (!cb) + return 0; + + indio_dev = sensorhub->push_data[id].indio_dev; + + if (sample->flag & MOTIONSENSE_SENSOR_FLAG_FLUSH) + return 0; + + return cb(indio_dev, sample->vector, sample->timestamp); +} + +/** + * cros_ec_sensorhub_register_push_data() - register the callback to the hub. + * + * @sensorhub : Sensor Hub object + * @sensor_num : The sensor the caller is interested in. + * @indio_dev : The iio device to use when a sample arrives. + * @cb : The callback to call when a sample arrives. + * + * The callback cb will be used by cros_ec_sensorhub_ring to distribute events + * from the EC. + * + * Return: 0 when callback is registered. + * EINVAL is the sensor number is invalid or the slot already used. + */ +int cros_ec_sensorhub_register_push_data(struct cros_ec_sensorhub *sensorhub, + u8 sensor_num, + struct iio_dev *indio_dev, + cros_ec_sensorhub_push_data_cb_t cb) +{ + if (sensor_num >= sensorhub->sensor_num) + return -EINVAL; + if (sensorhub->push_data[sensor_num].indio_dev) + return -EINVAL; + + sensorhub->push_data[sensor_num].indio_dev = indio_dev; + sensorhub->push_data[sensor_num].push_data_cb = cb; + + return 0; +} +EXPORT_SYMBOL_GPL(cros_ec_sensorhub_register_push_data); + +void cros_ec_sensorhub_unregister_push_data(struct cros_ec_sensorhub *sensorhub, + u8 sensor_num) +{ + sensorhub->push_data[sensor_num].indio_dev = NULL; + sensorhub->push_data[sensor_num].push_data_cb = NULL; +} +EXPORT_SYMBOL_GPL(cros_ec_sensorhub_unregister_push_data); + +/** + * cros_ec_sensorhub_ring_fifo_enable() - Enable or disable interrupt generation + * for FIFO events. + * @sensorhub: Sensor Hub object + * @on: true when events are requested. + * + * To be called before sleeping or when no one is listening. + * Return: 0 on success, or an error when we can not communicate with the EC. + * + */ +int cros_ec_sensorhub_ring_fifo_enable(struct cros_ec_sensorhub *sensorhub, + bool on) +{ + int ret, i; + + mutex_lock(&sensorhub->cmd_lock); + if (sensorhub->tight_timestamps) + for (i = 0; i < sensorhub->sensor_num; i++) + sensorhub->batch_state[i].last_len = 0; + + sensorhub->params->cmd = MOTIONSENSE_CMD_FIFO_INT_ENABLE; + sensorhub->params->fifo_int_enable.enable = on; + + sensorhub->msg->outsize = sizeof(struct ec_params_motion_sense); + sensorhub->msg->insize = sizeof(struct ec_response_motion_sense); + + ret = cros_ec_cmd_xfer_status(sensorhub->ec->ec_dev, sensorhub->msg); + mutex_unlock(&sensorhub->cmd_lock); + + /* We expect to receive a payload of 4 bytes, ignore. */ + if (ret > 0) + ret = 0; + /* + * Some platforms (such as Smaug) don't support the FIFO_INT_ENABLE + * command and the interrupt is always enabled. In the case, it + * returns -EINVAL. + * + * N.B: there is no danger of -EINVAL meaning any other invalid + * parameter since fifo_int_enable.enable is a bool and can never + * be in an invalid range. + */ + else if (ret == -EINVAL) + ret = 0; + + return ret; +} + +static void cros_ec_sensor_ring_median_swap(s64 *a, s64 *b) +{ + s64 tmp = *a; + *a = *b; + *b = tmp; +} + +/* + * cros_ec_sensor_ring_median: Gets median of an array of numbers + * + * It's implemented using the quickselect algorithm, which achieves an + * average time complexity of O(n) the middle element. In the worst case, + * the runtime of quickselect could regress to O(n^2). To mitigate this, + * algorithms like median-of-medians exist, which can guarantee O(n) even + * in the worst case. However, these algorithms come with a higher + * overhead and are more complex to implement, making quickselect a + * pragmatic choice for our use case. + * + * Warning: the input array gets modified! + */ +static s64 cros_ec_sensor_ring_median(s64 *array, size_t length) +{ + int lo = 0; + int hi = length - 1; + + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int pivot, i; + + if (array[lo] > array[mid]) + cros_ec_sensor_ring_median_swap(&array[lo], &array[mid]); + if (array[lo] > array[hi]) + cros_ec_sensor_ring_median_swap(&array[lo], &array[hi]); + if (array[mid] < array[hi]) + cros_ec_sensor_ring_median_swap(&array[mid], &array[hi]); + + pivot = array[hi]; + i = lo - 1; + + for (int j = lo; j < hi; j++) + if (array[j] < pivot) + cros_ec_sensor_ring_median_swap(&array[++i], &array[j]); + + /* The pivot's index corresponds to i+1. */ + cros_ec_sensor_ring_median_swap(&array[i + 1], &array[hi]); + if (i + 1 == length / 2) + return array[i + 1]; + if (i + 1 > length / 2) + hi = i; + else + lo = i + 2; + } + + /* Should never reach here. */ + return -1; +} + +/* + * IRQ Timestamp Filtering + * + * Lower down in cros_ec_sensor_ring_process_event(), for each sensor event + * we have to calculate it's timestamp in the AP timebase. There are 3 time + * points: + * a - EC timebase, sensor event + * b - EC timebase, IRQ + * c - AP timebase, IRQ + * a' - what we want: sensor even in AP timebase + * + * While a and b are recorded at accurate times (due to the EC real time + * nature); c is pretty untrustworthy, even though it's recorded the + * first thing in ec_irq_handler(). There is a very good chance we'll get + * added latency due to: + * other irqs + * ddrfreq + * cpuidle + * + * Normally a' = c - b + a, but if we do that naive math any jitter in c + * will get coupled in a', which we don't want. We want a function + * a' = cros_ec_sensor_ring_ts_filter(a) which will filter out outliers in c. + * + * Think of a graph of AP time(b) on the y axis vs EC time(c) on the x axis. + * The slope of the line won't be exactly 1, there will be some clock drift + * between the 2 chips for various reasons (mechanical stress, temperature, + * voltage). We need to extrapolate values for a future x, without trusting + * recent y values too much. + * + * We use a median filter for the slope, then another median filter for the + * y-intercept to calculate this function: + * dx[n] = x[n-1] - x[n] + * dy[n] = x[n-1] - x[n] + * m[n] = dy[n] / dx[n] + * median_m = median(m[n-k:n]) + * error[i] = y[n-i] - median_m * x[n-i] + * median_error = median(error[:k]) + * predicted_y = median_m * x + median_error + * + * Implementation differences from above: + * - Redefined y to be actually c - b, this gives us a lot more precision + * to do the math. (c-b)/b variations are more obvious than c/b variations. + * - Since we don't have floating point, any operations involving slope are + * done using fixed point math (*M_PRECISION) + * - Since x and y grow with time, we keep zeroing the graph (relative to + * the last sample), this way math involving *x[n-i] will not overflow + * - EC timestamps are kept in us, it improves the slope calculation precision + */ + +/** + * cros_ec_sensor_ring_ts_filter_update() - Update filter history. + * + * @state: Filter information. + * @b: IRQ timestamp, EC timebase (us) + * @c: IRQ timestamp, AP timebase (ns) + * + * Given a new IRQ timestamp pair (EC and AP timebases), add it to the filter + * history. + */ +static void +cros_ec_sensor_ring_ts_filter_update(struct cros_ec_sensors_ts_filter_state + *state, + s64 b, s64 c) +{ + s64 x, y; + s64 dx, dy; + s64 m; /* stored as *M_PRECISION */ + s64 *m_history_copy = state->temp_buf; + s64 *error = state->temp_buf; + int i; + + /* we trust b the most, that'll be our independent variable */ + x = b; + /* y is the offset between AP and EC times, in ns */ + y = c - b * 1000; + + dx = (state->x_history[0] + state->x_offset) - x; + if (dx == 0) + return; /* we already have this irq in the history */ + dy = (state->y_history[0] + state->y_offset) - y; + m = div64_s64(dy * M_PRECISION, dx); + + /* Empty filter if we haven't seen any action in a while. */ + if (-dx > TS_HISTORY_BORED_US) + state->history_len = 0; + + /* Move everything over, also update offset to all absolute coords .*/ + for (i = state->history_len - 1; i >= 1; i--) { + state->x_history[i] = state->x_history[i - 1] + dx; + state->y_history[i] = state->y_history[i - 1] + dy; + + state->m_history[i] = state->m_history[i - 1]; + /* + * Also use the same loop to copy m_history for future + * median extraction. + */ + m_history_copy[i] = state->m_history[i - 1]; + } + + /* Store the x and y, but remember offset is actually last sample. */ + state->x_offset = x; + state->y_offset = y; + state->x_history[0] = 0; + state->y_history[0] = 0; + + state->m_history[0] = m; + m_history_copy[0] = m; + + if (state->history_len < CROS_EC_SENSORHUB_TS_HISTORY_SIZE) + state->history_len++; + + /* Precalculate things for the filter. */ + if (state->history_len > TS_HISTORY_THRESHOLD) { + state->median_m = + cros_ec_sensor_ring_median(m_history_copy, + state->history_len - 1); + + /* + * Calculate y-intercepts as if m_median is the slope and + * points in the history are on the line. median_error will + * still be in the offset coordinate system. + */ + for (i = 0; i < state->history_len; i++) + error[i] = state->y_history[i] - + div_s64(state->median_m * state->x_history[i], + M_PRECISION); + state->median_error = + cros_ec_sensor_ring_median(error, state->history_len); + } else { + state->median_m = 0; + state->median_error = 0; + } + trace_cros_ec_sensorhub_filter(state, dx, dy); +} + +/** + * cros_ec_sensor_ring_ts_filter() - Translate EC timebase timestamp to AP + * timebase + * + * @state: filter information. + * @x: any ec timestamp (us): + * + * cros_ec_sensor_ring_ts_filter(a) => a' event timestamp, AP timebase + * cros_ec_sensor_ring_ts_filter(b) => calculated timestamp when the EC IRQ + * should have happened on the AP, with low jitter + * + * Note: The filter will only activate once state->history_len goes + * over TS_HISTORY_THRESHOLD. Otherwise it'll just do the naive c - b + a + * transform. + * + * How to derive the formula, starting from: + * f(x) = median_m * x + median_error + * That's the calculated AP - EC offset (at the x point in time) + * Undo the coordinate system transform: + * f(x) = median_m * (x - x_offset) + median_error + y_offset + * Remember to undo the "y = c - b * 1000" modification: + * f(x) = median_m * (x - x_offset) + median_error + y_offset + x * 1000 + * + * Return: timestamp in AP timebase (ns) + */ +static s64 +cros_ec_sensor_ring_ts_filter(struct cros_ec_sensors_ts_filter_state *state, + s64 x) +{ + return div_s64(state->median_m * (x - state->x_offset), M_PRECISION) + + state->median_error + state->y_offset + x * 1000; +} + +/* + * Since a and b were originally 32 bit values from the EC, + * they overflow relatively often, casting is not enough, so we need to + * add an offset. + */ +static void +cros_ec_sensor_ring_fix_overflow(s64 *ts, + const s64 overflow_period, + struct cros_ec_sensors_ec_overflow_state + *state) +{ + s64 adjust; + + *ts += state->offset; + if (abs(state->last - *ts) > (overflow_period / 2)) { + adjust = state->last > *ts ? overflow_period : -overflow_period; + state->offset += adjust; + *ts += adjust; + } + state->last = *ts; +} + +static void +cros_ec_sensor_ring_check_for_past_timestamp(struct cros_ec_sensorhub + *sensorhub, + struct cros_ec_sensors_ring_sample + *sample) +{ + const u8 sensor_id = sample->sensor_id; + + /* If this event is earlier than one we saw before... */ + if (sensorhub->batch_state[sensor_id].newest_sensor_event > + sample->timestamp) + /* mark it for spreading. */ + sample->timestamp = + sensorhub->batch_state[sensor_id].last_ts; + else + sensorhub->batch_state[sensor_id].newest_sensor_event = + sample->timestamp; +} + +/** + * cros_ec_sensor_ring_process_event() - Process one EC FIFO event + * + * @sensorhub: Sensor Hub object. + * @fifo_info: FIFO information from the EC (includes b point, EC timebase). + * @fifo_timestamp: EC IRQ, kernel timebase (aka c). + * @current_timestamp: calculated event timestamp, kernel timebase (aka a'). + * @in: incoming FIFO event from EC (includes a point, EC timebase). + * @out: outgoing event to user space (includes a'). + * + * Process one EC event, add it in the ring if necessary. + * + * Return: true if out event has been populated. + */ +static bool +cros_ec_sensor_ring_process_event(struct cros_ec_sensorhub *sensorhub, + const struct ec_response_motion_sense_fifo_info + *fifo_info, + const ktime_t fifo_timestamp, + ktime_t *current_timestamp, + struct ec_response_motion_sensor_data *in, + struct cros_ec_sensors_ring_sample *out) +{ + const s64 now = cros_ec_get_time_ns(); + int axis, async_flags; + + /* Do not populate the filter based on asynchronous events. */ + async_flags = in->flags & + (MOTIONSENSE_SENSOR_FLAG_ODR | MOTIONSENSE_SENSOR_FLAG_FLUSH); + + if (in->flags & MOTIONSENSE_SENSOR_FLAG_TIMESTAMP && !async_flags) { + s64 a = in->timestamp; + s64 b = fifo_info->timestamp; + s64 c = fifo_timestamp; + + cros_ec_sensor_ring_fix_overflow(&a, 1LL << 32, + &sensorhub->overflow_a); + cros_ec_sensor_ring_fix_overflow(&b, 1LL << 32, + &sensorhub->overflow_b); + + if (sensorhub->tight_timestamps) { + cros_ec_sensor_ring_ts_filter_update( + &sensorhub->filter, b, c); + *current_timestamp = cros_ec_sensor_ring_ts_filter( + &sensorhub->filter, a); + } else { + s64 new_timestamp; + + /* + * Disable filtering since we might add more jitter + * if b is in a random point in time. + */ + new_timestamp = c - b * 1000 + a * 1000; + /* + * The timestamp can be stale if we had to use the fifo + * info timestamp. + */ + if (new_timestamp - *current_timestamp > 0) + *current_timestamp = new_timestamp; + } + trace_cros_ec_sensorhub_timestamp(in->timestamp, + fifo_info->timestamp, + fifo_timestamp, + *current_timestamp, + now); + + /* + * A standalone timestamp event typically has a sensor_num of + * 0xff. Return early here to prevent it from hitting the + * bounds check below and spamming the logs. + */ + return false; + } + + /* Skip event if sensor_num from EC is out of bounds. */ + if (in->sensor_num >= sensorhub->sensor_num) { + dev_warn_ratelimited(sensorhub->dev, + "Invalid sensor number %u from EC\n", + in->sensor_num); + return false; + } + + if (in->flags & MOTIONSENSE_SENSOR_FLAG_ODR) { + if (sensorhub->tight_timestamps) { + sensorhub->batch_state[in->sensor_num].last_len = 0; + sensorhub->batch_state[in->sensor_num].penul_len = 0; + } + /* + * ODR change is only useful for the sensor_ring, it does not + * convey information to clients. + */ + return false; + } + + if (in->flags & MOTIONSENSE_SENSOR_FLAG_FLUSH) { + out->sensor_id = in->sensor_num; + out->timestamp = *current_timestamp; + out->flag = in->flags; + if (sensorhub->tight_timestamps) + sensorhub->batch_state[out->sensor_id].last_len = 0; + /* + * No other payload information provided with + * flush ack. + */ + return true; + } + + /* Regular sample */ + out->sensor_id = in->sensor_num; + trace_cros_ec_sensorhub_data(in->sensor_num, + fifo_info->timestamp, + fifo_timestamp, + *current_timestamp, + now); + + if (*current_timestamp - now > 0) { + /* + * This fix is needed to overcome the timestamp filter putting + * events in the future. + */ + sensorhub->future_timestamp_total_ns += + *current_timestamp - now; + if (++sensorhub->future_timestamp_count == + FUTURE_TS_ANALYTICS_COUNT_MAX) { + s64 avg = div_s64(sensorhub->future_timestamp_total_ns, + sensorhub->future_timestamp_count); + dev_warn_ratelimited(sensorhub->dev, + "100 timestamps in the future, %lldns shaved on average\n", + avg); + sensorhub->future_timestamp_count = 0; + sensorhub->future_timestamp_total_ns = 0; + } + out->timestamp = now; + } else { + out->timestamp = *current_timestamp; + } + + out->flag = in->flags; + for (axis = 0; axis < 3; axis++) + out->vector[axis] = in->data[axis]; + + if (sensorhub->tight_timestamps) + cros_ec_sensor_ring_check_for_past_timestamp(sensorhub, out); + return true; +} + +/* + * cros_ec_sensor_ring_spread_add: Calculate proper timestamps then add to + * ringbuffer. + * + * This is the new spreading code, assumes every sample's timestamp + * precedes the sample. Run if tight_timestamps == true. + * + * Sometimes the EC receives only one interrupt (hence timestamp) for + * a batch of samples. Only the first sample will have the correct + * timestamp. So we must interpolate the other samples. + * We use the previous batch timestamp and our current batch timestamp + * as a way to calculate period, then spread the samples evenly. + * + * s0 int, 0ms + * s1 int, 10ms + * s2 int, 20ms + * 30ms point goes by, no interrupt, previous one is still asserted + * downloading s2 and s3 + * s3 sample, 20ms (incorrect timestamp) + * s4 int, 40ms + * + * The batches are [(s0), (s1), (s2, s3), (s4)]. Since the 3rd batch + * has 2 samples in them, we adjust the timestamp of s3. + * s2 - s1 = 10ms, so s3 must be s2 + 10ms => 20ms. If s1 would have + * been part of a bigger batch things would have gotten a little + * more complicated. + * + * Note: we also assume another sensor sample doesn't break up a batch + * in 2 or more partitions. Example, there can't ever be a sync sensor + * in between S2 and S3. This simplifies the following code. + */ +static void +cros_ec_sensor_ring_spread_add(struct cros_ec_sensorhub *sensorhub, + unsigned long sensor_mask, + struct cros_ec_sensors_ring_sample *last_out) +{ + struct cros_ec_sensors_ring_sample *batch_start, *next_batch_start; + int id; + + for_each_set_bit(id, &sensor_mask, sensorhub->sensor_num) { + for (batch_start = sensorhub->ring; batch_start < last_out; + batch_start = next_batch_start) { + /* + * For each batch (where all samples have the same + * timestamp). + */ + int batch_len, sample_idx; + struct cros_ec_sensors_ring_sample *batch_end = + batch_start; + struct cros_ec_sensors_ring_sample *s; + s64 batch_timestamp = batch_start->timestamp; + s64 sample_period; + + /* + * Skip over batches that start with the sensor types + * we're not looking at right now. + */ + if (batch_start->sensor_id != id) { + next_batch_start = batch_start + 1; + continue; + } + + /* + * Do not start a batch + * from a flush, as it happens asynchronously to the + * regular flow of events. + */ + if (batch_start->flag & MOTIONSENSE_SENSOR_FLAG_FLUSH) { + cros_sensorhub_send_sample(sensorhub, + batch_start); + next_batch_start = batch_start + 1; + continue; + } + + if (batch_start->timestamp <= + sensorhub->batch_state[id].last_ts) { + batch_timestamp = + sensorhub->batch_state[id].last_ts; + batch_len = sensorhub->batch_state[id].last_len; + + sample_idx = batch_len; + + sensorhub->batch_state[id].last_ts = + sensorhub->batch_state[id].penul_ts; + sensorhub->batch_state[id].last_len = + sensorhub->batch_state[id].penul_len; + } else { + /* + * Push first sample in the batch to the, + * kfifo, it's guaranteed to be correct, the + * rest will follow later on. + */ + sample_idx = 1; + batch_len = 1; + cros_sensorhub_send_sample(sensorhub, + batch_start); + batch_start++; + } + + /* Find all samples have the same timestamp. */ + for (s = batch_start; s < last_out; s++) { + if (s->sensor_id != id) + /* + * Skip over other sensor types that + * are interleaved, don't count them. + */ + continue; + if (s->timestamp != batch_timestamp) + /* we discovered the next batch */ + break; + if (s->flag & MOTIONSENSE_SENSOR_FLAG_FLUSH) + /* break on flush packets */ + break; + batch_end = s; + batch_len++; + } + + if (batch_len == 1) + goto done_with_this_batch; + + /* Can we calculate period? */ + if (sensorhub->batch_state[id].last_len == 0) { + dev_warn(sensorhub->dev, "Sensor %d: lost %d samples when spreading\n", + id, batch_len - 1); + goto done_with_this_batch; + /* + * Note: we're dropping the rest of the samples + * in this batch since we have no idea where + * they're supposed to go without a period + * calculation. + */ + } + + sample_period = div_s64(batch_timestamp - + sensorhub->batch_state[id].last_ts, + sensorhub->batch_state[id].last_len); + dev_dbg(sensorhub->dev, + "Adjusting %d samples, sensor %d last_batch @%lld (%d samples) batch_timestamp=%lld => period=%lld\n", + batch_len, id, + sensorhub->batch_state[id].last_ts, + sensorhub->batch_state[id].last_len, + batch_timestamp, + sample_period); + + /* + * Adjust timestamps of the samples then push them to + * kfifo. + */ + for (s = batch_start; s <= batch_end; s++) { + if (s->sensor_id != id) + /* + * Skip over other sensor types that + * are interleaved, don't change them. + */ + continue; + + s->timestamp = batch_timestamp + + sample_period * sample_idx; + sample_idx++; + + cros_sensorhub_send_sample(sensorhub, s); + } + +done_with_this_batch: + sensorhub->batch_state[id].penul_ts = + sensorhub->batch_state[id].last_ts; + sensorhub->batch_state[id].penul_len = + sensorhub->batch_state[id].last_len; + + sensorhub->batch_state[id].last_ts = + batch_timestamp; + sensorhub->batch_state[id].last_len = batch_len; + + next_batch_start = batch_end + 1; + } + } +} + +/* + * cros_ec_sensor_ring_spread_add_legacy: Calculate proper timestamps then + * add to ringbuffer (legacy). + * + * Note: This assumes we're running old firmware, where timestamp + * is inserted after its sample(s)e. There can be several samples between + * timestamps, so several samples can have the same timestamp. + * + * timestamp | count + * ----------------- + * 1st sample --> TS1 | 1 + * TS2 | 2 + * TS2 | 3 + * TS3 | 4 + * last_out --> + * + * + * We spread time for the samples using period p = (current - TS1)/4. + * between TS1 and TS2: [TS1+p/4, TS1+2p/4, TS1+3p/4, current_timestamp]. + * + */ +static void +cros_ec_sensor_ring_spread_add_legacy(struct cros_ec_sensorhub *sensorhub, + unsigned long sensor_mask, + s64 current_timestamp, + struct cros_ec_sensors_ring_sample + *last_out) +{ + struct cros_ec_sensors_ring_sample *out; + int i; + + for_each_set_bit(i, &sensor_mask, sensorhub->sensor_num) { + s64 timestamp; + int count = 0; + s64 time_period; + + for (out = sensorhub->ring; out < last_out; out++) { + if (out->sensor_id != i) + continue; + + /* Timestamp to start with */ + timestamp = out->timestamp; + out++; + count = 1; + break; + } + for (; out < last_out; out++) { + /* Find last sample. */ + if (out->sensor_id != i) + continue; + count++; + } + if (count == 0) + continue; + + /* Spread uniformly between the first and last samples. */ + time_period = div_s64(current_timestamp - timestamp, count); + + for (out = sensorhub->ring; out < last_out; out++) { + if (out->sensor_id != i) + continue; + timestamp += time_period; + out->timestamp = timestamp; + } + } + + /* Push the event into the kfifo */ + for (out = sensorhub->ring; out < last_out; out++) + cros_sensorhub_send_sample(sensorhub, out); +} + +/** + * cros_ec_sensorhub_ring_handler() - The trigger handler function + * + * @sensorhub: Sensor Hub object. + * + * Called by the notifier, process the EC sensor FIFO queue. + */ +static void cros_ec_sensorhub_ring_handler(struct cros_ec_sensorhub *sensorhub) +{ + struct ec_response_motion_sense_fifo_info *fifo_info = + sensorhub->fifo_info; + struct cros_ec_dev *ec = sensorhub->ec; + ktime_t fifo_timestamp, current_timestamp; + int i, j, number_data, ret; + unsigned long sensor_mask = 0; + struct ec_response_motion_sensor_data *in; + struct cros_ec_sensors_ring_sample *out, *last_out; + + mutex_lock(&sensorhub->cmd_lock); + + /* Get FIFO information if there are lost vectors. */ + if (fifo_info->total_lost) { + int fifo_info_length = + sizeof(struct ec_response_motion_sense_fifo_info) + + sizeof(u16) * sensorhub->sensor_num; + + /* Need to retrieve the number of lost vectors per sensor */ + sensorhub->params->cmd = MOTIONSENSE_CMD_FIFO_INFO; + sensorhub->msg->outsize = 1; + sensorhub->msg->insize = fifo_info_length; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, sensorhub->msg); + if (ret < 0) + goto error; + if (ret != fifo_info_length) { + dev_warn_ratelimited(sensorhub->dev, + "Mismatch read length: size %d - expected %d\n", + ret, fifo_info_length); + goto error; + } + + memcpy(fifo_info, &sensorhub->resp->fifo_info, + fifo_info_length); + + /* + * Update collection time, will not be as precise as the + * non-error case. + */ + fifo_timestamp = cros_ec_get_time_ns(); + } else { + fifo_timestamp = sensorhub->fifo_timestamp[ + CROS_EC_SENSOR_NEW_TS]; + } + + if (fifo_info->count > sensorhub->fifo_size || + fifo_info->size != sensorhub->fifo_size) { + dev_warn(sensorhub->dev, + "Mismatch EC data: count %d, size %d - expected %d\n", + fifo_info->count, fifo_info->size, + sensorhub->fifo_size); + goto error; + } + + /* Copy elements in the main fifo */ + current_timestamp = sensorhub->fifo_timestamp[CROS_EC_SENSOR_LAST_TS]; + out = sensorhub->ring; + for (i = 0; i < fifo_info->count; i += number_data) { + sensorhub->params->cmd = MOTIONSENSE_CMD_FIFO_READ; + sensorhub->params->fifo_read.max_data_vector = + fifo_info->count - i; + sensorhub->msg->outsize = + sizeof(struct ec_params_motion_sense); + sensorhub->msg->insize = + sizeof(sensorhub->resp->fifo_read) + + sensorhub->params->fifo_read.max_data_vector * + sizeof(struct ec_response_motion_sensor_data); + ret = cros_ec_cmd_xfer_status(ec->ec_dev, sensorhub->msg); + if (ret < 0) { + dev_warn(sensorhub->dev, "Fifo error: %d\n", ret); + break; + } + number_data = sensorhub->resp->fifo_read.number_data; + if (number_data == 0) { + dev_dbg(sensorhub->dev, "Unexpected empty FIFO\n"); + break; + } + if (number_data > fifo_info->count - i) { + dev_warn(sensorhub->dev, + "Invalid EC data: too many entry received: %d, expected %d\n", + number_data, fifo_info->count - i); + break; + } + if (out + number_data > + sensorhub->ring + fifo_info->count) { + dev_warn(sensorhub->dev, + "Too many samples: %d (%zd data) to %d entries for expected %d entries\n", + i, out - sensorhub->ring, i + number_data, + fifo_info->count); + break; + } + + for (in = sensorhub->resp->fifo_read.data, j = 0; + j < number_data; j++, in++) { + if (cros_ec_sensor_ring_process_event( + sensorhub, fifo_info, + fifo_timestamp, + ¤t_timestamp, + in, out)) { + sensor_mask |= BIT(in->sensor_num); + out++; + } + } + } + mutex_unlock(&sensorhub->cmd_lock); + last_out = out; + + if (out == sensorhub->ring) + /* Unexpected empty FIFO. */ + goto ring_handler_end; + + /* + * Check if current_timestamp is ahead of the last sample. Normally, + * the EC appends a timestamp after the last sample, but if the AP + * is slow to respond to the IRQ, the EC may have added new samples. + * Use the FIFO info timestamp as last timestamp then. + */ + if (!sensorhub->tight_timestamps && + (last_out - 1)->timestamp == current_timestamp) + current_timestamp = fifo_timestamp; + + /* Warn on lost samples. */ + if (fifo_info->total_lost) + for (i = 0; i < sensorhub->sensor_num; i++) { + if (fifo_info->lost[i]) { + dev_warn_ratelimited(sensorhub->dev, + "Sensor %d: lost: %d out of %d\n", + i, fifo_info->lost[i], + fifo_info->total_lost); + if (sensorhub->tight_timestamps) + sensorhub->batch_state[i].last_len = 0; + } + } + + /* + * Spread samples in case of batching, then add them to the + * ringbuffer. + */ + if (sensorhub->tight_timestamps) + cros_ec_sensor_ring_spread_add(sensorhub, sensor_mask, + last_out); + else + cros_ec_sensor_ring_spread_add_legacy(sensorhub, sensor_mask, + current_timestamp, + last_out); + +ring_handler_end: + sensorhub->fifo_timestamp[CROS_EC_SENSOR_LAST_TS] = current_timestamp; + return; + +error: + mutex_unlock(&sensorhub->cmd_lock); +} + +static int cros_ec_sensorhub_event(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *_notify) +{ + struct cros_ec_sensorhub *sensorhub; + struct cros_ec_device *ec_dev; + + sensorhub = container_of(nb, struct cros_ec_sensorhub, notifier); + ec_dev = sensorhub->ec->ec_dev; + + if (ec_dev->event_data.event_type != EC_MKBP_EVENT_SENSOR_FIFO) + return NOTIFY_DONE; + + if (ec_dev->event_size != sizeof(ec_dev->event_data.data.sensor_fifo)) { + dev_warn(ec_dev->dev, "Invalid fifo info size\n"); + return NOTIFY_DONE; + } + + if (queued_during_suspend) + return NOTIFY_OK; + + memcpy(sensorhub->fifo_info, &ec_dev->event_data.data.sensor_fifo.info, + sizeof(*sensorhub->fifo_info)); + sensorhub->fifo_timestamp[CROS_EC_SENSOR_NEW_TS] = + ec_dev->last_event_time; + cros_ec_sensorhub_ring_handler(sensorhub); + + return NOTIFY_OK; +} + +/** + * cros_ec_sensorhub_ring_allocate() - Prepare the FIFO functionality if the EC + * supports it. + * + * @sensorhub : Sensor Hub object. + * + * Return: 0 on success. + */ +int cros_ec_sensorhub_ring_allocate(struct cros_ec_sensorhub *sensorhub) +{ + int fifo_info_length = + sizeof(struct ec_response_motion_sense_fifo_info) + + sizeof(u16) * sensorhub->sensor_num; + + /* Allocate the array for lost events. */ + sensorhub->fifo_info = devm_kzalloc(sensorhub->dev, fifo_info_length, + GFP_KERNEL); + if (!sensorhub->fifo_info) + return -ENOMEM; + + /* + * Allocate the callback area based on the number of sensors. + * Add one for the sensor ring. + */ + sensorhub->push_data = devm_kcalloc(sensorhub->dev, + sensorhub->sensor_num, + sizeof(*sensorhub->push_data), + GFP_KERNEL); + if (!sensorhub->push_data) + return -ENOMEM; + + sensorhub->tight_timestamps = cros_ec_check_features( + sensorhub->ec, + EC_FEATURE_MOTION_SENSE_TIGHT_TIMESTAMPS); + + if (sensorhub->tight_timestamps) { + sensorhub->batch_state = devm_kcalloc(sensorhub->dev, + sensorhub->sensor_num, + sizeof(*sensorhub->batch_state), + GFP_KERNEL); + if (!sensorhub->batch_state) + return -ENOMEM; + } + + return 0; +} + +/** + * cros_ec_sensorhub_ring_add() - Add the FIFO functionality if the EC + * supports it. + * + * @sensorhub : Sensor Hub object. + * + * Return: 0 on success. + */ +int cros_ec_sensorhub_ring_add(struct cros_ec_sensorhub *sensorhub) +{ + struct cros_ec_dev *ec = sensorhub->ec; + int ret; + int fifo_info_length = + sizeof(struct ec_response_motion_sense_fifo_info) + + sizeof(u16) * sensorhub->sensor_num; + + /* Retrieve FIFO information */ + sensorhub->msg->version = 2; + sensorhub->params->cmd = MOTIONSENSE_CMD_FIFO_INFO; + sensorhub->msg->outsize = 1; + sensorhub->msg->insize = fifo_info_length; + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, sensorhub->msg); + if (ret < 0) + return ret; + + /* + * Allocate the full fifo. We need to copy the whole FIFO to set + * timestamps properly. + */ + sensorhub->fifo_size = sensorhub->resp->fifo_info.size; + sensorhub->ring = devm_kcalloc(sensorhub->dev, sensorhub->fifo_size, + sizeof(*sensorhub->ring), GFP_KERNEL); + if (!sensorhub->ring) + return -ENOMEM; + + sensorhub->fifo_timestamp[CROS_EC_SENSOR_LAST_TS] = + cros_ec_get_time_ns(); + + /* Register the notifier that will act as a top half interrupt. */ + sensorhub->notifier.notifier_call = cros_ec_sensorhub_event; + ret = blocking_notifier_chain_register(&ec->ec_dev->event_notifier, + &sensorhub->notifier); + if (ret < 0) + return ret; + + /* Start collection samples. */ + return cros_ec_sensorhub_ring_fifo_enable(sensorhub, true); +} + +void cros_ec_sensorhub_ring_remove(void *arg) +{ + struct cros_ec_sensorhub *sensorhub = arg; + struct cros_ec_device *ec_dev = sensorhub->ec->ec_dev; + + /* Disable the ring, prevent EC interrupt to the AP for nothing. */ + cros_ec_sensorhub_ring_fifo_enable(sensorhub, false); + blocking_notifier_chain_unregister(&ec_dev->event_notifier, + &sensorhub->notifier); +} diff --git a/drivers/platform/chrome/cros_ec_sensorhub_trace.h b/drivers/platform/chrome/cros_ec_sensorhub_trace.h new file mode 100644 index 000000000..57d9b4785 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_sensorhub_trace.h @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Trace events for the ChromeOS Sensorhub kernel module + * + * Copyright 2021 Google LLC. + */ + +#undef TRACE_SYSTEM +#define TRACE_SYSTEM cros_ec + +#if !defined(_CROS_EC_SENSORHUB_TRACE_H_) || defined(TRACE_HEADER_MULTI_READ) +#define _CROS_EC_SENSORHUB_TRACE_H_ + +#include +#include + +#include + +TRACE_EVENT(cros_ec_sensorhub_timestamp, + TP_PROTO(u32 ec_sample_timestamp, u32 ec_fifo_timestamp, s64 fifo_timestamp, + s64 current_timestamp, s64 current_time), + TP_ARGS(ec_sample_timestamp, ec_fifo_timestamp, fifo_timestamp, current_timestamp, + current_time), + TP_STRUCT__entry( + __field(u32, ec_sample_timestamp) + __field(u32, ec_fifo_timestamp) + __field(s64, fifo_timestamp) + __field(s64, current_timestamp) + __field(s64, current_time) + __field(s64, delta) + ), + TP_fast_assign( + __entry->ec_sample_timestamp = ec_sample_timestamp; + __entry->ec_fifo_timestamp = ec_fifo_timestamp; + __entry->fifo_timestamp = fifo_timestamp; + __entry->current_timestamp = current_timestamp; + __entry->current_time = current_time; + __entry->delta = current_timestamp - current_time; + ), + TP_printk("ec_ts: %9u, ec_fifo_ts: %9u, fifo_ts: %12lld, curr_ts: %12lld, curr_time: %12lld, delta %12lld", + __entry->ec_sample_timestamp, + __entry->ec_fifo_timestamp, + __entry->fifo_timestamp, + __entry->current_timestamp, + __entry->current_time, + __entry->delta + ) +); + +TRACE_EVENT(cros_ec_sensorhub_data, + TP_PROTO(u32 ec_sensor_num, u32 ec_fifo_timestamp, s64 fifo_timestamp, + s64 current_timestamp, s64 current_time), + TP_ARGS(ec_sensor_num, ec_fifo_timestamp, fifo_timestamp, current_timestamp, current_time), + TP_STRUCT__entry( + __field(u32, ec_sensor_num) + __field(u32, ec_fifo_timestamp) + __field(s64, fifo_timestamp) + __field(s64, current_timestamp) + __field(s64, current_time) + __field(s64, delta) + ), + TP_fast_assign( + __entry->ec_sensor_num = ec_sensor_num; + __entry->ec_fifo_timestamp = ec_fifo_timestamp; + __entry->fifo_timestamp = fifo_timestamp; + __entry->current_timestamp = current_timestamp; + __entry->current_time = current_time; + __entry->delta = current_timestamp - current_time; + ), + TP_printk("ec_num: %4u, ec_fifo_ts: %9u, fifo_ts: %12lld, curr_ts: %12lld, curr_time: %12lld, delta %12lld", + __entry->ec_sensor_num, + __entry->ec_fifo_timestamp, + __entry->fifo_timestamp, + __entry->current_timestamp, + __entry->current_time, + __entry->delta + ) +); + +TRACE_EVENT(cros_ec_sensorhub_filter, + TP_PROTO(struct cros_ec_sensors_ts_filter_state *state, s64 dx, s64 dy), + TP_ARGS(state, dx, dy), + TP_STRUCT__entry( + __field(s64, dx) + __field(s64, dy) + __field(s64, median_m) + __field(s64, median_error) + __field(s64, history_len) + __field(s64, x) + __field(s64, y) + ), + TP_fast_assign( + __entry->dx = dx; + __entry->dy = dy; + __entry->median_m = state->median_m; + __entry->median_error = state->median_error; + __entry->history_len = state->history_len; + __entry->x = state->x_offset; + __entry->y = state->y_offset; + ), + TP_printk("dx: %12lld. dy: %12lld median_m: %12lld median_error: %12lld len: %lld x: %12lld y: %12lld", + __entry->dx, + __entry->dy, + __entry->median_m, + __entry->median_error, + __entry->history_len, + __entry->x, + __entry->y + ) +); + + +#endif /* _CROS_EC_SENSORHUB_TRACE_H_ */ + +/* this part must be outside header guard */ + +#undef TRACE_INCLUDE_PATH +#define TRACE_INCLUDE_PATH . + +#undef TRACE_INCLUDE_FILE +#define TRACE_INCLUDE_FILE cros_ec_sensorhub_trace + +#include diff --git a/drivers/platform/chrome/cros_ec_spi.c b/drivers/platform/chrome/cros_ec_spi.c new file mode 100644 index 000000000..28fa82f8c --- /dev/null +++ b/drivers/platform/chrome/cros_ec_spi.c @@ -0,0 +1,836 @@ +// SPDX-License-Identifier: GPL-2.0 +// SPI interface for ChromeOS Embedded Controller +// +// Copyright (C) 2012 Google, Inc + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +/* The header byte, which follows the preamble */ +#define EC_MSG_HEADER 0xec + +/* + * Number of EC preamble bytes we read at a time. Since it takes + * about 400-500us for the EC to respond there is not a lot of + * point in tuning this. If the EC could respond faster then + * we could increase this so that might expect the preamble and + * message to occur in a single transaction. However, the maximum + * SPI transfer size is 256 bytes, so at 5MHz we need a response + * time of perhaps <320us (200 bytes / 1600 bits). + */ +#define EC_MSG_PREAMBLE_COUNT 32 + +/* + * Allow for a long time for the EC to respond. We support i2c + * tunneling and support fairly long messages for the tunnel (249 + * bytes long at the moment). If we're talking to a 100 kHz device + * on the other end and need to transfer ~256 bytes, then we need: + * 10 us/bit * ~10 bits/byte * ~256 bytes = ~25ms + * + * We'll wait 8 times that to handle clock stretching and other + * paranoia. Note that some battery gas gauge ICs claim to have a + * clock stretch of 144ms in rare situations. That's incentive for + * not directly passing i2c through, but it's too late for that for + * existing hardware. + * + * It's pretty unlikely that we'll really see a 249 byte tunnel in + * anything other than testing. If this was more common we might + * consider having slow commands like this require a GET_STATUS + * wait loop. The 'flash write' command would be another candidate + * for this, clocking in at 2-3ms. + */ +#define EC_MSG_DEADLINE_MS 200 + +/* + * Time between raising the SPI chip select (for the end of a + * transaction) and dropping it again (for the next transaction). + * If we go too fast, the EC will miss the transaction. We know that we + * need at least 70 us with the 16 MHz STM32 EC, so go with 200 us to be + * safe. + */ +#define EC_SPI_RECOVERY_TIME_NS (200 * 1000) + +/** + * struct cros_ec_spi - information about a SPI-connected EC + * + * @spi: SPI device we are connected to + * @last_transfer_ns: time that we last finished a transfer. + * @start_of_msg_delay: used to set the delay_usecs on the spi_transfer that + * is sent when we want to turn on CS at the start of a transaction. + * @end_of_msg_delay: used to set the delay_usecs on the spi_transfer that + * is sent when we want to turn off CS at the end of a transaction. + * @high_pri_worker: Used to schedule high priority work. + */ +struct cros_ec_spi { + struct spi_device *spi; + s64 last_transfer_ns; + unsigned int start_of_msg_delay; + unsigned int end_of_msg_delay; + struct kthread_worker *high_pri_worker; +}; + +typedef int (*cros_ec_xfer_fn_t) (struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg); + +/** + * struct cros_ec_xfer_work_params - params for our high priority workers + * + * @work: The work_struct needed to queue work + * @fn: The function to use to transfer + * @ec_dev: ChromeOS EC device + * @ec_msg: Message to transfer + * @ret: The return value of the function + */ + +struct cros_ec_xfer_work_params { + struct kthread_work work; + cros_ec_xfer_fn_t fn; + struct cros_ec_device *ec_dev; + struct cros_ec_command *ec_msg; + int ret; +}; + +static void debug_packet(struct device *dev, const char *name, u8 *ptr, + int len) +{ +#ifdef DEBUG + dev_dbg(dev, "%s: %*ph\n", name, len, ptr); +#endif +} + +static int terminate_request(struct cros_ec_device *ec_dev) +{ + struct cros_ec_spi *ec_spi = ec_dev->priv; + struct spi_message msg; + struct spi_transfer trans; + int ret; + + /* + * Turn off CS, possibly adding a delay to ensure the rising edge + * doesn't come too soon after the end of the data. + */ + spi_message_init(&msg); + memset(&trans, 0, sizeof(trans)); + trans.delay.value = ec_spi->end_of_msg_delay; + trans.delay.unit = SPI_DELAY_UNIT_USECS; + spi_message_add_tail(&trans, &msg); + + ret = spi_sync_locked(ec_spi->spi, &msg); + + /* Reset end-of-response timer */ + ec_spi->last_transfer_ns = ktime_get_ns(); + if (ret < 0) { + dev_err(ec_dev->dev, + "cs-deassert spi transfer failed: %d\n", + ret); + } + + return ret; +} + +/** + * receive_n_bytes - receive n bytes from the EC. + * + * Assumes buf is a pointer into the ec_dev->din buffer + * + * @ec_dev: ChromeOS EC device. + * @buf: Pointer to the buffer receiving the data. + * @n: Number of bytes received. + */ +static int receive_n_bytes(struct cros_ec_device *ec_dev, u8 *buf, int n) +{ + struct cros_ec_spi *ec_spi = ec_dev->priv; + struct spi_transfer trans; + struct spi_message msg; + int ret; + + if (buf - ec_dev->din + n > ec_dev->din_size) + return -EINVAL; + + memset(&trans, 0, sizeof(trans)); + trans.cs_change = 1; + trans.rx_buf = buf; + trans.len = n; + + spi_message_init(&msg); + spi_message_add_tail(&trans, &msg); + ret = spi_sync_locked(ec_spi->spi, &msg); + if (ret < 0) + dev_err(ec_dev->dev, "spi transfer failed: %d\n", ret); + + return ret; +} + +/** + * cros_ec_spi_receive_packet - Receive a packet from the EC. + * + * This function has two phases: reading the preamble bytes (since if we read + * data from the EC before it is ready to send, we just get preamble) and + * reading the actual message. + * + * The received data is placed into ec_dev->din. + * + * @ec_dev: ChromeOS EC device + * @need_len: Number of message bytes we need to read + */ +static int cros_ec_spi_receive_packet(struct cros_ec_device *ec_dev, + int need_len) +{ + struct ec_host_response *response; + u8 *ptr, *end; + int ret; + unsigned long deadline; + int todo; + + if (ec_dev->din_size < EC_MSG_PREAMBLE_COUNT) + return -EINVAL; + + /* Receive data until we see the header byte */ + deadline = jiffies + msecs_to_jiffies(EC_MSG_DEADLINE_MS); + while (true) { + unsigned long start_jiffies = jiffies; + + ret = receive_n_bytes(ec_dev, + ec_dev->din, + EC_MSG_PREAMBLE_COUNT); + if (ret < 0) + return ret; + + ptr = ec_dev->din; + for (end = ptr + EC_MSG_PREAMBLE_COUNT; ptr != end; ptr++) { + if (*ptr == EC_SPI_FRAME_START) { + dev_dbg(ec_dev->dev, "msg found at %zd\n", + ptr - ec_dev->din); + break; + } + } + if (ptr != end) + break; + + /* + * Use the time at the start of the loop as a timeout. This + * gives us one last shot at getting the transfer and is useful + * in case we got context switched out for a while. + */ + if (time_after(start_jiffies, deadline)) { + dev_warn(ec_dev->dev, "EC failed to respond in time\n"); + return -ETIMEDOUT; + } + } + + /* + * ptr now points to the header byte. Copy any valid data to the + * start of our buffer + */ + todo = end - ++ptr; + todo = min(todo, need_len); + memmove(ec_dev->din, ptr, todo); + ptr = ec_dev->din + todo; + dev_dbg(ec_dev->dev, "need %d, got %d bytes from preamble\n", + need_len, todo); + need_len -= todo; + + /* If the entire response struct wasn't read, get the rest of it. */ + if (todo < sizeof(*response)) { + ret = receive_n_bytes(ec_dev, ptr, sizeof(*response) - todo); + if (ret < 0) + return -EBADMSG; + ptr += (sizeof(*response) - todo); + todo = sizeof(*response); + } + + response = (struct ec_host_response *)ec_dev->din; + + /* Abort if data_len is too large. */ + if (response->data_len > ec_dev->din_size) + return -EMSGSIZE; + + /* Receive data until we have it all */ + while (need_len > 0) { + /* + * We can't support transfers larger than the SPI FIFO size + * unless we have DMA. We don't have DMA on the ISP SPI ports + * for Exynos. We need a way of asking SPI driver for + * maximum-supported transfer size. + */ + todo = min(need_len, 256); + dev_dbg(ec_dev->dev, "loop, todo=%d, need_len=%d, ptr=%zd\n", + todo, need_len, ptr - ec_dev->din); + + ret = receive_n_bytes(ec_dev, ptr, todo); + if (ret < 0) + return ret; + + ptr += todo; + need_len -= todo; + } + + dev_dbg(ec_dev->dev, "loop done, ptr=%zd\n", ptr - ec_dev->din); + + return 0; +} + +/** + * cros_ec_spi_receive_response - Receive a response from the EC. + * + * This function has two phases: reading the preamble bytes (since if we read + * data from the EC before it is ready to send, we just get preamble) and + * reading the actual message. + * + * The received data is placed into ec_dev->din. + * + * @ec_dev: ChromeOS EC device + * @need_len: Number of message bytes we need to read + */ +static int cros_ec_spi_receive_response(struct cros_ec_device *ec_dev, + int need_len) +{ + u8 *ptr, *end; + int ret; + unsigned long deadline; + int todo; + + if (ec_dev->din_size < EC_MSG_PREAMBLE_COUNT) + return -EINVAL; + + /* Receive data until we see the header byte */ + deadline = jiffies + msecs_to_jiffies(EC_MSG_DEADLINE_MS); + while (true) { + unsigned long start_jiffies = jiffies; + + ret = receive_n_bytes(ec_dev, + ec_dev->din, + EC_MSG_PREAMBLE_COUNT); + if (ret < 0) + return ret; + + ptr = ec_dev->din; + for (end = ptr + EC_MSG_PREAMBLE_COUNT; ptr != end; ptr++) { + if (*ptr == EC_SPI_FRAME_START) { + dev_dbg(ec_dev->dev, "msg found at %zd\n", + ptr - ec_dev->din); + break; + } + } + if (ptr != end) + break; + + /* + * Use the time at the start of the loop as a timeout. This + * gives us one last shot at getting the transfer and is useful + * in case we got context switched out for a while. + */ + if (time_after(start_jiffies, deadline)) { + dev_warn(ec_dev->dev, "EC failed to respond in time\n"); + return -ETIMEDOUT; + } + } + + /* + * ptr now points to the header byte. Copy any valid data to the + * start of our buffer + */ + todo = end - ++ptr; + todo = min(todo, need_len); + memmove(ec_dev->din, ptr, todo); + ptr = ec_dev->din + todo; + dev_dbg(ec_dev->dev, "need %d, got %d bytes from preamble\n", + need_len, todo); + need_len -= todo; + + /* Receive data until we have it all */ + while (need_len > 0) { + /* + * We can't support transfers larger than the SPI FIFO size + * unless we have DMA. We don't have DMA on the ISP SPI ports + * for Exynos. We need a way of asking SPI driver for + * maximum-supported transfer size. + */ + todo = min(need_len, 256); + dev_dbg(ec_dev->dev, "loop, todo=%d, need_len=%d, ptr=%zd\n", + todo, need_len, ptr - ec_dev->din); + + ret = receive_n_bytes(ec_dev, ptr, todo); + if (ret < 0) + return ret; + + debug_packet(ec_dev->dev, "interim", ptr, todo); + ptr += todo; + need_len -= todo; + } + + dev_dbg(ec_dev->dev, "loop done, ptr=%zd\n", ptr - ec_dev->din); + + return 0; +} + +/** + * do_cros_ec_pkt_xfer_spi - Transfer a packet over SPI and receive the reply + * + * @ec_dev: ChromeOS EC device + * @ec_msg: Message to transfer + */ +static int do_cros_ec_pkt_xfer_spi(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + struct ec_host_response *response; + struct cros_ec_spi *ec_spi = ec_dev->priv; + struct spi_transfer trans, trans_delay; + struct spi_message msg; + int i, len; + u8 *ptr; + u8 *rx_buf; + u8 sum; + u8 rx_byte; + int ret = 0, final_ret; + unsigned long delay; + + len = cros_ec_prepare_tx(ec_dev, ec_msg); + if (len < 0) + return len; + dev_dbg(ec_dev->dev, "prepared, len=%d\n", len); + + /* If it's too soon to do another transaction, wait */ + delay = ktime_get_ns() - ec_spi->last_transfer_ns; + if (delay < EC_SPI_RECOVERY_TIME_NS) + ndelay(EC_SPI_RECOVERY_TIME_NS - delay); + + rx_buf = kzalloc(len, GFP_KERNEL); + if (!rx_buf) + return -ENOMEM; + + spi_bus_lock(ec_spi->spi->controller); + + /* + * Leave a gap between CS assertion and clocking of data to allow the + * EC time to wakeup. + */ + spi_message_init(&msg); + if (ec_spi->start_of_msg_delay) { + memset(&trans_delay, 0, sizeof(trans_delay)); + trans_delay.delay.value = ec_spi->start_of_msg_delay; + trans_delay.delay.unit = SPI_DELAY_UNIT_USECS; + spi_message_add_tail(&trans_delay, &msg); + } + + /* Transmit phase - send our message */ + memset(&trans, 0, sizeof(trans)); + trans.tx_buf = ec_dev->dout; + trans.rx_buf = rx_buf; + trans.len = len; + trans.cs_change = 1; + spi_message_add_tail(&trans, &msg); + ret = spi_sync_locked(ec_spi->spi, &msg); + + /* Get the response */ + if (!ret) { + /* Verify that EC can process command */ + for (i = 0; i < len; i++) { + rx_byte = rx_buf[i]; + /* + * Seeing the PAST_END, RX_BAD_DATA, or NOT_READY + * markers are all signs that the EC didn't fully + * receive our command. e.g., if the EC is flashing + * itself, it can't respond to any commands and instead + * clocks out EC_SPI_PAST_END from its SPI hardware + * buffer. Similar occurrences can happen if the AP is + * too slow to clock out data after asserting CS -- the + * EC will abort and fill its buffer with + * EC_SPI_RX_BAD_DATA. + * + * In all cases, these errors should be safe to retry. + * Report -EAGAIN and let the caller decide what to do + * about that. + */ + if (rx_byte == EC_SPI_PAST_END || + rx_byte == EC_SPI_RX_BAD_DATA || + rx_byte == EC_SPI_NOT_READY) { + ret = -EAGAIN; + break; + } + } + } + + if (!ret) + ret = cros_ec_spi_receive_packet(ec_dev, + ec_msg->insize + sizeof(*response)); + else if (ret != -EAGAIN) + dev_err(ec_dev->dev, "spi transfer failed: %d\n", ret); + + final_ret = terminate_request(ec_dev); + + spi_bus_unlock(ec_spi->spi->controller); + + if (!ret) + ret = final_ret; + if (ret < 0) + goto exit; + + ptr = ec_dev->din; + + /* check response error code */ + response = (struct ec_host_response *)ptr; + ec_msg->result = response->result; + + ret = cros_ec_check_result(ec_dev, ec_msg); + if (ret) + goto exit; + + len = response->data_len; + sum = 0; + if (len > ec_msg->insize) { + dev_err(ec_dev->dev, "packet too long (%d bytes, expected %d)", + len, ec_msg->insize); + ret = -EMSGSIZE; + goto exit; + } + + for (i = 0; i < sizeof(*response); i++) + sum += ptr[i]; + + /* copy response packet payload and compute checksum */ + memcpy(ec_msg->data, ptr + sizeof(*response), len); + for (i = 0; i < len; i++) + sum += ec_msg->data[i]; + + if (sum) { + dev_err(ec_dev->dev, + "bad packet checksum, calculated %x\n", + sum); + ret = -EBADMSG; + goto exit; + } + + ret = len; +exit: + kfree(rx_buf); + if (ec_msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +/** + * do_cros_ec_cmd_xfer_spi - Transfer a message over SPI and receive the reply + * + * @ec_dev: ChromeOS EC device + * @ec_msg: Message to transfer + */ +static int do_cros_ec_cmd_xfer_spi(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + struct cros_ec_spi *ec_spi = ec_dev->priv; + struct spi_transfer trans; + struct spi_message msg; + int i, len; + u8 *ptr; + u8 *rx_buf; + u8 rx_byte; + int sum; + int ret = 0, final_ret; + unsigned long delay; + + len = cros_ec_prepare_tx(ec_dev, ec_msg); + if (len < 0) + return len; + dev_dbg(ec_dev->dev, "prepared, len=%d\n", len); + + /* If it's too soon to do another transaction, wait */ + delay = ktime_get_ns() - ec_spi->last_transfer_ns; + if (delay < EC_SPI_RECOVERY_TIME_NS) + ndelay(EC_SPI_RECOVERY_TIME_NS - delay); + + rx_buf = kzalloc(len, GFP_KERNEL); + if (!rx_buf) + return -ENOMEM; + + spi_bus_lock(ec_spi->spi->controller); + + /* Transmit phase - send our message */ + debug_packet(ec_dev->dev, "out", ec_dev->dout, len); + memset(&trans, 0, sizeof(trans)); + trans.tx_buf = ec_dev->dout; + trans.rx_buf = rx_buf; + trans.len = len; + trans.cs_change = 1; + spi_message_init(&msg); + spi_message_add_tail(&trans, &msg); + ret = spi_sync_locked(ec_spi->spi, &msg); + + /* Get the response */ + if (!ret) { + /* Verify that EC can process command */ + for (i = 0; i < len; i++) { + rx_byte = rx_buf[i]; + /* See comments in cros_ec_pkt_xfer_spi() */ + if (rx_byte == EC_SPI_PAST_END || + rx_byte == EC_SPI_RX_BAD_DATA || + rx_byte == EC_SPI_NOT_READY) { + ret = -EAGAIN; + break; + } + } + } + + if (!ret) + ret = cros_ec_spi_receive_response(ec_dev, + ec_msg->insize + EC_MSG_TX_PROTO_BYTES); + else if (ret != -EAGAIN) + dev_err(ec_dev->dev, "spi transfer failed: %d\n", ret); + + final_ret = terminate_request(ec_dev); + + spi_bus_unlock(ec_spi->spi->controller); + + if (!ret) + ret = final_ret; + if (ret < 0) + goto exit; + + ptr = ec_dev->din; + + /* check response error code */ + ec_msg->result = ptr[0]; + ret = cros_ec_check_result(ec_dev, ec_msg); + if (ret) + goto exit; + + len = ptr[1]; + sum = ptr[0] + ptr[1]; + if (len > ec_msg->insize) { + dev_err(ec_dev->dev, "packet too long (%d bytes, expected %d)", + len, ec_msg->insize); + ret = -ENOSPC; + goto exit; + } + + /* copy response packet payload and compute checksum */ + for (i = 0; i < len; i++) { + sum += ptr[i + 2]; + if (ec_msg->insize) + ec_msg->data[i] = ptr[i + 2]; + } + sum &= 0xff; + + debug_packet(ec_dev->dev, "in", ptr, len + 3); + + if (sum != ptr[len + 2]) { + dev_err(ec_dev->dev, + "bad packet checksum, expected %02x, got %02x\n", + sum, ptr[len + 2]); + ret = -EBADMSG; + goto exit; + } + + ret = len; +exit: + kfree(rx_buf); + if (ec_msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +static void cros_ec_xfer_high_pri_work(struct kthread_work *work) +{ + struct cros_ec_xfer_work_params *params; + + params = container_of(work, struct cros_ec_xfer_work_params, work); + params->ret = params->fn(params->ec_dev, params->ec_msg); +} + +static int cros_ec_xfer_high_pri(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg, + cros_ec_xfer_fn_t fn) +{ + struct cros_ec_spi *ec_spi = ec_dev->priv; + struct cros_ec_xfer_work_params params = { + .work = KTHREAD_WORK_INIT(params.work, + cros_ec_xfer_high_pri_work), + .ec_dev = ec_dev, + .ec_msg = ec_msg, + .fn = fn, + }; + + /* + * This looks a bit ridiculous. Why do the work on a + * different thread if we're just going to block waiting for + * the thread to finish? The key here is that the thread is + * running at high priority but the calling context might not + * be. We need to be at high priority to avoid getting + * context switched out for too long and the EC giving up on + * the transfer. + */ + kthread_queue_work(ec_spi->high_pri_worker, ¶ms.work); + kthread_flush_work(¶ms.work); + + return params.ret; +} + +static int cros_ec_pkt_xfer_spi(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + return cros_ec_xfer_high_pri(ec_dev, ec_msg, do_cros_ec_pkt_xfer_spi); +} + +static int cros_ec_cmd_xfer_spi(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + return cros_ec_xfer_high_pri(ec_dev, ec_msg, do_cros_ec_cmd_xfer_spi); +} + +static void cros_ec_spi_dt_probe(struct cros_ec_spi *ec_spi, struct device *dev) +{ + struct device_node *np = dev->of_node; + u32 val; + int ret; + + ret = of_property_read_u32(np, "google,cros-ec-spi-pre-delay", &val); + if (!ret) + ec_spi->start_of_msg_delay = val; + + ret = of_property_read_u32(np, "google,cros-ec-spi-msg-delay", &val); + if (!ret) + ec_spi->end_of_msg_delay = val; +} + +static void cros_ec_spi_high_pri_release(void *worker) +{ + kthread_destroy_worker(worker); +} + +static int cros_ec_spi_devm_high_pri_alloc(struct device *dev, + struct cros_ec_spi *ec_spi) +{ + int err; + + ec_spi->high_pri_worker = + kthread_run_worker(0, "cros_ec_spi_high_pri"); + + if (IS_ERR(ec_spi->high_pri_worker)) { + err = PTR_ERR(ec_spi->high_pri_worker); + dev_err(dev, "Can't create cros_ec high pri worker: %d\n", err); + return err; + } + + err = devm_add_action_or_reset(dev, cros_ec_spi_high_pri_release, + ec_spi->high_pri_worker); + if (err) + return err; + + sched_set_fifo(ec_spi->high_pri_worker->task); + + return 0; +} + +static int cros_ec_spi_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct cros_ec_device *ec_dev; + struct cros_ec_spi *ec_spi; + int err; + + spi->rt = true; + err = spi_setup(spi); + if (err < 0) + return err; + + ec_spi = devm_kzalloc(dev, sizeof(*ec_spi), GFP_KERNEL); + if (ec_spi == NULL) + return -ENOMEM; + ec_spi->spi = spi; + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + /* Check for any DT properties */ + cros_ec_spi_dt_probe(ec_spi, dev); + + spi_set_drvdata(spi, ec_dev); + ec_dev->priv = ec_spi; + ec_dev->irq = spi->irq; + ec_dev->cmd_xfer = cros_ec_cmd_xfer_spi; + ec_dev->pkt_xfer = cros_ec_pkt_xfer_spi; + ec_dev->phys_name = dev_name(&ec_spi->spi->dev); + + ec_spi->last_transfer_ns = ktime_get_ns(); + + err = cros_ec_spi_devm_high_pri_alloc(dev, ec_spi); + if (err) + return err; + + err = cros_ec_register(ec_dev); + if (err) { + dev_err(dev, "cannot register EC\n"); + return err; + } + + device_init_wakeup(&spi->dev, true); + + return 0; +} + +static void cros_ec_spi_remove(struct spi_device *spi) +{ + struct cros_ec_device *ec_dev = spi_get_drvdata(spi); + + cros_ec_unregister(ec_dev); +} + +#ifdef CONFIG_PM_SLEEP +static int cros_ec_spi_suspend(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_suspend(ec_dev); +} + +static int cros_ec_spi_resume(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_resume(ec_dev); +} +#endif + +static SIMPLE_DEV_PM_OPS(cros_ec_spi_pm_ops, cros_ec_spi_suspend, + cros_ec_spi_resume); + +static const struct of_device_id cros_ec_spi_of_match[] = { + { .compatible = "google,cros-ec-spi", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, cros_ec_spi_of_match); + +static const struct spi_device_id cros_ec_spi_id[] = { + { "cros-ec-spi", 0 }, + { } +}; +MODULE_DEVICE_TABLE(spi, cros_ec_spi_id); + +static struct spi_driver cros_ec_driver_spi = { + .driver = { + .name = "cros-ec-spi", + .of_match_table = cros_ec_spi_of_match, + .pm = &cros_ec_spi_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .probe = cros_ec_spi_probe, + .remove = cros_ec_spi_remove, + .id_table = cros_ec_spi_id, +}; + +module_spi_driver(cros_ec_driver_spi); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("SPI interface for ChromeOS Embedded Controller"); diff --git a/drivers/platform/chrome/cros_ec_sysfs.c b/drivers/platform/chrome/cros_ec_sysfs.c new file mode 100644 index 000000000..fad593df3 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_sysfs.c @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Expose the ChromeOS EC through sysfs +// +// Copyright (C) 2014 Google, Inc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-sysfs" + +/* Accessor functions */ + +static ssize_t reboot_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int count = 0; + + count += sysfs_emit_at(buf, count, + "ro|rw|cancel|cold|disable-jump|hibernate|cold-ap-off"); + count += sysfs_emit_at(buf, count, " [at-shutdown]\n"); + return count; +} + +static ssize_t reboot_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + static const struct { + const char * const str; + uint8_t cmd; + uint8_t flags; + } words[] = { + {"cancel", EC_REBOOT_CANCEL, 0}, + {"ro", EC_REBOOT_JUMP_RO, 0}, + {"rw", EC_REBOOT_JUMP_RW, 0}, + {"cold-ap-off", EC_REBOOT_COLD_AP_OFF, 0}, + {"cold", EC_REBOOT_COLD, 0}, + {"disable-jump", EC_REBOOT_DISABLE_JUMP, 0}, + {"hibernate", EC_REBOOT_HIBERNATE, 0}, + {"at-shutdown", -1, EC_REBOOT_FLAG_ON_AP_SHUTDOWN}, + }; + struct cros_ec_command *msg; + struct ec_params_reboot_ec *param; + int got_cmd = 0, offset = 0; + int i; + int ret; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + msg = kmalloc(sizeof(*msg) + sizeof(*param), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_reboot_ec *)msg->data; + + param->flags = 0; + while (1) { + /* Find word to start scanning */ + while (buf[offset] && isspace(buf[offset])) + offset++; + if (!buf[offset]) + break; + + for (i = 0; i < ARRAY_SIZE(words); i++) { + if (!strncasecmp(words[i].str, buf+offset, + strlen(words[i].str))) { + if (words[i].flags) { + param->flags |= words[i].flags; + } else { + param->cmd = words[i].cmd; + got_cmd = 1; + } + break; + } + } + + /* On to the next word, if any */ + while (buf[offset] && !isspace(buf[offset])) + offset++; + } + + if (!got_cmd) { + count = -EINVAL; + goto exit; + } + + msg->version = 0; + msg->command = EC_CMD_REBOOT_EC + ec->cmd_offset; + msg->outsize = sizeof(*param); + msg->insize = 0; + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + count = ret; +exit: + kfree(msg); + return count; +} + +static ssize_t version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + static const char * const image_names[] = {"unknown", "RO", "RW"}; + struct ec_response_get_version *r_ver; + struct ec_response_get_chip_info *r_chip; + struct ec_response_board_version *r_board; + struct cros_ec_command *msg; + int ret; + int count = 0; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + msg = kmalloc(sizeof(*msg) + EC_HOST_PARAM_SIZE, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + /* Get versions. RW may change. */ + msg->version = 0; + msg->command = EC_CMD_GET_VERSION + ec->cmd_offset; + msg->insize = sizeof(*r_ver); + msg->outsize = 0; + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + count = ret; + goto exit; + } + r_ver = (struct ec_response_get_version *)msg->data; + /* Strings should be null-terminated, but let's be sure. */ + r_ver->version_string_ro[sizeof(r_ver->version_string_ro) - 1] = '\0'; + r_ver->version_string_rw[sizeof(r_ver->version_string_rw) - 1] = '\0'; + count += sysfs_emit_at(buf, count, "RO version: %s\n", r_ver->version_string_ro); + count += sysfs_emit_at(buf, count, "RW version: %s\n", r_ver->version_string_rw); + count += sysfs_emit_at(buf, count, "Firmware copy: %s\n", + (r_ver->current_image < ARRAY_SIZE(image_names) ? + image_names[r_ver->current_image] : "?")); + + /* Get build info. */ + msg->command = EC_CMD_GET_BUILD_INFO + ec->cmd_offset; + msg->insize = EC_HOST_PARAM_SIZE; + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + count += sysfs_emit_at(buf, count, + "Build info: XFER / EC ERROR %d / %d\n", + ret, msg->result); + } else { + msg->data[EC_HOST_PARAM_SIZE - 1] = '\0'; + count += sysfs_emit_at(buf, count, "Build info: %s\n", msg->data); + } + + /* Get chip info. */ + msg->command = EC_CMD_GET_CHIP_INFO + ec->cmd_offset; + msg->insize = sizeof(*r_chip); + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + count += sysfs_emit_at(buf, count, + "Chip info: XFER / EC ERROR %d / %d\n", + ret, msg->result); + } else { + r_chip = (struct ec_response_get_chip_info *)msg->data; + + r_chip->vendor[sizeof(r_chip->vendor) - 1] = '\0'; + r_chip->name[sizeof(r_chip->name) - 1] = '\0'; + r_chip->revision[sizeof(r_chip->revision) - 1] = '\0'; + count += sysfs_emit_at(buf, count, "Chip vendor: %s\n", r_chip->vendor); + count += sysfs_emit_at(buf, count, "Chip name: %s\n", r_chip->name); + count += sysfs_emit_at(buf, count, "Chip revision: %s\n", r_chip->revision); + } + + /* Get board version */ + msg->command = EC_CMD_GET_BOARD_VERSION + ec->cmd_offset; + msg->insize = sizeof(*r_board); + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) { + count += sysfs_emit_at(buf, count, + "Board version: XFER / EC ERROR %d / %d\n", + ret, msg->result); + } else { + r_board = (struct ec_response_board_version *)msg->data; + + count += sysfs_emit_at(buf, count, + "Board version: %d\n", + r_board->board_version); + } + +exit: + kfree(msg); + return count; +} + +static ssize_t flashinfo_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ec_response_flash_info *resp; + struct cros_ec_command *msg; + int ret; + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + msg = kmalloc(sizeof(*msg) + sizeof(*resp), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + /* The flash info shouldn't ever change, but ask each time anyway. */ + msg->version = 0; + msg->command = EC_CMD_FLASH_INFO + ec->cmd_offset; + msg->insize = sizeof(*resp); + msg->outsize = 0; + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + + resp = (struct ec_response_flash_info *)msg->data; + + ret = sysfs_emit(buf, + "FlashSize %d\nWriteSize %d\n" + "EraseSize %d\nProtectSize %d\n", + resp->flash_size, resp->write_block_size, + resp->erase_block_size, resp->protect_block_size); +exit: + kfree(msg); + return ret; +} + +/* Keyboard wake angle control */ +static ssize_t kb_wake_angle_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + struct ec_response_motion_sense *resp; + struct ec_params_motion_sense *param; + struct cros_ec_command *msg; + int ret; + + msg = kmalloc(sizeof(*msg) + EC_HOST_PARAM_SIZE, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_motion_sense *)msg->data; + msg->command = EC_CMD_MOTION_SENSE_CMD + ec->cmd_offset; + msg->version = 2; + param->cmd = MOTIONSENSE_CMD_KB_WAKE_ANGLE; + param->kb_wake_angle.data = EC_MOTION_SENSE_NO_VALUE; + msg->outsize = sizeof(*param); + msg->insize = sizeof(*resp); + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + if (ret < 0) + goto exit; + + resp = (struct ec_response_motion_sense *)msg->data; + ret = sysfs_emit(buf, "%d\n", resp->kb_wake_angle.ret); +exit: + kfree(msg); + return ret; +} + +static ssize_t kb_wake_angle_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + struct ec_params_motion_sense *param; + struct cros_ec_command *msg; + u16 angle; + int ret; + + ret = kstrtou16(buf, 0, &angle); + if (ret) + return ret; + + msg = kmalloc(sizeof(*msg) + EC_HOST_PARAM_SIZE, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + param = (struct ec_params_motion_sense *)msg->data; + msg->command = EC_CMD_MOTION_SENSE_CMD + ec->cmd_offset; + msg->version = 2; + param->cmd = MOTIONSENSE_CMD_KB_WAKE_ANGLE; + param->kb_wake_angle.data = angle; + msg->outsize = sizeof(*param); + msg->insize = sizeof(struct ec_response_motion_sense); + + ret = cros_ec_cmd_xfer_status(ec->ec_dev, msg); + kfree(msg); + if (ret < 0) + return ret; + return count; +} + +static ssize_t usbpdmuxinfo_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + ssize_t count = 0; + struct ec_response_usb_pd_ports resp_pd_ports; + int ret; + int i; + + ret = cros_ec_cmd(ec->ec_dev, 0, EC_CMD_USB_PD_PORTS, NULL, 0, + &resp_pd_ports, sizeof(resp_pd_ports)); + if (ret < 0) + return -EIO; + + for (i = 0; i < resp_pd_ports.num_ports; i++) { + struct ec_response_usb_pd_mux_info resp_mux; + struct ec_params_usb_pd_mux_info req = { + .port = i, + }; + + ret = cros_ec_cmd(ec->ec_dev, 0, EC_CMD_USB_PD_MUX_INFO, + &req, sizeof(req), &resp_mux, sizeof(resp_mux)); + + if (ret >= 0) { + count += sysfs_emit_at(buf, count, "Port %d:", i); + count += sysfs_emit_at(buf, count, " USB=%d", + !!(resp_mux.flags & USB_PD_MUX_USB_ENABLED)); + count += sysfs_emit_at(buf, count, " DP=%d", + !!(resp_mux.flags & USB_PD_MUX_DP_ENABLED)); + count += sysfs_emit_at(buf, count, " POLARITY=%s", + (resp_mux.flags & USB_PD_MUX_POLARITY_INVERTED) ? + "INVERTED" : "NORMAL"); + count += sysfs_emit_at(buf, count, " HPD_IRQ=%d", + !!(resp_mux.flags & USB_PD_MUX_HPD_IRQ)); + count += sysfs_emit_at(buf, count, " HPD_LVL=%d", + !!(resp_mux.flags & USB_PD_MUX_HPD_LVL)); + count += sysfs_emit_at(buf, count, " SAFE=%d", + !!(resp_mux.flags & USB_PD_MUX_SAFE_MODE)); + count += sysfs_emit_at(buf, count, " TBT=%d", + !!(resp_mux.flags & USB_PD_MUX_TBT_COMPAT_ENABLED)); + count += sysfs_emit_at(buf, count, " USB4=%d\n", + !!(resp_mux.flags & USB_PD_MUX_USB4_ENABLED)); + } + } + + return count ? : -EIO; +} + +static ssize_t ap_mode_entry_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + const bool ap_driven_altmode = cros_ec_check_features( + ec, EC_FEATURE_TYPEC_REQUIRE_AP_MODE_ENTRY); + + return sysfs_emit(buf, "%s\n", ap_driven_altmode ? "yes" : "no"); +} + +/* Module initialization */ + +static DEVICE_ATTR_RW(reboot); +static DEVICE_ATTR_RO(version); +static DEVICE_ATTR_RO(flashinfo); +static DEVICE_ATTR_RW(kb_wake_angle); +static DEVICE_ATTR_RO(usbpdmuxinfo); +static DEVICE_ATTR_RO(ap_mode_entry); + +static struct attribute *__ec_attrs[] = { + &dev_attr_kb_wake_angle.attr, + &dev_attr_reboot.attr, + &dev_attr_version.attr, + &dev_attr_flashinfo.attr, + &dev_attr_usbpdmuxinfo.attr, + &dev_attr_ap_mode_entry.attr, + NULL, +}; + +static umode_t cros_ec_ctrl_visible(struct kobject *kobj, + struct attribute *a, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + + if (a == &dev_attr_kb_wake_angle.attr && !ec->has_kb_wake_angle) + return 0; + + if (a == &dev_attr_usbpdmuxinfo.attr || + a == &dev_attr_ap_mode_entry.attr) { + struct cros_ec_platform *ec_platform = dev_get_platdata(ec->dev); + + if (strcmp(ec_platform->ec_name, CROS_EC_DEV_NAME)) + return 0; + } + + return a->mode; +} + +static const struct attribute_group cros_ec_attr_group = { + .attrs = __ec_attrs, + .is_visible = cros_ec_ctrl_visible, +}; + +static int cros_ec_sysfs_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + struct device *dev = &pd->dev; + int ret; + + ec_dev->group = &cros_ec_attr_group; + ret = sysfs_create_group(&ec_dev->class_dev.kobj, ec_dev->group); + if (ret < 0) + dev_err(dev, "failed to create attributes. err=%d\n", ret); + + return ret; +} + +static void cros_ec_sysfs_remove(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + + sysfs_remove_group(&ec_dev->class_dev.kobj, ec_dev->group); +} + +static const struct platform_device_id cros_ec_sysfs_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_sysfs_id); + +static struct platform_driver cros_ec_sysfs_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = cros_ec_sysfs_probe, + .remove = cros_ec_sysfs_remove, + .id_table = cros_ec_sysfs_id, +}; + +module_platform_driver(cros_ec_sysfs_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Expose the ChromeOS EC through sysfs"); diff --git a/drivers/platform/chrome/cros_ec_trace.c b/drivers/platform/chrome/cros_ec_trace.c new file mode 100644 index 000000000..9827b3117 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_trace.c @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0 +// Trace events for the ChromeOS Embedded Controller +// +// Copyright 2019 Google LLC. + +#define TRACE_SYMBOL(a) {a, #a} + +// Generate the list using the following script: +// sed -n 's/^#define \(EC_CMD_[[:alnum:]_]*\)\s.*/\tTRACE_SYMBOL(\1), \\/p' include/linux/platform_data/cros_ec_commands.h +#define EC_CMDS \ + TRACE_SYMBOL(EC_CMD_ACPI_READ), \ + TRACE_SYMBOL(EC_CMD_ACPI_WRITE), \ + TRACE_SYMBOL(EC_CMD_ACPI_BURST_ENABLE), \ + TRACE_SYMBOL(EC_CMD_ACPI_BURST_DISABLE), \ + TRACE_SYMBOL(EC_CMD_ACPI_QUERY_EVENT), \ + TRACE_SYMBOL(EC_CMD_PROTO_VERSION), \ + TRACE_SYMBOL(EC_CMD_HELLO), \ + TRACE_SYMBOL(EC_CMD_GET_VERSION), \ + TRACE_SYMBOL(EC_CMD_READ_TEST), \ + TRACE_SYMBOL(EC_CMD_GET_BUILD_INFO), \ + TRACE_SYMBOL(EC_CMD_GET_CHIP_INFO), \ + TRACE_SYMBOL(EC_CMD_GET_BOARD_VERSION), \ + TRACE_SYMBOL(EC_CMD_READ_MEMMAP), \ + TRACE_SYMBOL(EC_CMD_GET_CMD_VERSIONS), \ + TRACE_SYMBOL(EC_CMD_GET_COMMS_STATUS), \ + TRACE_SYMBOL(EC_CMD_TEST_PROTOCOL), \ + TRACE_SYMBOL(EC_CMD_GET_PROTOCOL_INFO), \ + TRACE_SYMBOL(EC_CMD_GSV_PAUSE_IN_S5), \ + TRACE_SYMBOL(EC_CMD_GET_FEATURES), \ + TRACE_SYMBOL(EC_CMD_GET_SKU_ID), \ + TRACE_SYMBOL(EC_CMD_SET_SKU_ID), \ + TRACE_SYMBOL(EC_CMD_FLASH_INFO), \ + TRACE_SYMBOL(EC_CMD_FLASH_READ), \ + TRACE_SYMBOL(EC_CMD_FLASH_WRITE), \ + TRACE_SYMBOL(EC_CMD_FLASH_ERASE), \ + TRACE_SYMBOL(EC_CMD_FLASH_PROTECT), \ + TRACE_SYMBOL(EC_CMD_FLASH_REGION_INFO), \ + TRACE_SYMBOL(EC_CMD_VBNV_CONTEXT), \ + TRACE_SYMBOL(EC_CMD_FLASH_SPI_INFO), \ + TRACE_SYMBOL(EC_CMD_FLASH_SELECT), \ + TRACE_SYMBOL(EC_CMD_PWM_GET_FAN_TARGET_RPM), \ + TRACE_SYMBOL(EC_CMD_PWM_SET_FAN_TARGET_RPM), \ + TRACE_SYMBOL(EC_CMD_PWM_GET_KEYBOARD_BACKLIGHT), \ + TRACE_SYMBOL(EC_CMD_PWM_SET_KEYBOARD_BACKLIGHT), \ + TRACE_SYMBOL(EC_CMD_PWM_SET_FAN_DUTY), \ + TRACE_SYMBOL(EC_CMD_PWM_SET_DUTY), \ + TRACE_SYMBOL(EC_CMD_PWM_GET_DUTY), \ + TRACE_SYMBOL(EC_CMD_LIGHTBAR_CMD), \ + TRACE_SYMBOL(EC_CMD_LED_CONTROL), \ + TRACE_SYMBOL(EC_CMD_VBOOT_HASH), \ + TRACE_SYMBOL(EC_CMD_MOTION_SENSE_CMD), \ + TRACE_SYMBOL(EC_CMD_FORCE_LID_OPEN), \ + TRACE_SYMBOL(EC_CMD_CONFIG_POWER_BUTTON), \ + TRACE_SYMBOL(EC_CMD_USB_CHARGE_SET_MODE), \ + TRACE_SYMBOL(EC_CMD_PSTORE_INFO), \ + TRACE_SYMBOL(EC_CMD_PSTORE_READ), \ + TRACE_SYMBOL(EC_CMD_PSTORE_WRITE), \ + TRACE_SYMBOL(EC_CMD_RTC_GET_VALUE), \ + TRACE_SYMBOL(EC_CMD_RTC_GET_ALARM), \ + TRACE_SYMBOL(EC_CMD_RTC_SET_VALUE), \ + TRACE_SYMBOL(EC_CMD_RTC_SET_ALARM), \ + TRACE_SYMBOL(EC_CMD_PORT80_LAST_BOOT), \ + TRACE_SYMBOL(EC_CMD_PORT80_READ), \ + TRACE_SYMBOL(EC_CMD_VSTORE_INFO), \ + TRACE_SYMBOL(EC_CMD_VSTORE_READ), \ + TRACE_SYMBOL(EC_CMD_VSTORE_WRITE), \ + TRACE_SYMBOL(EC_CMD_THERMAL_SET_THRESHOLD), \ + TRACE_SYMBOL(EC_CMD_THERMAL_GET_THRESHOLD), \ + TRACE_SYMBOL(EC_CMD_THERMAL_AUTO_FAN_CTRL), \ + TRACE_SYMBOL(EC_CMD_TMP006_GET_CALIBRATION), \ + TRACE_SYMBOL(EC_CMD_TMP006_SET_CALIBRATION), \ + TRACE_SYMBOL(EC_CMD_TMP006_GET_RAW), \ + TRACE_SYMBOL(EC_CMD_MKBP_STATE), \ + TRACE_SYMBOL(EC_CMD_MKBP_INFO), \ + TRACE_SYMBOL(EC_CMD_MKBP_SIMULATE_KEY), \ + TRACE_SYMBOL(EC_CMD_GET_KEYBOARD_ID), \ + TRACE_SYMBOL(EC_CMD_MKBP_SET_CONFIG), \ + TRACE_SYMBOL(EC_CMD_MKBP_GET_CONFIG), \ + TRACE_SYMBOL(EC_CMD_KEYSCAN_SEQ_CTRL), \ + TRACE_SYMBOL(EC_CMD_GET_NEXT_EVENT), \ + TRACE_SYMBOL(EC_CMD_KEYBOARD_FACTORY_TEST), \ + TRACE_SYMBOL(EC_CMD_TEMP_SENSOR_GET_INFO), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_GET_B), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_GET_SMI_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_GET_SCI_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_GET_WAKE_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_SET_SMI_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_SET_SCI_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_CLEAR), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_SET_WAKE_MASK), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT_CLEAR_B), \ + TRACE_SYMBOL(EC_CMD_HOST_EVENT), \ + TRACE_SYMBOL(EC_CMD_SWITCH_ENABLE_BKLIGHT), \ + TRACE_SYMBOL(EC_CMD_SWITCH_ENABLE_WIRELESS), \ + TRACE_SYMBOL(EC_CMD_GPIO_SET), \ + TRACE_SYMBOL(EC_CMD_GPIO_GET), \ + TRACE_SYMBOL(EC_CMD_I2C_READ), \ + TRACE_SYMBOL(EC_CMD_I2C_WRITE), \ + TRACE_SYMBOL(EC_CMD_CHARGE_CONTROL), \ + TRACE_SYMBOL(EC_CMD_CONSOLE_SNAPSHOT), \ + TRACE_SYMBOL(EC_CMD_CONSOLE_READ), \ + TRACE_SYMBOL(EC_CMD_BATTERY_CUT_OFF), \ + TRACE_SYMBOL(EC_CMD_USB_MUX), \ + TRACE_SYMBOL(EC_CMD_LDO_SET), \ + TRACE_SYMBOL(EC_CMD_LDO_GET), \ + TRACE_SYMBOL(EC_CMD_POWER_INFO), \ + TRACE_SYMBOL(EC_CMD_I2C_PASSTHRU), \ + TRACE_SYMBOL(EC_CMD_HANG_DETECT), \ + TRACE_SYMBOL(EC_CMD_CHARGE_STATE), \ + TRACE_SYMBOL(EC_CMD_CHARGE_CURRENT_LIMIT), \ + TRACE_SYMBOL(EC_CMD_EXTERNAL_POWER_LIMIT), \ + TRACE_SYMBOL(EC_CMD_OVERRIDE_DEDICATED_CHARGER_LIMIT), \ + TRACE_SYMBOL(EC_CMD_HIBERNATION_DELAY), \ + TRACE_SYMBOL(EC_CMD_HOST_SLEEP_EVENT), \ + TRACE_SYMBOL(EC_CMD_DEVICE_EVENT), \ + TRACE_SYMBOL(EC_CMD_SB_READ_WORD), \ + TRACE_SYMBOL(EC_CMD_SB_WRITE_WORD), \ + TRACE_SYMBOL(EC_CMD_SB_READ_BLOCK), \ + TRACE_SYMBOL(EC_CMD_SB_WRITE_BLOCK), \ + TRACE_SYMBOL(EC_CMD_BATTERY_VENDOR_PARAM), \ + TRACE_SYMBOL(EC_CMD_SB_FW_UPDATE), \ + TRACE_SYMBOL(EC_CMD_ENTERING_MODE), \ + TRACE_SYMBOL(EC_CMD_I2C_PASSTHRU_PROTECT), \ + TRACE_SYMBOL(EC_CMD_CEC_WRITE_MSG), \ + TRACE_SYMBOL(EC_CMD_CEC_READ_MSG), \ + TRACE_SYMBOL(EC_CMD_CEC_SET), \ + TRACE_SYMBOL(EC_CMD_CEC_GET), \ + TRACE_SYMBOL(EC_CMD_CEC_PORT_COUNT), \ + TRACE_SYMBOL(EC_CMD_EC_CODEC), \ + TRACE_SYMBOL(EC_CMD_EC_CODEC_DMIC), \ + TRACE_SYMBOL(EC_CMD_EC_CODEC_I2S_RX), \ + TRACE_SYMBOL(EC_CMD_EC_CODEC_WOV), \ + TRACE_SYMBOL(EC_CMD_REBOOT_EC), \ + TRACE_SYMBOL(EC_CMD_GET_PANIC_INFO), \ + TRACE_SYMBOL(EC_CMD_REBOOT), \ + TRACE_SYMBOL(EC_CMD_RESEND_RESPONSE), \ + TRACE_SYMBOL(EC_CMD_VERSION0), \ + TRACE_SYMBOL(EC_CMD_PD_EXCHANGE_STATUS), \ + TRACE_SYMBOL(EC_CMD_PD_HOST_EVENT_STATUS), \ + TRACE_SYMBOL(EC_CMD_USB_PD_CONTROL), \ + TRACE_SYMBOL(EC_CMD_USB_PD_PORTS), \ + TRACE_SYMBOL(EC_CMD_USB_PD_POWER_INFO), \ + TRACE_SYMBOL(EC_CMD_CHARGE_PORT_COUNT), \ + TRACE_SYMBOL(EC_CMD_USB_PD_FW_UPDATE), \ + TRACE_SYMBOL(EC_CMD_USB_PD_RW_HASH_ENTRY), \ + TRACE_SYMBOL(EC_CMD_USB_PD_DEV_INFO), \ + TRACE_SYMBOL(EC_CMD_USB_PD_DISCOVERY), \ + TRACE_SYMBOL(EC_CMD_PD_CHARGE_PORT_OVERRIDE), \ + TRACE_SYMBOL(EC_CMD_PD_GET_LOG_ENTRY), \ + TRACE_SYMBOL(EC_CMD_USB_PD_GET_AMODE), \ + TRACE_SYMBOL(EC_CMD_USB_PD_SET_AMODE), \ + TRACE_SYMBOL(EC_CMD_PD_WRITE_LOG_ENTRY), \ + TRACE_SYMBOL(EC_CMD_PD_CONTROL), \ + TRACE_SYMBOL(EC_CMD_USB_PD_MUX_INFO), \ + TRACE_SYMBOL(EC_CMD_PD_CHIP_INFO), \ + TRACE_SYMBOL(EC_CMD_RWSIG_CHECK_STATUS), \ + TRACE_SYMBOL(EC_CMD_RWSIG_ACTION), \ + TRACE_SYMBOL(EC_CMD_EFS_VERIFY), \ + TRACE_SYMBOL(EC_CMD_GET_CROS_BOARD_INFO), \ + TRACE_SYMBOL(EC_CMD_SET_CROS_BOARD_INFO), \ + TRACE_SYMBOL(EC_CMD_GET_UPTIME_INFO), \ + TRACE_SYMBOL(EC_CMD_ADD_ENTROPY), \ + TRACE_SYMBOL(EC_CMD_ADC_READ), \ + TRACE_SYMBOL(EC_CMD_ROLLBACK_INFO), \ + TRACE_SYMBOL(EC_CMD_AP_RESET), \ + TRACE_SYMBOL(EC_CMD_PCHG_COUNT), \ + TRACE_SYMBOL(EC_CMD_PCHG), \ + TRACE_SYMBOL(EC_CMD_PCHG_UPDATE), \ + TRACE_SYMBOL(EC_CMD_REGULATOR_GET_INFO), \ + TRACE_SYMBOL(EC_CMD_REGULATOR_ENABLE), \ + TRACE_SYMBOL(EC_CMD_REGULATOR_IS_ENABLED), \ + TRACE_SYMBOL(EC_CMD_REGULATOR_SET_VOLTAGE), \ + TRACE_SYMBOL(EC_CMD_REGULATOR_GET_VOLTAGE), \ + TRACE_SYMBOL(EC_CMD_TYPEC_DISCOVERY), \ + TRACE_SYMBOL(EC_CMD_TYPEC_CONTROL), \ + TRACE_SYMBOL(EC_CMD_TYPEC_STATUS), \ + TRACE_SYMBOL(EC_CMD_TYPEC_VDM_RESPONSE), \ + TRACE_SYMBOL(EC_CMD_CR51_BASE), \ + TRACE_SYMBOL(EC_CMD_CR51_LAST), \ + TRACE_SYMBOL(EC_CMD_FP_PASSTHRU), \ + TRACE_SYMBOL(EC_CMD_FP_MODE), \ + TRACE_SYMBOL(EC_CMD_FP_INFO), \ + TRACE_SYMBOL(EC_CMD_FP_FRAME), \ + TRACE_SYMBOL(EC_CMD_FP_TEMPLATE), \ + TRACE_SYMBOL(EC_CMD_FP_CONTEXT), \ + TRACE_SYMBOL(EC_CMD_FP_STATS), \ + TRACE_SYMBOL(EC_CMD_FP_SEED), \ + TRACE_SYMBOL(EC_CMD_FP_ENC_STATUS), \ + TRACE_SYMBOL(EC_CMD_TP_SELF_TEST), \ + TRACE_SYMBOL(EC_CMD_TP_FRAME_INFO), \ + TRACE_SYMBOL(EC_CMD_TP_FRAME_SNAPSHOT), \ + TRACE_SYMBOL(EC_CMD_TP_FRAME_GET), \ + TRACE_SYMBOL(EC_CMD_BATTERY_GET_STATIC), \ + TRACE_SYMBOL(EC_CMD_BATTERY_GET_DYNAMIC), \ + TRACE_SYMBOL(EC_CMD_CHARGER_CONTROL), \ + TRACE_SYMBOL(EC_CMD_USB_PD_MUX_ACK), \ + TRACE_SYMBOL(EC_CMD_BOARD_SPECIFIC_BASE), \ + TRACE_SYMBOL(EC_CMD_BOARD_SPECIFIC_LAST) + +/* See the enum ec_status in include/linux/platform_data/cros_ec_commands.h */ +#define EC_RESULT \ + TRACE_SYMBOL(EC_RES_SUCCESS), \ + TRACE_SYMBOL(EC_RES_INVALID_COMMAND), \ + TRACE_SYMBOL(EC_RES_ERROR), \ + TRACE_SYMBOL(EC_RES_INVALID_PARAM), \ + TRACE_SYMBOL(EC_RES_ACCESS_DENIED), \ + TRACE_SYMBOL(EC_RES_INVALID_RESPONSE), \ + TRACE_SYMBOL(EC_RES_INVALID_VERSION), \ + TRACE_SYMBOL(EC_RES_INVALID_CHECKSUM), \ + TRACE_SYMBOL(EC_RES_IN_PROGRESS), \ + TRACE_SYMBOL(EC_RES_UNAVAILABLE), \ + TRACE_SYMBOL(EC_RES_TIMEOUT), \ + TRACE_SYMBOL(EC_RES_OVERFLOW), \ + TRACE_SYMBOL(EC_RES_INVALID_HEADER), \ + TRACE_SYMBOL(EC_RES_REQUEST_TRUNCATED), \ + TRACE_SYMBOL(EC_RES_RESPONSE_TOO_BIG), \ + TRACE_SYMBOL(EC_RES_BUS_ERROR), \ + TRACE_SYMBOL(EC_RES_BUSY), \ + TRACE_SYMBOL(EC_RES_INVALID_HEADER_VERSION), \ + TRACE_SYMBOL(EC_RES_INVALID_HEADER_CRC), \ + TRACE_SYMBOL(EC_RES_INVALID_DATA_CRC), \ + TRACE_SYMBOL(EC_RES_DUP_UNAVAILABLE) + +#define CREATE_TRACE_POINTS +#include "cros_ec_trace.h" diff --git a/drivers/platform/chrome/cros_ec_trace.h b/drivers/platform/chrome/cros_ec_trace.h new file mode 100644 index 000000000..d7e407de8 --- /dev/null +++ b/drivers/platform/chrome/cros_ec_trace.h @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Trace events for the ChromeOS Embedded Controller + * + * Copyright 2019 Google LLC. + */ + +#undef TRACE_SYSTEM +#define TRACE_SYSTEM cros_ec + +#if !defined(_CROS_EC_TRACE_H_) || defined(TRACE_HEADER_MULTI_READ) +#define _CROS_EC_TRACE_H_ + +#include +#include +#include +#include + +#include + +TRACE_EVENT(cros_ec_request_start, + TP_PROTO(struct cros_ec_command *cmd), + TP_ARGS(cmd), + TP_STRUCT__entry( + __field(uint32_t, version) + __field(uint32_t, offset) + __field(uint32_t, command) + __field(uint32_t, outsize) + __field(uint32_t, insize) + ), + TP_fast_assign( + __entry->version = cmd->version; + __entry->offset = cmd->command / EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX); + __entry->command = cmd->command % EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX); + __entry->outsize = cmd->outsize; + __entry->insize = cmd->insize; + ), + TP_printk("version: %u, offset: %d, command: %s, outsize: %u, insize: %u", + __entry->version, __entry->offset, + __print_symbolic(__entry->command, EC_CMDS), + __entry->outsize, __entry->insize) +); + +TRACE_EVENT(cros_ec_request_done, + TP_PROTO(struct cros_ec_command *cmd, int retval), + TP_ARGS(cmd, retval), + TP_STRUCT__entry( + __field(uint32_t, version) + __field(uint32_t, offset) + __field(uint32_t, command) + __field(uint32_t, outsize) + __field(uint32_t, insize) + __field(uint32_t, result) + __field(int, retval) + ), + TP_fast_assign( + __entry->version = cmd->version; + __entry->offset = cmd->command / EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX); + __entry->command = cmd->command % EC_CMD_PASSTHRU_OFFSET(CROS_EC_DEV_PD_INDEX); + __entry->outsize = cmd->outsize; + __entry->insize = cmd->insize; + __entry->result = cmd->result; + __entry->retval = retval; + ), + TP_printk("version: %u, offset: %d, command: %s, outsize: %u, insize: %u, ec result: %s, retval: %u", + __entry->version, __entry->offset, + __print_symbolic(__entry->command, EC_CMDS), + __entry->outsize, __entry->insize, + __print_symbolic(__entry->result, EC_RESULT), + __entry->retval) +); + +#endif /* _CROS_EC_TRACE_H_ */ + +/* this part must be outside header guard */ + +#undef TRACE_INCLUDE_PATH +#define TRACE_INCLUDE_PATH . + +#undef TRACE_INCLUDE_FILE +#define TRACE_INCLUDE_FILE cros_ec_trace + +#include diff --git a/drivers/platform/chrome/cros_ec_typec.c b/drivers/platform/chrome/cros_ec_typec.c new file mode 100644 index 000000000..50a68819c --- /dev/null +++ b/drivers/platform/chrome/cros_ec_typec.c @@ -0,0 +1,1474 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2020 Google LLC + * + * This driver provides the ability to view and manage Type C ports through the + * Chrome OS EC. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec_typec.h" +#include "cros_typec_vdm.h" +#include "cros_typec_altmode.h" + +#define DRV_NAME "cros-ec-typec" + +#define DP_PORT_VDO (DP_CAP_DFP_D | DP_CAP_RECEPTACLE | \ + DP_CONF_SET_PIN_ASSIGN(BIT(DP_PIN_ASSIGN_C) | \ + BIT(DP_PIN_ASSIGN_D) | \ + BIT(DP_PIN_ASSIGN_E))) + +static void cros_typec_role_switch_quirk(struct fwnode_handle *fwnode) +{ +#ifdef CONFIG_ACPI + struct fwnode_handle *switch_fwnode; + + /* Supply the USB role switch with the correct pld_crc if it's missing. */ + switch_fwnode = fwnode_find_reference(fwnode, "usb-role-switch", 0); + if (!IS_ERR_OR_NULL(switch_fwnode)) { + struct acpi_device *adev = to_acpi_device_node(switch_fwnode); + + if (adev && !adev->pld_crc) + adev->pld_crc = to_acpi_device_node(fwnode)->pld_crc; + fwnode_handle_put(switch_fwnode); + } +#endif +} + +static int cros_typec_enter_usb_mode(struct typec_port *tc_port, enum usb_mode mode) +{ + struct cros_typec_port *port = typec_get_drvdata(tc_port); + struct ec_params_typec_control req = { + .port = port->port_num, + .command = (mode == USB_MODE_USB4) ? + TYPEC_CONTROL_COMMAND_ENTER_MODE : TYPEC_CONTROL_COMMAND_EXIT_MODES, + .mode_to_enter = CROS_EC_ALTMODE_USB4 + }; + + return cros_ec_cmd(port->typec_data->ec, 0, EC_CMD_TYPEC_CONTROL, + &req, sizeof(req), NULL, 0); +} + +static int cros_typec_perform_role_swap(struct typec_port *tc_port, int target_role, u8 swap_type) +{ + struct cros_typec_port *port = typec_get_drvdata(tc_port); + struct cros_typec_data *data = port->typec_data; + struct ec_response_usb_pd_control_v2 resp; + struct ec_params_usb_pd_control req; + int role, ret; + + /* Must be at least v1 to support role swap. */ + if (!data->pd_ctrl_ver) + return -EOPNOTSUPP; + + /* First query the state */ + req.port = port->port_num; + req.role = USB_PD_CTRL_ROLE_NO_CHANGE; + req.mux = USB_PD_CTRL_MUX_NO_CHANGE; + req.swap = USB_PD_CTRL_SWAP_NONE; + + ret = cros_ec_cmd(data->ec, data->pd_ctrl_ver, EC_CMD_USB_PD_CONTROL, + &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + switch (swap_type) { + case USB_PD_CTRL_SWAP_DATA: + role = (resp.role & PD_CTRL_RESP_ROLE_DATA) ? TYPEC_HOST : + TYPEC_DEVICE; + break; + case USB_PD_CTRL_SWAP_POWER: + role = (resp.role & PD_CTRL_RESP_ROLE_POWER) ? TYPEC_SOURCE : + TYPEC_SINK; + break; + default: + dev_warn(data->dev, "Unsupported role swap type %d\n", swap_type); + return -EOPNOTSUPP; + } + + if (role == target_role) + return 0; + + req.swap = swap_type; + ret = cros_ec_cmd(data->ec, data->pd_ctrl_ver, EC_CMD_USB_PD_CONTROL, + &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + switch (swap_type) { + case USB_PD_CTRL_SWAP_DATA: + role = resp.role & PD_CTRL_RESP_ROLE_DATA ? TYPEC_HOST : TYPEC_DEVICE; + if (role != target_role) { + dev_err(data->dev, "Data role swap failed despite EC returning success\n"); + return -EIO; + } + typec_set_data_role(tc_port, target_role); + break; + case USB_PD_CTRL_SWAP_POWER: + role = resp.role & PD_CTRL_RESP_ROLE_POWER ? TYPEC_SOURCE : TYPEC_SINK; + if (role != target_role) { + dev_err(data->dev, "Power role swap failed despite EC returning success\n"); + return -EIO; + } + typec_set_pwr_role(tc_port, target_role); + break; + default: + /* Should never execute */ + break; + } + + return 0; +} + +static int cros_typec_dr_swap(struct typec_port *port, enum typec_data_role role) +{ + return cros_typec_perform_role_swap(port, role, USB_PD_CTRL_SWAP_DATA); +} + +static int cros_typec_pr_swap(struct typec_port *port, enum typec_role role) +{ + return cros_typec_perform_role_swap(port, role, USB_PD_CTRL_SWAP_POWER); +} + +static const struct typec_operations cros_typec_usb_mode_ops = { + .enter_usb_mode = cros_typec_enter_usb_mode, + .dr_set = cros_typec_dr_swap, + .pr_set = cros_typec_pr_swap, +}; + +static int cros_typec_parse_port_props(struct typec_capability *cap, + struct fwnode_handle *fwnode, + struct device *dev) +{ + const char *buf; + int ret; + + memset(cap, 0, sizeof(*cap)); + ret = fwnode_property_read_string(fwnode, "power-role", &buf); + if (ret) { + dev_err(dev, "power-role not found: %d\n", ret); + return ret; + } + + ret = typec_find_port_power_role(buf); + if (ret < 0) + return ret; + cap->type = ret; + + ret = fwnode_property_read_string(fwnode, "data-role", &buf); + if (ret) { + dev_err(dev, "data-role not found: %d\n", ret); + return ret; + } + + ret = typec_find_port_data_role(buf); + if (ret < 0) + return ret; + cap->data = ret; + + /* Try-power-role is optional. */ + ret = fwnode_property_read_string(fwnode, "try-power-role", &buf); + if (ret) { + dev_warn(dev, "try-power-role not found: %d\n", ret); + cap->prefer_role = TYPEC_NO_PREFERRED_ROLE; + } else { + ret = typec_find_power_role(buf); + if (ret < 0) + return ret; + cap->prefer_role = ret; + } + + if (fwnode_property_present(fwnode, "usb2-port")) + cap->usb_capability |= USB_CAPABILITY_USB2; + if (fwnode_property_present(fwnode, "usb3-port")) + cap->usb_capability |= USB_CAPABILITY_USB3; + if (fwnode_property_present(fwnode, "usb4-port")) + cap->usb_capability |= USB_CAPABILITY_USB4; + + cros_typec_role_switch_quirk(fwnode); + + cap->fwnode = fwnode; + + return 0; +} + +static int cros_typec_get_switch_handles(struct cros_typec_port *port, + struct fwnode_handle *fwnode, + struct device *dev) +{ + int ret = 0; + + port->mux = fwnode_typec_mux_get(fwnode); + if (IS_ERR(port->mux)) { + ret = PTR_ERR(port->mux); + dev_err_probe(dev, ret, "Mux handle not found\n"); + goto mux_err; + } + + port->retimer = fwnode_typec_retimer_get(fwnode); + if (IS_ERR(port->retimer)) { + ret = PTR_ERR(port->retimer); + dev_err_probe(dev, ret, "Retimer handle not found\n"); + goto retimer_sw_err; + } + + port->ori_sw = fwnode_typec_switch_get(fwnode); + if (IS_ERR(port->ori_sw)) { + ret = PTR_ERR(port->ori_sw); + dev_err_probe(dev, ret, "Orientation switch handle not found\n"); + goto ori_sw_err; + } + + port->role_sw = fwnode_usb_role_switch_get(fwnode); + if (IS_ERR(port->role_sw)) { + ret = PTR_ERR(port->role_sw); + dev_err_probe(dev, ret, "USB role switch handle not found\n"); + goto role_sw_err; + } + + return 0; + +role_sw_err: + typec_switch_put(port->ori_sw); + port->ori_sw = NULL; +ori_sw_err: + typec_retimer_put(port->retimer); + port->retimer = NULL; +retimer_sw_err: + typec_mux_put(port->mux); + port->mux = NULL; +mux_err: + return ret; +} + +static int cros_typec_add_partner(struct cros_typec_data *typec, int port_num, + bool pd_en) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct typec_partner_desc p_desc = { + .usb_pd = pd_en, + }; + int ret = 0; + + /* + * Fill an initial PD identity, which will then be updated with info + * from the EC. + */ + p_desc.identity = &port->p_identity; + + port->partner = typec_register_partner(port->port, &p_desc); + if (IS_ERR(port->partner)) { + ret = PTR_ERR(port->partner); + port->partner = NULL; + } + + return ret; +} + +static void cros_typec_unregister_altmodes(struct cros_typec_data *typec, int port_num, + bool is_partner) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct cros_typec_altmode_node *node, *tmp; + struct list_head *head; + + head = is_partner ? &port->partner_mode_list : &port->plug_mode_list; + list_for_each_entry_safe(node, tmp, head, list) { + list_del(&node->list); + typec_unregister_altmode(node->amode); + devm_kfree(typec->dev, node); + } +} + +/* + * Map the Type-C Mux state to retimer state and call the retimer set function. We need this + * because we re-use the Type-C mux state for retimers. + */ +static int cros_typec_retimer_set(struct typec_retimer *retimer, struct typec_mux_state state) +{ + struct typec_retimer_state rstate = { + .alt = state.alt, + .mode = state.mode, + .data = state.data, + }; + + return typec_retimer_set(retimer, &rstate); +} + +static int cros_typec_usb_disconnect_state(struct cros_typec_port *port) +{ + port->state.alt = NULL; + port->state.mode = TYPEC_STATE_USB; + port->state.data = NULL; + + usb_role_switch_set_role(port->role_sw, USB_ROLE_NONE); + typec_switch_set(port->ori_sw, TYPEC_ORIENTATION_NONE); + cros_typec_retimer_set(port->retimer, port->state); + + return typec_mux_set(port->mux, &port->state); +} + +static void cros_typec_remove_partner(struct cros_typec_data *typec, + int port_num) +{ + struct cros_typec_port *port = typec->ports[port_num]; + + if (!port->partner) + return; + + cros_typec_unregister_altmodes(typec, port_num, true); + + typec_partner_set_usb_power_delivery(port->partner, NULL); + usb_power_delivery_unregister_capabilities(port->partner_sink_caps); + port->partner_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(port->partner_src_caps); + port->partner_src_caps = NULL; + usb_power_delivery_unregister(port->partner_pd); + port->partner_pd = NULL; + + cros_typec_usb_disconnect_state(port); + port->mux_flags = USB_PD_MUX_NONE; + + typec_unregister_partner(port->partner); + port->partner = NULL; + memset(&port->p_identity, 0, sizeof(port->p_identity)); + port->sop_disc_done = false; +} + +static void cros_typec_remove_cable(struct cros_typec_data *typec, + int port_num) +{ + struct cros_typec_port *port = typec->ports[port_num]; + + if (!port->cable) + return; + + cros_typec_unregister_altmodes(typec, port_num, false); + + typec_unregister_plug(port->plug); + port->plug = NULL; + typec_unregister_cable(port->cable); + port->cable = NULL; + memset(&port->c_identity, 0, sizeof(port->c_identity)); + port->sop_prime_disc_done = false; +} + +static void cros_typec_unregister_port_altmodes(struct cros_typec_port *port) +{ + int i; + + for (i = 0; i < CROS_EC_ALTMODE_MAX; i++) + typec_unregister_altmode(port->port_altmode[i]); +} + +static void cros_unregister_ports(struct cros_typec_data *typec) +{ + int i; + + for (i = 0; i < typec->num_ports; i++) { + if (!typec->ports[i]) + continue; + + cros_typec_remove_partner(typec, i); + cros_typec_remove_cable(typec, i); + + usb_role_switch_put(typec->ports[i]->role_sw); + typec_switch_put(typec->ports[i]->ori_sw); + typec_mux_put(typec->ports[i]->mux); + cros_typec_unregister_port_altmodes(typec->ports[i]); + typec_unregister_port(typec->ports[i]->port); + } +} + +/* + * Register port alt modes with known values till we start retrieving + * port capabilities from the EC. + */ +static int cros_typec_register_port_altmodes(struct cros_typec_data *typec, + int port_num) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct typec_altmode_desc desc; + struct typec_altmode *amode; + + /* All PD capable CrOS devices are assumed to support DP altmode. */ + memset(&desc, 0, sizeof(desc)); + desc.svid = USB_TYPEC_DP_SID; + desc.mode = USB_TYPEC_DP_MODE; + desc.vdo = DP_PORT_VDO; + amode = cros_typec_register_displayport(port, &desc, + typec->ap_driven_altmode); + if (IS_ERR(amode)) + return PTR_ERR(amode); + port->port_altmode[CROS_EC_ALTMODE_DP] = amode; + + /* + * Register TBT compatibility alt mode. The EC will not enter the mode + * if it doesn't support it and it will not enter automatically by + * design so we can use the |ap_driven_altmode| feature to check if we + * should register it. + */ + if (typec->ap_driven_altmode) { + memset(&desc, 0, sizeof(desc)); + desc.svid = USB_TYPEC_TBT_SID; + desc.mode = TBT_MODE; + desc.inactive = true; + amode = cros_typec_register_thunderbolt(port, &desc); + if (IS_ERR(amode)) + return PTR_ERR(amode); + port->port_altmode[CROS_EC_ALTMODE_TBT] = amode; + } + + port->state.alt = NULL; + port->state.mode = TYPEC_STATE_USB; + port->state.data = NULL; + + return 0; +} + +static int cros_typec_init_ports(struct cros_typec_data *typec) +{ + struct device *dev = typec->dev; + struct typec_capability *cap; + struct fwnode_handle *fwnode; + struct cros_typec_port *cros_port; + const char *port_prop; + int ret; + int nports; + u32 port_num = 0; + + nports = device_get_child_node_count(dev); + if (nports == 0) { + dev_err(dev, "No port entries found.\n"); + return -ENODEV; + } + + if (nports > typec->num_ports) { + dev_err(dev, "More ports listed than can be supported.\n"); + return -EINVAL; + } + + /* DT uses "reg" to specify port number. */ + port_prop = dev->of_node ? "reg" : "port-number"; + device_for_each_child_node(dev, fwnode) { + if (fwnode_property_read_u32(fwnode, port_prop, &port_num)) { + ret = -EINVAL; + dev_err(dev, "No port-number for port, aborting.\n"); + goto unregister_ports; + } + + if (port_num >= typec->num_ports) { + dev_err(dev, "Invalid port number.\n"); + ret = -EINVAL; + goto unregister_ports; + } + + dev_dbg(dev, "Registering port %d\n", port_num); + + cros_port = devm_kzalloc(dev, sizeof(*cros_port), GFP_KERNEL); + if (!cros_port) { + ret = -ENOMEM; + goto unregister_ports; + } + + cros_port->port_num = port_num; + cros_port->typec_data = typec; + typec->ports[port_num] = cros_port; + cap = &cros_port->caps; + + ret = cros_typec_parse_port_props(cap, fwnode, dev); + if (ret < 0) + goto unregister_ports; + + cap->driver_data = cros_port; + cap->ops = &cros_typec_usb_mode_ops; + cap->no_mode_control = !typec->ap_driven_altmode; + + cros_port->port = typec_register_port(dev, cap); + if (IS_ERR(cros_port->port)) { + ret = PTR_ERR(cros_port->port); + dev_err_probe(dev, ret, "Failed to register port %d\n", port_num); + goto unregister_ports; + } + + ret = cros_typec_get_switch_handles(cros_port, fwnode, dev); + if (ret) { + dev_dbg(dev, "No switch control for port %d, err: %d\n", port_num, ret); + if (ret == -EPROBE_DEFER) + goto unregister_ports; + } + + ret = cros_typec_register_port_altmodes(typec, port_num); + if (ret) { + dev_err(dev, "Failed to register port altmodes\n"); + goto unregister_ports; + } + + cros_port->disc_data = devm_kzalloc(dev, EC_PROTO2_MAX_RESPONSE_SIZE, GFP_KERNEL); + if (!cros_port->disc_data) { + ret = -ENOMEM; + goto unregister_ports; + } + + INIT_LIST_HEAD(&cros_port->partner_mode_list); + INIT_LIST_HEAD(&cros_port->plug_mode_list); + } + + return 0; + +unregister_ports: + fwnode_handle_put(fwnode); + cros_unregister_ports(typec); + return ret; +} + +static int cros_typec_usb_safe_state(struct cros_typec_port *port) +{ + int ret; + port->state.mode = TYPEC_STATE_SAFE; + + ret = cros_typec_retimer_set(port->retimer, port->state); + if (!ret) + ret = typec_mux_set(port->mux, &port->state); + + return ret; +} + +/** + * cros_typec_get_cable_vdo() - Get Cable VDO of the connected cable + * @port: Type-C port data + * @svid: Standard or Vendor ID to match + * + * Returns the Cable VDO if match is found and returns 0 if match is not found. + */ +static int cros_typec_get_cable_vdo(struct cros_typec_port *port, u16 svid) +{ + struct list_head *head = &port->plug_mode_list; + struct cros_typec_altmode_node *node; + u32 ret = 0; + + list_for_each_entry(node, head, list) { + if (node->amode->svid == svid) + return node->amode->vdo; + } + + return ret; +} + +/* + * Spoof the VDOs that were likely communicated by the partner for TBT alt + * mode. + */ +static int cros_typec_enable_tbt(struct cros_typec_data *typec, + int port_num, + struct ec_response_usb_pd_control_v2 *pd_ctrl) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct typec_thunderbolt_data data; + int ret; + + if (typec->pd_ctrl_ver < 2) { + dev_err(typec->dev, + "PD_CTRL version too old: %d\n", typec->pd_ctrl_ver); + return -ENOTSUPP; + } + + /* Device Discover Mode VDO */ + data.device_mode = TBT_MODE; + + if (pd_ctrl->control_flags & USB_PD_CTRL_TBT_LEGACY_ADAPTER) + data.device_mode = TBT_SET_ADAPTER(TBT_ADAPTER_TBT3); + + /* Cable Discover Mode VDO */ + data.cable_mode = TBT_MODE; + + data.cable_mode |= cros_typec_get_cable_vdo(port, USB_TYPEC_TBT_SID); + + data.cable_mode |= TBT_SET_CABLE_SPEED(pd_ctrl->cable_speed); + + if (pd_ctrl->control_flags & USB_PD_CTRL_OPTICAL_CABLE) + data.cable_mode |= TBT_CABLE_OPTICAL; + + if (pd_ctrl->control_flags & USB_PD_CTRL_ACTIVE_LINK_UNIDIR) + data.cable_mode |= TBT_CABLE_LINK_TRAINING; + + data.cable_mode |= TBT_SET_CABLE_ROUNDED(pd_ctrl->cable_gen); + + /* Enter Mode VDO */ + data.enter_vdo = TBT_SET_CABLE_SPEED(pd_ctrl->cable_speed); + + if (pd_ctrl->control_flags & USB_PD_CTRL_ACTIVE_CABLE) + data.enter_vdo |= TBT_ENTER_MODE_ACTIVE_CABLE; + + if (!port->state.alt) { + port->state.alt = port->port_altmode[CROS_EC_ALTMODE_TBT]; + ret = cros_typec_usb_safe_state(port); + if (ret) + return ret; + } + + port->state.data = &data; + port->state.mode = TYPEC_TBT_MODE; + + return typec_mux_set(port->mux, &port->state); +} + +/* Spoof the VDOs that were likely communicated by the partner. */ +static int cros_typec_enable_dp(struct cros_typec_data *typec, + int port_num, + struct ec_response_usb_pd_control_v2 *pd_ctrl) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct typec_displayport_data dp_data; + u32 cable_tbt_vdo; + u32 cable_dp_vdo; + int ret; + + if (typec->pd_ctrl_ver < 2) { + dev_err(typec->dev, + "PD_CTRL version too old: %d\n", typec->pd_ctrl_ver); + return -ENOTSUPP; + } + + if (!pd_ctrl->dp_mode) { + dev_err(typec->dev, "No valid DP mode provided.\n"); + return -EINVAL; + } + + /* Status VDO. */ + dp_data.status = DP_STATUS_ENABLED; + if (port->mux_flags & USB_PD_MUX_HPD_IRQ) + dp_data.status |= DP_STATUS_IRQ_HPD; + if (port->mux_flags & USB_PD_MUX_HPD_LVL) + dp_data.status |= DP_STATUS_HPD_STATE; + + /* Configuration VDO. */ + dp_data.conf = DP_CONF_SET_PIN_ASSIGN(pd_ctrl->dp_mode); + if (!port->state.alt) { + port->state.alt = port->port_altmode[CROS_EC_ALTMODE_DP]; + ret = cros_typec_usb_safe_state(port); + if (ret) + return ret; + } + + port->state.data = &dp_data; + port->state.mode = TYPEC_MODAL_STATE(ffs(pd_ctrl->dp_mode)); + + /* Get cable VDO for cables with DPSID to check DPAM2.1 is supported */ + cable_dp_vdo = cros_typec_get_cable_vdo(port, USB_TYPEC_DP_SID); + + /** + * Get cable VDO for thunderbolt cables and cables with DPSID but does not + * support DPAM2.1. + */ + cable_tbt_vdo = cros_typec_get_cable_vdo(port, USB_TYPEC_TBT_SID); + + if (cable_dp_vdo & DP_CAP_DPAM_VERSION) { + dp_data.conf |= cable_dp_vdo; + } else if (cable_tbt_vdo) { + dp_data.conf |= TBT_CABLE_SPEED(cable_tbt_vdo) << DP_CONF_SIGNALLING_SHIFT; + + /* Cable Type */ + if (cable_tbt_vdo & TBT_CABLE_OPTICAL) + dp_data.conf |= DP_CONF_CABLE_TYPE_OPTICAL << DP_CONF_CABLE_TYPE_SHIFT; + else if (cable_tbt_vdo & TBT_CABLE_RETIMER) + dp_data.conf |= DP_CONF_CABLE_TYPE_RE_TIMER << DP_CONF_CABLE_TYPE_SHIFT; + else if (cable_tbt_vdo & TBT_CABLE_ACTIVE_PASSIVE) + dp_data.conf |= DP_CONF_CABLE_TYPE_RE_DRIVER << DP_CONF_CABLE_TYPE_SHIFT; + } else if (PD_IDH_PTYPE(port->c_identity.id_header) == IDH_PTYPE_PCABLE) { + dp_data.conf |= VDO_TYPEC_CABLE_SPEED(port->c_identity.vdo[0]) << + DP_CONF_SIGNALLING_SHIFT; + } + + ret = cros_typec_retimer_set(port->retimer, port->state); + if (!ret) + ret = typec_mux_set(port->mux, &port->state); + + if (!ret) + ret = cros_typec_displayport_status_update(port->state.alt, + port->state.data); + + return ret; +} + +static int cros_typec_enable_usb4(struct cros_typec_data *typec, + int port_num, + struct ec_response_usb_pd_control_v2 *pd_ctrl) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct enter_usb_data data; + + data.eudo = EUDO_USB_MODE_USB4 << EUDO_USB_MODE_SHIFT; + + /* Cable Speed */ + data.eudo |= pd_ctrl->cable_speed << EUDO_CABLE_SPEED_SHIFT; + + /* Cable Type */ + if (pd_ctrl->control_flags & USB_PD_CTRL_OPTICAL_CABLE) + data.eudo |= EUDO_CABLE_TYPE_OPTICAL << EUDO_CABLE_TYPE_SHIFT; + else if (cros_typec_get_cable_vdo(port, USB_TYPEC_TBT_SID) & TBT_CABLE_RETIMER) + data.eudo |= EUDO_CABLE_TYPE_RE_TIMER << EUDO_CABLE_TYPE_SHIFT; + else if (pd_ctrl->control_flags & USB_PD_CTRL_ACTIVE_CABLE) + data.eudo |= EUDO_CABLE_TYPE_RE_DRIVER << EUDO_CABLE_TYPE_SHIFT; + + data.active_link_training = !!(pd_ctrl->control_flags & + USB_PD_CTRL_ACTIVE_LINK_UNIDIR); + + port->state.alt = NULL; + port->state.data = &data; + port->state.mode = TYPEC_MODE_USB4; + + return typec_mux_set(port->mux, &port->state); +} + +static int cros_typec_configure_mux(struct cros_typec_data *typec, int port_num, + struct ec_response_usb_pd_control_v2 *pd_ctrl) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct ec_response_usb_pd_mux_info resp; + struct ec_params_usb_pd_mux_info req = { + .port = port_num, + }; + struct ec_params_usb_pd_mux_ack mux_ack; + enum typec_orientation orientation; + struct cros_typec_altmode_node *node; + int ret; + + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_USB_PD_MUX_INFO, + &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) { + dev_warn(typec->dev, "Failed to get mux info for port: %d, err = %d\n", + port_num, ret); + return ret; + } + + /* No change needs to be made, let's exit early. */ + if (port->mux_flags == resp.flags && port->role == pd_ctrl->role) + return 0; + + port->mux_flags = resp.flags; + port->role = pd_ctrl->role; + + if (port->mux_flags == USB_PD_MUX_NONE) { + ret = cros_typec_usb_disconnect_state(port); + goto mux_ack; + } + + if (port->mux_flags & USB_PD_MUX_POLARITY_INVERTED) + orientation = TYPEC_ORIENTATION_REVERSE; + else + orientation = TYPEC_ORIENTATION_NORMAL; + + ret = typec_switch_set(port->ori_sw, orientation); + if (ret) + return ret; + + ret = usb_role_switch_set_role(typec->ports[port_num]->role_sw, + pd_ctrl->role & PD_CTRL_RESP_ROLE_DATA + ? USB_ROLE_HOST : USB_ROLE_DEVICE); + if (ret) + return ret; + + if (port->mux_flags & USB_PD_MUX_USB4_ENABLED) { + ret = cros_typec_enable_usb4(typec, port_num, pd_ctrl); + } else if (port->mux_flags & USB_PD_MUX_TBT_COMPAT_ENABLED) { + ret = cros_typec_enable_tbt(typec, port_num, pd_ctrl); + } else if (port->mux_flags & USB_PD_MUX_DP_ENABLED) { + ret = cros_typec_enable_dp(typec, port_num, pd_ctrl); + } else if (port->mux_flags & USB_PD_MUX_SAFE_MODE) { + ret = cros_typec_usb_safe_state(port); + } else if (port->mux_flags & USB_PD_MUX_USB_ENABLED) { + port->state.alt = NULL; + port->state.mode = TYPEC_STATE_USB; + + ret = cros_typec_retimer_set(port->retimer, port->state); + if (!ret) + ret = typec_mux_set(port->mux, &port->state); + } else { + dev_dbg(typec->dev, + "Unrecognized mode requested, mux flags: %x\n", + port->mux_flags); + } + + /* Iterate all partner alt-modes and set the active alternate mode. */ + list_for_each_entry(node, &port->partner_mode_list, list) { + typec_altmode_update_active( + node->amode, + port->state.alt && + node->amode->svid == port->state.alt->svid); + } + +mux_ack: + if (!typec->needs_mux_ack) + return ret; + + /* Sending Acknowledgment to EC */ + mux_ack.port = port_num; + + if (cros_ec_cmd(typec->ec, 0, EC_CMD_USB_PD_MUX_ACK, &mux_ack, + sizeof(mux_ack), NULL, 0) < 0) + dev_warn(typec->dev, + "Failed to send Mux ACK to EC for port: %d\n", + port_num); + + return ret; +} + +static void cros_typec_set_port_params_v0(struct cros_typec_data *typec, + int port_num, struct ec_response_usb_pd_control *resp) +{ + struct typec_port *port = typec->ports[port_num]->port; + enum typec_orientation polarity; + + if (!resp->enabled) + polarity = TYPEC_ORIENTATION_NONE; + else if (!resp->polarity) + polarity = TYPEC_ORIENTATION_NORMAL; + else + polarity = TYPEC_ORIENTATION_REVERSE; + + typec_set_pwr_role(port, resp->role ? TYPEC_SOURCE : TYPEC_SINK); + typec_set_orientation(port, polarity); +} + +static void cros_typec_set_port_params_v1(struct cros_typec_data *typec, + int port_num, struct ec_response_usb_pd_control_v1 *resp) +{ + struct typec_port *port = typec->ports[port_num]->port; + enum typec_orientation polarity; + bool pd_en; + int ret; + + if (!(resp->enabled & PD_CTRL_RESP_ENABLED_CONNECTED)) + polarity = TYPEC_ORIENTATION_NONE; + else if (!resp->polarity) + polarity = TYPEC_ORIENTATION_NORMAL; + else + polarity = TYPEC_ORIENTATION_REVERSE; + typec_set_orientation(port, polarity); + typec_set_data_role(port, resp->role & PD_CTRL_RESP_ROLE_DATA ? + TYPEC_HOST : TYPEC_DEVICE); + typec_set_pwr_role(port, resp->role & PD_CTRL_RESP_ROLE_POWER ? + TYPEC_SOURCE : TYPEC_SINK); + typec_set_vconn_role(port, resp->role & PD_CTRL_RESP_ROLE_VCONN ? + TYPEC_SOURCE : TYPEC_SINK); + + /* Register/remove partners when a connect/disconnect occurs. */ + if (resp->enabled & PD_CTRL_RESP_ENABLED_CONNECTED) { + if (typec->ports[port_num]->partner) + return; + + pd_en = resp->enabled & PD_CTRL_RESP_ENABLED_PD_CAPABLE; + ret = cros_typec_add_partner(typec, port_num, pd_en); + if (ret) + dev_warn(typec->dev, + "Failed to register partner on port: %d\n", + port_num); + } else { + cros_typec_remove_partner(typec, port_num); + cros_typec_remove_cable(typec, port_num); + } +} + +/* + * Helper function to register partner/plug altmodes. + */ +static int cros_typec_register_altmodes(struct cros_typec_data *typec, int port_num, + bool is_partner) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct ec_response_typec_discovery *sop_disc = port->disc_data; + struct cros_typec_altmode_node *node; + struct typec_altmode_desc desc; + struct typec_altmode *amode; + int num_altmodes = 0; + int ret = 0; + int i, j; + + for (i = 0; i < sop_disc->svid_count; i++) { + for (j = 0; j < sop_disc->svids[i].mode_count; j++) { + memset(&desc, 0, sizeof(desc)); + desc.svid = sop_disc->svids[i].svid; + desc.mode = j + 1; + desc.vdo = sop_disc->svids[i].mode_vdo[j]; + + if (is_partner) + amode = typec_partner_register_altmode(port->partner, &desc); + else + amode = typec_plug_register_altmode(port->plug, &desc); + + if (IS_ERR(amode)) { + ret = PTR_ERR(amode); + goto err_cleanup; + } + + /* If no memory is available we should unregister and exit. */ + node = devm_kzalloc(typec->dev, sizeof(*node), GFP_KERNEL); + if (!node) { + ret = -ENOMEM; + typec_unregister_altmode(amode); + goto err_cleanup; + } + + node->amode = amode; + + if (is_partner) + list_add_tail(&node->list, &port->partner_mode_list); + else + list_add_tail(&node->list, &port->plug_mode_list); + num_altmodes++; + } + } + + if (is_partner) + ret = typec_partner_set_num_altmodes(port->partner, num_altmodes); + else + ret = typec_plug_set_num_altmodes(port->plug, num_altmodes); + + if (ret < 0) { + dev_err(typec->dev, "Unable to set %s num_altmodes for port: %d\n", + is_partner ? "partner" : "plug", port_num); + goto err_cleanup; + } + + return 0; + +err_cleanup: + cros_typec_unregister_altmodes(typec, port_num, is_partner); + return ret; +} + +/* + * Parse the PD identity data from the EC PD discovery responses and copy that to the supplied + * PD identity struct. + */ +static void cros_typec_parse_pd_identity(struct usb_pd_identity *id, + struct ec_response_typec_discovery *disc) +{ + int i; + + /* First, update the PD identity VDOs for the partner. */ + if (disc->identity_count > 0) + id->id_header = disc->discovery_vdo[0]; + if (disc->identity_count > 1) + id->cert_stat = disc->discovery_vdo[1]; + if (disc->identity_count > 2) + id->product = disc->discovery_vdo[2]; + + /* Copy the remaining identity VDOs till a maximum of 6. */ + for (i = 3; i < disc->identity_count && i < VDO_MAX_OBJECTS; i++) + id->vdo[i - 3] = disc->discovery_vdo[i]; +} + +static int cros_typec_handle_sop_prime_disc(struct cros_typec_data *typec, int port_num, u16 pd_revision) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct ec_response_typec_discovery *disc = port->disc_data; + struct typec_cable_desc c_desc = {}; + struct typec_plug_desc p_desc; + struct ec_params_typec_discovery req = { + .port = port_num, + .partner_type = TYPEC_PARTNER_SOP_PRIME, + }; + u32 cable_plug_type; + int ret = 0; + + memset(disc, 0, EC_PROTO2_MAX_RESPONSE_SIZE); + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_DISCOVERY, &req, sizeof(req), + disc, EC_PROTO2_MAX_RESPONSE_SIZE); + if (ret < 0) { + dev_err(typec->dev, "Failed to get SOP' discovery data for port: %d\n", port_num); + goto sop_prime_disc_exit; + } + + /* Parse the PD identity data, even if only 0s were returned. */ + cros_typec_parse_pd_identity(&port->c_identity, disc); + + if (disc->identity_count != 0) { + cable_plug_type = VDO_TYPEC_CABLE_TYPE(port->c_identity.vdo[0]); + switch (cable_plug_type) { + case CABLE_ATYPE: + c_desc.type = USB_PLUG_TYPE_A; + break; + case CABLE_BTYPE: + c_desc.type = USB_PLUG_TYPE_B; + break; + case CABLE_CTYPE: + c_desc.type = USB_PLUG_TYPE_C; + break; + case CABLE_CAPTIVE: + c_desc.type = USB_PLUG_CAPTIVE; + break; + default: + c_desc.type = USB_PLUG_NONE; + } + c_desc.active = PD_IDH_PTYPE(port->c_identity.id_header) == IDH_PTYPE_ACABLE; + } + + c_desc.identity = &port->c_identity; + c_desc.pd_revision = pd_revision; + + port->cable = typec_register_cable(port->port, &c_desc); + if (IS_ERR(port->cable)) { + ret = PTR_ERR(port->cable); + port->cable = NULL; + goto sop_prime_disc_exit; + } + + p_desc.index = TYPEC_PLUG_SOP_P; + port->plug = typec_register_plug(port->cable, &p_desc); + if (IS_ERR(port->plug)) { + ret = PTR_ERR(port->plug); + port->plug = NULL; + goto sop_prime_disc_exit; + } + + ret = cros_typec_register_altmodes(typec, port_num, false); + if (ret < 0) { + dev_err(typec->dev, "Failed to register plug altmodes, port: %d\n", port_num); + goto sop_prime_disc_exit; + } + + return 0; + +sop_prime_disc_exit: + cros_typec_remove_cable(typec, port_num); + return ret; +} + +static int cros_typec_handle_sop_disc(struct cros_typec_data *typec, int port_num, u16 pd_revision) +{ + struct cros_typec_port *port = typec->ports[port_num]; + struct ec_response_typec_discovery *sop_disc = port->disc_data; + struct ec_params_typec_discovery req = { + .port = port_num, + .partner_type = TYPEC_PARTNER_SOP, + }; + int ret = 0; + + if (!port->partner) { + dev_err(typec->dev, + "SOP Discovery received without partner registered, port: %d\n", + port_num); + ret = -EINVAL; + goto disc_exit; + } + + typec_partner_set_pd_revision(port->partner, pd_revision); + + memset(sop_disc, 0, EC_PROTO2_MAX_RESPONSE_SIZE); + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_DISCOVERY, &req, sizeof(req), + sop_disc, EC_PROTO2_MAX_RESPONSE_SIZE); + if (ret < 0) { + dev_err(typec->dev, "Failed to get SOP discovery data for port: %d\n", port_num); + goto disc_exit; + } + + cros_typec_parse_pd_identity(&port->p_identity, sop_disc); + + ret = typec_partner_set_identity(port->partner); + if (ret < 0) { + dev_err(typec->dev, "Failed to update partner PD identity, port: %d\n", port_num); + goto disc_exit; + } + + ret = cros_typec_register_altmodes(typec, port_num, true); + if (ret < 0) { + dev_err(typec->dev, "Failed to register partner altmodes, port: %d\n", port_num); + goto disc_exit; + } + +disc_exit: + return ret; +} + +static int cros_typec_send_clear_event(struct cros_typec_data *typec, int port_num, u32 events_mask) +{ + struct ec_params_typec_control req = { + .port = port_num, + .command = TYPEC_CONTROL_COMMAND_CLEAR_EVENTS, + .clear_events_mask = events_mask, + }; + + return cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_CONTROL, &req, + sizeof(req), NULL, 0); +} + +static void cros_typec_register_partner_pdos(struct cros_typec_data *typec, + struct ec_response_typec_status *resp, int port_num) +{ + struct usb_power_delivery_capabilities_desc caps_desc = {}; + struct usb_power_delivery_desc desc = { + .revision = (le16_to_cpu(resp->sop_revision) & 0xff00) >> 4, + }; + struct cros_typec_port *port = typec->ports[port_num]; + + if (!port->partner || port->partner_pd) + return; + + /* If no caps are available, don't bother creating a device. */ + if (!resp->source_cap_count && !resp->sink_cap_count) + return; + + if (resp->source_cap_count > PDO_MAX_OBJECTS || + resp->sink_cap_count > PDO_MAX_OBJECTS) { + dev_warn(typec->dev, "Invalid PDO count from EC, port: %d\n", port_num); + return; + } + + port->partner_pd = typec_partner_usb_power_delivery_register(port->partner, &desc); + if (IS_ERR(port->partner_pd)) { + dev_warn(typec->dev, "Failed to register partner PD device, port: %d\n", port_num); + return; + } + + typec_partner_set_usb_power_delivery(port->partner, port->partner_pd); + + memcpy(caps_desc.pdo, resp->source_cap_pdos, sizeof(u32) * resp->source_cap_count); + caps_desc.role = TYPEC_SOURCE; + port->partner_src_caps = usb_power_delivery_register_capabilities(port->partner_pd, + &caps_desc); + if (IS_ERR(port->partner_src_caps)) + dev_warn(typec->dev, "Failed to register source caps, port: %d\n", port_num); + + memset(&caps_desc, 0, sizeof(caps_desc)); + memcpy(caps_desc.pdo, resp->sink_cap_pdos, sizeof(u32) * resp->sink_cap_count); + caps_desc.role = TYPEC_SINK; + port->partner_sink_caps = usb_power_delivery_register_capabilities(port->partner_pd, + &caps_desc); + if (IS_ERR(port->partner_sink_caps)) + dev_warn(typec->dev, "Failed to register sink caps, port: %d\n", port_num); +} + +static void cros_typec_handle_status(struct cros_typec_data *typec, int port_num) +{ + struct ec_response_typec_status resp; + struct ec_params_typec_status req = { + .port = port_num, + }; + int ret; + + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_STATUS, &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) { + dev_warn(typec->dev, "EC_CMD_TYPEC_STATUS failed for port: %d\n", port_num); + return; + } + + /* If we got a hard reset, unregister everything and return. */ + if (resp.events & PD_STATUS_EVENT_HARD_RESET) { + cros_typec_remove_partner(typec, port_num); + cros_typec_remove_cable(typec, port_num); + + ret = cros_typec_send_clear_event(typec, port_num, + PD_STATUS_EVENT_HARD_RESET); + if (ret < 0) + dev_warn(typec->dev, + "Failed hard reset event clear, port: %d\n", port_num); + return; + } + + /* Handle any events appropriately. */ + if (resp.events & PD_STATUS_EVENT_SOP_DISC_DONE && !typec->ports[port_num]->sop_disc_done) { + u16 sop_revision; + + /* Convert BCD to the format preferred by the TypeC framework */ + sop_revision = (le16_to_cpu(resp.sop_revision) & 0xff00) >> 4; + ret = cros_typec_handle_sop_disc(typec, port_num, sop_revision); + if (ret < 0) + dev_err(typec->dev, "Couldn't parse SOP Disc data, port: %d\n", port_num); + else { + typec->ports[port_num]->sop_disc_done = true; + ret = cros_typec_send_clear_event(typec, port_num, + PD_STATUS_EVENT_SOP_DISC_DONE); + if (ret < 0) + dev_warn(typec->dev, + "Failed SOP Disc event clear, port: %d\n", port_num); + } + if (resp.sop_connected) + typec_set_pwr_opmode(typec->ports[port_num]->port, TYPEC_PWR_MODE_PD); + + cros_typec_register_partner_pdos(typec, &resp, port_num); + } + + if (resp.events & PD_STATUS_EVENT_SOP_PRIME_DISC_DONE && + !typec->ports[port_num]->sop_prime_disc_done) { + u16 sop_prime_revision; + + /* Convert BCD to the format preferred by the TypeC framework */ + sop_prime_revision = (le16_to_cpu(resp.sop_prime_revision) & 0xff00) >> 4; + ret = cros_typec_handle_sop_prime_disc(typec, port_num, sop_prime_revision); + if (ret < 0) + dev_err(typec->dev, "Couldn't parse SOP' Disc data, port: %d\n", port_num); + else { + typec->ports[port_num]->sop_prime_disc_done = true; + ret = cros_typec_send_clear_event(typec, port_num, + PD_STATUS_EVENT_SOP_PRIME_DISC_DONE); + if (ret < 0) + dev_warn(typec->dev, + "Failed SOP Disc event clear, port: %d\n", port_num); + } + } + + if (resp.events & PD_STATUS_EVENT_VDM_REQ_REPLY) { + cros_typec_handle_vdm_response(typec, port_num); + ret = cros_typec_send_clear_event(typec, port_num, PD_STATUS_EVENT_VDM_REQ_REPLY); + if (ret < 0) + dev_warn(typec->dev, "Failed VDM Reply event clear, port: %d\n", port_num); + } + + if (resp.events & PD_STATUS_EVENT_VDM_ATTENTION) { + cros_typec_handle_vdm_attention(typec, port_num); + ret = cros_typec_send_clear_event(typec, port_num, PD_STATUS_EVENT_VDM_ATTENTION); + if (ret < 0) + dev_warn(typec->dev, "Failed VDM attention event clear, port: %d\n", + port_num); + } +} + +static int cros_typec_port_update(struct cros_typec_data *typec, int port_num) +{ + struct ec_params_usb_pd_control req; + struct ec_response_usb_pd_control_v2 resp; + int ret; + + if (port_num < 0 || port_num >= typec->num_ports) { + dev_err(typec->dev, "cannot get status for invalid port %d\n", + port_num); + return -EINVAL; + } + + req.port = port_num; + req.role = USB_PD_CTRL_ROLE_NO_CHANGE; + req.mux = USB_PD_CTRL_MUX_NO_CHANGE; + req.swap = USB_PD_CTRL_SWAP_NONE; + + ret = cros_ec_cmd(typec->ec, typec->pd_ctrl_ver, + EC_CMD_USB_PD_CONTROL, &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) + return ret; + + /* Update the switches if they exist, according to requested state */ + ret = cros_typec_configure_mux(typec, port_num, &resp); + if (ret) + dev_warn(typec->dev, "Configure muxes failed, err = %d\n", ret); + + dev_dbg(typec->dev, "Enabled %d: 0x%hhx\n", port_num, resp.enabled); + dev_dbg(typec->dev, "Role %d: 0x%hhx\n", port_num, resp.role); + dev_dbg(typec->dev, "Polarity %d: 0x%hhx\n", port_num, resp.polarity); + dev_dbg(typec->dev, "State %d: %s\n", port_num, resp.state); + + if (typec->pd_ctrl_ver != 0) + cros_typec_set_port_params_v1(typec, port_num, + (struct ec_response_usb_pd_control_v1 *)&resp); + else + cros_typec_set_port_params_v0(typec, port_num, + (struct ec_response_usb_pd_control *) &resp); + + if (typec->typec_cmd_supported) + cros_typec_handle_status(typec, port_num); + + return 0; +} + +static int cros_typec_get_cmd_version(struct cros_typec_data *typec) +{ + struct ec_params_get_cmd_versions_v1 req_v1; + struct ec_response_get_cmd_versions resp; + int ret; + + /* We're interested in the PD control command version. */ + req_v1.cmd = EC_CMD_USB_PD_CONTROL; + ret = cros_ec_cmd(typec->ec, 1, EC_CMD_GET_CMD_VERSIONS, + &req_v1, sizeof(req_v1), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + if (resp.version_mask & EC_VER_MASK(2)) + typec->pd_ctrl_ver = 2; + else if (resp.version_mask & EC_VER_MASK(1)) + typec->pd_ctrl_ver = 1; + else + typec->pd_ctrl_ver = 0; + + dev_dbg(typec->dev, "PD Control has version mask 0x%02x\n", + typec->pd_ctrl_ver & 0xff); + + return 0; +} + +static void cros_typec_port_work(struct work_struct *work) +{ + struct cros_typec_data *typec = container_of(work, struct cros_typec_data, port_work); + int ret, i; + + for (i = 0; i < typec->num_ports; i++) { + ret = cros_typec_port_update(typec, i); + if (ret < 0) + dev_warn(typec->dev, "Update failed for port: %d\n", i); + } +} + +static int cros_ec_typec_event(struct notifier_block *nb, + unsigned long host_event, void *_notify) +{ + struct cros_typec_data *typec = container_of(nb, struct cros_typec_data, nb); + + flush_work(&typec->port_work); + schedule_work(&typec->port_work); + + return NOTIFY_OK; +} + +#ifdef CONFIG_ACPI +static const struct acpi_device_id cros_typec_acpi_id[] = { + { "GOOG0014", 0 }, + {} +}; +MODULE_DEVICE_TABLE(acpi, cros_typec_acpi_id); +#endif + +#ifdef CONFIG_OF +static const struct of_device_id cros_typec_of_match[] = { + { .compatible = "google,cros-ec-typec", }, + {} +}; +MODULE_DEVICE_TABLE(of, cros_typec_of_match); +#endif + +static int cros_typec_probe(struct platform_device *pdev) +{ + struct cros_ec_dev *ec_dev = NULL; + struct device *dev = &pdev->dev; + struct cros_typec_data *typec; + struct ec_response_usb_pd_ports resp; + int ret, i; + + typec = devm_kzalloc(dev, sizeof(*typec), GFP_KERNEL); + if (!typec) + return -ENOMEM; + + typec->dev = dev; + + typec->ec = dev_get_drvdata(pdev->dev.parent); + if (!typec->ec || !typec->ec->ec) { + dev_warn(dev, "couldn't find parent EC device\n"); + return -EPROBE_DEFER; + } + + platform_set_drvdata(pdev, typec); + + ret = cros_typec_get_cmd_version(typec); + if (ret < 0) { + dev_err(dev, "failed to get PD command version info\n"); + return ret; + } + + ec_dev = dev_get_drvdata(&typec->ec->ec->dev); + if (!ec_dev) + return -EPROBE_DEFER; + + typec->typec_cmd_supported = cros_ec_check_features(ec_dev, EC_FEATURE_TYPEC_CMD); + typec->needs_mux_ack = cros_ec_check_features(ec_dev, EC_FEATURE_TYPEC_MUX_REQUIRE_AP_ACK); + typec->ap_driven_altmode = cros_ec_check_features( + ec_dev, EC_FEATURE_TYPEC_REQUIRE_AP_MODE_ENTRY); + + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_USB_PD_PORTS, NULL, 0, + &resp, sizeof(resp)); + if (ret < 0) + return ret; + + typec->num_ports = resp.num_ports; + if (typec->num_ports > EC_USB_PD_MAX_PORTS) { + dev_warn(typec->dev, + "Too many ports reported: %d, limiting to max: %d\n", + typec->num_ports, EC_USB_PD_MAX_PORTS); + typec->num_ports = EC_USB_PD_MAX_PORTS; + } + + ret = cros_typec_init_ports(typec); + if (ret < 0) + return ret; + + INIT_WORK(&typec->port_work, cros_typec_port_work); + + /* + * Safe to call port update here, since we haven't registered the + * PD notifier yet. + */ + for (i = 0; i < typec->num_ports; i++) { + ret = cros_typec_port_update(typec, i); + if (ret < 0) + goto unregister_ports; + } + + typec->nb.notifier_call = cros_ec_typec_event; + ret = cros_usbpd_register_notify(&typec->nb); + if (ret < 0) + goto unregister_ports; + + return 0; + +unregister_ports: + cros_unregister_ports(typec); + return ret; +} + +static void cros_typec_remove(struct platform_device *pdev) +{ + struct cros_typec_data *typec = platform_get_drvdata(pdev); + + cros_usbpd_unregister_notify(&typec->nb); + cancel_work_sync(&typec->port_work); + cros_unregister_ports(typec); +} + +static int __maybe_unused cros_typec_suspend(struct device *dev) +{ + struct cros_typec_data *typec = dev_get_drvdata(dev); + + cancel_work_sync(&typec->port_work); + + return 0; +} + +static int __maybe_unused cros_typec_resume(struct device *dev) +{ + struct cros_typec_data *typec = dev_get_drvdata(dev); + + /* Refresh port state. */ + schedule_work(&typec->port_work); + + return 0; +} + +static const struct dev_pm_ops cros_typec_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(cros_typec_suspend, cros_typec_resume) +}; + +static struct platform_driver cros_typec_driver = { + .driver = { + .name = DRV_NAME, + .acpi_match_table = ACPI_PTR(cros_typec_acpi_id), + .of_match_table = of_match_ptr(cros_typec_of_match), + .pm = &cros_typec_pm_ops, + }, + .probe = cros_typec_probe, + .remove = cros_typec_remove, +}; + +module_platform_driver(cros_typec_driver); + +MODULE_AUTHOR("Prashant Malani "); +MODULE_DESCRIPTION("Chrome OS EC Type C control"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_ec_typec.h b/drivers/platform/chrome/cros_ec_typec.h new file mode 100644 index 000000000..f9c31f04c --- /dev/null +++ b/drivers/platform/chrome/cros_ec_typec.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef __CROS_EC_TYPEC__ +#define __CROS_EC_TYPEC__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Supported alt modes. */ +enum { + CROS_EC_ALTMODE_DP = 0, + CROS_EC_ALTMODE_TBT, + CROS_EC_ALTMODE_USB4, + CROS_EC_ALTMODE_MAX, +}; + +/* Container for altmode pointer nodes. */ +struct cros_typec_altmode_node { + struct typec_altmode *amode; + struct list_head list; +}; + +/* Platform-specific data for the Chrome OS EC Type C controller. */ +struct cros_typec_data { + struct device *dev; + struct cros_ec_device *ec; + int num_ports; + unsigned int pd_ctrl_ver; + /* Array of ports, indexed by port number. */ + struct cros_typec_port *ports[EC_USB_PD_MAX_PORTS]; + struct notifier_block nb; + struct work_struct port_work; + bool typec_cmd_supported; + bool needs_mux_ack; + bool ap_driven_altmode; +}; + +/* Per port data. */ +struct cros_typec_port { + struct typec_port *port; + int port_num; + /* Initial capabilities for the port. */ + struct typec_capability caps; + struct typec_partner *partner; + struct typec_cable *cable; + /* SOP' plug. */ + struct typec_plug *plug; + /* Port partner PD identity info. */ + struct usb_pd_identity p_identity; + /* Port cable PD identity info. */ + struct usb_pd_identity c_identity; + struct typec_switch *ori_sw; + struct typec_mux *mux; + struct typec_retimer *retimer; + struct usb_role_switch *role_sw; + + /* Variables keeping track of switch state. */ + struct typec_mux_state state; + uint8_t mux_flags; + uint8_t role; + + struct typec_altmode *port_altmode[CROS_EC_ALTMODE_MAX]; + + /* Flag indicating that PD partner discovery data parsing is completed. */ + bool sop_disc_done; + bool sop_prime_disc_done; + struct ec_response_typec_discovery *disc_data; + struct list_head partner_mode_list; + struct list_head plug_mode_list; + + /* PDO-related structs */ + struct usb_power_delivery *partner_pd; + struct usb_power_delivery_capabilities *partner_src_caps; + struct usb_power_delivery_capabilities *partner_sink_caps; + + struct cros_typec_data *typec_data; +}; + +#endif /* __CROS_EC_TYPEC__ */ diff --git a/drivers/platform/chrome/cros_ec_uart.c b/drivers/platform/chrome/cros_ec_uart.c new file mode 100644 index 000000000..d5b37414f --- /dev/null +++ b/drivers/platform/chrome/cros_ec_uart.c @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * UART interface for ChromeOS Embedded Controller + * + * Copyright 2020-2022 Google LLC. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cros_ec.h" + +/* + * EC sends contiguous bytes of response packet on UART AP RX. + * TTY driver in AP accumulates incoming bytes and calls the registered callback + * function. Byte count can range from 1 to MAX bytes supported by EC. + * This driver should wait for long time for all callbacks to be processed. + * Considering the worst case scenario, wait for 500 msec. This timeout should + * account for max latency and some additional guard time. + * Best case: Entire packet is received in ~200 ms, wait queue will be released + * and packet will be processed. + * Worst case: TTY driver sends bytes in multiple callbacks. In this case this + * driver will wait for ~1 sec beyond which it will timeout. + * This timeout value should not exceed ~500 msec because in case if + * EC_CMD_REBOOT_EC sent, high level driver should be able to intercept EC + * in RO. + */ +#define EC_MSG_DEADLINE_MS 500 + +/** + * struct response_info - Encapsulate EC response related + * information for passing between function + * cros_ec_uart_pkt_xfer() and cros_ec_uart_rx_bytes() + * callback. + * @data: Copy the data received from EC here. + * @max_size: Max size allocated for the @data buffer. If the + * received data exceeds this value, we log an error. + * @size: Actual size of data received from EC. This is also + * used to accumulate byte count with response is received + * in dma chunks. + * @exp_len: Expected bytes of response from EC including header. + * @status: Re-init to 0 before sending a cmd. Updated to 1 when + * a response is successfully received, or an error number + * on failure. + * @wait_queue: Wait queue EC response where the cros_ec sends request + * to EC and waits + */ +struct response_info { + void *data; + size_t max_size; + size_t size; + size_t exp_len; + int status; + wait_queue_head_t wait_queue; +}; + +/** + * struct cros_ec_uart - information about a uart-connected EC + * + * @serdev: serdev uart device we are connected to. + * @baudrate: UART baudrate of attached EC device. + * @flowcontrol: UART flowcontrol of attached device. + * @irq: Linux IRQ number of associated serial device. + * @response: Response info passing between cros_ec_uart_pkt_xfer() + * and cros_ec_uart_rx_bytes() + */ +struct cros_ec_uart { + struct serdev_device *serdev; + u32 baudrate; + u8 flowcontrol; + u32 irq; + struct response_info response; +}; + +static size_t cros_ec_uart_rx_bytes(struct serdev_device *serdev, + const u8 *data, size_t count) +{ + struct ec_host_response *host_response; + struct cros_ec_device *ec_dev = serdev_device_get_drvdata(serdev); + struct cros_ec_uart *ec_uart = ec_dev->priv; + struct response_info *resp = &ec_uart->response; + + /* Check if bytes were sent out of band */ + if (!resp->data) { + /* Discard all bytes */ + dev_warn(ec_dev->dev, "Bytes received out of band, dropping them.\n"); + return count; + } + + /* + * Check if incoming bytes + resp->size is greater than allocated + * buffer in din by cros_ec. This will ensure that if EC sends more + * bytes than max_size, waiting process will be notified with an error. + */ + if (resp->size + count > resp->max_size) { + resp->status = -EMSGSIZE; + wake_up(&resp->wait_queue); + return count; + } + + memcpy(resp->data + resp->size, data, count); + + resp->size += count; + + /* Read data_len if we received response header and if exp_len was not read before. */ + if (resp->size >= sizeof(*host_response) && resp->exp_len == 0) { + host_response = (struct ec_host_response *)resp->data; + resp->exp_len = host_response->data_len + sizeof(*host_response); + } + + /* If driver received response header and payload from EC, wake up the wait queue. */ + if (resp->size >= sizeof(*host_response) && resp->size == resp->exp_len) { + resp->status = 1; + wake_up(&resp->wait_queue); + } + + return count; +} + +static int cros_ec_uart_pkt_xfer(struct cros_ec_device *ec_dev, + struct cros_ec_command *ec_msg) +{ + struct cros_ec_uart *ec_uart = ec_dev->priv; + struct serdev_device *serdev = ec_uart->serdev; + struct response_info *resp = &ec_uart->response; + struct ec_host_response *host_response; + unsigned int len; + int ret, i; + u8 sum; + + len = cros_ec_prepare_tx(ec_dev, ec_msg); + dev_dbg(ec_dev->dev, "Prepared len=%d\n", len); + + /* Setup for incoming response */ + resp->data = ec_dev->din; + resp->max_size = ec_dev->din_size; + resp->size = 0; + resp->exp_len = 0; + resp->status = 0; + + ret = serdev_device_write_buf(serdev, ec_dev->dout, len); + if (ret < 0 || ret < len) { + dev_err(ec_dev->dev, "Unable to write data\n"); + if (ret >= 0) + ret = -EIO; + goto exit; + } + + ret = wait_event_timeout(resp->wait_queue, resp->status, + msecs_to_jiffies(EC_MSG_DEADLINE_MS)); + if (ret == 0) { + dev_warn(ec_dev->dev, "Timed out waiting for response.\n"); + ret = -ETIMEDOUT; + goto exit; + } + + if (resp->status < 0) { + ret = resp->status; + dev_warn(ec_dev->dev, "Error response received: %d\n", ret); + goto exit; + } + + host_response = (struct ec_host_response *)ec_dev->din; + ec_msg->result = host_response->result; + + if (host_response->data_len > ec_msg->insize) { + dev_err(ec_dev->dev, "Resp too long (%d bytes, expected %d)\n", + host_response->data_len, ec_msg->insize); + ret = -ENOSPC; + goto exit; + } + + /* Validate checksum */ + sum = 0; + for (i = 0; i < sizeof(*host_response) + host_response->data_len; i++) + sum += ec_dev->din[i]; + + if (sum) { + dev_err(ec_dev->dev, "Bad packet checksum calculated %x\n", sum); + ret = -EBADMSG; + goto exit; + } + + memcpy(ec_msg->data, ec_dev->din + sizeof(*host_response), host_response->data_len); + + ret = host_response->data_len; + +exit: + /* Invalidate response buffer to guard against out of band rx data */ + resp->data = NULL; + + if (ec_msg->command == EC_CMD_REBOOT_EC) + msleep(EC_REBOOT_DELAY_MS); + + return ret; +} + +static int cros_ec_uart_resource(struct acpi_resource *ares, void *data) +{ + struct cros_ec_uart *ec_uart = data; + struct acpi_resource_uart_serialbus *sb = &ares->data.uart_serial_bus; + + if (ares->type == ACPI_RESOURCE_TYPE_SERIAL_BUS && + sb->type == ACPI_RESOURCE_SERIAL_TYPE_UART) { + ec_uart->baudrate = sb->default_baud_rate; + dev_dbg(&ec_uart->serdev->dev, "Baudrate %d\n", ec_uart->baudrate); + + ec_uart->flowcontrol = sb->flow_control; + dev_dbg(&ec_uart->serdev->dev, "Flow control %d\n", ec_uart->flowcontrol); + } + + return 0; +} + +static int cros_ec_uart_acpi_probe(struct cros_ec_uart *ec_uart) +{ + int ret; + LIST_HEAD(resources); + struct acpi_device *adev = ACPI_COMPANION(&ec_uart->serdev->dev); + + ret = acpi_dev_get_resources(adev, &resources, cros_ec_uart_resource, ec_uart); + if (ret < 0) + return ret; + + acpi_dev_free_resource_list(&resources); + + /* Retrieve GpioInt and translate it to Linux IRQ number */ + ret = acpi_dev_gpio_irq_get(adev, 0); + if (ret < 0) + return ret; + + ec_uart->irq = ret; + dev_dbg(&ec_uart->serdev->dev, "IRQ number %d\n", ec_uart->irq); + + return 0; +} + +static const struct serdev_device_ops cros_ec_uart_client_ops = { + .receive_buf = cros_ec_uart_rx_bytes, +}; + +static int cros_ec_uart_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + struct cros_ec_device *ec_dev; + struct cros_ec_uart *ec_uart; + int ret; + + ec_uart = devm_kzalloc(dev, sizeof(*ec_uart), GFP_KERNEL); + if (!ec_uart) + return -ENOMEM; + + ec_dev = cros_ec_device_alloc(dev); + if (!ec_dev) + return -ENOMEM; + + serdev_device_set_drvdata(serdev, ec_dev); + init_waitqueue_head(&ec_uart->response.wait_queue); + + ec_uart->serdev = serdev; + + ret = cros_ec_uart_acpi_probe(ec_uart); + if (ret < 0) { + dev_err(dev, "Failed to get ACPI info (%d)", ret); + return ret; + } + + /* Initialize ec_dev for cros_ec */ + ec_dev->phys_name = dev_name(dev); + ec_dev->priv = ec_uart; + ec_dev->irq = ec_uart->irq; + ec_dev->cmd_xfer = NULL; + ec_dev->pkt_xfer = cros_ec_uart_pkt_xfer; + + serdev_device_set_client_ops(serdev, &cros_ec_uart_client_ops); + + ret = devm_serdev_device_open(dev, serdev); + if (ret) { + dev_err(dev, "Unable to open UART device"); + return ret; + } + + ret = serdev_device_set_baudrate(serdev, ec_uart->baudrate); + if (ret < 0) { + dev_err(dev, "Failed to set up host baud rate (%d)", ret); + return ret; + } + + serdev_device_set_flow_control(serdev, ec_uart->flowcontrol); + + return cros_ec_register(ec_dev); +} + +static void cros_ec_uart_remove(struct serdev_device *serdev) +{ + struct cros_ec_device *ec_dev = serdev_device_get_drvdata(serdev); + + cros_ec_unregister(ec_dev); +}; + +static int __maybe_unused cros_ec_uart_suspend(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_suspend(ec_dev); +} + +static int __maybe_unused cros_ec_uart_resume(struct device *dev) +{ + struct cros_ec_device *ec_dev = dev_get_drvdata(dev); + + return cros_ec_resume(ec_dev); +} + +static SIMPLE_DEV_PM_OPS(cros_ec_uart_pm_ops, cros_ec_uart_suspend, + cros_ec_uart_resume); + +static const struct of_device_id cros_ec_uart_of_match[] = { + { .compatible = "google,cros-ec-uart" }, + {} +}; +MODULE_DEVICE_TABLE(of, cros_ec_uart_of_match); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id cros_ec_uart_acpi_id[] = { + { "GOOG0019", 0 }, + {} +}; + +MODULE_DEVICE_TABLE(acpi, cros_ec_uart_acpi_id); +#endif + +static struct serdev_device_driver cros_ec_uart_driver = { + .driver = { + .name = "cros-ec-uart", + .acpi_match_table = ACPI_PTR(cros_ec_uart_acpi_id), + .of_match_table = cros_ec_uart_of_match, + .pm = &cros_ec_uart_pm_ops, + }, + .probe = cros_ec_uart_probe, + .remove = cros_ec_uart_remove, +}; + +module_serdev_device_driver(cros_ec_uart_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("UART interface for ChromeOS Embedded Controller"); +MODULE_AUTHOR("Bhanu Prakash Maiya "); diff --git a/drivers/platform/chrome/cros_ec_vbc.c b/drivers/platform/chrome/cros_ec_vbc.c new file mode 100644 index 000000000..c21e1f85f --- /dev/null +++ b/drivers/platform/chrome/cros_ec_vbc.c @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Expose the vboot context nvram to userspace +// +// Copyright (C) 2012 Google, Inc. +// Copyright (C) 2015 Collabora Ltd. + +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-ec-vbc" + +static ssize_t vboot_context_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *att, char *buf, + loff_t pos, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + struct cros_ec_device *ecdev = ec->ec_dev; + struct cros_ec_command *msg; + /* + * This should be a pointer to the same type as op field in + * struct ec_params_vbnvcontext. + */ + uint32_t *params_op; + int err; + const size_t para_sz = sizeof(*params_op); + const size_t resp_sz = sizeof(struct ec_response_vbnvcontext); + const size_t payload = max(para_sz, resp_sz); + + msg = kmalloc(sizeof(*msg) + payload, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + /* NB: we only kmalloc()ated enough space for the op field */ + params_op = (uint32_t *)msg->data; + *params_op = EC_VBNV_CONTEXT_OP_READ; + + msg->version = EC_VER_VBNV_CONTEXT; + msg->command = EC_CMD_VBNV_CONTEXT; + msg->outsize = para_sz; + msg->insize = resp_sz; + + err = cros_ec_cmd_xfer_status(ecdev, msg); + if (err < 0) { + dev_err(dev, "Error sending read request: %d\n", err); + kfree(msg); + return err; + } + + memcpy(buf, msg->data, resp_sz); + + kfree(msg); + return resp_sz; +} + +static ssize_t vboot_context_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, + loff_t pos, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct cros_ec_dev *ec = to_cros_ec_dev(dev); + struct cros_ec_device *ecdev = ec->ec_dev; + struct ec_params_vbnvcontext *params; + struct cros_ec_command *msg; + int err; + const size_t para_sz = sizeof(*params); + const size_t data_sz = sizeof(params->block); + + /* Only write full values */ + if (count != data_sz) + return -EINVAL; + + msg = kmalloc(sizeof(*msg) + para_sz, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + params = (struct ec_params_vbnvcontext *)msg->data; + params->op = EC_VBNV_CONTEXT_OP_WRITE; + memcpy(params->block, buf, data_sz); + + msg->version = EC_VER_VBNV_CONTEXT; + msg->command = EC_CMD_VBNV_CONTEXT; + msg->outsize = para_sz; + msg->insize = 0; + + err = cros_ec_cmd_xfer_status(ecdev, msg); + if (err < 0) { + dev_err(dev, "Error sending write request: %d\n", err); + kfree(msg); + return err; + } + + kfree(msg); + return data_sz; +} + +static const BIN_ATTR_RW(vboot_context, 16); + +static const struct bin_attribute *const cros_ec_vbc_bin_attrs[] = { + &bin_attr_vboot_context, + NULL +}; + +static const struct attribute_group cros_ec_vbc_attr_group = { + .name = "vbc", + .bin_attrs = cros_ec_vbc_bin_attrs, +}; + +static int cros_ec_vbc_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + struct device *dev = &pd->dev; + int ret; + + ret = sysfs_create_group(&ec_dev->class_dev.kobj, + &cros_ec_vbc_attr_group); + if (ret < 0) + dev_err(dev, "failed to create %s attributes. err=%d\n", + cros_ec_vbc_attr_group.name, ret); + + return ret; +} + +static void cros_ec_vbc_remove(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + + sysfs_remove_group(&ec_dev->class_dev.kobj, + &cros_ec_vbc_attr_group); +} + +static const struct platform_device_id cros_ec_vbc_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_ec_vbc_id); + +static struct platform_driver cros_ec_vbc_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = cros_ec_vbc_probe, + .remove = cros_ec_vbc_remove, + .id_table = cros_ec_vbc_id, +}; + +module_platform_driver(cros_ec_vbc_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Expose the vboot context nvram to userspace"); diff --git a/drivers/platform/chrome/cros_hps_i2c.c b/drivers/platform/chrome/cros_hps_i2c.c new file mode 100644 index 000000000..3b9485627 --- /dev/null +++ b/drivers/platform/chrome/cros_hps_i2c.c @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for the ChromeOS human presence sensor (HPS), attached via I2C. + * + * The driver exposes HPS as a character device, although currently no read or + * write operations are supported. Instead, the driver only controls the power + * state of the sensor, keeping it on only while userspace holds an open file + * descriptor to the HPS device. + * + * Copyright 2022 Google LLC. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define HPS_ACPI_ID "GOOG0020" + +struct hps_drvdata { + struct i2c_client *client; + struct miscdevice misc_device; + struct gpio_desc *enable_gpio; +}; + +static void hps_set_power(struct hps_drvdata *hps, bool state) +{ + gpiod_set_value_cansleep(hps->enable_gpio, state); +} + +static int hps_open(struct inode *inode, struct file *file) +{ + struct hps_drvdata *hps = container_of(file->private_data, + struct hps_drvdata, misc_device); + struct device *dev = &hps->client->dev; + + return pm_runtime_resume_and_get(dev); +} + +static int hps_release(struct inode *inode, struct file *file) +{ + struct hps_drvdata *hps = container_of(file->private_data, + struct hps_drvdata, misc_device); + struct device *dev = &hps->client->dev; + + pm_runtime_put(dev); + + return 0; +} + +static const struct file_operations hps_fops = { + .owner = THIS_MODULE, + .open = hps_open, + .release = hps_release, +}; + +static int hps_i2c_probe(struct i2c_client *client) +{ + struct hps_drvdata *hps; + int ret; + + hps = devm_kzalloc(&client->dev, sizeof(*hps), GFP_KERNEL); + if (!hps) + return -ENOMEM; + + hps->misc_device.parent = &client->dev; + hps->misc_device.minor = MISC_DYNAMIC_MINOR; + hps->misc_device.name = "cros-hps"; + hps->misc_device.fops = &hps_fops; + + i2c_set_clientdata(client, hps); + hps->client = client; + + /* + * HPS is powered on from firmware before entering the kernel, so we + * acquire the line with GPIOD_OUT_HIGH here to preserve the existing + * state. The peripheral is powered off after successful probe below. + */ + hps->enable_gpio = devm_gpiod_get(&client->dev, "enable", GPIOD_OUT_HIGH); + if (IS_ERR(hps->enable_gpio)) { + ret = PTR_ERR(hps->enable_gpio); + dev_err(&client->dev, "failed to get enable gpio: %d\n", ret); + return ret; + } + + ret = misc_register(&hps->misc_device); + if (ret) { + dev_err(&client->dev, "failed to initialize misc device: %d\n", ret); + return ret; + } + + hps_set_power(hps, false); + pm_runtime_enable(&client->dev); + return 0; +} + +static void hps_i2c_remove(struct i2c_client *client) +{ + struct hps_drvdata *hps = i2c_get_clientdata(client); + + pm_runtime_disable(&client->dev); + misc_deregister(&hps->misc_device); + + /* + * Re-enable HPS, in order to return it to its default state + * (i.e. powered on). + */ + hps_set_power(hps, true); +} + +static int hps_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct hps_drvdata *hps = i2c_get_clientdata(client); + + hps_set_power(hps, false); + return 0; +} + +static int hps_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct hps_drvdata *hps = i2c_get_clientdata(client); + + hps_set_power(hps, true); + return 0; +} +static DEFINE_RUNTIME_DEV_PM_OPS(hps_pm_ops, hps_suspend, hps_resume, NULL); + +static const struct i2c_device_id hps_i2c_id[] = { + { .name = "cros-hps" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, hps_i2c_id); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id hps_acpi_id[] = { + { HPS_ACPI_ID, 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, hps_acpi_id); +#endif /* CONFIG_ACPI */ + +static struct i2c_driver hps_i2c_driver = { + .probe = hps_i2c_probe, + .remove = hps_i2c_remove, + .id_table = hps_i2c_id, + .driver = { + .name = "cros-hps", + .pm = pm_ptr(&hps_pm_ops), + .acpi_match_table = ACPI_PTR(hps_acpi_id), + }, +}; +module_i2c_driver(hps_i2c_driver); + +MODULE_ALIAS("acpi:" HPS_ACPI_ID); +MODULE_AUTHOR("Sami Kyöstilä "); +MODULE_DESCRIPTION("Driver for ChromeOS HPS"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_kbd_led_backlight.c b/drivers/platform/chrome/cros_kbd_led_backlight.c new file mode 100644 index 000000000..94888db1c --- /dev/null +++ b/drivers/platform/chrome/cros_kbd_led_backlight.c @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Keyboard backlight LED driver for ChromeOS +// +// Copyright (C) 2012 Google, Inc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct keyboard_led { + struct led_classdev cdev; + struct cros_ec_device *ec; +}; + +/** + * struct keyboard_led_drvdata - keyboard LED driver data. + * @init: Init function. + * @brightness_get: Get LED brightness level. + * @brightness_set: Set LED brightness level. Must not sleep. + * @brightness_set_blocking: Set LED brightness level. It can block the + * caller for the time required for accessing a + * LED device register + * + * See struct led_classdev in include/linux/leds.h for more details. + */ +struct keyboard_led_drvdata { + int (*init)(struct platform_device *pdev, struct keyboard_led *keyboard_led); + + enum led_brightness (*brightness_get)(struct led_classdev *led_cdev); + + void (*brightness_set)(struct led_classdev *led_cdev, + enum led_brightness brightness); + int (*brightness_set_blocking)(struct led_classdev *led_cdev, + enum led_brightness brightness); +}; + +#ifdef CONFIG_ACPI + +/* Keyboard LED ACPI Device must be defined in firmware */ +#define ACPI_KEYBOARD_BACKLIGHT_DEVICE "\\_SB.KBLT" +#define ACPI_KEYBOARD_BACKLIGHT_READ ACPI_KEYBOARD_BACKLIGHT_DEVICE ".KBQC" +#define ACPI_KEYBOARD_BACKLIGHT_WRITE ACPI_KEYBOARD_BACKLIGHT_DEVICE ".KBCM" + +static void keyboard_led_set_brightness_acpi(struct led_classdev *cdev, + enum led_brightness brightness) +{ + union acpi_object param; + struct acpi_object_list input; + acpi_status status; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = brightness; + input.count = 1; + input.pointer = ¶m; + + status = acpi_evaluate_object(NULL, ACPI_KEYBOARD_BACKLIGHT_WRITE, + &input, NULL); + if (ACPI_FAILURE(status)) + dev_err(cdev->dev, "Error setting keyboard LED value: %d\n", + status); +} + +static enum led_brightness +keyboard_led_get_brightness_acpi(struct led_classdev *cdev) +{ + unsigned long long brightness; + acpi_status status; + + status = acpi_evaluate_integer(NULL, ACPI_KEYBOARD_BACKLIGHT_READ, + NULL, &brightness); + if (ACPI_FAILURE(status)) { + dev_err(cdev->dev, "Error getting keyboard LED value: %d\n", + status); + return -EIO; + } + + return brightness; +} + +static int keyboard_led_init_acpi(struct platform_device *pdev, + struct keyboard_led *keyboard_led) +{ + acpi_handle handle; + acpi_status status; + + /* Look for the keyboard LED ACPI Device */ + status = acpi_get_handle(ACPI_ROOT_OBJECT, + ACPI_KEYBOARD_BACKLIGHT_DEVICE, + &handle); + if (ACPI_FAILURE(status)) { + dev_err(&pdev->dev, "Unable to find ACPI device %s: %d\n", + ACPI_KEYBOARD_BACKLIGHT_DEVICE, status); + return -ENXIO; + } + + return 0; +} + +static const struct keyboard_led_drvdata keyboard_led_drvdata_acpi = { + .init = keyboard_led_init_acpi, + .brightness_set = keyboard_led_set_brightness_acpi, + .brightness_get = keyboard_led_get_brightness_acpi, +}; + +#endif /* CONFIG_ACPI */ + +static int keyboard_led_init_ec_pwm_mfd(struct platform_device *pdev, + struct keyboard_led *keyboard_led) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pdev->dev.parent); + struct cros_ec_device *cros_ec = ec_dev->ec_dev; + + keyboard_led->ec = cros_ec; + + return 0; +} + +static int +keyboard_led_set_brightness_ec_pwm(struct led_classdev *cdev, + enum led_brightness brightness) +{ + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_params_pwm_set_keyboard_backlight)); + struct ec_params_pwm_set_keyboard_backlight *params = + (struct ec_params_pwm_set_keyboard_backlight *)msg->data; + struct keyboard_led *keyboard_led = container_of(cdev, struct keyboard_led, cdev); + + msg->command = EC_CMD_PWM_SET_KEYBOARD_BACKLIGHT; + msg->outsize = sizeof(*params); + + params->percent = brightness; + + return cros_ec_cmd_xfer_status(keyboard_led->ec, msg); +} + +static enum led_brightness +keyboard_led_get_brightness_ec_pwm(struct led_classdev *cdev) +{ + DEFINE_RAW_FLEX(struct cros_ec_command, msg, data, + sizeof(struct ec_response_pwm_get_keyboard_backlight)); + struct ec_response_pwm_get_keyboard_backlight *resp = + (struct ec_response_pwm_get_keyboard_backlight *)msg->data; + struct keyboard_led *keyboard_led = container_of(cdev, struct keyboard_led, cdev); + int ret; + + msg->command = EC_CMD_PWM_GET_KEYBOARD_BACKLIGHT; + msg->insize = sizeof(*resp); + + ret = cros_ec_cmd_xfer_status(keyboard_led->ec, msg); + if (ret < 0) + return ret; + + return resp->percent; +} + +static const struct keyboard_led_drvdata keyboard_led_drvdata_ec_pwm_mfd = { + .init = keyboard_led_init_ec_pwm_mfd, + .brightness_set_blocking = keyboard_led_set_brightness_ec_pwm, + .brightness_get = keyboard_led_get_brightness_ec_pwm, +}; + +static int keyboard_led_is_mfd_device(struct platform_device *pdev) +{ + return IS_ENABLED(CONFIG_MFD_CROS_EC_DEV) && mfd_get_cell(pdev); +} + +static int keyboard_led_probe(struct platform_device *pdev) +{ + const struct keyboard_led_drvdata *drvdata; + struct keyboard_led *keyboard_led; + int err; + + if (keyboard_led_is_mfd_device(pdev)) + drvdata = &keyboard_led_drvdata_ec_pwm_mfd; + else + drvdata = device_get_match_data(&pdev->dev); + if (!drvdata) + return -EINVAL; + + keyboard_led = devm_kzalloc(&pdev->dev, sizeof(*keyboard_led), GFP_KERNEL); + if (!keyboard_led) + return -ENOMEM; + + if (drvdata->init) { + err = drvdata->init(pdev, keyboard_led); + if (err) + return err; + } + + keyboard_led->cdev.name = "chromeos::kbd_backlight"; + keyboard_led->cdev.flags |= LED_CORE_SUSPENDRESUME | LED_REJECT_NAME_CONFLICT; + keyboard_led->cdev.max_brightness = 100; + keyboard_led->cdev.brightness_set = drvdata->brightness_set; + keyboard_led->cdev.brightness_set_blocking = drvdata->brightness_set_blocking; + keyboard_led->cdev.brightness_get = drvdata->brightness_get; + + err = devm_led_classdev_register(&pdev->dev, &keyboard_led->cdev); + if (err == -EEXIST) /* Already bound via other mechanism */ + return -ENODEV; + return err; +} + +#ifdef CONFIG_ACPI +static const struct acpi_device_id keyboard_led_acpi_match[] = { + { "GOOG0002", (kernel_ulong_t)&keyboard_led_drvdata_acpi }, + { } +}; +MODULE_DEVICE_TABLE(acpi, keyboard_led_acpi_match); +#endif + +static const struct platform_device_id keyboard_led_id[] = { + { .name = "cros-keyboard-leds" }, + { } +}; +MODULE_DEVICE_TABLE(platform, keyboard_led_id); + +static struct platform_driver keyboard_led_driver = { + .driver = { + .name = "cros-keyboard-leds", + .acpi_match_table = ACPI_PTR(keyboard_led_acpi_match), + }, + .probe = keyboard_led_probe, + .id_table = keyboard_led_id, +}; +module_platform_driver(keyboard_led_driver); + +MODULE_AUTHOR("Simon Que "); +MODULE_DESCRIPTION("ChromeOS Keyboard backlight LED Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_typec_altmode.c b/drivers/platform/chrome/cros_typec_altmode.c new file mode 100644 index 000000000..66c546bf8 --- /dev/null +++ b/drivers/platform/chrome/cros_typec_altmode.c @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Alt-mode implementation on ChromeOS EC. + * + * Copyright 2024 Google LLC + * Author: Abhishek Pandit-Subedi + */ +#include "cros_ec_typec.h" + +#include +#include +#include +#include +#include + +#include "cros_typec_altmode.h" + +struct cros_typec_altmode_data { + struct work_struct work; + struct cros_typec_port *port; + struct typec_altmode *alt; + bool ap_mode_entry; + + struct mutex lock; + u32 header; + u32 *vdo_data; + u8 vdo_size; + + u16 sid; + u8 mode; +}; + +struct cros_typec_dp_data { + struct cros_typec_altmode_data adata; + struct typec_displayport_data data; + bool configured; + bool pending_status_update; +}; + +static void cros_typec_altmode_work(struct work_struct *work) +{ + struct cros_typec_altmode_data *data = + container_of(work, struct cros_typec_altmode_data, work); + + mutex_lock(&data->lock); + + if (typec_altmode_vdm(data->alt, data->header, data->vdo_data, + data->vdo_size)) + dev_err(&data->alt->dev, "VDM 0x%x failed\n", data->header); + + data->header = 0; + data->vdo_data = NULL; + data->vdo_size = 0; + + mutex_unlock(&data->lock); +} + +static int cros_typec_altmode_enter(struct typec_altmode *alt, u32 *vdo) +{ + struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(alt); + struct ec_params_typec_control req = { + .port = adata->port->port_num, + .command = TYPEC_CONTROL_COMMAND_ENTER_MODE, + }; + int svdm_version; + int ret; + + if (!adata->ap_mode_entry) { + dev_warn(&alt->dev, + "EC does not support AP driven mode entry\n"); + return -EOPNOTSUPP; + } + + if (adata->sid == USB_TYPEC_DP_SID) + req.mode_to_enter = CROS_EC_ALTMODE_DP; + else if (adata->sid == USB_TYPEC_TBT_SID) + req.mode_to_enter = CROS_EC_ALTMODE_TBT; + else + return -EOPNOTSUPP; + + ret = cros_ec_cmd(adata->port->typec_data->ec, 0, EC_CMD_TYPEC_CONTROL, + &req, sizeof(req), NULL, 0); + if (ret < 0) + return ret; + + svdm_version = typec_altmode_get_svdm_version(alt); + if (svdm_version < 0) + return svdm_version; + + mutex_lock(&adata->lock); + + adata->header = VDO(adata->sid, 1, svdm_version, CMD_ENTER_MODE); + adata->header |= VDO_OPOS(adata->mode); + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + adata->vdo_data = NULL; + adata->vdo_size = 1; + schedule_work(&adata->work); + + mutex_unlock(&adata->lock); + return ret; +} + +static int cros_typec_altmode_exit(struct typec_altmode *alt) +{ + struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(alt); + struct ec_params_typec_control req = { + .port = adata->port->port_num, + .command = TYPEC_CONTROL_COMMAND_EXIT_MODES, + }; + int svdm_version; + int ret; + + if (!adata->ap_mode_entry) { + dev_warn(&alt->dev, + "EC does not support AP driven mode exit\n"); + return -EOPNOTSUPP; + } + + ret = cros_ec_cmd(adata->port->typec_data->ec, 0, EC_CMD_TYPEC_CONTROL, + &req, sizeof(req), NULL, 0); + + if (ret < 0) + return ret; + + svdm_version = typec_altmode_get_svdm_version(alt); + if (svdm_version < 0) + return svdm_version; + + mutex_lock(&adata->lock); + + adata->header = VDO(adata->sid, 1, svdm_version, CMD_EXIT_MODE); + adata->header |= VDO_OPOS(adata->mode); + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + adata->vdo_data = NULL; + adata->vdo_size = 1; + schedule_work(&adata->work); + + mutex_unlock(&adata->lock); + return ret; +} + +static int cros_typec_displayport_vdm(struct typec_altmode *alt, u32 header, + const u32 *data, int count) +{ + struct cros_typec_dp_data *dp_data = typec_altmode_get_drvdata(alt); + struct cros_typec_altmode_data *adata = &dp_data->adata; + + + int cmd_type = PD_VDO_CMDT(header); + int cmd = PD_VDO_CMD(header); + int svdm_version; + + svdm_version = typec_altmode_get_svdm_version(alt); + if (svdm_version < 0) + return svdm_version; + + mutex_lock(&adata->lock); + + switch (cmd_type) { + case CMDT_INIT: + if (PD_VDO_SVDM_VER(header) < svdm_version) { + typec_partner_set_svdm_version(adata->port->partner, + PD_VDO_SVDM_VER(header)); + svdm_version = PD_VDO_SVDM_VER(header); + } + + adata->header = VDO(adata->sid, 1, svdm_version, cmd); + adata->header |= VDO_OPOS(adata->mode); + + /* + * DP_CMD_CONFIGURE: We can't actually do anything with the + * provided VDO yet so just send back an ACK. + * + * DP_CMD_STATUS_UPDATE: We wait for Mux changes to send + * DPStatus Acks. + */ + switch (cmd) { + case DP_CMD_CONFIGURE: + dp_data->data.conf = *data; + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + dp_data->configured = true; + schedule_work(&adata->work); + break; + case DP_CMD_STATUS_UPDATE: + dp_data->pending_status_update = true; + break; + default: + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + schedule_work(&adata->work); + break; + } + + break; + default: + break; + } + + mutex_unlock(&adata->lock); + return 0; +} + +static int cros_typec_thunderbolt_vdm(struct typec_altmode *alt, u32 header, + const u32 *data, int count) +{ + struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(alt); + + int cmd_type = PD_VDO_CMDT(header); + int cmd = PD_VDO_CMD(header); + int svdm_version; + + svdm_version = typec_altmode_get_svdm_version(alt); + if (svdm_version < 0) + return svdm_version; + + mutex_lock(&adata->lock); + + switch (cmd_type) { + case CMDT_INIT: + if (PD_VDO_SVDM_VER(header) < svdm_version) { + typec_partner_set_svdm_version(adata->port->partner, + PD_VDO_SVDM_VER(header)); + svdm_version = PD_VDO_SVDM_VER(header); + } + + adata->header = VDO(adata->sid, 1, svdm_version, cmd); + adata->header |= VDO_OPOS(adata->mode); + + switch (cmd) { + case CMD_ENTER_MODE: + /* Don't respond to the enter mode vdm because it + * triggers mux configuration. This is handled directly + * by the cros_ec_typec driver so the Thunderbolt driver + * doesn't need to be involved. + */ + break; + default: + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + schedule_work(&adata->work); + break; + } + + break; + default: + break; + } + + mutex_unlock(&adata->lock); + return 0; +} + + +static int cros_typec_altmode_vdm(struct typec_altmode *alt, u32 header, + const u32 *data, int count) +{ + struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(alt); + + if (!adata->ap_mode_entry) + return -EOPNOTSUPP; + + if (adata->sid == USB_TYPEC_DP_SID) + return cros_typec_displayport_vdm(alt, header, data, count); + + if (adata->sid == USB_TYPEC_TBT_SID) + return cros_typec_thunderbolt_vdm(alt, header, data, count); + + return -EINVAL; +} + +static const struct typec_altmode_ops cros_typec_altmode_ops = { + .enter = cros_typec_altmode_enter, + .exit = cros_typec_altmode_exit, + .vdm = cros_typec_altmode_vdm, +}; + +#if IS_ENABLED(CONFIG_TYPEC_DP_ALTMODE) +int cros_typec_displayport_status_update(struct typec_altmode *altmode, + struct typec_displayport_data *data) +{ + struct cros_typec_dp_data *dp_data = + typec_altmode_get_drvdata(altmode); + struct cros_typec_altmode_data *adata = &dp_data->adata; + + if (!dp_data->pending_status_update) { + dev_dbg(&altmode->dev, + "Got DPStatus without a pending request\n"); + return 0; + } + + if (dp_data->configured && dp_data->data.conf != data->conf) + dev_dbg(&altmode->dev, + "DP Conf doesn't match. Requested 0x%04x, Actual 0x%04x\n", + dp_data->data.conf, data->conf); + + mutex_lock(&adata->lock); + + dp_data->data = *data; + dp_data->pending_status_update = false; + adata->header |= VDO_CMDT(CMDT_RSP_ACK); + adata->vdo_data = &dp_data->data.status; + adata->vdo_size = 2; + schedule_work(&adata->work); + + mutex_unlock(&adata->lock); + + return 0; +} + +struct typec_altmode * +cros_typec_register_displayport(struct cros_typec_port *port, + struct typec_altmode_desc *desc, + bool ap_mode_entry) +{ + struct typec_altmode *alt; + struct cros_typec_dp_data *dp_data; + struct cros_typec_altmode_data *adata; + + alt = typec_port_register_altmode(port->port, desc); + if (IS_ERR(alt)) + return alt; + + dp_data = devm_kzalloc(&alt->dev, sizeof(*dp_data), GFP_KERNEL); + if (!dp_data) { + typec_unregister_altmode(alt); + return ERR_PTR(-ENOMEM); + } + + adata = &dp_data->adata; + INIT_WORK(&adata->work, cros_typec_altmode_work); + mutex_init(&adata->lock); + adata->alt = alt; + adata->port = port; + adata->ap_mode_entry = ap_mode_entry; + adata->sid = desc->svid; + adata->mode = desc->mode; + + typec_altmode_set_ops(alt, &cros_typec_altmode_ops); + typec_altmode_set_drvdata(alt, adata); + + return alt; +} +#endif + +#if IS_ENABLED(CONFIG_TYPEC_TBT_ALTMODE) +struct typec_altmode * +cros_typec_register_thunderbolt(struct cros_typec_port *port, + struct typec_altmode_desc *desc) +{ + struct typec_altmode *alt; + struct cros_typec_altmode_data *adata; + + alt = typec_port_register_altmode(port->port, desc); + if (IS_ERR(alt)) + return alt; + + adata = devm_kzalloc(&alt->dev, sizeof(*adata), GFP_KERNEL); + if (!adata) { + typec_unregister_altmode(alt); + return ERR_PTR(-ENOMEM); + } + + INIT_WORK(&adata->work, cros_typec_altmode_work); + mutex_init(&adata->lock); + adata->alt = alt; + adata->port = port; + adata->ap_mode_entry = true; + adata->sid = desc->svid; + adata->mode = desc->mode; + + typec_altmode_set_ops(alt, &cros_typec_altmode_ops); + typec_altmode_set_drvdata(alt, adata); + + return alt; +} +#endif diff --git a/drivers/platform/chrome/cros_typec_altmode.h b/drivers/platform/chrome/cros_typec_altmode.h new file mode 100644 index 000000000..3f2aa95d0 --- /dev/null +++ b/drivers/platform/chrome/cros_typec_altmode.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef __CROS_TYPEC_ALTMODE_H__ +#define __CROS_TYPEC_ALTMODE_H__ + +#include +#include + +struct cros_typec_port; +struct typec_altmode; +struct typec_altmode_desc; +struct typec_displayport_data; + +#if IS_ENABLED(CONFIG_TYPEC_DP_ALTMODE) +struct typec_altmode * +cros_typec_register_displayport(struct cros_typec_port *port, + struct typec_altmode_desc *desc, + bool ap_mode_entry); + +int cros_typec_displayport_status_update(struct typec_altmode *altmode, + struct typec_displayport_data *data); +#else +static inline struct typec_altmode * +cros_typec_register_displayport(struct cros_typec_port *port, + struct typec_altmode_desc *desc, + bool ap_mode_entry) +{ + return typec_port_register_altmode(port->port, desc); +} + +static inline int cros_typec_displayport_status_update(struct typec_altmode *altmode, + struct typec_displayport_data *data) +{ + return 0; +} +#endif + +#if IS_ENABLED(CONFIG_TYPEC_TBT_ALTMODE) +struct typec_altmode * +cros_typec_register_thunderbolt(struct cros_typec_port *port, + struct typec_altmode_desc *desc); +#else +static inline struct typec_altmode * +cros_typec_register_thunderbolt(struct cros_typec_port *port, + struct typec_altmode_desc *desc) +{ + return typec_port_register_altmode(port->port, desc); +} +#endif + +#endif /* __CROS_TYPEC_ALTMODE_H__ */ diff --git a/drivers/platform/chrome/cros_typec_switch.c b/drivers/platform/chrome/cros_typec_switch.c new file mode 100644 index 000000000..fe0eddd2c --- /dev/null +++ b/drivers/platform/chrome/cros_typec_switch.c @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2022 Google LLC + * + * This driver provides the ability to configure Type-C muxes and retimers which are controlled by + * the ChromeOS EC. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Handles and other relevant data required for each port's switches. */ +struct cros_typec_port { + int port_num; + struct typec_mux_dev *mode_switch; + struct typec_retimer *retimer; + struct cros_typec_switch_data *sdata; +}; + +/* Driver-specific data. */ +struct cros_typec_switch_data { + struct device *dev; + struct cros_ec_device *ec; + struct cros_typec_port *ports[EC_USB_PD_MAX_PORTS]; +}; + +static int cros_typec_cmd_mux_set(struct cros_typec_switch_data *sdata, int port_num, u8 index, + u8 state) +{ + struct ec_params_typec_control req = { + .port = port_num, + .command = TYPEC_CONTROL_COMMAND_USB_MUX_SET, + .mux_params = { + .mux_index = index, + .mux_flags = state, + }, + }; + + return cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_CONTROL, &req, sizeof(req), NULL, 0); +} + +static int cros_typec_get_mux_state(unsigned long mode, struct typec_altmode *alt) +{ + int ret = -EOPNOTSUPP; + u8 pin_assign; + + if (mode == TYPEC_STATE_SAFE) { + ret = USB_PD_MUX_SAFE_MODE; + } else if (mode == TYPEC_STATE_USB) { + ret = USB_PD_MUX_USB_ENABLED; + } else if (alt && alt->svid == USB_TYPEC_DP_SID) { + ret = USB_PD_MUX_DP_ENABLED; + pin_assign = mode - TYPEC_STATE_MODAL; + if (pin_assign & DP_PIN_ASSIGN_D) + ret |= USB_PD_MUX_USB_ENABLED; + } + + return ret; +} + +static int cros_typec_send_clear_event(struct cros_typec_switch_data *sdata, int port_num, + u32 events_mask) +{ + struct ec_params_typec_control req = { + .port = port_num, + .command = TYPEC_CONTROL_COMMAND_CLEAR_EVENTS, + .clear_events_mask = events_mask, + }; + + return cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_CONTROL, &req, sizeof(req), NULL, 0); +} + +static bool cros_typec_check_event(struct cros_typec_switch_data *sdata, int port_num, u32 mask) +{ + struct ec_response_typec_status resp; + struct ec_params_typec_status req = { + .port = port_num, + }; + int ret; + + ret = cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_STATUS, &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) { + dev_warn(sdata->dev, "EC_CMD_TYPEC_STATUS failed for port: %d\n", port_num); + return false; + } + + if (resp.events & mask) + return true; + + return false; +} + +/* + * The ChromeOS EC treats both mode-switches and retimers as "muxes" for the purposes of the + * host command API. This common function configures and verifies the retimer/mode-switch + * according to the provided setting. + */ +static int cros_typec_configure_mux(struct cros_typec_switch_data *sdata, int port_num, int index, + unsigned long mode, struct typec_altmode *alt) +{ + unsigned long end; + u32 event_mask; + u8 mux_state; + int ret; + + ret = cros_typec_get_mux_state(mode, alt); + if (ret < 0) + return ret; + mux_state = (u8)ret; + + /* Clear any old mux set done event. */ + if (index == 0) + event_mask = PD_STATUS_EVENT_MUX_0_SET_DONE; + else + event_mask = PD_STATUS_EVENT_MUX_1_SET_DONE; + + ret = cros_typec_send_clear_event(sdata, port_num, event_mask); + if (ret < 0) + return ret; + + /* Send the set command. */ + ret = cros_typec_cmd_mux_set(sdata, port_num, index, mux_state); + if (ret < 0) + return ret; + + /* Check for the mux set done event. */ + end = jiffies + msecs_to_jiffies(1000); + do { + if (cros_typec_check_event(sdata, port_num, event_mask)) + return 0; + + usleep_range(500, 1000); + } while (time_before(jiffies, end)); + + dev_err(sdata->dev, "Timed out waiting for mux set done on index: %d, state: %d\n", + index, mux_state); + + return -ETIMEDOUT; +} + +static int cros_typec_mode_switch_set(struct typec_mux_dev *mode_switch, + struct typec_mux_state *state) +{ + struct cros_typec_port *port = typec_mux_get_drvdata(mode_switch); + + /* Mode switches have index 0. */ + return cros_typec_configure_mux(port->sdata, port->port_num, 0, state->mode, state->alt); +} + +static int cros_typec_retimer_set(struct typec_retimer *retimer, struct typec_retimer_state *state) +{ + struct cros_typec_port *port = typec_retimer_get_drvdata(retimer); + + /* Retimers have index 1. */ + return cros_typec_configure_mux(port->sdata, port->port_num, 1, state->mode, state->alt); +} + +static void cros_typec_unregister_switches(struct cros_typec_switch_data *sdata) +{ + int i; + + for (i = 0; i < EC_USB_PD_MAX_PORTS; i++) { + if (!sdata->ports[i]) + continue; + typec_retimer_unregister(sdata->ports[i]->retimer); + typec_mux_unregister(sdata->ports[i]->mode_switch); + } +} + +static int cros_typec_register_mode_switch(struct cros_typec_port *port, + struct fwnode_handle *fwnode) +{ + struct typec_mux_desc mode_switch_desc = { + .fwnode = fwnode, + .drvdata = port, + .name = fwnode_get_name(fwnode), + .set = cros_typec_mode_switch_set, + }; + + port->mode_switch = typec_mux_register(port->sdata->dev, &mode_switch_desc); + + return PTR_ERR_OR_ZERO(port->mode_switch); +} + +static int cros_typec_register_retimer(struct cros_typec_port *port, struct fwnode_handle *fwnode) +{ + struct typec_retimer_desc retimer_desc = { + .fwnode = fwnode, + .drvdata = port, + .name = fwnode_get_name(fwnode), + .set = cros_typec_retimer_set, + }; + + port->retimer = typec_retimer_register(port->sdata->dev, &retimer_desc); + + return PTR_ERR_OR_ZERO(port->retimer); +} + +static int cros_typec_register_switches(struct cros_typec_switch_data *sdata) +{ + struct cros_typec_port *port; + struct device *dev = sdata->dev; + struct fwnode_handle *fwnode; + int nports, ret; + u64 index; + + nports = device_get_child_node_count(dev); + if (nports == 0) { + dev_err(dev, "No switch devices found.\n"); + return -ENODEV; + } + + device_for_each_child_node(dev, fwnode) { + port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL); + if (!port) { + ret = -ENOMEM; + goto err_switch; + } + + ret = acpi_get_local_u64_address(ACPI_HANDLE_FWNODE(fwnode), &index); + if (ret) { + dev_err(dev, "_ADR wasn't evaluated for %pfwP\n", fwnode); + goto err_switch; + } + + if (index >= EC_USB_PD_MAX_PORTS) { + dev_err(dev, "%pfwP: Invalid port index number: %llu\n", fwnode, index); + ret = -EINVAL; + goto err_switch; + } + port->sdata = sdata; + port->port_num = index; + sdata->ports[index] = port; + + if (fwnode_property_present(fwnode, "retimer-switch")) { + ret = cros_typec_register_retimer(port, fwnode); + if (ret) { + dev_err(dev, "Retimer switch register failed\n"); + goto err_switch; + } + + dev_dbg(dev, "Retimer switch registered for index %llu\n", index); + } + + if (!fwnode_property_present(fwnode, "mode-switch")) + continue; + + ret = cros_typec_register_mode_switch(port, fwnode); + if (ret) { + dev_err(dev, "Mode switch register failed\n"); + goto err_switch; + } + + dev_dbg(dev, "Mode switch registered for index %llu\n", index); + } + + return 0; +err_switch: + fwnode_handle_put(fwnode); + cros_typec_unregister_switches(sdata); + return ret; +} + +static int cros_typec_switch_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cros_typec_switch_data *sdata; + + sdata = devm_kzalloc(dev, sizeof(*sdata), GFP_KERNEL); + if (!sdata) + return -ENOMEM; + + sdata->dev = dev; + sdata->ec = dev_get_drvdata(pdev->dev.parent); + + platform_set_drvdata(pdev, sdata); + + return cros_typec_register_switches(sdata); +} + +static void cros_typec_switch_remove(struct platform_device *pdev) +{ + struct cros_typec_switch_data *sdata = platform_get_drvdata(pdev); + + cros_typec_unregister_switches(sdata); +} + +#ifdef CONFIG_ACPI +static const struct acpi_device_id cros_typec_switch_acpi_id[] = { + { "GOOG001A", 0 }, + {} +}; +MODULE_DEVICE_TABLE(acpi, cros_typec_switch_acpi_id); +#endif + +static struct platform_driver cros_typec_switch_driver = { + .driver = { + .name = "cros-typec-switch", + .acpi_match_table = ACPI_PTR(cros_typec_switch_acpi_id), + }, + .probe = cros_typec_switch_probe, + .remove = cros_typec_switch_remove, +}; + +module_platform_driver(cros_typec_switch_driver); + +MODULE_AUTHOR("Prashant Malani "); +MODULE_DESCRIPTION("ChromeOS EC Type-C Switch control"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/cros_typec_vdm.c b/drivers/platform/chrome/cros_typec_vdm.c new file mode 100644 index 000000000..3f632fd35 --- /dev/null +++ b/drivers/platform/chrome/cros_typec_vdm.c @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * USB Power Delivery Vendor Defined Message (VDM) support code. + * + * Copyright 2023 Google LLC + * Author: Prashant Malani + */ + +#include +#include +#include + +#include "cros_ec_typec.h" +#include "cros_typec_vdm.h" + +/* + * Retrieves pending VDM attention messages from the EC and forwards them to the altmode driver + * based on SVID. + */ +void cros_typec_handle_vdm_attention(struct cros_typec_data *typec, int port_num) +{ + struct ec_response_typec_vdm_response resp; + struct ec_params_typec_vdm_response req = { + .port = port_num, + }; + struct typec_altmode *amode; + u16 svid; + u32 hdr; + int ret; + + do { + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_VDM_RESPONSE, &req, + sizeof(req), &resp, sizeof(resp)); + if (ret < 0) { + dev_warn(typec->dev, "Failed VDM response fetch, port: %d\n", port_num); + return; + } + + hdr = resp.vdm_response[0]; + svid = PD_VDO_VID(hdr); + dev_dbg(typec->dev, "Received VDM Attention header: %x, port: %d\n", hdr, port_num); + + amode = typec_match_altmode(typec->ports[port_num]->port_altmode, + CROS_EC_ALTMODE_MAX, svid, PD_VDO_OPOS(hdr)); + if (!amode) { + dev_err(typec->dev, + "Received VDM for unregistered altmode (SVID:%x), port: %d\n", + svid, port_num); + return; + } + + typec_altmode_attention(amode, resp.vdm_attention[1]); + } while (resp.vdm_attention_left); +} + +/* + * Retrieves a VDM response from the EC and forwards it to the altmode driver based on SVID. + */ +void cros_typec_handle_vdm_response(struct cros_typec_data *typec, int port_num) +{ + struct ec_response_typec_vdm_response resp; + struct ec_params_typec_vdm_response req = { + .port = port_num, + }; + struct typec_altmode *amode; + u16 svid; + u32 hdr; + int ret; + + ret = cros_ec_cmd(typec->ec, 0, EC_CMD_TYPEC_VDM_RESPONSE, &req, + sizeof(req), &resp, sizeof(resp)); + if (ret < 0) { + dev_warn(typec->dev, "Failed VDM response fetch, port: %d\n", port_num); + return; + } + + hdr = resp.vdm_response[0]; + svid = PD_VDO_VID(hdr); + dev_dbg(typec->dev, "Received VDM header: %x, port: %d\n", hdr, port_num); + + amode = typec_match_altmode(typec->ports[port_num]->port_altmode, CROS_EC_ALTMODE_MAX, + svid, PD_VDO_OPOS(hdr)); + if (!amode) { + dev_err(typec->dev, "Received VDM for unregistered altmode (SVID:%x), port: %d\n", + svid, port_num); + return; + } + + ret = typec_altmode_vdm(amode, hdr, &resp.vdm_response[1], resp.vdm_data_objects); + if (ret) + dev_err(typec->dev, "Failed to forward VDM to altmode (SVID:%x), port: %d\n", + svid, port_num); +} + +static int cros_typec_port_amode_enter(struct typec_altmode *amode, u32 *vdo) +{ + struct cros_typec_port *port = typec_altmode_get_drvdata(amode); + struct ec_params_typec_control req = { + .port = port->port_num, + .command = TYPEC_CONTROL_COMMAND_SEND_VDM_REQ, + }; + struct typec_vdm_req vdm_req = {}; + u32 hdr; + + hdr = VDO(amode->svid, 1, SVDM_VER_2_0, CMD_ENTER_MODE); + hdr |= VDO_OPOS(amode->mode); + + vdm_req.vdm_data[0] = hdr; + vdm_req.vdm_data_objects = 1; + vdm_req.partner_type = TYPEC_PARTNER_SOP; + req.vdm_req_params = vdm_req; + + dev_dbg(port->typec_data->dev, "Sending EnterMode VDM, hdr: %x, port: %d\n", + hdr, port->port_num); + + return cros_ec_cmd(port->typec_data->ec, 0, EC_CMD_TYPEC_CONTROL, &req, + sizeof(req), NULL, 0); +} + +static int cros_typec_port_amode_vdm(struct typec_altmode *amode, const u32 hdr, + const u32 *vdo, int cnt) +{ + struct cros_typec_port *port = typec_altmode_get_drvdata(amode); + struct ec_params_typec_control req = { + .port = port->port_num, + .command = TYPEC_CONTROL_COMMAND_SEND_VDM_REQ, + }; + struct typec_vdm_req vdm_req = {}; + int i; + + vdm_req.vdm_data[0] = hdr; + vdm_req.vdm_data_objects = cnt; + for (i = 1; i < cnt; i++) + vdm_req.vdm_data[i] = vdo[i-1]; + vdm_req.partner_type = TYPEC_PARTNER_SOP; + req.vdm_req_params = vdm_req; + + dev_dbg(port->typec_data->dev, "Sending VDM, hdr: %x, num_objects: %d, port: %d\n", + hdr, cnt, port->port_num); + + return cros_ec_cmd(port->typec_data->ec, 0, EC_CMD_TYPEC_CONTROL, &req, + sizeof(req), NULL, 0); +} + +const struct typec_altmode_ops port_amode_ops = { + .enter = cros_typec_port_amode_enter, + .vdm = cros_typec_port_amode_vdm, +}; diff --git a/drivers/platform/chrome/cros_typec_vdm.h b/drivers/platform/chrome/cros_typec_vdm.h new file mode 100644 index 000000000..631bd2ce4 --- /dev/null +++ b/drivers/platform/chrome/cros_typec_vdm.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef __CROS_TYPEC_VDM__ +#define __CROS_TYPEC_VDM__ + +#include + +extern const struct typec_altmode_ops port_amode_ops; + +void cros_typec_handle_vdm_attention(struct cros_typec_data *typec, int port_num); +void cros_typec_handle_vdm_response(struct cros_typec_data *typec, int port_num); + +#endif /* __CROS_TYPEC_VDM__ */ diff --git a/drivers/platform/chrome/cros_usbpd_logger.c b/drivers/platform/chrome/cros_usbpd_logger.c new file mode 100644 index 000000000..0eb714d67 --- /dev/null +++ b/drivers/platform/chrome/cros_usbpd_logger.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Logging driver for ChromeOS EC based USBPD Charger. + * + * Copyright 2018 Google LLC. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-usbpd-logger" + +#define CROS_USBPD_MAX_LOG_ENTRIES 30 +#define CROS_USBPD_LOG_UPDATE_DELAY msecs_to_jiffies(60000) +#define CROS_USBPD_DATA_SIZE 16 +#define CROS_USBPD_LOG_RESP_SIZE (sizeof(struct ec_response_pd_log) + \ + CROS_USBPD_DATA_SIZE) +#define CROS_USBPD_BUFFER_SIZE (sizeof(struct cros_ec_command) + \ + CROS_USBPD_LOG_RESP_SIZE) +/* Buffer for building the PDLOG string */ +#define BUF_SIZE 80 + +struct logger_data { + struct device *dev; + struct cros_ec_dev *ec_dev; + u8 ec_buffer[CROS_USBPD_BUFFER_SIZE]; + struct delayed_work log_work; + struct workqueue_struct *log_workqueue; +}; + +static const char * const chg_type_names[] = { + "None", "PD", "Type-C", "Proprietary", "DCP", "CDP", "SDP", + "Other", "VBUS" +}; + +static const char * const role_names[] = { + "Disconnected", "SRC", "SNK", "SNK (not charging)" +}; + +static const char * const fault_names[] = { + "---", "OCP", "fast OCP", "OVP", "Discharge" +}; + +__printf(3, 4) +static int append_str(char *buf, int pos, const char *fmt, ...) +{ + va_list args; + int i; + + va_start(args, fmt); + i = vsnprintf(buf + pos, BUF_SIZE - pos, fmt, args); + va_end(args); + + return i; +} + +static struct ec_response_pd_log *ec_get_log_entry(struct logger_data *logger) +{ + struct cros_ec_dev *ec_dev = logger->ec_dev; + struct cros_ec_command *msg; + int ret; + + msg = (struct cros_ec_command *)logger->ec_buffer; + + msg->command = ec_dev->cmd_offset + EC_CMD_PD_GET_LOG_ENTRY; + msg->insize = CROS_USBPD_LOG_RESP_SIZE; + + ret = cros_ec_cmd_xfer_status(ec_dev->ec_dev, msg); + if (ret < 0) + return ERR_PTR(ret); + + return (struct ec_response_pd_log *)msg->data; +} + +static void cros_usbpd_print_log_entry(struct ec_response_pd_log *r, + ktime_t tstamp) +{ + const char *fault, *role, *chg_type; + struct usb_chg_measures *meas; + struct mcdp_info *minfo; + int role_idx, type_idx; + char buf[BUF_SIZE + 1]; + struct rtc_time rt; + int len = 0; + s32 rem; + int i; + + /* The timestamp is the number of 1024th of seconds in the past */ + tstamp = ktime_sub_us(tstamp, r->timestamp << PD_LOG_TIMESTAMP_SHIFT); + rt = rtc_ktime_to_tm(tstamp); + + switch (r->type) { + case PD_EVENT_MCU_CHARGE: + if (r->data & CHARGE_FLAGS_OVERRIDE) + len += append_str(buf, len, "override "); + + if (r->data & CHARGE_FLAGS_DELAYED_OVERRIDE) + len += append_str(buf, len, "pending_override "); + + role_idx = r->data & CHARGE_FLAGS_ROLE_MASK; + role = role_idx < ARRAY_SIZE(role_names) ? + role_names[role_idx] : "Unknown"; + + type_idx = (r->data & CHARGE_FLAGS_TYPE_MASK) + >> CHARGE_FLAGS_TYPE_SHIFT; + + chg_type = type_idx < ARRAY_SIZE(chg_type_names) ? + chg_type_names[type_idx] : "???"; + + if (role_idx == USB_PD_PORT_POWER_DISCONNECTED || + role_idx == USB_PD_PORT_POWER_SOURCE) { + len += append_str(buf, len, "%s", role); + break; + } + + meas = (struct usb_chg_measures *)r->payload; + len += append_str(buf, len, "%s %s %s %dmV max %dmV / %dmA", + role, r->data & CHARGE_FLAGS_DUAL_ROLE ? + "DRP" : "Charger", + chg_type, meas->voltage_now, + meas->voltage_max, meas->current_max); + break; + case PD_EVENT_ACC_RW_FAIL: + len += append_str(buf, len, "RW signature check failed"); + break; + case PD_EVENT_PS_FAULT: + fault = r->data < ARRAY_SIZE(fault_names) ? fault_names[r->data] + : "???"; + len += append_str(buf, len, "Power supply fault: %s", fault); + break; + case PD_EVENT_VIDEO_DP_MODE: + len += append_str(buf, len, "DP mode %s", + str_enabled_disabled(r->data == 1)); + break; + case PD_EVENT_VIDEO_CODEC: + minfo = (struct mcdp_info *)r->payload; + len += append_str(buf, len, "HDMI info: family:%04x chipid:%04x ", + MCDP_FAMILY(minfo->family), + MCDP_CHIPID(minfo->chipid)); + len += append_str(buf, len, "irom:%d.%d.%d fw:%d.%d.%d", + minfo->irom.major, minfo->irom.minor, + minfo->irom.build, minfo->fw.major, + minfo->fw.minor, minfo->fw.build); + break; + default: + len += append_str(buf, len, "Event %02x (%04x) [", r->type, + r->data); + + for (i = 0; i < PD_LOG_SIZE(r->size_port); i++) + len += append_str(buf, len, "%02x ", r->payload[i]); + + len += append_str(buf, len, "]"); + break; + } + + div_s64_rem(ktime_to_ms(tstamp), MSEC_PER_SEC, &rem); + pr_info("PDLOG %d/%02d/%02d %02d:%02d:%02d.%03d P%d %s\n", + rt.tm_year + 1900, rt.tm_mon + 1, rt.tm_mday, + rt.tm_hour, rt.tm_min, rt.tm_sec, rem, + PD_LOG_PORT(r->size_port), buf); +} + +static void cros_usbpd_log_check(struct work_struct *work) +{ + struct logger_data *logger = container_of(to_delayed_work(work), + struct logger_data, + log_work); + struct device *dev = logger->dev; + struct ec_response_pd_log *r; + int entries = 0; + ktime_t now; + + while (entries++ < CROS_USBPD_MAX_LOG_ENTRIES) { + r = ec_get_log_entry(logger); + now = ktime_get_real(); + if (IS_ERR(r)) { + dev_dbg(dev, "Cannot get PD log %ld\n", PTR_ERR(r)); + break; + } + if (r->type == PD_EVENT_NO_ENTRY) + break; + + cros_usbpd_print_log_entry(r, now); + } + + queue_delayed_work(logger->log_workqueue, &logger->log_work, + CROS_USBPD_LOG_UPDATE_DELAY); +} + +static int cros_usbpd_logger_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + struct device *dev = &pd->dev; + struct logger_data *logger; + int ret; + + logger = devm_kzalloc(dev, sizeof(*logger), GFP_KERNEL); + if (!logger) + return -ENOMEM; + + logger->dev = dev; + logger->ec_dev = ec_dev; + + platform_set_drvdata(pd, logger); + + /* Retrieve PD event logs periodically */ + logger->log_workqueue = devm_alloc_ordered_workqueue(dev, "cros_usbpd_log", 0); + if (!logger->log_workqueue) + return -ENOMEM; + + ret = devm_delayed_work_autocancel(dev, &logger->log_work, cros_usbpd_log_check); + if (ret) + return ret; + + queue_delayed_work(logger->log_workqueue, &logger->log_work, + CROS_USBPD_LOG_UPDATE_DELAY); + + return 0; +} + +static int __maybe_unused cros_usbpd_logger_resume(struct device *dev) +{ + struct logger_data *logger = dev_get_drvdata(dev); + + queue_delayed_work(logger->log_workqueue, &logger->log_work, + CROS_USBPD_LOG_UPDATE_DELAY); + + return 0; +} + +static int __maybe_unused cros_usbpd_logger_suspend(struct device *dev) +{ + struct logger_data *logger = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&logger->log_work); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(cros_usbpd_logger_pm_ops, cros_usbpd_logger_suspend, + cros_usbpd_logger_resume); + +static const struct platform_device_id cros_usbpd_logger_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_usbpd_logger_id); + +static struct platform_driver cros_usbpd_logger_driver = { + .driver = { + .name = DRV_NAME, + .pm = &cros_usbpd_logger_pm_ops, + }, + .probe = cros_usbpd_logger_probe, + .id_table = cros_usbpd_logger_id, +}; + +module_platform_driver(cros_usbpd_logger_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Logging driver for ChromeOS EC USBPD Charger."); diff --git a/drivers/platform/chrome/cros_usbpd_notify.c b/drivers/platform/chrome/cros_usbpd_notify.c new file mode 100644 index 000000000..d842c05f4 --- /dev/null +++ b/drivers/platform/chrome/cros_usbpd_notify.c @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2020 Google LLC + * + * This driver serves as the receiver of cros_ec PD host events. + */ + +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "cros-usbpd-notify" +#define DRV_NAME_PLAT_ACPI "cros-usbpd-notify-acpi" +#define ACPI_DRV_NAME "GOOG0003" +#define CREC_DRV_NAME "GOOG0004" + +static BLOCKING_NOTIFIER_HEAD(cros_usbpd_notifier_list); + +struct cros_usbpd_notify_data { + struct device *dev; + struct cros_ec_device *ec; + struct notifier_block nb; +}; + +/** + * cros_usbpd_register_notify - Register a notifier callback for PD events. + * @nb: Notifier block pointer to register + * + * On ACPI platforms this corresponds to host events on the ECPD + * "GOOG0003" ACPI device. On non-ACPI platforms this will filter mkbp events + * for USB PD events. + * + * Return: 0 on success or negative error code. + */ +int cros_usbpd_register_notify(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&cros_usbpd_notifier_list, + nb); +} +EXPORT_SYMBOL_GPL(cros_usbpd_register_notify); + +/** + * cros_usbpd_unregister_notify - Unregister notifier callback for PD events. + * @nb: Notifier block pointer to unregister + * + * Unregister a notifier callback that was previously registered with + * cros_usbpd_register_notify(). + */ +void cros_usbpd_unregister_notify(struct notifier_block *nb) +{ + blocking_notifier_chain_unregister(&cros_usbpd_notifier_list, nb); +} +EXPORT_SYMBOL_GPL(cros_usbpd_unregister_notify); + +static void cros_usbpd_get_event_and_notify(struct device *dev, + struct cros_ec_device *ec_dev) +{ + struct ec_response_host_event_status host_event_status; + u32 event = 0; + int ret; + + /* + * We still send a 0 event out to older devices which don't + * have the updated device heirarchy. + */ + if (!ec_dev) { + dev_dbg(dev, + "EC device inaccessible; sending 0 event status.\n"); + goto send_notify; + } + + /* Check for PD host events on EC. */ + ret = cros_ec_cmd(ec_dev, 0, EC_CMD_PD_HOST_EVENT_STATUS, + NULL, 0, &host_event_status, sizeof(host_event_status)); + if (ret < 0) { + dev_warn(dev, "Can't get host event status (err: %d)\n", ret); + goto send_notify; + } + + event = host_event_status.status; + +send_notify: + blocking_notifier_call_chain(&cros_usbpd_notifier_list, event, NULL); +} + +#ifdef CONFIG_ACPI + +static void cros_usbpd_notify_acpi(acpi_handle device, u32 event, void *data) +{ + struct cros_usbpd_notify_data *pdnotify = data; + + cros_usbpd_get_event_and_notify(pdnotify->dev, pdnotify->ec); +} + +static int cros_usbpd_notify_probe_acpi(struct platform_device *pdev) +{ + struct cros_usbpd_notify_data *pdnotify; + struct device *dev = &pdev->dev; + struct acpi_device *adev, *parent_adev; + struct cros_ec_device *ec_dev; + struct fwnode_handle *parent_fwnode; + acpi_status status; + + adev = ACPI_COMPANION(dev); + + pdnotify = devm_kzalloc(dev, sizeof(*pdnotify), GFP_KERNEL); + if (!pdnotify) + return -ENOMEM; + + /* Get the EC device pointer needed to talk to the EC. */ + ec_dev = dev_get_drvdata(dev->parent); + if (!ec_dev) { + /* + * We continue even for older devices which don't have the + * correct device heirarchy, namely, GOOG0003 is a child + * of GOOG0004. If GOOG0003 is a child of GOOG0004 and we + * can't get a pointer to the Chrome EC device, defer the + * probe function. + */ + parent_fwnode = fwnode_get_parent(dev->fwnode); + if (parent_fwnode) { + parent_adev = to_acpi_device_node(parent_fwnode); + if (parent_adev && + acpi_dev_hid_match(parent_adev, CREC_DRV_NAME)) { + return -EPROBE_DEFER; + } + } + dev_warn(dev, "Couldn't get Chrome EC device pointer.\n"); + } + + pdnotify->dev = dev; + pdnotify->ec = ec_dev; + + status = acpi_install_notify_handler(adev->handle, + ACPI_ALL_NOTIFY, + cros_usbpd_notify_acpi, + pdnotify); + if (ACPI_FAILURE(status)) { + dev_warn(dev, "Failed to register notify handler %08x\n", + status); + return -EINVAL; + } + + return 0; +} + +static void cros_usbpd_notify_remove_acpi(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct acpi_device *adev = ACPI_COMPANION(dev); + + acpi_remove_notify_handler(adev->handle, ACPI_ALL_NOTIFY, + cros_usbpd_notify_acpi); +} + +static const struct acpi_device_id cros_usbpd_notify_acpi_device_ids[] = { + { ACPI_DRV_NAME, 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, cros_usbpd_notify_acpi_device_ids); + +static struct platform_driver cros_usbpd_notify_acpi_driver = { + .driver = { + .name = DRV_NAME_PLAT_ACPI, + .acpi_match_table = cros_usbpd_notify_acpi_device_ids, + }, + .probe = cros_usbpd_notify_probe_acpi, + .remove = cros_usbpd_notify_remove_acpi, +}; + +#endif /* CONFIG_ACPI */ + +static int cros_usbpd_notify_plat(struct notifier_block *nb, + unsigned long queued_during_suspend, + void *data) +{ + struct cros_usbpd_notify_data *pdnotify = container_of(nb, + struct cros_usbpd_notify_data, nb); + struct cros_ec_device *ec_dev = (struct cros_ec_device *)data; + u32 host_event = cros_ec_get_host_event(ec_dev); + + if (!host_event) + return NOTIFY_DONE; + + if (host_event & (EC_HOST_EVENT_MASK(EC_HOST_EVENT_PD_MCU) | + EC_HOST_EVENT_MASK(EC_HOST_EVENT_USB_MUX))) { + cros_usbpd_get_event_and_notify(pdnotify->dev, ec_dev); + return NOTIFY_OK; + } + return NOTIFY_DONE; +} + +static int cros_usbpd_notify_probe_plat(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cros_ec_dev *ecdev = dev_get_drvdata(dev->parent); + struct cros_usbpd_notify_data *pdnotify; + int ret; + + pdnotify = devm_kzalloc(dev, sizeof(*pdnotify), GFP_KERNEL); + if (!pdnotify) + return -ENOMEM; + + pdnotify->dev = dev; + pdnotify->ec = ecdev->ec_dev; + pdnotify->nb.notifier_call = cros_usbpd_notify_plat; + + dev_set_drvdata(dev, pdnotify); + + ret = blocking_notifier_chain_register(&ecdev->ec_dev->event_notifier, + &pdnotify->nb); + if (ret < 0) { + dev_err(dev, "Failed to register notifier\n"); + return ret; + } + + return 0; +} + +static void cros_usbpd_notify_remove_plat(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cros_ec_dev *ecdev = dev_get_drvdata(dev->parent); + struct cros_usbpd_notify_data *pdnotify = + (struct cros_usbpd_notify_data *)dev_get_drvdata(dev); + + blocking_notifier_chain_unregister(&ecdev->ec_dev->event_notifier, + &pdnotify->nb); +} + +static const struct platform_device_id cros_usbpd_notify_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, cros_usbpd_notify_id); + +static struct platform_driver cros_usbpd_notify_plat_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = cros_usbpd_notify_probe_plat, + .remove = cros_usbpd_notify_remove_plat, + .id_table = cros_usbpd_notify_id, +}; + +static int __init cros_usbpd_notify_init(void) +{ + int ret; + + ret = platform_driver_register(&cros_usbpd_notify_plat_driver); + if (ret < 0) + return ret; + +#ifdef CONFIG_ACPI + ret = platform_driver_register(&cros_usbpd_notify_acpi_driver); + if (ret) { + platform_driver_unregister(&cros_usbpd_notify_plat_driver); + return ret; + } +#endif + return 0; +} + +static void __exit cros_usbpd_notify_exit(void) +{ +#ifdef CONFIG_ACPI + platform_driver_unregister(&cros_usbpd_notify_acpi_driver); +#endif + platform_driver_unregister(&cros_usbpd_notify_plat_driver); +} + +module_init(cros_usbpd_notify_init); +module_exit(cros_usbpd_notify_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS power delivery notifier device"); +MODULE_AUTHOR("Jon Flatley "); diff --git a/drivers/platform/chrome/wilco_ec/Kconfig b/drivers/platform/chrome/wilco_ec/Kconfig new file mode 100644 index 000000000..d1648fb09 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/Kconfig @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: GPL-2.0-only +config WILCO_EC + tristate "ChromeOS Wilco Embedded Controller" + depends on X86 || COMPILE_TEST + depends on ACPI && CROS_EC_LPC && LEDS_CLASS + depends on HAS_IOPORT + help + If you say Y here, you get support for talking to the ChromeOS + Wilco EC over an eSPI bus. This uses a simple byte-level protocol + with a checksum. + + To compile this driver as a module, choose M here: the + module will be called wilco_ec. + +config WILCO_EC_DEBUGFS + tristate "Enable raw access to EC via debugfs" + depends on WILCO_EC + help + If you say Y here, you get support for sending raw commands to + the Wilco EC via debugfs. These commands do not do any byte + manipulation and allow for testing arbitrary commands. This + interface is intended for debug only and will not be present + on production devices. + +config WILCO_EC_EVENTS + tristate "Enable event forwarding from EC to userspace" + depends on WILCO_EC + help + If you say Y here, you get support for the EC to send events + (such as power state changes) to userspace. The EC sends the events + over ACPI, and a driver queues up the events to be read by a + userspace daemon from /dev/wilco_event using read() and poll(). + +config WILCO_EC_TELEMETRY + tristate "Enable querying telemetry data from EC" + depends on WILCO_EC + help + If you say Y here, you get support to query EC telemetry data from + /dev/wilco_telem0 using write() and then read(). diff --git a/drivers/platform/chrome/wilco_ec/Makefile b/drivers/platform/chrome/wilco_ec/Makefile new file mode 100644 index 000000000..ecb3145ca --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 + +wilco_ec-objs := core.o keyboard_leds.o mailbox.o \ + properties.o sysfs.o +obj-$(CONFIG_WILCO_EC) += wilco_ec.o +wilco_ec_debugfs-objs := debugfs.o +obj-$(CONFIG_WILCO_EC_DEBUGFS) += wilco_ec_debugfs.o +wilco_ec_events-objs := event.o +obj-$(CONFIG_WILCO_EC_EVENTS) += wilco_ec_events.o +wilco_ec_telem-objs := telemetry.o +obj-$(CONFIG_WILCO_EC_TELEMETRY) += wilco_ec_telem.o diff --git a/drivers/platform/chrome/wilco_ec/core.c b/drivers/platform/chrome/wilco_ec/core.c new file mode 100644 index 000000000..9d4521889 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/core.c @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Core driver for Wilco Embedded Controller + * + * Copyright 2018 Google LLC + * + * This is the entry point for the drivers that control the Wilco EC. + */ + +#include +#include +#include +#include +#include +#include + +#include "../cros_ec_lpc_mec.h" + +#define DRV_NAME "wilco-ec" + +static struct resource *wilco_get_resource(struct platform_device *pdev, + int index) +{ + struct device *dev = &pdev->dev; + struct resource *res; + + res = platform_get_resource(pdev, IORESOURCE_IO, index); + if (!res) { + dev_dbg(dev, "Couldn't find IO resource %d\n", index); + return res; + } + + return devm_request_region(dev, res->start, resource_size(res), + dev_name(dev)); +} + +static int wilco_ec_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct wilco_ec_device *ec; + int ret; + + ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); + if (!ec) + return -ENOMEM; + + platform_set_drvdata(pdev, ec); + ec->dev = dev; + mutex_init(&ec->mailbox_lock); + + ec->data_size = sizeof(struct wilco_ec_response) + EC_MAILBOX_DATA_SIZE; + ec->data_buffer = devm_kzalloc(dev, ec->data_size, GFP_KERNEL); + if (!ec->data_buffer) + return -ENOMEM; + + /* Prepare access to IO regions provided by ACPI */ + ec->io_data = wilco_get_resource(pdev, 0); /* Host Data */ + ec->io_command = wilco_get_resource(pdev, 1); /* Host Command */ + ec->io_packet = wilco_get_resource(pdev, 2); /* MEC EMI */ + if (!ec->io_data || !ec->io_command || !ec->io_packet) + return -ENODEV; + + /* Initialize cros_ec register interface for communication */ + cros_ec_lpc_mec_init(ec->io_packet->start, + ec->io_packet->start + EC_MAILBOX_DATA_SIZE); + + /* + * Register a child device that will be found by the debugfs driver. + * Ignore failure. + */ + ec->debugfs_pdev = platform_device_register_data(dev, + "wilco-ec-debugfs", + PLATFORM_DEVID_AUTO, + NULL, 0); + + /* Register a child device that will be found by the RTC driver. */ + ec->rtc_pdev = platform_device_register_data(dev, "rtc-wilco-ec", + PLATFORM_DEVID_AUTO, + NULL, 0); + if (IS_ERR(ec->rtc_pdev)) { + dev_err(dev, "Failed to create RTC platform device\n"); + ret = PTR_ERR(ec->rtc_pdev); + goto unregister_debugfs; + } + + /* Set up the keyboard backlight LEDs. */ + ret = wilco_keyboard_leds_init(ec); + if (ret < 0) { + dev_err(dev, + "Failed to initialize keyboard LEDs: %d\n", + ret); + goto unregister_rtc; + } + + ret = wilco_ec_add_sysfs(ec); + if (ret < 0) { + dev_err(dev, "Failed to create sysfs entries: %d\n", ret); + goto unregister_rtc; + } + + /* Register child device to be found by charger config driver. */ + ec->charger_pdev = platform_device_register_data(dev, "wilco-charger", + PLATFORM_DEVID_AUTO, + NULL, 0); + if (IS_ERR(ec->charger_pdev)) { + dev_err(dev, "Failed to create charger platform device\n"); + ret = PTR_ERR(ec->charger_pdev); + goto remove_sysfs; + } + + /* Register child device that will be found by the telemetry driver. */ + ec->telem_pdev = platform_device_register_data(dev, "wilco_telem", + PLATFORM_DEVID_AUTO, + ec, sizeof(*ec)); + if (IS_ERR(ec->telem_pdev)) { + dev_err(dev, "Failed to create telemetry platform device\n"); + ret = PTR_ERR(ec->telem_pdev); + goto unregister_charge_config; + } + + return 0; + +unregister_charge_config: + platform_device_unregister(ec->charger_pdev); +remove_sysfs: + wilco_ec_remove_sysfs(ec); +unregister_rtc: + platform_device_unregister(ec->rtc_pdev); +unregister_debugfs: + if (ec->debugfs_pdev) + platform_device_unregister(ec->debugfs_pdev); + return ret; +} + +static void wilco_ec_remove(struct platform_device *pdev) +{ + struct wilco_ec_device *ec = platform_get_drvdata(pdev); + + platform_device_unregister(ec->telem_pdev); + platform_device_unregister(ec->charger_pdev); + wilco_ec_remove_sysfs(ec); + platform_device_unregister(ec->rtc_pdev); + if (ec->debugfs_pdev) + platform_device_unregister(ec->debugfs_pdev); +} + +static const struct acpi_device_id wilco_ec_acpi_device_ids[] = { + { "GOOG000C", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, wilco_ec_acpi_device_ids); + +static const struct platform_device_id wilco_ec_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, wilco_ec_id); + +static struct platform_driver wilco_ec_driver = { + .driver = { + .name = DRV_NAME, + .acpi_match_table = wilco_ec_acpi_device_ids, + }, + .probe = wilco_ec_probe, + .remove = wilco_ec_remove, + .id_table = wilco_ec_id, +}; + +module_platform_driver(wilco_ec_driver); + +MODULE_AUTHOR("Nick Crews "); +MODULE_AUTHOR("Duncan Laurie "); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ChromeOS Wilco Embedded Controller driver"); diff --git a/drivers/platform/chrome/wilco_ec/debugfs.c b/drivers/platform/chrome/wilco_ec/debugfs.c new file mode 100644 index 000000000..32fb11939 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/debugfs.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * debugfs attributes for Wilco EC + * + * Copyright 2019 Google LLC + * + * See Documentation/ABI/testing/debugfs-wilco-ec for usage. + */ + +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "wilco-ec-debugfs" + +/* The raw bytes will take up more space when represented as a hex string */ +#define FORMATTED_BUFFER_SIZE (EC_MAILBOX_DATA_SIZE * 4) + +struct wilco_ec_debugfs { + struct wilco_ec_device *ec; + struct dentry *dir; + size_t response_size; + u8 raw_data[EC_MAILBOX_DATA_SIZE]; + u8 formatted_data[FORMATTED_BUFFER_SIZE]; +}; +static struct wilco_ec_debugfs *debug_info; + +/** + * parse_hex_sentence() - Convert a ascii hex representation into byte array. + * @in: Input buffer of ascii. + * @isize: Length of input buffer. + * @out: Output buffer. + * @osize: Length of output buffer, e.g. max number of bytes to parse. + * + * An valid input is a series of ascii hexadecimal numbers, separated by spaces. + * An example valid input is + * " 00 f2 0 000076 6 0 ff" + * + * If an individual "word" within the hex sentence is longer than MAX_WORD_SIZE, + * then the sentence is illegal, and parsing will fail. + * + * Return: Number of bytes parsed, or negative error code on failure. + */ +static int parse_hex_sentence(const char *in, int isize, u8 *out, int osize) +{ + int n_parsed = 0; + int word_start = 0; + int word_end; + int word_len; + /* Temp buffer for holding a "word" of chars that represents one byte */ + #define MAX_WORD_SIZE 16 + char tmp[MAX_WORD_SIZE + 1]; + u8 byte; + + while (word_start < isize && n_parsed < osize) { + /* Find the start of the next word */ + while (word_start < isize && isspace(in[word_start])) + word_start++; + /* reached the end of the input before next word? */ + if (word_start >= isize) + break; + + /* Find the end of this word */ + word_end = word_start; + while (word_end < isize && !isspace(in[word_end])) + word_end++; + + /* Copy to a tmp NULL terminated string */ + word_len = word_end - word_start; + if (word_len > MAX_WORD_SIZE) + return -EINVAL; + memcpy(tmp, in + word_start, word_len); + tmp[word_len] = '\0'; + + /* + * Convert from hex string, place in output. If fails to parse, + * just return -EINVAL because specific error code is only + * relevant for this one word, returning it would be confusing. + */ + if (kstrtou8(tmp, 16, &byte)) + return -EINVAL; + out[n_parsed++] = byte; + + word_start = word_end; + } + return n_parsed; +} + +/* The message type takes up two bytes*/ +#define TYPE_AND_DATA_SIZE ((EC_MAILBOX_DATA_SIZE) + 2) + +static ssize_t raw_write(struct file *file, const char __user *user_buf, + size_t count, loff_t *ppos) +{ + char *buf = debug_info->formatted_data; + struct wilco_ec_message msg; + u8 request_data[TYPE_AND_DATA_SIZE]; + ssize_t kcount; + int ret; + + if (count > FORMATTED_BUFFER_SIZE) + return -EINVAL; + + kcount = simple_write_to_buffer(buf, FORMATTED_BUFFER_SIZE, ppos, + user_buf, count); + if (kcount < 0) + return kcount; + + ret = parse_hex_sentence(buf, kcount, request_data, TYPE_AND_DATA_SIZE); + if (ret < 0) + return ret; + /* Need at least two bytes for message type and one byte of data */ + if (ret < 3) + return -EINVAL; + + msg.type = request_data[0] << 8 | request_data[1]; + msg.flags = 0; + msg.request_data = request_data + 2; + msg.request_size = ret - 2; + memset(debug_info->raw_data, 0, sizeof(debug_info->raw_data)); + msg.response_data = debug_info->raw_data; + msg.response_size = EC_MAILBOX_DATA_SIZE; + + ret = wilco_ec_mailbox(debug_info->ec, &msg); + if (ret < 0) + return ret; + debug_info->response_size = ret; + + return count; +} + +static ssize_t raw_read(struct file *file, char __user *user_buf, size_t count, + loff_t *ppos) +{ + int fmt_len = 0; + + if (debug_info->response_size) { + fmt_len = hex_dump_to_buffer(debug_info->raw_data, + debug_info->response_size, + 16, 1, debug_info->formatted_data, + sizeof(debug_info->formatted_data), + true); + /* Only return response the first time it is read */ + debug_info->response_size = 0; + } + + return simple_read_from_buffer(user_buf, count, ppos, + debug_info->formatted_data, fmt_len); +} + +static const struct file_operations fops_raw = { + .owner = THIS_MODULE, + .read = raw_read, + .write = raw_write, +}; + +#define CMD_KB_CHROME 0x88 +#define SUB_CMD_H1_GPIO 0x0A +#define SUB_CMD_TEST_EVENT 0x0B + +struct ec_request { + u8 cmd; /* Always CMD_KB_CHROME */ + u8 reserved; + u8 sub_cmd; +} __packed; + +struct ec_response { + u8 status; /* 0 if allowed */ + u8 val; +} __packed; + +static int send_ec_cmd(struct wilco_ec_device *ec, u8 sub_cmd, u8 *out_val) +{ + struct ec_request rq; + struct ec_response rs; + struct wilco_ec_message msg; + int ret; + + memset(&rq, 0, sizeof(rq)); + rq.cmd = CMD_KB_CHROME; + rq.sub_cmd = sub_cmd; + + memset(&msg, 0, sizeof(msg)); + msg.type = WILCO_EC_MSG_LEGACY; + msg.request_data = &rq; + msg.request_size = sizeof(rq); + msg.response_data = &rs; + msg.response_size = sizeof(rs); + ret = wilco_ec_mailbox(ec, &msg); + if (ret < 0) + return ret; + if (rs.status) + return -EIO; + + *out_val = rs.val; + + return 0; +} + +/** + * h1_gpio_get() - Gets h1 gpio status. + * @arg: The wilco EC device. + * @val: BIT(0)=ENTRY_TO_FACT_MODE, BIT(1)=SPI_CHROME_SEL + */ +static int h1_gpio_get(void *arg, u64 *val) +{ + int ret; + + ret = send_ec_cmd(arg, SUB_CMD_H1_GPIO, (u8 *)val); + if (ret == 0) + *val &= 0xFF; + return ret; +} + +DEFINE_DEBUGFS_ATTRIBUTE(fops_h1_gpio, h1_gpio_get, NULL, "0x%02llx\n"); + +/** + * test_event_set() - Sends command to EC to cause an EC test event. + * @arg: The wilco EC device. + * @val: unused. + */ +static int test_event_set(void *arg, u64 val) +{ + u8 ret; + + return send_ec_cmd(arg, SUB_CMD_TEST_EVENT, &ret); +} + +/* Format is unused since it is only required for get method which is NULL */ +DEFINE_DEBUGFS_ATTRIBUTE(fops_test_event, NULL, test_event_set, "%llu\n"); + +/** + * wilco_ec_debugfs_probe() - Create the debugfs node + * @pdev: The platform device, probably created in core.c + * + * Try to create a debugfs node. If it fails, then we don't want to change + * behavior at all, this is for debugging after all. Just fail silently. + * + * Return: 0 always. + */ +static int wilco_ec_debugfs_probe(struct platform_device *pdev) +{ + struct wilco_ec_device *ec = dev_get_drvdata(pdev->dev.parent); + + debug_info = devm_kzalloc(&pdev->dev, sizeof(*debug_info), GFP_KERNEL); + if (!debug_info) + return 0; + debug_info->ec = ec; + debug_info->dir = debugfs_create_dir("wilco_ec", NULL); + debugfs_create_file("raw", 0644, debug_info->dir, NULL, &fops_raw); + debugfs_create_file("h1_gpio", 0444, debug_info->dir, ec, + &fops_h1_gpio); + debugfs_create_file("test_event", 0200, debug_info->dir, ec, + &fops_test_event); + + return 0; +} + +static void wilco_ec_debugfs_remove(struct platform_device *pdev) +{ + debugfs_remove_recursive(debug_info->dir); +} + +static const struct platform_device_id wilco_ec_debugfs_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, wilco_ec_debugfs_id); + +static struct platform_driver wilco_ec_debugfs_driver = { + .driver = { + .name = DRV_NAME, + }, + .probe = wilco_ec_debugfs_probe, + .remove = wilco_ec_debugfs_remove, + .id_table = wilco_ec_debugfs_id, +}; + +module_platform_driver(wilco_ec_debugfs_driver); + +MODULE_AUTHOR("Nick Crews "); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Wilco EC debugfs driver"); diff --git a/drivers/platform/chrome/wilco_ec/event.c b/drivers/platform/chrome/wilco_ec/event.c new file mode 100644 index 000000000..1b5cb8983 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/event.c @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ACPI event handling for Wilco Embedded Controller + * + * Copyright 2019 Google LLC + * + * The Wilco Embedded Controller can create custom events that + * are not handled as standard ACPI objects. These events can + * contain information about changes in EC controlled features, + * such as errors and events in the dock or display. For example, + * an event is triggered if the dock is plugged into a display + * incorrectly. These events are needed for telemetry and + * diagnostics reasons, and for possibly alerting the user. + + * These events are triggered by the EC with an ACPI Notify(0x90), + * and then the BIOS reads the event buffer from EC RAM via an + * ACPI method. When the OS receives these events via ACPI, + * it passes them along to this driver. The events are put into + * a queue which can be read by a userspace daemon via a char device + * that implements read() and poll(). The event queue acts as a + * circular buffer of size 64, so if there are no userspace consumers + * the kernel will not run out of memory. The char device will appear at + * /dev/wilco_event{n}, where n is some small non-negative integer, + * starting from 0. Standard ACPI events such as the battery getting + * plugged/unplugged can also come through this path, but they are + * dealt with via other paths, and are ignored here. + + * To test, you can tail the binary data with + * $ cat /dev/wilco_event0 | hexdump -ve '1/1 "%x\n"' + * and then create an event by plugging/unplugging the battery. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ACPI Notify event code indicating event data is available. */ +#define EC_ACPI_NOTIFY_EVENT 0x90 +/* ACPI Method to execute to retrieve event data buffer from the EC. */ +#define EC_ACPI_GET_EVENT "QSET" +/* Maximum number of words in event data returned by the EC. */ +#define EC_ACPI_MAX_EVENT_WORDS 6 +#define EC_ACPI_MAX_EVENT_SIZE \ + (sizeof(struct ec_event) + (EC_ACPI_MAX_EVENT_WORDS) * sizeof(u16)) + +/* Node will appear in /dev/EVENT_DEV_NAME */ +#define EVENT_DEV_NAME "wilco_event" +#define EVENT_CLASS_NAME EVENT_DEV_NAME +#define DRV_NAME EVENT_DEV_NAME +#define EVENT_DEV_NAME_FMT (EVENT_DEV_NAME "%d") +static struct class event_class = { + .name = EVENT_CLASS_NAME, +}; + +/* Keep track of all the device numbers used. */ +#define EVENT_MAX_DEV 128 +static int event_major; +static DEFINE_IDA(event_ida); + +/* Size of circular queue of events. */ +#define MAX_NUM_EVENTS 64 + +/** + * struct ec_event - Extended event returned by the EC. + * @size: Number of 16bit words in structure after the size word. + * @type: Extended event type, meaningless for us. + * @event: Event data words. Max count is %EC_ACPI_MAX_EVENT_WORDS. + */ +struct ec_event { + u16 size; + u16 type; + u16 event[]; +} __packed; + +#define ec_event_num_words(ev) (ev->size - 1) +#define ec_event_size(ev) (sizeof(*ev) + (ec_event_num_words(ev) * sizeof(u16))) + +/** + * struct ec_event_queue - Circular queue for events. + * @capacity: Number of elements the queue can hold. + * @head: Next index to write to. + * @tail: Next index to read from. + * @entries: Array of events. + */ +struct ec_event_queue { + int capacity; + int head; + int tail; + struct ec_event *entries[] __counted_by(capacity); +}; + +/* Maximum number of events to store in ec_event_queue */ +static int queue_size = 64; +module_param(queue_size, int, 0644); + +static struct ec_event_queue *event_queue_new(int capacity) +{ + struct ec_event_queue *q; + + q = kzalloc_flex(*q, entries, capacity); + if (!q) + return NULL; + + q->capacity = capacity; + + return q; +} + +static inline bool event_queue_empty(struct ec_event_queue *q) +{ + /* head==tail when both full and empty, but head==NULL when empty */ + return q->head == q->tail && !q->entries[q->head]; +} + +static inline bool event_queue_full(struct ec_event_queue *q) +{ + /* head==tail when both full and empty, but head!=NULL when full */ + return q->head == q->tail && q->entries[q->head]; +} + +static struct ec_event *event_queue_pop(struct ec_event_queue *q) +{ + struct ec_event *ev; + + if (event_queue_empty(q)) + return NULL; + + ev = q->entries[q->tail]; + q->entries[q->tail] = NULL; + q->tail = (q->tail + 1) % q->capacity; + + return ev; +} + +/* + * If full, overwrite the oldest event and return it so the caller + * can kfree it. If not full, return NULL. + */ +static struct ec_event *event_queue_push(struct ec_event_queue *q, + struct ec_event *ev) +{ + struct ec_event *popped = NULL; + + if (event_queue_full(q)) + popped = event_queue_pop(q); + q->entries[q->head] = ev; + q->head = (q->head + 1) % q->capacity; + + return popped; +} + +static void event_queue_free(struct ec_event_queue *q) +{ + struct ec_event *event; + + while ((event = event_queue_pop(q)) != NULL) + kfree(event); + + kfree(q); +} + +/** + * struct event_device_data - Data for a Wilco EC device that responds to ACPI. + * @events: Circular queue of EC events to be provided to userspace. + * @queue_lock: Protect the queue from simultaneous read/writes. + * @wq: Wait queue to notify processes when events are available or the + * device has been removed. + * @cdev: Char dev that userspace reads() and polls() from. + * @dev: Device associated with the %cdev. + * @exist: Has the device been not been removed? Once a device has been removed, + * writes, reads, and new opens will fail. + * @available: Guarantee only one client can open() file and read from queue. + * + * There will be one of these structs for each ACPI device registered. This data + * is the queue of events received from ACPI that still need to be read from + * userspace, the device and char device that userspace is using, a wait queue + * used to notify different threads when something has changed, plus a flag + * on whether the ACPI device has been removed. + */ +struct event_device_data { + struct ec_event_queue *events; + spinlock_t queue_lock; + wait_queue_head_t wq; + struct device dev; + struct cdev cdev; + bool exist; + atomic_t available; +}; + +/** + * enqueue_events() - Place EC events in queue to be read by userspace. + * @dev: Device the events came from. + * @buf: Buffer of event data. + * @length: Length of event data buffer. + * + * %buf contains a number of ec_event's, packed one after the other. + * Each ec_event is of variable length. Start with the first event, copy it + * into a persistent ec_event, store that entry in the queue, move on + * to the next ec_event in buf, and repeat. + * + * Return: 0 on success or negative error code on failure. + */ +static int enqueue_events(struct device *dev, const u8 *buf, u32 length) +{ + struct event_device_data *dev_data = dev_get_drvdata(dev); + struct ec_event *event, *queue_event, *old_event; + size_t num_words, event_size; + u32 offset = 0; + + while (offset < length) { + event = (struct ec_event *)(buf + offset); + + num_words = ec_event_num_words(event); + event_size = ec_event_size(event); + if (num_words > EC_ACPI_MAX_EVENT_WORDS) { + dev_err(dev, "Too many event words: %zu > %d\n", + num_words, EC_ACPI_MAX_EVENT_WORDS); + return -EOVERFLOW; + } + + /* Ensure event does not overflow the available buffer */ + if ((offset + event_size) > length) { + dev_err(dev, "Event exceeds buffer: %zu > %d\n", + offset + event_size, length); + return -EOVERFLOW; + } + + /* Point to the next event in the buffer */ + offset += event_size; + + /* Copy event into the queue */ + queue_event = kmemdup(event, event_size, GFP_KERNEL); + if (!queue_event) + return -ENOMEM; + spin_lock(&dev_data->queue_lock); + old_event = event_queue_push(dev_data->events, queue_event); + spin_unlock(&dev_data->queue_lock); + kfree(old_event); + wake_up_interruptible(&dev_data->wq); + } + + return 0; +} + +/** + * event_device_notify() - Callback when EC generates an event over ACPI. + * @handle: ACPI handle of the device that the event is coming from. + * @value: Value passed to Notify() in ACPI. + * @data: Notify handler data. + * + * This function will read the events from the device and enqueue them. + */ +static void event_device_notify(acpi_handle handle, u32 value, void *data) +{ + struct acpi_buffer event_buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + struct device *dev = data; + struct acpi_device *adev = ACPI_COMPANION(dev); + union acpi_object *obj; + acpi_status status; + + if (value != EC_ACPI_NOTIFY_EVENT) { + dev_err(dev, "Invalid event: 0x%08x\n", value); + return; + } + + /* Execute ACPI method to get event data buffer. */ + status = acpi_evaluate_object(adev->handle, EC_ACPI_GET_EVENT, + NULL, &event_buffer); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Error executing ACPI method %s()\n", + EC_ACPI_GET_EVENT); + return; + } + + obj = (union acpi_object *)event_buffer.pointer; + if (!obj) { + dev_err(dev, "Nothing returned from %s()\n", + EC_ACPI_GET_EVENT); + return; + } + if (obj->type != ACPI_TYPE_BUFFER) { + dev_err(dev, "Invalid object returned from %s()\n", + EC_ACPI_GET_EVENT); + kfree(obj); + return; + } + if (obj->buffer.length < sizeof(struct ec_event)) { + dev_err(dev, "Invalid buffer length %d from %s()\n", + obj->buffer.length, EC_ACPI_GET_EVENT); + kfree(obj); + return; + } + + enqueue_events(dev, obj->buffer.pointer, obj->buffer.length); + kfree(obj); +} + +static int event_open(struct inode *inode, struct file *filp) +{ + struct event_device_data *dev_data; + + dev_data = container_of(inode->i_cdev, struct event_device_data, cdev); + if (!dev_data->exist) + return -ENODEV; + + if (atomic_cmpxchg(&dev_data->available, 1, 0) == 0) + return -EBUSY; + + /* Increase refcount on device so dev_data is not freed */ + get_device(&dev_data->dev); + stream_open(inode, filp); + filp->private_data = dev_data; + + return 0; +} + +static __poll_t event_poll(struct file *filp, poll_table *wait) +{ + struct event_device_data *dev_data = filp->private_data; + __poll_t mask = 0; + + poll_wait(filp, &dev_data->wq, wait); + if (!dev_data->exist) + return EPOLLHUP; + if (!event_queue_empty(dev_data->events)) + mask |= EPOLLIN | EPOLLRDNORM | EPOLLPRI; + return mask; +} + +/** + * event_read() - Callback for passing event data to userspace via read(). + * @filp: The file we are reading from. + * @buf: Pointer to userspace buffer to fill with one event. + * @count: Number of bytes requested. Must be at least EC_ACPI_MAX_EVENT_SIZE. + * @pos: File position pointer, irrelevant since we don't support seeking. + * + * Removes the first event from the queue, places it in the passed buffer. + * + * If there are no events in the queue, then one of two things happens, + * depending on if the file was opened in nonblocking mode: If in nonblocking + * mode, then return -EAGAIN to say there's no data. If in blocking mode, then + * block until an event is available. + * + * Return: Number of bytes placed in buffer, negative error code on failure. + */ +static ssize_t event_read(struct file *filp, char __user *buf, size_t count, + loff_t *pos) +{ + struct event_device_data *dev_data = filp->private_data; + struct ec_event *event; + ssize_t n_bytes_written = 0; + int err; + + /* We only will give them the entire event at once */ + if (count != 0 && count < EC_ACPI_MAX_EVENT_SIZE) + return -EINVAL; + + spin_lock(&dev_data->queue_lock); + while (event_queue_empty(dev_data->events)) { + spin_unlock(&dev_data->queue_lock); + if (filp->f_flags & O_NONBLOCK) + return -EAGAIN; + + err = wait_event_interruptible(dev_data->wq, + !event_queue_empty(dev_data->events) || + !dev_data->exist); + if (err) + return err; + + /* Device was removed as we waited? */ + if (!dev_data->exist) + return -ENODEV; + spin_lock(&dev_data->queue_lock); + } + event = event_queue_pop(dev_data->events); + spin_unlock(&dev_data->queue_lock); + n_bytes_written = ec_event_size(event); + if (copy_to_user(buf, event, n_bytes_written)) + n_bytes_written = -EFAULT; + kfree(event); + + return n_bytes_written; +} + +static int event_release(struct inode *inode, struct file *filp) +{ + struct event_device_data *dev_data = filp->private_data; + + atomic_set(&dev_data->available, 1); + put_device(&dev_data->dev); + + return 0; +} + +static const struct file_operations event_fops = { + .open = event_open, + .poll = event_poll, + .read = event_read, + .release = event_release, + .owner = THIS_MODULE, +}; + +/** + * free_device_data() - Callback to free the event_device_data structure. + * @d: The device embedded in our device data, which we have been ref counting. + * + * This is called only after event_device_remove() has been called and all + * userspace programs have called event_release() on all the open file + * descriptors. + */ +static void free_device_data(struct device *d) +{ + struct event_device_data *dev_data; + + dev_data = container_of(d, struct event_device_data, dev); + event_queue_free(dev_data->events); + kfree(dev_data); +} + +static void hangup_device(struct event_device_data *dev_data) +{ + dev_data->exist = false; + /* Wake up the waiting processes so they can close. */ + wake_up_interruptible(&dev_data->wq); + put_device(&dev_data->dev); +} + +/** + * event_device_probe() - Callback when creating a new device. + * @pdev: Platform device that we will be receiving events from. + * + * This finds a free minor number for the device, allocates and initializes + * some device data, and creates a new device and char dev node. + * + * The device data is freed in free_device_data(), which is called when + * %dev_data->dev is release()ed. This happens after all references to + * %dev_data->dev are dropped, which happens once both event_device_remove() + * has been called and every open()ed file descriptor has been release()ed. + * + * Return: 0 on success, negative error code on failure. + */ +static int event_device_probe(struct platform_device *pdev) +{ + struct event_device_data *dev_data; + struct acpi_device *adev; + int error, minor; + + adev = ACPI_COMPANION(&pdev->dev); + if (!adev) + return -ENODEV; + + minor = ida_alloc_max(&event_ida, EVENT_MAX_DEV-1, GFP_KERNEL); + if (minor < 0) { + error = minor; + dev_err(&pdev->dev, "Failed to find minor number: %d\n", error); + return error; + } + + dev_data = kzalloc_obj(*dev_data); + if (!dev_data) { + error = -ENOMEM; + goto free_minor; + } + + /* Initialize the device data. */ + platform_set_drvdata(pdev, dev_data); + dev_data->events = event_queue_new(queue_size); + if (!dev_data->events) { + kfree(dev_data); + error = -ENOMEM; + goto free_minor; + } + spin_lock_init(&dev_data->queue_lock); + init_waitqueue_head(&dev_data->wq); + dev_data->exist = true; + atomic_set(&dev_data->available, 1); + + /* Initialize the device. */ + dev_data->dev.devt = MKDEV(event_major, minor); + dev_data->dev.class = &event_class; + dev_data->dev.release = free_device_data; + dev_set_name(&dev_data->dev, EVENT_DEV_NAME_FMT, minor); + device_initialize(&dev_data->dev); + + /* Initialize the character device, and add it to userspace. */ + cdev_init(&dev_data->cdev, &event_fops); + error = cdev_device_add(&dev_data->cdev, &dev_data->dev); + if (error) + goto free_dev_data; + + /* Install an ACPI notify handler. */ + error = acpi_dev_install_notify_handler(adev, ACPI_DEVICE_NOTIFY, + event_device_notify, &pdev->dev); + if (error) + goto free_cdev; + + return 0; + +free_cdev: + cdev_device_del(&dev_data->cdev, &dev_data->dev); +free_dev_data: + hangup_device(dev_data); +free_minor: + ida_free(&event_ida, minor); + return error; +} + +static void event_device_remove(struct platform_device *pdev) +{ + struct event_device_data *dev_data = platform_get_drvdata(pdev); + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, event_device_notify); + cdev_device_del(&dev_data->cdev, &dev_data->dev); + ida_free(&event_ida, MINOR(dev_data->dev.devt)); + hangup_device(dev_data); +} + +static const struct acpi_device_id event_acpi_ids[] = { + { "GOOG000D", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, event_acpi_ids); + +static struct platform_driver event_driver = { + .probe = event_device_probe, + .remove = event_device_remove, + .driver = { + .name = DRV_NAME, + .acpi_match_table = event_acpi_ids, + }, +}; + +static int __init event_module_init(void) +{ + dev_t dev_num = 0; + int ret; + + ret = class_register(&event_class); + if (ret) { + pr_err(DRV_NAME ": Failed registering class: %d\n", ret); + return ret; + } + + /* Request device numbers, starting with minor=0. Save the major num. */ + ret = alloc_chrdev_region(&dev_num, 0, EVENT_MAX_DEV, EVENT_DEV_NAME); + if (ret) { + pr_err(DRV_NAME ": Failed allocating dev numbers: %d\n", ret); + goto destroy_class; + } + event_major = MAJOR(dev_num); + + ret = platform_driver_register(&event_driver); + if (ret < 0) { + pr_err(DRV_NAME ": Failed registering driver: %d\n", ret); + goto unregister_region; + } + + return 0; + +unregister_region: + unregister_chrdev_region(MKDEV(event_major, 0), EVENT_MAX_DEV); +destroy_class: + class_unregister(&event_class); + ida_destroy(&event_ida); + return ret; +} + +static void __exit event_module_exit(void) +{ + platform_driver_unregister(&event_driver); + unregister_chrdev_region(MKDEV(event_major, 0), EVENT_MAX_DEV); + class_unregister(&event_class); + ida_destroy(&event_ida); +} + +module_init(event_module_init); +module_exit(event_module_exit); + +MODULE_AUTHOR("Nick Crews "); +MODULE_DESCRIPTION("Wilco EC ACPI event driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/chrome/wilco_ec/keyboard_leds.c b/drivers/platform/chrome/wilco_ec/keyboard_leds.c new file mode 100644 index 000000000..6ce9c6782 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/keyboard_leds.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Keyboard backlight LED driver for the Wilco Embedded Controller + * + * Copyright 2019 Google LLC + * + * Since the EC will never change the backlight level of its own accord, + * we don't need to implement a brightness_get() method. + */ + +#include +#include +#include +#include +#include + +#define WILCO_EC_COMMAND_KBBL 0x75 +#define WILCO_KBBL_MODE_FLAG_PWM BIT(1) /* Set brightness by percent. */ +#define WILCO_KBBL_DEFAULT_BRIGHTNESS 0 + +struct wilco_keyboard_leds { + struct wilco_ec_device *ec; + struct led_classdev keyboard; +}; + +enum wilco_kbbl_subcommand { + WILCO_KBBL_SUBCMD_GET_FEATURES = 0x00, + WILCO_KBBL_SUBCMD_GET_STATE = 0x01, + WILCO_KBBL_SUBCMD_SET_STATE = 0x02, +}; + +/** + * struct wilco_keyboard_leds_msg - Message to/from EC for keyboard LED control. + * @command: Always WILCO_EC_COMMAND_KBBL. + * @status: Set by EC to 0 on success, 0xFF on failure. + * @subcmd: One of enum wilco_kbbl_subcommand. + * @reserved3: Should be 0. + * @mode: Bit flags for used mode, we want to use WILCO_KBBL_MODE_FLAG_PWM. + * @reserved5to8: Should be 0. + * @percent: Brightness in 0-100. Only meaningful in PWM mode. + * @reserved10to15: Should be 0. + */ +struct wilco_keyboard_leds_msg { + u8 command; + u8 status; + u8 subcmd; + u8 reserved3; + u8 mode; + u8 reserved5to8[4]; + u8 percent; + u8 reserved10to15[6]; +} __packed; + +/* Send a request, get a response, and check that the response is good. */ +static int send_kbbl_msg(struct wilco_ec_device *ec, + struct wilco_keyboard_leds_msg *request, + struct wilco_keyboard_leds_msg *response) +{ + struct wilco_ec_message msg; + int ret; + + memset(&msg, 0, sizeof(msg)); + msg.type = WILCO_EC_MSG_LEGACY; + msg.request_data = request; + msg.request_size = sizeof(*request); + msg.response_data = response; + msg.response_size = sizeof(*response); + + ret = wilco_ec_mailbox(ec, &msg); + if (ret < 0) { + dev_err(ec->dev, + "Failed sending keyboard LEDs command: %d\n", ret); + return ret; + } + + return 0; +} + +static int set_kbbl(struct wilco_ec_device *ec, enum led_brightness brightness) +{ + struct wilco_keyboard_leds_msg request; + struct wilco_keyboard_leds_msg response; + int ret; + + memset(&request, 0, sizeof(request)); + request.command = WILCO_EC_COMMAND_KBBL; + request.subcmd = WILCO_KBBL_SUBCMD_SET_STATE; + request.mode = WILCO_KBBL_MODE_FLAG_PWM; + request.percent = brightness; + + ret = send_kbbl_msg(ec, &request, &response); + if (ret < 0) + return ret; + + if (response.status) { + dev_err(ec->dev, + "EC reported failure sending keyboard LEDs command: %d\n", + response.status); + return -EIO; + } + + return 0; +} + +static int kbbl_exist(struct wilco_ec_device *ec, bool *exists) +{ + struct wilco_keyboard_leds_msg request; + struct wilco_keyboard_leds_msg response; + int ret; + + memset(&request, 0, sizeof(request)); + request.command = WILCO_EC_COMMAND_KBBL; + request.subcmd = WILCO_KBBL_SUBCMD_GET_FEATURES; + + ret = send_kbbl_msg(ec, &request, &response); + if (ret < 0) + return ret; + + *exists = response.status != 0xFF; + + return 0; +} + +/** + * kbbl_init() - Initialize the state of the keyboard backlight. + * @ec: EC device to talk to. + * + * Gets the current brightness, ensuring that the BIOS already initialized the + * backlight to PWM mode. If not in PWM mode, then the current brightness is + * meaningless, so set the brightness to WILCO_KBBL_DEFAULT_BRIGHTNESS. + * + * Return: Final brightness of the keyboard, or negative error code on failure. + */ +static int kbbl_init(struct wilco_ec_device *ec) +{ + struct wilco_keyboard_leds_msg request; + struct wilco_keyboard_leds_msg response; + int ret; + + memset(&request, 0, sizeof(request)); + request.command = WILCO_EC_COMMAND_KBBL; + request.subcmd = WILCO_KBBL_SUBCMD_GET_STATE; + + ret = send_kbbl_msg(ec, &request, &response); + if (ret < 0) + return ret; + + if (response.status) { + dev_err(ec->dev, + "EC reported failure sending keyboard LEDs command: %d\n", + response.status); + return -EIO; + } + + if (response.mode & WILCO_KBBL_MODE_FLAG_PWM) + return response.percent; + + ret = set_kbbl(ec, WILCO_KBBL_DEFAULT_BRIGHTNESS); + if (ret < 0) + return ret; + + return WILCO_KBBL_DEFAULT_BRIGHTNESS; +} + +static int wilco_keyboard_leds_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct wilco_keyboard_leds *wkl = + container_of(cdev, struct wilco_keyboard_leds, keyboard); + return set_kbbl(wkl->ec, brightness); +} + +int wilco_keyboard_leds_init(struct wilco_ec_device *ec) +{ + struct wilco_keyboard_leds *wkl; + bool leds_exist; + int ret; + + ret = kbbl_exist(ec, &leds_exist); + if (ret < 0) { + dev_err(ec->dev, + "Failed checking keyboard LEDs support: %d\n", ret); + return ret; + } + if (!leds_exist) + return 0; + + wkl = devm_kzalloc(ec->dev, sizeof(*wkl), GFP_KERNEL); + if (!wkl) + return -ENOMEM; + + wkl->ec = ec; + wkl->keyboard.name = "platform::kbd_backlight"; + wkl->keyboard.max_brightness = 100; + wkl->keyboard.flags = LED_CORE_SUSPENDRESUME; + wkl->keyboard.brightness_set_blocking = wilco_keyboard_leds_set; + ret = kbbl_init(ec); + if (ret < 0) + return ret; + wkl->keyboard.brightness = ret; + + return devm_led_classdev_register(ec->dev, &wkl->keyboard); +} diff --git a/drivers/platform/chrome/wilco_ec/mailbox.c b/drivers/platform/chrome/wilco_ec/mailbox.c new file mode 100644 index 000000000..4d8273b47 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/mailbox.c @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Mailbox interface for Wilco Embedded Controller + * + * Copyright 2018 Google LLC + * + * The Wilco EC is similar to a typical ChromeOS embedded controller. + * It uses the same MEC based low-level communication and a similar + * protocol, but with some important differences. The EC firmware does + * not support the same mailbox commands so it is not registered as a + * cros_ec device type. + * + * Most messages follow a standard format, but there are some exceptions + * and an interface is provided to do direct/raw transactions that do not + * make assumptions about byte placement. + */ + +#include +#include +#include +#include +#include + +#include "../cros_ec_lpc_mec.h" + +/* Version of mailbox interface */ +#define EC_MAILBOX_VERSION 0 + +/* Command to start mailbox transaction */ +#define EC_MAILBOX_START_COMMAND 0xda + +/* Version of EC protocol */ +#define EC_MAILBOX_PROTO_VERSION 3 + +/* Number of header bytes to be counted as data bytes */ +#define EC_MAILBOX_DATA_EXTRA 2 + +/* Maximum timeout */ +#define EC_MAILBOX_TIMEOUT HZ + +/* EC response flags */ +#define EC_CMDR_DATA BIT(0) /* Data ready for host to read */ +#define EC_CMDR_PENDING BIT(1) /* Write pending to EC */ +#define EC_CMDR_BUSY BIT(2) /* EC is busy processing a command */ +#define EC_CMDR_CMD BIT(3) /* Last host write was a command */ + +/** + * wilco_ec_response_timed_out() - Wait for EC response. + * @ec: EC device. + * + * Return: true if EC timed out, false if EC did not time out. + */ +static bool wilco_ec_response_timed_out(struct wilco_ec_device *ec) +{ + unsigned long timeout = jiffies + EC_MAILBOX_TIMEOUT; + + do { + if (!(inb(ec->io_command->start) & + (EC_CMDR_PENDING | EC_CMDR_BUSY))) + return false; + usleep_range(100, 200); + } while (time_before(jiffies, timeout)); + + return true; +} + +/** + * wilco_ec_checksum() - Compute 8-bit checksum over data range. + * @data: Data to checksum. + * @size: Number of bytes to checksum. + * + * Return: 8-bit checksum of provided data. + */ +static u8 wilco_ec_checksum(const void *data, size_t size) +{ + u8 *data_bytes = (u8 *)data; + u8 checksum = 0; + size_t i; + + for (i = 0; i < size; i++) + checksum += data_bytes[i]; + + return checksum; +} + +/** + * wilco_ec_prepare() - Prepare the request structure for the EC. + * @msg: EC message with request information. + * @rq: EC request structure to fill. + */ +static void wilco_ec_prepare(struct wilco_ec_message *msg, + struct wilco_ec_request *rq) +{ + memset(rq, 0, sizeof(*rq)); + rq->struct_version = EC_MAILBOX_PROTO_VERSION; + rq->mailbox_id = msg->type; + rq->mailbox_version = EC_MAILBOX_VERSION; + rq->data_size = msg->request_size; + + /* Checksum header and data */ + rq->checksum = wilco_ec_checksum(rq, sizeof(*rq)); + rq->checksum += wilco_ec_checksum(msg->request_data, msg->request_size); + rq->checksum = -rq->checksum; +} + +/** + * wilco_ec_transfer() - Perform actual data transfer. + * @ec: EC device. + * @msg: EC message data for request and response. + * @rq: Filled in request structure + * + * Context: ec->mailbox_lock should be held while using this function. + * Return: number of bytes received or negative error code on failure. + */ +static int wilco_ec_transfer(struct wilco_ec_device *ec, + struct wilco_ec_message *msg, + struct wilco_ec_request *rq) +{ + struct wilco_ec_response *rs; + int ret; + u8 flag; + + /* Write request header, then data */ + ret = cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, 0, sizeof(*rq), (u8 *)rq); + if (ret < 0) + return ret; + ret = cros_ec_lpc_io_bytes_mec(MEC_IO_WRITE, sizeof(*rq), msg->request_size, + msg->request_data); + if (ret < 0) + return ret; + + /* Start the command */ + outb(EC_MAILBOX_START_COMMAND, ec->io_command->start); + + /* For some commands (eg shutdown) the EC will not respond, that's OK */ + if (msg->flags & WILCO_EC_FLAG_NO_RESPONSE) { + dev_dbg(ec->dev, "EC does not respond to this command\n"); + return 0; + } + + /* Wait for it to complete */ + if (wilco_ec_response_timed_out(ec)) { + dev_dbg(ec->dev, "response timed out\n"); + return -ETIMEDOUT; + } + + /* Check result */ + flag = inb(ec->io_data->start); + if (flag) { + dev_dbg(ec->dev, "bad response: 0x%02x\n", flag); + return -EIO; + } + + /* Read back response */ + rs = ec->data_buffer; + ret = cros_ec_lpc_io_bytes_mec(MEC_IO_READ, 0, + sizeof(*rs) + EC_MAILBOX_DATA_SIZE, + (u8 *)rs); + if (ret < 0) + return ret; + if (ret) { + dev_dbg(ec->dev, "bad packet checksum 0x%02x\n", rs->checksum); + return -EBADMSG; + } + + if (rs->result) { + dev_dbg(ec->dev, "EC reported failure: 0x%02x\n", rs->result); + return -EBADMSG; + } + + if (rs->data_size != EC_MAILBOX_DATA_SIZE) { + dev_dbg(ec->dev, "unexpected packet size (%u != %u)\n", + rs->data_size, EC_MAILBOX_DATA_SIZE); + return -EMSGSIZE; + } + + if (rs->data_size < msg->response_size) { + dev_dbg(ec->dev, "EC didn't return enough data (%u < %zu)\n", + rs->data_size, msg->response_size); + return -EMSGSIZE; + } + + memcpy(msg->response_data, rs->data, msg->response_size); + + return rs->data_size; +} + +/** + * wilco_ec_mailbox() - Send EC request and receive EC response. + * @ec: EC device. + * @msg: EC message data for request and response. + * + * On entry msg->type, msg->request_size, and msg->request_data should all be + * filled in. If desired, msg->flags can be set. + * + * If a response is expected, msg->response_size should be set, and + * msg->response_data should point to a buffer with enough space. On exit + * msg->response_data will be filled. + * + * Return: number of bytes received or negative error code on failure. + */ +int wilco_ec_mailbox(struct wilco_ec_device *ec, struct wilco_ec_message *msg) +{ + struct wilco_ec_request *rq; + int ret; + + dev_dbg(ec->dev, "type=%04x flags=%02x rslen=%zu rqlen=%zu\n", + msg->type, msg->flags, msg->response_size, msg->request_size); + + mutex_lock(&ec->mailbox_lock); + /* Prepare request packet */ + rq = ec->data_buffer; + wilco_ec_prepare(msg, rq); + + ret = wilco_ec_transfer(ec, msg, rq); + mutex_unlock(&ec->mailbox_lock); + + return ret; + +} +EXPORT_SYMBOL_GPL(wilco_ec_mailbox); diff --git a/drivers/platform/chrome/wilco_ec/properties.c b/drivers/platform/chrome/wilco_ec/properties.c new file mode 100644 index 000000000..9951c8db0 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/properties.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2019 Google LLC + */ + +#include +#include +#include +#include +#include +#include + +/* Operation code; what the EC should do with the property */ +enum ec_property_op { + EC_OP_GET = 0, + EC_OP_SET = 1, +}; + +struct ec_property_request { + u8 op; /* One of enum ec_property_op */ + u8 property_id[4]; /* The 32 bit PID is stored Little Endian */ + u8 length; + u8 data[WILCO_EC_PROPERTY_MAX_SIZE]; +} __packed; + +struct ec_property_response { + u8 reserved[2]; + u8 op; /* One of enum ec_property_op */ + u8 property_id[4]; /* The 32 bit PID is stored Little Endian */ + u8 length; + u8 data[WILCO_EC_PROPERTY_MAX_SIZE]; +} __packed; + +static int send_property_msg(struct wilco_ec_device *ec, + struct ec_property_request *rq, + struct ec_property_response *rs) +{ + struct wilco_ec_message ec_msg; + int ret; + + memset(&ec_msg, 0, sizeof(ec_msg)); + ec_msg.type = WILCO_EC_MSG_PROPERTY; + ec_msg.request_data = rq; + ec_msg.request_size = sizeof(*rq); + ec_msg.response_data = rs; + ec_msg.response_size = sizeof(*rs); + + ret = wilco_ec_mailbox(ec, &ec_msg); + if (ret < 0) + return ret; + if (rs->op != rq->op) + return -EBADMSG; + if (memcmp(rq->property_id, rs->property_id, sizeof(rs->property_id))) + return -EBADMSG; + + return 0; +} + +int wilco_ec_get_property(struct wilco_ec_device *ec, + struct wilco_ec_property_msg *prop_msg) +{ + struct ec_property_request rq; + struct ec_property_response rs; + int ret; + + memset(&rq, 0, sizeof(rq)); + rq.op = EC_OP_GET; + put_unaligned_le32(prop_msg->property_id, rq.property_id); + + ret = send_property_msg(ec, &rq, &rs); + if (ret < 0) + return ret; + + prop_msg->length = rs.length; + memcpy(prop_msg->data, rs.data, rs.length); + + return 0; +} +EXPORT_SYMBOL_GPL(wilco_ec_get_property); + +int wilco_ec_set_property(struct wilco_ec_device *ec, + struct wilco_ec_property_msg *prop_msg) +{ + struct ec_property_request rq; + struct ec_property_response rs; + int ret; + + memset(&rq, 0, sizeof(rq)); + rq.op = EC_OP_SET; + put_unaligned_le32(prop_msg->property_id, rq.property_id); + rq.length = prop_msg->length; + memcpy(rq.data, prop_msg->data, prop_msg->length); + + ret = send_property_msg(ec, &rq, &rs); + if (ret < 0) + return ret; + if (rs.length != prop_msg->length) + return -EBADMSG; + + return 0; +} +EXPORT_SYMBOL_GPL(wilco_ec_set_property); + +int wilco_ec_get_byte_property(struct wilco_ec_device *ec, u32 property_id, + u8 *val) +{ + struct wilco_ec_property_msg msg; + int ret; + + msg.property_id = property_id; + + ret = wilco_ec_get_property(ec, &msg); + if (ret < 0) + return ret; + if (msg.length != 1) + return -EBADMSG; + + *val = msg.data[0]; + + return 0; +} +EXPORT_SYMBOL_GPL(wilco_ec_get_byte_property); + +int wilco_ec_set_byte_property(struct wilco_ec_device *ec, u32 property_id, + u8 val) +{ + struct wilco_ec_property_msg msg; + + msg.property_id = property_id; + msg.data[0] = val; + msg.length = 1; + + return wilco_ec_set_property(ec, &msg); +} +EXPORT_SYMBOL_GPL(wilco_ec_set_byte_property); diff --git a/drivers/platform/chrome/wilco_ec/sysfs.c b/drivers/platform/chrome/wilco_ec/sysfs.c new file mode 100644 index 000000000..d44c43559 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/sysfs.c @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2019 Google LLC + * + * Sysfs properties to view and modify EC-controlled features on Wilco devices. + * The entries will appear under /sys/bus/platform/devices/GOOG000C:00/ + * + * See Documentation/ABI/testing/sysfs-platform-wilco-ec for more information. + */ + +#include +#include +#include +#include +#include +#include + +#define CMD_KB_CMOS 0x7C +#define SUB_CMD_KB_CMOS_AUTO_ON 0x03 + +struct boot_on_ac_request { + u8 cmd; /* Always CMD_KB_CMOS */ + u8 reserved1; + u8 sub_cmd; /* Always SUB_CMD_KB_CMOS_AUTO_ON */ + u8 reserved3to5[3]; + u8 val; /* Either 0 or 1 */ + u8 reserved7; +} __packed; + +#define CMD_USB_CHARGE 0x39 + +enum usb_charge_op { + USB_CHARGE_GET = 0, + USB_CHARGE_SET = 1, +}; + +struct usb_charge_request { + u8 cmd; /* Always CMD_USB_CHARGE */ + u8 reserved; + u8 op; /* One of enum usb_charge_op */ + u8 val; /* When setting, either 0 or 1 */ +} __packed; + +struct usb_charge_response { + u8 reserved; + u8 status; /* Set by EC to 0 on success, other value on failure */ + u8 val; /* When getting, set by EC to either 0 or 1 */ +} __packed; + +#define CMD_EC_INFO 0x38 +enum get_ec_info_op { + CMD_GET_EC_LABEL = 0, + CMD_GET_EC_REV = 1, + CMD_GET_EC_MODEL = 2, + CMD_GET_EC_BUILD_DATE = 3, +}; + +struct get_ec_info_req { + u8 cmd; /* Always CMD_EC_INFO */ + u8 reserved; + u8 op; /* One of enum get_ec_info_op */ +} __packed; + +struct get_ec_info_resp { + u8 reserved[2]; + char value[9]; /* __nonstring: might not be null terminated */ +} __packed; + +static ssize_t boot_on_ac_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct wilco_ec_device *ec = dev_get_drvdata(dev); + struct boot_on_ac_request rq; + struct wilco_ec_message msg; + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret < 0) + return ret; + if (val > 1) + return -EINVAL; + + memset(&rq, 0, sizeof(rq)); + rq.cmd = CMD_KB_CMOS; + rq.sub_cmd = SUB_CMD_KB_CMOS_AUTO_ON; + rq.val = val; + + memset(&msg, 0, sizeof(msg)); + msg.type = WILCO_EC_MSG_LEGACY; + msg.request_data = &rq; + msg.request_size = sizeof(rq); + ret = wilco_ec_mailbox(ec, &msg); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR_WO(boot_on_ac); + +static ssize_t get_info(struct device *dev, char *buf, enum get_ec_info_op op) +{ + struct wilco_ec_device *ec = dev_get_drvdata(dev); + struct get_ec_info_req req = { .cmd = CMD_EC_INFO, .op = op }; + struct get_ec_info_resp resp; + int ret; + + struct wilco_ec_message msg = { + .type = WILCO_EC_MSG_LEGACY, + .request_data = &req, + .request_size = sizeof(req), + .response_data = &resp, + .response_size = sizeof(resp), + }; + + ret = wilco_ec_mailbox(ec, &msg); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%.*s\n", (int)sizeof(resp.value), (char *)&resp.value); +} + +static ssize_t version_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return get_info(dev, buf, CMD_GET_EC_LABEL); +} + +static DEVICE_ATTR_RO(version); + +static ssize_t build_revision_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return get_info(dev, buf, CMD_GET_EC_REV); +} + +static DEVICE_ATTR_RO(build_revision); + +static ssize_t build_date_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return get_info(dev, buf, CMD_GET_EC_BUILD_DATE); +} + +static DEVICE_ATTR_RO(build_date); + +static ssize_t model_number_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return get_info(dev, buf, CMD_GET_EC_MODEL); +} + +static DEVICE_ATTR_RO(model_number); + +static int send_usb_charge(struct wilco_ec_device *ec, + struct usb_charge_request *rq, + struct usb_charge_response *rs) +{ + struct wilco_ec_message msg; + int ret; + + memset(&msg, 0, sizeof(msg)); + msg.type = WILCO_EC_MSG_LEGACY; + msg.request_data = rq; + msg.request_size = sizeof(*rq); + msg.response_data = rs; + msg.response_size = sizeof(*rs); + ret = wilco_ec_mailbox(ec, &msg); + if (ret < 0) + return ret; + if (rs->status) + return -EIO; + + return 0; +} + +static ssize_t usb_charge_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct wilco_ec_device *ec = dev_get_drvdata(dev); + struct usb_charge_request rq; + struct usb_charge_response rs; + int ret; + + memset(&rq, 0, sizeof(rq)); + rq.cmd = CMD_USB_CHARGE; + rq.op = USB_CHARGE_GET; + + ret = send_usb_charge(ec, &rq, &rs); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", rs.val); +} + +static ssize_t usb_charge_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct wilco_ec_device *ec = dev_get_drvdata(dev); + struct usb_charge_request rq; + struct usb_charge_response rs; + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret < 0) + return ret; + if (val > 1) + return -EINVAL; + + memset(&rq, 0, sizeof(rq)); + rq.cmd = CMD_USB_CHARGE; + rq.op = USB_CHARGE_SET; + rq.val = val; + + ret = send_usb_charge(ec, &rq, &rs); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR_RW(usb_charge); + +static struct attribute *wilco_dev_attrs[] = { + &dev_attr_boot_on_ac.attr, + &dev_attr_build_date.attr, + &dev_attr_build_revision.attr, + &dev_attr_model_number.attr, + &dev_attr_usb_charge.attr, + &dev_attr_version.attr, + NULL, +}; + +static const struct attribute_group wilco_dev_attr_group = { + .attrs = wilco_dev_attrs, +}; + +int wilco_ec_add_sysfs(struct wilco_ec_device *ec) +{ + return sysfs_create_group(&ec->dev->kobj, &wilco_dev_attr_group); +} + +void wilco_ec_remove_sysfs(struct wilco_ec_device *ec) +{ + sysfs_remove_group(&ec->dev->kobj, &wilco_dev_attr_group); +} diff --git a/drivers/platform/chrome/wilco_ec/telemetry.c b/drivers/platform/chrome/wilco_ec/telemetry.c new file mode 100644 index 000000000..45be318b0 --- /dev/null +++ b/drivers/platform/chrome/wilco_ec/telemetry.c @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Telemetry communication for Wilco EC + * + * Copyright 2019 Google LLC + * + * The Wilco Embedded Controller is able to send telemetry data + * which is useful for enterprise applications. A daemon running on + * the OS sends a command to the EC via a write() to a char device, + * and can read the response with a read(). The write() request is + * verified by the driver to ensure that it is performing only one + * of the allowlisted commands, and that no extraneous data is + * being transmitted to the EC. The response is passed directly + * back to the reader with no modification. + * + * The character device will appear as /dev/wilco_telemN, where N + * is some small non-negative integer, starting with 0. Only one + * process may have the file descriptor open at a time. The calling + * userspace program needs to keep the device file descriptor open + * between the calls to write() and read() in order to preserve the + * response. Up to 32 bytes will be available for reading. + * + * For testing purposes, try requesting the EC's firmware build + * date, by sending the WILCO_EC_TELEM_GET_VERSION command with + * argument index=3. i.e. write [0x38, 0x00, 0x03] + * to the device node. An ASCII string of the build date is + * returned. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TELEM_DEV_NAME "wilco_telem" +#define TELEM_CLASS_NAME TELEM_DEV_NAME +#define DRV_NAME TELEM_DEV_NAME +#define TELEM_DEV_NAME_FMT (TELEM_DEV_NAME "%d") +static struct class telem_class = { + .name = TELEM_CLASS_NAME, +}; + +/* Keep track of all the device numbers used. */ +#define TELEM_MAX_DEV 128 +static int telem_major; +static DEFINE_IDA(telem_ida); + +/* EC telemetry command codes */ +#define WILCO_EC_TELEM_GET_LOG 0x99 +#define WILCO_EC_TELEM_GET_VERSION 0x38 +#define WILCO_EC_TELEM_GET_FAN_INFO 0x2E +#define WILCO_EC_TELEM_GET_DIAG_INFO 0xFA +#define WILCO_EC_TELEM_GET_TEMP_INFO 0x95 +#define WILCO_EC_TELEM_GET_TEMP_READ 0x2C +#define WILCO_EC_TELEM_GET_BATT_EXT_INFO 0x07 +#define WILCO_EC_TELEM_GET_BATT_PPID_INFO 0x8A + +#define TELEM_ARGS_SIZE_MAX 30 + +/* + * The following telem_args_get_* structs are embedded within the |args| field + * of wilco_ec_telem_request. + */ + +struct telem_args_get_log { + u8 log_type; + u8 log_index; +} __packed; + +/* + * Get a piece of info about the EC firmware version: + * 0 = label + * 1 = svn_rev + * 2 = model_no + * 3 = build_date + * 4 = frio_version + */ +struct telem_args_get_version { + u8 index; +} __packed; + +struct telem_args_get_fan_info { + u8 command; + u8 fan_number; + u8 arg; +} __packed; + +struct telem_args_get_diag_info { + u8 type; + u8 sub_type; +} __packed; + +struct telem_args_get_temp_info { + u8 command; + u8 index; + u8 field; + u8 zone; +} __packed; + +struct telem_args_get_temp_read { + u8 sensor_index; +} __packed; + +struct telem_args_get_batt_ext_info { + u8 var_args[5]; +} __packed; + +struct telem_args_get_batt_ppid_info { + u8 always1; /* Should always be 1 */ +} __packed; + +/** + * struct wilco_ec_telem_request - Telemetry command and arguments sent to EC. + * @command: One of WILCO_EC_TELEM_GET_* command codes. + * @reserved: Must be 0. + * @args: The first N bytes are one of telem_args_get_* structs, the rest is 0. + */ +struct wilco_ec_telem_request { + u8 command; + u8 reserved; + union { + u8 buf[TELEM_ARGS_SIZE_MAX]; + struct telem_args_get_log get_log; + struct telem_args_get_version get_version; + struct telem_args_get_fan_info get_fan_info; + struct telem_args_get_diag_info get_diag_info; + struct telem_args_get_temp_info get_temp_info; + struct telem_args_get_temp_read get_temp_read; + struct telem_args_get_batt_ext_info get_batt_ext_info; + struct telem_args_get_batt_ppid_info get_batt_ppid_info; + } args; +} __packed; + +/** + * check_telem_request() - Ensure that a request from userspace is valid. + * @rq: Request buffer copied from userspace. + * @size: Number of bytes copied from userspace. + * + * Return: 0 if valid, -EINVAL if bad command or reserved byte is non-zero, + * -EMSGSIZE if the request is too long. + * + * We do not want to allow userspace to send arbitrary telemetry commands to + * the EC. Therefore we check to ensure that + * 1. The request follows the format of struct wilco_ec_telem_request. + * 2. The supplied command code is one of the allowlisted commands. + * 3. The request only contains the necessary data for the header and arguments. + */ +static int check_telem_request(struct wilco_ec_telem_request *rq, + size_t size) +{ + size_t max_size = offsetof(struct wilco_ec_telem_request, args); + + if (rq->reserved) + return -EINVAL; + + switch (rq->command) { + case WILCO_EC_TELEM_GET_LOG: + max_size += sizeof(rq->args.get_log); + break; + case WILCO_EC_TELEM_GET_VERSION: + max_size += sizeof(rq->args.get_version); + break; + case WILCO_EC_TELEM_GET_FAN_INFO: + max_size += sizeof(rq->args.get_fan_info); + break; + case WILCO_EC_TELEM_GET_DIAG_INFO: + max_size += sizeof(rq->args.get_diag_info); + break; + case WILCO_EC_TELEM_GET_TEMP_INFO: + max_size += sizeof(rq->args.get_temp_info); + break; + case WILCO_EC_TELEM_GET_TEMP_READ: + max_size += sizeof(rq->args.get_temp_read); + break; + case WILCO_EC_TELEM_GET_BATT_EXT_INFO: + max_size += sizeof(rq->args.get_batt_ext_info); + break; + case WILCO_EC_TELEM_GET_BATT_PPID_INFO: + if (rq->args.get_batt_ppid_info.always1 != 1) + return -EINVAL; + + max_size += sizeof(rq->args.get_batt_ppid_info); + break; + default: + return -EINVAL; + } + + return (size <= max_size) ? 0 : -EMSGSIZE; +} + +/** + * struct telem_device_data - Data for a Wilco EC device that queries telemetry. + * @cdev: Char dev that userspace reads and polls from. + * @dev: Device associated with the %cdev. + * @ec: Wilco EC that we will be communicating with using the mailbox interface. + * @available: Boolean of if the device can be opened. + */ +struct telem_device_data { + struct device dev; + struct cdev cdev; + struct wilco_ec_device *ec; + atomic_t available; +}; + +#define TELEM_RESPONSE_SIZE EC_MAILBOX_DATA_SIZE + +/** + * struct telem_session_data - Data that exists between open() and release(). + * @dev_data: Pointer to get back to the device data and EC. + * @request: Command and arguments sent to EC. + * @response: Response buffer of data from EC. + * @has_msg: Is there data available to read from a previous write? + */ +struct telem_session_data { + struct telem_device_data *dev_data; + struct wilco_ec_telem_request request; + u8 response[TELEM_RESPONSE_SIZE]; + bool has_msg; +}; + +/** + * telem_open() - Callback for when the device node is opened. + * @inode: inode for this char device node. + * @filp: file for this char device node. + * + * We need to ensure that after writing a command to the device, + * the same userspace process reads the corresponding result. + * Therefore, we increment a refcount on opening the device, so that + * only one process can communicate with the EC at a time. + * + * Return: 0 on success, or negative error code on failure. + */ +static int telem_open(struct inode *inode, struct file *filp) +{ + struct telem_device_data *dev_data; + struct telem_session_data *sess_data; + + /* Ensure device isn't already open */ + dev_data = container_of(inode->i_cdev, struct telem_device_data, cdev); + if (atomic_cmpxchg(&dev_data->available, 1, 0) == 0) + return -EBUSY; + + get_device(&dev_data->dev); + + sess_data = kzalloc_obj(*sess_data); + if (!sess_data) { + atomic_set(&dev_data->available, 1); + return -ENOMEM; + } + sess_data->dev_data = dev_data; + sess_data->has_msg = false; + + stream_open(inode, filp); + filp->private_data = sess_data; + + return 0; +} + +static ssize_t telem_write(struct file *filp, const char __user *buf, + size_t count, loff_t *pos) +{ + struct telem_session_data *sess_data = filp->private_data; + struct wilco_ec_message msg = {}; + int ret; + + if (count > sizeof(sess_data->request)) + return -EMSGSIZE; + memset(&sess_data->request, 0, sizeof(sess_data->request)); + if (copy_from_user(&sess_data->request, buf, count)) + return -EFAULT; + ret = check_telem_request(&sess_data->request, count); + if (ret < 0) + return ret; + + memset(sess_data->response, 0, sizeof(sess_data->response)); + msg.type = WILCO_EC_MSG_TELEMETRY; + msg.request_data = &sess_data->request; + msg.request_size = sizeof(sess_data->request); + msg.response_data = sess_data->response; + msg.response_size = sizeof(sess_data->response); + + ret = wilco_ec_mailbox(sess_data->dev_data->ec, &msg); + if (ret < 0) + return ret; + if (ret != sizeof(sess_data->response)) + return -EMSGSIZE; + + sess_data->has_msg = true; + + return count; +} + +static ssize_t telem_read(struct file *filp, char __user *buf, size_t count, + loff_t *pos) +{ + struct telem_session_data *sess_data = filp->private_data; + + if (!sess_data->has_msg) + return -ENODATA; + if (count > sizeof(sess_data->response)) + return -EINVAL; + + if (copy_to_user(buf, sess_data->response, count)) + return -EFAULT; + + sess_data->has_msg = false; + + return count; +} + +static int telem_release(struct inode *inode, struct file *filp) +{ + struct telem_session_data *sess_data = filp->private_data; + + atomic_set(&sess_data->dev_data->available, 1); + put_device(&sess_data->dev_data->dev); + kfree(sess_data); + + return 0; +} + +static const struct file_operations telem_fops = { + .open = telem_open, + .write = telem_write, + .read = telem_read, + .release = telem_release, + .owner = THIS_MODULE, +}; + +/** + * telem_device_free() - Callback to free the telem_device_data structure. + * @d: The device embedded in our device data, which we have been ref counting. + * + * Once all open file descriptors are closed and the device has been removed, + * the refcount of the device will fall to 0 and this will be called. + */ +static void telem_device_free(struct device *d) +{ + struct telem_device_data *dev_data; + + dev_data = container_of(d, struct telem_device_data, dev); + kfree(dev_data); +} + +/** + * telem_device_probe() - Callback when creating a new device. + * @pdev: platform device that we will be receiving telems from. + * + * This finds a free minor number for the device, allocates and initializes + * some device data, and creates a new device and char dev node. + * + * Return: 0 on success, negative error code on failure. + */ +static int telem_device_probe(struct platform_device *pdev) +{ + struct telem_device_data *dev_data; + int error, minor; + + /* Get the next available device number */ + minor = ida_alloc_max(&telem_ida, TELEM_MAX_DEV-1, GFP_KERNEL); + if (minor < 0) { + error = minor; + dev_err(&pdev->dev, "Failed to find minor number: %d\n", error); + return error; + } + + dev_data = kzalloc_obj(*dev_data); + if (!dev_data) { + ida_free(&telem_ida, minor); + return -ENOMEM; + } + + /* Initialize the device data */ + dev_data->ec = dev_get_platdata(&pdev->dev); + atomic_set(&dev_data->available, 1); + platform_set_drvdata(pdev, dev_data); + + /* Initialize the device */ + dev_data->dev.devt = MKDEV(telem_major, minor); + dev_data->dev.class = &telem_class; + dev_data->dev.release = telem_device_free; + dev_set_name(&dev_data->dev, TELEM_DEV_NAME_FMT, minor); + device_initialize(&dev_data->dev); + + /* Initialize the character device and add it to userspace */ + cdev_init(&dev_data->cdev, &telem_fops); + error = cdev_device_add(&dev_data->cdev, &dev_data->dev); + if (error) { + put_device(&dev_data->dev); + ida_free(&telem_ida, minor); + return error; + } + + return 0; +} + +static void telem_device_remove(struct platform_device *pdev) +{ + struct telem_device_data *dev_data = platform_get_drvdata(pdev); + + cdev_device_del(&dev_data->cdev, &dev_data->dev); + ida_free(&telem_ida, MINOR(dev_data->dev.devt)); + put_device(&dev_data->dev); +} + +static const struct platform_device_id telem_id[] = { + { .name = DRV_NAME }, + { } +}; +MODULE_DEVICE_TABLE(platform, telem_id); + +static struct platform_driver telem_driver = { + .probe = telem_device_probe, + .remove = telem_device_remove, + .driver = { + .name = DRV_NAME, + }, + .id_table = telem_id, +}; + +static int __init telem_module_init(void) +{ + dev_t dev_num = 0; + int ret; + + ret = class_register(&telem_class); + if (ret) { + pr_err(DRV_NAME ": Failed registering class: %d\n", ret); + return ret; + } + + /* Request the kernel for device numbers, starting with minor=0 */ + ret = alloc_chrdev_region(&dev_num, 0, TELEM_MAX_DEV, TELEM_DEV_NAME); + if (ret) { + pr_err(DRV_NAME ": Failed allocating dev numbers: %d\n", ret); + goto destroy_class; + } + telem_major = MAJOR(dev_num); + + ret = platform_driver_register(&telem_driver); + if (ret < 0) { + pr_err(DRV_NAME ": Failed registering driver: %d\n", ret); + goto unregister_region; + } + + return 0; + +unregister_region: + unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV); +destroy_class: + class_unregister(&telem_class); + ida_destroy(&telem_ida); + return ret; +} + +static void __exit telem_module_exit(void) +{ + platform_driver_unregister(&telem_driver); + unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV); + class_unregister(&telem_class); + ida_destroy(&telem_ida); +} + +module_init(telem_module_init); +module_exit(telem_module_exit); + +MODULE_AUTHOR("Nick Crews "); +MODULE_DESCRIPTION("Wilco EC telemetry driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/cznic/Kconfig b/drivers/platform/cznic/Kconfig new file mode 100644 index 000000000..61cff5f7e --- /dev/null +++ b/drivers/platform/cznic/Kconfig @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# For a description of the syntax of this configuration file, +# see Documentation/kbuild/kconfig-language.rst. +# + +menuconfig CZNIC_PLATFORMS + bool "Platform support for CZ.NIC's Turris hardware" + depends on ARCH_MVEBU || COMPILE_TEST + help + Say Y here to be able to choose driver support for CZ.NIC's Turris + devices. This option alone does not add any kernel code. + +if CZNIC_PLATFORMS + +config TURRIS_OMNIA_MCU + tristate "Turris Omnia MCU driver" + depends on MACH_ARMADA_38X || COMPILE_TEST + depends on I2C + help + Say Y here to add support for the features implemented by the + microcontroller on the CZ.NIC's Turris Omnia SOHO router. + This option only enables the core part of the driver. Specific + features can be enabled by subsequent config options. + To compile this driver as a module, choose M here; the module will be + called turris-omnia-mcu. + +if TURRIS_OMNIA_MCU + +config TURRIS_OMNIA_MCU_GPIO + bool "Turris Omnia MCU GPIOs" + default y + depends on GPIOLIB + depends on OF + select GPIOLIB_IRQCHIP + help + Say Y here to add support for controlling MCU GPIO pins and receiving + MCU interrupts on CZ.NIC's Turris Omnia. + This enables you to + - get front button press events (the front button can be configured + either to generate press events to the CPU or to change front LEDs + panel brightness), + - enable / disable USB port voltage regulators and to detect USB + overcurrent, + - detect MiniPCIe / mSATA card presence in MiniPCIe port 0, + - configure resets of various peripherals on board revisions 32+, + - enable / disable the VHV voltage regulator to the SOC in order to be + able to program SOC's OTP on board revisions 32+, + - get input from the LED output pins of the WAN ethernet PHY, LAN + switch and MiniPCIe ports. + +config TURRIS_OMNIA_MCU_SYSOFF_WAKEUP + bool "Turris Omnia MCU system off and RTC wakeup" + default y + depends on RTC_CLASS + help + Say Y here to add support for CZ.NIC's Turris Omnia board poweroff + into true low power mode (with voltage regulators disabled) and the + ability to configure wake up from this mode (via rtcwake). + +config TURRIS_OMNIA_MCU_WATCHDOG + bool "Turris Omnia MCU watchdog" + default y + depends on WATCHDOG + select WATCHDOG_CORE + help + Say Y here to add support for watchdog provided by CZ.NIC's Turris + Omnia MCU. + +config TURRIS_OMNIA_MCU_TRNG + bool "Turris Omnia MCU true random number generator" + default y + depends on TURRIS_OMNIA_MCU_GPIO + depends on HW_RANDOM=y || HW_RANDOM=TURRIS_OMNIA_MCU + help + Say Y here to add support for the true random number generator + provided by CZ.NIC's Turris Omnia MCU. + +config TURRIS_OMNIA_MCU_KEYCTL + bool "Turris Omnia MCU ECDSA message signing" + default y + depends on KEYS + depends on ASYMMETRIC_KEY_TYPE + depends on TURRIS_OMNIA_MCU_GPIO + select TURRIS_SIGNING_KEY + help + Say Y here to add support for ECDSA message signing with board private + key (if available on the MCU). This is exposed via the keyctl() + syscall. + +endif # TURRIS_OMNIA_MCU + +config TURRIS_SIGNING_KEY + tristate + depends on KEYS + depends on ASYMMETRIC_KEY_TYPE + +endif # CZNIC_PLATFORMS diff --git a/drivers/platform/cznic/Makefile b/drivers/platform/cznic/Makefile new file mode 100644 index 000000000..ccad7bec8 --- /dev/null +++ b/drivers/platform/cznic/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only + +obj-$(CONFIG_TURRIS_OMNIA_MCU) += turris-omnia-mcu.o +turris-omnia-mcu-y := turris-omnia-mcu-base.o +turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_GPIO) += turris-omnia-mcu-gpio.o +turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_KEYCTL) += turris-omnia-mcu-keyctl.o +turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP) += turris-omnia-mcu-sys-off-wakeup.o +turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_TRNG) += turris-omnia-mcu-trng.o +turris-omnia-mcu-$(CONFIG_TURRIS_OMNIA_MCU_WATCHDOG) += turris-omnia-mcu-watchdog.o + +obj-$(CONFIG_TURRIS_SIGNING_KEY) += turris-signing-key.o diff --git a/drivers/platform/cznic/turris-omnia-mcu-base.c b/drivers/platform/cznic/turris-omnia-mcu-base.c new file mode 100644 index 000000000..e8fc0d7b3 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-base.c @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU driver + * + * 2024 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "turris-omnia-mcu.h" + +#define OMNIA_FW_VERSION_LEN 20 +#define OMNIA_FW_VERSION_HEX_LEN (2 * OMNIA_FW_VERSION_LEN + 1) +#define OMNIA_BOARD_INFO_LEN 16 + +int omnia_cmd_write_read(const struct i2c_client *client, + void *cmd, unsigned int cmd_len, + void *reply, unsigned int reply_len) +{ + struct i2c_msg msgs[2]; + int ret, num; + + msgs[0].addr = client->addr; + msgs[0].flags = 0; + msgs[0].len = cmd_len; + msgs[0].buf = cmd; + num = 1; + + if (reply_len) { + msgs[1].addr = client->addr; + msgs[1].flags = I2C_M_RD; + msgs[1].len = reply_len; + msgs[1].buf = reply; + num++; + } + + ret = i2c_transfer(client->adapter, msgs, num); + if (ret < 0) + return ret; + if (ret != num) + return -EIO; + + return 0; +} +EXPORT_SYMBOL_GPL(omnia_cmd_write_read); + +static int omnia_get_version_hash(struct omnia_mcu *mcu, bool bootloader, + char version[static OMNIA_FW_VERSION_HEX_LEN]) +{ + u8 reply[OMNIA_FW_VERSION_LEN]; + char *p; + int err; + + err = omnia_cmd_read(mcu->client, + bootloader ? OMNIA_CMD_GET_FW_VERSION_BOOT + : OMNIA_CMD_GET_FW_VERSION_APP, + reply, sizeof(reply)); + if (err) + return err; + + p = bin2hex(version, reply, OMNIA_FW_VERSION_LEN); + *p = '\0'; + + return 0; +} + +static ssize_t fw_version_hash_show(struct device *dev, char *buf, + bool bootloader) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + char version[OMNIA_FW_VERSION_HEX_LEN]; + int err; + + err = omnia_get_version_hash(mcu, bootloader, version); + if (err) + return err; + + return sysfs_emit(buf, "%s\n", version); +} + +static ssize_t fw_version_hash_application_show(struct device *dev, + struct device_attribute *a, + char *buf) +{ + return fw_version_hash_show(dev, buf, false); +} +static DEVICE_ATTR_RO(fw_version_hash_application); + +static ssize_t fw_version_hash_bootloader_show(struct device *dev, + struct device_attribute *a, + char *buf) +{ + return fw_version_hash_show(dev, buf, true); +} +static DEVICE_ATTR_RO(fw_version_hash_bootloader); + +static ssize_t fw_features_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "0x%x\n", mcu->features); +} +static DEVICE_ATTR_RO(fw_features); + +static ssize_t mcu_type_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%s\n", mcu->type); +} +static DEVICE_ATTR_RO(mcu_type); + +static ssize_t reset_selector_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + u8 reply; + int err; + + err = omnia_cmd_read_u8(to_i2c_client(dev), OMNIA_CMD_GET_RESET, + &reply); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", reply); +} +static DEVICE_ATTR_RO(reset_selector); + +static ssize_t serial_number_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%016llX\n", mcu->board_serial_number); +} +static DEVICE_ATTR_RO(serial_number); + +static ssize_t first_mac_address_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%pM\n", mcu->board_first_mac); +} +static DEVICE_ATTR_RO(first_mac_address); + +static ssize_t board_revision_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", mcu->board_revision); +} +static DEVICE_ATTR_RO(board_revision); + +static struct attribute *omnia_mcu_base_attrs[] = { + &dev_attr_fw_version_hash_application.attr, + &dev_attr_fw_version_hash_bootloader.attr, + &dev_attr_fw_features.attr, + &dev_attr_mcu_type.attr, + &dev_attr_reset_selector.attr, + &dev_attr_serial_number.attr, + &dev_attr_first_mac_address.attr, + &dev_attr_board_revision.attr, + NULL +}; + +static umode_t omnia_mcu_base_attrs_visible(struct kobject *kobj, + struct attribute *a, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + if ((a == &dev_attr_serial_number.attr || + a == &dev_attr_first_mac_address.attr || + a == &dev_attr_board_revision.attr) && + !(mcu->features & OMNIA_FEAT_BOARD_INFO)) + return 0; + + return a->mode; +} + +static const struct attribute_group omnia_mcu_base_group = { + .attrs = omnia_mcu_base_attrs, + .is_visible = omnia_mcu_base_attrs_visible, +}; + +static const struct attribute_group *omnia_mcu_groups[] = { + &omnia_mcu_base_group, +#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO + &omnia_mcu_gpio_group, +#endif +#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP + &omnia_mcu_poweroff_group, +#endif + NULL +}; + +static void omnia_mcu_print_version_hash(struct omnia_mcu *mcu, bool bootloader) +{ + const char *type = bootloader ? "bootloader" : "application"; + struct device *dev = &mcu->client->dev; + char version[OMNIA_FW_VERSION_HEX_LEN]; + int err; + + err = omnia_get_version_hash(mcu, bootloader, version); + if (err) { + dev_err(dev, "Cannot read MCU %s firmware version: %d\n", + type, err); + return; + } + + dev_info(dev, "MCU %s firmware version hash: %s\n", type, version); +} + +static const char *omnia_status_to_mcu_type(u16 status) +{ + switch (status & OMNIA_STS_MCU_TYPE_MASK) { + case OMNIA_STS_MCU_TYPE_STM32: + return "STM32"; + case OMNIA_STS_MCU_TYPE_GD32: + return "GD32"; + case OMNIA_STS_MCU_TYPE_MKL: + return "MKL"; + default: + return "unknown"; + } +} + +static void omnia_info_missing_feature(struct device *dev, const char *feature) +{ + dev_info(dev, + "Your board's MCU firmware does not support the %s feature.\n", + feature); +} + +static int omnia_mcu_read_features(struct omnia_mcu *mcu) +{ + static const struct { + u16 mask; + const char *name; + } features[] = { +#define _DEF_FEAT(_n, _m) { OMNIA_FEAT_ ## _n, _m } + _DEF_FEAT(EXT_CMDS, "extended control and status"), + _DEF_FEAT(WDT_PING, "watchdog pinging"), + _DEF_FEAT(LED_STATE_EXT_MASK, "peripheral LED pins reading"), + _DEF_FEAT(NEW_INT_API, "new interrupt API"), + _DEF_FEAT(POWEROFF_WAKEUP, "poweroff and wakeup"), + _DEF_FEAT(TRNG, "true random number generator"), + _DEF_FEAT(BRIGHTNESS_INT, "LED panel brightness change interrupt"), + _DEF_FEAT(LED_GAMMA_CORRECTION, "LED gamma correction"), +#undef _DEF_FEAT + }; + struct i2c_client *client = mcu->client; + struct device *dev = &client->dev; + bool suggest_fw_upgrade = false; + u16 status; + int err; + + /* status word holds MCU type, which we need below */ + err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_STATUS_WORD, &status); + if (err) + return err; + + /* + * Check whether MCU firmware supports the OMNIA_CMD_GET_FEATURES + * command. + */ + if (status & OMNIA_STS_FEATURES_SUPPORTED) { + /* try read 32-bit features */ + err = omnia_cmd_read_u32(client, OMNIA_CMD_GET_FEATURES, + &mcu->features); + if (err) { + /* try read 16-bit features */ + u16 features16; + + err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_FEATURES, + &features16); + if (err) + return err; + + mcu->features = features16; + } else { + if (mcu->features & OMNIA_FEAT_FROM_BIT_16_INVALID) + mcu->features &= GENMASK(15, 0); + } + } else { + dev_info(dev, + "Your board's MCU firmware does not support feature reading.\n"); + suggest_fw_upgrade = true; + } + + mcu->type = omnia_status_to_mcu_type(status); + dev_info(dev, "MCU type %s%s\n", mcu->type, + (mcu->features & OMNIA_FEAT_PERIPH_MCU) ? + ", with peripheral resets wired" : ""); + + omnia_mcu_print_version_hash(mcu, true); + + if (mcu->features & OMNIA_FEAT_BOOTLOADER) + dev_warn(dev, + "MCU is running bootloader firmware. Was firmware upgrade interrupted?\n"); + else + omnia_mcu_print_version_hash(mcu, false); + + for (unsigned int i = 0; i < ARRAY_SIZE(features); i++) { + if (mcu->features & features[i].mask) + continue; + + omnia_info_missing_feature(dev, features[i].name); + suggest_fw_upgrade = true; + } + + if (suggest_fw_upgrade) + dev_info(dev, + "Consider upgrading MCU firmware with the omnia-mcutool utility.\n"); + + return 0; +} + +static int omnia_mcu_read_board_info(struct omnia_mcu *mcu) +{ + u8 reply[1 + OMNIA_BOARD_INFO_LEN]; + int err; + + err = omnia_cmd_read(mcu->client, OMNIA_CMD_BOARD_INFO_GET, reply, + sizeof(reply)); + if (err) + return err; + + if (reply[0] != OMNIA_BOARD_INFO_LEN) + return -EIO; + + mcu->board_serial_number = get_unaligned_le64(&reply[1]); + + /* we can't use ether_addr_copy() because reply is not u16-aligned */ + memcpy(mcu->board_first_mac, &reply[9], sizeof(mcu->board_first_mac)); + + mcu->board_revision = reply[15]; + + return 0; +} + +static int omnia_mcu_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct omnia_mcu *mcu; + int err; + + if (!client->irq) + return dev_err_probe(dev, -EINVAL, "IRQ resource not found\n"); + + mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL); + if (!mcu) + return -ENOMEM; + + mcu->client = client; + i2c_set_clientdata(client, mcu); + + err = omnia_mcu_read_features(mcu); + if (err) + return dev_err_probe(dev, err, + "Cannot determine MCU supported features\n"); + + if (mcu->features & OMNIA_FEAT_BOARD_INFO) { + err = omnia_mcu_read_board_info(mcu); + if (err) + return dev_err_probe(dev, err, + "Cannot read board info\n"); + } + + err = omnia_mcu_register_sys_off_and_wakeup(mcu); + if (err) + return err; + + err = omnia_mcu_register_watchdog(mcu); + if (err) + return err; + + err = omnia_mcu_register_gpiochip(mcu); + if (err) + return err; + + err = omnia_mcu_register_keyctl(mcu); + if (err) + return err; + + return omnia_mcu_register_trng(mcu); +} + +static const struct of_device_id of_omnia_mcu_match[] = { + { .compatible = "cznic,turris-omnia-mcu" }, + {} +}; + +static struct i2c_driver omnia_mcu_driver = { + .probe = omnia_mcu_probe, + .driver = { + .name = "turris-omnia-mcu", + .of_match_table = of_omnia_mcu_match, + .dev_groups = omnia_mcu_groups, + }, +}; +module_i2c_driver(omnia_mcu_driver); + +MODULE_AUTHOR("Marek Behun "); +MODULE_DESCRIPTION("CZ.NIC's Turris Omnia MCU"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/cznic/turris-omnia-mcu-gpio.c b/drivers/platform/cznic/turris-omnia-mcu-gpio.c new file mode 100644 index 000000000..4e430d6c3 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-gpio.c @@ -0,0 +1,1123 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU GPIO and IRQ driver + * + * 2024 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "turris-omnia-mcu.h" + +#define OMNIA_CMD_INT_ARG_LEN 8 +#define FRONT_BUTTON_RELEASE_DELAY_MS 50 + +static const char * const omnia_mcu_gpio_names[64] = { + /* GPIOs with value read from the 16-bit wide status */ + [4] = "MiniPCIe0 Card Detect", + [5] = "MiniPCIe0 mSATA Indicator", + [6] = "Front USB3 port over-current", + [7] = "Rear USB3 port over-current", + [8] = "Front USB3 port power", + [9] = "Rear USB3 port power", + [12] = "Front Button", + + /* GPIOs with value read from the 32-bit wide extended status */ + [16] = "SFP nDET", + [28] = "MiniPCIe0 LED", + [29] = "MiniPCIe1 LED", + [30] = "MiniPCIe2 LED", + [31] = "MiniPCIe0 PAN LED", + [32] = "MiniPCIe1 PAN LED", + [33] = "MiniPCIe2 PAN LED", + [34] = "WAN PHY LED0", + [35] = "WAN PHY LED1", + [36] = "LAN switch p0 LED0", + [37] = "LAN switch p0 LED1", + [38] = "LAN switch p1 LED0", + [39] = "LAN switch p1 LED1", + [40] = "LAN switch p2 LED0", + [41] = "LAN switch p2 LED1", + [42] = "LAN switch p3 LED0", + [43] = "LAN switch p3 LED1", + [44] = "LAN switch p4 LED0", + [45] = "LAN switch p4 LED1", + [46] = "LAN switch p5 LED0", + [47] = "LAN switch p5 LED1", + + /* GPIOs with value read from the 16-bit wide extended control status */ + [48] = "eMMC nRESET", + [49] = "LAN switch nRESET", + [50] = "WAN PHY nRESET", + [51] = "MiniPCIe0 nPERST", + [52] = "MiniPCIe1 nPERST", + [53] = "MiniPCIe2 nPERST", + [54] = "WAN PHY SFP mux", + [56] = "VHV power disable", +}; + +struct omnia_gpio { + u8 cmd; + u8 ctl_cmd; + u8 bit; + u8 ctl_bit; + u8 int_bit; + u16 feat; + u16 feat_mask; +}; + +#define OMNIA_GPIO_INVALID_INT_BIT 0xff + +#define _DEF_GPIO(_cmd, _ctl_cmd, _bit, _ctl_bit, _int_bit, _feat, _feat_mask) \ + { \ + .cmd = _cmd, \ + .ctl_cmd = _ctl_cmd, \ + .bit = _bit, \ + .ctl_bit = _ctl_bit, \ + .int_bit = (_int_bit) < 0 ? OMNIA_GPIO_INVALID_INT_BIT \ + : (_int_bit), \ + .feat = _feat, \ + .feat_mask = _feat_mask, \ + } + +#define _DEF_GPIO_STS(_name) \ + _DEF_GPIO(OMNIA_CMD_GET_STATUS_WORD, 0, __bf_shf(OMNIA_STS_ ## _name), \ + 0, __bf_shf(OMNIA_INT_ ## _name), 0, 0) + +#define _DEF_GPIO_CTL(_name) \ + _DEF_GPIO(OMNIA_CMD_GET_STATUS_WORD, OMNIA_CMD_GENERAL_CONTROL, \ + __bf_shf(OMNIA_STS_ ## _name), __bf_shf(OMNIA_CTL_ ## _name), \ + -1, 0, 0) + +#define _DEF_GPIO_EXT_STS(_name, _feat) \ + _DEF_GPIO(OMNIA_CMD_GET_EXT_STATUS_DWORD, 0, \ + __bf_shf(OMNIA_EXT_STS_ ## _name), 0, \ + __bf_shf(OMNIA_INT_ ## _name), \ + OMNIA_FEAT_ ## _feat | OMNIA_FEAT_EXT_CMDS, \ + OMNIA_FEAT_ ## _feat | OMNIA_FEAT_EXT_CMDS) + +#define _DEF_GPIO_EXT_STS_LED(_name, _ledext) \ + _DEF_GPIO(OMNIA_CMD_GET_EXT_STATUS_DWORD, 0, \ + __bf_shf(OMNIA_EXT_STS_ ## _name), 0, \ + __bf_shf(OMNIA_INT_ ## _name), \ + OMNIA_FEAT_LED_STATE_ ## _ledext, \ + OMNIA_FEAT_LED_STATE_EXT_MASK) + +#define _DEF_GPIO_EXT_STS_LEDALL(_name) \ + _DEF_GPIO(OMNIA_CMD_GET_EXT_STATUS_DWORD, 0, \ + __bf_shf(OMNIA_EXT_STS_ ## _name), 0, \ + __bf_shf(OMNIA_INT_ ## _name), \ + OMNIA_FEAT_LED_STATE_EXT_MASK, 0) + +#define _DEF_GPIO_EXT_CTL(_name, _feat) \ + _DEF_GPIO(OMNIA_CMD_GET_EXT_CONTROL_STATUS, OMNIA_CMD_EXT_CONTROL, \ + __bf_shf(OMNIA_EXT_CTL_ ## _name), \ + __bf_shf(OMNIA_EXT_CTL_ ## _name), -1, \ + OMNIA_FEAT_ ## _feat | OMNIA_FEAT_EXT_CMDS, \ + OMNIA_FEAT_ ## _feat | OMNIA_FEAT_EXT_CMDS) + +#define _DEF_INT(_name) \ + _DEF_GPIO(0, 0, 0, 0, __bf_shf(OMNIA_INT_ ## _name), 0, 0) + +static inline bool is_int_bit_valid(const struct omnia_gpio *gpio) +{ + return gpio->int_bit != OMNIA_GPIO_INVALID_INT_BIT; +} + +static const struct omnia_gpio omnia_gpios[64] = { + /* GPIOs with value read from the 16-bit wide status */ + [4] = _DEF_GPIO_STS(CARD_DET), + [5] = _DEF_GPIO_STS(MSATA_IND), + [6] = _DEF_GPIO_STS(USB30_OVC), + [7] = _DEF_GPIO_STS(USB31_OVC), + [8] = _DEF_GPIO_CTL(USB30_PWRON), + [9] = _DEF_GPIO_CTL(USB31_PWRON), + + /* brightness changed interrupt, no GPIO */ + [11] = _DEF_INT(BRIGHTNESS_CHANGED), + + [12] = _DEF_GPIO_STS(BUTTON_PRESSED), + + /* TRNG interrupt, no GPIO */ + [13] = _DEF_INT(TRNG), + + /* MESSAGE_SIGNED interrupt, no GPIO */ + [14] = _DEF_INT(MESSAGE_SIGNED), + + /* GPIOs with value read from the 32-bit wide extended status */ + [16] = _DEF_GPIO_EXT_STS(SFP_nDET, PERIPH_MCU), + [28] = _DEF_GPIO_EXT_STS_LEDALL(WLAN0_MSATA_LED), + [29] = _DEF_GPIO_EXT_STS_LEDALL(WLAN1_LED), + [30] = _DEF_GPIO_EXT_STS_LEDALL(WLAN2_LED), + [31] = _DEF_GPIO_EXT_STS_LED(WPAN0_LED, EXT), + [32] = _DEF_GPIO_EXT_STS_LED(WPAN1_LED, EXT), + [33] = _DEF_GPIO_EXT_STS_LED(WPAN2_LED, EXT), + [34] = _DEF_GPIO_EXT_STS_LEDALL(WAN_LED0), + [35] = _DEF_GPIO_EXT_STS_LED(WAN_LED1, EXT_V32), + [36] = _DEF_GPIO_EXT_STS_LEDALL(LAN0_LED0), + [37] = _DEF_GPIO_EXT_STS_LEDALL(LAN0_LED1), + [38] = _DEF_GPIO_EXT_STS_LEDALL(LAN1_LED0), + [39] = _DEF_GPIO_EXT_STS_LEDALL(LAN1_LED1), + [40] = _DEF_GPIO_EXT_STS_LEDALL(LAN2_LED0), + [41] = _DEF_GPIO_EXT_STS_LEDALL(LAN2_LED1), + [42] = _DEF_GPIO_EXT_STS_LEDALL(LAN3_LED0), + [43] = _DEF_GPIO_EXT_STS_LEDALL(LAN3_LED1), + [44] = _DEF_GPIO_EXT_STS_LEDALL(LAN4_LED0), + [45] = _DEF_GPIO_EXT_STS_LEDALL(LAN4_LED1), + [46] = _DEF_GPIO_EXT_STS_LEDALL(LAN5_LED0), + [47] = _DEF_GPIO_EXT_STS_LEDALL(LAN5_LED1), + + /* GPIOs with value read from the 16-bit wide extended control status */ + [48] = _DEF_GPIO_EXT_CTL(nRES_MMC, PERIPH_MCU), + [49] = _DEF_GPIO_EXT_CTL(nRES_LAN, PERIPH_MCU), + [50] = _DEF_GPIO_EXT_CTL(nRES_PHY, PERIPH_MCU), + [51] = _DEF_GPIO_EXT_CTL(nPERST0, PERIPH_MCU), + [52] = _DEF_GPIO_EXT_CTL(nPERST1, PERIPH_MCU), + [53] = _DEF_GPIO_EXT_CTL(nPERST2, PERIPH_MCU), + [54] = _DEF_GPIO_EXT_CTL(PHY_SFP, PERIPH_MCU), + [56] = _DEF_GPIO_EXT_CTL(nVHV_CTRL, PERIPH_MCU), +}; + +/* mapping from interrupts to indexes of GPIOs in the omnia_gpios array */ +static const u8 omnia_int_to_gpio_idx[32] = { + [__bf_shf(OMNIA_INT_CARD_DET)] = 4, + [__bf_shf(OMNIA_INT_MSATA_IND)] = 5, + [__bf_shf(OMNIA_INT_USB30_OVC)] = 6, + [__bf_shf(OMNIA_INT_USB31_OVC)] = 7, + [__bf_shf(OMNIA_INT_BUTTON_PRESSED)] = 12, + [__bf_shf(OMNIA_INT_TRNG)] = 13, + [__bf_shf(OMNIA_INT_MESSAGE_SIGNED)] = 14, + [__bf_shf(OMNIA_INT_SFP_nDET)] = 16, + [__bf_shf(OMNIA_INT_BRIGHTNESS_CHANGED)] = 11, + [__bf_shf(OMNIA_INT_WLAN0_MSATA_LED)] = 28, + [__bf_shf(OMNIA_INT_WLAN1_LED)] = 29, + [__bf_shf(OMNIA_INT_WLAN2_LED)] = 30, + [__bf_shf(OMNIA_INT_WPAN0_LED)] = 31, + [__bf_shf(OMNIA_INT_WPAN1_LED)] = 32, + [__bf_shf(OMNIA_INT_WPAN2_LED)] = 33, + [__bf_shf(OMNIA_INT_WAN_LED0)] = 34, + [__bf_shf(OMNIA_INT_WAN_LED1)] = 35, + [__bf_shf(OMNIA_INT_LAN0_LED0)] = 36, + [__bf_shf(OMNIA_INT_LAN0_LED1)] = 37, + [__bf_shf(OMNIA_INT_LAN1_LED0)] = 38, + [__bf_shf(OMNIA_INT_LAN1_LED1)] = 39, + [__bf_shf(OMNIA_INT_LAN2_LED0)] = 40, + [__bf_shf(OMNIA_INT_LAN2_LED1)] = 41, + [__bf_shf(OMNIA_INT_LAN3_LED0)] = 42, + [__bf_shf(OMNIA_INT_LAN3_LED1)] = 43, + [__bf_shf(OMNIA_INT_LAN4_LED0)] = 44, + [__bf_shf(OMNIA_INT_LAN4_LED1)] = 45, + [__bf_shf(OMNIA_INT_LAN5_LED0)] = 46, + [__bf_shf(OMNIA_INT_LAN5_LED1)] = 47, +}; + +/* index of PHY_SFP GPIO in the omnia_gpios array */ +#define OMNIA_GPIO_PHY_SFP_OFFSET 54 + +static int omnia_ctl_cmd_locked(struct omnia_mcu *mcu, u8 cmd, u16 val, u16 mask) +{ + unsigned int len; + u8 buf[5]; + + buf[0] = cmd; + + switch (cmd) { + case OMNIA_CMD_GENERAL_CONTROL: + buf[1] = val; + buf[2] = mask; + len = 3; + break; + + case OMNIA_CMD_EXT_CONTROL: + put_unaligned_le16(val, &buf[1]); + put_unaligned_le16(mask, &buf[3]); + len = 5; + break; + + default: + BUG(); + } + + return omnia_cmd_write(mcu->client, buf, len); +} + +static int omnia_ctl_cmd(struct omnia_mcu *mcu, u8 cmd, u16 val, u16 mask) +{ + guard(mutex)(&mcu->lock); + + return omnia_ctl_cmd_locked(mcu, cmd, val, mask); +} + +static int omnia_gpio_request(struct gpio_chip *gc, unsigned int offset) +{ + if (!omnia_gpios[offset].cmd) + return -EINVAL; + + return 0; +} + +static int omnia_gpio_get_direction(struct gpio_chip *gc, unsigned int offset) +{ + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + if (offset == OMNIA_GPIO_PHY_SFP_OFFSET) { + int val; + + scoped_guard(mutex, &mcu->lock) { + val = omnia_cmd_read_bit(mcu->client, + OMNIA_CMD_GET_EXT_CONTROL_STATUS, + OMNIA_EXT_CTL_PHY_SFP_AUTO); + if (val < 0) + return val; + } + + if (val) + return GPIO_LINE_DIRECTION_IN; + + return GPIO_LINE_DIRECTION_OUT; + } + + if (omnia_gpios[offset].ctl_cmd) + return GPIO_LINE_DIRECTION_OUT; + + return GPIO_LINE_DIRECTION_IN; +} + +static int omnia_gpio_direction_input(struct gpio_chip *gc, unsigned int offset) +{ + const struct omnia_gpio *gpio = &omnia_gpios[offset]; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + if (offset == OMNIA_GPIO_PHY_SFP_OFFSET) + return omnia_ctl_cmd(mcu, OMNIA_CMD_EXT_CONTROL, + OMNIA_EXT_CTL_PHY_SFP_AUTO, + OMNIA_EXT_CTL_PHY_SFP_AUTO); + + if (gpio->ctl_cmd) + return -ENOTSUPP; + + return 0; +} + +static int omnia_gpio_direction_output(struct gpio_chip *gc, + unsigned int offset, int value) +{ + const struct omnia_gpio *gpio = &omnia_gpios[offset]; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + u16 val, mask; + + if (!gpio->ctl_cmd) + return -ENOTSUPP; + + mask = BIT(gpio->ctl_bit); + val = value ? mask : 0; + + if (offset == OMNIA_GPIO_PHY_SFP_OFFSET) + mask |= OMNIA_EXT_CTL_PHY_SFP_AUTO; + + return omnia_ctl_cmd(mcu, gpio->ctl_cmd, val, mask); +} + +static int omnia_gpio_get(struct gpio_chip *gc, unsigned int offset) +{ + const struct omnia_gpio *gpio = &omnia_gpios[offset]; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + /* + * If firmware does not support the new interrupt API, we are informed + * of every change of the status word by an interrupt from MCU and save + * its value in the interrupt service routine. Simply return the saved + * value. + */ + if (gpio->cmd == OMNIA_CMD_GET_STATUS_WORD && + !(mcu->features & OMNIA_FEAT_NEW_INT_API)) + return test_bit(gpio->bit, &mcu->last_status); + + guard(mutex)(&mcu->lock); + + /* + * If firmware does support the new interrupt API, we may have cached + * the value of a GPIO in the interrupt service routine. If not, read + * the relevant bit now. + */ + if (is_int_bit_valid(gpio) && test_bit(gpio->int_bit, &mcu->is_cached)) + return test_bit(gpio->int_bit, &mcu->cached); + + return omnia_cmd_read_bit(mcu->client, gpio->cmd, BIT(gpio->bit)); +} + +static unsigned long * +_relevant_field_for_sts_cmd(u8 cmd, unsigned long *sts, unsigned long *ext_sts, + unsigned long *ext_ctl) +{ + switch (cmd) { + case OMNIA_CMD_GET_STATUS_WORD: + return sts; + case OMNIA_CMD_GET_EXT_STATUS_DWORD: + return ext_sts; + case OMNIA_CMD_GET_EXT_CONTROL_STATUS: + return ext_ctl; + default: + return NULL; + } +} + +static int omnia_gpio_get_multiple(struct gpio_chip *gc, unsigned long *mask, + unsigned long *bits) +{ + unsigned long sts = 0, ext_sts = 0, ext_ctl = 0, *field; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + struct i2c_client *client = mcu->client; + unsigned int i; + int err; + + /* determine which bits to read from the 3 possible commands */ + for_each_set_bit(i, mask, ARRAY_SIZE(omnia_gpios)) { + field = _relevant_field_for_sts_cmd(omnia_gpios[i].cmd, + &sts, &ext_sts, &ext_ctl); + if (!field) + continue; + + __set_bit(omnia_gpios[i].bit, field); + } + + guard(mutex)(&mcu->lock); + + if (mcu->features & OMNIA_FEAT_NEW_INT_API) { + /* read relevant bits from status */ + err = omnia_cmd_read_bits(client, OMNIA_CMD_GET_STATUS_WORD, + sts, &sts); + if (err) + return err; + } else { + /* + * Use status word value cached in the interrupt service routine + * if firmware does not support the new interrupt API. + */ + sts = mcu->last_status; + } + + /* read relevant bits from extended status */ + err = omnia_cmd_read_bits(client, OMNIA_CMD_GET_EXT_STATUS_DWORD, + ext_sts, &ext_sts); + if (err) + return err; + + /* read relevant bits from extended control */ + err = omnia_cmd_read_bits(client, OMNIA_CMD_GET_EXT_CONTROL_STATUS, + ext_ctl, &ext_ctl); + if (err) + return err; + + /* assign relevant bits in result */ + for_each_set_bit(i, mask, ARRAY_SIZE(omnia_gpios)) { + field = _relevant_field_for_sts_cmd(omnia_gpios[i].cmd, + &sts, &ext_sts, &ext_ctl); + if (!field) + continue; + + __assign_bit(i, bits, test_bit(omnia_gpios[i].bit, field)); + } + + return 0; +} + +static int omnia_gpio_set(struct gpio_chip *gc, unsigned int offset, int value) +{ + const struct omnia_gpio *gpio = &omnia_gpios[offset]; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + u16 val, mask; + + if (!gpio->ctl_cmd) + return -EINVAL; + + mask = BIT(gpio->ctl_bit); + val = value ? mask : 0; + + return omnia_ctl_cmd(mcu, gpio->ctl_cmd, val, mask); +} + +static int omnia_gpio_set_multiple(struct gpio_chip *gc, unsigned long *mask, + unsigned long *bits) +{ + unsigned long ctl = 0, ctl_mask = 0, ext_ctl = 0, ext_ctl_mask = 0; + struct omnia_mcu *mcu = gpiochip_get_data(gc); + unsigned int i; + int err; + + for_each_set_bit(i, mask, ARRAY_SIZE(omnia_gpios)) { + unsigned long *field, *field_mask; + u8 bit = omnia_gpios[i].ctl_bit; + + switch (omnia_gpios[i].ctl_cmd) { + case OMNIA_CMD_GENERAL_CONTROL: + field = &ctl; + field_mask = &ctl_mask; + break; + case OMNIA_CMD_EXT_CONTROL: + field = &ext_ctl; + field_mask = &ext_ctl_mask; + break; + default: + field = field_mask = NULL; + break; + } + + if (!field) + continue; + + __set_bit(bit, field_mask); + __assign_bit(bit, field, test_bit(i, bits)); + } + + guard(mutex)(&mcu->lock); + + if (ctl_mask) { + err = omnia_ctl_cmd_locked(mcu, OMNIA_CMD_GENERAL_CONTROL, + ctl, ctl_mask); + if (err) + return err; + } + + if (ext_ctl_mask) { + err = omnia_ctl_cmd_locked(mcu, OMNIA_CMD_EXT_CONTROL, + ext_ctl, ext_ctl_mask); + if (err) + return err; + } + + return 0; +} + +static bool omnia_gpio_available(struct omnia_mcu *mcu, + const struct omnia_gpio *gpio) +{ + if (gpio->feat_mask) + return (mcu->features & gpio->feat_mask) == gpio->feat; + + if (gpio->feat) + return mcu->features & gpio->feat; + + return true; +} + +static int omnia_gpio_init_valid_mask(struct gpio_chip *gc, + unsigned long *valid_mask, + unsigned int ngpios) +{ + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + for (unsigned int i = 0; i < ngpios; i++) { + const struct omnia_gpio *gpio = &omnia_gpios[i]; + + if (gpio->cmd || is_int_bit_valid(gpio)) + __assign_bit(i, valid_mask, + omnia_gpio_available(mcu, gpio)); + else + __clear_bit(i, valid_mask); + } + + return 0; +} + +static int omnia_gpio_of_xlate(struct gpio_chip *gc, + const struct of_phandle_args *gpiospec, + u32 *flags) +{ + u32 bank, gpio; + + if (WARN_ON(gpiospec->args_count != 3)) + return -EINVAL; + + if (flags) + *flags = gpiospec->args[2]; + + bank = gpiospec->args[0]; + gpio = gpiospec->args[1]; + + switch (bank) { + case 0: + return gpio < 16 ? gpio : -EINVAL; + case 1: + return gpio < 32 ? 16 + gpio : -EINVAL; + case 2: + return gpio < 16 ? 48 + gpio : -EINVAL; + default: + return -EINVAL; + } +} + +static void omnia_irq_shutdown(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + irq_hw_number_t hwirq = irqd_to_hwirq(d); + u8 bit = omnia_gpios[hwirq].int_bit; + + __clear_bit(bit, &mcu->rising); + __clear_bit(bit, &mcu->falling); +} + +static void omnia_irq_mask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + irq_hw_number_t hwirq = irqd_to_hwirq(d); + u8 bit = omnia_gpios[hwirq].int_bit; + + if (!omnia_gpios[hwirq].cmd) + __clear_bit(bit, &mcu->rising); + __clear_bit(bit, &mcu->mask); + gpiochip_disable_irq(gc, hwirq); +} + +static void omnia_irq_unmask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + irq_hw_number_t hwirq = irqd_to_hwirq(d); + u8 bit = omnia_gpios[hwirq].int_bit; + + gpiochip_enable_irq(gc, hwirq); + __set_bit(bit, &mcu->mask); + if (!omnia_gpios[hwirq].cmd) + __set_bit(bit, &mcu->rising); +} + +static int omnia_irq_set_type(struct irq_data *d, unsigned int type) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + irq_hw_number_t hwirq = irqd_to_hwirq(d); + struct device *dev = &mcu->client->dev; + u8 bit = omnia_gpios[hwirq].int_bit; + + if (!(type & IRQ_TYPE_EDGE_BOTH)) { + dev_err(dev, "irq %u: unsupported type %u\n", d->irq, type); + return -EINVAL; + } + + __assign_bit(bit, &mcu->rising, type & IRQ_TYPE_EDGE_RISING); + __assign_bit(bit, &mcu->falling, type & IRQ_TYPE_EDGE_FALLING); + + return 0; +} + +static void omnia_irq_bus_lock(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + /* nothing to do if MCU firmware does not support new interrupt API */ + if (!(mcu->features & OMNIA_FEAT_NEW_INT_API)) + return; + + mutex_lock(&mcu->lock); +} + +/** + * omnia_mask_interleave - Interleaves the bytes from @rising and @falling + * @dst: the destination u8 array of interleaved bytes + * @rising: rising mask + * @falling: falling mask + * + * Interleaves the little-endian bytes from @rising and @falling words. + * + * If @rising = (r0, r1, r2, r3) and @falling = (f0, f1, f2, f3), the result is + * @dst = (r0, f0, r1, f1, r2, f2, r3, f3). + * + * The MCU receives an interrupt mask and reports a pending interrupt bitmap in + * this interleaved format. The rationale behind this is that the low-indexed + * bits are more important - in many cases, the user will be interested only in + * interrupts with indexes 0 to 7, and so the system can stop reading after + * first 2 bytes (r0, f0), to save time on the slow I2C bus. + * + * Feel free to remove this function and its inverse, omnia_mask_deinterleave, + * and use an appropriate bitmap_*() function once such a function exists. + */ +static void +omnia_mask_interleave(u8 *dst, unsigned long rising, unsigned long falling) +{ + for (unsigned int i = 0; i < sizeof(u32); i++) { + dst[2 * i] = rising >> (8 * i); + dst[2 * i + 1] = falling >> (8 * i); + } +} + +/** + * omnia_mask_deinterleave - Deinterleaves the bytes into @rising and @falling + * @src: the source u8 array containing the interleaved bytes + * @rising: pointer where to store the rising mask gathered from @src + * @falling: pointer where to store the falling mask gathered from @src + * + * This is the inverse function to omnia_mask_interleave. + */ +static void omnia_mask_deinterleave(const u8 *src, unsigned long *rising, + unsigned long *falling) +{ + *rising = *falling = 0; + + for (unsigned int i = 0; i < sizeof(u32); i++) { + *rising |= src[2 * i] << (8 * i); + *falling |= src[2 * i + 1] << (8 * i); + } +} + +static void omnia_irq_bus_sync_unlock(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct omnia_mcu *mcu = gpiochip_get_data(gc); + struct device *dev = &mcu->client->dev; + u8 cmd[1 + OMNIA_CMD_INT_ARG_LEN]; + unsigned long rising, falling; + int err; + + /* nothing to do if MCU firmware does not support new interrupt API */ + if (!(mcu->features & OMNIA_FEAT_NEW_INT_API)) + return; + + cmd[0] = OMNIA_CMD_SET_INT_MASK; + + rising = mcu->rising & mcu->mask; + falling = mcu->falling & mcu->mask; + + /* interleave the rising and falling bytes into the command arguments */ + omnia_mask_interleave(&cmd[1], rising, falling); + + dev_dbg(dev, "set int mask %8ph\n", &cmd[1]); + + err = omnia_cmd_write(mcu->client, cmd, sizeof(cmd)); + if (err) { + dev_err(dev, "Cannot set mask: %d\n", err); + goto unlock; + } + + /* + * Remember which GPIOs have both rising and falling interrupts enabled. + * For those we will cache their value so that .get() method is faster. + * We also need to forget cached values of GPIOs that aren't cached + * anymore. + */ + mcu->both = rising & falling; + mcu->is_cached &= mcu->both; + +unlock: + mutex_unlock(&mcu->lock); +} + +static const struct irq_chip omnia_mcu_irq_chip = { + .name = "Turris Omnia MCU interrupts", + .irq_shutdown = omnia_irq_shutdown, + .irq_mask = omnia_irq_mask, + .irq_unmask = omnia_irq_unmask, + .irq_set_type = omnia_irq_set_type, + .irq_bus_lock = omnia_irq_bus_lock, + .irq_bus_sync_unlock = omnia_irq_bus_sync_unlock, + .flags = IRQCHIP_IMMUTABLE, + GPIOCHIP_IRQ_RESOURCE_HELPERS, +}; + +static void omnia_irq_init_valid_mask(struct gpio_chip *gc, + unsigned long *valid_mask, + unsigned int ngpios) +{ + struct omnia_mcu *mcu = gpiochip_get_data(gc); + + for (unsigned int i = 0; i < ngpios; i++) { + const struct omnia_gpio *gpio = &omnia_gpios[i]; + + if (is_int_bit_valid(gpio)) + __assign_bit(i, valid_mask, + omnia_gpio_available(mcu, gpio)); + else + __clear_bit(i, valid_mask); + } +} + +static int omnia_irq_init_hw(struct gpio_chip *gc) +{ + struct omnia_mcu *mcu = gpiochip_get_data(gc); + u8 cmd[1 + OMNIA_CMD_INT_ARG_LEN] = {}; + + cmd[0] = OMNIA_CMD_SET_INT_MASK; + + return omnia_cmd_write(mcu->client, cmd, sizeof(cmd)); +} + +/* + * Determine how many bytes we need to read from the reply to the + * OMNIA_CMD_GET_INT_AND_CLEAR command in order to retrieve all unmasked + * interrupts. + */ +static unsigned int +omnia_irq_compute_pending_length(unsigned long rising, unsigned long falling) +{ + return max(omnia_compute_reply_length(rising, true, 0), + omnia_compute_reply_length(falling, true, 1)); +} + +static bool omnia_irq_read_pending_new(struct omnia_mcu *mcu, + unsigned long *pending) +{ + struct device *dev = &mcu->client->dev; + u8 reply[OMNIA_CMD_INT_ARG_LEN] = {}; + unsigned long rising, falling; + unsigned int len; + int err; + + len = omnia_irq_compute_pending_length(mcu->rising & mcu->mask, + mcu->falling & mcu->mask); + if (!len) + return false; + + guard(mutex)(&mcu->lock); + + err = omnia_cmd_read(mcu->client, OMNIA_CMD_GET_INT_AND_CLEAR, reply, + len); + if (err) { + dev_err(dev, "Cannot read pending IRQs: %d\n", err); + return false; + } + + /* deinterleave the reply bytes into rising and falling */ + omnia_mask_deinterleave(reply, &rising, &falling); + + rising &= mcu->mask; + falling &= mcu->mask; + *pending = rising | falling; + + /* cache values for GPIOs that have both edges enabled */ + mcu->is_cached &= ~(rising & falling); + mcu->is_cached |= mcu->both & (rising ^ falling); + mcu->cached = (mcu->cached | rising) & ~falling; + + return true; +} + +static int omnia_read_status_word_old_fw(struct omnia_mcu *mcu, + unsigned long *status) +{ + u16 raw_status; + int err; + + err = omnia_cmd_read_u16(mcu->client, OMNIA_CMD_GET_STATUS_WORD, + &raw_status); + if (err) + return err; + + /* + * Old firmware has a bug wherein it never resets the USB port + * overcurrent bits back to zero. Ignore them. + */ + *status = raw_status & ~(OMNIA_STS_USB30_OVC | OMNIA_STS_USB31_OVC); + + return 0; +} + +static void button_release_emul_fn(struct work_struct *work) +{ + struct omnia_mcu *mcu = container_of(to_delayed_work(work), + struct omnia_mcu, + button_release_emul_work); + + mcu->button_pressed_emul = false; + generic_handle_irq_safe(mcu->client->irq); +} + +static void +fill_int_from_sts(unsigned long *rising, unsigned long *falling, + unsigned long rising_sts, unsigned long falling_sts, + unsigned long sts_bit, unsigned long int_bit) +{ + if (rising_sts & sts_bit) + *rising |= int_bit; + if (falling_sts & sts_bit) + *falling |= int_bit; +} + +static bool omnia_irq_read_pending_old(struct omnia_mcu *mcu, + unsigned long *pending) +{ + unsigned long status, rising_sts, falling_sts, rising, falling; + struct device *dev = &mcu->client->dev; + int err; + + guard(mutex)(&mcu->lock); + + err = omnia_read_status_word_old_fw(mcu, &status); + if (err) { + dev_err(dev, "Cannot read pending IRQs: %d\n", err); + return false; + } + + /* + * The old firmware triggers an interrupt whenever status word changes, + * but does not inform about which bits rose or fell. We need to compute + * this here by comparing with the last status word value. + * + * The OMNIA_STS_BUTTON_PRESSED bit needs special handling, because the + * old firmware clears the OMNIA_STS_BUTTON_PRESSED bit on successful + * completion of the OMNIA_CMD_GET_STATUS_WORD command, resulting in + * another interrupt: + * - first we get an interrupt, we read the status word where + * OMNIA_STS_BUTTON_PRESSED is present, + * - MCU clears the OMNIA_STS_BUTTON_PRESSED bit because we read the + * status word, + * - we get another interrupt because the status word changed again + * (the OMNIA_STS_BUTTON_PRESSED bit was cleared). + * + * The gpiolib-cdev, gpiolib-sysfs and gpio-keys input driver all call + * the gpiochip's .get() method after an edge event on a requested GPIO + * occurs. + * + * We ensure that the .get() method reads 1 for the button GPIO for some + * time. + */ + + if (status & OMNIA_STS_BUTTON_PRESSED) { + mcu->button_pressed_emul = true; + mod_delayed_work(system_percpu_wq, &mcu->button_release_emul_work, + msecs_to_jiffies(FRONT_BUTTON_RELEASE_DELAY_MS)); + } else if (mcu->button_pressed_emul) { + status |= OMNIA_STS_BUTTON_PRESSED; + } + + rising_sts = ~mcu->last_status & status; + falling_sts = mcu->last_status & ~status; + + mcu->last_status = status; + + /* + * Fill in the relevant interrupt bits from status bits for CARD_DET, + * MSATA_IND and BUTTON_PRESSED. + */ + rising = 0; + falling = 0; + fill_int_from_sts(&rising, &falling, rising_sts, falling_sts, + OMNIA_STS_CARD_DET, OMNIA_INT_CARD_DET); + fill_int_from_sts(&rising, &falling, rising_sts, falling_sts, + OMNIA_STS_MSATA_IND, OMNIA_INT_MSATA_IND); + fill_int_from_sts(&rising, &falling, rising_sts, falling_sts, + OMNIA_STS_BUTTON_PRESSED, OMNIA_INT_BUTTON_PRESSED); + + /* Use only bits that are enabled */ + rising &= mcu->rising & mcu->mask; + falling &= mcu->falling & mcu->mask; + *pending = rising | falling; + + return true; +} + +static bool omnia_irq_read_pending(struct omnia_mcu *mcu, + unsigned long *pending) +{ + if (mcu->features & OMNIA_FEAT_NEW_INT_API) + return omnia_irq_read_pending_new(mcu, pending); + else + return omnia_irq_read_pending_old(mcu, pending); +} + +static irqreturn_t omnia_irq_thread_handler(int irq, void *dev_id) +{ + struct omnia_mcu *mcu = dev_id; + struct irq_domain *domain; + unsigned long pending; + unsigned int i; + + if (!omnia_irq_read_pending(mcu, &pending)) + return IRQ_NONE; + + domain = mcu->gc.irq.domain; + + for_each_set_bit(i, &pending, 32) { + unsigned int nested_irq; + + nested_irq = irq_find_mapping(domain, omnia_int_to_gpio_idx[i]); + + handle_nested_irq(nested_irq); + } + + return IRQ_RETVAL(pending); +} + +static const char * const front_button_modes[] = { "mcu", "cpu" }; + +static ssize_t front_button_mode_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + int val; + + if (mcu->features & OMNIA_FEAT_NEW_INT_API) { + val = omnia_cmd_read_bit(mcu->client, OMNIA_CMD_GET_STATUS_WORD, + OMNIA_STS_BUTTON_MODE); + if (val < 0) + return val; + } else { + val = !!(mcu->last_status & OMNIA_STS_BUTTON_MODE); + } + + return sysfs_emit(buf, "%s\n", front_button_modes[val]); +} + +static ssize_t front_button_mode_store(struct device *dev, + struct device_attribute *a, + const char *buf, size_t count) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + int err, i; + + i = sysfs_match_string(front_button_modes, buf); + if (i < 0) + return i; + + err = omnia_ctl_cmd_locked(mcu, OMNIA_CMD_GENERAL_CONTROL, + i ? OMNIA_CTL_BUTTON_MODE : 0, + OMNIA_CTL_BUTTON_MODE); + if (err) + return err; + + return count; +} +static DEVICE_ATTR_RW(front_button_mode); + +static struct attribute *omnia_mcu_gpio_attrs[] = { + &dev_attr_front_button_mode.attr, + NULL +}; + +const struct attribute_group omnia_mcu_gpio_group = { + .attrs = omnia_mcu_gpio_attrs, +}; + +int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu) +{ + bool new_api = mcu->features & OMNIA_FEAT_NEW_INT_API; + struct device *dev = &mcu->client->dev; + unsigned long irqflags; + int err; + + err = devm_mutex_init(dev, &mcu->lock); + if (err) + return err; + + mcu->gc.request = omnia_gpio_request; + mcu->gc.get_direction = omnia_gpio_get_direction; + mcu->gc.direction_input = omnia_gpio_direction_input; + mcu->gc.direction_output = omnia_gpio_direction_output; + mcu->gc.get = omnia_gpio_get; + mcu->gc.get_multiple = omnia_gpio_get_multiple; + mcu->gc.set = omnia_gpio_set; + mcu->gc.set_multiple = omnia_gpio_set_multiple; + mcu->gc.init_valid_mask = omnia_gpio_init_valid_mask; + mcu->gc.can_sleep = true; + mcu->gc.names = omnia_mcu_gpio_names; + mcu->gc.base = -1; + mcu->gc.ngpio = ARRAY_SIZE(omnia_gpios); + mcu->gc.label = "Turris Omnia MCU GPIOs"; + mcu->gc.parent = dev; + mcu->gc.owner = THIS_MODULE; + mcu->gc.of_gpio_n_cells = 3; + mcu->gc.of_xlate = omnia_gpio_of_xlate; + + gpio_irq_chip_set_chip(&mcu->gc.irq, &omnia_mcu_irq_chip); + /* This will let us handle the parent IRQ in the driver */ + mcu->gc.irq.parent_handler = NULL; + mcu->gc.irq.num_parents = 0; + mcu->gc.irq.parents = NULL; + mcu->gc.irq.default_type = IRQ_TYPE_NONE; + mcu->gc.irq.handler = handle_bad_irq; + mcu->gc.irq.threaded = true; + if (new_api) + mcu->gc.irq.init_hw = omnia_irq_init_hw; + mcu->gc.irq.init_valid_mask = omnia_irq_init_valid_mask; + + err = devm_gpiochip_add_data(dev, &mcu->gc, mcu); + if (err) + return dev_err_probe(dev, err, "Cannot add GPIO chip\n"); + + /* + * Before requesting the interrupt, if firmware does not support the new + * interrupt API, we need to cache the value of the status word, so that + * when it changes, we may compare the new value with the cached one in + * the interrupt handler. + */ + if (!new_api) { + err = omnia_read_status_word_old_fw(mcu, &mcu->last_status); + if (err) + return dev_err_probe(dev, err, + "Cannot read status word\n"); + + INIT_DELAYED_WORK(&mcu->button_release_emul_work, + button_release_emul_fn); + } + + irqflags = IRQF_ONESHOT; + if (new_api) + irqflags |= IRQF_TRIGGER_LOW; + else + irqflags |= IRQF_TRIGGER_FALLING; + + err = devm_request_threaded_irq(dev, mcu->client->irq, NULL, + omnia_irq_thread_handler, irqflags, + "turris-omnia-mcu", mcu); + if (err) + return dev_err_probe(dev, err, "Cannot request IRQ\n"); + + if (!new_api) { + /* + * The button_release_emul_work has to be initialized before the + * thread is requested, and on driver remove it needs to be + * canceled before the thread is freed. Therefore we can't use + * devm_delayed_work_autocancel() directly, because the order + * devm_delayed_work_autocancel(); + * devm_request_threaded_irq(); + * would cause improper release order: + * free_irq(); + * cancel_delayed_work_sync(); + * Instead we first initialize the work above, and only now + * after IRQ is requested we add the work devm action. + */ + err = devm_add_action(dev, devm_delayed_work_drop, + &mcu->button_release_emul_work); + if (err) + return err; + } + + return 0; +} + +int omnia_mcu_request_irq(struct omnia_mcu *mcu, u32 spec, + irq_handler_t thread_fn, const char *devname) +{ + u8 irq_idx; + int irq; + + if (!spec) + return -EINVAL; + + irq_idx = omnia_int_to_gpio_idx[ffs(spec) - 1]; + irq = gpiod_to_irq(gpio_device_get_desc(mcu->gc.gpiodev, irq_idx)); + if (irq < 0) + return irq; + + return devm_request_threaded_irq(&mcu->client->dev, irq, NULL, + thread_fn, IRQF_ONESHOT, devname, mcu); +} diff --git a/drivers/platform/cznic/turris-omnia-mcu-keyctl.c b/drivers/platform/cznic/turris-omnia-mcu-keyctl.c new file mode 100644 index 000000000..dc40f942f --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-keyctl.c @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU ECDSA message signing via keyctl + * + * 2025 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "turris-omnia-mcu.h" + +static irqreturn_t omnia_msg_signed_irq_handler(int irq, void *dev_id) +{ + u8 reply[1 + OMNIA_MCU_CRYPTO_SIGNATURE_LEN]; + struct omnia_mcu *mcu = dev_id; + int err; + + err = omnia_cmd_read(mcu->client, OMNIA_CMD_CRYPTO_COLLECT_SIGNATURE, + reply, sizeof(reply)); + if (!err && reply[0] != OMNIA_MCU_CRYPTO_SIGNATURE_LEN) + err = -EIO; + + guard(mutex)(&mcu->sign_lock); + + if (mcu->sign_requested) { + mcu->sign_err = err; + if (!err) + memcpy(mcu->signature, &reply[1], + OMNIA_MCU_CRYPTO_SIGNATURE_LEN); + mcu->sign_requested = false; + complete(&mcu->msg_signed); + } + + return IRQ_HANDLED; +} + +static int omnia_mcu_sign(const struct key *key, const void *msg, + void *signature) +{ + struct omnia_mcu *mcu = dev_get_drvdata(turris_signing_key_get_dev(key)); + u8 cmd[1 + SHA256_DIGEST_SIZE], reply; + int err; + + scoped_guard(mutex, &mcu->sign_lock) { + if (mcu->sign_requested) + return -EBUSY; + + cmd[0] = OMNIA_CMD_CRYPTO_SIGN_MESSAGE; + memcpy(&cmd[1], msg, SHA256_DIGEST_SIZE); + + err = omnia_cmd_write_read(mcu->client, cmd, sizeof(cmd), + &reply, 1); + if (err) + return err; + + if (!reply) + return -EBUSY; + + mcu->sign_requested = true; + } + + if (wait_for_completion_interruptible(&mcu->msg_signed)) + return -EINTR; + + guard(mutex)(&mcu->sign_lock); + + if (mcu->sign_err) + return mcu->sign_err; + + memcpy(signature, mcu->signature, OMNIA_MCU_CRYPTO_SIGNATURE_LEN); + + /* forget the signature, for security */ + memzero_explicit(mcu->signature, sizeof(mcu->signature)); + + return OMNIA_MCU_CRYPTO_SIGNATURE_LEN; +} + +static const void *omnia_mcu_get_public_key(const struct key *key) +{ + struct omnia_mcu *mcu = dev_get_drvdata(turris_signing_key_get_dev(key)); + + return mcu->board_public_key; +} + +static const struct turris_signing_key_subtype omnia_signing_key_subtype = { + .key_size = 256, + .data_size = SHA256_DIGEST_SIZE, + .sig_size = OMNIA_MCU_CRYPTO_SIGNATURE_LEN, + .public_key_size = OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN, + .hash_algo = "sha256", + .get_public_key = omnia_mcu_get_public_key, + .sign = omnia_mcu_sign, +}; + +static int omnia_mcu_read_public_key(struct omnia_mcu *mcu) +{ + u8 reply[1 + OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN]; + int err; + + err = omnia_cmd_read(mcu->client, OMNIA_CMD_CRYPTO_GET_PUBLIC_KEY, + reply, sizeof(reply)); + if (err) + return err; + + if (reply[0] != OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN) + return -EIO; + + memcpy(mcu->board_public_key, &reply[1], + OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN); + + return 0; +} + +int omnia_mcu_register_keyctl(struct omnia_mcu *mcu) +{ + struct device *dev = &mcu->client->dev; + char desc[48]; + int err; + + if (!(mcu->features & OMNIA_FEAT_CRYPTO)) + return 0; + + err = omnia_mcu_read_public_key(mcu); + if (err) + return dev_err_probe(dev, err, + "Cannot read board public key\n"); + + err = devm_mutex_init(dev, &mcu->sign_lock); + if (err) + return err; + + init_completion(&mcu->msg_signed); + + err = omnia_mcu_request_irq(mcu, OMNIA_INT_MESSAGE_SIGNED, + omnia_msg_signed_irq_handler, + "turris-omnia-mcu-keyctl"); + if (err) + return dev_err_probe(dev, err, + "Cannot request MESSAGE_SIGNED IRQ\n"); + + sprintf(desc, "Turris Omnia SN %016llX MCU ECDSA key", + mcu->board_serial_number); + + err = devm_turris_signing_key_create(dev, &omnia_signing_key_subtype, + desc); + if (err) + return dev_err_probe(dev, err, "Cannot create signing key\n"); + + return 0; +} diff --git a/drivers/platform/cznic/turris-omnia-mcu-sys-off-wakeup.c b/drivers/platform/cznic/turris-omnia-mcu-sys-off-wakeup.c new file mode 100644 index 000000000..0e8ab15b6 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-sys-off-wakeup.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU system off and RTC wakeup driver + * + * This is not a true RTC driver (in the sense that it does not provide a + * real-time clock), rather the MCU implements a wakeup from powered off state + * at a specified time relative to MCU boot, and we expose this feature via RTC + * alarm, so that it can be used via the rtcwake command, which is the standard + * Linux command for this. + * + * 2024 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "turris-omnia-mcu.h" + +static int omnia_get_uptime_wakeup(const struct i2c_client *client, u32 *uptime, + u32 *wakeup) +{ + __le32 reply[2]; + int err; + + err = omnia_cmd_read(client, OMNIA_CMD_GET_UPTIME_AND_WAKEUP, reply, + sizeof(reply)); + if (err) + return err; + + if (uptime) + *uptime = le32_to_cpu(reply[0]); + + if (wakeup) + *wakeup = le32_to_cpu(reply[1]); + + return 0; +} + +static int omnia_read_time(struct device *dev, struct rtc_time *tm) +{ + u32 uptime; + int err; + + err = omnia_get_uptime_wakeup(to_i2c_client(dev), &uptime, NULL); + if (err) + return err; + + rtc_time64_to_tm(uptime, tm); + + return 0; +} + +static int omnia_read_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ + struct i2c_client *client = to_i2c_client(dev); + struct omnia_mcu *mcu = i2c_get_clientdata(client); + u32 wakeup; + int err; + + err = omnia_get_uptime_wakeup(client, NULL, &wakeup); + if (err) + return err; + + alrm->enabled = !!wakeup; + rtc_time64_to_tm(wakeup ?: mcu->rtc_alarm, &alrm->time); + + return 0; +} + +static int omnia_set_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ + struct i2c_client *client = to_i2c_client(dev); + struct omnia_mcu *mcu = i2c_get_clientdata(client); + + mcu->rtc_alarm = rtc_tm_to_time64(&alrm->time); + + if (alrm->enabled) + return omnia_cmd_write_u32(client, OMNIA_CMD_SET_WAKEUP, + mcu->rtc_alarm); + + return 0; +} + +static int omnia_alarm_irq_enable(struct device *dev, unsigned int enabled) +{ + struct i2c_client *client = to_i2c_client(dev); + struct omnia_mcu *mcu = i2c_get_clientdata(client); + + return omnia_cmd_write_u32(client, OMNIA_CMD_SET_WAKEUP, + enabled ? mcu->rtc_alarm : 0); +} + +static const struct rtc_class_ops omnia_rtc_ops = { + .read_time = omnia_read_time, + .read_alarm = omnia_read_alarm, + .set_alarm = omnia_set_alarm, + .alarm_irq_enable = omnia_alarm_irq_enable, +}; + +static int omnia_power_off(struct sys_off_data *data) +{ + struct omnia_mcu *mcu = data->cb_data; + __be32 tmp; + u8 cmd[9]; + u16 arg; + int err; + + if (mcu->front_button_poweron) + arg = OMNIA_CMD_POWER_OFF_POWERON_BUTTON; + else + arg = 0; + + cmd[0] = OMNIA_CMD_POWER_OFF; + put_unaligned_le16(OMNIA_CMD_POWER_OFF_MAGIC, &cmd[1]); + put_unaligned_le16(arg, &cmd[3]); + + /* + * Although all values from and to MCU are passed in little-endian, the + * MCU's CRC unit uses big-endian CRC32 polynomial (0x04c11db7), so we + * need to use crc32_be() here. + */ + tmp = cpu_to_be32(get_unaligned_le32(&cmd[1])); + put_unaligned_le32(crc32_be(~0, (void *)&tmp, sizeof(tmp)), &cmd[5]); + + err = omnia_cmd_write(mcu->client, cmd, sizeof(cmd)); + if (err) + dev_err(&mcu->client->dev, + "Unable to send the poweroff command: %d\n", err); + + return NOTIFY_DONE; +} + +static int omnia_restart(struct sys_off_data *data) +{ + struct omnia_mcu *mcu = data->cb_data; + u8 cmd[3]; + int err; + + cmd[0] = OMNIA_CMD_GENERAL_CONTROL; + + if (reboot_mode == REBOOT_HARD) + cmd[1] = cmd[2] = OMNIA_CTL_HARD_RST; + else + cmd[1] = cmd[2] = OMNIA_CTL_LIGHT_RST; + + err = omnia_cmd_write(mcu->client, cmd, sizeof(cmd)); + if (err) + dev_err(&mcu->client->dev, + "Unable to send the restart command: %d\n", err); + + /* + * MCU needs a little bit to process the I2C command, otherwise it will + * do a light reset based on SOC SYSRES_OUT pin. + */ + mdelay(1); + + return NOTIFY_DONE; +} + +static ssize_t front_button_poweron_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", mcu->front_button_poweron); +} + +static ssize_t front_button_poweron_store(struct device *dev, + struct device_attribute *a, + const char *buf, size_t count) +{ + struct omnia_mcu *mcu = dev_get_drvdata(dev); + bool val; + int err; + + err = kstrtobool(buf, &val); + if (err) + return err; + + mcu->front_button_poweron = val; + + return count; +} +static DEVICE_ATTR_RW(front_button_poweron); + +static struct attribute *omnia_mcu_poweroff_attrs[] = { + &dev_attr_front_button_poweron.attr, + NULL +}; + +static umode_t poweroff_attrs_visible(struct kobject *kobj, struct attribute *a, + int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct omnia_mcu *mcu = dev_get_drvdata(dev); + + if (mcu->features & OMNIA_FEAT_POWEROFF_WAKEUP) + return a->mode; + + return 0; +} + +const struct attribute_group omnia_mcu_poweroff_group = { + .attrs = omnia_mcu_poweroff_attrs, + .is_visible = poweroff_attrs_visible, +}; + +int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu) +{ + struct device *dev = &mcu->client->dev; + int err; + + /* MCU restart is always available */ + err = devm_register_sys_off_handler(dev, SYS_OFF_MODE_RESTART, + SYS_OFF_PRIO_FIRMWARE, + omnia_restart, mcu); + if (err) + return dev_err_probe(dev, err, + "Cannot register system restart handler\n"); + + /* + * Poweroff and wakeup are available only if POWEROFF_WAKEUP feature is + * present. + */ + if (!(mcu->features & OMNIA_FEAT_POWEROFF_WAKEUP)) + return 0; + + err = devm_register_sys_off_handler(dev, SYS_OFF_MODE_POWER_OFF, + SYS_OFF_PRIO_FIRMWARE, + omnia_power_off, mcu); + if (err) + return dev_err_probe(dev, err, + "Cannot register system power off handler\n"); + + mcu->rtcdev = devm_rtc_allocate_device(dev); + if (IS_ERR(mcu->rtcdev)) + return dev_err_probe(dev, PTR_ERR(mcu->rtcdev), + "Cannot allocate RTC device\n"); + + mcu->rtcdev->ops = &omnia_rtc_ops; + mcu->rtcdev->range_max = U32_MAX; + set_bit(RTC_FEATURE_ALARM_WAKEUP_ONLY, mcu->rtcdev->features); + + err = devm_rtc_register_device(mcu->rtcdev); + if (err) + return dev_err_probe(dev, err, "Cannot register RTC device\n"); + + mcu->front_button_poweron = true; + + return 0; +} diff --git a/drivers/platform/cznic/turris-omnia-mcu-trng.c b/drivers/platform/cznic/turris-omnia-mcu-trng.c new file mode 100644 index 000000000..e3826959e --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-trng.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU TRNG driver + * + * 2024 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "turris-omnia-mcu.h" + +#define OMNIA_CMD_TRNG_MAX_ENTROPY_LEN 64 + +static irqreturn_t omnia_trng_irq_handler(int irq, void *dev_id) +{ + struct omnia_mcu *mcu = dev_id; + + complete(&mcu->trng_entropy_ready); + + return IRQ_HANDLED; +} + +static int omnia_trng_read(struct hwrng *rng, void *data, size_t max, bool wait) +{ + struct omnia_mcu *mcu = container_of(rng, struct omnia_mcu, trng); + u8 reply[1 + OMNIA_CMD_TRNG_MAX_ENTROPY_LEN]; + int err, bytes; + + if (!wait && !completion_done(&mcu->trng_entropy_ready)) + return 0; + + do { + if (wait_for_completion_interruptible(&mcu->trng_entropy_ready)) + return -ERESTARTSYS; + + err = omnia_cmd_read(mcu->client, + OMNIA_CMD_TRNG_COLLECT_ENTROPY, + reply, sizeof(reply)); + if (err) + return err; + + bytes = min3(reply[0], max, OMNIA_CMD_TRNG_MAX_ENTROPY_LEN); + } while (wait && !bytes); + + memcpy(data, &reply[1], bytes); + + return bytes; +} + +int omnia_mcu_register_trng(struct omnia_mcu *mcu) +{ + struct device *dev = &mcu->client->dev; + u8 dummy; + int err; + + if (!(mcu->features & OMNIA_FEAT_TRNG)) + return 0; + + /* + * If someone else cleared the TRNG interrupt but did not read the + * entropy, a new interrupt won't be generated, and entropy collection + * will be stuck. Ensure an interrupt will be generated by executing + * the collect entropy command (and discarding the result). + */ + err = omnia_cmd_read(mcu->client, OMNIA_CMD_TRNG_COLLECT_ENTROPY, + &dummy, 1); + if (err) + return err; + + init_completion(&mcu->trng_entropy_ready); + + err = omnia_mcu_request_irq(mcu, OMNIA_INT_TRNG, omnia_trng_irq_handler, + "turris-omnia-mcu-trng"); + if (err) + return dev_err_probe(dev, err, "Cannot request TRNG IRQ\n"); + + mcu->trng.name = "turris-omnia-mcu-trng"; + mcu->trng.read = omnia_trng_read; + + err = devm_hwrng_register(dev, &mcu->trng); + if (err) + return dev_err_probe(dev, err, "Cannot register TRNG\n"); + + return 0; +} diff --git a/drivers/platform/cznic/turris-omnia-mcu-watchdog.c b/drivers/platform/cznic/turris-omnia-mcu-watchdog.c new file mode 100644 index 000000000..3ad146ec1 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-watchdog.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia MCU watchdog driver + * + * 2024 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "turris-omnia-mcu.h" + +#define WATCHDOG_TIMEOUT 120 + +static unsigned int timeout; +module_param(timeout, int, 0); +MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds"); + +static bool nowayout = WATCHDOG_NOWAYOUT; +module_param(nowayout, bool, 0); +MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" + __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); + +static int omnia_wdt_start(struct watchdog_device *wdt) +{ + struct omnia_mcu *mcu = watchdog_get_drvdata(wdt); + + return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 1); +} + +static int omnia_wdt_stop(struct watchdog_device *wdt) +{ + struct omnia_mcu *mcu = watchdog_get_drvdata(wdt); + + return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 0); +} + +static int omnia_wdt_ping(struct watchdog_device *wdt) +{ + struct omnia_mcu *mcu = watchdog_get_drvdata(wdt); + + return omnia_cmd_write_u8(mcu->client, OMNIA_CMD_SET_WATCHDOG_STATE, 1); +} + +static int omnia_wdt_set_timeout(struct watchdog_device *wdt, + unsigned int timeout) +{ + struct omnia_mcu *mcu = watchdog_get_drvdata(wdt); + + return omnia_cmd_write_u16(mcu->client, OMNIA_CMD_SET_WDT_TIMEOUT, + timeout * DECI); +} + +static unsigned int omnia_wdt_get_timeleft(struct watchdog_device *wdt) +{ + struct omnia_mcu *mcu = watchdog_get_drvdata(wdt); + u16 timeleft; + int err; + + err = omnia_cmd_read_u16(mcu->client, OMNIA_CMD_GET_WDT_TIMELEFT, + &timeleft); + if (err) { + dev_err(&mcu->client->dev, "Cannot get watchdog timeleft: %d\n", + err); + return 0; + } + + return timeleft / DECI; +} + +static const struct watchdog_info omnia_wdt_info = { + .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE, + .identity = "Turris Omnia MCU Watchdog", +}; + +static const struct watchdog_ops omnia_wdt_ops = { + .owner = THIS_MODULE, + .start = omnia_wdt_start, + .stop = omnia_wdt_stop, + .ping = omnia_wdt_ping, + .set_timeout = omnia_wdt_set_timeout, + .get_timeleft = omnia_wdt_get_timeleft, +}; + +int omnia_mcu_register_watchdog(struct omnia_mcu *mcu) +{ + struct device *dev = &mcu->client->dev; + u8 state; + int err; + + if (!(mcu->features & OMNIA_FEAT_WDT_PING)) + return 0; + + mcu->wdt.info = &omnia_wdt_info; + mcu->wdt.ops = &omnia_wdt_ops; + mcu->wdt.parent = dev; + mcu->wdt.min_timeout = 1; + mcu->wdt.max_timeout = 65535 / DECI; + + mcu->wdt.timeout = WATCHDOG_TIMEOUT; + watchdog_init_timeout(&mcu->wdt, timeout, dev); + + watchdog_set_drvdata(&mcu->wdt, mcu); + + omnia_wdt_set_timeout(&mcu->wdt, mcu->wdt.timeout); + + err = omnia_cmd_read_u8(mcu->client, OMNIA_CMD_GET_WATCHDOG_STATE, + &state); + if (err) + return dev_err_probe(dev, err, + "Cannot get MCU watchdog state\n"); + + if (state) + set_bit(WDOG_HW_RUNNING, &mcu->wdt.status); + + watchdog_set_nowayout(&mcu->wdt, nowayout); + watchdog_stop_on_reboot(&mcu->wdt); + err = devm_watchdog_register_device(dev, &mcu->wdt); + if (err) + return dev_err_probe(dev, err, + "Cannot register MCU watchdog\n"); + + return 0; +} diff --git a/drivers/platform/cznic/turris-omnia-mcu.h b/drivers/platform/cznic/turris-omnia-mcu.h new file mode 100644 index 000000000..8473a3031 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu.h @@ -0,0 +1,163 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * CZ.NIC's Turris Omnia MCU driver + * + * 2024 by Marek Behún + */ + +#ifndef __TURRIS_OMNIA_MCU_H +#define __TURRIS_OMNIA_MCU_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN = 1 + 32, + OMNIA_MCU_CRYPTO_SIGNATURE_LEN = 64, +}; + +struct i2c_client; +struct rtc_device; + +/** + * struct omnia_mcu - driver private data structure + * @client: I2C client + * @type: MCU type (STM32, GD32, MKL, or unknown) + * @features: bitmap of features supported by the MCU firmware + * @board_serial_number: board serial number, if stored in MCU + * @board_first_mac: board first MAC address, if stored in MCU + * @board_revision: board revision, if stored in MCU + * @gc: GPIO chip + * @lock: mutex to protect internal GPIO chip state + * @mask: bitmap of masked IRQs + * @rising: bitmap of rising edge IRQs + * @falling: bitmap of falling edge IRQs + * @both: bitmap of both edges IRQs + * @cached: bitmap of cached IRQ line values (when an IRQ line is configured for + * both edges, we cache the corresponding GPIO values in the IRQ + * handler) + * @is_cached: bitmap of which IRQ line values are cached + * @button_release_emul_work: front button release emulation work, used with old MCU firmware + * versions which did not send button release events, only button press + * events + * @last_status: cached value of the status word, to be compared with new value to + * determine which interrupt events occurred, used with old MCU + * firmware versions which only informed that the status word changed, + * but not which bits of the status word changed + * @button_pressed_emul: the front button is still emulated to be pressed + * @rtcdev: RTC device, does not actually count real-time, the device is only + * used for the RTC alarm mechanism, so that the board can be + * configured to wake up from poweroff state at a specific time + * @rtc_alarm: RTC alarm that was set for the board to wake up on, in MCU time + * (seconds since last MCU reset) + * @front_button_poweron: the front button should power on the device after it is powered off + * @wdt: watchdog driver structure + * @trng: RNG driver structure + * @trng_entropy_ready: RNG entropy ready completion + * @msg_signed: message signed completion + * @sign_lock: mutex to protect message signing state + * @sign_requested: flag indicating that message signing was requested but not completed + * @sign_err: message signing error number, filled in interrupt handler + * @signature: message signing signature, filled in interrupt handler + * @board_public_key: board public key, if stored in MCU + */ +struct omnia_mcu { + struct i2c_client *client; + const char *type; + u32 features; + + u64 board_serial_number; + u8 board_first_mac[ETH_ALEN]; + u8 board_revision; + +#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO + struct gpio_chip gc; + struct mutex lock; + unsigned long mask, rising, falling, both, cached, is_cached; + struct delayed_work button_release_emul_work; + unsigned long last_status; + bool button_pressed_emul; +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP + struct rtc_device *rtcdev; + u32 rtc_alarm; + bool front_button_poweron; +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_WATCHDOG + struct watchdog_device wdt; +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_TRNG + struct hwrng trng; + struct completion trng_entropy_ready; +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_KEYCTL + struct completion msg_signed; + struct mutex sign_lock; + bool sign_requested; + int sign_err; + u8 signature[OMNIA_MCU_CRYPTO_SIGNATURE_LEN]; + u8 board_public_key[OMNIA_MCU_CRYPTO_PUBLIC_KEY_LEN]; +#endif +}; + +#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO +extern const struct attribute_group omnia_mcu_gpio_group; +int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu); +int omnia_mcu_request_irq(struct omnia_mcu *mcu, u32 spec, + irq_handler_t thread_fn, const char *devname); +#else +static inline int omnia_mcu_register_gpiochip(struct omnia_mcu *mcu) +{ + return 0; +} +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_KEYCTL +int omnia_mcu_register_keyctl(struct omnia_mcu *mcu); +#else +static inline int omnia_mcu_register_keyctl(struct omnia_mcu *mcu) +{ + return 0; +} +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP +extern const struct attribute_group omnia_mcu_poweroff_group; +int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu); +#else +static inline int omnia_mcu_register_sys_off_and_wakeup(struct omnia_mcu *mcu) +{ + return 0; +} +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_TRNG +int omnia_mcu_register_trng(struct omnia_mcu *mcu); +#else +static inline int omnia_mcu_register_trng(struct omnia_mcu *mcu) +{ + return 0; +} +#endif + +#ifdef CONFIG_TURRIS_OMNIA_MCU_WATCHDOG +int omnia_mcu_register_watchdog(struct omnia_mcu *mcu); +#else +static inline int omnia_mcu_register_watchdog(struct omnia_mcu *mcu) +{ + return 0; +} +#endif + +#endif /* __TURRIS_OMNIA_MCU_H */ diff --git a/drivers/platform/cznic/turris-signing-key.c b/drivers/platform/cznic/turris-signing-key.c new file mode 100644 index 000000000..382717856 --- /dev/null +++ b/drivers/platform/cznic/turris-signing-key.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Some of CZ.NIC's Turris devices support signing messages with a per-device unique asymmetric + * cryptographic key that was burned into the device at manufacture. + * + * This helper module exposes this message signing ability via the keyctl() syscall. Upon load, it + * creates the `.turris-signing-keys` keyring. A device-specific driver then has to create a signing + * key by calling devm_turris_signing_key_create(). + * + * 2025 by Marek Behún + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static int turris_signing_key_instantiate(struct key *key, + struct key_preparsed_payload *payload) +{ + return 0; +} + +static void turris_signing_key_describe(const struct key *key, struct seq_file *m) +{ + const struct turris_signing_key_subtype *subtype = dereference_key_rcu(key); + + if (!subtype) + return; + + seq_printf(m, "%s: %*phN", key->description, subtype->public_key_size, + subtype->get_public_key(key)); +} + +static long turris_signing_key_read(const struct key *key, char *buffer, size_t buflen) +{ + const struct turris_signing_key_subtype *subtype = dereference_key_rcu(key); + + if (!subtype) + return -EIO; + + if (buffer) { + if (buflen > subtype->public_key_size) + buflen = subtype->public_key_size; + + memcpy(buffer, subtype->get_public_key(key), subtype->public_key_size); + } + + return subtype->public_key_size; +} + +static bool turris_signing_key_asym_valid_params(const struct turris_signing_key_subtype *subtype, + const struct kernel_pkey_params *params) +{ + if (params->encoding && strcmp(params->encoding, "raw")) + return false; + + if (params->hash_algo && strcmp(params->hash_algo, subtype->hash_algo)) + return false; + + return true; +} + +static int turris_signing_key_asym_query(const struct kernel_pkey_params *params, + struct kernel_pkey_query *info) +{ + const struct turris_signing_key_subtype *subtype = dereference_key_rcu(params->key); + + if (!subtype) + return -EIO; + + if (!turris_signing_key_asym_valid_params(subtype, params)) + return -EINVAL; + + info->supported_ops = KEYCTL_SUPPORTS_SIGN; + info->key_size = subtype->key_size; + info->max_data_size = subtype->data_size; + info->max_sig_size = subtype->sig_size; + info->max_enc_size = 0; + info->max_dec_size = 0; + + return 0; +} + +static int turris_signing_key_asym_eds_op(struct kernel_pkey_params *params, + const void *in, void *out) +{ + const struct turris_signing_key_subtype *subtype = dereference_key_rcu(params->key); + int err; + + if (!subtype) + return -EIO; + + if (!turris_signing_key_asym_valid_params(subtype, params)) + return -EINVAL; + + if (params->op != kernel_pkey_sign) + return -EOPNOTSUPP; + + if (params->in_len != subtype->data_size || params->out_len != subtype->sig_size) + return -EINVAL; + + err = subtype->sign(params->key, in, out); + if (err) + return err; + + return subtype->sig_size; +} + +static struct key_type turris_signing_key_type = { + .name = "turris-signing-key", + .instantiate = turris_signing_key_instantiate, + .describe = turris_signing_key_describe, + .read = turris_signing_key_read, + .asym_query = turris_signing_key_asym_query, + .asym_eds_op = turris_signing_key_asym_eds_op, +}; + +static struct key *turris_signing_keyring; + +static void turris_signing_key_release(void *key) +{ + key_unlink(turris_signing_keyring, key); + key_put(key); +} + +int +devm_turris_signing_key_create(struct device *dev, const struct turris_signing_key_subtype *subtype, + const char *desc) +{ + struct key *key; + key_ref_t kref; + + kref = key_create(make_key_ref(turris_signing_keyring, true), + turris_signing_key_type.name, desc, NULL, 0, + (KEY_POS_ALL & ~KEY_POS_SETATTR) | KEY_USR_VIEW | KEY_USR_READ | + KEY_USR_SEARCH, + KEY_ALLOC_BUILT_IN | KEY_ALLOC_SET_KEEP | KEY_ALLOC_NOT_IN_QUOTA); + if (IS_ERR(kref)) + return PTR_ERR(kref); + + key = key_ref_to_ptr(kref); + key->payload.data[1] = dev; + rcu_assign_keypointer(key, subtype); + + return devm_add_action_or_reset(dev, turris_signing_key_release, key); +} +EXPORT_SYMBOL_GPL(devm_turris_signing_key_create); + +static int turris_signing_key_init(void) +{ + int err; + + err = register_key_type(&turris_signing_key_type); + if (err) + return err; + + turris_signing_keyring = keyring_alloc(".turris-signing-keys", + GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, current_cred(), + (KEY_POS_ALL & ~KEY_POS_SETATTR) | KEY_USR_VIEW | + KEY_USR_READ | KEY_USR_SEARCH, + KEY_ALLOC_BUILT_IN | KEY_ALLOC_SET_KEEP | + KEY_ALLOC_NOT_IN_QUOTA, + NULL, NULL); + if (IS_ERR(turris_signing_keyring)) { + pr_err("Cannot allocate Turris keyring\n"); + + unregister_key_type(&turris_signing_key_type); + + return PTR_ERR(turris_signing_keyring); + } + + return 0; +} +module_init(turris_signing_key_init); + +static void turris_signing_key_exit(void) +{ + key_put(turris_signing_keyring); + unregister_key_type(&turris_signing_key_type); +} +module_exit(turris_signing_key_exit); + +MODULE_AUTHOR("Marek Behun "); +MODULE_DESCRIPTION("CZ.NIC's Turris signing key helper"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/goldfish/Kconfig b/drivers/platform/goldfish/Kconfig new file mode 100644 index 000000000..03ca5bf19 --- /dev/null +++ b/drivers/platform/goldfish/Kconfig @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig GOLDFISH + bool "Platform support for Goldfish virtual devices" + depends on HAS_IOMEM && HAS_DMA + default X86_GOLDFISH + help + Say Y here to get to see options for the Goldfish virtual platform. + This option alone does not add any kernel code. + + Unless you are building for the Android Goldfish emulator say N here. + +if GOLDFISH + +config GOLDFISH_PIPE + tristate "Goldfish virtual device for QEMU pipes" + help + This is a virtual device to drive the QEMU pipe interface used by + the Goldfish Android Virtual Device. + +endif # GOLDFISH diff --git a/drivers/platform/goldfish/Makefile b/drivers/platform/goldfish/Makefile new file mode 100644 index 000000000..76ba1d571 --- /dev/null +++ b/drivers/platform/goldfish/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for Goldfish platform specific drivers +# +obj-$(CONFIG_GOLDFISH_PIPE) += goldfish_pipe.o diff --git a/drivers/platform/goldfish/goldfish_pipe.c b/drivers/platform/goldfish/goldfish_pipe.c new file mode 100644 index 000000000..fa241ca8f --- /dev/null +++ b/drivers/platform/goldfish/goldfish_pipe.c @@ -0,0 +1,947 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2012 Intel, Inc. + * Copyright (C) 2013 Intel, Inc. + * Copyright (C) 2014 Linaro Limited + * Copyright (C) 2011-2016 Google, Inc. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/* This source file contains the implementation of a special device driver + * that intends to provide a *very* fast communication channel between the + * guest system and the QEMU emulator. + * + * Usage from the guest is simply the following (error handling simplified): + * + * int fd = open("/dev/qemu_pipe",O_RDWR); + * .... write() or read() through the pipe. + * + * This driver doesn't deal with the exact protocol used during the session. + * It is intended to be as simple as something like: + * + * // do this _just_ after opening the fd to connect to a specific + * // emulator service. + * const char* msg = ""; + * if (write(fd, msg, strlen(msg)+1) < 0) { + * ... could not connect to service + * close(fd); + * } + * + * // after this, simply read() and write() to communicate with the + * // service. Exact protocol details left as an exercise to the reader. + * + * This driver is very fast because it doesn't copy any data through + * intermediate buffers, since the emulator is capable of translating + * guest user addresses into host ones. + * + * Note that we must however ensure that each user page involved in the + * exchange is properly mapped during a transfer. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "goldfish_pipe_qemu.h" + +/* + * Update this when something changes in the driver's behavior so the host + * can benefit from knowing it + */ +enum { + PIPE_DRIVER_VERSION = 2, + PIPE_CURRENT_DEVICE_VERSION = 2 +}; + +enum { + MAX_BUFFERS_PER_COMMAND = 336, + MAX_SIGNALLED_PIPES = 64, + INITIAL_PIPES_CAPACITY = 64 +}; + +struct goldfish_pipe_dev; + +/* A per-pipe command structure, shared with the host */ +struct goldfish_pipe_command { + s32 cmd; /* PipeCmdCode, guest -> host */ + s32 id; /* pipe id, guest -> host */ + s32 status; /* command execution status, host -> guest */ + s32 reserved; /* to pad to 64-bit boundary */ + union { + /* Parameters for PIPE_CMD_{READ,WRITE} */ + struct { + /* number of buffers, guest -> host */ + u32 buffers_count; + /* number of consumed bytes, host -> guest */ + s32 consumed_size; + /* buffer pointers, guest -> host */ + u64 ptrs[MAX_BUFFERS_PER_COMMAND]; + /* buffer sizes, guest -> host */ + u32 sizes[MAX_BUFFERS_PER_COMMAND]; + } rw_params; + }; +}; + +/* A single signalled pipe information */ +struct signalled_pipe_buffer { + u32 id; + u32 flags; +}; + +/* Parameters for the PIPE_CMD_OPEN command */ +struct open_command_param { + u64 command_buffer_ptr; + u32 rw_params_max_count; +}; + +/* Device-level set of buffers shared with the host */ +struct goldfish_pipe_dev_buffers { + struct open_command_param open_command_params; + struct signalled_pipe_buffer + signalled_pipe_buffers[MAX_SIGNALLED_PIPES]; +}; + +/* This data type models a given pipe instance */ +struct goldfish_pipe { + /* pipe ID - index into goldfish_pipe_dev::pipes array */ + u32 id; + + /* The wake flags pipe is waiting for + * Note: not protected with any lock, uses atomic operations + * and barriers to make it thread-safe. + */ + unsigned long flags; + + /* wake flags host have signalled, + * - protected by goldfish_pipe_dev::lock + */ + unsigned long signalled_flags; + + /* A pointer to command buffer */ + struct goldfish_pipe_command *command_buffer; + + /* doubly linked list of signalled pipes, protected by + * goldfish_pipe_dev::lock + */ + struct goldfish_pipe *prev_signalled; + struct goldfish_pipe *next_signalled; + + /* + * A pipe's own lock. Protects the following: + * - *command_buffer - makes sure a command can safely write its + * parameters to the host and read the results back. + */ + struct mutex lock; + + /* A wake queue for sleeping until host signals an event */ + wait_queue_head_t wake_queue; + + /* Pointer to the parent goldfish_pipe_dev instance */ + struct goldfish_pipe_dev *dev; + + /* A buffer of pages, too large to fit into a stack frame */ + struct page *pages[MAX_BUFFERS_PER_COMMAND]; +}; + +/* The global driver data. Holds a reference to the i/o page used to + * communicate with the emulator, and a wake queue for blocked tasks + * waiting to be awoken. + */ +struct goldfish_pipe_dev { + /* A magic number to check if this is an instance of this struct */ + void *magic; + + /* + * Global device spinlock. Protects the following members: + * - pipes, pipes_capacity + * - [*pipes, *pipes + pipes_capacity) - array data + * - first_signalled_pipe, + * goldfish_pipe::prev_signalled, + * goldfish_pipe::next_signalled, + * goldfish_pipe::signalled_flags - all singnalled-related fields, + * in all allocated pipes + * - open_command_params - PIPE_CMD_OPEN-related buffers + * + * It looks like a lot of different fields, but the trick is that + * the only operation that happens often is the signalled pipes array + * manipulation. That's why it's OK for now to keep the rest of the + * fields under the same lock. If we notice too much contention because + * of PIPE_CMD_OPEN, then we should add a separate lock there. + */ + spinlock_t lock; + + /* + * Array of the pipes of |pipes_capacity| elements, + * indexed by goldfish_pipe::id + */ + struct goldfish_pipe **pipes; + u32 pipes_capacity; + + /* Pointers to the buffers host uses for interaction with this driver */ + struct goldfish_pipe_dev_buffers *buffers; + + /* Head of a doubly linked list of signalled pipes */ + struct goldfish_pipe *first_signalled_pipe; + + /* ptr to platform device's device struct */ + struct device *pdev_dev; + + /* Some device-specific data */ + int irq; + int version; + unsigned char __iomem *base; + + struct miscdevice miscdev; +}; + +static int goldfish_pipe_cmd_locked(struct goldfish_pipe *pipe, + enum PipeCmdCode cmd) +{ + pipe->command_buffer->cmd = cmd; + /* failure by default */ + pipe->command_buffer->status = PIPE_ERROR_INVAL; + writel(pipe->id, pipe->dev->base + PIPE_REG_CMD); + return pipe->command_buffer->status; +} + +static int goldfish_pipe_cmd(struct goldfish_pipe *pipe, enum PipeCmdCode cmd) +{ + int status; + + if (mutex_lock_interruptible(&pipe->lock)) + return PIPE_ERROR_IO; + status = goldfish_pipe_cmd_locked(pipe, cmd); + mutex_unlock(&pipe->lock); + return status; +} + +/* + * This function converts an error code returned by the emulator through + * the PIPE_REG_STATUS i/o register into a valid negative errno value. + */ +static int goldfish_pipe_error_convert(int status) +{ + switch (status) { + case PIPE_ERROR_AGAIN: + return -EAGAIN; + case PIPE_ERROR_NOMEM: + return -ENOMEM; + case PIPE_ERROR_IO: + return -EIO; + default: + return -EINVAL; + } +} + +static int goldfish_pin_pages(unsigned long first_page, + unsigned long last_page, + unsigned int last_page_size, + int is_write, + struct page *pages[MAX_BUFFERS_PER_COMMAND], + unsigned int *iter_last_page_size) +{ + int ret; + int requested_pages = ((last_page - first_page) >> PAGE_SHIFT) + 1; + + if (requested_pages > MAX_BUFFERS_PER_COMMAND) { + requested_pages = MAX_BUFFERS_PER_COMMAND; + *iter_last_page_size = PAGE_SIZE; + } else { + *iter_last_page_size = last_page_size; + } + + ret = pin_user_pages_fast(first_page, requested_pages, + !is_write ? FOLL_WRITE : 0, + pages); + if (ret <= 0) + return -EFAULT; + if (ret < requested_pages) + *iter_last_page_size = PAGE_SIZE; + + return ret; +} + +/* Populate the call parameters, merging adjacent pages together */ +static void populate_rw_params(struct page **pages, + int pages_count, + unsigned long address, + unsigned long address_end, + unsigned long first_page, + unsigned long last_page, + unsigned int iter_last_page_size, + int is_write, + struct goldfish_pipe_command *command) +{ + /* + * Process the first page separately - it's the only page that + * needs special handling for its start address. + */ + unsigned long xaddr = page_to_phys(pages[0]); + unsigned long xaddr_prev = xaddr; + int buffer_idx = 0; + int i = 1; + int size_on_page = first_page == last_page + ? (int)(address_end - address) + : (PAGE_SIZE - (address & ~PAGE_MASK)); + command->rw_params.ptrs[0] = (u64)(xaddr | (address & ~PAGE_MASK)); + command->rw_params.sizes[0] = size_on_page; + for (; i < pages_count; ++i) { + xaddr = page_to_phys(pages[i]); + size_on_page = (i == pages_count - 1) ? + iter_last_page_size : PAGE_SIZE; + if (xaddr == xaddr_prev + PAGE_SIZE) { + command->rw_params.sizes[buffer_idx] += size_on_page; + } else { + ++buffer_idx; + command->rw_params.ptrs[buffer_idx] = (u64)xaddr; + command->rw_params.sizes[buffer_idx] = size_on_page; + } + xaddr_prev = xaddr; + } + command->rw_params.buffers_count = buffer_idx + 1; +} + +static int transfer_max_buffers(struct goldfish_pipe *pipe, + unsigned long address, + unsigned long address_end, + int is_write, + unsigned long last_page, + unsigned int last_page_size, + s32 *consumed_size, + int *status) +{ + unsigned long first_page = address & PAGE_MASK; + unsigned int iter_last_page_size; + int pages_count; + + /* Serialize access to the pipe command buffers */ + if (mutex_lock_interruptible(&pipe->lock)) + return -ERESTARTSYS; + + pages_count = goldfish_pin_pages(first_page, last_page, + last_page_size, is_write, + pipe->pages, &iter_last_page_size); + if (pages_count < 0) { + mutex_unlock(&pipe->lock); + return pages_count; + } + + populate_rw_params(pipe->pages, pages_count, address, address_end, + first_page, last_page, iter_last_page_size, is_write, + pipe->command_buffer); + + /* Transfer the data */ + *status = goldfish_pipe_cmd_locked(pipe, + is_write ? PIPE_CMD_WRITE : PIPE_CMD_READ); + + *consumed_size = pipe->command_buffer->rw_params.consumed_size; + + unpin_user_pages_dirty_lock(pipe->pages, pages_count, + !is_write && *consumed_size > 0); + + mutex_unlock(&pipe->lock); + return 0; +} + +static int wait_for_host_signal(struct goldfish_pipe *pipe, int is_write) +{ + u32 wake_bit = is_write ? BIT_WAKE_ON_WRITE : BIT_WAKE_ON_READ; + + set_bit(wake_bit, &pipe->flags); + + /* Tell the emulator we're going to wait for a wake event */ + goldfish_pipe_cmd(pipe, + is_write ? PIPE_CMD_WAKE_ON_WRITE : PIPE_CMD_WAKE_ON_READ); + + while (test_bit(wake_bit, &pipe->flags)) { + if (wait_event_interruptible(pipe->wake_queue, + !test_bit(wake_bit, &pipe->flags))) + return -ERESTARTSYS; + + if (test_bit(BIT_CLOSED_ON_HOST, &pipe->flags)) + return -EIO; + } + + return 0; +} + +static ssize_t goldfish_pipe_read_write(struct file *filp, + char __user *buffer, + size_t bufflen, + int is_write) +{ + struct goldfish_pipe *pipe = filp->private_data; + int count = 0, ret = -EINVAL; + unsigned long address, address_end, last_page; + unsigned int last_page_size; + + /* If the emulator already closed the pipe, no need to go further */ + if (unlikely(test_bit(BIT_CLOSED_ON_HOST, &pipe->flags))) + return -EIO; + /* Null reads or writes succeeds */ + if (unlikely(bufflen == 0)) + return 0; + /* Check the buffer range for access */ + if (unlikely(!access_ok(buffer, bufflen))) + return -EFAULT; + + address = (unsigned long)buffer; + address_end = address + bufflen; + last_page = (address_end - 1) & PAGE_MASK; + last_page_size = ((address_end - 1) & ~PAGE_MASK) + 1; + + while (address < address_end) { + s32 consumed_size; + int status; + + ret = transfer_max_buffers(pipe, address, address_end, is_write, + last_page, last_page_size, + &consumed_size, &status); + if (ret < 0) + break; + + if (consumed_size > 0) { + /* No matter what's the status, we've transferred + * something. + */ + count += consumed_size; + address += consumed_size; + } + if (status > 0) + continue; + if (status == 0) { + /* EOF */ + ret = 0; + break; + } + if (count > 0) { + /* + * An error occurred, but we already transferred + * something on one of the previous iterations. + * Just return what we already copied and log this + * err. + */ + if (status != PIPE_ERROR_AGAIN) + dev_err_ratelimited(pipe->dev->pdev_dev, + "backend error %d on %s\n", + status, is_write ? "write" : "read"); + break; + } + + /* + * If the error is not PIPE_ERROR_AGAIN, or if we are in + * non-blocking mode, just return the error code. + */ + if (status != PIPE_ERROR_AGAIN || + (filp->f_flags & O_NONBLOCK) != 0) { + ret = goldfish_pipe_error_convert(status); + break; + } + + status = wait_for_host_signal(pipe, is_write); + if (status < 0) + return status; + } + + if (count > 0) + return count; + return ret; +} + +static ssize_t goldfish_pipe_read(struct file *filp, char __user *buffer, + size_t bufflen, loff_t *ppos) +{ + return goldfish_pipe_read_write(filp, buffer, bufflen, + /* is_write */ 0); +} + +static ssize_t goldfish_pipe_write(struct file *filp, + const char __user *buffer, size_t bufflen, + loff_t *ppos) +{ + /* cast away the const */ + char __user *no_const_buffer = (char __user *)buffer; + + return goldfish_pipe_read_write(filp, no_const_buffer, bufflen, + /* is_write */ 1); +} + +static __poll_t goldfish_pipe_poll(struct file *filp, poll_table *wait) +{ + struct goldfish_pipe *pipe = filp->private_data; + __poll_t mask = 0; + int status; + + poll_wait(filp, &pipe->wake_queue, wait); + + status = goldfish_pipe_cmd(pipe, PIPE_CMD_POLL); + if (status < 0) + return -ERESTARTSYS; + + if (status & PIPE_POLL_IN) + mask |= EPOLLIN | EPOLLRDNORM; + if (status & PIPE_POLL_OUT) + mask |= EPOLLOUT | EPOLLWRNORM; + if (status & PIPE_POLL_HUP) + mask |= EPOLLHUP; + if (test_bit(BIT_CLOSED_ON_HOST, &pipe->flags)) + mask |= EPOLLERR; + + return mask; +} + +static void signalled_pipes_add_locked(struct goldfish_pipe_dev *dev, + u32 id, u32 flags) +{ + struct goldfish_pipe *pipe; + + if (WARN_ON(id >= dev->pipes_capacity)) + return; + + pipe = dev->pipes[id]; + if (!pipe) + return; + pipe->signalled_flags |= flags; + + if (pipe->prev_signalled || pipe->next_signalled || + dev->first_signalled_pipe == pipe) + return; /* already in the list */ + pipe->next_signalled = dev->first_signalled_pipe; + if (dev->first_signalled_pipe) + dev->first_signalled_pipe->prev_signalled = pipe; + dev->first_signalled_pipe = pipe; +} + +static void signalled_pipes_remove_locked(struct goldfish_pipe_dev *dev, + struct goldfish_pipe *pipe) +{ + if (pipe->prev_signalled) + pipe->prev_signalled->next_signalled = pipe->next_signalled; + if (pipe->next_signalled) + pipe->next_signalled->prev_signalled = pipe->prev_signalled; + if (pipe == dev->first_signalled_pipe) + dev->first_signalled_pipe = pipe->next_signalled; + pipe->prev_signalled = NULL; + pipe->next_signalled = NULL; +} + +static struct goldfish_pipe *signalled_pipes_pop_front( + struct goldfish_pipe_dev *dev, int *wakes) +{ + struct goldfish_pipe *pipe; + unsigned long flags; + + spin_lock_irqsave(&dev->lock, flags); + + pipe = dev->first_signalled_pipe; + if (pipe) { + *wakes = pipe->signalled_flags; + pipe->signalled_flags = 0; + /* + * This is an optimized version of + * signalled_pipes_remove_locked() + * - We want to make it as fast as possible to + * wake the sleeping pipe operations faster. + */ + dev->first_signalled_pipe = pipe->next_signalled; + if (dev->first_signalled_pipe) + dev->first_signalled_pipe->prev_signalled = NULL; + pipe->next_signalled = NULL; + } + + spin_unlock_irqrestore(&dev->lock, flags); + return pipe; +} + +static irqreturn_t goldfish_interrupt_task(int irq, void *dev_addr) +{ + /* Iterate over the signalled pipes and wake them one by one */ + struct goldfish_pipe_dev *dev = dev_addr; + struct goldfish_pipe *pipe; + int wakes; + + while ((pipe = signalled_pipes_pop_front(dev, &wakes)) != NULL) { + if (wakes & PIPE_WAKE_CLOSED) { + pipe->flags = 1 << BIT_CLOSED_ON_HOST; + } else { + if (wakes & PIPE_WAKE_READ) + clear_bit(BIT_WAKE_ON_READ, &pipe->flags); + if (wakes & PIPE_WAKE_WRITE) + clear_bit(BIT_WAKE_ON_WRITE, &pipe->flags); + } + /* + * wake_up_interruptible() implies a write barrier, so don't + * explicitly add another one here. + */ + wake_up_interruptible(&pipe->wake_queue); + } + return IRQ_HANDLED; +} + +static void goldfish_pipe_device_deinit(struct platform_device *pdev, + struct goldfish_pipe_dev *dev); + +/* + * The general idea of the (threaded) interrupt handling: + * + * 1. device raises an interrupt if there's at least one signalled pipe + * 2. IRQ handler reads the signalled pipes and their count from the device + * 3. device writes them into a shared buffer and returns the count + * it only resets the IRQ if it has returned all signalled pipes, + * otherwise it leaves it raised, so IRQ handler will be called + * again for the next chunk + * 4. IRQ handler adds all returned pipes to the device's signalled pipes list + * 5. IRQ handler defers processing the signalled pipes from the list in a + * separate context + */ +static irqreturn_t goldfish_pipe_interrupt(int irq, void *dev_id) +{ + u32 count; + u32 i; + unsigned long flags; + struct goldfish_pipe_dev *dev = dev_id; + + if (dev->magic != &goldfish_pipe_device_deinit) + return IRQ_NONE; + + /* Request the signalled pipes from the device */ + spin_lock_irqsave(&dev->lock, flags); + + count = readl(dev->base + PIPE_REG_GET_SIGNALLED); + if (count == 0) { + spin_unlock_irqrestore(&dev->lock, flags); + return IRQ_NONE; + } + if (count > MAX_SIGNALLED_PIPES) + count = MAX_SIGNALLED_PIPES; + + for (i = 0; i < count; ++i) + signalled_pipes_add_locked(dev, + dev->buffers->signalled_pipe_buffers[i].id, + dev->buffers->signalled_pipe_buffers[i].flags); + + spin_unlock_irqrestore(&dev->lock, flags); + + return IRQ_WAKE_THREAD; +} + +static int get_free_pipe_id_locked(struct goldfish_pipe_dev *dev) +{ + int id; + + for (id = 0; id < dev->pipes_capacity; ++id) + if (!dev->pipes[id]) + return id; + + { + /* Reallocate the array. + * Since get_free_pipe_id_locked runs with interrupts disabled, + * we don't want to make calls that could lead to sleep. + */ + u32 new_capacity = 2 * dev->pipes_capacity; + struct goldfish_pipe **pipes = + kzalloc_objs(*pipes, new_capacity, GFP_ATOMIC); + if (!pipes) + return -ENOMEM; + memcpy(pipes, dev->pipes, sizeof(*pipes) * dev->pipes_capacity); + kfree(dev->pipes); + dev->pipes = pipes; + id = dev->pipes_capacity; + dev->pipes_capacity = new_capacity; + } + return id; +} + +/* A helper function to get the instance of goldfish_pipe_dev from file */ +static struct goldfish_pipe_dev *to_goldfish_pipe_dev(struct file *file) +{ + struct miscdevice *miscdev = file->private_data; + + return container_of(miscdev, struct goldfish_pipe_dev, miscdev); +} + +/** + * goldfish_pipe_open - open a channel to the AVD + * @inode: inode of device + * @file: file struct of opener + * + * Create a new pipe link between the emulator and the use application. + * Each new request produces a new pipe. + * + * Note: we use the pipe ID as a mux. All goldfish emulations are 32bit + * right now so this is fine. A move to 64bit will need this addressing + */ +static int goldfish_pipe_open(struct inode *inode, struct file *file) +{ + struct goldfish_pipe_dev *dev = to_goldfish_pipe_dev(file); + unsigned long flags; + int id; + int status; + + /* Allocate new pipe kernel object */ + struct goldfish_pipe *pipe = kzalloc_obj(*pipe); + + if (!pipe) + return -ENOMEM; + + pipe->dev = dev; + mutex_init(&pipe->lock); + init_waitqueue_head(&pipe->wake_queue); + + /* + * Command buffer needs to be allocated on its own page to make sure + * it is physically contiguous in host's address space. + */ + BUILD_BUG_ON(sizeof(struct goldfish_pipe_command) > PAGE_SIZE); + pipe->command_buffer = + (struct goldfish_pipe_command *)__get_free_page(GFP_KERNEL); + if (!pipe->command_buffer) { + status = -ENOMEM; + goto err_pipe; + } + + spin_lock_irqsave(&dev->lock, flags); + + id = get_free_pipe_id_locked(dev); + if (id < 0) { + status = id; + goto err_id_locked; + } + + dev->pipes[id] = pipe; + pipe->id = id; + pipe->command_buffer->id = id; + + /* Now tell the emulator we're opening a new pipe. */ + dev->buffers->open_command_params.rw_params_max_count = + MAX_BUFFERS_PER_COMMAND; + dev->buffers->open_command_params.command_buffer_ptr = + (u64)(unsigned long)__pa(pipe->command_buffer); + status = goldfish_pipe_cmd_locked(pipe, PIPE_CMD_OPEN); + spin_unlock_irqrestore(&dev->lock, flags); + if (status < 0) + goto err_cmd; + /* All is done, save the pipe into the file's private data field */ + file->private_data = pipe; + return 0; + +err_cmd: + spin_lock_irqsave(&dev->lock, flags); + dev->pipes[id] = NULL; +err_id_locked: + spin_unlock_irqrestore(&dev->lock, flags); + free_page((unsigned long)pipe->command_buffer); +err_pipe: + kfree(pipe); + return status; +} + +static int goldfish_pipe_release(struct inode *inode, struct file *filp) +{ + unsigned long flags; + struct goldfish_pipe *pipe = filp->private_data; + struct goldfish_pipe_dev *dev = pipe->dev; + + /* The guest is closing the channel, so tell the emulator right now */ + goldfish_pipe_cmd(pipe, PIPE_CMD_CLOSE); + + spin_lock_irqsave(&dev->lock, flags); + dev->pipes[pipe->id] = NULL; + signalled_pipes_remove_locked(dev, pipe); + spin_unlock_irqrestore(&dev->lock, flags); + + filp->private_data = NULL; + free_page((unsigned long)pipe->command_buffer); + kfree(pipe); + return 0; +} + +static const struct file_operations goldfish_pipe_fops = { + .owner = THIS_MODULE, + .read = goldfish_pipe_read, + .write = goldfish_pipe_write, + .poll = goldfish_pipe_poll, + .open = goldfish_pipe_open, + .release = goldfish_pipe_release, +}; + +static void init_miscdevice(struct miscdevice *miscdev) +{ + memset(miscdev, 0, sizeof(*miscdev)); + + miscdev->minor = MISC_DYNAMIC_MINOR; + miscdev->name = "goldfish_pipe"; + miscdev->fops = &goldfish_pipe_fops; +} + +static void write_pa_addr(void *addr, void __iomem *portl, void __iomem *porth) +{ + const unsigned long paddr = __pa(addr); + + writel(upper_32_bits(paddr), porth); + writel(lower_32_bits(paddr), portl); +} + +static int goldfish_pipe_device_init(struct platform_device *pdev, + struct goldfish_pipe_dev *dev) +{ + int err; + + err = devm_request_threaded_irq(&pdev->dev, dev->irq, + goldfish_pipe_interrupt, + goldfish_interrupt_task, + IRQF_SHARED, "goldfish_pipe", dev); + if (err) { + dev_err(&pdev->dev, "unable to allocate IRQ for v2\n"); + return err; + } + + init_miscdevice(&dev->miscdev); + err = misc_register(&dev->miscdev); + if (err) { + dev_err(&pdev->dev, "unable to register v2 device\n"); + return err; + } + + dev->pdev_dev = &pdev->dev; + dev->first_signalled_pipe = NULL; + dev->pipes_capacity = INITIAL_PIPES_CAPACITY; + dev->pipes = kzalloc_objs(*dev->pipes, dev->pipes_capacity); + if (!dev->pipes) { + misc_deregister(&dev->miscdev); + return -ENOMEM; + } + + /* + * We're going to pass two buffers, open_command_params and + * signalled_pipe_buffers, to the host. This means each of those buffers + * needs to be contained in a single physical page. The easiest choice + * is to just allocate a page and place the buffers in it. + */ + BUILD_BUG_ON(sizeof(struct goldfish_pipe_dev_buffers) > PAGE_SIZE); + dev->buffers = (struct goldfish_pipe_dev_buffers *) + __get_free_page(GFP_KERNEL); + if (!dev->buffers) { + kfree(dev->pipes); + misc_deregister(&dev->miscdev); + return -ENOMEM; + } + + /* Send the buffer addresses to the host */ + write_pa_addr(&dev->buffers->signalled_pipe_buffers, + dev->base + PIPE_REG_SIGNAL_BUFFER, + dev->base + PIPE_REG_SIGNAL_BUFFER_HIGH); + + writel(MAX_SIGNALLED_PIPES, + dev->base + PIPE_REG_SIGNAL_BUFFER_COUNT); + + write_pa_addr(&dev->buffers->open_command_params, + dev->base + PIPE_REG_OPEN_BUFFER, + dev->base + PIPE_REG_OPEN_BUFFER_HIGH); + + platform_set_drvdata(pdev, dev); + return 0; +} + +static void goldfish_pipe_device_deinit(struct platform_device *pdev, + struct goldfish_pipe_dev *dev) +{ + misc_deregister(&dev->miscdev); + kfree(dev->pipes); + free_page((unsigned long)dev->buffers); +} + +static int goldfish_pipe_probe(struct platform_device *pdev) +{ + struct resource *r; + struct goldfish_pipe_dev *dev; + + dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + dev->magic = &goldfish_pipe_device_deinit; + spin_lock_init(&dev->lock); + + r = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!r || resource_size(r) < PAGE_SIZE) { + dev_err(&pdev->dev, "can't allocate i/o page\n"); + return -EINVAL; + } + dev->base = devm_ioremap(&pdev->dev, r->start, PAGE_SIZE); + if (!dev->base) { + dev_err(&pdev->dev, "ioremap failed\n"); + return -EINVAL; + } + + dev->irq = platform_get_irq(pdev, 0); + if (dev->irq < 0) + return dev->irq; + + /* + * Exchange the versions with the host device + * + * Note: v1 driver used to not report its version, so we write it before + * reading device version back: this allows the host implementation to + * detect the old driver (if there was no version write before read). + */ + writel(PIPE_DRIVER_VERSION, dev->base + PIPE_REG_VERSION); + dev->version = readl(dev->base + PIPE_REG_VERSION); + if (WARN_ON(dev->version < PIPE_CURRENT_DEVICE_VERSION)) + return -EINVAL; + + return goldfish_pipe_device_init(pdev, dev); +} + +static void goldfish_pipe_remove(struct platform_device *pdev) +{ + struct goldfish_pipe_dev *dev = platform_get_drvdata(pdev); + + goldfish_pipe_device_deinit(pdev, dev); +} + +static const struct acpi_device_id goldfish_pipe_acpi_match[] = { + { "GFSH0003", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, goldfish_pipe_acpi_match); + +static const struct of_device_id goldfish_pipe_of_match[] = { + { .compatible = "google,android-pipe", }, + {}, +}; +MODULE_DEVICE_TABLE(of, goldfish_pipe_of_match); + +static struct platform_driver goldfish_pipe_driver = { + .probe = goldfish_pipe_probe, + .remove = goldfish_pipe_remove, + .driver = { + .name = "goldfish_pipe", + .of_match_table = goldfish_pipe_of_match, + .acpi_match_table = goldfish_pipe_acpi_match, + } +}; + +module_platform_driver(goldfish_pipe_driver); +MODULE_AUTHOR("David Turner "); +MODULE_DESCRIPTION("Goldfish virtual device for QEMU pipes"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/goldfish/goldfish_pipe_qemu.h b/drivers/platform/goldfish/goldfish_pipe_qemu.h new file mode 100644 index 000000000..b4d78c108 --- /dev/null +++ b/drivers/platform/goldfish/goldfish_pipe_qemu.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * IMPORTANT: The following constants must match the ones used and defined in + * external/qemu/include/hw/misc/goldfish_pipe.h + */ + +#ifndef GOLDFISH_PIPE_QEMU_H +#define GOLDFISH_PIPE_QEMU_H + +/* List of bitflags returned in status of CMD_POLL command */ +enum PipePollFlags { + PIPE_POLL_IN = 1 << 0, + PIPE_POLL_OUT = 1 << 1, + PIPE_POLL_HUP = 1 << 2 +}; + +/* Possible status values used to signal errors */ +enum PipeErrors { + PIPE_ERROR_INVAL = -1, + PIPE_ERROR_AGAIN = -2, + PIPE_ERROR_NOMEM = -3, + PIPE_ERROR_IO = -4 +}; + +/* Bit-flags used to signal events from the emulator */ +enum PipeWakeFlags { + /* emulator closed pipe */ + PIPE_WAKE_CLOSED = 1 << 0, + + /* pipe can now be read from */ + PIPE_WAKE_READ = 1 << 1, + + /* pipe can now be written to */ + PIPE_WAKE_WRITE = 1 << 2, + + /* unlock this pipe's DMA buffer */ + PIPE_WAKE_UNLOCK_DMA = 1 << 3, + + /* unlock DMA buffer of the pipe shared to this pipe */ + PIPE_WAKE_UNLOCK_DMA_SHARED = 1 << 4, +}; + +/* Possible pipe closing reasons */ +enum PipeCloseReason { + /* guest sent a close command */ + PIPE_CLOSE_GRACEFUL = 0, + + /* guest rebooted, we're closing the pipes */ + PIPE_CLOSE_REBOOT = 1, + + /* close old pipes on snapshot load */ + PIPE_CLOSE_LOAD_SNAPSHOT = 2, + + /* some unrecoverable error on the pipe */ + PIPE_CLOSE_ERROR = 3, +}; + +/* Bit flags for the 'flags' field */ +enum PipeFlagsBits { + BIT_CLOSED_ON_HOST = 0, /* pipe closed by host */ + BIT_WAKE_ON_WRITE = 1, /* want to be woken on writes */ + BIT_WAKE_ON_READ = 2, /* want to be woken on reads */ +}; + +enum PipeRegs { + PIPE_REG_CMD = 0, + + PIPE_REG_SIGNAL_BUFFER_HIGH = 4, + PIPE_REG_SIGNAL_BUFFER = 8, + PIPE_REG_SIGNAL_BUFFER_COUNT = 12, + + PIPE_REG_OPEN_BUFFER_HIGH = 20, + PIPE_REG_OPEN_BUFFER = 24, + + PIPE_REG_VERSION = 36, + + PIPE_REG_GET_SIGNALLED = 48, +}; + +enum PipeCmdCode { + /* to be used by the pipe device itself */ + PIPE_CMD_OPEN = 1, + + PIPE_CMD_CLOSE, + PIPE_CMD_POLL, + PIPE_CMD_WRITE, + PIPE_CMD_WAKE_ON_WRITE, + PIPE_CMD_READ, + PIPE_CMD_WAKE_ON_READ, + + /* + * TODO(zyy): implement a deferred read/write execution to allow + * parallel processing of pipe operations on the host. + */ + PIPE_CMD_WAKE_ON_DONE_IO, +}; + +#endif /* GOLDFISH_PIPE_QEMU_H */ diff --git a/drivers/platform/loongarch/Kconfig b/drivers/platform/loongarch/Kconfig new file mode 100644 index 000000000..447528797 --- /dev/null +++ b/drivers/platform/loongarch/Kconfig @@ -0,0 +1,31 @@ +# +# LoongArch Platform Specific Drivers +# + +menuconfig LOONGARCH_PLATFORM_DEVICES + bool "LoongArch Platform Specific Device Drivers" + default y + depends on LOONGARCH + help + Say Y here to get to see options for device drivers of various + LoongArch platforms, including vendor-specific laptop/desktop + extension and hardware monitor drivers. This option itself does + not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if LOONGARCH_PLATFORM_DEVICES + +config LOONGSON_LAPTOP + tristate "Generic Loongson-3 Laptop Driver" + depends on ACPI_EC + depends on BACKLIGHT_CLASS_DEVICE + depends on INPUT + depends on MACH_LOONGSON64 + select ACPI_VIDEO + select INPUT_SPARSEKMAP + default y + help + ACPI-based Loongson-3 family laptops generic driver. + +endif # LOONGARCH_PLATFORM_DEVICES diff --git a/drivers/platform/loongarch/Makefile b/drivers/platform/loongarch/Makefile new file mode 100644 index 000000000..f43ab03db --- /dev/null +++ b/drivers/platform/loongarch/Makefile @@ -0,0 +1 @@ +obj-$(CONFIG_LOONGSON_LAPTOP) += loongson-laptop.o diff --git a/drivers/platform/loongarch/loongson-laptop.c b/drivers/platform/loongarch/loongson-laptop.c new file mode 100644 index 000000000..4ec56621d --- /dev/null +++ b/drivers/platform/loongarch/loongson-laptop.c @@ -0,0 +1,633 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Generic Loongson processor based LAPTOP/ALL-IN-ONE driver + * + * Jianmin Lv + * Huacai Chen + * + * Copyright (C) 2022 Loongson Technology Corporation Limited + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 1. Driver-wide structs and misc. variables */ + +/* ACPI HIDs */ +#define LOONGSON_ACPI_EC_HID "PNP0C09" +#define LOONGSON_ACPI_HKEY_HID "LOON0000" + +#define ACPI_LAPTOP_NAME "loongson-laptop" + +#define MAX_ACPI_ARGS 3 +#define GENERIC_HOTKEY_MAP_MAX 64 + +#define GENERIC_EVENT_TYPE_OFF 12 +#define GENERIC_EVENT_TYPE_MASK 0xF000 +#define GENERIC_EVENT_CODE_MASK 0x0FFF + +struct generic_sub_driver { + u32 type; + char *name; + acpi_handle *handle; + struct acpi_device *device; + struct platform_driver *driver; + int (*init)(struct generic_sub_driver *sub_driver); + void (*notify)(struct generic_sub_driver *sub_driver, u32 event); + u8 acpi_notify_installed; +}; + +static u32 input_device_registered; +static struct input_dev *generic_inputdev; + +static acpi_handle hotkey_handle; +static struct key_entry hotkey_keycode_map[GENERIC_HOTKEY_MAP_MAX]; + +static bool bl_powered; +static int loongson_laptop_backlight_update(struct backlight_device *bd); + +/* 2. ACPI Helpers and device model */ + +static int acpi_evalf(acpi_handle handle, int *res, char *method, char *fmt, ...) +{ + char res_type; + char *fmt0 = fmt; + va_list ap; + int success, quiet; + acpi_status status; + struct acpi_object_list params; + struct acpi_buffer result, *resultp; + union acpi_object in_objs[MAX_ACPI_ARGS], out_obj; + + if (!*fmt) { + pr_err("acpi_evalf() called with empty format\n"); + return 0; + } + + if (*fmt == 'q') { + quiet = 1; + fmt++; + } else + quiet = 0; + + res_type = *(fmt++); + + params.count = 0; + params.pointer = &in_objs[0]; + + va_start(ap, fmt); + while (*fmt) { + char c = *(fmt++); + switch (c) { + case 'd': /* int */ + in_objs[params.count].integer.value = va_arg(ap, int); + in_objs[params.count++].type = ACPI_TYPE_INTEGER; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", c); + va_end(ap); + return 0; + } + } + va_end(ap); + + if (res_type != 'v') { + result.length = sizeof(out_obj); + result.pointer = &out_obj; + resultp = &result; + } else + resultp = NULL; + + status = acpi_evaluate_object(handle, method, ¶ms, resultp); + + switch (res_type) { + case 'd': /* int */ + success = (status == AE_OK && out_obj.type == ACPI_TYPE_INTEGER); + if (success && res) + *res = out_obj.integer.value; + break; + case 'v': /* void */ + success = status == AE_OK; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", res_type); + return 0; + } + + if (!success && !quiet) + pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", + method, fmt0, acpi_format_exception(status)); + + return success; +} + +static int hotkey_status_get(int *status) +{ + if (!acpi_evalf(hotkey_handle, status, "GSWS", "d")) + return -EIO; + + return 0; +} + +static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct generic_sub_driver *sub_driver = data; + + if (!sub_driver || !sub_driver->notify) + return; + sub_driver->notify(sub_driver, event); +} + +static int __init setup_acpi_notify(struct generic_sub_driver *sub_driver) +{ + acpi_status status; + + if (!*sub_driver->handle) + return 0; + + sub_driver->device = acpi_fetch_acpi_dev(*sub_driver->handle); + if (!sub_driver->device) { + pr_err("acpi_fetch_acpi_dev(%s) failed\n", sub_driver->name); + return -ENODEV; + } + + sub_driver->device->driver_data = sub_driver; + + status = acpi_install_notify_handler(*sub_driver->handle, + sub_driver->type, dispatch_acpi_notify, sub_driver); + if (ACPI_FAILURE(status)) { + if (status == AE_ALREADY_EXISTS) { + pr_notice("Another device driver is already " + "handling %s events\n", sub_driver->name); + } else { + pr_err("acpi_install_notify_handler(%s) failed: %s\n", + sub_driver->name, acpi_format_exception(status)); + } + return -ENODEV; + } + sub_driver->acpi_notify_installed = 1; + + return 0; +} + +static int loongson_hotkey_suspend(struct device *dev) +{ + bl_powered = false; + return 0; +} + +static int loongson_hotkey_resume(struct device *dev) +{ + int status = 0; + struct key_entry ke; + struct backlight_device *bd; + + bd = backlight_device_get_by_type(BACKLIGHT_PLATFORM); + if (bd) { + loongson_laptop_backlight_update(bd) ? + pr_warn("Loongson_backlight: resume brightness failed") : + pr_info("Loongson_backlight: resume brightness %d\n", bd->props.brightness); + } + + /* + * Only if the firmware supports SW_LID event model, we can handle the + * event. This is for the consideration of development board without EC. + */ + if (test_bit(SW_LID, generic_inputdev->swbit)) { + if (hotkey_status_get(&status) < 0) + return -EIO; + /* + * The input device sw element records the last lid status. + * When the system is awakened by other wake-up sources, + * the lid event will also be reported. The judgment of + * adding SW_LID bit which in sw element can avoid this + * case. + * + * Input system will drop lid event when current lid event + * value and last lid status in the same. So laptop driver + * doesn't report repeated events. + * + * Lid status is generally 0, but hardware exception is + * considered. So add lid status confirmation. + */ + if (test_bit(SW_LID, generic_inputdev->sw) && !(status & (1 << SW_LID))) { + ke.type = KE_SW; + ke.sw.value = (u8)status; + ke.sw.code = SW_LID; + sparse_keymap_report_entry(generic_inputdev, &ke, 1, true); + } + } + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(loongson_hotkey_pm, + loongson_hotkey_suspend, loongson_hotkey_resume); + +static int loongson_hotkey_probe(struct platform_device *pdev) +{ + hotkey_handle = ACPI_HANDLE(&pdev->dev); + + if (!hotkey_handle) + return -ENODEV; + + return 0; +} + +static const struct acpi_device_id loongson_device_ids[] = { + {LOONGSON_ACPI_HKEY_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, loongson_device_ids); + +static struct platform_driver loongson_hotkey_driver = { + .probe = loongson_hotkey_probe, + .driver = { + .name = "loongson-hotkey", + .owner = THIS_MODULE, + .pm = pm_ptr(&loongson_hotkey_pm), + .acpi_match_table = loongson_device_ids, + }, +}; + +static int hotkey_map(void) +{ + u32 index; + acpi_status status; + struct acpi_buffer buf; + union acpi_object *pack; + + buf.length = ACPI_ALLOCATE_BUFFER; + status = acpi_evaluate_object_typed(hotkey_handle, "KMAP", NULL, &buf, ACPI_TYPE_PACKAGE); + if (status != AE_OK) { + pr_err("ACPI exception: %s\n", acpi_format_exception(status)); + return -1; + } + pack = buf.pointer; + for (index = 0; index < pack->package.count; index++) { + union acpi_object *element, *sub_pack; + + sub_pack = &pack->package.elements[index]; + + element = &sub_pack->package.elements[0]; + hotkey_keycode_map[index].type = element->integer.value; + element = &sub_pack->package.elements[1]; + hotkey_keycode_map[index].code = element->integer.value; + element = &sub_pack->package.elements[2]; + hotkey_keycode_map[index].keycode = element->integer.value; + } + + return 0; +} + +static int hotkey_backlight_set(bool enable) +{ + if (!acpi_evalf(hotkey_handle, NULL, "VCBL", "vd", enable ? 1 : 0)) + return -EIO; + + return 0; +} + +static int ec_get_brightness(void) +{ + int status = 0; + + if (!hotkey_handle) + return -ENXIO; + + if (!acpi_evalf(hotkey_handle, &status, "ECBG", "d")) + return -EIO; + + return status; +} + +static int ec_set_brightness(int level) +{ + + int ret = 0; + + if (!hotkey_handle) + return -ENXIO; + + if (!acpi_evalf(hotkey_handle, NULL, "ECBS", "vd", level)) + ret = -EIO; + + return ret; +} + +static int ec_backlight_level(u8 level) +{ + int status = 0; + + if (!hotkey_handle) + return -ENXIO; + + if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d")) + return -EIO; + + if ((status < 0) || (level > status)) + return status; + + if (!acpi_evalf(hotkey_handle, &status, "ECSL", "d")) + return -EIO; + + if ((status < 0) || (level < status)) + return status; + + return level; +} + +static int ec_backlight_set_power(bool state) +{ + int status; + union acpi_object arg0 = { ACPI_TYPE_INTEGER }; + struct acpi_object_list args = { 1, &arg0 }; + + arg0.integer.value = state; + status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL); + if (ACPI_FAILURE(status)) { + pr_info("Loongson lvds error: 0x%x\n", status); + return -EIO; + } + + return 0; +} + +static int loongson_laptop_backlight_update(struct backlight_device *bd) +{ + bool target_powered = !backlight_is_blank(bd); + int ret = 0, lvl = ec_backlight_level(bd->props.brightness); + + if (lvl < 0) + return -EIO; + + if (ec_set_brightness(lvl)) + return -EIO; + + if (target_powered != bl_powered) { + ret = ec_backlight_set_power(target_powered); + if (ret < 0) + return ret; + + bl_powered = target_powered; + } + + return ret; +} + +static int loongson_laptop_get_brightness(struct backlight_device *bd) +{ + int level; + + level = ec_get_brightness(); + if (level < 0) + return -EIO; + + return level; +} + +static const struct backlight_ops backlight_laptop_ops = { + .update_status = loongson_laptop_backlight_update, + .get_brightness = loongson_laptop_get_brightness, +}; + +static int laptop_backlight_register(void) +{ + int status = 0, ret; + struct backlight_properties props; + + memset(&props, 0, sizeof(props)); + + if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d")) + return -EIO; + + ret = ec_backlight_set_power(true); + if (ret) + return ret; + + bl_powered = true; + + props.max_brightness = status; + props.brightness = ec_get_brightness(); + props.power = BACKLIGHT_POWER_ON; + props.type = BACKLIGHT_PLATFORM; + + backlight_device_register("loongson_laptop", + NULL, NULL, &backlight_laptop_ops, &props); + + + return 0; +} + +static int __init event_init(struct generic_sub_driver *sub_driver) +{ + int ret; + + ret = hotkey_map(); + if (ret < 0) { + pr_err("Failed to parse keymap from DSDT\n"); + return ret; + } + + ret = sparse_keymap_setup(generic_inputdev, hotkey_keycode_map, NULL); + if (ret < 0) { + pr_err("Failed to setup input device keymap\n"); + input_free_device(generic_inputdev); + generic_inputdev = NULL; + + return ret; + } + + /* + * This hotkey driver handle backlight event when + * acpi_video_get_backlight_type() gets acpi_backlight_vendor + */ + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) + hotkey_backlight_set(true); + else + hotkey_backlight_set(false); + + pr_info("ACPI: enabling firmware HKEY event interface...\n"); + + return ret; +} + +static void event_notify(struct generic_sub_driver *sub_driver, u32 event) +{ + int type, scan_code; + struct key_entry *ke = NULL; + + scan_code = event & GENERIC_EVENT_CODE_MASK; + type = (event & GENERIC_EVENT_TYPE_MASK) >> GENERIC_EVENT_TYPE_OFF; + ke = sparse_keymap_entry_from_scancode(generic_inputdev, scan_code); + if (ke) { + if (type == KE_SW) { + int status = 0; + + if (hotkey_status_get(&status) < 0) + return; + + ke->sw.value = !!(status & (1 << ke->sw.code)); + } + sparse_keymap_report_entry(generic_inputdev, ke, 1, true); + } +} + +/* 3. Infrastructure */ + +static void generic_subdriver_exit(struct generic_sub_driver *sub_driver); + +static int __init generic_subdriver_init(struct generic_sub_driver *sub_driver) +{ + int ret; + + if (!sub_driver || !sub_driver->driver) + return -EINVAL; + + ret = platform_driver_register(sub_driver->driver); + if (ret) + return -EINVAL; + + if (sub_driver->init) { + ret = sub_driver->init(sub_driver); + if (ret) + goto err_out; + } + + if (sub_driver->notify) { + ret = setup_acpi_notify(sub_driver); + if (ret == -ENODEV) { + ret = 0; + goto err_out; + } + if (ret < 0) + goto err_out; + } + + return 0; + +err_out: + generic_subdriver_exit(sub_driver); + return ret; +} + +static void generic_subdriver_exit(struct generic_sub_driver *sub_driver) +{ + + if (sub_driver->acpi_notify_installed) { + acpi_remove_notify_handler(*sub_driver->handle, + sub_driver->type, dispatch_acpi_notify); + sub_driver->acpi_notify_installed = 0; + } + platform_driver_unregister(sub_driver->driver); +} + +static struct generic_sub_driver generic_sub_drivers[] __refdata = { + { + .name = "hotkey", + .init = event_init, + .notify = event_notify, + .handle = &hotkey_handle, + .type = ACPI_DEVICE_NOTIFY, + .driver = &loongson_hotkey_driver, + }, +}; + +static int __init generic_acpi_laptop_init(void) +{ + bool ec_found; + int i, ret, status; + + if (acpi_disabled) + return -ENODEV; + + /* The EC device is required */ + ec_found = acpi_dev_found(LOONGSON_ACPI_EC_HID); + if (!ec_found) + return -ENODEV; + + /* Enable SCI for EC */ + acpi_write_bit_register(ACPI_BITREG_SCI_ENABLE, 1); + + generic_inputdev = input_allocate_device(); + if (!generic_inputdev) { + pr_err("Unable to allocate input device\n"); + return -ENOMEM; + } + + /* Prepare input device, but don't register */ + generic_inputdev->name = + "Loongson Generic Laptop/All-in-One Extra Buttons"; + generic_inputdev->phys = ACPI_LAPTOP_NAME "/input0"; + generic_inputdev->id.bustype = BUS_HOST; + generic_inputdev->dev.parent = NULL; + + /* Init subdrivers */ + for (i = 0; i < ARRAY_SIZE(generic_sub_drivers); i++) { + ret = generic_subdriver_init(&generic_sub_drivers[i]); + if (ret < 0) { + input_free_device(generic_inputdev); + while (--i >= 0) + generic_subdriver_exit(&generic_sub_drivers[i]); + return ret; + } + } + + ret = input_register_device(generic_inputdev); + if (ret < 0) { + input_free_device(generic_inputdev); + while (--i >= 0) + generic_subdriver_exit(&generic_sub_drivers[i]); + pr_err("Unable to register input device\n"); + return ret; + } + + input_device_registered = 1; + + if (acpi_evalf(hotkey_handle, &status, "ECBG", "d")) { + pr_info("Loongson Laptop used, init brightness is 0x%x\n", status); + ret = laptop_backlight_register(); + if (ret < 0) + pr_err("Loongson Laptop: laptop-backlight device register failed\n"); + } + + return 0; +} + +static void __exit generic_acpi_laptop_exit(void) +{ + int i; + + if (generic_inputdev) { + if (!input_device_registered) { + input_free_device(generic_inputdev); + } else { + input_unregister_device(generic_inputdev); + + for (i = 0; i < ARRAY_SIZE(generic_sub_drivers); i++) + generic_subdriver_exit(&generic_sub_drivers[i]); + } + } +} + +module_init(generic_acpi_laptop_init); +module_exit(generic_acpi_laptop_exit); + +MODULE_AUTHOR("Jianmin Lv "); +MODULE_AUTHOR("Huacai Chen "); +MODULE_DESCRIPTION("Loongson Laptop/All-in-One ACPI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/mellanox/Kconfig b/drivers/platform/mellanox/Kconfig new file mode 100644 index 000000000..e3afbe62c --- /dev/null +++ b/drivers/platform/mellanox/Kconfig @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Platform support for Mellanox hardware +# + +menuconfig MELLANOX_PLATFORM + bool "Platform support for Mellanox hardware" + depends on X86 || ARM || ARM64 || COMPILE_TEST + help + Say Y here to get to see options for platform support for + Mellanox systems. This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if MELLANOX_PLATFORM + +config MLX_PLATFORM + tristate "Mellanox Technologies platform support" + depends on ACPI && I2C && PCI + select REGMAP + help + This option enables system support for the Mellanox Technologies + platform. The Mellanox systems provide data center networking + solutions based on Virtual Protocol Interconnect (VPI) technology + enable seamless connectivity to 56/100Gb/s InfiniBand or 10/40/56GbE + connection. + + If you have a Mellanox system, say Y or M here. + +config MLXREG_DPU + tristate "Nvidia Data Processor Unit platform driver support" + depends on I2C + select REGMAP_I2C + help + This driver provides support for the Nvidia BF3 Data Processor Units, + which are the part of SN4280 Ethernet smart switch systems + providing a high performance switching solution for Enterprise Data + Centers (EDC) for building Ethernet based clusters, High-Performance + Computing (HPC) and embedded environments. + + If you have a Nvidia smart switch system, say Y or M here. + +config MLXREG_HOTPLUG + tristate "Mellanox platform hotplug driver support" + depends on HWMON + depends on I2C + select REGMAP + help + This driver handles hot-plug events for the power suppliers, power + cables and fans on the wide range Mellanox IB and Ethernet systems. + +config MLXREG_IO + tristate "Mellanox platform register access driver support" + depends on HWMON + select REGMAP + help + This driver allows access to Mellanox programmable device register + space through sysfs interface. The sets of registers for sysfs access + are defined per system type bases and include the registers related + to system resets operation, system reset causes monitoring and some + kinds of mux selection. + +config MLXREG_LC + tristate "Mellanox line card platform driver support" + depends on HWMON + depends on I2C + select REGMAP + help + This driver provides support for the Mellanox MSN4800-XX line cards, + which are the part of MSN4800 Ethernet modular switch systems + providing a high performance switching solution for Enterprise Data + Centers (EDC) for building Ethernet based clusters, High-Performance + Computing (HPC) and embedded environments. + +config MLXBF_TMFIFO + tristate "Mellanox BlueField SoC TmFifo platform driver" + depends on ARM64 + depends on ACPI + depends on VIRTIO_CONSOLE && VIRTIO_NET + help + Say y here to enable TmFifo support. The TmFifo driver provides + platform driver support for the TmFifo which supports console + and networking based on the virtio framework. + +config MLXBF_BOOTCTL + tristate "Mellanox BlueField Firmware Boot Control driver" + depends on ARM64 + depends on ACPI + depends on NET + help + The Mellanox BlueField firmware implements functionality to + request swapping the primary and alternate eMMC boot partition, + and to set up a watchdog that can undo that swap if the system + does not boot up correctly. This driver provides sysfs access + to the userspace tools, to be used in conjunction with the eMMC + device driver to do necessary initial swap of the boot partition. + +config MLXBF_PMC + tristate "Mellanox BlueField Performance Monitoring Counters driver" + depends on ARM64 + depends on HWMON + depends on ACPI + help + Say y here to enable PMC support. The PMC driver provides access + to performance monitoring counters within various blocks in the + Mellanox BlueField SoC via a sysfs interface. + +config NVSW_SN2201 + tristate "Nvidia SN2201 platform driver support" + depends on HWMON && I2C + depends on ACPI || COMPILE_TEST + select REGMAP_I2C + help + This driver provides support for the Nvidia SN2201 platform. + The SN2201 is a highly integrated for one rack unit system with + L3 management switches. It has 48 x 1Gbps RJ45 + 4 x 100G QSFP28 + ports in a compact 1RU form factor. The system also including a + serial port (RS-232 interface), an OOB port (1G/100M MDI interface) + and USB ports for management functions. + The processor used on SN2201 is Intel Atom®Processor C Series, + C3338R which is one of the Denverton product families. + System equipped with Nvidia®Spectrum-1 32x100GbE Ethernet switch. + +endif # MELLANOX_PLATFORM diff --git a/drivers/platform/mellanox/Makefile b/drivers/platform/mellanox/Makefile new file mode 100644 index 000000000..e86723b44 --- /dev/null +++ b/drivers/platform/mellanox/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/mellanox +# Mellanox Platform-Specific Drivers +# +obj-$(CONFIG_MLX_PLATFORM) += mlx-platform.o +obj-$(CONFIG_MLXBF_BOOTCTL) += mlxbf-bootctl.o +obj-$(CONFIG_MLXBF_PMC) += mlxbf-pmc.o +obj-$(CONFIG_MLXBF_TMFIFO) += mlxbf-tmfifo.o +obj-$(CONFIG_MLXREG_DPU) += mlxreg-dpu.o +obj-$(CONFIG_MLXREG_HOTPLUG) += mlxreg-hotplug.o +obj-$(CONFIG_MLXREG_IO) += mlxreg-io.o +obj-$(CONFIG_MLXREG_LC) += mlxreg-lc.o +obj-$(CONFIG_NVSW_SN2201) += nvsw-sn2201.o diff --git a/drivers/platform/mellanox/mlx-platform.c b/drivers/platform/mellanox/mlx-platform.c new file mode 100644 index 000000000..efcba68d3 --- /dev/null +++ b/drivers/platform/mellanox/mlx-platform.c @@ -0,0 +1,8661 @@ +// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0 +/* + * Mellanox platform driver + * + * Copyright (C) 2016-2018 Mellanox Technologies + * Copyright (C) 2016-2018 Vadim Pasternak + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MLX_PLAT_DEVICE_NAME "mlxplat" + +/* LPC bus IO offsets */ +#define MLXPLAT_CPLD_LPC_I2C_BASE_ADRR 0x2000 +#define MLXPLAT_CPLD_LPC_REG_BASE_ADRR 0x2500 +#define MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET 0x00 +#define MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET 0x01 +#define MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET 0x02 +#define MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET 0x03 +#define MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET 0x04 +#define MLXPLAT_CPLD_LPC_REG_CPLD1_PN1_OFFSET 0x05 +#define MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET 0x06 +#define MLXPLAT_CPLD_LPC_REG_CPLD2_PN1_OFFSET 0x07 +#define MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET 0x08 +#define MLXPLAT_CPLD_LPC_REG_CPLD3_PN1_OFFSET 0x09 +#define MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET 0x0a +#define MLXPLAT_CPLD_LPC_REG_CPLD4_PN1_OFFSET 0x0b +#define MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET 0x17 +#define MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET 0x19 +#define MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET 0x1b +#define MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET 0x1c +#define MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET 0x1d +#define MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET 0x1e +#define MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET 0x1f +#define MLXPLAT_CPLD_LPC_REG_LED1_OFFSET 0x20 +#define MLXPLAT_CPLD_LPC_REG_LED2_OFFSET 0x21 +#define MLXPLAT_CPLD_LPC_REG_LED3_OFFSET 0x22 +#define MLXPLAT_CPLD_LPC_REG_LED4_OFFSET 0x23 +#define MLXPLAT_CPLD_LPC_REG_LED5_OFFSET 0x24 +#define MLXPLAT_CPLD_LPC_REG_LED6_OFFSET 0x25 +#define MLXPLAT_CPLD_LPC_REG_LED7_OFFSET 0x26 +#define MLXPLAT_CPLD_LPC_REG_LED8_OFFSET 0x27 +#define MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION 0x2a +#define MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET 0x2b +#define MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET 0x2d +#define MLXPLAT_CPLD_LPC_REG_GP1_RO_OFFSET 0x2c +#define MLXPLAT_CPLD_LPC_REG_GP0_OFFSET 0x2e +#define MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET 0x2f +#define MLXPLAT_CPLD_LPC_REG_GP1_OFFSET 0x30 +#define MLXPLAT_CPLD_LPC_REG_WP1_OFFSET 0x31 +#define MLXPLAT_CPLD_LPC_REG_GP2_OFFSET 0x32 +#define MLXPLAT_CPLD_LPC_REG_WP2_OFFSET 0x33 +#define MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE 0x34 +#define MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET 0x35 +#define MLXPLAT_CPLD_LPC_SAFE_BIOS_WP_OFFSET 0x36 +#define MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET 0x37 +#define MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET 0x3a +#define MLXPLAT_CPLD_LPC_REG_AGGR_MASK_OFFSET 0x3b +#define MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET 0x3c +#define MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET 0x40 +#define MLXPLAT_CPLD_LPC_REG_AGGRLO_MASK_OFFSET 0x41 +#define MLXPLAT_CPLD_LPC_REG_AGGRCO_OFFSET 0x42 +#define MLXPLAT_CPLD_LPC_REG_AGGRCO_MASK_OFFSET 0x43 +#define MLXPLAT_CPLD_LPC_REG_AGGRCX_OFFSET 0x44 +#define MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET 0x45 +#define MLXPLAT_CPLD_LPC_REG_GP3_OFFSET 0x46 +#define MLXPLAT_CPLD_LPC_REG_BRD_OFFSET 0x47 +#define MLXPLAT_CPLD_LPC_REG_BRD_EVENT_OFFSET 0x48 +#define MLXPLAT_CPLD_LPC_REG_BRD_MASK_OFFSET 0x49 +#define MLXPLAT_CPLD_LPC_REG_GWP_OFFSET 0x4a +#define MLXPLAT_CPLD_LPC_REG_GWP_EVENT_OFFSET 0x4b +#define MLXPLAT_CPLD_LPC_REG_GWP_MASK_OFFSET 0x4c +#define MLXPLAT_CPLD_LPC_REG_GPI_MASK_OFFSET 0x4e +#define MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET 0x50 +#define MLXPLAT_CPLD_LPC_REG_ASIC_EVENT_OFFSET 0x51 +#define MLXPLAT_CPLD_LPC_REG_ASIC_MASK_OFFSET 0x52 +#define MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET 0x53 +#define MLXPLAT_CPLD_LPC_REG_ASIC2_EVENT_OFFSET 0x54 +#define MLXPLAT_CPLD_LPC_REG_ASIC2_MASK_OFFSET 0x55 +#define MLXPLAT_CPLD_LPC_REG_AGGRLC_OFFSET 0x56 +#define MLXPLAT_CPLD_LPC_REG_AGGRLC_MASK_OFFSET 0x57 +#define MLXPLAT_CPLD_LPC_REG_PSU_OFFSET 0x58 +#define MLXPLAT_CPLD_LPC_REG_PSU_EVENT_OFFSET 0x59 +#define MLXPLAT_CPLD_LPC_REG_PSU_MASK_OFFSET 0x5a +#define MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET 0x5e +#define MLXPLAT_CPLD_LPC_REG_PWR_OFFSET 0x64 +#define MLXPLAT_CPLD_LPC_REG_PWR_EVENT_OFFSET 0x65 +#define MLXPLAT_CPLD_LPC_REG_PWR_MASK_OFFSET 0x66 +#define MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET 0x6a +#define MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET 0x70 +#define MLXPLAT_CPLD_LPC_REG_LC_IN_EVENT_OFFSET 0x71 +#define MLXPLAT_CPLD_LPC_REG_LC_IN_MASK_OFFSET 0x72 +#define MLXPLAT_CPLD_LPC_REG_FAN_OFFSET 0x88 +#define MLXPLAT_CPLD_LPC_REG_FAN_EVENT_OFFSET 0x89 +#define MLXPLAT_CPLD_LPC_REG_FAN_MASK_OFFSET 0x8a +#define MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET 0x8b +#define MLXPLAT_CPLD_LPC_REG_FAN2_EVENT_OFFSET 0x8c +#define MLXPLAT_CPLD_LPC_REG_FAN2_MASK_OFFSET 0x8d +#define MLXPLAT_CPLD_LPC_REG_CPLD5_VER_OFFSET 0x8e +#define MLXPLAT_CPLD_LPC_REG_CPLD5_PN_OFFSET 0x8f +#define MLXPLAT_CPLD_LPC_REG_CPLD5_PN1_OFFSET 0x90 +#define MLXPLAT_CPLD_LPC_REG_EROT_OFFSET 0x91 +#define MLXPLAT_CPLD_LPC_REG_EROT_EVENT_OFFSET 0x92 +#define MLXPLAT_CPLD_LPC_REG_EROT_MASK_OFFSET 0x93 +#define MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET 0x94 +#define MLXPLAT_CPLD_LPC_REG_EROTE_EVENT_OFFSET 0x95 +#define MLXPLAT_CPLD_LPC_REG_EROTE_MASK_OFFSET 0x96 +#define MLXPLAT_CPLD_LPC_REG_PWRB_OFFSET 0x97 +#define MLXPLAT_CPLD_LPC_REG_PWRB_EVENT_OFFSET 0x98 +#define MLXPLAT_CPLD_LPC_REG_PWRB_MASK_OFFSET 0x99 +#define MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET 0x9a +#define MLXPLAT_CPLD_LPC_REG_LC_VR_EVENT_OFFSET 0x9b +#define MLXPLAT_CPLD_LPC_REG_LC_VR_MASK_OFFSET 0x9c +#define MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET 0x9d +#define MLXPLAT_CPLD_LPC_REG_LC_PG_EVENT_OFFSET 0x9e +#define MLXPLAT_CPLD_LPC_REG_LC_PG_MASK_OFFSET 0x9f +#define MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET 0xa0 +#define MLXPLAT_CPLD_LPC_REG_LC_RD_EVENT_OFFSET 0xa1 +#define MLXPLAT_CPLD_LPC_REG_LC_RD_MASK_OFFSET 0xa2 +#define MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET 0xa3 +#define MLXPLAT_CPLD_LPC_REG_LC_SN_EVENT_OFFSET 0xa4 +#define MLXPLAT_CPLD_LPC_REG_LC_SN_MASK_OFFSET 0xa5 +#define MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET 0xa6 +#define MLXPLAT_CPLD_LPC_REG_LC_OK_EVENT_OFFSET 0xa7 +#define MLXPLAT_CPLD_LPC_REG_LC_OK_MASK_OFFSET 0xa8 +#define MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET 0xa9 +#define MLXPLAT_CPLD_LPC_REG_LC_SD_EVENT_OFFSET 0xaa +#define MLXPLAT_CPLD_LPC_REG_LC_SD_MASK_OFFSET 0xab +#define MLXPLAT_CPLD_LPC_REG_LC_PWR_ON 0xb2 +#define MLXPLAT_CPLD_LPC_REG_TACHO19_OFFSET 0xb4 +#define MLXPLAT_CPLD_LPC_REG_TACHO20_OFFSET 0xb5 +#define MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET 0xb6 +#define MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET 0xb7 +#define MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET 0xb8 +#define MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET 0xb9 +#define MLXPLAT_CPLD_LPC_REG_TACHO17_OFFSET 0xba +#define MLXPLAT_CPLD_LPC_REG_TACHO18_OFFSET 0xbb +#define MLXPLAT_CPLD_LPC_REG_ASIC_CAP_OFFSET 0xc1 +#define MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET 0xc2 +#define MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT 0xc3 +#define MLXPLAT_CPLD_LPC_REG_CPLD5_MVER_OFFSET 0xc4 +#define MLXPLAT_CPLD_LPC_REG_WD_CLEAR_OFFSET 0xc7 +#define MLXPLAT_CPLD_LPC_REG_WD_CLEAR_WP_OFFSET 0xc8 +#define MLXPLAT_CPLD_LPC_REG_WD1_TMR_OFFSET 0xc9 +#define MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET 0xcb +#define MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET 0xcd +#define MLXPLAT_CPLD_LPC_REG_WD2_TLEFT_OFFSET 0xce +#define MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET 0xcf +#define MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET 0xd1 +#define MLXPLAT_CPLD_LPC_REG_WD3_TLEFT_OFFSET 0xd2 +#define MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET 0xd3 +#define MLXPLAT_CPLD_LPC_REG_CPLD6_MVER_OFFSET 0xd9 +#define MLXPLAT_CPLD_LPC_REG_I2C_CH1_OFFSET 0xdb +#define MLXPLAT_CPLD_LPC_REG_I2C_CH2_OFFSET 0xda +#define MLXPLAT_CPLD_LPC_REG_I2C_CH3_OFFSET 0xdc +#define MLXPLAT_CPLD_LPC_REG_I2C_CH4_OFFSET 0xdd +#define MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET 0xde +#define MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET 0xdf +#define MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET 0xe0 +#define MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET 0xe1 +#define MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET 0xe2 +#define MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET 0xe3 +#define MLXPLAT_CPLD_LPC_REG_TACHO1_OFFSET 0xe4 +#define MLXPLAT_CPLD_LPC_REG_TACHO2_OFFSET 0xe5 +#define MLXPLAT_CPLD_LPC_REG_TACHO3_OFFSET 0xe6 +#define MLXPLAT_CPLD_LPC_REG_TACHO4_OFFSET 0xe7 +#define MLXPLAT_CPLD_LPC_REG_TACHO5_OFFSET 0xe8 +#define MLXPLAT_CPLD_LPC_REG_TACHO6_OFFSET 0xe9 +#define MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET 0xea +#define MLXPLAT_CPLD_LPC_REG_TACHO7_OFFSET 0xeb +#define MLXPLAT_CPLD_LPC_REG_TACHO8_OFFSET 0xec +#define MLXPLAT_CPLD_LPC_REG_TACHO9_OFFSET 0xed +#define MLXPLAT_CPLD_LPC_REG_TACHO10_OFFSET 0xee +#define MLXPLAT_CPLD_LPC_REG_TACHO11_OFFSET 0xef +#define MLXPLAT_CPLD_LPC_REG_TACHO12_OFFSET 0xf0 +#define MLXPLAT_CPLD_LPC_REG_TACHO13_OFFSET 0xf1 +#define MLXPLAT_CPLD_LPC_REG_TACHO14_OFFSET 0xf2 +#define MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET 0xf3 +#define MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET 0xf4 +#define MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET 0xf5 +#define MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET 0xf6 +#define MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET 0xf7 +#define MLXPLAT_CPLD_LPC_REG_TACHO_SPEED_OFFSET 0xf8 +#define MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET 0xf9 +#define MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET 0xfa +#define MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET 0xfb +#define MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET 0xfc +#define MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET 0xfd +#define MLXPLAT_CPLD_LPC_REG_TACHO15_OFFSET 0xfe +#define MLXPLAT_CPLD_LPC_REG_TACHO16_OFFSET 0xff + +#define MLXPLAT_CPLD_LPC_IO_RANGE 0x100 + +#define MLXPLAT_CPLD_LPC_PIO_OFFSET 0x10000UL +#define MLXPLAT_CPLD_LPC_REG1 ((MLXPLAT_CPLD_LPC_REG_BASE_ADRR + \ + MLXPLAT_CPLD_LPC_REG_I2C_CH1_OFFSET) | \ + MLXPLAT_CPLD_LPC_PIO_OFFSET) +#define MLXPLAT_CPLD_LPC_REG2 ((MLXPLAT_CPLD_LPC_REG_BASE_ADRR + \ + MLXPLAT_CPLD_LPC_REG_I2C_CH2_OFFSET) | \ + MLXPLAT_CPLD_LPC_PIO_OFFSET) +#define MLXPLAT_CPLD_LPC_REG3 ((MLXPLAT_CPLD_LPC_REG_BASE_ADRR + \ + MLXPLAT_CPLD_LPC_REG_I2C_CH3_OFFSET) | \ + MLXPLAT_CPLD_LPC_PIO_OFFSET) +#define MLXPLAT_CPLD_LPC_REG4 ((MLXPLAT_CPLD_LPC_REG_BASE_ADRR + \ + MLXPLAT_CPLD_LPC_REG_I2C_CH4_OFFSET) | \ + MLXPLAT_CPLD_LPC_PIO_OFFSET) + +/* Masks for aggregation, psu, pwr and fan event in CPLD related registers. */ +#define MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF 0x04 +#define MLXPLAT_CPLD_AGGR_PSU_MASK_DEF 0x08 +#define MLXPLAT_CPLD_AGGR_PWR_MASK_DEF 0x08 +#define MLXPLAT_CPLD_AGGR_FAN_MASK_DEF 0x40 +#define MLXPLAT_CPLD_AGGR_MASK_DEF (MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF | \ + MLXPLAT_CPLD_AGGR_PSU_MASK_DEF | \ + MLXPLAT_CPLD_AGGR_FAN_MASK_DEF) +#define MLXPLAT_CPLD_AGGR_ASIC_MASK_NG 0x01 +#define MLXPLAT_CPLD_AGGR_MASK_NG_DEF 0x04 +#define MLXPLAT_CPLD_AGGR_MASK_COMEX BIT(0) +#define MLXPLAT_CPLD_AGGR_MASK_LC BIT(3) +#define MLXPLAT_CPLD_AGGR_MASK_DPU_BRD BIT(4) +#define MLXPLAT_CPLD_AGGR_MASK_DPU_CORE BIT(5) +#define MLXPLAT_CPLD_AGGR_MASK_MODULAR (MLXPLAT_CPLD_AGGR_MASK_NG_DEF | \ + MLXPLAT_CPLD_AGGR_MASK_COMEX | \ + MLXPLAT_CPLD_AGGR_MASK_LC) +#define MLXPLAT_CPLD_AGGR_MASK_SMART_SW (MLXPLAT_CPLD_AGGR_MASK_COMEX | \ + MLXPLAT_CPLD_AGGR_MASK_NG_DEF | \ + MLXPLAT_CPLD_AGGR_MASK_DPU_BRD | \ + MLXPLAT_CPLD_AGGR_MASK_DPU_CORE) +#define MLXPLAT_CPLD_AGGR_MASK_LC_PRSNT BIT(0) +#define MLXPLAT_CPLD_AGGR_MASK_LC_RDY BIT(1) +#define MLXPLAT_CPLD_AGGR_MASK_LC_PG BIT(2) +#define MLXPLAT_CPLD_AGGR_MASK_LC_SCRD BIT(3) +#define MLXPLAT_CPLD_AGGR_MASK_LC_SYNC BIT(4) +#define MLXPLAT_CPLD_AGGR_MASK_LC_ACT BIT(5) +#define MLXPLAT_CPLD_AGGR_MASK_LC_SDWN BIT(6) +#define MLXPLAT_CPLD_AGGR_MASK_LC_LOW (MLXPLAT_CPLD_AGGR_MASK_LC_PRSNT | \ + MLXPLAT_CPLD_AGGR_MASK_LC_RDY | \ + MLXPLAT_CPLD_AGGR_MASK_LC_PG | \ + MLXPLAT_CPLD_AGGR_MASK_LC_SCRD | \ + MLXPLAT_CPLD_AGGR_MASK_LC_SYNC | \ + MLXPLAT_CPLD_AGGR_MASK_LC_ACT | \ + MLXPLAT_CPLD_AGGR_MASK_LC_SDWN) +#define MLXPLAT_CPLD_LOW_AGGR_MASK_LOW 0xc1 +#define MLXPLAT_CPLD_LOW_AGGR_MASK_ASIC2 BIT(2) +#define MLXPLAT_CPLD_LOW_AGGR_MASK_PWR_BUT GENMASK(5, 4) +#define MLXPLAT_CPLD_LOW_AGGR_MASK_I2C BIT(6) +#define MLXPLAT_CPLD_PSU_MASK GENMASK(1, 0) +#define MLXPLAT_CPLD_PWR_MASK GENMASK(1, 0) +#define MLXPLAT_CPLD_PSU_EXT_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_PWR_EXT_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_PSU_XDR_MASK GENMASK(7, 0) +#define MLXPLAT_CPLD_PWR_XDR_MASK GENMASK(7, 0) +#define MLXPLAT_CPLD_FAN_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_ASIC_MASK GENMASK(1, 0) +#define MLXPLAT_CPLD_ASIC_XDR_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_FAN_NG_MASK GENMASK(6, 0) +#define MLXPLAT_CPLD_FAN_XDR_MASK GENMASK(7, 0) +#define MLXPLAT_CPLD_LED_LO_NIBBLE_MASK GENMASK(7, 4) +#define MLXPLAT_CPLD_LED_HI_NIBBLE_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_VOLTREG_UPD_MASK GENMASK(5, 4) +#define MLXPLAT_CPLD_GWP_MASK GENMASK(0, 0) +#define MLXPLAT_CPLD_EROT_MASK GENMASK(1, 0) +#define MLXPLAT_CPLD_FU_CAP_MASK GENMASK(1, 0) +#define MLXPLAT_CPLD_BIOS_STATUS_MASK GENMASK(3, 1) +#define MLXPLAT_CPLD_DPU_MASK GENMASK(3, 0) +#define MLXPLAT_CPLD_PWR_BUTTON_MASK BIT(0) +#define MLXPLAT_CPLD_LATCH_RST_MASK BIT(6) +#define MLXPLAT_CPLD_THERMAL1_PDB_MASK BIT(3) +#define MLXPLAT_CPLD_THERMAL2_PDB_MASK BIT(4) +#define MLXPLAT_CPLD_INTRUSION_MASK BIT(6) +#define MLXPLAT_CPLD_PWM_PG_MASK BIT(7) +#define MLXPLAT_CPLD_L1_CHA_HEALTH_MASK (MLXPLAT_CPLD_THERMAL1_PDB_MASK | \ + MLXPLAT_CPLD_THERMAL2_PDB_MASK | \ + MLXPLAT_CPLD_INTRUSION_MASK |\ + MLXPLAT_CPLD_PWM_PG_MASK) +#define MLXPLAT_CPLD_I2C_CAP_BIT 0x04 +#define MLXPLAT_CPLD_I2C_CAP_MASK GENMASK(5, MLXPLAT_CPLD_I2C_CAP_BIT) +#define MLXPLAT_CPLD_SYS_RESET_MASK BIT(0) + +/* Masks for aggregation for comex carriers */ +#define MLXPLAT_CPLD_AGGR_MASK_CARRIER BIT(1) +#define MLXPLAT_CPLD_AGGR_MASK_CARR_DEF (MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF | \ + MLXPLAT_CPLD_AGGR_MASK_CARRIER) +#define MLXPLAT_CPLD_LOW_AGGRCX_MASK 0xc1 + +/* Masks for aggregation for modular systems */ +#define MLXPLAT_CPLD_LPC_LC_MASK GENMASK(7, 0) + +/* Masks for aggregation for smart switch systems */ +#define MLXPLAT_CPLD_LPC_SM_SW_MASK GENMASK(7, 0) + +#define MLXPLAT_CPLD_HALT_MASK BIT(3) +#define MLXPLAT_CPLD_RESET_MASK GENMASK(7, 1) + +/* Default I2C parent bus number */ +#define MLXPLAT_CPLD_PHYS_ADAPTER_DEF_NR 1 + +/* Maximum number of possible physical buses equipped on system */ +#define MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM 16 +#define MLXPLAT_CPLD_MAX_PHYS_EXT_ADAPTER_NUM 24 + +/* Number of channels in group */ +#define MLXPLAT_CPLD_GRP_CHNL_NUM 8 + +/* Start channel numbers */ +#define MLXPLAT_CPLD_CH1 2 +#define MLXPLAT_CPLD_CH2 10 +#define MLXPLAT_CPLD_CH3 18 +#define MLXPLAT_CPLD_CH2_ETH_MODULAR 3 +#define MLXPLAT_CPLD_CH3_ETH_MODULAR 43 +#define MLXPLAT_CPLD_CH4_ETH_MODULAR 51 +#define MLXPLAT_CPLD_CH2_RACK_SWITCH 18 +#define MLXPLAT_CPLD_CH2_NG800 34 + +/* Number of LPC attached MUX platform devices */ +#define MLXPLAT_CPLD_LPC_MUX_DEVS 4 + +/* Hotplug devices adapter numbers */ +#define MLXPLAT_CPLD_NR_NONE -1 +#define MLXPLAT_CPLD_PSU_DEFAULT_NR 10 +#define MLXPLAT_CPLD_PSU_MSNXXXX_NR 4 +#define MLXPLAT_CPLD_PSU_XDR_NR 3 +#define MLXPLAT_CPLD_FAN1_DEFAULT_NR 11 +#define MLXPLAT_CPLD_FAN2_DEFAULT_NR 12 +#define MLXPLAT_CPLD_FAN3_DEFAULT_NR 13 +#define MLXPLAT_CPLD_FAN4_DEFAULT_NR 14 +#define MLXPLAT_CPLD_NR_ASIC 3 +#define MLXPLAT_CPLD_NR_LC_BASE 34 +#define MLXPLAT_CPLD_NR_DPU_BASE 18 + +#define MLXPLAT_CPLD_NR_LC_SET(nr) (MLXPLAT_CPLD_NR_LC_BASE + (nr)) +#define MLXPLAT_CPLD_LC_ADDR 0x32 +#define MLXPLAT_CPLD_DPU_ADDR 0x68 + +/* Masks and default values for watchdogs */ +#define MLXPLAT_CPLD_WD1_CLEAR_MASK GENMASK(7, 1) +#define MLXPLAT_CPLD_WD2_CLEAR_MASK (GENMASK(7, 0) & ~BIT(1)) + +#define MLXPLAT_CPLD_WD_TYPE1_TO_MASK GENMASK(7, 4) +#define MLXPLAT_CPLD_WD_TYPE2_TO_MASK 0 +#define MLXPLAT_CPLD_WD_RESET_ACT_MASK GENMASK(7, 1) +#define MLXPLAT_CPLD_WD_FAN_ACT_MASK (GENMASK(7, 0) & ~BIT(4)) +#define MLXPLAT_CPLD_WD_COUNT_ACT_MASK (GENMASK(7, 0) & ~BIT(7)) +#define MLXPLAT_CPLD_WD_CPBLTY_MASK (GENMASK(7, 0) & ~BIT(6)) +#define MLXPLAT_CPLD_WD_DFLT_TIMEOUT 30 +#define MLXPLAT_CPLD_WD3_DFLT_TIMEOUT 600 +#define MLXPLAT_CPLD_WD_MAX_DEVS 2 +#define MLXPLAT_CPLD_DPU_MAX_DEVS 4 + +#define MLXPLAT_CPLD_LPC_SYSIRQ 17 + +/* Minimum power required for turning on Ethernet modular system (WATT) */ +#define MLXPLAT_CPLD_ETH_MODULAR_PWR_MIN 50 + +/* Default value for PWM control register for rack switch system */ +#define MLXPLAT_REGMAP_NVSWITCH_PWM_DEFAULT 0xf4 + +#define MLXPLAT_I2C_MAIN_BUS_NOTIFIED 0x01 +#define MLXPLAT_I2C_MAIN_BUS_HANDLE_CREATED 0x02 + +/* Lattice FPGA PCI configuration */ +#define PCI_VENDOR_ID_LATTICE 0x1204 +#define PCI_DEVICE_ID_LATTICE_I2C_BRIDGE 0x9c2f +#define PCI_DEVICE_ID_LATTICE_JTAG_BRIDGE 0x9c30 +#define PCI_DEVICE_ID_LATTICE_LPC_BRIDGE 0x9c32 + +/* mlxplat_priv - platform private data + * @pdev_i2c - i2c controller platform device + * @pdev_mux - array of mux platform devices + * @pdev_hotplug - hotplug platform devices + * @pdev_led - led platform devices + * @pdev_io_regs - register access platform devices + * @pdev_fan - FAN platform devices + * @pdev_wd - array of watchdog platform devices + * pdev_dpu - array of Data Processor Unit platform devices + * @regmap: device register map + * @hotplug_resources: system hotplug resources + * @hotplug_resources_size: size of system hotplug resources + * @hi2c_main_init_status: init status of I2C main bus + * @irq_fpga: FPGA IRQ number + */ +struct mlxplat_priv { + struct platform_device *pdev_i2c; + struct platform_device *pdev_mux[MLXPLAT_CPLD_LPC_MUX_DEVS]; + struct platform_device *pdev_hotplug; + struct platform_device *pdev_led; + struct platform_device *pdev_io_regs; + struct platform_device *pdev_fan; + struct platform_device *pdev_wd[MLXPLAT_CPLD_WD_MAX_DEVS]; + struct platform_device *pdev_dpu[MLXPLAT_CPLD_DPU_MAX_DEVS]; + void *regmap; + struct resource *hotplug_resources; + unsigned int hotplug_resources_size; + u8 i2c_main_init_status; + int irq_fpga; +}; + +static struct platform_device *mlxplat_dev; +static int mlxplat_i2c_main_completion_notify(void *handle, int id); +static void __iomem *i2c_bridge_addr, *jtag_bridge_addr; + +/* Regions for LPC I2C controller and LPC base register space */ +static const struct resource mlxplat_lpc_resources[] = { + [0] = DEFINE_RES_NAMED(MLXPLAT_CPLD_LPC_I2C_BASE_ADRR, + MLXPLAT_CPLD_LPC_IO_RANGE, + "mlxplat_cpld_lpc_i2c_ctrl", IORESOURCE_IO), + [1] = DEFINE_RES_NAMED(MLXPLAT_CPLD_LPC_REG_BASE_ADRR, + MLXPLAT_CPLD_LPC_IO_RANGE, + "mlxplat_cpld_lpc_regs", + IORESOURCE_IO), +}; + +/* Platform systems default i2c data */ +static struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_i2c_default_data = { + .completion_notify = mlxplat_i2c_main_completion_notify, +}; + +/* Platform i2c next generation systems data */ +static struct mlxreg_core_data mlxplat_mlxcpld_i2c_ng_items_data[] = { + { + .reg = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .mask = MLXPLAT_CPLD_I2C_CAP_MASK, + .bit = MLXPLAT_CPLD_I2C_CAP_BIT, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_i2c_ng_items[] = { + { + .data = mlxplat_mlxcpld_i2c_ng_items_data, + }, +}; + +/* Platform next generation systems i2c data */ +static struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_i2c_ng_data = { + .items = mlxplat_mlxcpld_i2c_ng_items, + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRCO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_I2C, + .completion_notify = mlxplat_i2c_main_completion_notify, +}; + +/* Platform default channels */ +static const int mlxplat_default_channels[][MLXPLAT_CPLD_GRP_CHNL_NUM] = { + { + MLXPLAT_CPLD_CH1, MLXPLAT_CPLD_CH1 + 1, MLXPLAT_CPLD_CH1 + 2, + MLXPLAT_CPLD_CH1 + 3, MLXPLAT_CPLD_CH1 + 4, MLXPLAT_CPLD_CH1 + + 5, MLXPLAT_CPLD_CH1 + 6, MLXPLAT_CPLD_CH1 + 7 + }, + { + MLXPLAT_CPLD_CH2, MLXPLAT_CPLD_CH2 + 1, MLXPLAT_CPLD_CH2 + 2, + MLXPLAT_CPLD_CH2 + 3, MLXPLAT_CPLD_CH2 + 4, MLXPLAT_CPLD_CH2 + + 5, MLXPLAT_CPLD_CH2 + 6, MLXPLAT_CPLD_CH2 + 7 + }, +}; + +/* Platform channels for MSN21xx system family */ +static const int mlxplat_msn21xx_channels[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + +/* Platform mux data */ +static struct i2c_mux_reg_platform_data mlxplat_default_mux_data[] = { + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH1, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG1, + .reg_size = 1, + .idle_in_use = 1, + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH2, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG2, + .reg_size = 1, + .idle_in_use = 1, + }, + +}; + +/* Platform mux configuration variables */ +static int mlxplat_max_adap_num; +static int mlxplat_mux_num; +static struct i2c_mux_reg_platform_data *mlxplat_mux_data; +static struct notifier_block *mlxplat_reboot_nb; + +/* Platform extended mux data */ +static struct i2c_mux_reg_platform_data mlxplat_extended_mux_data[] = { + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH1, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG1, + .reg_size = 1, + .idle_in_use = 1, + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH2, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG3, + .reg_size = 1, + .idle_in_use = 1, + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH3, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG2, + .reg_size = 1, + .idle_in_use = 1, + }, + +}; + +/* Platform channels for modular system family */ +static const int mlxplat_modular_upper_channel[] = { 1 }; +static const int mlxplat_modular_channels[] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 39, 40 +}; + +/* Platform modular mux data */ +static struct i2c_mux_reg_platform_data mlxplat_modular_mux_data[] = { + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH1, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG4, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_modular_upper_channel, + .n_values = ARRAY_SIZE(mlxplat_modular_upper_channel), + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH2_ETH_MODULAR, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG1, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_modular_channels, + .n_values = ARRAY_SIZE(mlxplat_modular_channels), + }, + { + .parent = MLXPLAT_CPLD_CH1, + .base_nr = MLXPLAT_CPLD_CH3_ETH_MODULAR, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG3, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_msn21xx_channels, + .n_values = ARRAY_SIZE(mlxplat_msn21xx_channels), + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH4_ETH_MODULAR, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG2, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_msn21xx_channels, + .n_values = ARRAY_SIZE(mlxplat_msn21xx_channels), + }, +}; + +/* Platform channels for rack switch system family */ +static const int mlxplat_rack_switch_channels[] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, +}; + +/* Platform rack switch mux data */ +static struct i2c_mux_reg_platform_data mlxplat_rack_switch_mux_data[] = { + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH1, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG1, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_rack_switch_channels, + .n_values = ARRAY_SIZE(mlxplat_rack_switch_channels), + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH2_RACK_SWITCH, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG2, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_msn21xx_channels, + .n_values = ARRAY_SIZE(mlxplat_msn21xx_channels), + }, + +}; + +/* Platform channels for ng800 system family */ +static const int mlxplat_ng800_channels[] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 +}; + +/* Platform ng800 mux data */ +static struct i2c_mux_reg_platform_data mlxplat_ng800_mux_data[] = { + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH1, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG1, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_ng800_channels, + .n_values = ARRAY_SIZE(mlxplat_ng800_channels), + }, + { + .parent = 1, + .base_nr = MLXPLAT_CPLD_CH2_NG800, + .write_only = 1, + .reg = (void __iomem *)MLXPLAT_CPLD_LPC_REG2, + .reg_size = 1, + .idle_in_use = 1, + .values = mlxplat_msn21xx_channels, + .n_values = ARRAY_SIZE(mlxplat_msn21xx_channels), + }, + +}; + +/* Platform hotplug devices */ +static struct i2c_board_info mlxplat_mlxcpld_pwr[] = { + { + I2C_BOARD_INFO("dps460", 0x59), + }, + { + I2C_BOARD_INFO("dps460", 0x58), + }, +}; + +static struct i2c_board_info mlxplat_mlxcpld_ext_pwr[] = { + { + I2C_BOARD_INFO("dps460", 0x5b), + }, + { + I2C_BOARD_INFO("dps460", 0x5a), + }, +}; + +static struct i2c_board_info mlxplat_mlxcpld_pwr_ng800[] = { + { + I2C_BOARD_INFO("dps460", 0x59), + }, + { + I2C_BOARD_INFO("dps460", 0x5a), + }, +}; + +static struct i2c_board_info mlxplat_mlxcpld_xdr_pwr[] = { + { + I2C_BOARD_INFO("dps460", 0x5d), + }, + { + I2C_BOARD_INFO("dps460", 0x5c), + }, + { + I2C_BOARD_INFO("dps460", 0x5e), + }, + { + I2C_BOARD_INFO("dps460", 0x5f), + }, +}; + +static struct i2c_board_info mlxplat_mlxcpld_fan[] = { + { + I2C_BOARD_INFO("24c32", 0x50), + }, + { + I2C_BOARD_INFO("24c32", 0x50), + }, + { + I2C_BOARD_INFO("24c32", 0x50), + }, + { + I2C_BOARD_INFO("24c32", 0x50), + }, +}; + +/* Platform hotplug comex carrier system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_comex_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +/* Platform hotplug default data */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +/* Platform hotplug dgx data */ +static struct mlxreg_core_data mlxplat_mlxcpld_dgx_pdb_items_data[] = { + { + .label = "pdb1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_DEFAULT_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_DEFAULT_NR, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_pwr_wc_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_pwr_ng800_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr_ng800[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr_ng800[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_dgx_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_fan_items_data[] = { + { + .label = "fan1", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_fan[0], + .hpdev.nr = MLXPLAT_CPLD_FAN1_DEFAULT_NR, + }, + { + .label = "fan2", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_fan[1], + .hpdev.nr = MLXPLAT_CPLD_FAN2_DEFAULT_NR, + }, + { + .label = "fan3", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_fan[2], + .hpdev.nr = MLXPLAT_CPLD_FAN3_DEFAULT_NR, + }, + { + .label = "fan4", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_fan[3], + .hpdev.nr = MLXPLAT_CPLD_FAN4_DEFAULT_NR, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_asic_items_data[] = { + { + .label = "asic1", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_asic2_items_data[] = { + { + .label = "asic2", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_default_items[] = { + { + .data = mlxplat_mlxcpld_default_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PSU_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PWR_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_FAN_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_comex_items[] = { + { + .data = mlxplat_mlxcpld_comex_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_CARRIER, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_CARRIER, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_CARRIER, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_default_data = { + .items = mlxplat_mlxcpld_default_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_default_wc_items[] = { + { + .data = mlxplat_mlxcpld_comex_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_CARRIER, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_pwr_wc_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_CARRIER, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_default_wc_data = { + .items = mlxplat_mlxcpld_default_wc_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_wc_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_comex_data = { + .items = mlxplat_mlxcpld_comex_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_comex_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_CARR_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRCX_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGRCX_MASK, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_msn21xx_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +/* Platform hotplug MSN21xx system family data */ +static struct mlxreg_core_item mlxplat_mlxcpld_msn21xx_items[] = { + { + .data = mlxplat_mlxcpld_msn21xx_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PWR_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn21xx_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_msn21xx_data = { + .items = mlxplat_mlxcpld_msn21xx_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn21xx_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug msn274x system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_msn274x_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_ng_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_msn274x_fan_items_data[] = { + { + .label = "fan1", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan2", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan3", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(2), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan4", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(3), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_msn274x_items[] = { + { + .data = mlxplat_mlxcpld_msn274x_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn274x_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_msn274x_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn274x_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_msn274x_data = { + .items = mlxplat_mlxcpld_msn274x_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn274x_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug MSN201x system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_msn201x_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_msn201x_items[] = { + { + .data = mlxplat_mlxcpld_msn201x_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PWR_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn201x_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_ASIC_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_msn201x_data = { + .items = mlxplat_mlxcpld_msn201x_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_msn201x_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_DEF, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug next generation system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_ng_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_default_ng_fan_items_data[] = { + { + .label = "fan1", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan2", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(1), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan3", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(2), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan4", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(3), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan5", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(4), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan6", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(5), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan7", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(6), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_default_ng_items[] = { + { + .data = mlxplat_mlxcpld_default_ng_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_default_ng_data = { + .items = mlxplat_mlxcpld_default_ng_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug extended system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_ext_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu3", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(2), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu4", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(3), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_ext_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr3", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr4", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_ext_items[] = { + { + .data = mlxplat_mlxcpld_ext_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_ext_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, + { + .data = mlxplat_mlxcpld_default_asic2_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic2_items_data), + .inversed = 0, + .health = true, + } +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_ext_dgx_items[] = { + { + .data = mlxplat_mlxcpld_dgx_pdb_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PSU_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_dgx_pdb_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_dgx_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_PWR_MASK_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_dgx_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_ng800_items[] = { + { + .data = mlxplat_mlxcpld_default_ng_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_pwr_ng800_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_pwr_ng800_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_ext_data = { + .items = mlxplat_mlxcpld_ext_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW | MLXPLAT_CPLD_LOW_AGGR_MASK_ASIC2, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_dgx_ext_data = { + .items = mlxplat_mlxcpld_ext_dgx_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_dgx_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW | MLXPLAT_CPLD_LOW_AGGR_MASK_ASIC2, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_ng800_data = { + .items = mlxplat_mlxcpld_ng800_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ng800_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW | MLXPLAT_CPLD_LOW_AGGR_MASK_ASIC2, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr3", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr4", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_lc_act = { + .irq = MLXPLAT_CPLD_LPC_SYSIRQ, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_asic_items_data[] = { + { + .label = "asic1", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct i2c_board_info mlxplat_mlxcpld_lc_i2c_dev[] = { + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, + { + I2C_BOARD_INFO("mlxreg-lc", MLXPLAT_CPLD_LC_ADDR), + .platform_data = &mlxplat_mlxcpld_lc_act, + }, +}; + +static struct mlxreg_core_hotplug_notifier mlxplat_mlxcpld_modular_lc_notifier[] = { + { + .identity = "lc1", + }, + { + .identity = "lc2", + }, + { + .identity = "lc3", + }, + { + .identity = "lc4", + }, + { + .identity = "lc5", + }, + { + .identity = "lc6", + }, + { + .identity = "lc7", + }, + { + .identity = "lc8", + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_pr_items_data[] = { + { + .label = "lc1_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_present", + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_ver_items_data[] = { + { + .label = "lc1_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(0), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(1), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(2), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(3), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(4), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(5), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(6), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_verified", + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = BIT(7), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .reg_sync = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .reg_pwr = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .reg_ena = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_pg_data[] = { + { + .label = "lc1_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_powered", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_ready_data[] = { + { + .label = "lc1_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_synced_data[] = { + { + .label = "lc1_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_synced", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_act_data[] = { + { + .label = "lc1_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_active", + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_modular_lc_sd_data[] = { + { + .label = "lc1_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[0], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(0), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[0], + .slot = 1, + }, + { + .label = "lc2_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[1], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(1), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[1], + .slot = 2, + }, + { + .label = "lc3_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[2], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(2), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[2], + .slot = 3, + }, + { + .label = "lc4_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[3], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(3), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[3], + .slot = 4, + }, + { + .label = "lc5_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(4), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[4], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(4), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[4], + .slot = 5, + }, + { + .label = "lc6_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(5), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[5], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(5), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[5], + .slot = 6, + }, + { + .label = "lc7_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(6), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[6], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(6), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[6], + .slot = 7, + }, + { + .label = "lc8_shutdown", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = BIT(7), + .hpdev.brdinfo = &mlxplat_mlxcpld_lc_i2c_dev[7], + .hpdev.nr = MLXPLAT_CPLD_NR_LC_SET(7), + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_modular_lc_notifier[7], + .slot = 8, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_modular_items[] = { + { + .data = mlxplat_mlxcpld_ext_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_asic_items_data), + .inversed = 0, + .health = true, + }, + { + .data = mlxplat_mlxcpld_modular_lc_pr_items_data, + .kind = MLXREG_HOTPLUG_LC_PRESENT, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_pr_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_ver_items_data, + .kind = MLXREG_HOTPLUG_LC_VERIFIED, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_ver_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_pg_data, + .kind = MLXREG_HOTPLUG_LC_POWERED, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_pg_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_ready_data, + .kind = MLXREG_HOTPLUG_LC_READY, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_ready_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_synced_data, + .kind = MLXREG_HOTPLUG_LC_SYNCED, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_synced_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_act_data, + .kind = MLXREG_HOTPLUG_LC_ACTIVE, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_act_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_lc_sd_data, + .kind = MLXREG_HOTPLUG_LC_THERMAL, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_LC, + .reg = MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET, + .mask = MLXPLAT_CPLD_LPC_LC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_lc_sd_data), + .inversed = 0, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_modular_data = { + .items = mlxplat_mlxcpld_modular_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_modular_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_MODULAR, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug for NVLink blade systems family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_global_wp_items_data[] = { + { + .label = "global_wp_grant", + .reg = MLXPLAT_CPLD_LPC_REG_GWP_OFFSET, + .mask = MLXPLAT_CPLD_GWP_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_chassis_blade_items[] = { + { + .data = mlxplat_mlxcpld_global_wp_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_GWP_OFFSET, + .mask = MLXPLAT_CPLD_GWP_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_global_wp_items_data), + .inversed = 0, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_chassis_blade_data = { + .items = mlxplat_mlxcpld_chassis_blade_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_chassis_blade_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug for switch systems family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_erot_ap_items_data[] = { + { + .label = "erot1_ap", + .reg = MLXPLAT_CPLD_LPC_REG_EROT_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "erot2_ap", + .reg = MLXPLAT_CPLD_LPC_REG_EROT_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_erot_error_items_data[] = { + { + .label = "erot1_error", + .reg = MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET, + .mask = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "erot2_error", + .reg = MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET, + .mask = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_rack_switch_items[] = { + { + .data = mlxplat_mlxcpld_ext_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_ext_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_erot_ap_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_EROT_OFFSET, + .mask = MLXPLAT_CPLD_EROT_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_erot_ap_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_erot_error_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET, + .mask = MLXPLAT_CPLD_EROT_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_erot_error_items_data), + .inversed = 1, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_rack_switch_data = { + .items = mlxplat_mlxcpld_rack_switch_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_rack_switch_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Platform hotplug XDR and smart switch system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_psu_items_data[] = { + { + .label = "psu1", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(0), + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu2", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(1), + .slot = 2, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu3", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(2), + .slot = 3, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu4", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(3), + .slot = 4, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu5", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(4), + .slot = 5, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu6", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(5), + .slot = 6, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu7", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(6), + .slot = 7, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "psu8", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = BIT(7), + .slot = 8, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_pwr_items_data[] = { + { + .label = "pwr1", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(0), + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr2", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(1), + .slot = 2, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr3", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(2), + .slot = 3, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr4", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(3), + .slot = 4, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_ext_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_MSNXXXX_NR, + }, + { + .label = "pwr5", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(4), + .slot = 5, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_xdr_pwr[0], + .hpdev.nr = MLXPLAT_CPLD_PSU_XDR_NR, + }, + { + .label = "pwr6", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(5), + .slot = 6, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_xdr_pwr[1], + .hpdev.nr = MLXPLAT_CPLD_PSU_XDR_NR, + }, + { + .label = "pwr7", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(6), + .slot = 7, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_xdr_pwr[2], + .hpdev.nr = MLXPLAT_CPLD_PSU_XDR_NR, + }, + { + .label = "pwr8", + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = BIT(7), + .slot = 8, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .hpdev.brdinfo = &mlxplat_mlxcpld_xdr_pwr[3], + .hpdev.nr = MLXPLAT_CPLD_PSU_XDR_NR, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_fan_items_data[] = { + { + .label = "fan1", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(0), + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan2", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(1), + .slot = 2, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan3", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(2), + .slot = 3, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan4", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(3), + .slot = 4, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan5", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(4), + .slot = 5, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan6", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(5), + .slot = 6, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan7", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(6), + .slot = 7, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "fan8", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = BIT(7), + .slot = 8, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(7), + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_asic1_items_data[] = { + { + .label = "asic1", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_ASIC_CAP_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + } +}; + +/* Platform hotplug for smart switch systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_smart_switch_dpu_ready_data[] = { + { + .label = "dpu1_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(0), + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu2_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(1), + .slot = 2, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu3_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(2), + .slot = 3, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu4_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = BIT(3), + .slot = 4, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_smart_switch_dpu_shtdn_ready_data[] = { + { + .label = "dpu1_shtdn_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(0), + .slot = 1, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu2_shtdn_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(1), + .slot = 2, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu3_shtdn_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(2), + .slot = 3, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "dpu4_shtdn_ready", + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = BIT(3), + .slot = 4, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_smart_switch_items[] = { + { + .data = mlxplat_mlxcpld_xdr_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_XDR_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_xdr_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_xdr_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_XDR_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_xdr_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_xdr_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_XDR_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_xdr_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_xdr_asic1_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_XDR_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_ASIC_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_xdr_asic1_items_data), + .inversed = 0, + .health = true, + }, + { + .data = mlxplat_mlxcpld_smart_switch_dpu_ready_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_DPU_CORE, + .reg = MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET, + .mask = MLXPLAT_CPLD_DPU_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_smart_switch_dpu_ready_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_smart_switch_dpu_shtdn_ready_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_DPU_CORE, + .reg = MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET, + .mask = MLXPLAT_CPLD_DPU_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_smart_switch_dpu_shtdn_ready_data), + .inversed = 1, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_smart_switch_data = { + .items = mlxplat_mlxcpld_smart_switch_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_smart_switch_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX | + MLXPLAT_CPLD_AGGR_MASK_DPU_BRD | MLXPLAT_CPLD_AGGR_MASK_DPU_CORE, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW, +}; + +/* Smart switch data processor units data */ +static struct i2c_board_info mlxplat_mlxcpld_smart_switch_dpu_devs[] = { + { + I2C_BOARD_INFO("mlxreg-dpu", MLXPLAT_CPLD_DPU_ADDR), + .irq = MLXPLAT_CPLD_LPC_SYSIRQ, + }, + { + I2C_BOARD_INFO("mlxreg-dpu", MLXPLAT_CPLD_DPU_ADDR), + .irq = MLXPLAT_CPLD_LPC_SYSIRQ, + }, + { + I2C_BOARD_INFO("mlxreg-dpu", MLXPLAT_CPLD_DPU_ADDR), + .irq = MLXPLAT_CPLD_LPC_SYSIRQ, + }, + { + I2C_BOARD_INFO("mlxreg-dpu", MLXPLAT_CPLD_DPU_ADDR), + .irq = MLXPLAT_CPLD_LPC_SYSIRQ, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_smart_switch_dpu_data[] = { + { + .label = "dpu1", + .hpdev.brdinfo = &mlxplat_mlxcpld_smart_switch_dpu_devs[0], + .hpdev.nr = MLXPLAT_CPLD_NR_DPU_BASE, + .slot = 1, + }, + { + .label = "dpu2", + .hpdev.brdinfo = &mlxplat_mlxcpld_smart_switch_dpu_devs[1], + .hpdev.nr = MLXPLAT_CPLD_NR_DPU_BASE + 1, + .slot = 2, + }, + { + .label = "dpu3", + .hpdev.brdinfo = &mlxplat_mlxcpld_smart_switch_dpu_devs[2], + .hpdev.nr = MLXPLAT_CPLD_NR_DPU_BASE + 2, + .slot = 3, + }, + { + .label = "dpu4", + .hpdev.brdinfo = &mlxplat_mlxcpld_smart_switch_dpu_devs[3], + .hpdev.nr = MLXPLAT_CPLD_NR_DPU_BASE + 3, + .slot = 4, + }, +}; + +/* Callback performs graceful shutdown after notification about power button event */ +static int +mlxplat_mlxcpld_l1_switch_pwr_events_handler(void *handle, enum mlxreg_hotplug_kind kind, + u8 action) +{ + if (action) { + dev_info(&mlxplat_dev->dev, "System shutdown due to short press of power button"); + kernel_power_off(); + } + + return 0; +} + +static struct mlxreg_core_hotplug_notifier mlxplat_mlxcpld_l1_switch_pwr_events_notifier = { + .user_handler = mlxplat_mlxcpld_l1_switch_pwr_events_handler, +}; + +/* Platform hotplug for l1 switch systems family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_l1_switch_pwr_events_items_data[] = { + { + .label = "power_button", + .reg = MLXPLAT_CPLD_LPC_REG_PWRB_OFFSET, + .mask = MLXPLAT_CPLD_PWR_BUTTON_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_l1_switch_pwr_events_notifier, + }, +}; + +/* Callback activates latch reset flow after notification about intrusion event */ +static int +mlxplat_mlxcpld_l1_switch_intrusion_events_handler(void *handle, enum mlxreg_hotplug_kind kind, + u8 action) +{ + struct mlxplat_priv *priv = platform_get_drvdata(mlxplat_dev); + u32 regval; + int err; + + err = regmap_read(priv->regmap, MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, ®val); + if (err) + goto fail_regmap_read; + + if (action) { + dev_info(&mlxplat_dev->dev, "Detected intrusion - system latch is opened"); + err = regmap_write(priv->regmap, MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + regval | MLXPLAT_CPLD_LATCH_RST_MASK); + } else { + dev_info(&mlxplat_dev->dev, "System latch is properly closed"); + err = regmap_write(priv->regmap, MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + regval & ~MLXPLAT_CPLD_LATCH_RST_MASK); + } + + if (err) + goto fail_regmap_write; + + return 0; + +fail_regmap_read: +fail_regmap_write: + dev_err(&mlxplat_dev->dev, "Register access failed"); + return err; +} + +static struct mlxreg_core_hotplug_notifier mlxplat_mlxcpld_l1_switch_intrusion_events_notifier = { + .user_handler = mlxplat_mlxcpld_l1_switch_intrusion_events_handler, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_l1_switch_health_events_items_data[] = { + { + .label = "thermal1_pdb", + .reg = MLXPLAT_CPLD_LPC_REG_BRD_OFFSET, + .mask = MLXPLAT_CPLD_THERMAL1_PDB_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "thermal2_pdb", + .reg = MLXPLAT_CPLD_LPC_REG_BRD_OFFSET, + .mask = MLXPLAT_CPLD_THERMAL2_PDB_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, + { + .label = "intrusion", + .reg = MLXPLAT_CPLD_LPC_REG_BRD_OFFSET, + .mask = MLXPLAT_CPLD_INTRUSION_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + .hpdev.action = MLXREG_HOTPLUG_DEVICE_NO_ACTION, + .hpdev.notifier = &mlxplat_mlxcpld_l1_switch_intrusion_events_notifier, + }, + { + .label = "pwm_pg", + .reg = MLXPLAT_CPLD_LPC_REG_BRD_OFFSET, + .mask = MLXPLAT_CPLD_PWM_PG_MASK, + .hpdev.nr = MLXPLAT_CPLD_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxplat_mlxcpld_l1_switch_events_items[] = { + { + .data = mlxplat_mlxcpld_default_ng_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_NG_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_erot_ap_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_EROT_OFFSET, + .mask = MLXPLAT_CPLD_EROT_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_erot_ap_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_erot_error_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET, + .mask = MLXPLAT_CPLD_EROT_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_erot_error_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_l1_switch_pwr_events_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWRB_OFFSET, + .mask = MLXPLAT_CPLD_PWR_BUTTON_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_l1_switch_pwr_events_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_l1_switch_health_events_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_BRD_OFFSET, + .mask = MLXPLAT_CPLD_L1_CHA_HEALTH_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_l1_switch_health_events_items_data), + .inversed = 1, + .health = false, + .ind = 8, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_l1_switch_data = { + .items = mlxplat_mlxcpld_l1_switch_events_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_l1_switch_events_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW | MLXPLAT_CPLD_LOW_AGGR_MASK_PWR_BUT, +}; + +/* Platform hotplug for 800G systems family data */ +static struct mlxreg_core_item mlxplat_mlxcpld_ng800_hi171_items[] = { + { + .data = mlxplat_mlxcpld_ext_psu_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PSU_OFFSET, + .mask = MLXPLAT_CPLD_PSU_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_modular_pwr_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_PWR_OFFSET, + .mask = MLXPLAT_CPLD_PWR_EXT_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ext_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = mlxplat_mlxcpld_xdr_fan_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + .mask = MLXPLAT_CPLD_FAN_XDR_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .count = ARRAY_SIZE(mlxplat_mlxcpld_xdr_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = mlxplat_mlxcpld_default_asic_items_data, + .aggr_mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF, + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(mlxplat_mlxcpld_default_asic_items_data), + .inversed = 0, + .health = true, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxplat_mlxcpld_ng800_hi171_data = { + .items = mlxplat_mlxcpld_ng800_hi171_items, + .count = ARRAY_SIZE(mlxplat_mlxcpld_ng800_hi171_items), + .cell = MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET, + .mask = MLXPLAT_CPLD_AGGR_MASK_NG_DEF | MLXPLAT_CPLD_AGGR_MASK_COMEX, + .cell_low = MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET, + .mask_low = MLXPLAT_CPLD_LOW_AGGR_MASK_LOW | MLXPLAT_CPLD_LOW_AGGR_MASK_ASIC2, +}; + +/* Platform led default data */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan1:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan2:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan3:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan4:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_led_data = { + .data = mlxplat_mlxcpld_default_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_led_data), +}; + +/* Platform led default data for water cooling */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_led_wc_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_led_wc_data = { + .data = mlxplat_mlxcpld_default_led_wc_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_led_wc_data), +}; + +/* Platform led default data for water cooling Ethernet switch blade */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_led_eth_wc_blade_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_led_eth_wc_blade_data = { + .data = mlxplat_mlxcpld_default_led_eth_wc_blade_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_led_eth_wc_blade_data), +}; + +/* Platform led MSN21xx system family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_msn21xx_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "fan:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "psu1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "psu1:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "psu2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu2:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_msn21xx_led_data = { + .data = mlxplat_mlxcpld_msn21xx_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_msn21xx_led_data), +}; + +/* Platform led for default data for 200GbE systems */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_ng_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan1:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan2:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan3:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan4:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan5:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan5:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan6:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "fan6:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "fan7:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + }, + { + .label = "fan7:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_ng_led_data = { + .data = mlxplat_mlxcpld_default_ng_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_led_data), +}; + +/* Platform led for Comex based 100GbE systems */ +static struct mlxreg_core_data mlxplat_mlxcpld_comex_100G_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan1:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan2:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan3:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan4:red", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_comex_100G_led_data = { + .data = mlxplat_mlxcpld_comex_100G_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_comex_100G_led_data), +}; + +/* Platform led for data for modular systems */ +static struct mlxreg_core_data mlxplat_mlxcpld_modular_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan1:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan2:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan3:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan4:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan5:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan5:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan6:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "fan6:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "fan7:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + }, + { + .label = "fan7:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(6), + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan_front:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "fan_front:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "mgmt:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "mgmt:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_modular_led_data = { + .data = mlxplat_mlxcpld_modular_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_modular_led_data), +}; + +/* Platform led data for chassis system */ +static struct mlxreg_core_data mlxplat_mlxcpld_l1_switch_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan1:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(0), + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan2:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(1), + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan3:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(2), + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan4:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(3), + }, + { + .label = "fan5:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan5:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(4), + }, + { + .label = "fan6:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "fan6:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .bit = BIT(5), + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_l1_switch_led_data = { + .data = mlxplat_mlxcpld_l1_switch_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_l1_switch_led_data), +}; + +/* Platform led data for XDR and smart switch systems */ +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_led_data[] = { + { + .label = "status:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "status:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, + { + .label = "psu:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "psu:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED1_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + }, + { + .label = "fan1:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 1, + }, + { + .label = "fan1:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 1, + }, + { + .label = "fan2:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 2, + }, + { + .label = "fan2:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED2_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 2, + }, + { + .label = "fan3:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 3, + }, + { + .label = "fan3:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 3, + }, + { + .label = "fan4:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 4, + }, + { + .label = "fan4:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED3_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 4, + }, + { + .label = "fan5:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 5, + }, + { + .label = "fan5:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 5, + }, + { + .label = "fan6:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 6, + }, + { + .label = "fan6:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED4_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 6, + }, + { + .label = "fan7:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 7, + }, + { + .label = "fan7:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED6_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 7, + }, + { + .label = "fan8:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 8, + }, + { + .label = "fan8:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 8, + }, + { + .label = "fan9:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 9, + }, + { + .label = "fan9:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED7_OFFSET, + .mask = MLXPLAT_CPLD_LED_HI_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 9, + }, + { + .label = "fan10:green", + .reg = MLXPLAT_CPLD_LPC_REG_LED8_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 10, + }, + { + .label = "fan10:orange", + .reg = MLXPLAT_CPLD_LPC_REG_LED8_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .slot = 10, + }, + { + .label = "uid:blue", + .reg = MLXPLAT_CPLD_LPC_REG_LED5_OFFSET, + .mask = MLXPLAT_CPLD_LED_LO_NIBBLE_MASK, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_xdr_led_data = { + .data = mlxplat_mlxcpld_xdr_led_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_xdr_led_data), +}; + +/* Platform register access default */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_main_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_fw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_hotswap_or_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "psu1_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "psu2_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "select_iio", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_regs_io_data = { + .data = mlxplat_mlxcpld_default_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_regs_io_data), +}; + +/* Platform register access MSN21xx, MSN201x, MSN274x systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_msn21xx_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_main_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_hotswap_or_halt", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_sff_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "psu1_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "psu2_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "select_iio", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_msn21xx_regs_io_data = { + .data = mlxplat_mlxcpld_msn21xx_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_msn21xx_regs_io_data), +}; + +/* Platform register access for next generation systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_ng_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld5_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD5_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld3_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld4_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld5_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD5_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld5_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD5_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "asic_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "asic2_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "erot1_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "erot2_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "clk_brd_prog_en", + .reg = MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + .secured = 1, + }, + { + .label = "erot1_recovery", + .reg = MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "erot2_recovery", + .reg = MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "erot1_wp", + .reg = MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + .secured = 1, + }, + { + .label = "erot2_wp", + .reg = MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + .secured = 1, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_swb_dc_dc_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_from_asic", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_swb_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_platform", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_soc", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_comex_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_pwr_converter_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_sw_pwr_off", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_reload_bios", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_ac_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_ac_ok_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "psu1_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "psu2_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "deep_pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0200, + }, + { + .label = "latch_reset", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0200, + }, + { + .label = "jtag_cap", + .reg = MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET, + .mask = MLXPLAT_CPLD_FU_CAP_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "jtag_enable", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "dbg1", + .reg = MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg2", + .reg = MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg3", + .reg = MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg4", + .reg = MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "asic2_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "fan_dir", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "bios_safe_mode", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "bios_active_image", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "bios_auth_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "bios_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET, + .mask = MLXPLAT_CPLD_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "pwr_converter_prog_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + .secured = 1, + }, + { + .label = "vpd_wp", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "pcie_asic_reset_dis", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "shutdown_unlock", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "erot1_ap_reset", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "erot2_ap_reset", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "lid_open", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "clk_brd1_boot_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "clk_brd2_boot_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "clk_brd_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "asic_pg_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "spi_chnl_select", + .reg = MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT, + .mask = GENMASK(7, 0), + .bit = 1, + .mode = 0644, + }, + { + .label = "config1", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config2", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config3", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "ufm_version", + .reg = MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_ng_regs_io_data = { + .data = mlxplat_mlxcpld_default_ng_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_ng_regs_io_data), +}; + +/* Platform register access for next generation systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_dgx_ng_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld3_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld4_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "asic_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_swb_dc_dc_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_from_asic", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_swb_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_platform", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_soc", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_comex_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_sw_pwr_off", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_reload_bios", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_pdb_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "pdb_reset_stby", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "deep_pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0200, + }, + { + .label = "latch_reset", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0200, + }, + { + .label = "jtag_cap", + .reg = MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET, + .mask = MLXPLAT_CPLD_FU_CAP_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "jtag_enable", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "dbg1", + .reg = MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg2", + .reg = MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg3", + .reg = MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "dbg4", + .reg = MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "fan_dir", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "bios_safe_mode", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "bios_active_image", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "bios_auth_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "bios_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET, + .mask = MLXPLAT_CPLD_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "pwr_converter_prog_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + .secured = 1, + }, + { + .label = "vpd_wp", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "pcie_asic_reset_dis", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "shutdown_unlock", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "config1", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config2", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config3", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "ufm_version", + .reg = MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_dgx_ng_regs_io_data = { + .data = mlxplat_mlxcpld_dgx_ng_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_dgx_ng_regs_io_data), +}; + +/* Platform register access for modular systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_modular_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld3_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld4_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld4_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "lc1_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "lc2_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "lc3_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0644, + }, + { + .label = "lc4_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "lc5_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "lc6_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "lc7_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "lc8_enable", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_fu", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_mgmt_dc_dc_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_sys_comex_bios", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_reload", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_platform", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_soc", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_pwr_off_from_carrier", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_swb_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_swb_aux_pwr_or_fu", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_swb_dc_dc_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_swb_12v_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_thermal_spc_or_pciesw", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "bios_safe_mode", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "bios_active_image", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "bios_auth_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "bios_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET, + .mask = MLXPLAT_CPLD_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "vpd_wp", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "pcie_asic_reset_dis", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "shutdown_unlock", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "lc1_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "lc2_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "lc3_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "lc4_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "lc5_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0200, + }, + { + .label = "lc6_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0200, + }, + { + .label = "lc7_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0200, + }, + { + .label = "lc8_rst_mask", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0200, + }, + { + .label = "psu1_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "psu2_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "psu3_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0200, + }, + { + .label = "psu4_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0200, + }, + { + .label = "auto_power_mode", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "pm_mgmt_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "jtag_enable", + .reg = MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE, + .mask = GENMASK(3, 0), + .bit = 1, + .mode = 0644, + }, + { + .label = "safe_bios_dis", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "safe_bios_dis_wp", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_WP_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "fan_dir", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "lc1_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "lc2_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "lc3_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0644, + }, + { + .label = "lc4_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "lc5_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "lc6_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "lc7_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "lc8_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_LC_PWR_ON, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "config1", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config2", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config3", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "ufm_version", + .reg = MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_modular_regs_io_data = { + .data = mlxplat_mlxcpld_modular_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_modular_regs_io_data), +}; + +/* Platform register access for chassis blade systems family data */ +static struct mlxreg_core_data mlxplat_mlxcpld_chassis_blade_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_from_comex", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_platform", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_soc", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_comex_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_voltmon_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_sw_pwr_off", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_reload_bios", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_ac_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_long_pwr_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "global_wp_request", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "jtag_enable", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "comm_chnl_ready", + .reg = MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0200, + }, + { + .label = "bios_safe_mode", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "bios_active_image", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "bios_auth_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "bios_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET, + .mask = MLXPLAT_CPLD_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "vpd_wp", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "pcie_asic_reset_dis", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "global_wp_response", + .reg = MLXPLAT_CPLD_LPC_REG_GWP_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "config1", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config2", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config3", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "ufm_version", + .reg = MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_chassis_blade_regs_io_data = { + .data = mlxplat_mlxcpld_chassis_blade_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_chassis_blade_regs_io_data), +}; + +/* Platform register access for smart switch systems families data */ +static struct mlxreg_core_data mlxplat_mlxcpld_smart_switch_regs_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld2_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld3_pn", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld2_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld3_version_min", + .reg = MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "kexec_activated", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "asic_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "eth_switch_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "dpu1_rst", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "dpu2_rst", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "dpu3_rst", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "dpu4_rst", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "dpu1_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "dpu2_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "dpu3_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "dpu4_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "reset_long_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_swb_dc_dc_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_swb_wd", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_reload", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_platform", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_soc", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_pwr", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_pwr_converter_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_sw_pwr_off", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_thermal", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_ac_pwr_fail", + .reg = MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET, + .mask = MLXPLAT_CPLD_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "port80", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_RO_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "bios_status", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = MLXPLAT_CPLD_BIOS_STATUS_MASK, + .bit = 2, + .mode = 0444, + }, + { + .label = "bios_start_retry", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "bios_active_image", + .reg = MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "vpd_wp", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "pcie_asic_reset_dis", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "shutdown_unlock", + .reg = MLXPLAT_CPLD_LPC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + }, + { + .label = "fan_dir", + .reg = MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "dpu1_rst_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "dpu2_rst_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "dpu3_rst_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "dpu4_rst_en", + .reg = MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "psu1_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "psu2_on", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "pwr_cycle", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "pwr_down", + .reg = MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "jtag_cap", + .reg = MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET, + .mask = MLXPLAT_CPLD_FU_CAP_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "jtag_enable", + .reg = MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE, + .mask = GENMASK(1, 0), + .bit = 1, + .mode = 0644, + }, + { + .label = "non_active_bios_select", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0644, + }, + { + .label = "bios_upgrade_fail", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "bios_image_invert", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "me_reboot", + .reg = MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0644, + }, + { + .label = "dpu1_pwr_force", + .reg = MLXPLAT_CPLD_LPC_REG_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0200, + }, + { + .label = "dpu2_pwr_force", + .reg = MLXPLAT_CPLD_LPC_REG_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0200, + }, + { + .label = "dpu3_pwr_force", + .reg = MLXPLAT_CPLD_LPC_REG_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0200, + }, + { + .label = "dpu4_pwr_force", + .reg = MLXPLAT_CPLD_LPC_REG_GP3_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0200, + }, + { + .label = "ufm_done", + .reg = MLXPLAT_CPLD_LPC_REG_GPI_MASK_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "asic_health", + .reg = MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET, + .mask = MLXPLAT_CPLD_ASIC_MASK, + .bit = 1, + .mode = 0444, + }, + { + .label = "psu1_ac_ok", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "psu2_ac_ok", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "psu1_no_alert", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "psu2_no_alert", + .reg = MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "asic_pg_fail", + .reg = MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "spi_chnl_select", + .reg = MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT, + .mask = GENMASK(7, 0), + .bit = 1, + .mode = 0644, + }, + { + .label = "config1", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config2", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "config3", + .reg = MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "ufm_version", + .reg = MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_smart_switch_regs_io_data = { + .data = mlxplat_mlxcpld_smart_switch_regs_io_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_smart_switch_regs_io_data), +}; + +/* Platform FAN default */ +static struct mlxreg_core_data mlxplat_mlxcpld_default_fan_data[] = { + { + .label = "pwm1", + .reg = MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET, + }, + { + .label = "pwm2", + .reg = MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET, + }, + { + .label = "pwm3", + .reg = MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET, + }, + { + .label = "pwm4", + .reg = MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET, + }, + { + .label = "tacho1", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO1_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(0), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + + }, + { + .label = "tacho2", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO2_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(1), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho3", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO3_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(2), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho4", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO4_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(3), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho5", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO5_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(4), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho6", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO6_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(5), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho7", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO7_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(6), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho8", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO8_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .bit = BIT(7), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho9", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO9_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(0), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho10", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO10_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(1), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho11", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO11_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(2), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho12", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO12_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(3), + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho13", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO13_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(4), + }, + { + .label = "tacho14", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO14_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET, + .bit = BIT(5), + }, + { + .label = "conf", + .capability = MLXPLAT_CPLD_LPC_REG_TACHO_SPEED_OFFSET, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_default_fan_data = { + .data = mlxplat_mlxcpld_default_fan_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_default_fan_data), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, +}; + +/* XDR and smart switch platform fan data */ +static struct mlxreg_core_data mlxplat_mlxcpld_xdr_fan_data[] = { + { + .label = "pwm1", + .reg = MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET, + }, + { + .label = "tacho1", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO1_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 1, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho2", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO2_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 2, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho3", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO3_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 3, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho4", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO4_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 4, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho5", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO5_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 5, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho6", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO6_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 6, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho7", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO7_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 7, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho8", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO8_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 8, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho9", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO9_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 9, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho10", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO10_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 10, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho11", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO11_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 11, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho12", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO12_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 12, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho13", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO13_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 13, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho14", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO14_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 14, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho15", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO15_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 15, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho16", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO16_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 16, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN_OFFSET, + }, + { + .label = "tacho17", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO17_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 17, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET, + }, + { + .label = "tacho18", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO18_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 18, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET, + }, + { + .label = "tacho19", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO19_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 19, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET, + }, + { + .label = "tacho20", + .reg = MLXPLAT_CPLD_LPC_REG_TACHO20_OFFSET, + .mask = GENMASK(7, 0), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET, + .slot = 20, + .reg_prsnt = MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET, + }, + { + .label = "conf", + .capability = MLXPLAT_CPLD_LPC_REG_TACHO_SPEED_OFFSET, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_xdr_fan_data = { + .data = mlxplat_mlxcpld_xdr_fan_data, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_xdr_fan_data), + .capability = MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET, + .version = 1, +}; + +/* Watchdog type1: hardware implementation version1 + * (MSN2700, MSN2410, MSN2740, MSN2100 and MSN2140 systems). + */ +static struct mlxreg_core_data mlxplat_mlxcpld_wd_main_regs_type1[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_RESET_ACT_MASK, + .bit = 0, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD1_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE1_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD_DFLT_TIMEOUT, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD_CLEAR_OFFSET, + .mask = MLXPLAT_CPLD_WD1_CLEAR_MASK, + .bit = 0, + }, + { + .label = "reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .bit = 6, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_wd_aux_regs_type1[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_FAN_ACT_MASK, + .bit = 4, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE1_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD_DFLT_TIMEOUT, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD_CLEAR_OFFSET, + .mask = MLXPLAT_CPLD_WD1_CLEAR_MASK, + .bit = 1, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_mlxcpld_wd_set_type1[] = { + { + .data = mlxplat_mlxcpld_wd_main_regs_type1, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_main_regs_type1), + .version = MLX_WDT_TYPE1, + .identity = "mlx-wdt-main", + }, + { + .data = mlxplat_mlxcpld_wd_aux_regs_type1, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_aux_regs_type1), + .version = MLX_WDT_TYPE1, + .identity = "mlx-wdt-aux", + }, +}; + +/* Watchdog type2: hardware implementation version 2 + * (all systems except (MSN2700, MSN2410, MSN2740, MSN2100 and MSN2140). + */ +static struct mlxreg_core_data mlxplat_mlxcpld_wd_main_regs_type2[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_RESET_ACT_MASK, + .bit = 0, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD_DFLT_TIMEOUT, + }, + { + .label = "timeleft", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_TLEFT_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_RESET_ACT_MASK, + .bit = 0, + }, + { + .label = "reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .bit = 6, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_wd_aux_regs_type2[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_FAN_ACT_MASK, + .bit = 4, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD_DFLT_TIMEOUT, + }, + { + .label = "timeleft", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_TLEFT_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_FAN_ACT_MASK, + .bit = 4, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_mlxcpld_wd_set_type2[] = { + { + .data = mlxplat_mlxcpld_wd_main_regs_type2, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_main_regs_type2), + .version = MLX_WDT_TYPE2, + .identity = "mlx-wdt-main", + }, + { + .data = mlxplat_mlxcpld_wd_aux_regs_type2, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_aux_regs_type2), + .version = MLX_WDT_TYPE2, + .identity = "mlx-wdt-aux", + }, +}; + +/* Watchdog type3: hardware implementation version 3 + * Can be on all systems. It's differentiated by WD capability bit. + * Old systems (MSN2700, MSN2410, MSN2740, MSN2100 and MSN2140) + * still have only one main watchdog. + */ +static struct mlxreg_core_data mlxplat_mlxcpld_wd_main_regs_type3[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_RESET_ACT_MASK, + .bit = 0, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD3_DFLT_TIMEOUT, + }, + { + .label = "timeleft", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_RESET_ACT_MASK, + .bit = 0, + }, + { + .label = "reset", + .reg = MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .bit = 6, + }, +}; + +static struct mlxreg_core_data mlxplat_mlxcpld_wd_aux_regs_type3[] = { + { + .label = "action", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_FAN_ACT_MASK, + .bit = 4, + }, + { + .label = "timeout", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + .health_cntr = MLXPLAT_CPLD_WD3_DFLT_TIMEOUT, + }, + { + .label = "timeleft", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET, + .mask = MLXPLAT_CPLD_WD_TYPE2_TO_MASK, + }, + { + .label = "ping", + .reg = MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, + .mask = MLXPLAT_CPLD_WD_FAN_ACT_MASK, + .bit = 4, + }, +}; + +static struct mlxreg_core_platform_data mlxplat_mlxcpld_wd_set_type3[] = { + { + .data = mlxplat_mlxcpld_wd_main_regs_type3, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_main_regs_type3), + .version = MLX_WDT_TYPE3, + .identity = "mlx-wdt-main", + }, + { + .data = mlxplat_mlxcpld_wd_aux_regs_type3, + .counter = ARRAY_SIZE(mlxplat_mlxcpld_wd_aux_regs_type3), + .version = MLX_WDT_TYPE3, + .identity = "mlx-wdt-aux", + }, +}; + +static bool mlxplat_mlxcpld_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED5_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED6_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED7_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED8_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP0_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_WP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN2_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN2_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PWR_ON: + case MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT: + case MLXPLAT_CPLD_LPC_REG_WD_CLEAR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD_CLEAR_WP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD1_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET: + return true; + } + return false; +} + +static bool mlxplat_mlxcpld_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED5_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED6_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED7_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED8_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION: + case MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP1_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP0_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_WP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCX_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GPI_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN2_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN2_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLC_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PWR_ON: + case MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT: + case MLXPLAT_CPLD_LPC_REG_WD_CLEAR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD_CLEAR_WP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD1_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD6_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO5_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO6_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO7_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO8_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO9_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO10_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO11_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO12_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO13_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO14_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO15_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO16_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO17_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO18_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO19_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO20_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO_SPEED_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET: + return true; + } + return false; +} + +static bool mlxplat_mlxcpld_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXPLAT_CPLD_LPC_REG_CPLD1_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_VER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_PN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_PN1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_GP4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RESET_CAUSE_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RST_CAUSE1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_RST_CAUSE2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED5_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED6_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED7_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LED8_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_DIRECTION: + case MLXPLAT_CPLD_LPC_REG_GP0_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GPCOM0_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP1_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP0_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP_RST_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GP3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FIELD_UPGRADE: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_OFFSET: + case MLXPLAT_CPLD_LPC_SAFE_BIOS_WP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FU_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_DBG4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCO_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCX_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GWP_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_GPI_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_BRD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_HEALTH_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_HEALTH_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC2_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_AC_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_ALERT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROT_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_EROTE_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWRB_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLC_OFFSET: + case MLXPLAT_CPLD_LPC_REG_AGGRLC_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_IN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_VR_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PG_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_RD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_OK_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SN_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_EVENT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_SD_MASK_OFFSET: + case MLXPLAT_CPLD_LPC_REG_LC_PWR_ON: + case MLXPLAT_CPLD_LPC_REG_GP4_RO_OFFSET: + case MLXPLAT_CPLD_LPC_REG_SPI_CHNL_SELECT: + case MLXPLAT_CPLD_LPC_REG_WD2_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD2_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TMR_OFFSET: + case MLXPLAT_CPLD_LPC_REG_WD3_TLEFT_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD6_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_I2C_CH4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD1_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD2_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD3_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD4_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CPLD5_MVER_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO4_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO5_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO6_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO7_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO8_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO9_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO10_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO11_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO12_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO13_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO14_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO15_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO16_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO17_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO18_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO19_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO20_OFFSET: + case MLXPLAT_CPLD_LPC_REG_ASIC_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_CAP1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_CAP2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_FAN_DRW_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_TACHO_SPEED_OFFSET: + case MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET: + case MLXPLAT_CPLD_LPC_REG_SLOT_QTY_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG1_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG2_OFFSET: + case MLXPLAT_CPLD_LPC_REG_CONFIG3_OFFSET: + case MLXPLAT_CPLD_LPC_REG_UFM_VERSION_OFFSET: + return true; + } + return false; +} + +static const struct reg_default mlxplat_mlxcpld_regmap_default[] = { + { MLXPLAT_CPLD_LPC_REG_WP1_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WP2_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD_CLEAR_WP_OFFSET, 0x00 }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_ng[] = { + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD_CLEAR_WP_OFFSET, 0x00 }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_comex_default[] = { + { MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET, + MLXPLAT_CPLD_LOW_AGGRCX_MASK }, + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_ng400[] = { + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, 0x00 }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_rack_switch[] = { + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, MLXPLAT_REGMAP_NVSWITCH_PWM_DEFAULT }, + { MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, 0x00 }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_eth_modular[] = { + { MLXPLAT_CPLD_LPC_REG_GP2_OFFSET, 0x61 }, + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_PWM2_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_PWM3_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_PWM4_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_AGGRLC_MASK_OFFSET, + MLXPLAT_CPLD_AGGR_MASK_LC_LOW }, +}; + +static const struct reg_default mlxplat_mlxcpld_regmap_smart_switch[] = { + { MLXPLAT_CPLD_LPC_REG_PWM_CONTROL_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD1_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD2_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_WD3_ACT_OFFSET, 0x00 }, + { MLXPLAT_CPLD_LPC_REG_AGGRCX_MASK_OFFSET, MLXPLAT_CPLD_LPC_SM_SW_MASK }, +}; + +struct mlxplat_mlxcpld_regmap_context { + void __iomem *base; +}; + +static struct mlxplat_mlxcpld_regmap_context mlxplat_mlxcpld_regmap_ctx; + +static int +mlxplat_mlxcpld_reg_read(void *context, unsigned int reg, unsigned int *val) +{ + struct mlxplat_mlxcpld_regmap_context *ctx = context; + + *val = ioread8(ctx->base + reg); + return 0; +} + +static int +mlxplat_mlxcpld_reg_write(void *context, unsigned int reg, unsigned int val) +{ + struct mlxplat_mlxcpld_regmap_context *ctx = context; + + iowrite8(val, ctx->base + reg); + return 0; +} + +static const struct regmap_config mlxplat_mlxcpld_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_default, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_default), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_ng = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_ng, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_ng), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_comex = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_comex_default, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_comex_default), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_ng400 = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_ng400, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_ng400), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_rack_switch = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_rack_switch, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_rack_switch), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_eth_modular = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_eth_modular, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_eth_modular), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static const struct regmap_config mlxplat_mlxcpld_regmap_config_smart_switch = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 255, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxplat_mlxcpld_writeable_reg, + .readable_reg = mlxplat_mlxcpld_readable_reg, + .volatile_reg = mlxplat_mlxcpld_volatile_reg, + .reg_defaults = mlxplat_mlxcpld_regmap_smart_switch, + .num_reg_defaults = ARRAY_SIZE(mlxplat_mlxcpld_regmap_smart_switch), + .reg_read = mlxplat_mlxcpld_reg_read, + .reg_write = mlxplat_mlxcpld_reg_write, +}; + +static struct resource mlxplat_mlxcpld_resources[] = { + [0] = DEFINE_RES_IRQ_NAMED(MLXPLAT_CPLD_LPC_SYSIRQ, "mlxreg-hotplug"), +}; + +static struct mlxreg_core_hotplug_platform_data *mlxplat_i2c; +static struct mlxreg_core_hotplug_platform_data *mlxplat_hotplug; +static struct mlxreg_core_platform_data *mlxplat_led; +static struct mlxreg_core_platform_data *mlxplat_regs_io; +static struct mlxreg_core_platform_data *mlxplat_fan; +static struct mlxreg_core_platform_data + *mlxplat_wd_data[MLXPLAT_CPLD_WD_MAX_DEVS]; +static struct mlxreg_core_data *mlxplat_dpu_data[MLXPLAT_CPLD_DPU_MAX_DEVS]; +static const struct regmap_config *mlxplat_regmap_config; +static struct pci_dev *lpc_bridge; +static struct pci_dev *i2c_bridge; +static struct pci_dev *jtag_bridge; + +/* Platform default reset function */ +static int mlxplat_reboot_notifier(struct notifier_block *nb, unsigned long action, void *unused) +{ + struct mlxplat_priv *priv = platform_get_drvdata(mlxplat_dev); + u32 regval; + int ret; + + ret = regmap_read(priv->regmap, MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET, ®val); + + if (action == SYS_RESTART && !ret && regval & MLXPLAT_CPLD_SYS_RESET_MASK) + regmap_write(priv->regmap, MLXPLAT_CPLD_LPC_REG_RESET_GP1_OFFSET, + MLXPLAT_CPLD_RESET_MASK); + + return NOTIFY_DONE; +} + +static struct notifier_block mlxplat_reboot_default_nb = { + .notifier_call = mlxplat_reboot_notifier, +}; + +/* Platform default poweroff function */ +static void mlxplat_poweroff(void) +{ + struct mlxplat_priv *priv = platform_get_drvdata(mlxplat_dev); + + if (mlxplat_reboot_nb) + unregister_reboot_notifier(mlxplat_reboot_nb); + regmap_write(priv->regmap, MLXPLAT_CPLD_LPC_REG_GP1_OFFSET, MLXPLAT_CPLD_HALT_MASK); + kernel_halt(); +} + +static int __init mlxplat_register_platform_device(void) +{ + mlxplat_dev = platform_device_register_simple(MLX_PLAT_DEVICE_NAME, -1, + mlxplat_lpc_resources, + ARRAY_SIZE(mlxplat_lpc_resources)); + if (IS_ERR(mlxplat_dev)) + return PTR_ERR(mlxplat_dev); + else + return 1; +} + +static int __init mlxplat_dmi_default_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_default_channels[i]; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_default_channels[i]); + } + mlxplat_hotplug = &mlxplat_mlxcpld_default_data; + mlxplat_hotplug->deferred_nr = + mlxplat_default_channels[i - 1][MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_led_data; + mlxplat_regs_io = &mlxplat_default_regs_io_data; + mlxplat_wd_data[0] = &mlxplat_mlxcpld_wd_set_type1[0]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_default_wc_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_default_channels[i]; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_default_channels[i]); + } + mlxplat_hotplug = &mlxplat_mlxcpld_default_wc_data; + mlxplat_hotplug->deferred_nr = + mlxplat_default_channels[i - 1][MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_led_wc_data; + mlxplat_regs_io = &mlxplat_default_regs_io_data; + mlxplat_wd_data[0] = &mlxplat_mlxcpld_wd_set_type1[0]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_default_eth_wc_blade_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_default_wc_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_led_eth_wc_blade_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_msn21xx_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_msn21xx_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_msn21xx_led_data; + mlxplat_regs_io = &mlxplat_msn21xx_regs_io_data; + mlxplat_wd_data[0] = &mlxplat_mlxcpld_wd_set_type1[0]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_msn274x_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_msn274x_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_led_data; + mlxplat_regs_io = &mlxplat_msn21xx_regs_io_data; + mlxplat_wd_data[0] = &mlxplat_mlxcpld_wd_set_type1[0]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_msn201x_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_msn201x_data; + mlxplat_hotplug->deferred_nr = + mlxplat_default_channels[i - 1][MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_msn21xx_led_data; + mlxplat_regs_io = &mlxplat_msn21xx_regs_io_data; + mlxplat_wd_data[0] = &mlxplat_mlxcpld_wd_set_type1[0]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_qmb7xx_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_default_ng_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_comex_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_EXT_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_extended_mux_data); + mlxplat_mux_data = mlxplat_extended_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_comex_data; + mlxplat_hotplug->deferred_nr = MLXPLAT_CPLD_MAX_PHYS_EXT_ADAPTER_NUM; + mlxplat_led = &mlxplat_comex_100G_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_default_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_comex; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_ng400_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_ext_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_ng400_dgx_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_hotplug = &mlxplat_mlxcpld_dgx_ext_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_dgx_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_modular_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_modular_mux_data); + mlxplat_mux_data = mlxplat_modular_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_modular_data; + mlxplat_hotplug->deferred_nr = MLXPLAT_CPLD_CH4_ETH_MODULAR; + mlxplat_led = &mlxplat_modular_led_data; + mlxplat_regs_io = &mlxplat_modular_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_eth_modular; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_chassis_blade_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_default_mux_data); + mlxplat_mux_data = mlxplat_default_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_chassis_blade_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + for (i = 0; i < mlxplat_mux_num; i++) { + mlxplat_mux_data[i].values = mlxplat_msn21xx_channels; + mlxplat_mux_data[i].n_values = + ARRAY_SIZE(mlxplat_msn21xx_channels); + } + mlxplat_regs_io = &mlxplat_chassis_blade_regs_io_data; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_rack_switch_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_rack_switch_mux_data); + mlxplat_mux_data = mlxplat_rack_switch_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_rack_switch_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_rack_switch; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_ng800_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_ng800_mux_data); + mlxplat_mux_data = mlxplat_ng800_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_ng800_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_ng800_dgx_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_ng800_mux_data); + mlxplat_mux_data = mlxplat_ng800_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_dgx_ext_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_default_ng_led_data; + mlxplat_regs_io = &mlxplat_dgx_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_l1_switch_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_rack_switch_mux_data); + mlxplat_mux_data = mlxplat_rack_switch_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_l1_switch_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_l1_switch_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_default_fan_data; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_rack_switch; + pm_power_off = mlxplat_poweroff; + mlxplat_reboot_nb = &mlxplat_reboot_default_nb; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_smart_switch_matched(const struct dmi_system_id *dmi) +{ + int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_ng800_mux_data); + mlxplat_mux_data = mlxplat_ng800_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_smart_switch_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_xdr_led_data; + mlxplat_regs_io = &mlxplat_smart_switch_regs_io_data; + mlxplat_fan = &mlxplat_xdr_fan_data; + + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type2); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type2[i]; + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_smart_switch_dpu_data); i++) + mlxplat_dpu_data[i] = &mlxplat_mlxcpld_smart_switch_dpu_data[i]; + + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_smart_switch; + + return mlxplat_register_platform_device(); +} + +static int __init mlxplat_dmi_ng400_hi171_matched(const struct dmi_system_id *dmi) +{ + unsigned int i; + + mlxplat_max_adap_num = MLXPLAT_CPLD_MAX_PHYS_ADAPTER_NUM; + mlxplat_mux_num = ARRAY_SIZE(mlxplat_ng800_mux_data); + mlxplat_mux_data = mlxplat_ng800_mux_data; + mlxplat_hotplug = &mlxplat_mlxcpld_ng800_hi171_data; + mlxplat_hotplug->deferred_nr = + mlxplat_msn21xx_channels[MLXPLAT_CPLD_GRP_CHNL_NUM - 1]; + mlxplat_led = &mlxplat_xdr_led_data; + mlxplat_regs_io = &mlxplat_default_ng_regs_io_data; + mlxplat_fan = &mlxplat_xdr_fan_data; + + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type3); i++) + mlxplat_wd_data[i] = &mlxplat_mlxcpld_wd_set_type3[i]; + + mlxplat_i2c = &mlxplat_mlxcpld_i2c_ng_data; + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config_ng400; + + return mlxplat_register_platform_device(); +} + +static const struct dmi_system_id mlxplat_dmi_table[] __initconst = { + { + .callback = mlxplat_dmi_default_wc_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0001"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI138"), + }, + }, + { + .callback = mlxplat_dmi_default_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0001"), + }, + }, + { + .callback = mlxplat_dmi_msn21xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0002"), + }, + }, + { + .callback = mlxplat_dmi_msn274x_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0003"), + }, + }, + { + .callback = mlxplat_dmi_msn201x_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0004"), + }, + }, + { + .callback = mlxplat_dmi_default_eth_wc_blade_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0005"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI139"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0005"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0007"), + }, + }, + { + .callback = mlxplat_dmi_comex_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0009"), + }, + }, + { + .callback = mlxplat_dmi_rack_switch_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0010"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI142"), + }, + }, + { + .callback = mlxplat_dmi_ng400_dgx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0010"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI173"), + }, + }, + { + .callback = mlxplat_dmi_ng400_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0010"), + }, + }, + { + .callback = mlxplat_dmi_modular_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0011"), + }, + }, + { + .callback = mlxplat_dmi_ng800_dgx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0013"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI174"), + }, + }, + { + .callback = mlxplat_dmi_ng800_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0013"), + }, + }, + { + .callback = mlxplat_dmi_chassis_blade_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0015"), + }, + }, + { + .callback = mlxplat_dmi_l1_switch_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0017"), + }, + }, + { + .callback = mlxplat_dmi_smart_switch_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0019"), + }, + }, + { + .callback = mlxplat_dmi_ng400_hi171_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0022"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI171"), + }, + }, + { + .callback = mlxplat_dmi_ng400_hi171_matched, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "VMOD0022"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "HI172"), + }, + }, + { + .callback = mlxplat_dmi_msn274x_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN274"), + }, + }, + { + .callback = mlxplat_dmi_default_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN24"), + }, + }, + { + .callback = mlxplat_dmi_default_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN27"), + }, + }, + { + .callback = mlxplat_dmi_default_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSB"), + }, + }, + { + .callback = mlxplat_dmi_default_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSX"), + }, + }, + { + .callback = mlxplat_dmi_msn21xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN21"), + }, + }, + { + .callback = mlxplat_dmi_msn201x_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN201"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MQM87"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN37"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN34"), + }, + }, + { + .callback = mlxplat_dmi_qmb7xx_matched, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Mellanox Technologies"), + DMI_MATCH(DMI_PRODUCT_NAME, "MSN38"), + }, + }, + { } +}; + +MODULE_DEVICE_TABLE(dmi, mlxplat_dmi_table); + +static int mlxplat_mlxcpld_verify_bus_topology(int *nr) +{ + struct i2c_adapter *search_adap; + int i, shift = 0; + + /* Scan adapters from expected id to verify it is free. */ + *nr = MLXPLAT_CPLD_PHYS_ADAPTER_DEF_NR; + for (i = MLXPLAT_CPLD_PHYS_ADAPTER_DEF_NR; i < + mlxplat_max_adap_num; i++) { + search_adap = i2c_get_adapter(i); + if (search_adap) { + i2c_put_adapter(search_adap); + continue; + } + + /* Return if expected parent adapter is free. */ + if (i == MLXPLAT_CPLD_PHYS_ADAPTER_DEF_NR) + return 0; + break; + } + + /* Return with error if free id for adapter is not found. */ + if (i == mlxplat_max_adap_num) + return -ENODEV; + + /* Shift adapter ids, since expected parent adapter is not free. */ + *nr = i; + for (i = 0; i < mlxplat_mux_num; i++) { + shift = *nr - mlxplat_mux_data[i].parent; + mlxplat_mux_data[i].parent = *nr; + mlxplat_mux_data[i].base_nr += shift; + } + + if (shift > 0) + mlxplat_hotplug->shift_nr = shift; + + return 0; +} + +static int mlxplat_mlxcpld_check_wd_capability(void *regmap) +{ + u32 regval; + int i, rc; + + rc = regmap_read(regmap, MLXPLAT_CPLD_LPC_REG_PSU_I2C_CAP_OFFSET, + ®val); + if (rc) + return rc; + + if (!(regval & ~MLXPLAT_CPLD_WD_CPBLTY_MASK)) { + for (i = 0; i < ARRAY_SIZE(mlxplat_mlxcpld_wd_set_type3); i++) { + if (mlxplat_wd_data[i]) + mlxplat_wd_data[i] = + &mlxplat_mlxcpld_wd_set_type3[i]; + } + } + + return 0; +} + +static int mlxplat_lpc_cpld_device_init(struct resource **hotplug_resources, + unsigned int *hotplug_resources_size) +{ + int err; + + mlxplat_mlxcpld_regmap_ctx.base = devm_ioport_map(&mlxplat_dev->dev, + mlxplat_lpc_resources[1].start, 1); + if (!mlxplat_mlxcpld_regmap_ctx.base) { + err = -ENOMEM; + goto fail_devm_ioport_map; + } + + *hotplug_resources = mlxplat_mlxcpld_resources; + *hotplug_resources_size = ARRAY_SIZE(mlxplat_mlxcpld_resources); + + return 0; + +fail_devm_ioport_map: + return err; +} + +static void mlxplat_lpc_cpld_device_exit(void) +{ +} + +static int +mlxplat_pci_fpga_device_init(unsigned int device, const char *res_name, struct pci_dev **pci_bridge, + void __iomem **pci_bridge_addr) +{ + void __iomem *pci_mem_addr; + struct pci_dev *pci_dev; + int err; + + pci_dev = pci_get_device(PCI_VENDOR_ID_LATTICE, device, NULL); + if (!pci_dev) + return -ENODEV; + + err = pci_enable_device(pci_dev); + if (err) { + dev_err(&pci_dev->dev, "pci_enable_device failed with error %d\n", err); + goto fail_pci_enable_device; + } + + err = pci_request_region(pci_dev, 0, res_name); + if (err) { + dev_err(&pci_dev->dev, "pci_request_regions failed with error %d\n", err); + goto fail_pci_request_regions; + } + + err = dma_set_mask_and_coherent(&pci_dev->dev, DMA_BIT_MASK(64)); + if (err) { + err = dma_set_mask(&pci_dev->dev, DMA_BIT_MASK(32)); + if (err) { + dev_err(&pci_dev->dev, "dma_set_mask failed with error %d\n", err); + goto fail_pci_set_dma_mask; + } + } + + pci_set_master(pci_dev); + + pci_mem_addr = devm_ioremap(&pci_dev->dev, pci_resource_start(pci_dev, 0), + pci_resource_len(pci_dev, 0)); + if (!pci_mem_addr) { + dev_err(&mlxplat_dev->dev, "ioremap failed\n"); + err = -EIO; + goto fail_ioremap; + } + + *pci_bridge = pci_dev; + *pci_bridge_addr = pci_mem_addr; + + return 0; + +fail_ioremap: +fail_pci_set_dma_mask: + pci_release_regions(pci_dev); +fail_pci_request_regions: + pci_disable_device(pci_dev); +fail_pci_enable_device: + pci_dev_put(pci_dev); + return err; +} + +static void +mlxplat_pci_fpga_device_exit(struct pci_dev *pci_bridge, + void __iomem *pci_bridge_addr) +{ + iounmap(pci_bridge_addr); + pci_release_regions(pci_bridge); + pci_disable_device(pci_bridge); + pci_dev_put(pci_bridge); +} + +static int +mlxplat_pci_fpga_devices_init(struct resource **hotplug_resources, + unsigned int *hotplug_resources_size) +{ + int err; + + err = mlxplat_pci_fpga_device_init(PCI_DEVICE_ID_LATTICE_LPC_BRIDGE, + "mlxplat_lpc_bridge", &lpc_bridge, + &mlxplat_mlxcpld_regmap_ctx.base); + if (err) + goto mlxplat_pci_fpga_device_init_lpc_fail; + + err = mlxplat_pci_fpga_device_init(PCI_DEVICE_ID_LATTICE_I2C_BRIDGE, + "mlxplat_i2c_bridge", &i2c_bridge, + &i2c_bridge_addr); + if (err) + goto mlxplat_pci_fpga_device_init_i2c_fail; + + err = mlxplat_pci_fpga_device_init(PCI_DEVICE_ID_LATTICE_JTAG_BRIDGE, + "mlxplat_jtag_bridge", &jtag_bridge, + &jtag_bridge_addr); + if (err) + goto mlxplat_pci_fpga_device_init_jtag_fail; + + return 0; + +mlxplat_pci_fpga_device_init_jtag_fail: + mlxplat_pci_fpga_device_exit(i2c_bridge, i2c_bridge_addr); +mlxplat_pci_fpga_device_init_i2c_fail: + mlxplat_pci_fpga_device_exit(lpc_bridge, mlxplat_mlxcpld_regmap_ctx.base); +mlxplat_pci_fpga_device_init_lpc_fail: + return err; +} + +static void mlxplat_pci_fpga_devices_exit(void) +{ + mlxplat_pci_fpga_device_exit(jtag_bridge, jtag_bridge_addr); + mlxplat_pci_fpga_device_exit(i2c_bridge, i2c_bridge_addr); + mlxplat_pci_fpga_device_exit(lpc_bridge, mlxplat_mlxcpld_regmap_ctx.base); +} + +static int +mlxplat_logicdev_init(struct resource **hotplug_resources, unsigned int *hotplug_resources_size) +{ + int err; + + err = mlxplat_pci_fpga_devices_init(hotplug_resources, hotplug_resources_size); + if (err == -ENODEV) + return mlxplat_lpc_cpld_device_init(hotplug_resources, hotplug_resources_size); + + return err; +} + +static void mlxplat_logicdev_exit(void) +{ + if (lpc_bridge) + mlxplat_pci_fpga_devices_exit(); + else + mlxplat_lpc_cpld_device_exit(); +} + +static int mlxplat_platdevs_init(struct mlxplat_priv *priv) +{ + int i = 0, err; + + /* Add hotplug driver */ + if (mlxplat_hotplug) { + mlxplat_hotplug->regmap = priv->regmap; + if (priv->irq_fpga) + mlxplat_hotplug->irq = priv->irq_fpga; + priv->pdev_hotplug = + platform_device_register_resndata(&mlxplat_dev->dev, + "mlxreg-hotplug", PLATFORM_DEVID_NONE, + priv->hotplug_resources, + priv->hotplug_resources_size, + mlxplat_hotplug, sizeof(*mlxplat_hotplug)); + if (IS_ERR(priv->pdev_hotplug)) { + err = PTR_ERR(priv->pdev_hotplug); + goto fail_platform_hotplug_register; + } + } + + /* Add LED driver. */ + if (mlxplat_led) { + mlxplat_led->regmap = priv->regmap; + priv->pdev_led = + platform_device_register_resndata(&mlxplat_dev->dev, "leds-mlxreg", + PLATFORM_DEVID_NONE, NULL, 0, mlxplat_led, + sizeof(*mlxplat_led)); + if (IS_ERR(priv->pdev_led)) { + err = PTR_ERR(priv->pdev_led); + goto fail_platform_leds_register; + } + } + + /* Add registers io access driver. */ + if (mlxplat_regs_io) { + mlxplat_regs_io->regmap = priv->regmap; + priv->pdev_io_regs = platform_device_register_resndata(&mlxplat_dev->dev, + "mlxreg-io", + PLATFORM_DEVID_NONE, NULL, + 0, mlxplat_regs_io, + sizeof(*mlxplat_regs_io)); + if (IS_ERR(priv->pdev_io_regs)) { + err = PTR_ERR(priv->pdev_io_regs); + goto fail_platform_io_register; + } + } + + /* Add FAN driver. */ + if (mlxplat_fan) { + mlxplat_fan->regmap = priv->regmap; + priv->pdev_fan = platform_device_register_resndata(&mlxplat_dev->dev, "mlxreg-fan", + PLATFORM_DEVID_NONE, NULL, 0, + mlxplat_fan, + sizeof(*mlxplat_fan)); + if (IS_ERR(priv->pdev_fan)) { + err = PTR_ERR(priv->pdev_fan); + goto fail_platform_fan_register; + } + } + + /* Add WD drivers. */ + err = mlxplat_mlxcpld_check_wd_capability(priv->regmap); + if (err) + goto fail_platform_wd_register; + for (i = 0; i < MLXPLAT_CPLD_WD_MAX_DEVS; i++) { + if (mlxplat_wd_data[i]) { + mlxplat_wd_data[i]->regmap = priv->regmap; + priv->pdev_wd[i] = + platform_device_register_resndata(&mlxplat_dev->dev, "mlx-wdt", i, + NULL, 0, mlxplat_wd_data[i], + sizeof(*mlxplat_wd_data[i])); + if (IS_ERR(priv->pdev_wd[i])) { + err = PTR_ERR(priv->pdev_wd[i]); + goto fail_platform_wd_register; + } + } + } + + /* Add DPU drivers. */ + for (i = 0; i < MLXPLAT_CPLD_DPU_MAX_DEVS; i++) { + if (!mlxplat_dpu_data[i]) + continue; + priv->pdev_dpu[i] = + platform_device_register_resndata(&mlxplat_dev->dev, "mlxreg-dpu", + i, NULL, 0, mlxplat_dpu_data[i], + sizeof(*mlxplat_dpu_data[i])); + if (IS_ERR(priv->pdev_dpu[i])) { + err = PTR_ERR(priv->pdev_dpu[i]); + goto fail_platform_dpu_register; + } + } + + return 0; + +fail_platform_dpu_register: + while (i--) + platform_device_unregister(priv->pdev_dpu[i]); +fail_platform_wd_register: + while (i--) + platform_device_unregister(priv->pdev_wd[i]); +fail_platform_fan_register: + if (mlxplat_regs_io) + platform_device_unregister(priv->pdev_io_regs); +fail_platform_io_register: + if (mlxplat_led) + platform_device_unregister(priv->pdev_led); +fail_platform_leds_register: + if (mlxplat_hotplug) + platform_device_unregister(priv->pdev_hotplug); +fail_platform_hotplug_register: + return err; +} + +static void mlxplat_platdevs_exit(struct mlxplat_priv *priv) +{ + int i; + + for (i = MLXPLAT_CPLD_DPU_MAX_DEVS - 1; i >= 0; i--) + platform_device_unregister(priv->pdev_dpu[i]); + for (i = MLXPLAT_CPLD_WD_MAX_DEVS - 1; i >= 0; i--) + platform_device_unregister(priv->pdev_wd[i]); + if (priv->pdev_fan) + platform_device_unregister(priv->pdev_fan); + if (priv->pdev_io_regs) + platform_device_unregister(priv->pdev_io_regs); + if (priv->pdev_led) + platform_device_unregister(priv->pdev_led); + if (priv->pdev_hotplug) + platform_device_unregister(priv->pdev_hotplug); +} + +static int +mlxplat_i2c_mux_complition_notify(void *handle, struct i2c_adapter *parent, + struct i2c_adapter *adapters[]) +{ + struct mlxplat_priv *priv = handle; + + return mlxplat_platdevs_init(priv); +} + +static int mlxplat_i2c_mux_topology_init(struct mlxplat_priv *priv) +{ + int i, err; + + if (!priv->pdev_i2c) { + priv->i2c_main_init_status = MLXPLAT_I2C_MAIN_BUS_NOTIFIED; + return 0; + } + + priv->i2c_main_init_status = MLXPLAT_I2C_MAIN_BUS_HANDLE_CREATED; + for (i = 0; i < mlxplat_mux_num; i++) { + priv->pdev_mux[i] = platform_device_register_resndata(&priv->pdev_i2c->dev, + "i2c-mux-reg", i, NULL, 0, + &mlxplat_mux_data[i], + sizeof(mlxplat_mux_data[i])); + if (IS_ERR(priv->pdev_mux[i])) { + err = PTR_ERR(priv->pdev_mux[i]); + goto fail_platform_mux_register; + } + } + + return mlxplat_i2c_mux_complition_notify(priv, NULL, NULL); + +fail_platform_mux_register: + while (i--) + platform_device_unregister(priv->pdev_mux[i]); + return err; +} + +static void mlxplat_i2c_mux_topology_exit(struct mlxplat_priv *priv) +{ + int i; + + for (i = mlxplat_mux_num - 1; i >= 0; i--) { + if (priv->pdev_mux[i]) + platform_device_unregister(priv->pdev_mux[i]); + } +} + +static int mlxplat_i2c_main_completion_notify(void *handle, int id) +{ + struct mlxplat_priv *priv = handle; + + return mlxplat_i2c_mux_topology_init(priv); +} + +static int mlxplat_i2c_main_init(struct mlxplat_priv *priv) +{ + int nr, err; + + if (!mlxplat_i2c) + return 0; + + err = mlxplat_mlxcpld_verify_bus_topology(&nr); + if (nr < 0) + goto fail_mlxplat_mlxcpld_verify_bus_topology; + + nr = (nr == mlxplat_max_adap_num) ? -1 : nr; + mlxplat_i2c->regmap = priv->regmap; + mlxplat_i2c->handle = priv; + + /* Set mapped base address of I2C-LPC bridge over PCIe */ + if (lpc_bridge) + mlxplat_i2c->addr = i2c_bridge_addr; + priv->pdev_i2c = platform_device_register_resndata(&mlxplat_dev->dev, "i2c_mlxcpld", + nr, priv->hotplug_resources, + priv->hotplug_resources_size, + mlxplat_i2c, sizeof(*mlxplat_i2c)); + if (IS_ERR(priv->pdev_i2c)) { + err = PTR_ERR(priv->pdev_i2c); + goto fail_platform_i2c_register; + } + + if (priv->i2c_main_init_status == MLXPLAT_I2C_MAIN_BUS_NOTIFIED) { + err = mlxplat_i2c_mux_topology_init(priv); + if (err) + goto fail_mlxplat_i2c_mux_topology_init; + } + + return 0; + +fail_mlxplat_i2c_mux_topology_init: + platform_device_unregister(priv->pdev_i2c); +fail_platform_i2c_register: +fail_mlxplat_mlxcpld_verify_bus_topology: + return err; +} + +static void mlxplat_i2c_main_exit(struct mlxplat_priv *priv) +{ + mlxplat_platdevs_exit(priv); + mlxplat_i2c_mux_topology_exit(priv); + if (priv->pdev_i2c) + platform_device_unregister(priv->pdev_i2c); +} + +static int mlxplat_probe(struct platform_device *pdev) +{ + unsigned int hotplug_resources_size = 0; + struct resource *hotplug_resources = NULL; + struct acpi_device *acpi_dev; + struct mlxplat_priv *priv; + int irq_fpga = 0, i, err; + + acpi_dev = ACPI_COMPANION(&pdev->dev); + if (acpi_dev) { + irq_fpga = acpi_dev_gpio_irq_get(acpi_dev, 0); + if (irq_fpga < 0) + return -ENODEV; + mlxplat_dev = pdev; + } + + err = mlxplat_logicdev_init(&hotplug_resources, &hotplug_resources_size); + if (err) + return err; + + priv = devm_kzalloc(&mlxplat_dev->dev, sizeof(struct mlxplat_priv), + GFP_KERNEL); + if (!priv) { + err = -ENOMEM; + goto fail_alloc; + } + platform_set_drvdata(mlxplat_dev, priv); + priv->hotplug_resources = hotplug_resources; + priv->hotplug_resources_size = hotplug_resources_size; + priv->irq_fpga = irq_fpga; + + if (!mlxplat_regmap_config) + mlxplat_regmap_config = &mlxplat_mlxcpld_regmap_config; + + priv->regmap = devm_regmap_init(&mlxplat_dev->dev, NULL, + &mlxplat_mlxcpld_regmap_ctx, + mlxplat_regmap_config); + if (IS_ERR(priv->regmap)) { + err = PTR_ERR(priv->regmap); + goto fail_alloc; + } + + /* Set default registers. */ + for (i = 0; i < mlxplat_regmap_config->num_reg_defaults; i++) { + err = regmap_write(priv->regmap, + mlxplat_regmap_config->reg_defaults[i].reg, + mlxplat_regmap_config->reg_defaults[i].def); + if (err) + goto fail_regmap_write; + } + + err = mlxplat_i2c_main_init(priv); + if (err) + goto fail_mlxplat_i2c_main_init; + + /* Sync registers with hardware. */ + regcache_mark_dirty(priv->regmap); + err = regcache_sync(priv->regmap); + if (err) + goto fail_regcache_sync; + + if (mlxplat_reboot_nb) { + err = register_reboot_notifier(mlxplat_reboot_nb); + if (err) + goto fail_register_reboot_notifier; + } + + return 0; + +fail_register_reboot_notifier: +fail_regcache_sync: + mlxplat_i2c_main_exit(priv); +fail_mlxplat_i2c_main_init: +fail_regmap_write: +fail_alloc: + mlxplat_logicdev_exit(); + + return err; +} + +static void mlxplat_remove(struct platform_device *pdev) +{ + struct mlxplat_priv *priv = platform_get_drvdata(mlxplat_dev); + + if (pm_power_off) + pm_power_off = NULL; + if (mlxplat_reboot_nb) + unregister_reboot_notifier(mlxplat_reboot_nb); + mlxplat_i2c_main_exit(priv); + mlxplat_logicdev_exit(); +} + +static const struct acpi_device_id mlxplat_acpi_table[] = { + { "MLNXBF49", 0 }, + {} +}; +MODULE_DEVICE_TABLE(acpi, mlxplat_acpi_table); + +static struct platform_driver mlxplat_driver = { + .driver = { + .name = "mlxplat", + .acpi_match_table = mlxplat_acpi_table, + .probe_type = PROBE_FORCE_SYNCHRONOUS, + }, + .probe = mlxplat_probe, + .remove = mlxplat_remove, +}; + +static int __init mlxplat_init(void) +{ + int err; + + if (!dmi_check_system(mlxplat_dmi_table)) + return -ENODEV; + + err = platform_driver_register(&mlxplat_driver); + if (err) + return err; + return 0; +} +module_init(mlxplat_init); + +static void __exit mlxplat_exit(void) +{ + if (mlxplat_dev) + platform_device_unregister(mlxplat_dev); + + platform_driver_unregister(&mlxplat_driver); +} +module_exit(mlxplat_exit); + +MODULE_AUTHOR("Vadim Pasternak "); +MODULE_DESCRIPTION("Mellanox platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); diff --git a/drivers/platform/mellanox/mlxbf-bootctl.c b/drivers/platform/mellanox/mlxbf-bootctl.c new file mode 100644 index 000000000..f67c7f56a --- /dev/null +++ b/drivers/platform/mellanox/mlxbf-bootctl.c @@ -0,0 +1,1084 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Mellanox boot control driver + * + * This driver provides a sysfs interface for systems management + * software to manage reset-time actions. + * + * Copyright (C) 2019 Mellanox Technologies + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "mlxbf-bootctl.h" + +#define MLXBF_BOOTCTL_SB_SECURE_MASK 0x03 +#define MLXBF_BOOTCTL_SB_TEST_MASK 0x0c +#define MLXBF_BOOTCTL_SB_DEV_MASK BIT(4) + +#define MLXBF_SB_KEY_NUM 4 + +/* UUID used to probe ATF service. */ +static const char *mlxbf_bootctl_svc_uuid_str = + "89c036b4-e7d7-11e6-8797-001aca00bfc4"; + +struct mlxbf_bootctl_name { + u32 value; + const char *name; +}; + +static struct mlxbf_bootctl_name boot_names[] = { + { MLXBF_BOOTCTL_EXTERNAL, "external" }, + { MLXBF_BOOTCTL_EMMC, "emmc" }, + { MLNX_BOOTCTL_SWAP_EMMC, "swap_emmc" }, + { MLXBF_BOOTCTL_EMMC_LEGACY, "emmc_legacy" }, + { MLXBF_BOOTCTL_NONE, "none" }, +}; + +enum { + MLXBF_BOOTCTL_SB_LIFECYCLE_PRODUCTION = 0, + MLXBF_BOOTCTL_SB_LIFECYCLE_GA_SECURE = 1, + MLXBF_BOOTCTL_SB_LIFECYCLE_GA_NON_SECURE = 2, + MLXBF_BOOTCTL_SB_LIFECYCLE_RMA = 3 +}; + +static const char * const mlxbf_bootctl_lifecycle_states[] = { + [MLXBF_BOOTCTL_SB_LIFECYCLE_PRODUCTION] = "Production", + [MLXBF_BOOTCTL_SB_LIFECYCLE_GA_SECURE] = "GA Secured", + [MLXBF_BOOTCTL_SB_LIFECYCLE_GA_NON_SECURE] = "GA Non-Secured", + [MLXBF_BOOTCTL_SB_LIFECYCLE_RMA] = "RMA", +}; + +/* Log header format. */ +#define MLXBF_RSH_LOG_TYPE_MASK GENMASK_ULL(59, 56) +#define MLXBF_RSH_LOG_LEN_MASK GENMASK_ULL(54, 48) +#define MLXBF_RSH_LOG_LEVEL_MASK GENMASK_ULL(7, 0) + +/* Log module ID and type (only MSG type in Linux driver for now). */ +#define MLXBF_RSH_LOG_TYPE_MSG 0x04ULL + +/* Log ctl/data register offset. */ +#define MLXBF_RSH_SCRATCH_BUF_CTL_OFF 0 +#define MLXBF_RSH_SCRATCH_BUF_DATA_OFF 0x10 + +/* Log message levels. */ +enum { + MLXBF_RSH_LOG_INFO, + MLXBF_RSH_LOG_WARN, + MLXBF_RSH_LOG_ERR, + MLXBF_RSH_LOG_ASSERT +}; + +/* Mapped pointer for RSH_BOOT_FIFO_DATA and RSH_BOOT_FIFO_COUNT register. */ +static void __iomem *mlxbf_rsh_boot_data; +static void __iomem *mlxbf_rsh_boot_cnt; + +/* Mapped pointer for rsh log semaphore/ctrl/data register. */ +static void __iomem *mlxbf_rsh_semaphore; +static void __iomem *mlxbf_rsh_scratch_buf_ctl; +static void __iomem *mlxbf_rsh_scratch_buf_data; + +/* Rsh log levels. */ +static const char * const mlxbf_rsh_log_level[] = { + "INFO", "WARN", "ERR", "ASSERT"}; + +static DEFINE_MUTEX(icm_ops_lock); +static DEFINE_MUTEX(os_up_lock); +static DEFINE_MUTEX(mfg_ops_lock); +static DEFINE_MUTEX(rtc_ops_lock); + +/* + * Objects are stored within the MFG partition per type. + * Type 0 is not supported. + */ +enum { + MLNX_MFG_TYPE_OOB_MAC = 1, + MLNX_MFG_TYPE_OPN_0, + MLNX_MFG_TYPE_OPN_1, + MLNX_MFG_TYPE_OPN_2, + MLNX_MFG_TYPE_SKU_0, + MLNX_MFG_TYPE_SKU_1, + MLNX_MFG_TYPE_SKU_2, + MLNX_MFG_TYPE_MODL_0, + MLNX_MFG_TYPE_MODL_1, + MLNX_MFG_TYPE_MODL_2, + MLNX_MFG_TYPE_SN_0, + MLNX_MFG_TYPE_SN_1, + MLNX_MFG_TYPE_SN_2, + MLNX_MFG_TYPE_UUID_0, + MLNX_MFG_TYPE_UUID_1, + MLNX_MFG_TYPE_UUID_2, + MLNX_MFG_TYPE_UUID_3, + MLNX_MFG_TYPE_UUID_4, + MLNX_MFG_TYPE_REV, +}; + +#define MLNX_MFG_OPN_VAL_LEN 24 +#define MLNX_MFG_SKU_VAL_LEN 24 +#define MLNX_MFG_MODL_VAL_LEN 24 +#define MLNX_MFG_SN_VAL_LEN 24 +#define MLNX_MFG_UUID_VAL_LEN 40 +#define MLNX_MFG_REV_VAL_LEN 8 +#define MLNX_MFG_VAL_QWORD_CNT(type) \ + (MLNX_MFG_##type##_VAL_LEN / sizeof(u64)) + +/* + * The MAC address consists of 6 bytes (2 digits each) separated by ':'. + * The expected format is: "XX:XX:XX:XX:XX:XX" + */ +#define MLNX_MFG_OOB_MAC_FORMAT_LEN \ + ((ETH_ALEN * 2) + (ETH_ALEN - 1)) + +/* ARM SMC call which is atomic and no need for lock. */ +static int mlxbf_bootctl_smc(unsigned int smc_op, int smc_arg) +{ + struct arm_smccc_res res; + + arm_smccc_smc(smc_op, smc_arg, 0, 0, 0, 0, 0, 0, &res); + + return res.a0; +} + +/* Return the action in integer or an error code. */ +static int mlxbf_bootctl_reset_action_to_val(const char *action) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(boot_names); i++) + if (sysfs_streq(boot_names[i].name, action)) + return boot_names[i].value; + + return -EINVAL; +} + +/* Return the action in string. */ +static const char *mlxbf_bootctl_action_to_string(int action) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(boot_names); i++) + if (boot_names[i].value == action) + return boot_names[i].name; + + return "invalid action"; +} + +static ssize_t post_reset_wdog_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = mlxbf_bootctl_smc(MLXBF_BOOTCTL_GET_POST_RESET_WDOG, 0); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t post_reset_wdog_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long value; + int ret; + + ret = kstrtoul(buf, 10, &value); + if (ret) + return ret; + + ret = mlxbf_bootctl_smc(MLXBF_BOOTCTL_SET_POST_RESET_WDOG, value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t mlxbf_bootctl_show(int smc_op, char *buf) +{ + int action; + + action = mlxbf_bootctl_smc(smc_op, 0); + if (action < 0) + return action; + + return sysfs_emit(buf, "%s\n", mlxbf_bootctl_action_to_string(action)); +} + +static int mlxbf_bootctl_store(int smc_op, const char *buf, size_t count) +{ + int ret, action; + + action = mlxbf_bootctl_reset_action_to_val(buf); + if (action < 0) + return action; + + ret = mlxbf_bootctl_smc(smc_op, action); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t reset_action_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return mlxbf_bootctl_show(MLXBF_BOOTCTL_GET_RESET_ACTION, buf); +} + +static ssize_t reset_action_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return mlxbf_bootctl_store(MLXBF_BOOTCTL_SET_RESET_ACTION, buf, count); +} + +static ssize_t second_reset_action_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return mlxbf_bootctl_show(MLXBF_BOOTCTL_GET_SECOND_RESET_ACTION, buf); +} + +static ssize_t second_reset_action_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return mlxbf_bootctl_store(MLXBF_BOOTCTL_SET_SECOND_RESET_ACTION, buf, + count); +} + +static ssize_t lifecycle_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int status_bits; + int use_dev_key; + int test_state; + int lc_state; + + status_bits = mlxbf_bootctl_smc(MLXBF_BOOTCTL_GET_TBB_FUSE_STATUS, + MLXBF_BOOTCTL_FUSE_STATUS_LIFECYCLE); + if (status_bits < 0) + return status_bits; + + use_dev_key = status_bits & MLXBF_BOOTCTL_SB_DEV_MASK; + test_state = status_bits & MLXBF_BOOTCTL_SB_TEST_MASK; + lc_state = status_bits & MLXBF_BOOTCTL_SB_SECURE_MASK; + + /* + * If the test bits are set, we specify that the current state may be + * due to using the test bits. + */ + if (test_state) { + return sysfs_emit(buf, "%s(test)\n", + mlxbf_bootctl_lifecycle_states[lc_state]); + } else if (use_dev_key && + (lc_state == MLXBF_BOOTCTL_SB_LIFECYCLE_GA_SECURE)) { + return sysfs_emit(buf, "Secured (development)\n"); + } + + return sysfs_emit(buf, "%s\n", mlxbf_bootctl_lifecycle_states[lc_state]); +} + +static ssize_t secure_boot_fuse_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int burnt, valid, key, key_state, buf_len = 0, upper_key_used = 0; + const char *status; + + key_state = mlxbf_bootctl_smc(MLXBF_BOOTCTL_GET_TBB_FUSE_STATUS, + MLXBF_BOOTCTL_FUSE_STATUS_KEYS); + if (key_state < 0) + return key_state; + + /* + * key_state contains the bits for 4 Key versions, loaded from eFuses + * after a hard reset. Lower 4 bits are a thermometer code indicating + * key programming has started for key n (0000 = none, 0001 = version 0, + * 0011 = version 1, 0111 = version 2, 1111 = version 3). Upper 4 bits + * are a thermometer code indicating key programming has completed for + * key n (same encodings as the start bits). This allows for detection + * of an interruption in the programming process which has left the key + * partially programmed (and thus invalid). The process is to burn the + * eFuse for the new key start bit, burn the key eFuses, then burn the + * eFuse for the new key complete bit. + * + * For example 0000_0000: no key valid, 0001_0001: key version 0 valid, + * 0011_0011: key 1 version valid, 0011_0111: key version 2 started + * programming but did not complete, etc. The most recent key for which + * both start and complete bit is set is loaded. On soft reset, this + * register is not modified. + */ + for (key = MLXBF_SB_KEY_NUM - 1; key >= 0; key--) { + burnt = key_state & BIT(key); + valid = key_state & BIT(key + MLXBF_SB_KEY_NUM); + + if (burnt && valid) + upper_key_used = 1; + + if (upper_key_used) { + if (burnt) + status = valid ? "Used" : "Wasted"; + else + status = valid ? "Invalid" : "Skipped"; + } else { + if (burnt) + status = valid ? "InUse" : "Incomplete"; + else + status = valid ? "Invalid" : "Free"; + } + buf_len += sysfs_emit_at(buf, buf_len, "%d:%s ", key, status); + } + buf_len += sysfs_emit_at(buf, buf_len, "\n"); + + return buf_len; +} + +static ssize_t fw_reset_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long key; + int err; + + err = kstrtoul(buf, 16, &key); + if (err) + return err; + + if (mlxbf_bootctl_smc(MLXBF_BOOTCTL_FW_RESET, key) < 0) + return -EINVAL; + + return count; +} + +/* Size(8-byte words) of the log buffer. */ +#define RSH_SCRATCH_BUF_CTL_IDX_MASK 0x7f + +/* 100ms timeout */ +#define RSH_SCRATCH_BUF_POLL_TIMEOUT 100000 + +static int mlxbf_rsh_log_sem_lock(void) +{ + unsigned long reg; + + return readq_poll_timeout(mlxbf_rsh_semaphore, reg, !reg, 0, + RSH_SCRATCH_BUF_POLL_TIMEOUT); +} + +static void mlxbf_rsh_log_sem_unlock(void) +{ + writeq(0, mlxbf_rsh_semaphore); +} + +static ssize_t rsh_log_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int rc, idx, num, len, level = MLXBF_RSH_LOG_INFO; + size_t size = count; + u64 data; + + if (!size) + return -EINVAL; + + if (!mlxbf_rsh_semaphore || !mlxbf_rsh_scratch_buf_ctl) + return -EOPNOTSUPP; + + /* Ignore line break at the end. */ + if (buf[size - 1] == '\n') + size--; + + /* Check the message prefix. */ + for (idx = 0; idx < ARRAY_SIZE(mlxbf_rsh_log_level); idx++) { + len = strlen(mlxbf_rsh_log_level[idx]); + if (len + 1 < size && + !strncmp(buf, mlxbf_rsh_log_level[idx], len)) { + buf += len; + size -= len; + level = idx; + break; + } + } + + /* Ignore leading spaces. */ + while (size > 0 && buf[0] == ' ') { + size--; + buf++; + } + + /* Take the semaphore. */ + rc = mlxbf_rsh_log_sem_lock(); + if (rc) + return rc; + + /* Calculate how many words are available. */ + idx = readq(mlxbf_rsh_scratch_buf_ctl); + num = min((int)DIV_ROUND_UP(size, sizeof(u64)), + RSH_SCRATCH_BUF_CTL_IDX_MASK - idx - 1); + if (num <= 0) + goto done; + + /* Write Header. */ + data = FIELD_PREP(MLXBF_RSH_LOG_TYPE_MASK, MLXBF_RSH_LOG_TYPE_MSG); + data |= FIELD_PREP(MLXBF_RSH_LOG_LEN_MASK, num); + data |= FIELD_PREP(MLXBF_RSH_LOG_LEVEL_MASK, level); + writeq(data, mlxbf_rsh_scratch_buf_data); + + /* Write message. */ + for (idx = 0; idx < num && size > 0; idx++) { + if (size < sizeof(u64)) { + data = 0; + memcpy(&data, buf, size); + size = 0; + } else { + memcpy(&data, buf, sizeof(u64)); + size -= sizeof(u64); + buf += sizeof(u64); + } + writeq(data, mlxbf_rsh_scratch_buf_data); + } + +done: + /* Release the semaphore. */ + mlxbf_rsh_log_sem_unlock(); + + /* Ignore the rest if no more space. */ + return count; +} + +static ssize_t large_icm_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct arm_smccc_res res; + + mutex_lock(&icm_ops_lock); + arm_smccc_smc(MLNX_HANDLE_GET_ICM_INFO, 0, 0, 0, 0, + 0, 0, 0, &res); + mutex_unlock(&icm_ops_lock); + if (res.a0) + return -EPERM; + + return sysfs_emit(buf, "0x%lx", res.a1); +} + +static ssize_t large_icm_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct arm_smccc_res res; + unsigned long icm_data; + int err; + + err = kstrtoul(buf, MLXBF_LARGE_ICMC_MAX_STRING_SIZE, &icm_data); + if (err) + return err; + + if ((icm_data != 0 && icm_data < MLXBF_LARGE_ICMC_SIZE_MIN) || + icm_data > MLXBF_LARGE_ICMC_SIZE_MAX || icm_data % MLXBF_LARGE_ICMC_GRANULARITY) + return -EPERM; + + mutex_lock(&icm_ops_lock); + arm_smccc_smc(MLNX_HANDLE_SET_ICM_INFO, icm_data, 0, 0, 0, 0, 0, 0, &res); + mutex_unlock(&icm_ops_lock); + + return res.a0 ? -EPERM : count; +} + +static ssize_t rtc_battery_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct arm_smccc_res res; + + mutex_lock(&rtc_ops_lock); + arm_smccc_smc(MLNX_HANDLE_GET_RTC_LOW_BATT, 0, 0, 0, 0, + 0, 0, 0, &res); + mutex_unlock(&rtc_ops_lock); + + if (res.a0) + return -EPERM; + + return sysfs_emit(buf, "0x%lx\n", res.a1); +} + +static ssize_t os_up_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct arm_smccc_res res; + unsigned long val; + int err; + + err = kstrtoul(buf, 10, &val); + if (err) + return err; + + if (val != 1) + return -EINVAL; + + mutex_lock(&os_up_lock); + arm_smccc_smc(MLNX_HANDLE_OS_UP, 0, 0, 0, 0, 0, 0, 0, &res); + mutex_unlock(&os_up_lock); + + return count; +} + +static ssize_t oob_mac_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct arm_smccc_res res; + u8 *mac_byte_ptr; + + mutex_lock(&mfg_ops_lock); + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, MLNX_MFG_TYPE_OOB_MAC, 0, 0, 0, + 0, 0, 0, &res); + mutex_unlock(&mfg_ops_lock); + if (res.a0) + return -EPERM; + + mac_byte_ptr = (u8 *)&res.a1; + + return sysfs_format_mac(buf, mac_byte_ptr, ETH_ALEN); +} + +static ssize_t oob_mac_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int byte[MLNX_MFG_OOB_MAC_FORMAT_LEN] = { 0 }; + struct arm_smccc_res res; + int byte_idx, len; + u64 mac_addr = 0; + u8 *mac_byte_ptr; + + if ((count - 1) != MLNX_MFG_OOB_MAC_FORMAT_LEN) + return -EINVAL; + + len = sscanf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", + &byte[0], &byte[1], &byte[2], + &byte[3], &byte[4], &byte[5]); + if (len != ETH_ALEN) + return -EINVAL; + + mac_byte_ptr = (u8 *)&mac_addr; + + for (byte_idx = 0; byte_idx < ETH_ALEN; byte_idx++) + mac_byte_ptr[byte_idx] = (u8)byte[byte_idx]; + + mutex_lock(&mfg_ops_lock); + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, MLNX_MFG_TYPE_OOB_MAC, + ETH_ALEN, mac_addr, 0, 0, 0, 0, &res); + mutex_unlock(&mfg_ops_lock); + + return res.a0 ? -EPERM : count; +} + +static ssize_t opn_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 opn_data[MLNX_MFG_VAL_QWORD_CNT(OPN) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(OPN); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_OPN_0 + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + opn_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)opn_data); +} + +static ssize_t opn_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 opn[MLNX_MFG_VAL_QWORD_CNT(OPN)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_OPN_VAL_LEN) + return -EINVAL; + + memcpy(opn, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(OPN); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_OPN_0 + word, + sizeof(u64), opn[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t sku_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 sku_data[MLNX_MFG_VAL_QWORD_CNT(SKU) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(SKU); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_SKU_0 + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + sku_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)sku_data); +} + +static ssize_t sku_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 sku[MLNX_MFG_VAL_QWORD_CNT(SKU)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_SKU_VAL_LEN) + return -EINVAL; + + memcpy(sku, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(SKU); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_SKU_0 + word, + sizeof(u64), sku[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t modl_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 modl_data[MLNX_MFG_VAL_QWORD_CNT(MODL) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(MODL); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_MODL_0 + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + modl_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)modl_data); +} + +static ssize_t modl_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 modl[MLNX_MFG_VAL_QWORD_CNT(MODL)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_MODL_VAL_LEN) + return -EINVAL; + + memcpy(modl, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(MODL); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_MODL_0 + word, + sizeof(u64), modl[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t sn_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 sn_data[MLNX_MFG_VAL_QWORD_CNT(SN) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(SN); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_SN_0 + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + sn_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)sn_data); +} + +static ssize_t sn_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 sn[MLNX_MFG_VAL_QWORD_CNT(SN)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_SN_VAL_LEN) + return -EINVAL; + + memcpy(sn, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(SN); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_SN_0 + word, + sizeof(u64), sn[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t uuid_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 uuid_data[MLNX_MFG_VAL_QWORD_CNT(UUID) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(UUID); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_UUID_0 + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + uuid_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)uuid_data); +} + +static ssize_t uuid_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 uuid[MLNX_MFG_VAL_QWORD_CNT(UUID)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_UUID_VAL_LEN) + return -EINVAL; + + memcpy(uuid, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(UUID); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_UUID_0 + word, + sizeof(u64), uuid[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t rev_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u64 rev_data[MLNX_MFG_VAL_QWORD_CNT(REV) + 1] = { 0 }; + struct arm_smccc_res res; + int word; + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(REV); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_GET_MFG_INFO, + MLNX_MFG_TYPE_REV + word, + 0, 0, 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + rev_data[word] = res.a1; + } + mutex_unlock(&mfg_ops_lock); + + return sysfs_emit(buf, "%s", (char *)rev_data); +} + +static ssize_t rev_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 rev[MLNX_MFG_VAL_QWORD_CNT(REV)] = { 0 }; + struct arm_smccc_res res; + int word; + + if (count > MLNX_MFG_REV_VAL_LEN) + return -EINVAL; + + memcpy(rev, buf, count); + + mutex_lock(&mfg_ops_lock); + for (word = 0; word < MLNX_MFG_VAL_QWORD_CNT(REV); word++) { + arm_smccc_smc(MLXBF_BOOTCTL_SET_MFG_INFO, + MLNX_MFG_TYPE_REV + word, + sizeof(u64), rev[word], 0, 0, 0, 0, &res); + if (res.a0) { + mutex_unlock(&mfg_ops_lock); + return -EPERM; + } + } + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static ssize_t mfg_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct arm_smccc_res res; + unsigned long val; + int err; + + err = kstrtoul(buf, 10, &val); + if (err) + return err; + + if (val != 1) + return -EINVAL; + + mutex_lock(&mfg_ops_lock); + arm_smccc_smc(MLXBF_BOOTCTL_LOCK_MFG_INFO, 0, 0, 0, 0, 0, 0, 0, &res); + mutex_unlock(&mfg_ops_lock); + + return count; +} + +static DEVICE_ATTR_RW(post_reset_wdog); +static DEVICE_ATTR_RW(reset_action); +static DEVICE_ATTR_RW(second_reset_action); +static DEVICE_ATTR_RO(lifecycle_state); +static DEVICE_ATTR_RO(secure_boot_fuse_state); +static DEVICE_ATTR_WO(fw_reset); +static DEVICE_ATTR_WO(rsh_log); +static DEVICE_ATTR_RW(large_icm); +static DEVICE_ATTR_WO(os_up); +static DEVICE_ATTR_RW(oob_mac); +static DEVICE_ATTR_RW(opn); +static DEVICE_ATTR_RW(sku); +static DEVICE_ATTR_RW(modl); +static DEVICE_ATTR_RW(sn); +static DEVICE_ATTR_RW(uuid); +static DEVICE_ATTR_RW(rev); +static DEVICE_ATTR_WO(mfg_lock); +static DEVICE_ATTR_RO(rtc_battery); + +static struct attribute *mlxbf_bootctl_attrs[] = { + &dev_attr_post_reset_wdog.attr, + &dev_attr_reset_action.attr, + &dev_attr_second_reset_action.attr, + &dev_attr_lifecycle_state.attr, + &dev_attr_secure_boot_fuse_state.attr, + &dev_attr_fw_reset.attr, + &dev_attr_rsh_log.attr, + &dev_attr_large_icm.attr, + &dev_attr_os_up.attr, + &dev_attr_oob_mac.attr, + &dev_attr_opn.attr, + &dev_attr_sku.attr, + &dev_attr_modl.attr, + &dev_attr_sn.attr, + &dev_attr_uuid.attr, + &dev_attr_rev.attr, + &dev_attr_mfg_lock.attr, + &dev_attr_rtc_battery.attr, + NULL +}; + +ATTRIBUTE_GROUPS(mlxbf_bootctl); + +static const struct acpi_device_id mlxbf_bootctl_acpi_ids[] = { + {"MLNXBF04", 0}, + {} +}; + +MODULE_DEVICE_TABLE(acpi, mlxbf_bootctl_acpi_ids); + +static ssize_t mlxbf_bootctl_bootfifo_read(struct file *filp, + struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buf, loff_t pos, + size_t count) +{ + unsigned long timeout = msecs_to_jiffies(500); + unsigned long expire = jiffies + timeout; + u64 data, cnt = 0; + char *p = buf; + + while (count >= sizeof(data)) { + /* Give up reading if no more data within 500ms. */ + if (!cnt) { + cnt = readq(mlxbf_rsh_boot_cnt); + if (!cnt) { + if (time_after(jiffies, expire)) + break; + usleep_range(10, 50); + continue; + } + } + + data = readq(mlxbf_rsh_boot_data); + memcpy(p, &data, sizeof(data)); + count -= sizeof(data); + p += sizeof(data); + cnt--; + expire = jiffies + timeout; + } + + return p - buf; +} + +static const struct bin_attribute mlxbf_bootctl_bootfifo_sysfs_attr = { + .attr = { .name = "bootfifo", .mode = 0400 }, + .read = mlxbf_bootctl_bootfifo_read, +}; + +static bool mlxbf_bootctl_guid_match(const guid_t *guid, + const struct arm_smccc_res *res) +{ + guid_t id = GUID_INIT(res->a0, res->a1, res->a1 >> 16, + res->a2, res->a2 >> 8, res->a2 >> 16, + res->a2 >> 24, res->a3, res->a3 >> 8, + res->a3 >> 16, res->a3 >> 24); + + return guid_equal(guid, &id); +} + +static int mlxbf_bootctl_probe(struct platform_device *pdev) +{ + struct arm_smccc_res res = { 0 }; + void __iomem *reg; + guid_t guid; + int ret; + + /* Map the resource of the bootfifo data register. */ + mlxbf_rsh_boot_data = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(mlxbf_rsh_boot_data)) + return PTR_ERR(mlxbf_rsh_boot_data); + + /* Map the resource of the bootfifo counter register. */ + mlxbf_rsh_boot_cnt = devm_platform_ioremap_resource(pdev, 1); + if (IS_ERR(mlxbf_rsh_boot_cnt)) + return PTR_ERR(mlxbf_rsh_boot_cnt); + + /* Map the resource of the rshim semaphore register. */ + mlxbf_rsh_semaphore = devm_platform_ioremap_resource(pdev, 2); + if (IS_ERR(mlxbf_rsh_semaphore)) + return PTR_ERR(mlxbf_rsh_semaphore); + + /* Map the resource of the scratch buffer (log) registers. */ + reg = devm_platform_ioremap_resource(pdev, 3); + if (IS_ERR(reg)) + return PTR_ERR(reg); + mlxbf_rsh_scratch_buf_ctl = reg + MLXBF_RSH_SCRATCH_BUF_CTL_OFF; + mlxbf_rsh_scratch_buf_data = reg + MLXBF_RSH_SCRATCH_BUF_DATA_OFF; + + /* Ensure we have the UUID we expect for this service. */ + arm_smccc_smc(MLXBF_BOOTCTL_SIP_SVC_UID, 0, 0, 0, 0, 0, 0, 0, &res); + guid_parse(mlxbf_bootctl_svc_uuid_str, &guid); + if (!mlxbf_bootctl_guid_match(&guid, &res)) + return -ENODEV; + + /* + * When watchdog is used, it sets boot mode to MLXBF_BOOTCTL_SWAP_EMMC + * in case of boot failures. However it doesn't clear the state if there + * is no failure. Restore the default boot mode here to avoid any + * unnecessary boot partition swapping. + */ + ret = mlxbf_bootctl_smc(MLXBF_BOOTCTL_SET_RESET_ACTION, + MLXBF_BOOTCTL_EMMC); + if (ret < 0) + dev_warn(&pdev->dev, "Unable to reset the EMMC boot mode\n"); + + ret = sysfs_create_bin_file(&pdev->dev.kobj, + &mlxbf_bootctl_bootfifo_sysfs_attr); + if (ret) + pr_err("Unable to create bootfifo sysfs file, error %d\n", ret); + + return ret; +} + +static void mlxbf_bootctl_remove(struct platform_device *pdev) +{ + sysfs_remove_bin_file(&pdev->dev.kobj, + &mlxbf_bootctl_bootfifo_sysfs_attr); +} + +static struct platform_driver mlxbf_bootctl_driver = { + .probe = mlxbf_bootctl_probe, + .remove = mlxbf_bootctl_remove, + .driver = { + .name = "mlxbf-bootctl", + .dev_groups = mlxbf_bootctl_groups, + .acpi_match_table = mlxbf_bootctl_acpi_ids, + } +}; + +module_platform_driver(mlxbf_bootctl_driver); + +MODULE_DESCRIPTION("Mellanox boot control driver"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Mellanox Technologies"); diff --git a/drivers/platform/mellanox/mlxbf-bootctl.h b/drivers/platform/mellanox/mlxbf-bootctl.h new file mode 100644 index 000000000..90bbbdc65 --- /dev/null +++ b/drivers/platform/mellanox/mlxbf-bootctl.h @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, Mellanox Technologies. All rights reserved. + */ + +#ifndef __MLXBF_BOOTCTL_H__ +#define __MLXBF_BOOTCTL_H__ + +/* + * Request that the on-chip watchdog be enabled, or disabled, after + * the next chip soft reset. This call does not affect the current + * status of the on-chip watchdog. If non-zero, the argument + * specifies the watchdog interval in seconds. If zero, the watchdog + * will not be enabled after the next soft reset. Non-zero errors are + * returned as documented below. + */ +#define MLXBF_BOOTCTL_SET_POST_RESET_WDOG 0x82000000 + +/* + * Query the status which has been requested for the on-chip watchdog + * after the next chip soft reset. Returns the interval as set by + * MLXBF_BOOTCTL_SET_POST_RESET_WDOG. + */ +#define MLXBF_BOOTCTL_GET_POST_RESET_WDOG 0x82000001 + +/* + * Request that a specific boot action be taken at the next soft + * reset. By default, the boot action is set by external chip pins, + * which are sampled on hard reset. Note that the boot action + * requested by this call will persist on subsequent resets unless + * this service, or the MLNX_SET_SECOND_RESET_ACTION service, is + * invoked. See below for the available MLNX_BOOT_xxx parameter + * values. Non-zero errors are returned as documented below. + */ +#define MLXBF_BOOTCTL_SET_RESET_ACTION 0x82000002 + +/* + * Return the specific boot action which will be taken at the next + * soft reset. Returns the reset action (see below for the parameter + * values for MLXBF_BOOTCTL_SET_RESET_ACTION). + */ +#define MLXBF_BOOTCTL_GET_RESET_ACTION 0x82000003 + +/* + * Request that a specific boot action be taken at the soft reset + * after the next soft reset. For a specified valid boot mode, the + * effect of this call is identical to that of invoking + * MLXBF_BOOTCTL_SET_RESET_ACTION after the next chip soft reset; in + * particular, after that reset, the action for the now next reset can + * be queried with MLXBF_BOOTCTL_GET_RESET_ACTION and modified with + * MLXBF_BOOTCTL_SET_RESET_ACTION. You may also specify the parameter as + * MLNX_BOOT_NONE, which is equivalent to specifying that no call to + * MLXBF_BOOTCTL_SET_RESET_ACTION be taken after the next chip soft reset. + * This call does not affect the action to be taken at the next soft + * reset. Non-zero errors are returned as documented below. + */ +#define MLXBF_BOOTCTL_SET_SECOND_RESET_ACTION 0x82000004 + +/* + * Return the specific boot action which will be taken at the soft + * reset after the next soft reset; this will be one of the valid + * actions for MLXBF_BOOTCTL_SET_SECOND_RESET_ACTION. + */ +#define MLXBF_BOOTCTL_GET_SECOND_RESET_ACTION 0x82000005 + +/* + * Return the fuse status of the current chip. The caller should specify + * with the second argument if the state of the lifecycle fuses or the + * version of secure boot fuse keys left should be returned. + */ +#define MLXBF_BOOTCTL_GET_TBB_FUSE_STATUS 0x82000006 + +/* Reset eMMC by programming the RST_N register. */ +#define MLXBF_BOOTCTL_SET_EMMC_RST_N 0x82000007 + +#define MLXBF_BOOTCTL_GET_DIMM_INFO 0x82000008 + +/* + * Initiate Firmware Reset via TYU. This might be invoked during the reset + * flow in isolation mode. + */ +#define MLXBF_BOOTCTL_FW_RESET 0x8200000D + +/* + * SMC function IDs to set, get and lock the manufacturing information + * stored within the eeprom. + */ +#define MLXBF_BOOTCTL_SET_MFG_INFO 0x8200000E +#define MLXBF_BOOTCTL_GET_MFG_INFO 0x8200000F +#define MLXBF_BOOTCTL_LOCK_MFG_INFO 0x82000011 + +/* + * SMC function IDs to set and get the large ICM carveout size + * stored in the eeprom. + */ +#define MLNX_HANDLE_SET_ICM_INFO 0x82000012 +#define MLNX_HANDLE_GET_ICM_INFO 0x82000013 + +#define MAX_ICM_BUFFER_SIZE 10 + +/* + * SMC function ID to set the ARM boot state to up + */ +#define MLNX_HANDLE_OS_UP 0x82000014 + +/* + * SMC function ID to get and clear the RTC low voltage bit + */ +#define MLNX_HANDLE_GET_RTC_LOW_BATT 0x82000023 + +/* SMC function IDs for SiP Service queries */ +#define MLXBF_BOOTCTL_SIP_SVC_CALL_COUNT 0x8200ff00 +#define MLXBF_BOOTCTL_SIP_SVC_UID 0x8200ff01 +#define MLXBF_BOOTCTL_SIP_SVC_VERSION 0x8200ff03 + +/* ARM Standard Service Calls version numbers */ +#define MLXBF_BOOTCTL_SVC_VERSION_MAJOR 0x0 +#define MLXBF_BOOTCTL_SVC_VERSION_MINOR 0x2 + +/* Number of svc calls defined. */ +#define MLXBF_BOOTCTL_NUM_SVC_CALLS 12 + +/* Valid reset actions for MLXBF_BOOTCTL_SET_RESET_ACTION. */ +#define MLXBF_BOOTCTL_EXTERNAL 0 /* Not boot from eMMC */ +#define MLXBF_BOOTCTL_EMMC 1 /* From primary eMMC boot partition */ +#define MLNX_BOOTCTL_SWAP_EMMC 2 /* Swap eMMC boot partitions and reboot */ +#define MLXBF_BOOTCTL_EMMC_LEGACY 3 /* From primary eMMC in legacy mode */ + +/* Valid arguments for requesting the fuse status. */ +#define MLXBF_BOOTCTL_FUSE_STATUS_LIFECYCLE 0 /* Return lifecycle status. */ +#define MLXBF_BOOTCTL_FUSE_STATUS_KEYS 1 /* Return secure boot key status */ + +/* Additional value to disable the MLXBF_BOOTCTL_SET_SECOND_RESET_ACTION. */ +#define MLXBF_BOOTCTL_NONE 0x7fffffff /* Don't change next boot action */ + +#define MLXBF_LARGE_ICMC_MAX_STRING_SIZE 16 +#define MLXBF_LARGE_ICMC_SIZE_MIN 0x80 +#define MLXBF_LARGE_ICMC_SIZE_MAX 0x100000 +#define MLXBF_LARGE_ICMC_GRANULARITY MLXBF_LARGE_ICMC_SIZE_MIN + +#endif /* __MLXBF_BOOTCTL_H__ */ diff --git a/drivers/platform/mellanox/mlxbf-pmc.c b/drivers/platform/mellanox/mlxbf-pmc.c new file mode 100644 index 000000000..5ec1ad471 --- /dev/null +++ b/drivers/platform/mellanox/mlxbf-pmc.c @@ -0,0 +1,2371 @@ +// SPDX-License-Identifier: GPL-2.0-only OR Linux-OpenIB +/* + * Mellanox BlueField Performance Monitoring Counters driver + * + * This driver provides a sysfs interface for monitoring + * performance statistics in BlueField SoC. + * + * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MLXBF_PMC_WRITE_REG_32 0x82000009 +#define MLXBF_PMC_READ_REG_32 0x8200000A +#define MLXBF_PMC_WRITE_REG_64 0x8200000B +#define MLXBF_PMC_READ_REG_64 0x8200000C +#define MLXBF_PMC_SIP_SVC_UID 0x8200ff01 +#define MLXBF_PMC_SIP_SVC_VERSION 0x8200ff03 +#define MLXBF_PMC_SVC_REQ_MAJOR 0 +#define MLXBF_PMC_SVC_MIN_MINOR 3 + +#define MLXBF_PMC_SMCCC_ACCESS_VIOLATION -4 + +#define MLXBF_PMC_EVENT_SET_BF1 0 +#define MLXBF_PMC_EVENT_SET_BF2 1 +#define MLXBF_PMC_EVENT_SET_BF3 2 +#define MLXBF_PMC_EVENT_INFO_LEN 100 + +#define MLXBF_PMC_MAX_BLOCKS 40 +#define MLXBF_PMC_MAX_ATTRS 70 +#define MLXBF_PMC_INFO_SZ 4 +#define MLXBF_PMC_REG_SIZE 8 +#define MLXBF_PMC_L3C_REG_SIZE 4 + +#define MLXBF_PMC_TYPE_CRSPACE 2 +#define MLXBF_PMC_TYPE_COUNTER 1 +#define MLXBF_PMC_TYPE_REGISTER 0 + +#define MLXBF_PMC_PERFCTL 0 +#define MLXBF_PMC_PERFEVT 1 +#define MLXBF_PMC_PERFACC0 4 + +#define MLXBF_PMC_PERFMON_CONFIG_WR_R_B BIT(0) +#define MLXBF_PMC_PERFMON_CONFIG_STROBE BIT(1) +#define MLXBF_PMC_PERFMON_CONFIG_ADDR GENMASK_ULL(4, 2) +#define MLXBF_PMC_PERFMON_CONFIG_WDATA GENMASK_ULL(60, 5) + +#define MLXBF_PMC_PERFCTL_FM0 GENMASK_ULL(18, 16) +#define MLXBF_PMC_PERFCTL_MS0 GENMASK_ULL(21, 20) +#define MLXBF_PMC_PERFCTL_ACCM0 GENMASK_ULL(26, 24) +#define MLXBF_PMC_PERFCTL_AD0 BIT(27) +#define MLXBF_PMC_PERFCTL_ETRIG0 GENMASK_ULL(29, 28) +#define MLXBF_PMC_PERFCTL_EB0 BIT(30) +#define MLXBF_PMC_PERFCTL_EN0 BIT(31) + +#define MLXBF_PMC_PERFEVT_EVTSEL GENMASK_ULL(31, 24) + +#define MLXBF_PMC_L3C_PERF_CNT_CFG 0x0 +#define MLXBF_PMC_L3C_PERF_CNT_SEL 0x10 +#define MLXBF_PMC_L3C_PERF_CNT_SEL_1 0x14 +#define MLXBF_PMC_L3C_PERF_CNT_LOW 0x40 +#define MLXBF_PMC_L3C_PERF_CNT_HIGH 0x60 + +#define MLXBF_PMC_L3C_PERF_CNT_CFG_EN BIT(0) +#define MLXBF_PMC_L3C_PERF_CNT_CFG_RST BIT(1) +#define MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_0 GENMASK(5, 0) +#define MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_1 GENMASK(13, 8) +#define MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_2 GENMASK(21, 16) +#define MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_3 GENMASK(29, 24) + +#define MLXBF_PMC_L3C_PERF_CNT_SEL_1_CNT_4 GENMASK(5, 0) + +#define MLXBF_PMC_L3C_PERF_CNT_LOW_VAL GENMASK(31, 0) +#define MLXBF_PMC_L3C_PERF_CNT_HIGH_VAL GENMASK(24, 0) + +#define MLXBF_PMC_CRSPACE_PERFMON_REG0 0x0 +#define MLXBF_PMC_CRSPACE_PERFSEL_SZ 4 +#define MLXBF_PMC_CRSPACE_PERFSEL0 GENMASK(23, 16) +#define MLXBF_PMC_CRSPACE_PERFSEL1 GENMASK(7, 0) +#define MLXBF_PMC_CRSPACE_PERFMON_REG0_SZ 0x2 +#define MLXBF_PMC_CRSPACE_PERFMON_CTL(n) (n * MLXBF_PMC_CRSPACE_PERFMON_REG0_SZ) +#define MLXBF_PMC_CRSPACE_PERFMON_EN BIT(30) +#define MLXBF_PMC_CRSPACE_PERFMON_CLR BIT(28) +#define MLXBF_PMC_CRSPACE_PERFMON_COUNT_CLOCK(n) (MLXBF_PMC_CRSPACE_PERFMON_CTL(n) + 0x4) +#define MLXBF_PMC_CRSPACE_PERFMON_VAL0(n) (MLXBF_PMC_CRSPACE_PERFMON_CTL(n) + 0xc) + +/** + * struct mlxbf_pmc_attribute - Structure to hold attribute and block info + * for each sysfs entry + * @dev_attr: Device attribute struct + * @index: index to identify counter number within a block + * @nr: block number to which the sysfs belongs + */ +struct mlxbf_pmc_attribute { + struct device_attribute dev_attr; + unsigned int index; + unsigned int nr; +}; + +/** + * struct mlxbf_pmc_block_info - Structure to hold info for each HW block + * + * @mmio_base: The VA at which the PMC block is mapped + * @blk_size: Size of each mapped region + * @counters: Number of counters in the block + * @type: Type of counters in the block + * @attr_counter: Attributes for "counter" sysfs files + * @attr_event: Attributes for "event" sysfs files + * @attr_event_list: Attributes for "event_list" sysfs files + * @attr_enable: Attributes for "enable" sysfs files + * @attr_count_clock: Attributes for "count_clock" sysfs files + * @block_attr: All attributes needed for the block + * @block_attr_grp: Attribute group for the block + */ +struct mlxbf_pmc_block_info { + void __iomem *mmio_base; + size_t blk_size; + size_t counters; + unsigned int type; + struct mlxbf_pmc_attribute *attr_counter; + struct mlxbf_pmc_attribute *attr_event; + struct mlxbf_pmc_attribute attr_event_list; + struct mlxbf_pmc_attribute attr_enable; + struct mlxbf_pmc_attribute attr_count_clock; + struct attribute *block_attr[MLXBF_PMC_MAX_ATTRS]; + struct attribute_group block_attr_grp; +}; + +/** + * struct mlxbf_pmc_context - Structure to hold PMC context info + * + * @pdev: The kernel structure representing the device + * @total_blocks: Total number of blocks + * @tile_count: Number of tiles in the system + * @apt_enable: Info on enabled APTs + * @llt_enable: Info on enabled LLTs + * @mss_enable: Info on enabled MSSs + * @group_num: Group number assigned to each valid block + * @hwmon_dev: Hwmon device for bfperf + * @block_name: Block name + * @block: Block info + * @groups: Attribute groups from each block + * @svc_sreg_support: Whether SMCs are used to access performance registers + * @sreg_tbl_perf: Secure register access table number + * @event_set: Event set to use + */ +struct mlxbf_pmc_context { + struct platform_device *pdev; + u32 total_blocks; + u32 tile_count; + u8 apt_enable; + u8 llt_enable; + u8 mss_enable; + u32 group_num; + struct device *hwmon_dev; + const char *block_name[MLXBF_PMC_MAX_BLOCKS]; + struct mlxbf_pmc_block_info block[MLXBF_PMC_MAX_BLOCKS]; + const struct attribute_group *groups[MLXBF_PMC_MAX_BLOCKS]; + bool svc_sreg_support; + u32 sreg_tbl_perf; + unsigned int event_set; +}; + +/** + * struct mlxbf_pmc_events - Structure to hold supported events for each block + * @evt_num: Event number used to program counters + * @evt_name: Name of the event + */ +struct mlxbf_pmc_events { + u32 evt_num; + char *evt_name; +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_pcie_events[] = { + { 0x0, "IN_P_PKT_CNT" }, + { 0x10, "IN_NP_PKT_CNT" }, + { 0x18, "IN_C_PKT_CNT" }, + { 0x20, "OUT_P_PKT_CNT" }, + { 0x28, "OUT_NP_PKT_CNT" }, + { 0x30, "OUT_C_PKT_CNT" }, + { 0x38, "IN_P_BYTE_CNT" }, + { 0x40, "IN_NP_BYTE_CNT" }, + { 0x48, "IN_C_BYTE_CNT" }, + { 0x50, "OUT_P_BYTE_CNT" }, + { 0x58, "OUT_NP_BYTE_CNT" }, + { 0x60, "OUT_C_BYTE_CNT" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_smgen_events[] = { + { 0x0, "AW_REQ" }, + { 0x1, "AW_BEATS" }, + { 0x2, "AW_TRANS" }, + { 0x3, "AW_RESP" }, + { 0x4, "AW_STL" }, + { 0x5, "AW_LAT" }, + { 0x6, "AW_REQ_TBU" }, + { 0x8, "AR_REQ" }, + { 0x9, "AR_BEATS" }, + { 0xa, "AR_TRANS" }, + { 0xb, "AR_STL" }, + { 0xc, "AR_LAT" }, + { 0xd, "AR_REQ_TBU" }, + { 0xe, "TBU_MISS" }, + { 0xf, "TX_DAT_AF" }, + { 0x10, "RX_DAT_AF" }, + { 0x11, "RETRYQ_CRED" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_trio_events_1[] = { + { 0x0, "DISABLE" }, + { 0xa0, "TPIO_DATA_BEAT" }, + { 0xa1, "TDMA_DATA_BEAT" }, + { 0xa2, "MAP_DATA_BEAT" }, + { 0xa3, "TXMSG_DATA_BEAT" }, + { 0xa4, "TPIO_DATA_PACKET" }, + { 0xa5, "TDMA_DATA_PACKET" }, + { 0xa6, "MAP_DATA_PACKET" }, + { 0xa7, "TXMSG_DATA_PACKET" }, + { 0xa8, "TDMA_RT_AF" }, + { 0xa9, "TDMA_PBUF_MAC_AF" }, + { 0xaa, "TRIO_MAP_WRQ_BUF_EMPTY" }, + { 0xab, "TRIO_MAP_CPL_BUF_EMPTY" }, + { 0xac, "TRIO_MAP_RDQ0_BUF_EMPTY" }, + { 0xad, "TRIO_MAP_RDQ1_BUF_EMPTY" }, + { 0xae, "TRIO_MAP_RDQ2_BUF_EMPTY" }, + { 0xaf, "TRIO_MAP_RDQ3_BUF_EMPTY" }, + { 0xb0, "TRIO_MAP_RDQ4_BUF_EMPTY" }, + { 0xb1, "TRIO_MAP_RDQ5_BUF_EMPTY" }, + { 0xb2, "TRIO_MAP_RDQ6_BUF_EMPTY" }, + { 0xb3, "TRIO_MAP_RDQ7_BUF_EMPTY" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_trio_events_2[] = { + { 0x0, "DISABLE" }, + { 0xa0, "TPIO_DATA_BEAT" }, + { 0xa1, "TDMA_DATA_BEAT" }, + { 0xa2, "MAP_DATA_BEAT" }, + { 0xa3, "TXMSG_DATA_BEAT" }, + { 0xa4, "TPIO_DATA_PACKET" }, + { 0xa5, "TDMA_DATA_PACKET" }, + { 0xa6, "MAP_DATA_PACKET" }, + { 0xa7, "TXMSG_DATA_PACKET" }, + { 0xa8, "TDMA_RT_AF" }, + { 0xa9, "TDMA_PBUF_MAC_AF" }, + { 0xaa, "TRIO_MAP_WRQ_BUF_EMPTY" }, + { 0xab, "TRIO_MAP_CPL_BUF_EMPTY" }, + { 0xac, "TRIO_MAP_RDQ0_BUF_EMPTY" }, + { 0xad, "TRIO_MAP_RDQ1_BUF_EMPTY" }, + { 0xae, "TRIO_MAP_RDQ2_BUF_EMPTY" }, + { 0xaf, "TRIO_MAP_RDQ3_BUF_EMPTY" }, + { 0xb0, "TRIO_MAP_RDQ4_BUF_EMPTY" }, + { 0xb1, "TRIO_MAP_RDQ5_BUF_EMPTY" }, + { 0xb2, "TRIO_MAP_RDQ6_BUF_EMPTY" }, + { 0xb3, "TRIO_MAP_RDQ7_BUF_EMPTY" }, + { 0xb4, "TRIO_RING_TX_FLIT_CH0" }, + { 0xb5, "TRIO_RING_TX_FLIT_CH1" }, + { 0xb6, "TRIO_RING_TX_FLIT_CH2" }, + { 0xb7, "TRIO_RING_TX_FLIT_CH3" }, + { 0xb8, "TRIO_RING_TX_FLIT_CH4" }, + { 0xb9, "TRIO_RING_RX_FLIT_CH0" }, + { 0xba, "TRIO_RING_RX_FLIT_CH1" }, + { 0xbb, "TRIO_RING_RX_FLIT_CH2" }, + { 0xbc, "TRIO_RING_RX_FLIT_CH3" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_ecc_events[] = { + { 0x0, "DISABLE" }, + { 0x100, "ECC_SINGLE_ERROR_CNT" }, + { 0x104, "ECC_DOUBLE_ERROR_CNT" }, + { 0x114, "SERR_INJ" }, + { 0x118, "DERR_INJ" }, + { 0x124, "ECC_SINGLE_ERROR_0" }, + { 0x164, "ECC_DOUBLE_ERROR_0" }, + { 0x340, "DRAM_ECC_COUNT" }, + { 0x344, "DRAM_ECC_INJECT" }, + { 0x348, "DRAM_ECC_ERROR" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_mss_events_1[] = { + { 0x0, "DISABLE" }, + { 0xc0, "RXREQ_MSS" }, + { 0xc1, "RXDAT_MSS" }, + { 0xc2, "TXRSP_MSS" }, + { 0xc3, "TXDAT_MSS" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_mss_events_3[] = { + {0, "SKYLIB_CDN_TX_FLITS"}, + {1, "SKYLIB_DDN_TX_FLITS"}, + {2, "SKYLIB_NDN_TX_FLITS"}, + {3, "SKYLIB_SDN_TX_FLITS"}, + {4, "SKYLIB_UDN_TX_FLITS"}, + {5, "SKYLIB_CDN_RX_FLITS"}, + {6, "SKYLIB_DDN_RX_FLITS"}, + {7, "SKYLIB_NDN_RX_FLITS"}, + {8, "SKYLIB_SDN_RX_FLITS"}, + {9, "SKYLIB_UDN_RX_FLITS"}, + {10, "SKYLIB_CDN_TX_STALL"}, + {11, "SKYLIB_DDN_TX_STALL"}, + {12, "SKYLIB_NDN_TX_STALL"}, + {13, "SKYLIB_SDN_TX_STALL"}, + {14, "SKYLIB_UDN_TX_STALL"}, + {15, "SKYLIB_CDN_RX_STALL"}, + {16, "SKYLIB_DDN_RX_STALL"}, + {17, "SKYLIB_NDN_RX_STALL"}, + {18, "SKYLIB_SDN_RX_STALL"}, + {19, "SKYLIB_UDN_RX_STALL"}, + {20, "SKYLIB_CHI_REQ0_TX_FLITS"}, + {21, "SKYLIB_CHI_DATA0_TX_FLITS"}, + {22, "SKYLIB_CHI_RESP0_TX_FLITS"}, + {23, "SKYLIB_CHI_SNP0_TX_FLITS"}, + {24, "SKYLIB_CHI_REQ1_TX_FLITS"}, + {25, "SKYLIB_CHI_DATA1_TX_FLITS"}, + {26, "SKYLIB_CHI_RESP1_TX_FLITS"}, + {27, "SKYLIB_CHI_SNP1_TX_FLITS"}, + {28, "SKYLIB_CHI_REQ2_TX_FLITS"}, + {29, "SKYLIB_CHI_DATA2_TX_FLITS"}, + {30, "SKYLIB_CHI_RESP2_TX_FLITS"}, + {31, "SKYLIB_CHI_SNP2_TX_FLITS"}, + {32, "SKYLIB_CHI_REQ3_TX_FLITS"}, + {33, "SKYLIB_CHI_DATA3_TX_FLITS"}, + {34, "SKYLIB_CHI_RESP3_TX_FLITS"}, + {35, "SKYLIB_CHI_SNP3_TX_FLITS"}, + {36, "SKYLIB_TLP_REQ_TX_FLITS"}, + {37, "SKYLIB_TLP_RESP_TX_FLITS"}, + {38, "SKYLIB_TLP_META_TX_FLITS"}, + {39, "SKYLIB_AXIS_DATA_TX_FLITS"}, + {40, "SKYLIB_AXIS_CRED_TX_FLITS"}, + {41, "SKYLIB_APB_TX_FLITS"}, + {42, "SKYLIB_VW_TX_FLITS"}, + {43, "SKYLIB_GGA_MSN_W_TX_FLITS"}, + {44, "SKYLIB_GGA_MSN_N_TX_FLITS"}, + {45, "SKYLIB_CR_REQ_TX_FLITS"}, + {46, "SKYLIB_CR_RESP_TX_FLITS"}, + {47, "SKYLIB_MSN_PRNF_TX_FLITS"}, + {48, "SKYLIB_DBG_DATA_TX_FLITS"}, + {49, "SKYLIB_DBG_CRED_TX_FLITS"}, + {50, "SKYLIB_CHI_REQ0_RX_FLITS"}, + {51, "SKYLIB_CHI_DATA0_RX_FLITS"}, + {52, "SKYLIB_CHI_RESP0_RX_FLITS"}, + {53, "SKYLIB_CHI_SNP0_RX_FLITS"}, + {54, "SKYLIB_CHI_REQ1_RX_FLITS"}, + {55, "SKYLIB_CHI_DATA1_RX_FLITS"}, + {56, "SKYLIB_CHI_RESP1_RX_FLITS"}, + {57, "SKYLIB_CHI_SNP1_RX_FLITS"}, + {58, "SKYLIB_CHI_REQ2_RX_FLITS"}, + {59, "SKYLIB_CHI_DATA2_RX_FLITS"}, + {60, "SKYLIB_CHI_RESP2_RX_FLITS"}, + {61, "SKYLIB_CHI_SNP2_RX_FLITS"}, + {62, "SKYLIB_CHI_REQ3_RX_FLITS"}, + {63, "SKYLIB_CHI_DATA3_RX_FLITS"}, + {64, "SKYLIB_CHI_RESP3_RX_FLITS"}, + {65, "SKYLIB_CHI_SNP3_RX_FLITS"}, + {66, "SKYLIB_TLP_REQ_RX_FLITS"}, + {67, "SKYLIB_TLP_RESP_RX_FLITS"}, + {68, "SKYLIB_TLP_META_RX_FLITS"}, + {69, "SKYLIB_AXIS_DATA_RX_FLITS"}, + {70, "SKYLIB_AXIS_CRED_RX_FLITS"}, + {71, "SKYLIB_APB_RX_FLITS"}, + {72, "SKYLIB_VW_RX_FLITS"}, + {73, "SKYLIB_GGA_MSN_W_RX_FLITS"}, + {74, "SKYLIB_GGA_MSN_N_RX_FLITS"}, + {75, "SKYLIB_CR_REQ_RX_FLITS"}, + {76, "SKYLIB_CR_RESP_RX_FLITS"}, + {77, "SKYLIB_MSN_PRNF_RX_FLITS"}, + {78, "SKYLIB_DBG_DATA_RX_FLITS"}, + {79, "SKYLIB_DBG_CRED_RX_FLITS"}, + {80, "SKYLIB_CHI_REQ0_TX_STALL"}, + {81, "SKYLIB_CHI_DATA0_TX_STALL"}, + {82, "SKYLIB_CHI_RESP0_TX_STALL"}, + {83, "SKYLIB_CHI_SNP0_TX_STALL"}, + {84, "SKYLIB_CHI_REQ1_TX_STALL"}, + {85, "SKYLIB_CHI_DATA1_TX_STALL"}, + {86, "SKYLIB_CHI_RESP1_TX_STALL"}, + {87, "SKYLIB_CHI_SNP1_TX_STALL"}, + {88, "SKYLIB_CHI_REQ2_TX_STALL"}, + {89, "SKYLIB_CHI_DATA2_TX_STALL"}, + {90, "SKYLIB_CHI_RESP2_TX_STALL"}, + {91, "SKYLIB_CHI_SNP2_TX_STALL"}, + {92, "SKYLIB_CHI_REQ3_TX_STALL"}, + {93, "SKYLIB_CHI_DATA3_TX_STALL"}, + {94, "SKYLIB_CHI_RESP3_TX_STALL"}, + {95, "SKYLIB_CHI_SNP3_TX_STALL"}, + {96, "SKYLIB_TLP_REQ_TX_STALL"}, + {97, "SKYLIB_TLP_RESP_TX_STALL"}, + {98, "SKYLIB_TLP_META_TX_STALL"}, + {99, "SKYLIB_AXIS_DATA_TX_STALL"}, + {100, "SKYLIB_AXIS_CRED_TX_STALL"}, + {101, "SKYLIB_APB_TX_STALL"}, + {102, "SKYLIB_VW_TX_STALL"}, + {103, "SKYLIB_GGA_MSN_W_TX_STALL"}, + {104, "SKYLIB_GGA_MSN_N_TX_STALL"}, + {105, "SKYLIB_CR_REQ_TX_STALL"}, + {106, "SKYLIB_CR_RESP_TX_STALL"}, + {107, "SKYLIB_MSN_PRNF_TX_STALL"}, + {108, "SKYLIB_DBG_DATA_TX_STALL"}, + {109, "SKYLIB_DBG_CRED_TX_STALL"}, + {110, "SKYLIB_CHI_REQ0_RX_STALL"}, + {111, "SKYLIB_CHI_DATA0_RX_STALL"}, + {112, "SKYLIB_CHI_RESP0_RX_STALL"}, + {113, "SKYLIB_CHI_SNP0_RX_STALL"}, + {114, "SKYLIB_CHI_REQ1_RX_STALL"}, + {115, "SKYLIB_CHI_DATA1_RX_STALL"}, + {116, "SKYLIB_CHI_RESP1_RX_STALL"}, + {117, "SKYLIB_CHI_SNP1_RX_STALL"}, + {118, "SKYLIB_CHI_REQ2_RX_STALL"}, + {119, "SKYLIB_CHI_DATA2_RX_STALL"}, + {120, "SKYLIB_CHI_RESP2_RX_STALL"}, + {121, "SKYLIB_CHI_SNP2_RX_STALL"}, + {122, "SKYLIB_CHI_REQ3_RX_STALL"}, + {123, "SKYLIB_CHI_DATA3_RX_STALL"}, + {124, "SKYLIB_CHI_RESP3_RX_STALL"}, + {125, "SKYLIB_CHI_SNP3_RX_STALL"}, + {126, "SKYLIB_TLP_REQ_RX_STALL"}, + {127, "SKYLIB_TLP_RESP_RX_STALL"}, + {128, "SKYLIB_TLP_META_RX_STALL"}, + {129, "SKYLIB_AXIS_DATA_RX_STALL"}, + {130, "SKYLIB_AXIS_CRED_RX_STALL"}, + {131, "SKYLIB_APB_RX_STALL"}, + {132, "SKYLIB_VW_RX_STALL"}, + {133, "SKYLIB_GGA_MSN_W_RX_STALL"}, + {134, "SKYLIB_GGA_MSN_N_RX_STALL"}, + {135, "SKYLIB_CR_REQ_RX_STALL"}, + {136, "SKYLIB_CR_RESP_RX_STALL"}, + {137, "SKYLIB_MSN_PRNF_RX_STALL"}, + {138, "SKYLIB_DBG_DATA_RX_STALL"}, + {139, "SKYLIB_DBG_CRED_RX_STALL"}, + {140, "SKYLIB_CDN_LOOPBACK_FLITS"}, + {141, "SKYLIB_DDN_LOOPBACK_FLITS"}, + {142, "SKYLIB_NDN_LOOPBACK_FLITS"}, + {143, "SKYLIB_SDN_LOOPBACK_FLITS"}, + {144, "SKYLIB_UDN_LOOPBACK_FLITS"}, + {145, "HISTOGRAM_HISTOGRAM_BIN0"}, + {146, "HISTOGRAM_HISTOGRAM_BIN1"}, + {147, "HISTOGRAM_HISTOGRAM_BIN2"}, + {148, "HISTOGRAM_HISTOGRAM_BIN3"}, + {149, "HISTOGRAM_HISTOGRAM_BIN4"}, + {150, "HISTOGRAM_HISTOGRAM_BIN5"}, + {151, "HISTOGRAM_HISTOGRAM_BIN6"}, + {152, "HISTOGRAM_HISTOGRAM_BIN7"}, + {153, "HISTOGRAM_HISTOGRAM_BIN8"}, + {154, "HISTOGRAM_HISTOGRAM_BIN9"}, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_hnf_events[] = { + { 0x0, "DISABLE" }, + { 0x45, "HNF_REQUESTS" }, + { 0x46, "HNF_REJECTS" }, + { 0x47, "ALL_BUSY" }, + { 0x48, "MAF_BUSY" }, + { 0x49, "MAF_REQUESTS" }, + { 0x4a, "RNF_REQUESTS" }, + { 0x4b, "REQUEST_TYPE" }, + { 0x4c, "MEMORY_READS" }, + { 0x4d, "MEMORY_WRITES" }, + { 0x4e, "VICTIM_WRITE" }, + { 0x4f, "POC_FULL" }, + { 0x50, "POC_FAIL" }, + { 0x51, "POC_SUCCESS" }, + { 0x52, "POC_WRITES" }, + { 0x53, "POC_READS" }, + { 0x54, "FORWARD" }, + { 0x55, "RXREQ_HNF" }, + { 0x56, "RXRSP_HNF" }, + { 0x57, "RXDAT_HNF" }, + { 0x58, "TXREQ_HNF" }, + { 0x59, "TXRSP_HNF" }, + { 0x5a, "TXDAT_HNF" }, + { 0x5b, "TXSNP_HNF" }, + { 0x5c, "INDEX_MATCH" }, + { 0x5d, "A72_ACCESS" }, + { 0x5e, "IO_ACCESS" }, + { 0x5f, "TSO_WRITE" }, + { 0x60, "TSO_CONFLICT" }, + { 0x61, "DIR_HIT" }, + { 0x62, "HNF_ACCEPTS" }, + { 0x63, "REQ_BUF_EMPTY" }, + { 0x64, "REQ_BUF_IDLE_MAF" }, + { 0x65, "TSO_NOARB" }, + { 0x66, "TSO_NOARB_CYCLES" }, + { 0x67, "MSS_NO_CREDIT" }, + { 0x68, "TXDAT_NO_LCRD" }, + { 0x69, "TXSNP_NO_LCRD" }, + { 0x6a, "TXRSP_NO_LCRD" }, + { 0x6b, "TXREQ_NO_LCRD" }, + { 0x6c, "TSO_CL_MATCH" }, + { 0x6d, "MEMORY_READS_BYPASS" }, + { 0x6e, "TSO_NOARB_TIMEOUT" }, + { 0x6f, "ALLOCATE" }, + { 0x70, "VICTIM" }, + { 0x71, "A72_WRITE" }, + { 0x72, "A72_READ" }, + { 0x73, "IO_WRITE" }, + { 0x74, "IO_READ" }, + { 0x75, "TSO_REJECT" }, + { 0x80, "TXREQ_RN" }, + { 0x81, "TXRSP_RN" }, + { 0x82, "TXDAT_RN" }, + { 0x83, "RXSNP_RN" }, + { 0x84, "RXRSP_RN" }, + { 0x85, "RXDAT_RN" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_hnfnet_events[] = { + { 0x0, "DISABLE" }, + { 0x12, "CDN_REQ" }, + { 0x13, "DDN_REQ" }, + { 0x14, "NDN_REQ" }, + { 0x15, "CDN_DIAG_N_OUT_OF_CRED" }, + { 0x16, "CDN_DIAG_S_OUT_OF_CRED" }, + { 0x17, "CDN_DIAG_E_OUT_OF_CRED" }, + { 0x18, "CDN_DIAG_W_OUT_OF_CRED" }, + { 0x19, "CDN_DIAG_C_OUT_OF_CRED" }, + { 0x1a, "CDN_DIAG_N_EGRESS" }, + { 0x1b, "CDN_DIAG_S_EGRESS" }, + { 0x1c, "CDN_DIAG_E_EGRESS" }, + { 0x1d, "CDN_DIAG_W_EGRESS" }, + { 0x1e, "CDN_DIAG_C_EGRESS" }, + { 0x1f, "CDN_DIAG_N_INGRESS" }, + { 0x20, "CDN_DIAG_S_INGRESS" }, + { 0x21, "CDN_DIAG_E_INGRESS" }, + { 0x22, "CDN_DIAG_W_INGRESS" }, + { 0x23, "CDN_DIAG_C_INGRESS" }, + { 0x24, "CDN_DIAG_CORE_SENT" }, + { 0x25, "DDN_DIAG_N_OUT_OF_CRED" }, + { 0x26, "DDN_DIAG_S_OUT_OF_CRED" }, + { 0x27, "DDN_DIAG_E_OUT_OF_CRED" }, + { 0x28, "DDN_DIAG_W_OUT_OF_CRED" }, + { 0x29, "DDN_DIAG_C_OUT_OF_CRED" }, + { 0x2a, "DDN_DIAG_N_EGRESS" }, + { 0x2b, "DDN_DIAG_S_EGRESS" }, + { 0x2c, "DDN_DIAG_E_EGRESS" }, + { 0x2d, "DDN_DIAG_W_EGRESS" }, + { 0x2e, "DDN_DIAG_C_EGRESS" }, + { 0x2f, "DDN_DIAG_N_INGRESS" }, + { 0x30, "DDN_DIAG_S_INGRESS" }, + { 0x31, "DDN_DIAG_E_INGRESS" }, + { 0x32, "DDN_DIAG_W_INGRESS" }, + { 0x33, "DDN_DIAG_C_INGRESS" }, + { 0x34, "DDN_DIAG_CORE_SENT" }, + { 0x35, "NDN_DIAG_N_OUT_OF_CRED" }, + { 0x36, "NDN_DIAG_S_OUT_OF_CRED" }, + { 0x37, "NDN_DIAG_E_OUT_OF_CRED" }, + { 0x38, "NDN_DIAG_W_OUT_OF_CRED" }, + { 0x39, "NDN_DIAG_C_OUT_OF_CRED" }, + { 0x3a, "NDN_DIAG_N_EGRESS" }, + { 0x3b, "NDN_DIAG_S_EGRESS" }, + { 0x3c, "NDN_DIAG_E_EGRESS" }, + { 0x3d, "NDN_DIAG_W_EGRESS" }, + { 0x3e, "NDN_DIAG_C_EGRESS" }, + { 0x3f, "NDN_DIAG_N_INGRESS" }, + { 0x40, "NDN_DIAG_S_INGRESS" }, + { 0x41, "NDN_DIAG_E_INGRESS" }, + { 0x42, "NDN_DIAG_W_INGRESS" }, + { 0x43, "NDN_DIAG_C_INGRESS" }, + { 0x44, "NDN_DIAG_CORE_SENT" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_l3c_events[] = { + { 0x00, "DISABLE" }, + { 0x01, "CYCLES" }, + { 0x02, "TOTAL_RD_REQ_IN" }, + { 0x03, "TOTAL_WR_REQ_IN" }, + { 0x04, "TOTAL_WR_DBID_ACK" }, + { 0x05, "TOTAL_WR_DATA_IN" }, + { 0x06, "TOTAL_WR_COMP" }, + { 0x07, "TOTAL_RD_DATA_OUT" }, + { 0x08, "TOTAL_CDN_REQ_IN_BANK0" }, + { 0x09, "TOTAL_CDN_REQ_IN_BANK1" }, + { 0x0a, "TOTAL_DDN_REQ_IN_BANK0" }, + { 0x0b, "TOTAL_DDN_REQ_IN_BANK1" }, + { 0x0c, "TOTAL_EMEM_RD_RES_IN_BANK0" }, + { 0x0d, "TOTAL_EMEM_RD_RES_IN_BANK1" }, + { 0x0e, "TOTAL_CACHE_RD_RES_IN_BANK0" }, + { 0x0f, "TOTAL_CACHE_RD_RES_IN_BANK1" }, + { 0x10, "TOTAL_EMEM_RD_REQ_BANK0" }, + { 0x11, "TOTAL_EMEM_RD_REQ_BANK1" }, + { 0x12, "TOTAL_EMEM_WR_REQ_BANK0" }, + { 0x13, "TOTAL_EMEM_WR_REQ_BANK1" }, + { 0x14, "TOTAL_RD_REQ_OUT" }, + { 0x15, "TOTAL_WR_REQ_OUT" }, + { 0x16, "TOTAL_RD_RES_IN" }, + { 0x17, "HITS_BANK0" }, + { 0x18, "HITS_BANK1" }, + { 0x19, "MISSES_BANK0" }, + { 0x1a, "MISSES_BANK1" }, + { 0x1b, "ALLOCATIONS_BANK0" }, + { 0x1c, "ALLOCATIONS_BANK1" }, + { 0x1d, "EVICTIONS_BANK0" }, + { 0x1e, "EVICTIONS_BANK1" }, + { 0x1f, "DBID_REJECT" }, + { 0x20, "WRDB_REJECT_BANK0" }, + { 0x21, "WRDB_REJECT_BANK1" }, + { 0x22, "CMDQ_REJECT_BANK0" }, + { 0x23, "CMDQ_REJECT_BANK1" }, + { 0x24, "COB_REJECT_BANK0" }, + { 0x25, "COB_REJECT_BANK1" }, + { 0x26, "TRB_REJECT_BANK0" }, + { 0x27, "TRB_REJECT_BANK1" }, + { 0x28, "TAG_REJECT_BANK0" }, + { 0x29, "TAG_REJECT_BANK1" }, + { 0x2a, "ANY_REJECT_BANK0" }, + { 0x2b, "ANY_REJECT_BANK1" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_llt_events[] = { + {0, "HNF0_CYCLES"}, + {1, "HNF0_REQS_RECEIVED"}, + {2, "HNF0_REQS_PROCESSED"}, + {3, "HNF0_DIR_HIT"}, + {4, "HNF0_DIR_MISS"}, + {5, "HNF0_DIR_RD_ALLOC"}, + {6, "HNF0_DIR_WR_ALLOC"}, + {7, "HNF0_DIR_VICTIM"}, + {8, "HNF0_CL_HAZARD"}, + {9, "HNF0_ALL_HAZARD"}, + {10, "HNF0_PIPE_STALLS"}, + {11, "HNF0_MEM_READS"}, + {12, "HNF0_MEM_WRITES"}, + {13, "HNF0_MEM_ACCESS"}, + {14, "HNF0_DCL_READ"}, + {15, "HNF0_DCL_INVAL"}, + {16, "HNF0_CHI_RXDAT"}, + {17, "HNF0_CHI_RXRSP"}, + {18, "HNF0_CHI_TXDAT"}, + {19, "HNF0_CHI_TXRSP"}, + {20, "HNF0_CHI_TXSNP"}, + {21, "HNF0_DCT_SNP"}, + {22, "HNF0_SNP_FWD_DATA"}, + {23, "HNF0_SNP_FWD_RSP"}, + {24, "HNF0_SNP_RSP"}, + {25, "HNF0_EXCL_FULL"}, + {26, "HNF0_EXCL_WRITE_F"}, + {27, "HNF0_EXCL_WRITE_S"}, + {28, "HNF0_EXCL_WRITE"}, + {29, "HNF0_EXCL_READ"}, + {30, "HNF0_REQ_BUF_EMPTY"}, + {31, "HNF0_ALL_MAFS_BUSY"}, + {32, "HNF0_TXDAT_NO_LCRD"}, + {33, "HNF0_TXSNP_NO_LCRD"}, + {34, "HNF0_TXRSP_NO_LCRD"}, + {35, "HNF0_TXREQ_NO_LCRD"}, + {36, "HNF0_WRITE"}, + {37, "HNF0_READ"}, + {38, "HNF0_ACCESS"}, + {39, "HNF0_MAF_N_BUSY"}, + {40, "HNF0_MAF_N_REQS"}, + {41, "HNF0_SEL_OPCODE"}, + {42, "HNF1_CYCLES"}, + {43, "HNF1_REQS_RECEIVED"}, + {44, "HNF1_REQS_PROCESSED"}, + {45, "HNF1_DIR_HIT"}, + {46, "HNF1_DIR_MISS"}, + {47, "HNF1_DIR_RD_ALLOC"}, + {48, "HNF1_DIR_WR_ALLOC"}, + {49, "HNF1_DIR_VICTIM"}, + {50, "HNF1_CL_HAZARD"}, + {51, "HNF1_ALL_HAZARD"}, + {52, "HNF1_PIPE_STALLS"}, + {53, "HNF1_MEM_READS"}, + {54, "HNF1_MEM_WRITES"}, + {55, "HNF1_MEM_ACCESS"}, + {56, "HNF1_DCL_READ"}, + {57, "HNF1_DCL_INVAL"}, + {58, "HNF1_CHI_RXDAT"}, + {59, "HNF1_CHI_RXRSP"}, + {60, "HNF1_CHI_TXDAT"}, + {61, "HNF1_CHI_TXRSP"}, + {62, "HNF1_CHI_TXSNP"}, + {63, "HNF1_DCT_SNP"}, + {64, "HNF1_SNP_FWD_DATA"}, + {65, "HNF1_SNP_FWD_RSP"}, + {66, "HNF1_SNP_RSP"}, + {67, "HNF1_EXCL_FULL"}, + {68, "HNF1_EXCL_WRITE_F"}, + {69, "HNF1_EXCL_WRITE_S"}, + {70, "HNF1_EXCL_WRITE"}, + {71, "HNF1_EXCL_READ"}, + {72, "HNF1_REQ_BUF_EMPTY"}, + {73, "HNF1_ALL_MAFS_BUSY"}, + {74, "HNF1_TXDAT_NO_LCRD"}, + {75, "HNF1_TXSNP_NO_LCRD"}, + {76, "HNF1_TXRSP_NO_LCRD"}, + {77, "HNF1_TXREQ_NO_LCRD"}, + {78, "HNF1_WRITE"}, + {79, "HNF1_READ"}, + {80, "HNF1_ACCESS"}, + {81, "HNF1_MAF_N_BUSY"}, + {82, "HNF1_MAF_N_REQS"}, + {83, "HNF1_SEL_OPCODE"}, + {84, "GDC_BANK0_RD_REQ"}, + {85, "GDC_BANK0_WR_REQ"}, + {86, "GDC_BANK0_ALLOCATE"}, + {87, "GDC_BANK0_HIT"}, + {88, "GDC_BANK0_MISS"}, + {89, "GDC_BANK0_INVALIDATE"}, + {90, "GDC_BANK0_EVICT"}, + {91, "GDC_BANK0_RD_RESP"}, + {92, "GDC_BANK0_WR_ACK"}, + {93, "GDC_BANK0_SNOOP"}, + {94, "GDC_BANK0_SNOOP_NORMAL"}, + {95, "GDC_BANK0_SNOOP_FWD"}, + {96, "GDC_BANK0_SNOOP_STASH"}, + {97, "GDC_BANK0_SNOOP_STASH_INDPND_RD"}, + {98, "GDC_BANK0_FOLLOWER"}, + {99, "GDC_BANK0_FW"}, + {100, "GDC_BANK0_HIT_DCL_BOTH"}, + {101, "GDC_BANK0_HIT_DCL_PARTIAL"}, + {102, "GDC_BANK0_EVICT_DCL"}, + {103, "GDC_BANK0_G_RSE_PIPE_CACHE_DATA0"}, + {104, "GDC_BANK0_G_RSE_PIPE_CACHE_DATA1"}, + {105, "GDC_BANK0_ARB_STRB"}, + {106, "GDC_BANK0_ARB_WAIT"}, + {107, "GDC_BANK0_GGA_STRB"}, + {108, "GDC_BANK0_GGA_WAIT"}, + {109, "GDC_BANK0_FW_STRB"}, + {110, "GDC_BANK0_FW_WAIT"}, + {111, "GDC_BANK0_SNP_STRB"}, + {112, "GDC_BANK0_SNP_WAIT"}, + {113, "GDC_BANK0_MISS_INARB_STRB"}, + {114, "GDC_BANK0_MISS_INARB_WAIT"}, + {115, "GDC_BANK0_G_FIFO_FF_GGA_RSP_RD0"}, + {116, "GDC_BANK0_G_FIFO_FF_GGA_RSP_RD1"}, + {117, "GDC_BANK0_G_FIFO_FF_GGA_RSP_RD2"}, + {118, "GDC_BANK0_G_FIFO_FF_GGA_RSP_RD3"}, + {119, "GDC_BANK0_G_FIFO_FF_GGA_RSP_WR0"}, + {120, "GDC_BANK0_G_FIFO_FF_GGA_RSP_WR1"}, + {121, "GDC_BANK0_G_FIFO_FF_GGA_RSP_WR2"}, + {122, "GDC_BANK0_G_FIFO_FF_GGA_RSP_WR3"}, + {123, "GDC_BANK1_RD_REQ"}, + {124, "GDC_BANK1_WR_REQ"}, + {125, "GDC_BANK1_ALLOCATE"}, + {126, "GDC_BANK1_HIT"}, + {127, "GDC_BANK1_MISS"}, + {128, "GDC_BANK1_INVALIDATE"}, + {129, "GDC_BANK1_EVICT"}, + {130, "GDC_BANK1_RD_RESP"}, + {131, "GDC_BANK1_WR_ACK"}, + {132, "GDC_BANK1_SNOOP"}, + {133, "GDC_BANK1_SNOOP_NORMAL"}, + {134, "GDC_BANK1_SNOOP_FWD"}, + {135, "GDC_BANK1_SNOOP_STASH"}, + {136, "GDC_BANK1_SNOOP_STASH_INDPND_RD"}, + {137, "GDC_BANK1_FOLLOWER"}, + {138, "GDC_BANK1_FW"}, + {139, "GDC_BANK1_HIT_DCL_BOTH"}, + {140, "GDC_BANK1_HIT_DCL_PARTIAL"}, + {141, "GDC_BANK1_EVICT_DCL"}, + {142, "GDC_BANK1_G_RSE_PIPE_CACHE_DATA0"}, + {143, "GDC_BANK1_G_RSE_PIPE_CACHE_DATA1"}, + {144, "GDC_BANK1_ARB_STRB"}, + {145, "GDC_BANK1_ARB_WAIT"}, + {146, "GDC_BANK1_GGA_STRB"}, + {147, "GDC_BANK1_GGA_WAIT"}, + {148, "GDC_BANK1_FW_STRB"}, + {149, "GDC_BANK1_FW_WAIT"}, + {150, "GDC_BANK1_SNP_STRB"}, + {151, "GDC_BANK1_SNP_WAIT"}, + {152, "GDC_BANK1_MISS_INARB_STRB"}, + {153, "GDC_BANK1_MISS_INARB_WAIT"}, + {154, "GDC_BANK1_G_FIFO_FF_GGA_RSP_RD0"}, + {155, "GDC_BANK1_G_FIFO_FF_GGA_RSP_RD1"}, + {156, "GDC_BANK1_G_FIFO_FF_GGA_RSP_RD2"}, + {157, "GDC_BANK1_G_FIFO_FF_GGA_RSP_RD3"}, + {158, "GDC_BANK1_G_FIFO_FF_GGA_RSP_WR0"}, + {159, "GDC_BANK1_G_FIFO_FF_GGA_RSP_WR1"}, + {160, "GDC_BANK1_G_FIFO_FF_GGA_RSP_WR2"}, + {161, "GDC_BANK1_G_FIFO_FF_GGA_RSP_WR3"}, + {162, "HISTOGRAM_HISTOGRAM_BIN0"}, + {163, "HISTOGRAM_HISTOGRAM_BIN1"}, + {164, "HISTOGRAM_HISTOGRAM_BIN2"}, + {165, "HISTOGRAM_HISTOGRAM_BIN3"}, + {166, "HISTOGRAM_HISTOGRAM_BIN4"}, + {167, "HISTOGRAM_HISTOGRAM_BIN5"}, + {168, "HISTOGRAM_HISTOGRAM_BIN6"}, + {169, "HISTOGRAM_HISTOGRAM_BIN7"}, + {170, "HISTOGRAM_HISTOGRAM_BIN8"}, + {171, "HISTOGRAM_HISTOGRAM_BIN9"}, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_llt_miss_events[] = { + {0, "GDC_MISS_MACHINE_RD_REQ"}, + {1, "GDC_MISS_MACHINE_WR_REQ"}, + {2, "GDC_MISS_MACHINE_SNP_REQ"}, + {3, "GDC_MISS_MACHINE_EVICT_REQ"}, + {4, "GDC_MISS_MACHINE_FW_REQ"}, + {5, "GDC_MISS_MACHINE_RD_RESP"}, + {6, "GDC_MISS_MACHINE_WR_RESP"}, + {7, "GDC_MISS_MACHINE_SNP_STASH_DATAPULL_DROP"}, + {8, "GDC_MISS_MACHINE_SNP_STASH_DATAPULL_DROP_TXDAT"}, + {9, "GDC_MISS_MACHINE_CHI_TXREQ"}, + {10, "GDC_MISS_MACHINE_CHI_RXRSP"}, + {11, "GDC_MISS_MACHINE_CHI_TXDAT"}, + {12, "GDC_MISS_MACHINE_CHI_RXDAT"}, + {13, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC0_0"}, + {14, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC0_1"}, + {15, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC0_2"}, + {16, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC0_3"}, + {17, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC1_0"}, + {18, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC1_1"}, + {19, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC1_2"}, + {20, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC1_3"}, + {21, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE0_0"}, + {22, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE0_1"}, + {23, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE0_2"}, + {24, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE0_3"}, + {25, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE1_0"}, + {26, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE1_1"}, + {27, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE1_2"}, + {28, "GDC_MISS_MACHINE_G_FIFO_FF_EXEC_DONE1_3"}, + {29, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_0"}, + {30, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_1"}, + {31, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_2"}, + {32, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_3"}, + {33, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_4"}, + {34, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_5"}, + {35, "GDC_MISS_MACHINE_GDC_LINK_LIST_FF_6"}, + {36, "GDC_MISS_MACHINE_G_RSE_PIPE_TXREQ_0"}, + {37, "GDC_MISS_MACHINE_G_RSE_PIPE_TXREQ_1"}, + {38, "GDC_MISS_MACHINE_G_CREDIT_TXREQ_0"}, + {39, "GDC_MISS_MACHINE_G_CREDIT_TXREQ_1"}, + {40, "GDC_MISS_MACHINE_G_RSE_PIPE_TXDAT_0"}, + {41, "GDC_MISS_MACHINE_G_RSE_PIPE_TXDAT_1"}, + {42, "GDC_MISS_MACHINE_G_CREDIT_TXDAT_0"}, + {43, "GDC_MISS_MACHINE_G_CREDIT_TXDAT_1"}, + {44, "GDC_MISS_MACHINE_G_FIFO_FF_COMPACK_0"}, + {45, "GDC_MISS_MACHINE_G_FIFO_FF_COMPACK_1"}, + {46, "GDC_MISS_MACHINE_G_FIFO_FF_COMPACK_2"}, + {47, "GDC_MISS_MACHINE_G_FIFO_FF_COMPACK_3"}, + {48, "GDC_MISS_MACHINE_G_RSE_PIPE_TXRSP_0"}, + {49, "GDC_MISS_MACHINE_G_RSE_PIPE_TXRSP_1"}, + {50, "GDC_MISS_MACHINE_G_CREDIT_TXRSP_0"}, + {51, "GDC_MISS_MACHINE_G_CREDIT_TXRSP_1"}, + {52, "GDC_MISS_MACHINE_G_RSE_PIPE_INARB_0"}, + {53, "GDC_MISS_MACHINE_G_RSE_PIPE_INARB_1"}, + {54, "GDC_MISS_MACHINE_G_FIFO_FF_SNOOP_IN_0"}, + {55, "GDC_MISS_MACHINE_G_FIFO_FF_SNOOP_IN_1"}, + {56, "GDC_MISS_MACHINE_G_FIFO_FF_SNOOP_IN_2"}, + {57, "GDC_MISS_MACHINE_G_FIFO_FF_SNOOP_IN_3"}, + {58, "GDC_MISS_MACHINE_G_FIFO_FF_TXRSP_SNOOP_DATAPULL_0"}, + {59, "GDC_MISS_MACHINE_G_FIFO_FF_TXRSP_SNOOP_DATAPULL_1"}, + {60, "GDC_MISS_MACHINE_G_FIFO_FF_TXRSP_SNOOP_DATAPULL_2"}, + {61, "GDC_MISS_MACHINE_G_FIFO_FF_TXRSP_SNOOP_DATAPULL_3"}, + {62, "GDC_MISS_MACHINE_G_FIFO_FF_TXDAT_SNOOP_DATAPULL_4"}, + {63, "GDC_MISS_MACHINE_G_FIFO_FF_TXDAT_SNOOP_DATAPULL_5"}, + {64, "GDC_MISS_MACHINE_G_FIFO_FF_TXDAT_SNOOP_DATAPULL_6"}, + {65, "GDC_MISS_MACHINE_G_FIFO_FF_TXDAT_SNOOP_DATAPULL_7"}, + {66, "HISTOGRAM_HISTOGRAM_BIN0"}, + {67, "HISTOGRAM_HISTOGRAM_BIN1"}, + {68, "HISTOGRAM_HISTOGRAM_BIN2"}, + {69, "HISTOGRAM_HISTOGRAM_BIN3"}, + {70, "HISTOGRAM_HISTOGRAM_BIN4"}, + {71, "HISTOGRAM_HISTOGRAM_BIN5"}, + {72, "HISTOGRAM_HISTOGRAM_BIN6"}, + {73, "HISTOGRAM_HISTOGRAM_BIN7"}, + {74, "HISTOGRAM_HISTOGRAM_BIN8"}, + {75, "HISTOGRAM_HISTOGRAM_BIN9"}, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_clock_events[] = { + { 0x0, "FMON_CLK_LAST_COUNT_PLL_D1_INST0" }, + { 0x4, "REFERENCE_WINDOW_WIDTH_PLL_D1_INST0" }, + { 0x8, "FMON_CLK_LAST_COUNT_PLL_D1_INST1" }, + { 0xc, "REFERENCE_WINDOW_WIDTH_PLL_D1_INST1" }, + { 0x10, "FMON_CLK_LAST_COUNT_PLL_G1" }, + { 0x14, "REFERENCE_WINDOW_WIDTH_PLL_G1" }, + { 0x18, "FMON_CLK_LAST_COUNT_PLL_W1" }, + { 0x1c, "REFERENCE_WINDOW_WIDTH_PLL_W1" }, + { 0x20, "FMON_CLK_LAST_COUNT_PLL_T1" }, + { 0x24, "REFERENCE_WINDOW_WIDTH_PLL_T1" }, + { 0x28, "FMON_CLK_LAST_COUNT_PLL_A0" }, + { 0x2c, "REFERENCE_WINDOW_WIDTH_PLL_A0" }, + { 0x30, "FMON_CLK_LAST_COUNT_PLL_C0" }, + { 0x34, "REFERENCE_WINDOW_WIDTH_PLL_C0" }, + { 0x38, "FMON_CLK_LAST_COUNT_PLL_N1" }, + { 0x3c, "REFERENCE_WINDOW_WIDTH_PLL_N1" }, + { 0x40, "FMON_CLK_LAST_COUNT_PLL_I1" }, + { 0x44, "REFERENCE_WINDOW_WIDTH_PLL_I1" }, + { 0x48, "FMON_CLK_LAST_COUNT_PLL_R1" }, + { 0x4c, "REFERENCE_WINDOW_WIDTH_PLL_R1" }, + { 0x50, "FMON_CLK_LAST_COUNT_PLL_P1" }, + { 0x54, "REFERENCE_WINDOW_WIDTH_PLL_P1" }, + { 0x58, "FMON_CLK_LAST_COUNT_REF_100_INST0" }, + { 0x5c, "REFERENCE_WINDOW_WIDTH_REF_100_INST0" }, + { 0x60, "FMON_CLK_LAST_COUNT_REF_100_INST1" }, + { 0x64, "REFERENCE_WINDOW_WIDTH_REF_100_INST1" }, + { 0x68, "FMON_CLK_LAST_COUNT_REF_156" }, + { 0x6c, "REFERENCE_WINDOW_WIDTH_REF_156" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_gga_events[] = { + { 0, "GGA_PERF_DESC_WQE_STRB" }, + { 5, "GGA_PERF_DESC_CQE_STRB" }, + { 8, "GGA_PERF_DESC_TPT_REQUEST_STRB" }, + { 17, "GGA_PERF_DESC_TPT_RESPONSESTRB" }, + { 120, "GGA_PERF_DESC_ENGINE0_IN_DATA_STRB" }, + { 121, "GGA_PERF_DESC_ENGINE1_IN_DATA_STRB" }, + { 122, "GGA_PERF_DESC_ENGINE2_IN_DATA_STRB" }, + { 123, "GGA_PERF_DESC_ENGINE3_IN_DATA_STRB" }, + { 124, "GGA_PERF_DESC_ENGINE4_IN_DATA_STRB" }, + { 125, "GGA_PERF_DESC_ENGINE5_IN_DATA_STRB" }, + { 126, "GGA_PERF_DESC_ENGINE6_IN_DATA_STRB" }, + { 127, "GGA_PERF_DESC_ENGINE7_IN_DATA_STRB" }, + { 128, "GGA_PERF_DESC_ENGINE8_IN_DATA_STRB" }, + { 129, "GGA_PERF_DESC_ENGINE9_IN_DATA_STRB" }, + { 130, "GGA_PERF_DESC_ENGINE10_IN_DATA_STRB" }, + { 131, "GGA_PERF_DESC_ENGINE11_IN_DATA_STRB" }, + { 132, "GGA_PERF_DESC_ENGINE12_IN_DATA_STRB" }, + { 133, "GGA_PERF_DESC_ENGINE13_IN_DATA_STRB" }, + { 134, "GGA_PERF_DESC_ENGINE14_IN_DATA_STRB" }, + { 195, "GGA_PERF_DESC_ENGINE0_OUT_DATA_STRB" }, + { 196, "GGA_PERF_DESC_ENGINE1_OUT_DATA_STRB" }, + { 197, "GGA_PERF_DESC_ENGINE2_OUT_DATA_STRB" }, + { 198, "GGA_PERF_DESC_ENGINE3_OUT_DATA_STRB" }, + { 199, "GGA_PERF_DESC_ENGINE4_OUT_DATA_STRB" }, + { 200, "GGA_PERF_DESC_ENGINE5_OUT_DATA_STRB" }, + { 201, "GGA_PERF_DESC_ENGINE6_OUT_DATA_STRB" }, + { 202, "GGA_PERF_DESC_ENGINE7_OUT_DATA_STRB" }, + { 203, "GGA_PERF_DESC_ENGINE8_OUT_DATA_STRB" }, + { 204, "GGA_PERF_DESC_ENGINE9_OUT_DATA_STRB" }, + { 205, "GGA_PERF_DESC_ENGINE10_OUT_DATA_STRB" }, + { 206, "GGA_PERF_DESC_ENGINE11_OUT_DATA_STRB" }, + { 207, "GGA_PERF_DESC_ENGINE12_OUT_DATA_STRB" }, + { 208, "GGA_PERF_DESC_ENGINE13_OUT_DATA_STRB" }, + { 209, "GGA_PERF_DESC_ENGINE14_OUT_DATA_STRB" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_apt_events[] = { + { 0, "APT_DATA_0" }, + { 1, "APT_DATA_1" }, + { 2, "APT_DATA_2" }, + { 3, "APT_DATA_3" }, + { 4, "APT_DATA_4" }, + { 5, "APT_DATA_5" }, + { 6, "APT_DATA_6" }, + { 7, "APT_DATA_7" }, + { 8, "APT_DATA_8" }, + { 9, "APT_DATA_9" }, + { 10, "APT_DATA_10" }, + { 11, "APT_DATA_11" }, + { 12, "APT_DATA_12" }, + { 13, "APT_DATA_13" }, + { 14, "APT_DATA_14" }, + { 15, "APT_DATA_15" }, + { 16, "APT_DATA_16" }, + { 17, "APT_DATA_17" }, + { 18, "APT_DATA_18" }, + { 19, "APT_DATA_19" }, + { 20, "APT_DATA_20" }, + { 21, "APT_DATA_21" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_emi_events[] = { + { 0, "MCH_WR_IN_MCH_REQ_IN_STRB" }, + { 10, "MCH_RD_IN_MCH_REQ_IN_STRB" }, + { 20, "MCH_RD_RESP_DATA_MCH_RESP_OUT_STRB" }, + { 98, "EMI_ARBITER_EARB2CTRL_STRB" }, + { 99, "EMI_ARBITER_EARB2CTRL_RAS_STRB" }, + { 100, "EMI_ARBITER_EARB2CTRL_CAS_STRB" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_prnf_events[] = { + { 0, "PRNF_DMA_RD_TLP_REQ" }, + { 1, "PRNF_DMA_RD_ICMC_BYPASS_REQ" }, + { 8, "PRNF_DMA_RD_TLP_SENT_TO_CHI" }, + { 11, "PRNF_DMA_RD_CHI_RES" }, + { 17, "PRNF_DMA_RD_TLP_RES_SENT" }, + { 18, "PRNF_DMA_WR_WR0_SLICE_ALLOC_RO" }, + { 19, "PRNF_DMA_WR_WR0_SLICE_ALLOC_NRO" }, + { 24, "PRNF_DMA_WR_WR1_SLICE_ALLOC_RO" }, + { 25, "PRNF_DMA_WR_WR1_SLICE_ALLOC_NRO" }, + { 30, "PRNF_PIO_POSTED_REQ_PUSH" }, + { 31, "PRNF_PIO_POSTED_REQ_POP" }, + { 32, "PRNF_PIO_NP_REQ_PUSH" }, + { 33, "PRNF_PIO_NP_REQ_POP" }, + { 34, "PRNF_PIO_COMP_RO_PUSH" }, + { 35, "PRNF_PIO_COMP_RO_POP" }, + { 36, "PRNF_PIO_COMP_NRO_PUSH" }, + { 37, "PRNF_PIO_COMP_NRO_POP" }, +}; + +static const struct mlxbf_pmc_events mlxbf_pmc_msn_events[] = { + { 46, "MSN_CORE_MMA_WQE_DONE_PUSH_STRB" }, + { 116, "MSN_CORE_MSN2MMA_WQE_STRB" }, + { 164, "MSN_CORE_WQE_TOP_TILE_WQE_STRB" }, + { 168, "MSN_CORE_TPT_TOP_GGA_REQ_STRB" }, + { 171, "MSN_CORE_TPT_TOP_MMA_REQ_STRB" }, + { 174, "MSN_CORE_TPT_TOP_GGA_RES_STRB" }, + { 177, "MSN_CORE_TPT_TOP_MMA_RES_STRB" }, +}; + +static struct mlxbf_pmc_context *pmc; + +/* UUID used to probe ATF service. */ +static const char *mlxbf_pmc_svc_uuid_str = "89c036b4-e7d7-11e6-8797-001aca00bfc4"; + +/* Calls an SMC to access a performance register */ +static int mlxbf_pmc_secure_read(void __iomem *addr, u32 command, u64 *result) +{ + struct arm_smccc_res res; + int status, err = 0; + + arm_smccc_smc(command, pmc->sreg_tbl_perf, (uintptr_t)addr, 0, 0, 0, 0, + 0, &res); + + status = res.a0; + + switch (status) { + case PSCI_RET_NOT_SUPPORTED: + err = -EINVAL; + break; + case MLXBF_PMC_SMCCC_ACCESS_VIOLATION: + err = -EACCES; + break; + default: + *result = res.a1; + break; + } + + return err; +} + +/* Read from a performance counter */ +static int mlxbf_pmc_read(void __iomem *addr, u32 command, u64 *result) +{ + if (pmc->svc_sreg_support) + return mlxbf_pmc_secure_read(addr, command, result); + + if (command == MLXBF_PMC_READ_REG_32) + *result = readl(addr); + else + *result = readq(addr); + + return 0; +} + +/* Convenience function for 32-bit reads */ +static int mlxbf_pmc_readl(void __iomem *addr, u32 *result) +{ + u64 read_out; + int status; + + status = mlxbf_pmc_read(addr, MLXBF_PMC_READ_REG_32, &read_out); + if (status) + return status; + *result = (u32)read_out; + + return 0; +} + +/* Calls an SMC to access a performance register */ +static int mlxbf_pmc_secure_write(void __iomem *addr, u32 command, u64 value) +{ + struct arm_smccc_res res; + int status, err = 0; + + arm_smccc_smc(command, pmc->sreg_tbl_perf, value, (uintptr_t)addr, 0, 0, + 0, 0, &res); + + status = res.a0; + + switch (status) { + case PSCI_RET_NOT_SUPPORTED: + err = -EINVAL; + break; + case MLXBF_PMC_SMCCC_ACCESS_VIOLATION: + err = -EACCES; + break; + } + + return err; +} + +/* Write to a performance counter */ +static int mlxbf_pmc_write(void __iomem *addr, int command, u64 value) +{ + if (pmc->svc_sreg_support) + return mlxbf_pmc_secure_write(addr, command, value); + + if (command == MLXBF_PMC_WRITE_REG_32) + writel(value, addr); + else + writeq(value, addr); + + return 0; +} + +/* Check if the register offset is within the mapped region for the block */ +static bool mlxbf_pmc_valid_range(unsigned int blk_num, u32 offset) +{ + if ((offset >= 0) && !(offset % MLXBF_PMC_REG_SIZE) && + (offset + MLXBF_PMC_REG_SIZE <= pmc->block[blk_num].blk_size)) + return true; /* inside the mapped PMC space */ + + return false; +} + +/* Get the event list corresponding to a certain block */ +static const struct mlxbf_pmc_events *mlxbf_pmc_event_list(const char *blk, size_t *psize) +{ + const struct mlxbf_pmc_events *events; + size_t size; + + if (strstr(blk, "tilenet")) { + events = mlxbf_pmc_hnfnet_events; + size = ARRAY_SIZE(mlxbf_pmc_hnfnet_events); + } else if (strstr(blk, "tile")) { + events = mlxbf_pmc_hnf_events; + size = ARRAY_SIZE(mlxbf_pmc_hnf_events); + } else if (strstr(blk, "triogen")) { + events = mlxbf_pmc_smgen_events; + size = ARRAY_SIZE(mlxbf_pmc_smgen_events); + } else if (strstr(blk, "trio")) { + switch (pmc->event_set) { + case MLXBF_PMC_EVENT_SET_BF1: + events = mlxbf_pmc_trio_events_1; + size = ARRAY_SIZE(mlxbf_pmc_trio_events_1); + break; + case MLXBF_PMC_EVENT_SET_BF2: + events = mlxbf_pmc_trio_events_2; + size = ARRAY_SIZE(mlxbf_pmc_trio_events_2); + break; + default: + events = NULL; + size = 0; + break; + } + } else if (strstr(blk, "mss")) { + switch (pmc->event_set) { + case MLXBF_PMC_EVENT_SET_BF1: + case MLXBF_PMC_EVENT_SET_BF2: + events = mlxbf_pmc_mss_events_1; + size = ARRAY_SIZE(mlxbf_pmc_mss_events_1); + break; + case MLXBF_PMC_EVENT_SET_BF3: + events = mlxbf_pmc_mss_events_3; + size = ARRAY_SIZE(mlxbf_pmc_mss_events_3); + break; + default: + events = NULL; + size = 0; + break; + } + } else if (strstr(blk, "ecc")) { + events = mlxbf_pmc_ecc_events; + size = ARRAY_SIZE(mlxbf_pmc_ecc_events); + } else if (strstr(blk, "pcie")) { + events = mlxbf_pmc_pcie_events; + size = ARRAY_SIZE(mlxbf_pmc_pcie_events); + } else if (strstr(blk, "l3cache")) { + events = mlxbf_pmc_l3c_events; + size = ARRAY_SIZE(mlxbf_pmc_l3c_events); + } else if (strstr(blk, "gic")) { + events = mlxbf_pmc_smgen_events; + size = ARRAY_SIZE(mlxbf_pmc_smgen_events); + } else if (strstr(blk, "smmu")) { + events = mlxbf_pmc_smgen_events; + size = ARRAY_SIZE(mlxbf_pmc_smgen_events); + } else if (strstr(blk, "llt_miss")) { + events = mlxbf_pmc_llt_miss_events; + size = ARRAY_SIZE(mlxbf_pmc_llt_miss_events); + } else if (strstr(blk, "llt")) { + events = mlxbf_pmc_llt_events; + size = ARRAY_SIZE(mlxbf_pmc_llt_events); + } else if (strstr(blk, "clock_measure")) { + events = mlxbf_pmc_clock_events; + size = ARRAY_SIZE(mlxbf_pmc_clock_events); + } else if (strstr(blk, "gga")) { + events = mlxbf_pmc_gga_events; + size = ARRAY_SIZE(mlxbf_pmc_gga_events); + } else if (strstr(blk, "apt")) { + events = mlxbf_pmc_apt_events; + size = ARRAY_SIZE(mlxbf_pmc_apt_events); + } else if (strstr(blk, "emi")) { + events = mlxbf_pmc_emi_events; + size = ARRAY_SIZE(mlxbf_pmc_emi_events); + } else if (strstr(blk, "prnf")) { + events = mlxbf_pmc_prnf_events; + size = ARRAY_SIZE(mlxbf_pmc_prnf_events); + } else if (strstr(blk, "msn")) { + events = mlxbf_pmc_msn_events; + size = ARRAY_SIZE(mlxbf_pmc_msn_events); + } else { + events = NULL; + size = 0; + } + + if (psize) + *psize = size; + + return events; +} + +static bool mlxbf_pmc_event_supported(const char *blk) +{ + return !!mlxbf_pmc_event_list(blk, NULL); +} + +/* Get the event number given the name */ +static int mlxbf_pmc_get_event_num(const char *blk, const char *evt) +{ + const struct mlxbf_pmc_events *events; + unsigned int i; + size_t size; + + events = mlxbf_pmc_event_list(blk, &size); + if (!events) + return -EINVAL; + + for (i = 0; i < size; ++i) { + if (!strcmp(evt, events[i].evt_name)) + return events[i].evt_num; + } + + return -ENODEV; +} + +/* Get the event name given the number */ +static char *mlxbf_pmc_get_event_name(const char *blk, u32 evt) +{ + const struct mlxbf_pmc_events *events; + unsigned int i; + size_t size; + + events = mlxbf_pmc_event_list(blk, &size); + if (!events) + return NULL; + + for (i = 0; i < size; ++i) { + if (evt == events[i].evt_num) + return events[i].evt_name; + } + + return NULL; +} + +/* Method to enable/disable/reset l3cache counters */ +static int mlxbf_pmc_config_l3_counters(unsigned int blk_num, bool enable, bool reset) +{ + u32 perfcnt_cfg = 0; + + if (enable) + perfcnt_cfg |= MLXBF_PMC_L3C_PERF_CNT_CFG_EN; + if (reset) + perfcnt_cfg |= MLXBF_PMC_L3C_PERF_CNT_CFG_RST; + + return mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_CFG, + MLXBF_PMC_WRITE_REG_32, perfcnt_cfg); +} + +/* Method to handle l3cache counter programming */ +static int mlxbf_pmc_program_l3_counter(unsigned int blk_num, u32 cnt_num, u32 evt) +{ + u32 perfcnt_sel_1 = 0, perfcnt_sel = 0, *wordaddr; + void __iomem *pmcaddr; + int ret; + + /* Disable all counters before programming them */ + if (mlxbf_pmc_config_l3_counters(blk_num, false, false)) + return -EINVAL; + + /* Select appropriate register information */ + switch (cnt_num) { + case 0 ... 3: + pmcaddr = pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_SEL; + wordaddr = &perfcnt_sel; + break; + case 4: + pmcaddr = pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_SEL_1; + wordaddr = &perfcnt_sel_1; + break; + default: + return -EINVAL; + } + + ret = mlxbf_pmc_readl(pmcaddr, wordaddr); + if (ret) + return ret; + + switch (cnt_num) { + case 0: + perfcnt_sel &= ~MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_0; + perfcnt_sel |= FIELD_PREP(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_0, + evt); + break; + case 1: + perfcnt_sel &= ~MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_1; + perfcnt_sel |= FIELD_PREP(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_1, + evt); + break; + case 2: + perfcnt_sel &= ~MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_2; + perfcnt_sel |= FIELD_PREP(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_2, + evt); + break; + case 3: + perfcnt_sel &= ~MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_3; + perfcnt_sel |= FIELD_PREP(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_3, + evt); + break; + case 4: + perfcnt_sel_1 &= ~MLXBF_PMC_L3C_PERF_CNT_SEL_1_CNT_4; + perfcnt_sel_1 |= FIELD_PREP(MLXBF_PMC_L3C_PERF_CNT_SEL_1_CNT_4, + evt); + break; + default: + return -EINVAL; + } + + return mlxbf_pmc_write(pmcaddr, MLXBF_PMC_WRITE_REG_32, *wordaddr); +} + +/* Method to handle crspace counter programming */ +static int mlxbf_pmc_program_crspace_counter(unsigned int blk_num, u32 cnt_num, u32 evt) +{ + void __iomem *addr; + u32 word; + int ret; + + addr = pmc->block[blk_num].mmio_base + + ((cnt_num / 2) * MLXBF_PMC_CRSPACE_PERFSEL_SZ); + ret = mlxbf_pmc_readl(addr, &word); + if (ret) + return ret; + + if (cnt_num % 2) { + word &= ~MLXBF_PMC_CRSPACE_PERFSEL1; + word |= FIELD_PREP(MLXBF_PMC_CRSPACE_PERFSEL1, evt); + } else { + word &= ~MLXBF_PMC_CRSPACE_PERFSEL0; + word |= FIELD_PREP(MLXBF_PMC_CRSPACE_PERFSEL0, evt); + } + + return mlxbf_pmc_write(addr, MLXBF_PMC_WRITE_REG_32, word); +} + +/* Method to clear crspace counter value */ +static int mlxbf_pmc_clear_crspace_counter(unsigned int blk_num, u32 cnt_num) +{ + void __iomem *addr; + + addr = pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_VAL0(pmc->block[blk_num].counters) + + (cnt_num * 4); + + return mlxbf_pmc_write(addr, MLXBF_PMC_WRITE_REG_32, 0x0); +} + +/* Method to program a counter to monitor an event */ +static int mlxbf_pmc_program_counter(unsigned int blk_num, u32 cnt_num, u32 evt, bool is_l3) +{ + u64 perfctl, perfevt, perfmon_cfg; + + if (cnt_num >= pmc->block[blk_num].counters) + return -ENODEV; + + if (is_l3) + return mlxbf_pmc_program_l3_counter(blk_num, cnt_num, evt); + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) + return mlxbf_pmc_program_crspace_counter(blk_num, cnt_num, + evt); + + /* Configure the counter */ + perfctl = FIELD_PREP(MLXBF_PMC_PERFCTL_EN0, 1); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_EB0, 0); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_ETRIG0, 1); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_AD0, 0); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_ACCM0, 0); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_MS0, 0); + perfctl |= FIELD_PREP(MLXBF_PMC_PERFCTL_FM0, 0); + + perfmon_cfg = FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WDATA, perfctl); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_ADDR, + MLXBF_PMC_PERFCTL); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_STROBE, 1); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WR_R_B, 1); + + if (mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + cnt_num * MLXBF_PMC_REG_SIZE, + MLXBF_PMC_WRITE_REG_64, perfmon_cfg)) + return -EFAULT; + + /* Select the event */ + perfevt = FIELD_PREP(MLXBF_PMC_PERFEVT_EVTSEL, evt); + + perfmon_cfg = FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WDATA, perfevt); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_ADDR, + MLXBF_PMC_PERFEVT); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_STROBE, 1); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WR_R_B, 1); + + if (mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + cnt_num * MLXBF_PMC_REG_SIZE, + MLXBF_PMC_WRITE_REG_64, perfmon_cfg)) + return -EFAULT; + + /* Clear the accumulator */ + perfmon_cfg = FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_ADDR, + MLXBF_PMC_PERFACC0); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_STROBE, 1); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WR_R_B, 1); + + if (mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + cnt_num * MLXBF_PMC_REG_SIZE, + MLXBF_PMC_WRITE_REG_64, perfmon_cfg)) + return -EFAULT; + + return 0; +} + +/* Method to handle l3 counter reads */ +static int mlxbf_pmc_read_l3_counter(unsigned int blk_num, u32 cnt_num, u64 *result) +{ + u32 perfcnt_low = 0, perfcnt_high = 0; + int status; + u64 value; + + status = mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_LOW + + cnt_num * MLXBF_PMC_L3C_REG_SIZE, + &perfcnt_low); + + if (status) + return status; + + status = mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_HIGH + + cnt_num * MLXBF_PMC_L3C_REG_SIZE, + &perfcnt_high); + + if (status) + return status; + + value = perfcnt_high; + value = value << 32; + value |= perfcnt_low; + *result = value; + + return 0; +} + +/* Method to handle crspace counter reads */ +static int mlxbf_pmc_read_crspace_counter(unsigned int blk_num, u32 cnt_num, u64 *result) +{ + int status = 0; + u32 value; + + status = mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_VAL0(pmc->block[blk_num].counters) + + (cnt_num * 4), &value); + if (status) + return status; + + *result = value; + + return 0; +} + +/* Method to read the counter value */ +static int mlxbf_pmc_read_counter(unsigned int blk_num, u32 cnt_num, bool is_l3, u64 *result) +{ + u32 perfcfg_offset, perfval_offset; + u64 perfmon_cfg; + int status; + + if (cnt_num >= pmc->block[blk_num].counters) + return -EINVAL; + + if (is_l3) + return mlxbf_pmc_read_l3_counter(blk_num, cnt_num, result); + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) + return mlxbf_pmc_read_crspace_counter(blk_num, cnt_num, result); + + perfcfg_offset = cnt_num * MLXBF_PMC_REG_SIZE; + perfval_offset = perfcfg_offset + + pmc->block[blk_num].counters * MLXBF_PMC_REG_SIZE; + + /* Set counter in "read" mode */ + perfmon_cfg = FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_ADDR, + MLXBF_PMC_PERFACC0); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_STROBE, 1); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WR_R_B, 0); + + status = mlxbf_pmc_write(pmc->block[blk_num].mmio_base + perfcfg_offset, + MLXBF_PMC_WRITE_REG_64, perfmon_cfg); + + if (status) + return status; + + /* Get the counter value */ + return mlxbf_pmc_read(pmc->block[blk_num].mmio_base + perfval_offset, + MLXBF_PMC_READ_REG_64, result); +} + +/* Method to read L3 block event */ +static int mlxbf_pmc_read_l3_event(unsigned int blk_num, u32 cnt_num, u64 *result) +{ + u32 perfcnt_sel = 0, perfcnt_sel_1 = 0, *wordaddr; + void __iomem *pmcaddr; + u64 evt; + + /* Select appropriate register information */ + switch (cnt_num) { + case 0 ... 3: + pmcaddr = pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_SEL; + wordaddr = &perfcnt_sel; + break; + case 4: + pmcaddr = pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_SEL_1; + wordaddr = &perfcnt_sel_1; + break; + default: + return -EINVAL; + } + + if (mlxbf_pmc_readl(pmcaddr, wordaddr)) + return -EINVAL; + + /* Read from appropriate register field for the counter */ + switch (cnt_num) { + case 0: + evt = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_0, perfcnt_sel); + break; + case 1: + evt = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_1, perfcnt_sel); + break; + case 2: + evt = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_2, perfcnt_sel); + break; + case 3: + evt = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_SEL_CNT_3, perfcnt_sel); + break; + case 4: + evt = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_SEL_1_CNT_4, + perfcnt_sel_1); + break; + default: + return -EINVAL; + } + *result = evt; + + return 0; +} + +/* Method to read crspace block event */ +static int mlxbf_pmc_read_crspace_event(unsigned int blk_num, u32 cnt_num, u64 *result) +{ + u32 word, evt; + void __iomem *addr; + int ret; + + addr = pmc->block[blk_num].mmio_base + + ((cnt_num / 2) * MLXBF_PMC_CRSPACE_PERFSEL_SZ); + ret = mlxbf_pmc_readl(addr, &word); + if (ret) + return ret; + + if (cnt_num % 2) + evt = FIELD_GET(MLXBF_PMC_CRSPACE_PERFSEL1, word); + else + evt = FIELD_GET(MLXBF_PMC_CRSPACE_PERFSEL0, word); + + *result = evt; + + return 0; +} + +/* Method to find the event currently being monitored by a counter */ +static int mlxbf_pmc_read_event(unsigned int blk_num, u32 cnt_num, bool is_l3, u64 *result) +{ + u32 perfcfg_offset, perfval_offset; + u64 perfmon_cfg, perfevt; + + if (cnt_num >= pmc->block[blk_num].counters) + return -EINVAL; + + if (is_l3) + return mlxbf_pmc_read_l3_event(blk_num, cnt_num, result); + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) + return mlxbf_pmc_read_crspace_event(blk_num, cnt_num, result); + + perfcfg_offset = cnt_num * MLXBF_PMC_REG_SIZE; + perfval_offset = perfcfg_offset + + pmc->block[blk_num].counters * MLXBF_PMC_REG_SIZE; + + /* Set counter in "read" mode */ + perfmon_cfg = FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_ADDR, + MLXBF_PMC_PERFEVT); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_STROBE, 1); + perfmon_cfg |= FIELD_PREP(MLXBF_PMC_PERFMON_CONFIG_WR_R_B, 0); + + if (mlxbf_pmc_write(pmc->block[blk_num].mmio_base + perfcfg_offset, + MLXBF_PMC_WRITE_REG_64, perfmon_cfg)) + return -EFAULT; + + /* Get the event number */ + if (mlxbf_pmc_read(pmc->block[blk_num].mmio_base + perfval_offset, + MLXBF_PMC_READ_REG_64, &perfevt)) + return -EFAULT; + + *result = FIELD_GET(MLXBF_PMC_PERFEVT_EVTSEL, perfevt); + + return 0; +} + +/* Method to read a register */ +static int mlxbf_pmc_read_reg(unsigned int blk_num, u32 offset, u64 *result) +{ + u32 reg; + + if ((strstr(pmc->block_name[blk_num], "ecc")) || + (strstr(pmc->block_name[blk_num], "clock_measure"))) { + if (mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + offset, + ®)) + return -EFAULT; + + *result = reg; + return 0; + } + + if (mlxbf_pmc_valid_range(blk_num, offset)) + return mlxbf_pmc_read(pmc->block[blk_num].mmio_base + offset, + MLXBF_PMC_READ_REG_64, result); + + return -EINVAL; +} + +/* Method to write to a register */ +static int mlxbf_pmc_write_reg(unsigned int blk_num, u32 offset, u64 data) +{ + if (strstr(pmc->block_name[blk_num], "clock_measure")) + return -EINVAL; + + if (strstr(pmc->block_name[blk_num], "ecc")) { + return mlxbf_pmc_write(pmc->block[blk_num].mmio_base + offset, + MLXBF_PMC_WRITE_REG_32, data); + } + + if (mlxbf_pmc_valid_range(blk_num, offset)) + return mlxbf_pmc_write(pmc->block[blk_num].mmio_base + offset, + MLXBF_PMC_WRITE_REG_64, data); + + return -EINVAL; +} + +/* Show function for "counter" sysfs files */ +static ssize_t mlxbf_pmc_counter_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct mlxbf_pmc_attribute *attr_counter = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num, cnt_num; + bool is_l3 = false; + int offset; + u64 value; + + blk_num = attr_counter->nr; + cnt_num = attr_counter->index; + + if (strstr(pmc->block_name[blk_num], "l3cache")) + is_l3 = true; + + if ((pmc->block[blk_num].type == MLXBF_PMC_TYPE_COUNTER) || + (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE)) { + if (mlxbf_pmc_read_counter(blk_num, cnt_num, is_l3, &value)) + return -EINVAL; + } else if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_REGISTER) { + offset = mlxbf_pmc_get_event_num(pmc->block_name[blk_num], + attr->attr.name); + if (offset < 0) + return -EINVAL; + if (mlxbf_pmc_read_reg(blk_num, offset, &value)) + return -EINVAL; + } else + return -EINVAL; + + return sysfs_emit(buf, "0x%llx\n", value); +} + +/* Store function for "counter" sysfs files */ +static ssize_t mlxbf_pmc_counter_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct mlxbf_pmc_attribute *attr_counter = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num, cnt_num, data; + bool is_l3 = false; + u64 evt_num; + int offset; + int err; + + blk_num = attr_counter->nr; + cnt_num = attr_counter->index; + + err = kstrtouint(buf, 0, &data); + if (err < 0) + return err; + + /* Allow non-zero writes only to the ecc regs */ + if (!(strstr(pmc->block_name[blk_num], "ecc")) && data) + return -EINVAL; + + /* Do not allow writes to the L3C regs */ + if (strstr(pmc->block_name[blk_num], "l3cache")) + return -EINVAL; + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_COUNTER) { + err = mlxbf_pmc_read_event(blk_num, cnt_num, is_l3, &evt_num); + if (err) + return err; + err = mlxbf_pmc_program_counter(blk_num, cnt_num, evt_num, + is_l3); + if (err) + return err; + } else if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_REGISTER) { + offset = mlxbf_pmc_get_event_num(pmc->block_name[blk_num], + attr->attr.name); + if (offset < 0) + return -EINVAL; + err = mlxbf_pmc_write_reg(blk_num, offset, data); + if (err) + return err; + } else if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) { + if (sscanf(attr->attr.name, "counter%u", &cnt_num) != 1) + return -EINVAL; + err = mlxbf_pmc_clear_crspace_counter(blk_num, cnt_num); + } else + return -EINVAL; + + return count; +} + +/* Show function for "event" sysfs files */ +static ssize_t mlxbf_pmc_event_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct mlxbf_pmc_attribute *attr_event = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num, cnt_num; + bool is_l3 = false; + char *evt_name; + u64 evt_num; + int err; + + blk_num = attr_event->nr; + cnt_num = attr_event->index; + + if (strstr(pmc->block_name[blk_num], "l3cache")) + is_l3 = true; + + err = mlxbf_pmc_read_event(blk_num, cnt_num, is_l3, &evt_num); + if (err) + return sysfs_emit(buf, "No event being monitored\n"); + + evt_name = mlxbf_pmc_get_event_name(pmc->block_name[blk_num], evt_num); + if (!evt_name) + return -EINVAL; + + return sysfs_emit(buf, "0x%llx: %s\n", evt_num, evt_name); +} + +/* Store function for "event" sysfs files */ +static ssize_t mlxbf_pmc_event_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct mlxbf_pmc_attribute *attr_event = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num, cnt_num; + bool is_l3 = false; + char *evt_name; + int evt_num; + int err; + + blk_num = attr_event->nr; + cnt_num = attr_event->index; + + if (isalpha(buf[0])) { + /* Remove the trailing newline character if present */ + evt_name = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!evt_name) + return -ENOMEM; + + evt_num = mlxbf_pmc_get_event_num(pmc->block_name[blk_num], + evt_name); + kfree(evt_name); + if (evt_num < 0) + return -EINVAL; + } else { + err = kstrtouint(buf, 0, &evt_num); + if (err < 0) + return err; + + if (!mlxbf_pmc_get_event_name(pmc->block_name[blk_num], evt_num)) + return -EINVAL; + } + + if (strstr(pmc->block_name[blk_num], "l3cache")) + is_l3 = true; + + err = mlxbf_pmc_program_counter(blk_num, cnt_num, evt_num, is_l3); + if (err) + return err; + + return count; +} + +/* Show function for "event_list" sysfs files */ +static ssize_t mlxbf_pmc_event_list_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct mlxbf_pmc_attribute *attr_event_list = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + const struct mlxbf_pmc_events *events; + char e_info[MLXBF_PMC_EVENT_INFO_LEN]; + unsigned int blk_num, i, len = 0; + size_t size; + int ret = 0; + + blk_num = attr_event_list->nr; + + events = mlxbf_pmc_event_list(pmc->block_name[blk_num], &size); + if (!events) + return -EINVAL; + + for (i = 0, buf[0] = '\0'; i < size; ++i) { + len += snprintf(e_info, sizeof(e_info), "0x%x: %s\n", + events[i].evt_num, events[i].evt_name); + if (len >= PAGE_SIZE) + break; + strcat(buf, e_info); + ret = len; + } + + return ret; +} + +/* Show function for "enable" sysfs files - only for l3cache & crspace */ +static ssize_t mlxbf_pmc_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct mlxbf_pmc_attribute *attr_enable = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num, value; + u32 perfcnt_cfg, word; + + blk_num = attr_enable->nr; + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) { + if (mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_CTL(pmc->block[blk_num].counters), + &word)) + return -EINVAL; + + value = FIELD_GET(MLXBF_PMC_CRSPACE_PERFMON_EN, word); + } else { + if (mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_L3C_PERF_CNT_CFG, + &perfcnt_cfg)) + return -EINVAL; + + value = FIELD_GET(MLXBF_PMC_L3C_PERF_CNT_CFG_EN, perfcnt_cfg); + } + + return sysfs_emit(buf, "%u\n", value); +} + +/* Store function for "enable" sysfs files - only for l3cache & crspace */ +static ssize_t mlxbf_pmc_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct mlxbf_pmc_attribute *attr_enable = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num; + u32 word; + int err; + bool en; + + blk_num = attr_enable->nr; + + err = kstrtobool(buf, &en); + if (err < 0) + return err; + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) { + err = mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_CTL(pmc->block[blk_num].counters), + &word); + if (err) + return -EINVAL; + + word &= ~MLXBF_PMC_CRSPACE_PERFMON_EN; + word |= FIELD_PREP(MLXBF_PMC_CRSPACE_PERFMON_EN, en); + if (en) + word |= FIELD_PREP(MLXBF_PMC_CRSPACE_PERFMON_CLR, 1); + + mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_CTL(pmc->block[blk_num].counters), + MLXBF_PMC_WRITE_REG_32, word); + } else { + err = mlxbf_pmc_config_l3_counters(blk_num, false, !!en); + if (err) + return err; + + if (en) { + err = mlxbf_pmc_config_l3_counters(blk_num, true, false); + if (err) + return err; + } + } + + return count; +} + +/* Show function for "count_clock" sysfs files - only for crspace */ +static ssize_t mlxbf_pmc_count_clock_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct mlxbf_pmc_attribute *attr_count_clock = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num; + u32 reg; + + blk_num = attr_count_clock->nr; + + if (mlxbf_pmc_readl(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_COUNT_CLOCK(pmc->block[blk_num].counters), + ®)) + return -EINVAL; + + return sysfs_emit(buf, "%u\n", reg); +} + +/* Store function for "count_clock" sysfs files - only for crspace */ +static ssize_t mlxbf_pmc_count_clock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct mlxbf_pmc_attribute *attr_count_clock = container_of( + attr, struct mlxbf_pmc_attribute, dev_attr); + unsigned int blk_num; + u32 reg; + int err; + + blk_num = attr_count_clock->nr; + + err = kstrtouint(buf, 0, ®); + if (err < 0) + return err; + + mlxbf_pmc_write(pmc->block[blk_num].mmio_base + + MLXBF_PMC_CRSPACE_PERFMON_COUNT_CLOCK(pmc->block[blk_num].counters), + MLXBF_PMC_WRITE_REG_32, reg); + + return count; +} + +/* Populate attributes for blocks with counters to monitor performance */ +static int mlxbf_pmc_init_perftype_counter(struct device *dev, unsigned int blk_num) +{ + struct mlxbf_pmc_attribute *attr; + unsigned int i = 0, j = 0; + + if (!mlxbf_pmc_event_supported(pmc->block_name[blk_num])) + return -ENOENT; + + /* "event_list" sysfs to list events supported by the block */ + attr = &pmc->block[blk_num].attr_event_list; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0444; + attr->dev_attr.show = mlxbf_pmc_event_list_show; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, "event_list"); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[i] = &attr->dev_attr.attr; + attr = NULL; + + /* "enable" sysfs to start/stop the counters. Only in L3C blocks */ + if (strstr(pmc->block_name[blk_num], "l3cache") || + ((pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE))) { + attr = &pmc->block[blk_num].attr_enable; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0644; + attr->dev_attr.show = mlxbf_pmc_enable_show; + attr->dev_attr.store = mlxbf_pmc_enable_store; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, + "enable"); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[++i] = &attr->dev_attr.attr; + attr = NULL; + } + + if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE) { + /* Program crspace counters to count clock cycles using "count_clock" sysfs */ + attr = &pmc->block[blk_num].attr_count_clock; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0644; + attr->dev_attr.show = mlxbf_pmc_count_clock_show; + attr->dev_attr.store = mlxbf_pmc_count_clock_store; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, + "count_clock"); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[++i] = &attr->dev_attr.attr; + attr = NULL; + } + + pmc->block[blk_num].attr_counter = devm_kcalloc( + dev, pmc->block[blk_num].counters, + sizeof(struct mlxbf_pmc_attribute), GFP_KERNEL); + if (!pmc->block[blk_num].attr_counter) + return -ENOMEM; + + pmc->block[blk_num].attr_event = devm_kcalloc( + dev, pmc->block[blk_num].counters, + sizeof(struct mlxbf_pmc_attribute), GFP_KERNEL); + if (!pmc->block[blk_num].attr_event) + return -ENOMEM; + + /* "eventX" and "counterX" sysfs to program and read counter values */ + for (j = 0; j < pmc->block[blk_num].counters; ++j) { + attr = &pmc->block[blk_num].attr_counter[j]; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0644; + attr->dev_attr.show = mlxbf_pmc_counter_show; + attr->dev_attr.store = mlxbf_pmc_counter_store; + attr->index = j; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, "counter%u", j); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[++i] = &attr->dev_attr.attr; + attr = NULL; + + attr = &pmc->block[blk_num].attr_event[j]; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0644; + attr->dev_attr.show = mlxbf_pmc_event_show; + attr->dev_attr.store = mlxbf_pmc_event_store; + attr->index = j; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, "event%u", j); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[++i] = &attr->dev_attr.attr; + attr = NULL; + } + + return 0; +} + +/* Populate attributes for blocks with registers to monitor performance */ +static int mlxbf_pmc_init_perftype_reg(struct device *dev, unsigned int blk_num) +{ + const struct mlxbf_pmc_events *events; + struct mlxbf_pmc_attribute *attr; + unsigned int i = 0; + size_t count = 0; + + events = mlxbf_pmc_event_list(pmc->block_name[blk_num], &count); + if (!events) + return -ENOENT; + + pmc->block[blk_num].attr_event = devm_kcalloc( + dev, count, sizeof(struct mlxbf_pmc_attribute), GFP_KERNEL); + if (!pmc->block[blk_num].attr_event) + return -ENOMEM; + + while (count > 0) { + --count; + attr = &pmc->block[blk_num].attr_event[count]; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.mode = 0644; + attr->dev_attr.show = mlxbf_pmc_counter_show; + attr->dev_attr.store = mlxbf_pmc_counter_store; + attr->nr = blk_num; + attr->dev_attr.attr.name = devm_kasprintf(dev, GFP_KERNEL, + events[count].evt_name); + if (!attr->dev_attr.attr.name) + return -ENOMEM; + pmc->block[blk_num].block_attr[i] = &attr->dev_attr.attr; + attr = NULL; + i++; + } + + return 0; +} + +/* Helper to create the bfperf sysfs sub-directories and files */ +static int mlxbf_pmc_create_groups(struct device *dev, unsigned int blk_num) +{ + int err; + + /* Populate attributes based on counter type */ + if ((pmc->block[blk_num].type == MLXBF_PMC_TYPE_COUNTER) || + (pmc->block[blk_num].type == MLXBF_PMC_TYPE_CRSPACE)) + err = mlxbf_pmc_init_perftype_counter(dev, blk_num); + else if (pmc->block[blk_num].type == MLXBF_PMC_TYPE_REGISTER) + err = mlxbf_pmc_init_perftype_reg(dev, blk_num); + else + err = -ENOENT; + + if (err) + return err; + + /* Add a new attribute_group for the block */ + pmc->block[blk_num].block_attr_grp.attrs = pmc->block[blk_num].block_attr; + pmc->block[blk_num].block_attr_grp.name = devm_kasprintf( + dev, GFP_KERNEL, pmc->block_name[blk_num]); + if (!pmc->block[blk_num].block_attr_grp.name) + return -ENOMEM; + pmc->groups[pmc->group_num] = &pmc->block[blk_num].block_attr_grp; + pmc->group_num++; + + return 0; +} + +static bool mlxbf_pmc_guid_match(const guid_t *guid, + const struct arm_smccc_res *res) +{ + guid_t id = GUID_INIT(res->a0, res->a1, res->a1 >> 16, res->a2, + res->a2 >> 8, res->a2 >> 16, res->a2 >> 24, + res->a3, res->a3 >> 8, res->a3 >> 16, + res->a3 >> 24); + + return guid_equal(guid, &id); +} + +/* Helper to map the Performance Counters from the varios blocks */ +static int mlxbf_pmc_map_counters(struct device *dev) +{ + u64 info[MLXBF_PMC_INFO_SZ]; + unsigned int tile_num, i; + int ret; + + for (i = 0; i < pmc->total_blocks; ++i) { + /* Create sysfs for tiles only if block number < tile_count */ + if (strstr(pmc->block_name[i], "tilenet")) { + if (sscanf(pmc->block_name[i], "tilenet%u", &tile_num) != 1) + continue; + + if (tile_num >= pmc->tile_count) + continue; + } else if (strstr(pmc->block_name[i], "tile")) { + if (sscanf(pmc->block_name[i], "tile%u", &tile_num) != 1) + continue; + + if (tile_num >= pmc->tile_count) + continue; + } + + /* Create sysfs only for enabled MSS blocks */ + if (strstr(pmc->block_name[i], "mss") && + pmc->event_set == MLXBF_PMC_EVENT_SET_BF3) { + unsigned int mss_num; + + if (sscanf(pmc->block_name[i], "mss%u", &mss_num) != 1) + continue; + + if (!((pmc->mss_enable >> mss_num) & 0x1)) + continue; + } + + /* Create sysfs only for enabled EMI blocks */ + if (strstr(pmc->block_name[i], "emi") && + pmc->event_set == MLXBF_PMC_EVENT_SET_BF3) { + unsigned int emi_num; + + if (sscanf(pmc->block_name[i], "emi%u", &emi_num) != 1) + continue; + + if (!((pmc->mss_enable >> (emi_num / 2)) & 0x1)) + continue; + } + + /* Create sysfs only for enabled LLT blocks */ + if (strstr(pmc->block_name[i], "llt_miss")) { + unsigned int llt_num; + + if (sscanf(pmc->block_name[i], "llt_miss%u", &llt_num) != 1) + continue; + + if (!((pmc->llt_enable >> llt_num) & 0x1)) + continue; + } else if (strstr(pmc->block_name[i], "llt")) { + unsigned int llt_num; + + if (sscanf(pmc->block_name[i], "llt%u", &llt_num) != 1) + continue; + + if (!((pmc->llt_enable >> llt_num) & 0x1)) + continue; + } + + /* Create sysfs only for enabled APT blocks */ + if (strstr(pmc->block_name[i], "apt")) { + unsigned int apt_num; + + if (sscanf(pmc->block_name[i], "apt%u", &apt_num) != 1) + continue; + + if (!((pmc->apt_enable >> apt_num) & 0x1)) + continue; + } + + ret = device_property_read_u64_array(dev, pmc->block_name[i], + info, MLXBF_PMC_INFO_SZ); + if (ret) + return ret; + + /* + * Do not remap if the proper SMC calls are supported, + * since the SMC calls expect physical addresses. + */ + if (pmc->svc_sreg_support) + pmc->block[i].mmio_base = (void __iomem *)info[0]; + else + pmc->block[i].mmio_base = + devm_ioremap(dev, info[0], info[1]); + + pmc->block[i].blk_size = info[1]; + pmc->block[i].counters = info[2]; + pmc->block[i].type = info[3]; + + if (!pmc->block[i].mmio_base) + return -ENOMEM; + + ret = mlxbf_pmc_create_groups(dev, i); + if (ret == -ENOENT) { + dev_warn(dev, "ignoring unsupported block: '%s'\n", pmc->block_name[i]); + continue; + } + if (ret) + return ret; + } + + return 0; +} + +static int mlxbf_pmc_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi_dev = ACPI_COMPANION(&pdev->dev); + const char *hid = acpi_device_hid(acpi_dev); + struct device *dev = &pdev->dev; + struct arm_smccc_res res; + guid_t guid; + int ret; + + /* Ensure we have the UUID we expect for this service. */ + arm_smccc_smc(MLXBF_PMC_SIP_SVC_UID, 0, 0, 0, 0, 0, 0, 0, &res); + guid_parse(mlxbf_pmc_svc_uuid_str, &guid); + if (!mlxbf_pmc_guid_match(&guid, &res)) + return -ENODEV; + + pmc = devm_kzalloc(dev, sizeof(struct mlxbf_pmc_context), GFP_KERNEL); + if (!pmc) + return -ENOMEM; + + /* + * ACPI indicates whether we use SMCs to access registers or not. + * If sreg_tbl_perf is not present, just assume we're not using SMCs. + */ + ret = device_property_read_u32(dev, "sec_reg_block", + &pmc->sreg_tbl_perf); + if (ret) { + pmc->svc_sreg_support = false; + } else { + /* + * Check service version to see if we actually do support the + * needed SMCs. If we have the calls we need, mark support for + * them in the pmc struct. + */ + arm_smccc_smc(MLXBF_PMC_SIP_SVC_VERSION, 0, 0, 0, 0, 0, 0, 0, + &res); + if (res.a0 == MLXBF_PMC_SVC_REQ_MAJOR && + res.a1 >= MLXBF_PMC_SVC_MIN_MINOR) + pmc->svc_sreg_support = true; + else + return -EINVAL; + } + + if (!strcmp(hid, "MLNXBFD0")) + pmc->event_set = MLXBF_PMC_EVENT_SET_BF1; + else if (!strcmp(hid, "MLNXBFD1")) + pmc->event_set = MLXBF_PMC_EVENT_SET_BF2; + else if (!strcmp(hid, "MLNXBFD2")) + pmc->event_set = MLXBF_PMC_EVENT_SET_BF3; + else + return -ENODEV; + + ret = device_property_read_u32(dev, "block_num", &pmc->total_blocks); + if (ret) + return ret; + + ret = device_property_read_string_array(dev, "block_name", + pmc->block_name, + pmc->total_blocks); + if (ret != pmc->total_blocks) + return -EFAULT; + + if (device_property_read_u32(dev, "tile_num", &pmc->tile_count)) { + if (device_property_read_u8(dev, "apt_enable", &pmc->apt_enable)) { + dev_warn(dev, "Number of APTs undefined, ignoring blocks\n"); + pmc->apt_enable = 0; + } + if (device_property_read_u8(dev, "llt_enable", &pmc->llt_enable)) { + dev_warn(dev, "Number of LLTs undefined, ignoring blocks\n"); + pmc->llt_enable = 0; + } + if (device_property_read_u8(dev, "mss_enable", &pmc->mss_enable)) { + dev_warn(dev, "Number of MSSs undefined, ignoring blocks\n"); + pmc->mss_enable = 0; + } + } + + pmc->pdev = pdev; + pmc->group_num = 0; + + ret = mlxbf_pmc_map_counters(dev); + if (ret) + return ret; + + pmc->hwmon_dev = devm_hwmon_device_register_with_groups( + dev, "bfperf", pmc, pmc->groups); + if (IS_ERR(pmc->hwmon_dev)) + return PTR_ERR(pmc->hwmon_dev); + platform_set_drvdata(pdev, pmc); + + return 0; +} + +static const struct acpi_device_id mlxbf_pmc_acpi_ids[] = { { "MLNXBFD0", 0 }, + { "MLNXBFD1", 0 }, + { "MLNXBFD2", 0 }, + {}, }; + +MODULE_DEVICE_TABLE(acpi, mlxbf_pmc_acpi_ids); +static struct platform_driver pmc_driver = { + .driver = { .name = "mlxbf-pmc", + .acpi_match_table = ACPI_PTR(mlxbf_pmc_acpi_ids), }, + .probe = mlxbf_pmc_probe, +}; + +module_platform_driver(pmc_driver); + +MODULE_AUTHOR("Shravan Kumar Ramani "); +MODULE_DESCRIPTION("Mellanox PMC driver"); +MODULE_LICENSE("Dual BSD/GPL"); diff --git a/drivers/platform/mellanox/mlxbf-tmfifo-regs.h b/drivers/platform/mellanox/mlxbf-tmfifo-regs.h new file mode 100644 index 000000000..44fb8c5b1 --- /dev/null +++ b/drivers/platform/mellanox/mlxbf-tmfifo-regs.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, Mellanox Technologies. All rights reserved. + */ + +#ifndef __MLXBF_TMFIFO_REGS_H__ +#define __MLXBF_TMFIFO_REGS_H__ + +#include +#include + +#define MLXBF_TMFIFO_TX_DATA 0x00 +#define MLXBF_TMFIFO_TX_STS 0x08 +#define MLXBF_TMFIFO_TX_STS__LENGTH 0x0001 +#define MLXBF_TMFIFO_TX_STS__COUNT_SHIFT 0 +#define MLXBF_TMFIFO_TX_STS__COUNT_WIDTH 9 +#define MLXBF_TMFIFO_TX_STS__COUNT_RESET_VAL 0 +#define MLXBF_TMFIFO_TX_STS__COUNT_RMASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_TX_STS__COUNT_MASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_TX_CTL 0x10 +#define MLXBF_TMFIFO_TX_CTL__LENGTH 0x0001 +#define MLXBF_TMFIFO_TX_CTL__LWM_SHIFT 0 +#define MLXBF_TMFIFO_TX_CTL__LWM_WIDTH 8 +#define MLXBF_TMFIFO_TX_CTL__LWM_RESET_VAL 128 +#define MLXBF_TMFIFO_TX_CTL__LWM_RMASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_TX_CTL__LWM_MASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_TX_CTL__HWM_SHIFT 8 +#define MLXBF_TMFIFO_TX_CTL__HWM_WIDTH 8 +#define MLXBF_TMFIFO_TX_CTL__HWM_RESET_VAL 128 +#define MLXBF_TMFIFO_TX_CTL__HWM_RMASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_TX_CTL__HWM_MASK GENMASK_ULL(15, 8) +#define MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_SHIFT 32 +#define MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_WIDTH 9 +#define MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_RESET_VAL 256 +#define MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_RMASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_MASK GENMASK_ULL(40, 32) +#define MLXBF_TMFIFO_RX_DATA 0x00 +#define MLXBF_TMFIFO_RX_STS 0x08 +#define MLXBF_TMFIFO_RX_STS__LENGTH 0x0001 +#define MLXBF_TMFIFO_RX_STS__COUNT_SHIFT 0 +#define MLXBF_TMFIFO_RX_STS__COUNT_WIDTH 9 +#define MLXBF_TMFIFO_RX_STS__COUNT_RESET_VAL 0 +#define MLXBF_TMFIFO_RX_STS__COUNT_RMASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_RX_STS__COUNT_MASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_RX_CTL 0x10 +#define MLXBF_TMFIFO_RX_CTL__LENGTH 0x0001 +#define MLXBF_TMFIFO_RX_CTL__LWM_SHIFT 0 +#define MLXBF_TMFIFO_RX_CTL__LWM_WIDTH 8 +#define MLXBF_TMFIFO_RX_CTL__LWM_RESET_VAL 128 +#define MLXBF_TMFIFO_RX_CTL__LWM_RMASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_RX_CTL__LWM_MASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_RX_CTL__HWM_SHIFT 8 +#define MLXBF_TMFIFO_RX_CTL__HWM_WIDTH 8 +#define MLXBF_TMFIFO_RX_CTL__HWM_RESET_VAL 128 +#define MLXBF_TMFIFO_RX_CTL__HWM_RMASK GENMASK_ULL(7, 0) +#define MLXBF_TMFIFO_RX_CTL__HWM_MASK GENMASK_ULL(15, 8) +#define MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_SHIFT 32 +#define MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_WIDTH 9 +#define MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_RESET_VAL 256 +#define MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_RMASK GENMASK_ULL(8, 0) +#define MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_MASK GENMASK_ULL(40, 32) + +/* BF3 register offsets within resource 0. */ +#define MLXBF_TMFIFO_RX_DATA_BF3 0x0000 +#define MLXBF_TMFIFO_TX_DATA_BF3 0x1000 + +/* BF3 register offsets within resource 1. */ +#define MLXBF_TMFIFO_RX_STS_BF3 0x0000 +#define MLXBF_TMFIFO_RX_CTL_BF3 0x0008 +#define MLXBF_TMFIFO_TX_STS_BF3 0x0100 +#define MLXBF_TMFIFO_TX_CTL_BF3 0x0108 + +#endif /* !defined(__MLXBF_TMFIFO_REGS_H__) */ diff --git a/drivers/platform/mellanox/mlxbf-tmfifo.c b/drivers/platform/mellanox/mlxbf-tmfifo.c new file mode 100644 index 000000000..3c6408581 --- /dev/null +++ b/drivers/platform/mellanox/mlxbf-tmfifo.c @@ -0,0 +1,1461 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Mellanox BlueField SoC TmFifo driver + * + * Copyright (C) 2019 Mellanox Technologies + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "mlxbf-tmfifo-regs.h" + +/* Vring size. */ +#define MLXBF_TMFIFO_VRING_SIZE SZ_1K + +/* Console Tx buffer size. */ +#define MLXBF_TMFIFO_CON_TX_BUF_SIZE SZ_32K + +/* Console Tx buffer reserved space. */ +#define MLXBF_TMFIFO_CON_TX_BUF_RSV_SIZE 8 + +/* House-keeping timer interval. */ +#define MLXBF_TMFIFO_TIMER_INTERVAL (HZ / 10) + +/* Virtual devices sharing the TM FIFO. */ +#define MLXBF_TMFIFO_VDEV_MAX (VIRTIO_ID_CONSOLE + 1) + +/* + * Reserve 1/16 of TmFifo space, so console messages are not starved by + * the networking traffic. + */ +#define MLXBF_TMFIFO_RESERVE_RATIO 16 + +/* Message with data needs at least two words (for header & data). */ +#define MLXBF_TMFIFO_DATA_MIN_WORDS 2 + +/* Tx timeout in milliseconds. */ +#define TMFIFO_TX_TIMEOUT 2000 + +/* ACPI UID for BlueField-3. */ +#define TMFIFO_BF3_UID 1 + +struct mlxbf_tmfifo; + +/** + * struct mlxbf_tmfifo_vring - Structure of the TmFifo virtual ring + * @va: virtual address of the ring + * @dma: dma address of the ring + * @vq: pointer to the virtio virtqueue + * @desc: current descriptor of the pending packet + * @desc_head: head descriptor of the pending packet + * @drop_desc: dummy desc for packet dropping + * @cur_len: processed length of the current descriptor + * @rem_len: remaining length of the pending packet + * @rem_padding: remaining bytes to send as paddings + * @pkt_len: total length of the pending packet + * @next_avail: next avail descriptor id + * @num: vring size (number of descriptors) + * @align: vring alignment size + * @index: vring index + * @vdev_id: vring virtio id (VIRTIO_ID_xxx) + * @tx_timeout: expire time of last tx packet + * @fifo: pointer to the tmfifo structure + */ +struct mlxbf_tmfifo_vring { + void *va; + dma_addr_t dma; + struct virtqueue *vq; + struct vring_desc *desc; + struct vring_desc *desc_head; + struct vring_desc drop_desc; + int cur_len; + int rem_len; + int rem_padding; + u32 pkt_len; + u16 next_avail; + int num; + int align; + int index; + int vdev_id; + unsigned long tx_timeout; + struct mlxbf_tmfifo *fifo; +}; + +/* Check whether vring is in drop mode. */ +#define IS_VRING_DROP(_r) ({ \ + typeof(_r) (r) = (_r); \ + r->desc_head == &r->drop_desc; }) + +/* A stub length to drop maximum length packet. */ +#define VRING_DROP_DESC_MAX_LEN GENMASK(15, 0) + +/* Interrupt types. */ +enum { + MLXBF_TM_RX_LWM_IRQ, + MLXBF_TM_RX_HWM_IRQ, + MLXBF_TM_TX_LWM_IRQ, + MLXBF_TM_TX_HWM_IRQ, + MLXBF_TM_MAX_IRQ +}; + +/* Ring types (Rx & Tx). */ +enum { + MLXBF_TMFIFO_VRING_RX, + MLXBF_TMFIFO_VRING_TX, + MLXBF_TMFIFO_VRING_MAX +}; + +/** + * struct mlxbf_tmfifo_vdev - Structure of the TmFifo virtual device + * @vdev: virtio device, in which the vdev.id.device field has the + * VIRTIO_ID_xxx id to distinguish the virtual device. + * @status: status of the device + * @features: supported features of the device + * @vrings: array of tmfifo vrings of this device + * @config: non-anonymous union for cons and net + * @config.cons: virtual console config - + * select if vdev.id.device is VIRTIO_ID_CONSOLE + * @config.net: virtual network config - + * select if vdev.id.device is VIRTIO_ID_NET + * @tx_buf: tx buffer used to buffer data before writing into the FIFO + */ +struct mlxbf_tmfifo_vdev { + struct virtio_device vdev; + u8 status; + u64 features; + struct mlxbf_tmfifo_vring vrings[MLXBF_TMFIFO_VRING_MAX]; + union { + struct virtio_console_config cons; + struct virtio_net_config net; + } config; + struct circ_buf tx_buf; +}; + +/** + * struct mlxbf_tmfifo_irq_info - Structure of the interrupt information + * @fifo: pointer to the tmfifo structure + * @irq: interrupt number + * @index: index into the interrupt array + */ +struct mlxbf_tmfifo_irq_info { + struct mlxbf_tmfifo *fifo; + int irq; + int index; +}; + +/** + * struct mlxbf_tmfifo_io - Structure of the TmFifo IO resource (for both rx & tx) + * @ctl: control register offset (TMFIFO_RX_CTL / TMFIFO_TX_CTL) + * @sts: status register offset (TMFIFO_RX_STS / TMFIFO_TX_STS) + * @data: data register offset (TMFIFO_RX_DATA / TMFIFO_TX_DATA) + */ +struct mlxbf_tmfifo_io { + void __iomem *ctl; + void __iomem *sts; + void __iomem *data; +}; + +/** + * struct mlxbf_tmfifo - Structure of the TmFifo + * @vdev: array of the virtual devices running over the TmFifo + * @lock: lock to protect the TmFifo access + * @res0: mapped resource block 0 + * @res1: mapped resource block 1 + * @rx: rx io resource + * @tx: tx io resource + * @rx_fifo_size: number of entries of the Rx FIFO + * @tx_fifo_size: number of entries of the Tx FIFO + * @pend_events: pending bits for deferred events + * @irq_info: interrupt information + * @work: work struct for deferred process + * @timer: background timer + * @vring: Tx/Rx ring + * @spin_lock: Tx/Rx spin lock + * @is_ready: ready flag + */ +struct mlxbf_tmfifo { + struct mlxbf_tmfifo_vdev *vdev[MLXBF_TMFIFO_VDEV_MAX]; + struct mutex lock; /* TmFifo lock */ + void __iomem *res0; + void __iomem *res1; + struct mlxbf_tmfifo_io rx; + struct mlxbf_tmfifo_io tx; + int rx_fifo_size; + int tx_fifo_size; + unsigned long pend_events; + struct mlxbf_tmfifo_irq_info irq_info[MLXBF_TM_MAX_IRQ]; + struct work_struct work; + struct timer_list timer; + struct mlxbf_tmfifo_vring *vring[2]; + spinlock_t spin_lock[2]; /* spin lock */ + bool is_ready; +}; + +/** + * struct mlxbf_tmfifo_msg_hdr - Structure of the TmFifo message header + * @type: message type + * @len: payload length in network byte order. Messages sent into the FIFO + * will be read by the other side as data stream in the same byte order. + * The length needs to be encoded into network order so both sides + * could understand it. + */ +struct mlxbf_tmfifo_msg_hdr { + u8 type; + __be16 len; + /* private: */ + u8 unused[5]; +} __packed __aligned(sizeof(u64)); + +/* + * Default MAC. + * This MAC address will be read from EFI persistent variable if configured. + * It can also be reconfigured with standard Linux tools. + */ +static u8 mlxbf_tmfifo_net_default_mac[ETH_ALEN] = { + 0x00, 0x1A, 0xCA, 0xFF, 0xFF, 0x01 +}; + +/* EFI variable name of the MAC address. */ +static efi_char16_t mlxbf_tmfifo_efi_name[] = L"RshimMacAddr"; + +/* Maximum L2 header length. */ +#define MLXBF_TMFIFO_NET_L2_OVERHEAD (ETH_HLEN + VLAN_HLEN) + +/* Supported virtio-net features. */ +#define MLXBF_TMFIFO_NET_FEATURES \ + (BIT_ULL(VIRTIO_NET_F_MTU) | BIT_ULL(VIRTIO_NET_F_STATUS) | \ + BIT_ULL(VIRTIO_NET_F_MAC)) + +#define mlxbf_vdev_to_tmfifo(d) container_of(d, struct mlxbf_tmfifo_vdev, vdev) + +/* Free vrings of the FIFO device. */ +static void mlxbf_tmfifo_free_vrings(struct mlxbf_tmfifo *fifo, + struct mlxbf_tmfifo_vdev *tm_vdev) +{ + struct mlxbf_tmfifo_vring *vring; + int i, size; + + for (i = 0; i < ARRAY_SIZE(tm_vdev->vrings); i++) { + vring = &tm_vdev->vrings[i]; + if (vring->va) { + size = vring_size(vring->num, vring->align); + dma_free_coherent(tm_vdev->vdev.dev.parent, size, + vring->va, vring->dma); + vring->va = NULL; + if (vring->vq) { + vring_del_virtqueue(vring->vq); + vring->vq = NULL; + } + } + } +} + +/* Allocate vrings for the FIFO. */ +static int mlxbf_tmfifo_alloc_vrings(struct mlxbf_tmfifo *fifo, + struct mlxbf_tmfifo_vdev *tm_vdev) +{ + struct mlxbf_tmfifo_vring *vring; + struct device *dev; + dma_addr_t dma; + int i, size; + void *va; + + for (i = 0; i < ARRAY_SIZE(tm_vdev->vrings); i++) { + vring = &tm_vdev->vrings[i]; + vring->fifo = fifo; + vring->num = MLXBF_TMFIFO_VRING_SIZE; + vring->align = SMP_CACHE_BYTES; + vring->index = i; + vring->vdev_id = tm_vdev->vdev.id.device; + vring->drop_desc.len = cpu_to_virtio32(&tm_vdev->vdev, + VRING_DROP_DESC_MAX_LEN); + dev = &tm_vdev->vdev.dev; + + size = vring_size(vring->num, vring->align); + va = dma_alloc_coherent(dev->parent, size, &dma, GFP_KERNEL); + if (!va) { + mlxbf_tmfifo_free_vrings(fifo, tm_vdev); + dev_err(dev->parent, "dma_alloc_coherent failed\n"); + return -ENOMEM; + } + + vring->va = va; + vring->dma = dma; + } + + return 0; +} + +/* Disable interrupts of the FIFO device. */ +static void mlxbf_tmfifo_disable_irqs(struct mlxbf_tmfifo *fifo) +{ + int i, irq; + + for (i = 0; i < MLXBF_TM_MAX_IRQ; i++) { + irq = fifo->irq_info[i].irq; + fifo->irq_info[i].irq = 0; + disable_irq(irq); + } +} + +/* Interrupt handler. */ +static irqreturn_t mlxbf_tmfifo_irq_handler(int irq, void *arg) +{ + struct mlxbf_tmfifo_irq_info *irq_info = arg; + + if (!test_and_set_bit(irq_info->index, &irq_info->fifo->pend_events)) + schedule_work(&irq_info->fifo->work); + + return IRQ_HANDLED; +} + +/* Get the next packet descriptor from the vring. */ +static struct vring_desc * +mlxbf_tmfifo_get_next_desc(struct mlxbf_tmfifo_vring *vring) +{ + const struct vring *vr = virtqueue_get_vring(vring->vq); + struct virtio_device *vdev = vring->vq->vdev; + unsigned int idx, head; + + if (vring->next_avail == virtio16_to_cpu(vdev, vr->avail->idx)) + return NULL; + + /* Make sure 'avail->idx' is visible already. */ + virtio_rmb(false); + + idx = vring->next_avail % vr->num; + head = virtio16_to_cpu(vdev, vr->avail->ring[idx]); + if (WARN_ON(head >= vr->num)) + return NULL; + + vring->next_avail++; + + return &vr->desc[head]; +} + +/* Release virtio descriptor. */ +static void mlxbf_tmfifo_release_desc(struct mlxbf_tmfifo_vring *vring, + struct vring_desc *desc, u32 len) +{ + const struct vring *vr = virtqueue_get_vring(vring->vq); + struct virtio_device *vdev = vring->vq->vdev; + u16 idx, vr_idx; + + vr_idx = virtio16_to_cpu(vdev, vr->used->idx); + idx = vr_idx % vr->num; + vr->used->ring[idx].id = cpu_to_virtio32(vdev, desc - vr->desc); + vr->used->ring[idx].len = cpu_to_virtio32(vdev, len); + + /* + * Virtio could poll and check the 'idx' to decide whether the desc is + * done or not. Add a memory barrier here to make sure the update above + * completes before updating the idx. + */ + virtio_mb(false); + vr->used->idx = cpu_to_virtio16(vdev, vr_idx + 1); +} + +/* Get the total length of the descriptor chain. */ +static u32 mlxbf_tmfifo_get_pkt_len(struct mlxbf_tmfifo_vring *vring, + struct vring_desc *desc) +{ + const struct vring *vr = virtqueue_get_vring(vring->vq); + struct virtio_device *vdev = vring->vq->vdev; + u32 len = 0, idx; + + while (desc) { + len += virtio32_to_cpu(vdev, desc->len); + if (!(virtio16_to_cpu(vdev, desc->flags) & VRING_DESC_F_NEXT)) + break; + idx = virtio16_to_cpu(vdev, desc->next); + desc = &vr->desc[idx]; + } + + return len; +} + +static void mlxbf_tmfifo_release_pkt(struct mlxbf_tmfifo_vring *vring) +{ + struct vring_desc *desc_head; + u32 len = 0; + + if (vring->desc_head) { + desc_head = vring->desc_head; + len = vring->pkt_len; + } else { + desc_head = mlxbf_tmfifo_get_next_desc(vring); + len = mlxbf_tmfifo_get_pkt_len(vring, desc_head); + } + + if (desc_head) + mlxbf_tmfifo_release_desc(vring, desc_head, len); + + vring->pkt_len = 0; + vring->desc = NULL; + vring->desc_head = NULL; +} + +static void mlxbf_tmfifo_init_net_desc(struct mlxbf_tmfifo_vring *vring, + struct vring_desc *desc, bool is_rx) +{ + struct virtio_device *vdev = vring->vq->vdev; + struct virtio_net_hdr *net_hdr; + + net_hdr = phys_to_virt(virtio64_to_cpu(vdev, desc->addr)); + memset(net_hdr, 0, sizeof(*net_hdr)); +} + +/* Get and initialize the next packet. */ +static struct vring_desc * +mlxbf_tmfifo_get_next_pkt(struct mlxbf_tmfifo_vring *vring, bool is_rx) +{ + struct vring_desc *desc; + + desc = mlxbf_tmfifo_get_next_desc(vring); + if (desc && is_rx && vring->vdev_id == VIRTIO_ID_NET) + mlxbf_tmfifo_init_net_desc(vring, desc, is_rx); + + vring->desc_head = desc; + vring->desc = desc; + + return desc; +} + +/* House-keeping timer. */ +static void mlxbf_tmfifo_timer(struct timer_list *t) +{ + struct mlxbf_tmfifo *fifo = container_of(t, struct mlxbf_tmfifo, timer); + int rx, tx; + + rx = !test_and_set_bit(MLXBF_TM_RX_HWM_IRQ, &fifo->pend_events); + tx = !test_and_set_bit(MLXBF_TM_TX_LWM_IRQ, &fifo->pend_events); + + if (rx || tx) + schedule_work(&fifo->work); + + mod_timer(&fifo->timer, jiffies + MLXBF_TMFIFO_TIMER_INTERVAL); +} + +/* Copy one console packet into the output buffer. */ +static void mlxbf_tmfifo_console_output_one(struct mlxbf_tmfifo_vdev *cons, + struct mlxbf_tmfifo_vring *vring, + struct vring_desc *desc) +{ + const struct vring *vr = virtqueue_get_vring(vring->vq); + struct virtio_device *vdev = &cons->vdev; + u32 len, idx, seg; + void *addr; + + while (desc) { + addr = phys_to_virt(virtio64_to_cpu(vdev, desc->addr)); + len = virtio32_to_cpu(vdev, desc->len); + + seg = CIRC_SPACE_TO_END(cons->tx_buf.head, cons->tx_buf.tail, + MLXBF_TMFIFO_CON_TX_BUF_SIZE); + if (len <= seg) { + memcpy(cons->tx_buf.buf + cons->tx_buf.head, addr, len); + } else { + memcpy(cons->tx_buf.buf + cons->tx_buf.head, addr, seg); + addr += seg; + memcpy(cons->tx_buf.buf, addr, len - seg); + } + cons->tx_buf.head = (cons->tx_buf.head + len) % + MLXBF_TMFIFO_CON_TX_BUF_SIZE; + + if (!(virtio16_to_cpu(vdev, desc->flags) & VRING_DESC_F_NEXT)) + break; + idx = virtio16_to_cpu(vdev, desc->next); + desc = &vr->desc[idx]; + } +} + +/* Copy console data into the output buffer. */ +static void mlxbf_tmfifo_console_output(struct mlxbf_tmfifo_vdev *cons, + struct mlxbf_tmfifo_vring *vring) +{ + struct vring_desc *desc; + u32 len, avail; + + desc = mlxbf_tmfifo_get_next_desc(vring); + while (desc) { + /* Release the packet if not enough space. */ + len = mlxbf_tmfifo_get_pkt_len(vring, desc); + avail = CIRC_SPACE(cons->tx_buf.head, cons->tx_buf.tail, + MLXBF_TMFIFO_CON_TX_BUF_SIZE); + if (len + MLXBF_TMFIFO_CON_TX_BUF_RSV_SIZE > avail) { + mlxbf_tmfifo_release_desc(vring, desc, len); + break; + } + + mlxbf_tmfifo_console_output_one(cons, vring, desc); + mlxbf_tmfifo_release_desc(vring, desc, len); + desc = mlxbf_tmfifo_get_next_desc(vring); + } +} + +/* Get the number of available words in Rx FIFO for receiving. */ +static int mlxbf_tmfifo_get_rx_avail(struct mlxbf_tmfifo *fifo) +{ + u64 sts; + + sts = readq(fifo->rx.sts); + return FIELD_GET(MLXBF_TMFIFO_RX_STS__COUNT_MASK, sts); +} + +/* Get the number of available words in the TmFifo for sending. */ +static int mlxbf_tmfifo_get_tx_avail(struct mlxbf_tmfifo *fifo, int vdev_id) +{ + int tx_reserve; + u32 count; + u64 sts; + + /* Reserve some room in FIFO for console messages. */ + if (vdev_id == VIRTIO_ID_NET) + tx_reserve = fifo->tx_fifo_size / MLXBF_TMFIFO_RESERVE_RATIO; + else + tx_reserve = 1; + + sts = readq(fifo->tx.sts); + count = FIELD_GET(MLXBF_TMFIFO_TX_STS__COUNT_MASK, sts); + return fifo->tx_fifo_size - tx_reserve - count; +} + +/* Console Tx (move data from the output buffer into the TmFifo). */ +static void mlxbf_tmfifo_console_tx(struct mlxbf_tmfifo *fifo, int avail) +{ + struct mlxbf_tmfifo_msg_hdr hdr; + struct mlxbf_tmfifo_vdev *cons; + unsigned long flags; + int size, seg; + void *addr; + u64 data; + + /* Return if not enough space available. */ + if (avail < MLXBF_TMFIFO_DATA_MIN_WORDS) + return; + + cons = fifo->vdev[VIRTIO_ID_CONSOLE]; + if (!cons || !cons->tx_buf.buf) + return; + + /* Return if no data to send. */ + size = CIRC_CNT(cons->tx_buf.head, cons->tx_buf.tail, + MLXBF_TMFIFO_CON_TX_BUF_SIZE); + if (size == 0) + return; + + /* Adjust the size to available space. */ + if (size + sizeof(hdr) > avail * sizeof(u64)) + size = avail * sizeof(u64) - sizeof(hdr); + + /* Write header. */ + hdr.type = VIRTIO_ID_CONSOLE; + hdr.len = htons(size); + writeq(*(u64 *)&hdr, fifo->tx.data); + + /* Use spin-lock to protect the 'cons->tx_buf'. */ + spin_lock_irqsave(&fifo->spin_lock[0], flags); + + while (size > 0) { + addr = cons->tx_buf.buf + cons->tx_buf.tail; + + seg = CIRC_CNT_TO_END(cons->tx_buf.head, cons->tx_buf.tail, + MLXBF_TMFIFO_CON_TX_BUF_SIZE); + if (seg >= sizeof(u64)) { + memcpy(&data, addr, sizeof(u64)); + } else { + memcpy(&data, addr, seg); + memcpy((u8 *)&data + seg, cons->tx_buf.buf, + sizeof(u64) - seg); + } + writeq(data, fifo->tx.data); + + if (size >= sizeof(u64)) { + cons->tx_buf.tail = (cons->tx_buf.tail + sizeof(u64)) % + MLXBF_TMFIFO_CON_TX_BUF_SIZE; + size -= sizeof(u64); + } else { + cons->tx_buf.tail = (cons->tx_buf.tail + size) % + MLXBF_TMFIFO_CON_TX_BUF_SIZE; + size = 0; + } + } + + spin_unlock_irqrestore(&fifo->spin_lock[0], flags); +} + +/* Rx/Tx one word in the descriptor buffer. */ +static void mlxbf_tmfifo_rxtx_word(struct mlxbf_tmfifo_vring *vring, + struct vring_desc *desc, + bool is_rx, int len) +{ + struct virtio_device *vdev = vring->vq->vdev; + struct mlxbf_tmfifo *fifo = vring->fifo; + void *addr; + u64 data; + + /* Get the buffer address of this desc. */ + addr = phys_to_virt(virtio64_to_cpu(vdev, desc->addr)); + + /* Read a word from FIFO for Rx. */ + if (is_rx) + data = readq(fifo->rx.data); + + if (vring->cur_len + sizeof(u64) <= len) { + /* The whole word. */ + if (is_rx) { + if (!IS_VRING_DROP(vring)) + memcpy(addr + vring->cur_len, &data, + sizeof(u64)); + } else { + memcpy(&data, addr + vring->cur_len, + sizeof(u64)); + } + vring->cur_len += sizeof(u64); + } else { + /* Leftover bytes. */ + if (is_rx) { + if (!IS_VRING_DROP(vring)) + memcpy(addr + vring->cur_len, &data, + len - vring->cur_len); + } else { + data = 0; + memcpy(&data, addr + vring->cur_len, + len - vring->cur_len); + } + vring->cur_len = len; + } + + /* Write the word into FIFO for Tx. */ + if (!is_rx) + writeq(data, fifo->tx.data); +} + +/* + * Rx/Tx packet header. + * + * In Rx case, the packet might be found to belong to a different vring since + * the TmFifo is shared by different services. In such case, the 'vring_change' + * flag is set. + */ +static void mlxbf_tmfifo_rxtx_header(struct mlxbf_tmfifo_vring *vring, + struct vring_desc **desc, + bool is_rx, bool *vring_change) +{ + struct mlxbf_tmfifo *fifo = vring->fifo; + struct virtio_net_config *config; + struct mlxbf_tmfifo_msg_hdr hdr; + int vdev_id, hdr_len; + bool drop_rx = false; + + /* Read/Write packet header. */ + if (is_rx) { + /* Drain one word from the FIFO. */ + *(u64 *)&hdr = readq(fifo->rx.data); + + /* Skip the length 0 packets (keepalive). */ + if (hdr.len == 0) + return; + + /* Check packet type. */ + if (hdr.type == VIRTIO_ID_NET) { + vdev_id = VIRTIO_ID_NET; + hdr_len = sizeof(struct virtio_net_hdr); + config = &fifo->vdev[vdev_id]->config.net; + /* A legacy-only interface for now. */ + if (ntohs(hdr.len) > + __virtio16_to_cpu(virtio_legacy_is_little_endian(), + config->mtu) + + MLXBF_TMFIFO_NET_L2_OVERHEAD) + drop_rx = true; + } else { + vdev_id = VIRTIO_ID_CONSOLE; + hdr_len = 0; + } + + /* + * Check whether the new packet still belongs to this vring. + * If not, update the pkt_len of the new vring. + */ + if (vdev_id != vring->vdev_id) { + struct mlxbf_tmfifo_vdev *tm_dev2 = fifo->vdev[vdev_id]; + + if (!tm_dev2) + return; + vring->desc = *desc; + vring = &tm_dev2->vrings[MLXBF_TMFIFO_VRING_RX]; + *vring_change = true; + } + + if (drop_rx && !IS_VRING_DROP(vring)) { + if (vring->desc_head) + mlxbf_tmfifo_release_pkt(vring); + *desc = &vring->drop_desc; + vring->desc_head = *desc; + vring->desc = *desc; + } + + vring->pkt_len = ntohs(hdr.len) + hdr_len; + } else { + /* Network virtio has an extra header. */ + hdr_len = (vring->vdev_id == VIRTIO_ID_NET) ? + sizeof(struct virtio_net_hdr) : 0; + vring->pkt_len = mlxbf_tmfifo_get_pkt_len(vring, *desc); + hdr.type = (vring->vdev_id == VIRTIO_ID_NET) ? + VIRTIO_ID_NET : VIRTIO_ID_CONSOLE; + hdr.len = htons(vring->pkt_len - hdr_len); + writeq(*(u64 *)&hdr, fifo->tx.data); + } + + vring->cur_len = hdr_len; + vring->rem_len = vring->pkt_len; + fifo->vring[is_rx] = vring; +} + +/* + * Rx/Tx one descriptor. + * + * Return true to indicate more data available. + */ +static bool mlxbf_tmfifo_rxtx_one_desc(struct mlxbf_tmfifo_vring *vring, + bool is_rx, int *avail) +{ + const struct vring *vr = virtqueue_get_vring(vring->vq); + struct mlxbf_tmfifo *fifo = vring->fifo; + struct virtio_device *vdev; + bool vring_change = false; + struct vring_desc *desc; + unsigned long flags; + u32 len, idx; + + vdev = &fifo->vdev[vring->vdev_id]->vdev; + + /* Get the descriptor of the next packet. */ + if (!vring->desc) { + desc = mlxbf_tmfifo_get_next_pkt(vring, is_rx); + if (!desc) { + /* Drop next Rx packet to avoid stuck. */ + if (is_rx) { + desc = &vring->drop_desc; + vring->desc_head = desc; + vring->desc = desc; + } else { + return false; + } + } + } else { + desc = vring->desc; + } + + /* Beginning of a packet. Start to Rx/Tx packet header. */ + if (vring->pkt_len == 0) { + mlxbf_tmfifo_rxtx_header(vring, &desc, is_rx, &vring_change); + (*avail)--; + + /* Return if new packet is for another ring. */ + if (vring_change) + return false; + goto mlxbf_tmfifo_desc_done; + } + + /* Get the length of this desc. */ + len = virtio32_to_cpu(vdev, desc->len); + if (len > vring->rem_len) + len = vring->rem_len; + + /* Rx/Tx one word (8 bytes) if not done. */ + if (vring->cur_len < len) { + mlxbf_tmfifo_rxtx_word(vring, desc, is_rx, len); + (*avail)--; + } + + /* Check again whether it's done. */ + if (vring->cur_len == len) { + vring->cur_len = 0; + vring->rem_len -= len; + + /* Get the next desc on the chain. */ + if (!IS_VRING_DROP(vring) && vring->rem_len > 0 && + (virtio16_to_cpu(vdev, desc->flags) & VRING_DESC_F_NEXT)) { + idx = virtio16_to_cpu(vdev, desc->next); + desc = &vr->desc[idx]; + goto mlxbf_tmfifo_desc_done; + } + + /* Done and release the packet. */ + desc = NULL; + fifo->vring[is_rx] = NULL; + if (!IS_VRING_DROP(vring)) { + mlxbf_tmfifo_release_pkt(vring); + } else { + vring->pkt_len = 0; + vring->desc_head = NULL; + vring->desc = NULL; + return false; + } + + /* + * Make sure the load/store are in order before + * returning back to virtio. + */ + virtio_mb(false); + + /* Notify upper layer that packet is done. */ + spin_lock_irqsave(&fifo->spin_lock[is_rx], flags); + vring_interrupt(0, vring->vq); + spin_unlock_irqrestore(&fifo->spin_lock[is_rx], flags); + } + +mlxbf_tmfifo_desc_done: + /* Save the current desc. */ + vring->desc = desc; + + return true; +} + +static void mlxbf_tmfifo_check_tx_timeout(struct mlxbf_tmfifo_vring *vring) +{ + unsigned long flags; + + /* Only handle Tx timeout for network vdev. */ + if (vring->vdev_id != VIRTIO_ID_NET) + return; + + /* Initialize the timeout or return if not expired. */ + if (!vring->tx_timeout) { + /* Initialize the timeout. */ + vring->tx_timeout = jiffies + + msecs_to_jiffies(TMFIFO_TX_TIMEOUT); + return; + } else if (time_before(jiffies, vring->tx_timeout)) { + /* Return if not timeout yet. */ + return; + } + + /* + * Drop the packet after timeout. The outstanding packet is + * released and the remaining bytes will be sent with padding byte 0x00 + * as a recovery. On the peer(host) side, the padding bytes 0x00 will be + * either dropped directly, or appended into existing outstanding packet + * thus dropped as corrupted network packet. + */ + vring->rem_padding = round_up(vring->rem_len, sizeof(u64)); + mlxbf_tmfifo_release_pkt(vring); + vring->cur_len = 0; + vring->rem_len = 0; + vring->fifo->vring[0] = NULL; + + /* + * Make sure the load/store are in order before + * returning back to virtio. + */ + virtio_mb(false); + + /* Notify upper layer. */ + spin_lock_irqsave(&vring->fifo->spin_lock[0], flags); + vring_interrupt(0, vring->vq); + spin_unlock_irqrestore(&vring->fifo->spin_lock[0], flags); +} + +/* Rx & Tx processing of a queue. */ +static void mlxbf_tmfifo_rxtx(struct mlxbf_tmfifo_vring *vring, bool is_rx) +{ + int avail = 0, devid = vring->vdev_id; + struct mlxbf_tmfifo *fifo; + bool more; + + fifo = vring->fifo; + + /* Return if vdev is not ready. */ + if (!fifo || !fifo->vdev[devid]) + return; + + /* Return if another vring is running. */ + if (fifo->vring[is_rx] && fifo->vring[is_rx] != vring) + return; + + /* Only handle console and network for now. */ + if (WARN_ON(devid != VIRTIO_ID_NET && devid != VIRTIO_ID_CONSOLE)) + return; + + do { +retry: + /* Get available FIFO space. */ + if (avail == 0) { + if (is_rx) + avail = mlxbf_tmfifo_get_rx_avail(fifo); + else + avail = mlxbf_tmfifo_get_tx_avail(fifo, devid); + if (avail <= 0) + break; + } + + /* Insert paddings for discarded Tx packet. */ + if (!is_rx) { + vring->tx_timeout = 0; + while (vring->rem_padding >= sizeof(u64)) { + writeq(0, vring->fifo->tx.data); + vring->rem_padding -= sizeof(u64); + if (--avail == 0) + goto retry; + } + } + + /* Console output always comes from the Tx buffer. */ + if (!is_rx && devid == VIRTIO_ID_CONSOLE) { + mlxbf_tmfifo_console_tx(fifo, avail); + break; + } + + /* Handle one descriptor. */ + more = mlxbf_tmfifo_rxtx_one_desc(vring, is_rx, &avail); + } while (more); + + /* Check Tx timeout. */ + if (avail <= 0 && !is_rx) + mlxbf_tmfifo_check_tx_timeout(vring); +} + +/* Handle Rx or Tx queues. */ +static void mlxbf_tmfifo_work_rxtx(struct mlxbf_tmfifo *fifo, int queue_id, + int irq_id, bool is_rx) +{ + struct mlxbf_tmfifo_vdev *tm_vdev; + struct mlxbf_tmfifo_vring *vring; + int i; + + if (!test_and_clear_bit(irq_id, &fifo->pend_events) || + !fifo->irq_info[irq_id].irq) + return; + + for (i = 0; i < MLXBF_TMFIFO_VDEV_MAX; i++) { + tm_vdev = fifo->vdev[i]; + if (tm_vdev) { + vring = &tm_vdev->vrings[queue_id]; + if (vring->vq) + mlxbf_tmfifo_rxtx(vring, is_rx); + } + } +} + +/* Work handler for Rx and Tx case. */ +static void mlxbf_tmfifo_work_handler(struct work_struct *work) +{ + struct mlxbf_tmfifo *fifo; + + fifo = container_of(work, struct mlxbf_tmfifo, work); + if (!fifo->is_ready) + return; + + mutex_lock(&fifo->lock); + + /* Tx (Send data to the TmFifo). */ + mlxbf_tmfifo_work_rxtx(fifo, MLXBF_TMFIFO_VRING_TX, + MLXBF_TM_TX_LWM_IRQ, false); + + /* Rx (Receive data from the TmFifo). */ + mlxbf_tmfifo_work_rxtx(fifo, MLXBF_TMFIFO_VRING_RX, + MLXBF_TM_RX_HWM_IRQ, true); + + mutex_unlock(&fifo->lock); +} + +/* The notify function is called when new buffers are posted. */ +static bool mlxbf_tmfifo_virtio_notify(struct virtqueue *vq) +{ + struct mlxbf_tmfifo_vring *vring = vq->priv; + struct mlxbf_tmfifo_vdev *tm_vdev; + struct mlxbf_tmfifo *fifo; + unsigned long flags; + + fifo = vring->fifo; + + /* + * Virtio maintains vrings in pairs, even number ring for Rx + * and odd number ring for Tx. + */ + if (vring->index & BIT(0)) { + /* + * Console could make blocking call with interrupts disabled. + * In such case, the vring needs to be served right away. For + * other cases, just set the TX LWM bit to start Tx in the + * worker handler. + */ + if (vring->vdev_id == VIRTIO_ID_CONSOLE) { + spin_lock_irqsave(&fifo->spin_lock[0], flags); + tm_vdev = fifo->vdev[VIRTIO_ID_CONSOLE]; + mlxbf_tmfifo_console_output(tm_vdev, vring); + spin_unlock_irqrestore(&fifo->spin_lock[0], flags); + set_bit(MLXBF_TM_TX_LWM_IRQ, &fifo->pend_events); + } else if (test_and_set_bit(MLXBF_TM_TX_LWM_IRQ, + &fifo->pend_events)) { + return true; + } + } else { + if (test_and_set_bit(MLXBF_TM_RX_HWM_IRQ, &fifo->pend_events)) + return true; + } + + schedule_work(&fifo->work); + + return true; +} + +/* Get the array of feature bits for this device. */ +static u64 mlxbf_tmfifo_virtio_get_features(struct virtio_device *vdev) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + return tm_vdev->features; +} + +/* Confirm device features to use. */ +static int mlxbf_tmfifo_virtio_finalize_features(struct virtio_device *vdev) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + tm_vdev->features = vdev->features; + + return 0; +} + +/* Free virtqueues found by find_vqs(). */ +static void mlxbf_tmfifo_virtio_del_vqs(struct virtio_device *vdev) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + struct mlxbf_tmfifo_vring *vring; + struct virtqueue *vq; + int i; + + for (i = 0; i < ARRAY_SIZE(tm_vdev->vrings); i++) { + vring = &tm_vdev->vrings[i]; + + /* Release the pending packet. */ + if (vring->desc) + mlxbf_tmfifo_release_pkt(vring); + vq = vring->vq; + if (vq) { + vring->vq = NULL; + vring_del_virtqueue(vq); + } + } +} + +/* Create and initialize the virtual queues. */ +static int mlxbf_tmfifo_virtio_find_vqs(struct virtio_device *vdev, + unsigned int nvqs, + struct virtqueue *vqs[], + struct virtqueue_info vqs_info[], + struct irq_affinity *desc) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + struct mlxbf_tmfifo_vring *vring; + struct virtqueue *vq; + int i, ret, size; + + if (nvqs > ARRAY_SIZE(tm_vdev->vrings)) + return -EINVAL; + + for (i = 0; i < nvqs; ++i) { + struct virtqueue_info *vqi = &vqs_info[i]; + + if (!vqi->name) { + ret = -EINVAL; + goto error; + } + vring = &tm_vdev->vrings[i]; + + /* zero vring */ + size = vring_size(vring->num, vring->align); + memset(vring->va, 0, size); + vq = vring_new_virtqueue(i, vring->num, vring->align, vdev, + false, false, vring->va, + mlxbf_tmfifo_virtio_notify, + vqi->callback, vqi->name); + if (!vq) { + dev_err(&vdev->dev, "vring_new_virtqueue failed\n"); + ret = -ENOMEM; + goto error; + } + + vq->num_max = vring->num; + + vq->priv = vring; + + /* Make vq update visible before using it. */ + virtio_mb(false); + + vqs[i] = vq; + vring->vq = vq; + } + + return 0; + +error: + mlxbf_tmfifo_virtio_del_vqs(vdev); + return ret; +} + +/* Read the status byte. */ +static u8 mlxbf_tmfifo_virtio_get_status(struct virtio_device *vdev) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + return tm_vdev->status; +} + +/* Write the status byte. */ +static void mlxbf_tmfifo_virtio_set_status(struct virtio_device *vdev, + u8 status) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + tm_vdev->status = status; +} + +/* Reset the device. Not much here for now. */ +static void mlxbf_tmfifo_virtio_reset(struct virtio_device *vdev) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + tm_vdev->status = 0; +} + +/* Read the value of a configuration field. */ +static void mlxbf_tmfifo_virtio_get(struct virtio_device *vdev, + unsigned int offset, + void *buf, + unsigned int len) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + if ((u64)offset + len > sizeof(tm_vdev->config)) + return; + + memcpy(buf, (u8 *)&tm_vdev->config + offset, len); +} + +/* Write the value of a configuration field. */ +static void mlxbf_tmfifo_virtio_set(struct virtio_device *vdev, + unsigned int offset, + const void *buf, + unsigned int len) +{ + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + if ((u64)offset + len > sizeof(tm_vdev->config)) + return; + + memcpy((u8 *)&tm_vdev->config + offset, buf, len); +} + +static void tmfifo_virtio_dev_release(struct device *device) +{ + struct virtio_device *vdev = + container_of(device, struct virtio_device, dev); + struct mlxbf_tmfifo_vdev *tm_vdev = mlxbf_vdev_to_tmfifo(vdev); + + kfree(tm_vdev); +} + +/* Virtio config operations. */ +static const struct virtio_config_ops mlxbf_tmfifo_virtio_config_ops = { + .get_features = mlxbf_tmfifo_virtio_get_features, + .finalize_features = mlxbf_tmfifo_virtio_finalize_features, + .find_vqs = mlxbf_tmfifo_virtio_find_vqs, + .del_vqs = mlxbf_tmfifo_virtio_del_vqs, + .reset = mlxbf_tmfifo_virtio_reset, + .set_status = mlxbf_tmfifo_virtio_set_status, + .get_status = mlxbf_tmfifo_virtio_get_status, + .get = mlxbf_tmfifo_virtio_get, + .set = mlxbf_tmfifo_virtio_set, +}; + +/* Create vdev for the FIFO. */ +static int mlxbf_tmfifo_create_vdev(struct device *dev, + struct mlxbf_tmfifo *fifo, + int vdev_id, u64 features, + void *config, u32 size) +{ + struct mlxbf_tmfifo_vdev *tm_vdev, *reg_dev = NULL; + int ret; + + mutex_lock(&fifo->lock); + + tm_vdev = fifo->vdev[vdev_id]; + if (tm_vdev) { + dev_err(dev, "vdev %d already exists\n", vdev_id); + ret = -EEXIST; + goto fail; + } + + tm_vdev = kzalloc_obj(*tm_vdev); + if (!tm_vdev) { + ret = -ENOMEM; + goto fail; + } + + tm_vdev->vdev.id.device = vdev_id; + tm_vdev->vdev.config = &mlxbf_tmfifo_virtio_config_ops; + tm_vdev->vdev.dev.parent = dev; + tm_vdev->vdev.dev.release = tmfifo_virtio_dev_release; + tm_vdev->features = features; + if (config) + memcpy(&tm_vdev->config, config, size); + + if (mlxbf_tmfifo_alloc_vrings(fifo, tm_vdev)) { + dev_err(dev, "unable to allocate vring\n"); + ret = -ENOMEM; + goto vdev_fail; + } + + /* Allocate an output buffer for the console device. */ + if (vdev_id == VIRTIO_ID_CONSOLE) + tm_vdev->tx_buf.buf = devm_kmalloc(dev, + MLXBF_TMFIFO_CON_TX_BUF_SIZE, + GFP_KERNEL); + fifo->vdev[vdev_id] = tm_vdev; + + /* Register the virtio device. */ + ret = register_virtio_device(&tm_vdev->vdev); + reg_dev = tm_vdev; + if (ret) { + dev_err(dev, "register_virtio_device failed\n"); + goto vdev_fail; + } + + mutex_unlock(&fifo->lock); + return 0; + +vdev_fail: + mlxbf_tmfifo_free_vrings(fifo, tm_vdev); + fifo->vdev[vdev_id] = NULL; + if (reg_dev) + put_device(&tm_vdev->vdev.dev); + else + kfree(tm_vdev); +fail: + mutex_unlock(&fifo->lock); + return ret; +} + +/* Delete vdev for the FIFO. */ +static int mlxbf_tmfifo_delete_vdev(struct mlxbf_tmfifo *fifo, int vdev_id) +{ + struct mlxbf_tmfifo_vdev *tm_vdev; + + mutex_lock(&fifo->lock); + + /* Unregister vdev. */ + tm_vdev = fifo->vdev[vdev_id]; + if (tm_vdev) { + unregister_virtio_device(&tm_vdev->vdev); + mlxbf_tmfifo_free_vrings(fifo, tm_vdev); + fifo->vdev[vdev_id] = NULL; + } + + mutex_unlock(&fifo->lock); + + return 0; +} + +/* Read the configured network MAC address from efi variable. */ +static void mlxbf_tmfifo_get_cfg_mac(u8 *mac) +{ + efi_guid_t guid = EFI_GLOBAL_VARIABLE_GUID; + unsigned long size = ETH_ALEN; + u8 buf[ETH_ALEN]; + efi_status_t rc; + + rc = efi.get_variable(mlxbf_tmfifo_efi_name, &guid, NULL, &size, buf); + if (rc == EFI_SUCCESS && size == ETH_ALEN) + ether_addr_copy(mac, buf); + else + ether_addr_copy(mac, mlxbf_tmfifo_net_default_mac); +} + +/* Set TmFifo thresholds which is used to trigger interrupts. */ +static void mlxbf_tmfifo_set_threshold(struct mlxbf_tmfifo *fifo) +{ + u64 ctl; + + /* Get Tx FIFO size and set the low/high watermark. */ + ctl = readq(fifo->tx.ctl); + fifo->tx_fifo_size = + FIELD_GET(MLXBF_TMFIFO_TX_CTL__MAX_ENTRIES_MASK, ctl); + ctl = (ctl & ~MLXBF_TMFIFO_TX_CTL__LWM_MASK) | + FIELD_PREP(MLXBF_TMFIFO_TX_CTL__LWM_MASK, + fifo->tx_fifo_size / 2); + ctl = (ctl & ~MLXBF_TMFIFO_TX_CTL__HWM_MASK) | + FIELD_PREP(MLXBF_TMFIFO_TX_CTL__HWM_MASK, + fifo->tx_fifo_size - 1); + writeq(ctl, fifo->tx.ctl); + + /* Get Rx FIFO size and set the low/high watermark. */ + ctl = readq(fifo->rx.ctl); + fifo->rx_fifo_size = + FIELD_GET(MLXBF_TMFIFO_RX_CTL__MAX_ENTRIES_MASK, ctl); + ctl = (ctl & ~MLXBF_TMFIFO_RX_CTL__LWM_MASK) | + FIELD_PREP(MLXBF_TMFIFO_RX_CTL__LWM_MASK, 0); + ctl = (ctl & ~MLXBF_TMFIFO_RX_CTL__HWM_MASK) | + FIELD_PREP(MLXBF_TMFIFO_RX_CTL__HWM_MASK, 1); + writeq(ctl, fifo->rx.ctl); +} + +static void mlxbf_tmfifo_cleanup(struct mlxbf_tmfifo *fifo) +{ + int i; + + fifo->is_ready = false; + timer_delete_sync(&fifo->timer); + mlxbf_tmfifo_disable_irqs(fifo); + cancel_work_sync(&fifo->work); + for (i = 0; i < MLXBF_TMFIFO_VDEV_MAX; i++) + mlxbf_tmfifo_delete_vdev(fifo, i); +} + +/* Probe the TMFIFO. */ +static int mlxbf_tmfifo_probe(struct platform_device *pdev) +{ + struct virtio_net_config net_config; + struct device *dev = &pdev->dev; + struct mlxbf_tmfifo *fifo; + u64 dev_id; + int i, rc; + + rc = acpi_dev_uid_to_integer(ACPI_COMPANION(dev), &dev_id); + if (rc) { + dev_err(dev, "Cannot retrieve UID\n"); + return rc; + } + + fifo = devm_kzalloc(dev, sizeof(*fifo), GFP_KERNEL); + if (!fifo) + return -ENOMEM; + + spin_lock_init(&fifo->spin_lock[0]); + spin_lock_init(&fifo->spin_lock[1]); + INIT_WORK(&fifo->work, mlxbf_tmfifo_work_handler); + mutex_init(&fifo->lock); + + /* Get the resource of the Rx FIFO. */ + fifo->res0 = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(fifo->res0)) + return PTR_ERR(fifo->res0); + + /* Get the resource of the Tx FIFO. */ + fifo->res1 = devm_platform_ioremap_resource(pdev, 1); + if (IS_ERR(fifo->res1)) + return PTR_ERR(fifo->res1); + + if (dev_id == TMFIFO_BF3_UID) { + fifo->rx.ctl = fifo->res1 + MLXBF_TMFIFO_RX_CTL_BF3; + fifo->rx.sts = fifo->res1 + MLXBF_TMFIFO_RX_STS_BF3; + fifo->rx.data = fifo->res0 + MLXBF_TMFIFO_RX_DATA_BF3; + fifo->tx.ctl = fifo->res1 + MLXBF_TMFIFO_TX_CTL_BF3; + fifo->tx.sts = fifo->res1 + MLXBF_TMFIFO_TX_STS_BF3; + fifo->tx.data = fifo->res0 + MLXBF_TMFIFO_TX_DATA_BF3; + } else { + fifo->rx.ctl = fifo->res0 + MLXBF_TMFIFO_RX_CTL; + fifo->rx.sts = fifo->res0 + MLXBF_TMFIFO_RX_STS; + fifo->rx.data = fifo->res0 + MLXBF_TMFIFO_RX_DATA; + fifo->tx.ctl = fifo->res1 + MLXBF_TMFIFO_TX_CTL; + fifo->tx.sts = fifo->res1 + MLXBF_TMFIFO_TX_STS; + fifo->tx.data = fifo->res1 + MLXBF_TMFIFO_TX_DATA; + } + + platform_set_drvdata(pdev, fifo); + + timer_setup(&fifo->timer, mlxbf_tmfifo_timer, 0); + + for (i = 0; i < MLXBF_TM_MAX_IRQ; i++) { + fifo->irq_info[i].index = i; + fifo->irq_info[i].fifo = fifo; + fifo->irq_info[i].irq = platform_get_irq(pdev, i); + rc = devm_request_irq(dev, fifo->irq_info[i].irq, + mlxbf_tmfifo_irq_handler, 0, + "tmfifo", &fifo->irq_info[i]); + if (rc) { + dev_err(dev, "devm_request_irq failed\n"); + fifo->irq_info[i].irq = 0; + return rc; + } + } + + mlxbf_tmfifo_set_threshold(fifo); + + /* Create the console vdev. */ + rc = mlxbf_tmfifo_create_vdev(dev, fifo, VIRTIO_ID_CONSOLE, 0, NULL, 0); + if (rc) + goto fail; + + /* Create the network vdev. */ + memset(&net_config, 0, sizeof(net_config)); + + /* A legacy-only interface for now. */ + net_config.mtu = __cpu_to_virtio16(virtio_legacy_is_little_endian(), + ETH_DATA_LEN); + net_config.status = __cpu_to_virtio16(virtio_legacy_is_little_endian(), + VIRTIO_NET_S_LINK_UP); + mlxbf_tmfifo_get_cfg_mac(net_config.mac); + rc = mlxbf_tmfifo_create_vdev(dev, fifo, VIRTIO_ID_NET, + MLXBF_TMFIFO_NET_FEATURES, &net_config, + sizeof(net_config)); + if (rc) + goto fail; + + mod_timer(&fifo->timer, jiffies + MLXBF_TMFIFO_TIMER_INTERVAL); + + /* Make all updates visible before setting the 'is_ready' flag. */ + virtio_mb(false); + + fifo->is_ready = true; + return 0; + +fail: + mlxbf_tmfifo_cleanup(fifo); + return rc; +} + +/* Device remove function. */ +static void mlxbf_tmfifo_remove(struct platform_device *pdev) +{ + struct mlxbf_tmfifo *fifo = platform_get_drvdata(pdev); + + mlxbf_tmfifo_cleanup(fifo); +} + +static const struct acpi_device_id mlxbf_tmfifo_acpi_match[] = { + { "MLNXBF01", 0 }, + {} +}; +MODULE_DEVICE_TABLE(acpi, mlxbf_tmfifo_acpi_match); + +static struct platform_driver mlxbf_tmfifo_driver = { + .probe = mlxbf_tmfifo_probe, + .remove = mlxbf_tmfifo_remove, + .driver = { + .name = "bf-tmfifo", + .acpi_match_table = mlxbf_tmfifo_acpi_match, + }, +}; + +module_platform_driver(mlxbf_tmfifo_driver); + +MODULE_DESCRIPTION("Mellanox BlueField SoC TmFifo Driver"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Mellanox Technologies"); diff --git a/drivers/platform/mellanox/mlxreg-dpu.c b/drivers/platform/mellanox/mlxreg-dpu.c new file mode 100644 index 000000000..39f89c471 --- /dev/null +++ b/drivers/platform/mellanox/mlxreg-dpu.c @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Nvidia Data Processor Unit platform driver + * + * Copyright (C) 2025 Nvidia Technologies Ltd. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* I2C bus IO offsets */ +#define MLXREG_DPU_REG_FPGA1_VER_OFFSET 0x2400 +#define MLXREG_DPU_REG_FPGA1_PN_OFFSET 0x2404 +#define MLXREG_DPU_REG_FPGA1_PN1_OFFSET 0x2405 +#define MLXREG_DPU_REG_PG_OFFSET 0x2414 +#define MLXREG_DPU_REG_PG_EVENT_OFFSET 0x2415 +#define MLXREG_DPU_REG_PG_MASK_OFFSET 0x2416 +#define MLXREG_DPU_REG_RESET_GP1_OFFSET 0x2417 +#define MLXREG_DPU_REG_RST_CAUSE1_OFFSET 0x241e +#define MLXREG_DPU_REG_GP0_RO_OFFSET 0x242b +#define MLXREG_DPU_REG_GP0_OFFSET 0x242e +#define MLXREG_DPU_REG_GP1_OFFSET 0x242c +#define MLXREG_DPU_REG_GP4_OFFSET 0x2438 +#define MLXREG_DPU_REG_AGGRCO_OFFSET 0x2442 +#define MLXREG_DPU_REG_AGGRCO_MASK_OFFSET 0x2443 +#define MLXREG_DPU_REG_HEALTH_OFFSET 0x244d +#define MLXREG_DPU_REG_HEALTH_EVENT_OFFSET 0x244e +#define MLXREG_DPU_REG_HEALTH_MASK_OFFSET 0x244f +#define MLXREG_DPU_REG_FPGA1_MVER_OFFSET 0x24de +#define MLXREG_DPU_REG_CONFIG3_OFFSET 0x24fd +#define MLXREG_DPU_REG_MAX 0x3fff + +/* Power Good event masks. */ +#define MLXREG_DPU_PG_VDDIO_MASK BIT(0) +#define MLXREG_DPU_PG_VDD_CPU_MASK BIT(1) +#define MLXREG_DPU_PG_VDD_MASK BIT(2) +#define MLXREG_DPU_PG_1V8_MASK BIT(3) +#define MLXREG_DPU_PG_COMPARATOR_MASK BIT(4) +#define MLXREG_DPU_PG_VDDQ_MASK BIT(5) +#define MLXREG_DPU_PG_HVDD_MASK BIT(6) +#define MLXREG_DPU_PG_DVDD_MASK BIT(7) +#define MLXREG_DPU_PG_MASK (MLXREG_DPU_PG_DVDD_MASK | \ + MLXREG_DPU_PG_HVDD_MASK | \ + MLXREG_DPU_PG_VDDQ_MASK | \ + MLXREG_DPU_PG_COMPARATOR_MASK | \ + MLXREG_DPU_PG_1V8_MASK | \ + MLXREG_DPU_PG_VDD_CPU_MASK | \ + MLXREG_DPU_PG_VDD_MASK | \ + MLXREG_DPU_PG_VDDIO_MASK) + +/* Health event masks. */ +#define MLXREG_DPU_HLTH_THERMAL_TRIP_MASK BIT(0) +#define MLXREG_DPU_HLTH_UFM_UPGRADE_DONE_MASK BIT(1) +#define MLXREG_DPU_HLTH_VDDQ_HOT_ALERT_MASK BIT(2) +#define MLXREG_DPU_HLTH_VDD_CPU_HOT_ALERT_MASK BIT(3) +#define MLXREG_DPU_HLTH_VDDQ_ALERT_MASK BIT(4) +#define MLXREG_DPU_HLTH_VDD_CPU_ALERT_MASK BIT(5) +#define MLXREG_DPU_HEALTH_MASK (MLXREG_DPU_HLTH_UFM_UPGRADE_DONE_MASK | \ + MLXREG_DPU_HLTH_VDDQ_HOT_ALERT_MASK | \ + MLXREG_DPU_HLTH_VDD_CPU_HOT_ALERT_MASK | \ + MLXREG_DPU_HLTH_VDDQ_ALERT_MASK | \ + MLXREG_DPU_HLTH_VDD_CPU_ALERT_MASK | \ + MLXREG_DPU_HLTH_THERMAL_TRIP_MASK) + +/* Hotplug aggregation masks. */ +#define MLXREG_DPU_HEALTH_AGGR_MASK BIT(0) +#define MLXREG_DPU_PG_AGGR_MASK BIT(1) +#define MLXREG_DPU_AGGR_MASK (MLXREG_DPU_HEALTH_AGGR_MASK | \ + MLXREG_DPU_PG_AGGR_MASK) + +/* Voltage regulator firmware update status mask. */ +#define MLXREG_DPU_VOLTREG_UPD_MASK GENMASK(5, 4) + +#define MLXREG_DPU_NR_NONE (-1) + +/* + * enum mlxreg_dpu_type - Data Processor Unit types + * + * @MLXREG_DPU_BF3: DPU equipped with BF3 SoC; + */ +enum mlxreg_dpu_type { + MLXREG_DPU_BF3 = 0x0050, +}; + +/* Default register access data. */ +static struct mlxreg_core_data mlxreg_dpu_io_data[] = { + { + .label = "fpga1_version", + .reg = MLXREG_DPU_REG_FPGA1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "fpga1_pn", + .reg = MLXREG_DPU_REG_FPGA1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "fpga1_version_min", + .reg = MLXREG_DPU_REG_FPGA1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "perst_rst", + .reg = MLXREG_DPU_REG_RESET_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "usbphy_rst", + .reg = MLXREG_DPU_REG_RESET_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "phy_rst", + .reg = MLXREG_DPU_REG_RESET_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0644, + }, + { + .label = "tpm_rst", + .reg = MLXREG_DPU_REG_RESET_GP1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + }, + { + .label = "reset_from_main_board", + .reg = MLXREG_DPU_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_reload", + .reg = MLXREG_DPU_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_comex_pwr_fail", + .reg = MLXREG_DPU_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_dpu_thermal", + .reg = MLXREG_DPU_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_pwr_off", + .reg = MLXREG_DPU_REG_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "dpu_id", + .reg = MLXREG_DPU_REG_GP0_RO_OFFSET, + .bit = GENMASK(3, 0), + .mode = 0444, + }, + { + .label = "voltreg_update_status", + .reg = MLXREG_DPU_REG_GP0_RO_OFFSET, + .mask = MLXREG_DPU_VOLTREG_UPD_MASK, + .bit = 5, + .mode = 0444, + }, + { + .label = "boot_progress", + .reg = MLXREG_DPU_REG_GP1_OFFSET, + .mask = GENMASK(3, 0), + .mode = 0444, + }, + { + .label = "ufm_upgrade", + .reg = MLXREG_DPU_REG_GP4_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, +}; + +static struct mlxreg_core_platform_data mlxreg_dpu_default_regs_io_data = { + .data = mlxreg_dpu_io_data, + .counter = ARRAY_SIZE(mlxreg_dpu_io_data), +}; + +/* Default hotplug data. */ +static struct mlxreg_core_data mlxreg_dpu_power_events_items_data[] = { + { + .label = "pg_vddio", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_VDDIO_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_vdd_cpu", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_VDD_CPU_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_vdd", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_VDD_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_1v8", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_1V8_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_comparator", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_COMPARATOR_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_vddq", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_VDDQ_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_hvdd", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_HVDD_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "pg_dvdd", + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_DVDD_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, +}; + +static struct mlxreg_core_data mlxreg_dpu_health_events_items_data[] = { + { + .label = "thermal_trip", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_THERMAL_TRIP_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "ufm_upgrade_done", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_UFM_UPGRADE_DONE_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "vddq_hot_alert", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_VDDQ_HOT_ALERT_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "vdd_cpu_hot_alert", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_VDD_CPU_HOT_ALERT_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "vddq_alert", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_VDDQ_ALERT_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, + { + .label = "vdd_cpu_alert", + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HLTH_VDD_CPU_ALERT_MASK, + .hpdev.nr = MLXREG_DPU_NR_NONE, + }, +}; + +static struct mlxreg_core_item mlxreg_dpu_hotplug_items[] = { + { + .data = mlxreg_dpu_power_events_items_data, + .aggr_mask = MLXREG_DPU_PG_AGGR_MASK, + .reg = MLXREG_DPU_REG_PG_OFFSET, + .mask = MLXREG_DPU_PG_MASK, + .count = ARRAY_SIZE(mlxreg_dpu_power_events_items_data), + .health = false, + .inversed = 0, + }, + { + .data = mlxreg_dpu_health_events_items_data, + .aggr_mask = MLXREG_DPU_HEALTH_AGGR_MASK, + .reg = MLXREG_DPU_REG_HEALTH_OFFSET, + .mask = MLXREG_DPU_HEALTH_MASK, + .count = ARRAY_SIZE(mlxreg_dpu_health_events_items_data), + .health = false, + .inversed = 0, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data mlxreg_dpu_default_hotplug_data = { + .items = mlxreg_dpu_hotplug_items, + .count = ARRAY_SIZE(mlxreg_dpu_hotplug_items), + .cell = MLXREG_DPU_REG_AGGRCO_OFFSET, + .mask = MLXREG_DPU_AGGR_MASK, +}; + +/** + * struct mlxreg_dpu - device private data + * @dev: platform device + * @data: platform core data + * @io_data: register access platform data + * @io_regs: register access device + * @hotplug_data: hotplug platform data + * @hotplug: hotplug device + */ +struct mlxreg_dpu { + struct device *dev; + struct mlxreg_core_data *data; + struct mlxreg_core_platform_data *io_data; + struct platform_device *io_regs; + struct mlxreg_core_hotplug_platform_data *hotplug_data; + struct platform_device *hotplug; +}; + +static bool mlxreg_dpu_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_DPU_REG_PG_EVENT_OFFSET: + case MLXREG_DPU_REG_PG_MASK_OFFSET: + case MLXREG_DPU_REG_RESET_GP1_OFFSET: + case MLXREG_DPU_REG_GP0_OFFSET: + case MLXREG_DPU_REG_GP1_OFFSET: + case MLXREG_DPU_REG_GP4_OFFSET: + case MLXREG_DPU_REG_AGGRCO_OFFSET: + case MLXREG_DPU_REG_AGGRCO_MASK_OFFSET: + case MLXREG_DPU_REG_HEALTH_EVENT_OFFSET: + case MLXREG_DPU_REG_HEALTH_MASK_OFFSET: + return true; + } + return false; +} + +static bool mlxreg_dpu_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_DPU_REG_FPGA1_VER_OFFSET: + case MLXREG_DPU_REG_FPGA1_PN_OFFSET: + case MLXREG_DPU_REG_FPGA1_PN1_OFFSET: + case MLXREG_DPU_REG_PG_OFFSET: + case MLXREG_DPU_REG_PG_EVENT_OFFSET: + case MLXREG_DPU_REG_PG_MASK_OFFSET: + case MLXREG_DPU_REG_RESET_GP1_OFFSET: + case MLXREG_DPU_REG_RST_CAUSE1_OFFSET: + case MLXREG_DPU_REG_GP0_RO_OFFSET: + case MLXREG_DPU_REG_GP0_OFFSET: + case MLXREG_DPU_REG_GP1_OFFSET: + case MLXREG_DPU_REG_GP4_OFFSET: + case MLXREG_DPU_REG_AGGRCO_OFFSET: + case MLXREG_DPU_REG_AGGRCO_MASK_OFFSET: + case MLXREG_DPU_REG_HEALTH_OFFSET: + case MLXREG_DPU_REG_HEALTH_EVENT_OFFSET: + case MLXREG_DPU_REG_HEALTH_MASK_OFFSET: + case MLXREG_DPU_REG_FPGA1_MVER_OFFSET: + case MLXREG_DPU_REG_CONFIG3_OFFSET: + return true; + } + return false; +} + +static bool mlxreg_dpu_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_DPU_REG_FPGA1_VER_OFFSET: + case MLXREG_DPU_REG_FPGA1_PN_OFFSET: + case MLXREG_DPU_REG_FPGA1_PN1_OFFSET: + case MLXREG_DPU_REG_PG_OFFSET: + case MLXREG_DPU_REG_PG_EVENT_OFFSET: + case MLXREG_DPU_REG_PG_MASK_OFFSET: + case MLXREG_DPU_REG_RESET_GP1_OFFSET: + case MLXREG_DPU_REG_RST_CAUSE1_OFFSET: + case MLXREG_DPU_REG_GP0_RO_OFFSET: + case MLXREG_DPU_REG_GP0_OFFSET: + case MLXREG_DPU_REG_GP1_OFFSET: + case MLXREG_DPU_REG_GP4_OFFSET: + case MLXREG_DPU_REG_AGGRCO_OFFSET: + case MLXREG_DPU_REG_AGGRCO_MASK_OFFSET: + case MLXREG_DPU_REG_HEALTH_OFFSET: + case MLXREG_DPU_REG_HEALTH_EVENT_OFFSET: + case MLXREG_DPU_REG_HEALTH_MASK_OFFSET: + case MLXREG_DPU_REG_FPGA1_MVER_OFFSET: + case MLXREG_DPU_REG_CONFIG3_OFFSET: + return true; + } + return false; +} + +/* Configuration for the register map of a device with 2 bytes address space. */ +static const struct regmap_config mlxreg_dpu_regmap_conf = { + .reg_bits = 16, + .val_bits = 8, + .max_register = MLXREG_DPU_REG_MAX, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxreg_dpu_writeable_reg, + .readable_reg = mlxreg_dpu_readable_reg, + .volatile_reg = mlxreg_dpu_volatile_reg, +}; + +static int +mlxreg_dpu_copy_hotplug_data(struct device *dev, struct mlxreg_dpu *mlxreg_dpu, + const struct mlxreg_core_hotplug_platform_data *hotplug_data) +{ + struct mlxreg_core_item *item; + int i; + + mlxreg_dpu->hotplug_data = devm_kmemdup(dev, hotplug_data, + sizeof(*mlxreg_dpu->hotplug_data), GFP_KERNEL); + if (!mlxreg_dpu->hotplug_data) + return -ENOMEM; + + mlxreg_dpu->hotplug_data->items = devm_kmemdup(dev, hotplug_data->items, + mlxreg_dpu->hotplug_data->count * + sizeof(*mlxreg_dpu->hotplug_data->items), + GFP_KERNEL); + if (!mlxreg_dpu->hotplug_data->items) + return -ENOMEM; + + item = mlxreg_dpu->hotplug_data->items; + for (i = 0; i < hotplug_data->count; i++, item++) { + item->data = devm_kmemdup(dev, hotplug_data->items[i].data, + hotplug_data->items[i].count * sizeof(*item->data), + GFP_KERNEL); + if (!item->data) + return -ENOMEM; + } + + return 0; +} + +static int mlxreg_dpu_config_init(struct mlxreg_dpu *mlxreg_dpu, void *regmap, + struct mlxreg_core_data *data, int irq) +{ + struct device *dev = &data->hpdev.client->dev; + u32 regval; + int err; + + /* Validate DPU type. */ + err = regmap_read(regmap, MLXREG_DPU_REG_CONFIG3_OFFSET, ®val); + if (err) + return err; + + switch (regval) { + case MLXREG_DPU_BF3: + /* Copy platform specific hotplug data. */ + err = mlxreg_dpu_copy_hotplug_data(dev, mlxreg_dpu, + &mlxreg_dpu_default_hotplug_data); + if (err) + return err; + + mlxreg_dpu->io_data = &mlxreg_dpu_default_regs_io_data; + + break; + default: + return -ENODEV; + } + + /* Register IO access driver. */ + if (mlxreg_dpu->io_data) { + mlxreg_dpu->io_data->regmap = regmap; + mlxreg_dpu->io_regs = + platform_device_register_resndata(dev, "mlxreg-io", + data->slot, NULL, 0, + mlxreg_dpu->io_data, + sizeof(*mlxreg_dpu->io_data)); + if (IS_ERR(mlxreg_dpu->io_regs)) { + dev_err(dev, "Failed to create region for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, + data->hpdev.brdinfo->addr); + return PTR_ERR(mlxreg_dpu->io_regs); + } + } + + /* Register hotplug driver. */ + if (mlxreg_dpu->hotplug_data && irq) { + mlxreg_dpu->hotplug_data->regmap = regmap; + mlxreg_dpu->hotplug_data->irq = irq; + mlxreg_dpu->hotplug = + platform_device_register_resndata(dev, "mlxreg-hotplug", + data->slot, NULL, 0, + mlxreg_dpu->hotplug_data, + sizeof(*mlxreg_dpu->hotplug_data)); + if (IS_ERR(mlxreg_dpu->hotplug)) { + err = PTR_ERR(mlxreg_dpu->hotplug); + goto fail_register_hotplug; + } + } + + return 0; + +fail_register_hotplug: + platform_device_unregister(mlxreg_dpu->io_regs); + + return err; +} + +static void mlxreg_dpu_config_exit(struct mlxreg_dpu *mlxreg_dpu) +{ + platform_device_unregister(mlxreg_dpu->hotplug); + platform_device_unregister(mlxreg_dpu->io_regs); +} + +static int mlxreg_dpu_probe(struct platform_device *pdev) +{ + struct mlxreg_core_data *data; + struct mlxreg_dpu *mlxreg_dpu; + void *regmap; + int err; + + data = dev_get_platdata(&pdev->dev); + if (!data || !data->hpdev.brdinfo) + return -EINVAL; + + data->hpdev.adapter = i2c_get_adapter(data->hpdev.nr); + if (!data->hpdev.adapter) + return -EPROBE_DEFER; + + mlxreg_dpu = devm_kzalloc(&pdev->dev, sizeof(*mlxreg_dpu), GFP_KERNEL); + if (!mlxreg_dpu) { + err = -ENOMEM; + goto alloc_fail; + } + + /* Create device at the top of DPU I2C tree. */ + data->hpdev.client = i2c_new_client_device(data->hpdev.adapter, + data->hpdev.brdinfo); + if (IS_ERR(data->hpdev.client)) { + dev_err(&pdev->dev, "Failed to create client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + err = PTR_ERR(data->hpdev.client); + goto i2c_new_device_fail; + } + + regmap = devm_regmap_init_i2c(data->hpdev.client, &mlxreg_dpu_regmap_conf); + if (IS_ERR(regmap)) { + dev_err(&pdev->dev, "Failed to create regmap for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + err = PTR_ERR(regmap); + goto devm_regmap_init_i2c_fail; + } + + /* Sync registers with hardware. */ + regcache_mark_dirty(regmap); + err = regcache_sync(regmap); + if (err) { + dev_err(&pdev->dev, "Failed to sync regmap for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + goto regcache_sync_fail; + } + + mlxreg_dpu->data = data; + mlxreg_dpu->dev = &pdev->dev; + platform_set_drvdata(pdev, mlxreg_dpu); + + err = mlxreg_dpu_config_init(mlxreg_dpu, regmap, data, data->hpdev.brdinfo->irq); + if (err) + goto mlxreg_dpu_config_init_fail; + + return err; + +mlxreg_dpu_config_init_fail: +regcache_sync_fail: +devm_regmap_init_i2c_fail: + i2c_unregister_device(data->hpdev.client); +i2c_new_device_fail: +alloc_fail: + i2c_put_adapter(data->hpdev.adapter); + return err; +} + +static void mlxreg_dpu_remove(struct platform_device *pdev) +{ + struct mlxreg_core_data *data = dev_get_platdata(&pdev->dev); + struct mlxreg_dpu *mlxreg_dpu = platform_get_drvdata(pdev); + + mlxreg_dpu_config_exit(mlxreg_dpu); + i2c_unregister_device(data->hpdev.client); + i2c_put_adapter(data->hpdev.adapter); +} + +static struct platform_driver mlxreg_dpu_driver = { + .probe = mlxreg_dpu_probe, + .remove = mlxreg_dpu_remove, + .driver = { + .name = "mlxreg-dpu", + }, +}; + +module_platform_driver(mlxreg_dpu_driver); + +MODULE_AUTHOR("Vadim Pasternak "); +MODULE_DESCRIPTION("Nvidia Data Processor Unit platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:mlxreg-dpu"); diff --git a/drivers/platform/mellanox/mlxreg-hotplug.c b/drivers/platform/mellanox/mlxreg-hotplug.c new file mode 100644 index 000000000..d246772aa --- /dev/null +++ b/drivers/platform/mellanox/mlxreg-hotplug.c @@ -0,0 +1,797 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Mellanox hotplug driver + * + * Copyright (C) 2016-2020 Mellanox Technologies + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Offset of event and mask registers from status register. */ +#define MLXREG_HOTPLUG_EVENT_OFF 1 +#define MLXREG_HOTPLUG_MASK_OFF 2 +#define MLXREG_HOTPLUG_AGGR_MASK_OFF 1 + +/* ASIC good health mask. */ +#define MLXREG_HOTPLUG_GOOD_HEALTH_MASK 0x02 + +#define MLXREG_HOTPLUG_ATTRS_MAX 128 +#define MLXREG_HOTPLUG_NOT_ASSERT 3 + +/** + * struct mlxreg_hotplug_priv_data - platform private data: + * @irq: platform device interrupt number; + * @dev: basic device; + * @pdev: platform device; + * @plat: platform data; + * @regmap: register map handle; + * @dwork_irq: delayed work template; + * @lock: spin lock; + * @hwmon: hwmon device; + * @mlxreg_hotplug_attr: sysfs attributes array; + * @mlxreg_hotplug_dev_attr: sysfs sensor device attribute array; + * @group: sysfs attribute group; + * @groups: list of sysfs attribute group for hwmon registration; + * @cell: location of top aggregation interrupt register; + * @mask: top aggregation interrupt common mask; + * @aggr_cache: last value of aggregation register status; + * @after_probe: flag indication probing completion; + * @not_asserted: number of entries in workqueue with no signal assertion; + */ +struct mlxreg_hotplug_priv_data { + int irq; + struct device *dev; + struct platform_device *pdev; + struct mlxreg_hotplug_platform_data *plat; + struct regmap *regmap; + struct delayed_work dwork_irq; + spinlock_t lock; /* sync with interrupt */ + struct device *hwmon; + struct attribute *mlxreg_hotplug_attr[MLXREG_HOTPLUG_ATTRS_MAX + 1]; + struct sensor_device_attribute_2 + mlxreg_hotplug_dev_attr[MLXREG_HOTPLUG_ATTRS_MAX]; + struct attribute_group group; + const struct attribute_group *groups[2]; + u32 cell; + u32 mask; + u32 aggr_cache; + bool after_probe; + u8 not_asserted; +}; + +/* Environment variables array for udev. */ +static char *mlxreg_hotplug_udev_envp[] = { NULL, NULL }; + +static int +mlxreg_hotplug_udev_event_send(struct kobject *kobj, + struct mlxreg_core_data *data, bool action) +{ + char event_str[MLXREG_CORE_LABEL_MAX_SIZE + 2]; + char label[MLXREG_CORE_LABEL_MAX_SIZE] = { 0 }; + + mlxreg_hotplug_udev_envp[0] = event_str; + string_upper(label, data->label); + snprintf(event_str, MLXREG_CORE_LABEL_MAX_SIZE, "%s=%d", label, !!action); + + return kobject_uevent_env(kobj, KOBJ_CHANGE, mlxreg_hotplug_udev_envp); +} + +static void +mlxreg_hotplug_pdata_export(void *pdata, void *regmap) +{ + struct mlxreg_core_hotplug_platform_data *dev_pdata = pdata; + + /* Export regmap to underlying device. */ + dev_pdata->regmap = regmap; +} + +static int mlxreg_hotplug_device_create(struct mlxreg_hotplug_priv_data *priv, + struct mlxreg_core_data *data, + enum mlxreg_hotplug_kind kind) +{ + struct i2c_board_info *brdinfo = data->hpdev.brdinfo; + struct mlxreg_core_hotplug_platform_data *pdata; + struct i2c_client *client; + + /* Notify user by sending hwmon uevent. */ + mlxreg_hotplug_udev_event_send(&priv->hwmon->kobj, data, true); + + /* + * Return if adapter number is negative. It could be in case hotplug + * event is not associated with hotplug device. + */ + if (data->hpdev.nr < 0 && data->hpdev.action != MLXREG_HOTPLUG_DEVICE_NO_ACTION) + return 0; + + pdata = dev_get_platdata(&priv->pdev->dev); + switch (data->hpdev.action) { + case MLXREG_HOTPLUG_DEVICE_DEFAULT_ACTION: + data->hpdev.adapter = i2c_get_adapter(data->hpdev.nr + + pdata->shift_nr); + if (!data->hpdev.adapter) { + dev_err(priv->dev, "Failed to get adapter for bus %d\n", + data->hpdev.nr + pdata->shift_nr); + return -EFAULT; + } + + /* Export platform data to underlying device. */ + if (brdinfo->platform_data) + mlxreg_hotplug_pdata_export(brdinfo->platform_data, pdata->regmap); + + client = i2c_new_client_device(data->hpdev.adapter, + brdinfo); + if (IS_ERR(client)) { + dev_err(priv->dev, "Failed to create client %s at bus %d at addr 0x%02x\n", + brdinfo->type, data->hpdev.nr + + pdata->shift_nr, brdinfo->addr); + + i2c_put_adapter(data->hpdev.adapter); + data->hpdev.adapter = NULL; + return PTR_ERR(client); + } + + data->hpdev.client = client; + break; + case MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION: + /* Export platform data to underlying device. */ + if (data->hpdev.brdinfo && data->hpdev.brdinfo->platform_data) + mlxreg_hotplug_pdata_export(data->hpdev.brdinfo->platform_data, + pdata->regmap); + /* Pass parent hotplug device handle to underlying device. */ + data->notifier = data->hpdev.notifier; + data->hpdev.pdev = platform_device_register_resndata(&priv->pdev->dev, + brdinfo->type, + data->hpdev.nr, + NULL, 0, data, + sizeof(*data)); + if (IS_ERR(data->hpdev.pdev)) + return PTR_ERR(data->hpdev.pdev); + + break; + default: + break; + } + + if (data->hpdev.notifier && data->hpdev.notifier->user_handler) + return data->hpdev.notifier->user_handler(data->hpdev.notifier->handle, kind, 1); + + return 0; +} + +static void +mlxreg_hotplug_device_destroy(struct mlxreg_hotplug_priv_data *priv, + struct mlxreg_core_data *data, + enum mlxreg_hotplug_kind kind) +{ + /* Notify user by sending hwmon uevent. */ + mlxreg_hotplug_udev_event_send(&priv->hwmon->kobj, data, false); + if (data->hpdev.notifier && data->hpdev.notifier->user_handler) + data->hpdev.notifier->user_handler(data->hpdev.notifier->handle, kind, 0); + + switch (data->hpdev.action) { + case MLXREG_HOTPLUG_DEVICE_DEFAULT_ACTION: + if (data->hpdev.client) { + i2c_unregister_device(data->hpdev.client); + data->hpdev.client = NULL; + } + + if (data->hpdev.adapter) { + i2c_put_adapter(data->hpdev.adapter); + data->hpdev.adapter = NULL; + } + break; + case MLXREG_HOTPLUG_DEVICE_PLATFORM_ACTION: + if (data->hpdev.pdev) + platform_device_unregister(data->hpdev.pdev); + break; + default: + break; + } +} + +static ssize_t mlxreg_hotplug_attr_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct mlxreg_hotplug_priv_data *priv = dev_get_drvdata(dev); + struct mlxreg_core_hotplug_platform_data *pdata; + int index = to_sensor_dev_attr_2(attr)->index; + int nr = to_sensor_dev_attr_2(attr)->nr; + struct mlxreg_core_item *item; + struct mlxreg_core_data *data; + u32 regval; + int ret; + + pdata = dev_get_platdata(&priv->pdev->dev); + item = pdata->items + nr; + data = item->data + index; + + ret = regmap_read(priv->regmap, data->reg, ®val); + if (ret) + return ret; + + if (item->health) { + regval &= data->mask; + } else { + /* Bit = 0 : functional if item->inversed is true. */ + if (item->inversed) + regval = !(regval & data->mask); + else + regval = !!(regval & data->mask); + } + + return sysfs_emit(buf, "%u\n", regval); +} + +#define PRIV_ATTR(i) priv->mlxreg_hotplug_attr[i] +#define PRIV_DEV_ATTR(i) priv->mlxreg_hotplug_dev_attr[i] + +static int mlxreg_hotplug_item_label_index_get(u32 mask, u32 bit) +{ + int i, j; + + for (i = 0, j = -1; i <= bit; i++) { + if (mask & BIT(i)) + j++; + } + return j; +} + +static int mlxreg_hotplug_attr_init(struct mlxreg_hotplug_priv_data *priv) +{ + struct mlxreg_core_hotplug_platform_data *pdata; + struct mlxreg_core_item *item; + struct mlxreg_core_data *data; + unsigned long mask; + u32 regval; + int num_attrs = 0, id = 0, i, j, k, count, ret; + + pdata = dev_get_platdata(&priv->pdev->dev); + item = pdata->items; + + /* Go over all kinds of items - psu, pwr, fan. */ + for (i = 0; i < pdata->count; i++, item++) { + if (item->capability) { + /* + * Read group capability register to get actual number + * of interrupt capable components and set group mask + * accordingly. + */ + ret = regmap_read(priv->regmap, item->capability, + ®val); + if (ret) + return ret; + + item->mask = GENMASK((regval & item->mask) - 1, 0); + } + + data = item->data; + + /* Go over all unmasked units within item. */ + mask = item->mask; + k = 0; + count = item->ind ? item->ind : item->count; + for_each_set_bit(j, &mask, count) { + if (data->capability) { + /* + * Read capability register and skip non + * relevant attributes. + */ + ret = regmap_read(priv->regmap, + data->capability, ®val); + if (ret) + return ret; + + if (!(regval & data->bit)) { + data++; + continue; + } + } + + PRIV_ATTR(id) = &PRIV_DEV_ATTR(id).dev_attr.attr; + PRIV_ATTR(id)->name = devm_kasprintf(&priv->pdev->dev, + GFP_KERNEL, + data->label); + if (!PRIV_ATTR(id)->name) { + dev_err(priv->dev, "Memory allocation failed for attr %d.\n", + id); + return -ENOMEM; + } + + PRIV_DEV_ATTR(id).dev_attr.attr.name = + PRIV_ATTR(id)->name; + PRIV_DEV_ATTR(id).dev_attr.attr.mode = 0444; + PRIV_DEV_ATTR(id).dev_attr.show = + mlxreg_hotplug_attr_show; + PRIV_DEV_ATTR(id).nr = i; + PRIV_DEV_ATTR(id).index = k; + sysfs_attr_init(&PRIV_DEV_ATTR(id).dev_attr.attr); + data++; + id++; + k++; + } + num_attrs += k; + } + + priv->group.attrs = devm_kcalloc(&priv->pdev->dev, + num_attrs, + sizeof(struct attribute *), + GFP_KERNEL); + if (!priv->group.attrs) + return -ENOMEM; + + priv->group.attrs = priv->mlxreg_hotplug_attr; + priv->groups[0] = &priv->group; + priv->groups[1] = NULL; + + return 0; +} + +static void +mlxreg_hotplug_work_helper(struct mlxreg_hotplug_priv_data *priv, + struct mlxreg_core_item *item) +{ + struct mlxreg_core_data *data; + unsigned long asserted; + u32 regval, bit; + int ret; + + /* Mask event. */ + ret = regmap_write(priv->regmap, item->reg + MLXREG_HOTPLUG_MASK_OFF, + 0); + if (ret) + goto out; + + /* Read status. */ + ret = regmap_read(priv->regmap, item->reg, ®val); + if (ret) + goto out; + + /* Set asserted bits and save last status. */ + regval &= item->mask; + asserted = item->cache ^ regval; + item->cache = regval; + for_each_set_bit(bit, &asserted, 8) { + int pos; + + pos = mlxreg_hotplug_item_label_index_get(item->mask, bit); + if (pos < 0) + goto out; + + data = item->data + pos; + if (regval & BIT(bit)) { + if (item->inversed) + mlxreg_hotplug_device_destroy(priv, data, item->kind); + else + mlxreg_hotplug_device_create(priv, data, item->kind); + } else { + if (item->inversed) + mlxreg_hotplug_device_create(priv, data, item->kind); + else + mlxreg_hotplug_device_destroy(priv, data, item->kind); + } + } + + /* Acknowledge event. */ + ret = regmap_write(priv->regmap, item->reg + MLXREG_HOTPLUG_EVENT_OFF, + 0); + if (ret) + goto out; + + /* Unmask event. */ + ret = regmap_write(priv->regmap, item->reg + MLXREG_HOTPLUG_MASK_OFF, + item->mask); + + out: + if (ret) + dev_err(priv->dev, "Failed to complete workqueue.\n"); +} + +static void +mlxreg_hotplug_health_work_helper(struct mlxreg_hotplug_priv_data *priv, + struct mlxreg_core_item *item) +{ + struct mlxreg_core_data *data = item->data; + u32 regval; + int i, ret = 0; + + for (i = 0; i < item->count; i++, data++) { + /* Mask event. */ + ret = regmap_write(priv->regmap, data->reg + + MLXREG_HOTPLUG_MASK_OFF, 0); + if (ret) + goto out; + + /* Read status. */ + ret = regmap_read(priv->regmap, data->reg, ®val); + if (ret) + goto out; + + regval &= data->mask; + + if (item->cache == regval) + goto ack_event; + + /* + * ASIC health indication is provided through two bits. Bits + * value 0x2 indicates that ASIC reached the good health, value + * 0x0 indicates ASIC the bad health or dormant state and value + * 0x3 indicates the booting state. During ASIC reset it should + * pass the following states: dormant -> booting -> good. + */ + if (regval == MLXREG_HOTPLUG_GOOD_HEALTH_MASK) { + if (!data->attached) { + /* + * ASIC is in steady state. Connect associated + * device, if configured. + */ + mlxreg_hotplug_device_create(priv, data, item->kind); + data->attached = true; + } + } else { + if (data->attached) { + /* + * ASIC health is failed after ASIC has been + * in steady state. Disconnect associated + * device, if it has been connected. + */ + mlxreg_hotplug_device_destroy(priv, data, item->kind); + data->attached = false; + data->health_cntr = 0; + } + } + item->cache = regval; +ack_event: + /* Acknowledge event. */ + ret = regmap_write(priv->regmap, data->reg + + MLXREG_HOTPLUG_EVENT_OFF, 0); + if (ret) + goto out; + + /* Unmask event. */ + ret = regmap_write(priv->regmap, data->reg + + MLXREG_HOTPLUG_MASK_OFF, data->mask); + if (ret) + goto out; + } + + out: + if (ret) + dev_err(priv->dev, "Failed to complete workqueue.\n"); +} + +/* + * mlxreg_hotplug_work_handler - performs traversing of device interrupt + * registers according to the below hierarchy schema: + * + * Aggregation registers (status/mask) + * PSU registers: *---* + * *-----------------* | | + * |status/event/mask|-----> | * | + * *-----------------* | | + * Power registers: | | + * *-----------------* | | + * |status/event/mask|-----> | * | + * *-----------------* | | + * FAN registers: | |--> CPU + * *-----------------* | | + * |status/event/mask|-----> | * | + * *-----------------* | | + * ASIC registers: | | + * *-----------------* | | + * |status/event/mask|-----> | * | + * *-----------------* | | + * *---* + * + * In case some system changed are detected: FAN in/out, PSU in/out, power + * cable attached/detached, ASIC health good/bad, relevant device is created + * or destroyed. + */ +static void mlxreg_hotplug_work_handler(struct work_struct *work) +{ + struct mlxreg_core_hotplug_platform_data *pdata; + struct mlxreg_hotplug_priv_data *priv; + struct mlxreg_core_item *item; + u32 regval, aggr_asserted; + unsigned long flags; + int i, ret; + + priv = container_of(work, struct mlxreg_hotplug_priv_data, + dwork_irq.work); + pdata = dev_get_platdata(&priv->pdev->dev); + item = pdata->items; + + /* Mask aggregation event. */ + ret = regmap_write(priv->regmap, pdata->cell + + MLXREG_HOTPLUG_AGGR_MASK_OFF, 0); + if (ret < 0) + goto out; + + /* Read aggregation status. */ + ret = regmap_read(priv->regmap, pdata->cell, ®val); + if (ret) + goto out; + + regval &= pdata->mask; + aggr_asserted = priv->aggr_cache ^ regval; + priv->aggr_cache = regval; + + /* + * Handler is invoked, but no assertion is detected at top aggregation + * status level. Set aggr_asserted to mask value to allow handler extra + * run over all relevant signals to recover any missed signal. + */ + if (priv->not_asserted == MLXREG_HOTPLUG_NOT_ASSERT) { + priv->not_asserted = 0; + aggr_asserted = pdata->mask; + } + if (!aggr_asserted) + goto unmask_event; + + /* Handle topology and health configuration changes. */ + for (i = 0; i < pdata->count; i++, item++) { + if (aggr_asserted & item->aggr_mask) { + if (item->health) + mlxreg_hotplug_health_work_helper(priv, item); + else + mlxreg_hotplug_work_helper(priv, item); + } + } + + spin_lock_irqsave(&priv->lock, flags); + + /* + * It is possible, that some signals have been inserted, while + * interrupt has been masked by mlxreg_hotplug_work_handler. In this + * case such signals will be missed. In order to handle these signals + * delayed work is canceled and work task re-scheduled for immediate + * execution. It allows to handle missed signals, if any. In other case + * work handler just validates that no new signals have been received + * during masking. + */ + cancel_delayed_work(&priv->dwork_irq); + schedule_delayed_work(&priv->dwork_irq, 0); + + spin_unlock_irqrestore(&priv->lock, flags); + + return; + +unmask_event: + priv->not_asserted++; + /* Unmask aggregation event (no need acknowledge). */ + ret = regmap_write(priv->regmap, pdata->cell + + MLXREG_HOTPLUG_AGGR_MASK_OFF, pdata->mask); + + out: + if (ret) + dev_err(priv->dev, "Failed to complete workqueue.\n"); +} + +static int mlxreg_hotplug_set_irq(struct mlxreg_hotplug_priv_data *priv) +{ + struct mlxreg_core_hotplug_platform_data *pdata; + struct mlxreg_core_item *item; + struct mlxreg_core_data *data; + u32 regval; + int i, j, ret; + + pdata = dev_get_platdata(&priv->pdev->dev); + item = pdata->items; + + for (i = 0; i < pdata->count; i++, item++) { + /* Clear group presense event. */ + ret = regmap_write(priv->regmap, item->reg + + MLXREG_HOTPLUG_EVENT_OFF, 0); + if (ret) + goto out; + + /* + * Verify if hardware configuration requires to disable + * interrupt capability for some of components. + */ + data = item->data; + for (j = 0; j < item->count; j++, data++) { + /* Verify if the attribute has capability register. */ + if (data->capability) { + /* Read capability register. */ + ret = regmap_read(priv->regmap, + data->capability, ®val); + if (ret) + goto out; + + if (!(regval & data->bit)) + item->mask &= ~BIT(j); + } + } + + /* Set group initial status as mask and unmask group event. */ + if (item->inversed) { + item->cache = item->mask; + ret = regmap_write(priv->regmap, item->reg + + MLXREG_HOTPLUG_MASK_OFF, + item->mask); + if (ret) + goto out; + } + } + + /* Keep aggregation initial status as zero and unmask events. */ + ret = regmap_write(priv->regmap, pdata->cell + + MLXREG_HOTPLUG_AGGR_MASK_OFF, pdata->mask); + if (ret) + goto out; + + /* Keep low aggregation initial status as zero and unmask events. */ + if (pdata->cell_low) { + ret = regmap_write(priv->regmap, pdata->cell_low + + MLXREG_HOTPLUG_AGGR_MASK_OFF, + pdata->mask_low); + if (ret) + goto out; + } + + /* Invoke work handler for initializing hot plug devices setting. */ + mlxreg_hotplug_work_handler(&priv->dwork_irq.work); + + out: + if (ret) + dev_err(priv->dev, "Failed to set interrupts.\n"); + enable_irq(priv->irq); + return ret; +} + +static void mlxreg_hotplug_unset_irq(struct mlxreg_hotplug_priv_data *priv) +{ + struct mlxreg_core_hotplug_platform_data *pdata; + struct mlxreg_core_item *item; + struct mlxreg_core_data *data; + int count, i, j; + + pdata = dev_get_platdata(&priv->pdev->dev); + item = pdata->items; + disable_irq(priv->irq); + cancel_delayed_work_sync(&priv->dwork_irq); + + /* Mask low aggregation event, if defined. */ + if (pdata->cell_low) + regmap_write(priv->regmap, pdata->cell_low + + MLXREG_HOTPLUG_AGGR_MASK_OFF, 0); + + /* Mask aggregation event. */ + regmap_write(priv->regmap, pdata->cell + MLXREG_HOTPLUG_AGGR_MASK_OFF, + 0); + + /* Clear topology configurations. */ + for (i = 0; i < pdata->count; i++, item++) { + data = item->data; + /* Mask group presense event. */ + regmap_write(priv->regmap, data->reg + MLXREG_HOTPLUG_MASK_OFF, + 0); + /* Clear group presense event. */ + regmap_write(priv->regmap, data->reg + + MLXREG_HOTPLUG_EVENT_OFF, 0); + + /* Remove all the attached devices in group. */ + count = item->count; + for (j = 0; j < count; j++, data++) + mlxreg_hotplug_device_destroy(priv, data, item->kind); + } +} + +static irqreturn_t mlxreg_hotplug_irq_handler(int irq, void *dev) +{ + struct mlxreg_hotplug_priv_data *priv; + + priv = (struct mlxreg_hotplug_priv_data *)dev; + + /* Schedule work task for immediate execution.*/ + schedule_delayed_work(&priv->dwork_irq, 0); + + return IRQ_HANDLED; +} + +static int mlxreg_hotplug_probe(struct platform_device *pdev) +{ + struct mlxreg_core_hotplug_platform_data *pdata; + struct mlxreg_hotplug_priv_data *priv; + struct i2c_adapter *deferred_adap; + int err; + + pdata = dev_get_platdata(&pdev->dev); + if (!pdata) { + dev_err(&pdev->dev, "Failed to get platform data.\n"); + return -EINVAL; + } + + /* Defer probing if the necessary adapter is not configured yet. */ + deferred_adap = i2c_get_adapter(pdata->deferred_nr); + if (!deferred_adap) + return -EPROBE_DEFER; + i2c_put_adapter(deferred_adap); + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + if (pdata->irq) { + priv->irq = pdata->irq; + } else { + priv->irq = platform_get_irq(pdev, 0); + if (priv->irq < 0) + return priv->irq; + } + + priv->regmap = pdata->regmap; + priv->dev = pdev->dev.parent; + priv->pdev = pdev; + + err = devm_request_irq(&pdev->dev, priv->irq, + mlxreg_hotplug_irq_handler, IRQF_TRIGGER_FALLING + | IRQF_SHARED, "mlxreg-hotplug", priv); + if (err) { + dev_err(&pdev->dev, "Failed to request irq: %d\n", err); + return err; + } + + disable_irq(priv->irq); + spin_lock_init(&priv->lock); + INIT_DELAYED_WORK(&priv->dwork_irq, mlxreg_hotplug_work_handler); + dev_set_drvdata(&pdev->dev, priv); + + err = mlxreg_hotplug_attr_init(priv); + if (err) { + dev_err(&pdev->dev, "Failed to allocate attributes: %d\n", + err); + return err; + } + + priv->hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, + "mlxreg_hotplug", priv, priv->groups); + if (IS_ERR(priv->hwmon)) { + dev_err(&pdev->dev, "Failed to register hwmon device %ld\n", + PTR_ERR(priv->hwmon)); + return PTR_ERR(priv->hwmon); + } + + /* Perform initial interrupts setup. */ + mlxreg_hotplug_set_irq(priv); + priv->after_probe = true; + + return 0; +} + +static void mlxreg_hotplug_remove(struct platform_device *pdev) +{ + struct mlxreg_hotplug_priv_data *priv = dev_get_drvdata(&pdev->dev); + + /* Clean interrupts setup. */ + mlxreg_hotplug_unset_irq(priv); + devm_free_irq(&pdev->dev, priv->irq, priv); +} + +static struct platform_driver mlxreg_hotplug_driver = { + .driver = { + .name = "mlxreg-hotplug", + }, + .probe = mlxreg_hotplug_probe, + .remove = mlxreg_hotplug_remove, +}; + +module_platform_driver(mlxreg_hotplug_driver); + +MODULE_AUTHOR("Vadim Pasternak "); +MODULE_DESCRIPTION("Mellanox regmap hotplug platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:mlxreg-hotplug"); diff --git a/drivers/platform/mellanox/mlxreg-io.c b/drivers/platform/mellanox/mlxreg-io.c new file mode 100644 index 000000000..97fefe6c3 --- /dev/null +++ b/drivers/platform/mellanox/mlxreg-io.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Mellanox register access driver + * + * Copyright (C) 2018 Mellanox Technologies + * Copyright (C) 2018 Vadim Pasternak + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Attribute parameters. */ +#define MLXREG_IO_ATT_SIZE 10 +#define MLXREG_IO_ATT_NUM 96 + +/** + * struct mlxreg_io_priv_data - driver's private data: + * + * @pdev: platform device; + * @pdata: platform data; + * @hwmon: hwmon device; + * @mlxreg_io_attr: sysfs attributes array; + * @mlxreg_io_dev_attr: sysfs sensor device attribute array; + * @group: sysfs attribute group; + * @groups: list of sysfs attribute group for hwmon registration; + * @regsize: size of a register value; + * @io_lock: user access locking; + */ +struct mlxreg_io_priv_data { + struct platform_device *pdev; + struct mlxreg_core_platform_data *pdata; + struct device *hwmon; + struct attribute *mlxreg_io_attr[MLXREG_IO_ATT_NUM + 1]; + struct sensor_device_attribute mlxreg_io_dev_attr[MLXREG_IO_ATT_NUM]; + struct attribute_group group; + const struct attribute_group *groups[2]; + int regsize; + struct mutex io_lock; /* Protects user access. */ +}; + +static int +mlxreg_io_get_reg(void *regmap, struct mlxreg_core_data *data, u32 in_val, + bool rw_flag, int regsize, u32 *regval) +{ + int i, val, ret; + + ret = regmap_read(regmap, data->reg, regval); + if (ret) + goto access_error; + + /* + * There are four kinds of attributes: single bit, full register's + * bits, bit sequence, bits in few registers For the first kind field + * mask indicates which bits are not related and field bit is set zero. + * For the second kind field mask is set to zero and field bit is set + * with all bits one. No special handling for such kind of attributes - + * pass value as is. For the third kind, the field mask indicates which + * bits are related and the field bit is set to the first bit number + * (from 1 to 32) is the bit sequence. For the fourth kind - the number + * of registers which should be read for getting an attribute are + * specified through 'data->regnum' field. + */ + if (!data->bit) { + /* Single bit. */ + if (rw_flag) { + /* For show: expose effective bit value as 0 or 1. */ + *regval = !!(*regval & ~data->mask); + } else { + /* For store: set effective bit value. */ + *regval &= data->mask; + if (in_val) + *regval |= ~data->mask; + } + } else if (data->mask) { + /* Bit sequence. */ + if (rw_flag) { + /* For show: mask and shift right. */ + *regval = ror32(*regval & data->mask, (data->bit - 1)); + } else { + /* For store: shift to the position and mask. */ + in_val = rol32(in_val, data->bit - 1) & data->mask; + /* Clear relevant bits and set them to new value. */ + *regval = (*regval & ~data->mask) | in_val; + } + } else { + /* + * Some attributes could occupied few registers in case regmap + * bit size is 8 or 16. Compose such attributes from 'regnum' + * registers. Such attributes contain read-only data. + */ + for (i = 1; i < data->regnum; i++) { + ret = regmap_read(regmap, data->reg + i, &val); + if (ret) + goto access_error; + + *regval |= rol32(val, regsize * i * 8); + } + } + +access_error: + return ret; +} + +static ssize_t +mlxreg_io_attr_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct mlxreg_io_priv_data *priv = dev_get_drvdata(dev); + int index = to_sensor_dev_attr(attr)->index; + struct mlxreg_core_data *data = priv->pdata->data + index; + u32 regval = 0; + int ret; + + mutex_lock(&priv->io_lock); + + ret = mlxreg_io_get_reg(priv->pdata->regmap, data, 0, true, + priv->regsize, ®val); + if (ret) + goto access_error; + + mutex_unlock(&priv->io_lock); + + return sysfs_emit(buf, "%u\n", regval); + +access_error: + mutex_unlock(&priv->io_lock); + return ret; +} + +static ssize_t +mlxreg_io_attr_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t len) +{ + struct mlxreg_io_priv_data *priv = dev_get_drvdata(dev); + int index = to_sensor_dev_attr(attr)->index; + struct mlxreg_core_data *data = priv->pdata->data + index; + u32 input_val, regval; + int ret; + + if (len > MLXREG_IO_ATT_SIZE) + return -EINVAL; + + /* Convert buffer to input value. */ + ret = kstrtou32(buf, 0, &input_val); + if (ret) + return ret; + + mutex_lock(&priv->io_lock); + + ret = mlxreg_io_get_reg(priv->pdata->regmap, data, input_val, false, + priv->regsize, ®val); + if (ret) + goto access_error; + + ret = regmap_write(priv->pdata->regmap, data->reg, regval); + if (ret) + goto access_error; + + mutex_unlock(&priv->io_lock); + + return len; + +access_error: + mutex_unlock(&priv->io_lock); + dev_err(&priv->pdev->dev, "Bus access error\n"); + return ret; +} + +static struct device_attribute mlxreg_io_devattr_rw = { + .show = mlxreg_io_attr_show, + .store = mlxreg_io_attr_store, +}; + +static int mlxreg_io_attr_init(struct mlxreg_io_priv_data *priv) +{ + int i; + + priv->group.attrs = devm_kcalloc(&priv->pdev->dev, + priv->pdata->counter, + sizeof(struct attribute *), + GFP_KERNEL); + if (!priv->group.attrs) + return -ENOMEM; + + for (i = 0; i < priv->pdata->counter; i++) { + priv->mlxreg_io_attr[i] = + &priv->mlxreg_io_dev_attr[i].dev_attr.attr; + memcpy(&priv->mlxreg_io_dev_attr[i].dev_attr, + &mlxreg_io_devattr_rw, sizeof(struct device_attribute)); + + /* Set attribute name as a label. */ + priv->mlxreg_io_attr[i]->name = + devm_kasprintf(&priv->pdev->dev, GFP_KERNEL, + priv->pdata->data[i].label); + + if (!priv->mlxreg_io_attr[i]->name) { + dev_err(&priv->pdev->dev, "Memory allocation failed for sysfs attribute %d.\n", + i + 1); + return -ENOMEM; + } + + priv->mlxreg_io_dev_attr[i].dev_attr.attr.mode = + priv->pdata->data[i].mode; + priv->mlxreg_io_dev_attr[i].dev_attr.attr.name = + priv->mlxreg_io_attr[i]->name; + priv->mlxreg_io_dev_attr[i].index = i; + sysfs_attr_init(&priv->mlxreg_io_dev_attr[i].dev_attr.attr); + } + + priv->group.attrs = priv->mlxreg_io_attr; + priv->groups[0] = &priv->group; + priv->groups[1] = NULL; + + return 0; +} + +static int mlxreg_io_probe(struct platform_device *pdev) +{ + struct mlxreg_io_priv_data *priv; + int err; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->pdata = dev_get_platdata(&pdev->dev); + if (!priv->pdata) { + dev_err(&pdev->dev, "Failed to get platform data.\n"); + return -EINVAL; + } + + priv->pdev = pdev; + priv->regsize = regmap_get_val_bytes(priv->pdata->regmap); + if (priv->regsize < 0) + return priv->regsize; + + err = mlxreg_io_attr_init(priv); + if (err) { + dev_err(&priv->pdev->dev, "Failed to allocate attributes: %d\n", + err); + return err; + } + + priv->hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, + "mlxreg_io", + priv, + priv->groups); + if (IS_ERR(priv->hwmon)) { + dev_err(&pdev->dev, "Failed to register hwmon device %ld\n", + PTR_ERR(priv->hwmon)); + return PTR_ERR(priv->hwmon); + } + + mutex_init(&priv->io_lock); + dev_set_drvdata(&pdev->dev, priv); + + return 0; +} + +static void mlxreg_io_remove(struct platform_device *pdev) +{ + struct mlxreg_io_priv_data *priv = dev_get_drvdata(&pdev->dev); + + mutex_destroy(&priv->io_lock); +} + +static struct platform_driver mlxreg_io_driver = { + .driver = { + .name = "mlxreg-io", + }, + .probe = mlxreg_io_probe, + .remove = mlxreg_io_remove, +}; + +module_platform_driver(mlxreg_io_driver); + +MODULE_AUTHOR("Vadim Pasternak "); +MODULE_DESCRIPTION("Mellanox regmap I/O access driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:mlxreg-io"); diff --git a/drivers/platform/mellanox/mlxreg-lc.c b/drivers/platform/mellanox/mlxreg-lc.c new file mode 100644 index 000000000..d1518598d --- /dev/null +++ b/drivers/platform/mellanox/mlxreg-lc.c @@ -0,0 +1,958 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Nvidia line card driver + * + * Copyright (C) 2020 Nvidia Technologies Ltd. + */ + +#include +#include +#include +#include +#include +#include +#include + +/* I2C bus IO offsets */ +#define MLXREG_LC_REG_CPLD1_VER_OFFSET 0x2500 +#define MLXREG_LC_REG_FPGA1_VER_OFFSET 0x2501 +#define MLXREG_LC_REG_CPLD1_PN_OFFSET 0x2504 +#define MLXREG_LC_REG_FPGA1_PN_OFFSET 0x2506 +#define MLXREG_LC_REG_RESET_CAUSE_OFFSET 0x251d +#define MLXREG_LC_REG_LED1_OFFSET 0x2520 +#define MLXREG_LC_REG_GP0_OFFSET 0x252e +#define MLXREG_LC_REG_FIELD_UPGRADE 0x2534 +#define MLXREG_LC_CHANNEL_I2C_REG 0x25dc +#define MLXREG_LC_REG_CPLD1_MVER_OFFSET 0x25de +#define MLXREG_LC_REG_FPGA1_MVER_OFFSET 0x25df +#define MLXREG_LC_REG_MAX_POWER_OFFSET 0x25f1 +#define MLXREG_LC_REG_CONFIG_OFFSET 0x25fb +#define MLXREG_LC_REG_MAX 0x3fff + +/** + * enum mlxreg_lc_type - line cards types + * + * @MLXREG_LC_SN4800_C16: 100GbE line card with 16 QSFP28 ports; + */ +enum mlxreg_lc_type { + MLXREG_LC_SN4800_C16 = 0x0000, +}; + +/** + * enum mlxreg_lc_state - line cards state + * + * @MLXREG_LC_INITIALIZED: line card is initialized; + * @MLXREG_LC_POWERED: line card is powered; + * @MLXREG_LC_SYNCED: line card is synchronized between hardware and firmware; + */ +enum mlxreg_lc_state { + MLXREG_LC_INITIALIZED = BIT(0), + MLXREG_LC_POWERED = BIT(1), + MLXREG_LC_SYNCED = BIT(2), +}; + +#define MLXREG_LC_CONFIGURED (MLXREG_LC_INITIALIZED | MLXREG_LC_POWERED | MLXREG_LC_SYNCED) + +/* mlxreg_lc - device private data + * @dev: platform device; + * @lock: line card lock; + * @par_regmap: parent device regmap handle; + * @data: platform core data; + * @io_data: register access platform data; + * @led_data: LED platform data; + * @mux_data: MUX platform data; + * @led: LED device; + * @io_regs: register access device; + * @mux_brdinfo: mux configuration; + * @mux: mux devices; + * @aux_devs: I2C devices feeding by auxiliary power; + * @aux_devs_num: number of I2C devices feeding by auxiliary power; + * @main_devs: I2C devices feeding by main power; + * @main_devs_num: number of I2C devices feeding by main power; + * @state: line card state; + */ +struct mlxreg_lc { + struct device *dev; + struct mutex lock; /* line card access lock */ + void *par_regmap; + struct mlxreg_core_data *data; + struct mlxreg_core_platform_data *io_data; + struct mlxreg_core_platform_data *led_data; + struct mlxcpld_mux_plat_data *mux_data; + struct platform_device *led; + struct platform_device *io_regs; + struct i2c_board_info *mux_brdinfo; + struct platform_device *mux; + struct mlxreg_hotplug_device *aux_devs; + int aux_devs_num; + struct mlxreg_hotplug_device *main_devs; + int main_devs_num; + enum mlxreg_lc_state state; +}; + +static bool mlxreg_lc_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_LC_REG_LED1_OFFSET: + case MLXREG_LC_REG_GP0_OFFSET: + case MLXREG_LC_REG_FIELD_UPGRADE: + case MLXREG_LC_CHANNEL_I2C_REG: + return true; + } + return false; +} + +static bool mlxreg_lc_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_LC_REG_CPLD1_VER_OFFSET: + case MLXREG_LC_REG_FPGA1_VER_OFFSET: + case MLXREG_LC_REG_CPLD1_PN_OFFSET: + case MLXREG_LC_REG_FPGA1_PN_OFFSET: + case MLXREG_LC_REG_RESET_CAUSE_OFFSET: + case MLXREG_LC_REG_LED1_OFFSET: + case MLXREG_LC_REG_GP0_OFFSET: + case MLXREG_LC_REG_FIELD_UPGRADE: + case MLXREG_LC_CHANNEL_I2C_REG: + case MLXREG_LC_REG_CPLD1_MVER_OFFSET: + case MLXREG_LC_REG_FPGA1_MVER_OFFSET: + case MLXREG_LC_REG_MAX_POWER_OFFSET: + case MLXREG_LC_REG_CONFIG_OFFSET: + return true; + } + return false; +} + +static bool mlxreg_lc_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case MLXREG_LC_REG_CPLD1_VER_OFFSET: + case MLXREG_LC_REG_FPGA1_VER_OFFSET: + case MLXREG_LC_REG_CPLD1_PN_OFFSET: + case MLXREG_LC_REG_FPGA1_PN_OFFSET: + case MLXREG_LC_REG_RESET_CAUSE_OFFSET: + case MLXREG_LC_REG_LED1_OFFSET: + case MLXREG_LC_REG_GP0_OFFSET: + case MLXREG_LC_REG_FIELD_UPGRADE: + case MLXREG_LC_CHANNEL_I2C_REG: + case MLXREG_LC_REG_CPLD1_MVER_OFFSET: + case MLXREG_LC_REG_FPGA1_MVER_OFFSET: + case MLXREG_LC_REG_MAX_POWER_OFFSET: + case MLXREG_LC_REG_CONFIG_OFFSET: + return true; + } + return false; +} + +static const struct reg_default mlxreg_lc_regmap_default[] = { + { MLXREG_LC_CHANNEL_I2C_REG, 0x00 }, +}; + +/* Configuration for the register map of a device with 2 bytes address space. */ +static const struct regmap_config mlxreg_lc_regmap_conf = { + .reg_bits = 16, + .val_bits = 8, + .max_register = MLXREG_LC_REG_MAX, + .cache_type = REGCACHE_FLAT, + .writeable_reg = mlxreg_lc_writeable_reg, + .readable_reg = mlxreg_lc_readable_reg, + .volatile_reg = mlxreg_lc_volatile_reg, + .reg_defaults = mlxreg_lc_regmap_default, + .num_reg_defaults = ARRAY_SIZE(mlxreg_lc_regmap_default), +}; + +/* Default channels vector. + * It contains only the channels, which physically connected to the devices, + * empty channels are skipped. + */ +static int mlxreg_lc_chan[] = { + 0x04, 0x05, 0x06, 0x07, 0x08, 0x10, 0x20, 0x21, 0x22, 0x23, 0x40, 0x41, + 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, + 0x4e, 0x4f +}; + +/* Default mux configuration. */ +static struct mlxcpld_mux_plat_data mlxreg_lc_mux_data[] = { + { + .chan_ids = mlxreg_lc_chan, + .num_adaps = ARRAY_SIZE(mlxreg_lc_chan), + .sel_reg_addr = MLXREG_LC_CHANNEL_I2C_REG, + .reg_size = 2, + }, +}; + +/* Default mux board info. */ +static struct i2c_board_info mlxreg_lc_mux_brdinfo = { + I2C_BOARD_INFO("i2c-mux-mlxcpld", 0x32), +}; + +/* Line card default auxiliary power static devices. */ +static struct i2c_board_info mlxreg_lc_aux_pwr_devices[] = { + { + I2C_BOARD_INFO("24c32", 0x51), + }, + { + I2C_BOARD_INFO("24c32", 0x51), + }, +}; + +/* Line card default auxiliary power board info. */ +static struct mlxreg_hotplug_device mlxreg_lc_aux_pwr_brdinfo[] = { + { + .brdinfo = &mlxreg_lc_aux_pwr_devices[0], + .nr = 3, + }, + { + .brdinfo = &mlxreg_lc_aux_pwr_devices[1], + .nr = 4, + }, +}; + +/* Line card default main power static devices. */ +static struct i2c_board_info mlxreg_lc_main_pwr_devices[] = { + { + I2C_BOARD_INFO("mp2975", 0x62), + }, + { + I2C_BOARD_INFO("mp2975", 0x64), + }, + { + I2C_BOARD_INFO("max11603", 0x6d), + }, + { + I2C_BOARD_INFO("lm25066", 0x15), + }, +}; + +/* Line card default main power board info. */ +static struct mlxreg_hotplug_device mlxreg_lc_main_pwr_brdinfo[] = { + { + .brdinfo = &mlxreg_lc_main_pwr_devices[0], + .nr = 0, + }, + { + .brdinfo = &mlxreg_lc_main_pwr_devices[1], + .nr = 0, + }, + { + .brdinfo = &mlxreg_lc_main_pwr_devices[2], + .nr = 1, + }, + { + .brdinfo = &mlxreg_lc_main_pwr_devices[3], + .nr = 2, + }, +}; + +/* LED default data. */ +static struct mlxreg_core_data mlxreg_lc_led_data[] = { + { + .label = "status:green", + .reg = MLXREG_LC_REG_LED1_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "status:orange", + .reg = MLXREG_LC_REG_LED1_OFFSET, + .mask = GENMASK(7, 4), + }, +}; + +static struct mlxreg_core_platform_data mlxreg_lc_led = { + .identity = "pci", + .data = mlxreg_lc_led_data, + .counter = ARRAY_SIZE(mlxreg_lc_led_data), +}; + +/* Default register access data. */ +static struct mlxreg_core_data mlxreg_lc_io_data[] = { + { + .label = "cpld1_version", + .reg = MLXREG_LC_REG_CPLD1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "fpga1_version", + .reg = MLXREG_LC_REG_FPGA1_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = MLXREG_LC_REG_CPLD1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "fpga1_pn", + .reg = MLXREG_LC_REG_FPGA1_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "cpld1_version_min", + .reg = MLXREG_LC_REG_CPLD1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "fpga1_version_min", + .reg = MLXREG_LC_REG_FPGA1_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "reset_fpga_not_done", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_ref", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_dc_dc_pwr_fail", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_from_chassis", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_pwr_off_from_chassis", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_line_card", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_line_card_pwr_en", + .reg = MLXREG_LC_REG_RESET_CAUSE_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "cpld_upgrade_en", + .reg = MLXREG_LC_REG_FIELD_UPGRADE, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + .secured = 1, + }, + { + .label = "fpga_upgrade_en", + .reg = MLXREG_LC_REG_FIELD_UPGRADE, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + .secured = 1, + }, + { + .label = "qsfp_pwr_en", + .reg = MLXREG_LC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "vpd_wp", + .reg = MLXREG_LC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + .secured = 1, + }, + { + .label = "agb_spi_burn_en", + .reg = MLXREG_LC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0644, + .secured = 1, + }, + { + .label = "fpga_spi_burn_en", + .reg = MLXREG_LC_REG_GP0_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0644, + .secured = 1, + }, + { + .label = "max_power", + .reg = MLXREG_LC_REG_MAX_POWER_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "config", + .reg = MLXREG_LC_REG_CONFIG_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, +}; + +static struct mlxreg_core_platform_data mlxreg_lc_regs_io = { + .data = mlxreg_lc_io_data, + .counter = ARRAY_SIZE(mlxreg_lc_io_data), +}; + +static int +mlxreg_lc_create_static_devices(struct mlxreg_lc *mlxreg_lc, struct mlxreg_hotplug_device *devs, + int size) +{ + struct mlxreg_hotplug_device *dev = devs; + int i, ret; + + /* Create static I2C device feeding by auxiliary or main power. */ + for (i = 0; i < size; i++, dev++) { + dev->client = i2c_new_client_device(dev->adapter, dev->brdinfo); + if (IS_ERR(dev->client)) { + dev_err(mlxreg_lc->dev, "Failed to create client %s at bus %d at addr 0x%02x\n", + dev->brdinfo->type, dev->nr, dev->brdinfo->addr); + + dev->adapter = NULL; + ret = PTR_ERR(dev->client); + goto fail_create_static_devices; + } + } + + return 0; + +fail_create_static_devices: + while (--i >= 0) { + dev = devs + i; + i2c_unregister_device(dev->client); + dev->client = NULL; + } + return ret; +} + +static void +mlxreg_lc_destroy_static_devices(struct mlxreg_lc *mlxreg_lc, struct mlxreg_hotplug_device *devs, + int size) +{ + struct mlxreg_hotplug_device *dev = devs; + int i; + + /* Destroy static I2C device feeding by auxiliary or main power. */ + for (i = 0; i < size; i++, dev++) { + if (dev->client) { + i2c_unregister_device(dev->client); + dev->client = NULL; + } + } +} + +static int mlxreg_lc_power_on_off(struct mlxreg_lc *mlxreg_lc, u8 action) +{ + u32 regval; + int err; + + err = regmap_read(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_pwr, ®val); + if (err) + goto regmap_read_fail; + + if (action) + regval |= BIT(mlxreg_lc->data->slot - 1); + else + regval &= ~BIT(mlxreg_lc->data->slot - 1); + + err = regmap_write(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_pwr, regval); + +regmap_read_fail: + return err; +} + +static int mlxreg_lc_enable_disable(struct mlxreg_lc *mlxreg_lc, bool action) +{ + u32 regval; + int err; + + /* + * Hardware holds the line card after powering on in the disabled state. Holding line card + * in disabled state protects access to the line components, like FPGA and gearboxes. + * Line card should be enabled in order to get it in operational state. Line card could be + * disabled for moving it to non-operational state. Enabling line card does not affect the + * line card which is already has been enabled. Disabling does not affect the disabled line + * card. + */ + err = regmap_read(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_ena, ®val); + if (err) + goto regmap_read_fail; + + if (action) + regval |= BIT(mlxreg_lc->data->slot - 1); + else + regval &= ~BIT(mlxreg_lc->data->slot - 1); + + err = regmap_write(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_ena, regval); + +regmap_read_fail: + return err; +} + +static int +mlxreg_lc_sn4800_c16_config_init(struct mlxreg_lc *mlxreg_lc, void *regmap, + struct mlxreg_core_data *data) +{ + struct device *dev = &data->hpdev.client->dev; + + /* Set line card configuration according to the type. */ + mlxreg_lc->mux_data = mlxreg_lc_mux_data; + mlxreg_lc->io_data = &mlxreg_lc_regs_io; + mlxreg_lc->led_data = &mlxreg_lc_led; + mlxreg_lc->mux_brdinfo = &mlxreg_lc_mux_brdinfo; + + mlxreg_lc->aux_devs = devm_kmemdup(dev, mlxreg_lc_aux_pwr_brdinfo, + sizeof(mlxreg_lc_aux_pwr_brdinfo), GFP_KERNEL); + if (!mlxreg_lc->aux_devs) + return -ENOMEM; + mlxreg_lc->aux_devs_num = ARRAY_SIZE(mlxreg_lc_aux_pwr_brdinfo); + mlxreg_lc->main_devs = devm_kmemdup(dev, mlxreg_lc_main_pwr_brdinfo, + sizeof(mlxreg_lc_main_pwr_brdinfo), GFP_KERNEL); + if (!mlxreg_lc->main_devs) + return -ENOMEM; + mlxreg_lc->main_devs_num = ARRAY_SIZE(mlxreg_lc_main_pwr_brdinfo); + + return 0; +} + +static void +mlxreg_lc_state_update(struct mlxreg_lc *mlxreg_lc, enum mlxreg_lc_state state, u8 action) +{ + if (action) + mlxreg_lc->state |= state; + else + mlxreg_lc->state &= ~state; +} + +static void +mlxreg_lc_state_update_locked(struct mlxreg_lc *mlxreg_lc, enum mlxreg_lc_state state, u8 action) +{ + mutex_lock(&mlxreg_lc->lock); + + if (action) + mlxreg_lc->state |= state; + else + mlxreg_lc->state &= ~state; + + mutex_unlock(&mlxreg_lc->lock); +} + +/* + * Callback is to be called from mlxreg-hotplug driver to notify about line card about received + * event. + */ +static int mlxreg_lc_event_handler(void *handle, enum mlxreg_hotplug_kind kind, u8 action) +{ + struct mlxreg_lc *mlxreg_lc = handle; + int err = 0; + + dev_info(mlxreg_lc->dev, "linecard#%d state %d event kind %d action %d\n", + mlxreg_lc->data->slot, mlxreg_lc->state, kind, action); + + mutex_lock(&mlxreg_lc->lock); + if (!(mlxreg_lc->state & MLXREG_LC_INITIALIZED)) + goto mlxreg_lc_non_initialzed_exit; + + switch (kind) { + case MLXREG_HOTPLUG_LC_SYNCED: + /* + * Synchronization event - hardware and firmware are synchronized. Power on/off + * line card - to allow/disallow main power source. + */ + mlxreg_lc_state_update(mlxreg_lc, MLXREG_LC_SYNCED, action); + /* Power line card if it is not powered yet. */ + if (!(mlxreg_lc->state & MLXREG_LC_POWERED) && action) { + err = mlxreg_lc_power_on_off(mlxreg_lc, 1); + if (err) + goto mlxreg_lc_power_on_off_fail; + } + /* In case line card is configured - enable it. */ + if (mlxreg_lc->state & MLXREG_LC_CONFIGURED && action) + err = mlxreg_lc_enable_disable(mlxreg_lc, 1); + break; + case MLXREG_HOTPLUG_LC_POWERED: + /* Power event - attach or de-attach line card device feeding by the main power. */ + if (action) { + /* Do not create devices, if line card is already powered. */ + if (mlxreg_lc->state & MLXREG_LC_POWERED) { + /* In case line card is configured - enable it. */ + if (mlxreg_lc->state & MLXREG_LC_CONFIGURED) + err = mlxreg_lc_enable_disable(mlxreg_lc, 1); + + goto mlxreg_lc_enable_disable_exit; + } + err = mlxreg_lc_create_static_devices(mlxreg_lc, mlxreg_lc->main_devs, + mlxreg_lc->main_devs_num); + if (err) + goto mlxreg_lc_create_static_devices_fail; + + /* In case line card is already in ready state - enable it. */ + if (mlxreg_lc->state & MLXREG_LC_CONFIGURED) + err = mlxreg_lc_enable_disable(mlxreg_lc, 1); + } else { + mlxreg_lc_destroy_static_devices(mlxreg_lc, mlxreg_lc->main_devs, + mlxreg_lc->main_devs_num); + } + mlxreg_lc_state_update(mlxreg_lc, MLXREG_LC_POWERED, action); + break; + case MLXREG_HOTPLUG_LC_READY: + /* + * Ready event – enable line card by releasing it from reset or disable it by put + * to reset state. + */ + err = mlxreg_lc_enable_disable(mlxreg_lc, !!action); + break; + case MLXREG_HOTPLUG_LC_THERMAL: + /* Thermal shutdown event – power off line card. */ + if (action) + err = mlxreg_lc_power_on_off(mlxreg_lc, 0); + break; + default: + break; + } + +mlxreg_lc_enable_disable_exit: +mlxreg_lc_power_on_off_fail: +mlxreg_lc_create_static_devices_fail: +mlxreg_lc_non_initialzed_exit: + mutex_unlock(&mlxreg_lc->lock); + + return err; +} + +/* + * Callback is to be called from i2c-mux-mlxcpld driver to indicate that all adapter devices has + * been created. + */ +static int mlxreg_lc_completion_notify(void *handle, struct i2c_adapter *parent, + struct i2c_adapter *adapters[]) +{ + struct mlxreg_hotplug_device *main_dev, *aux_dev; + struct mlxreg_lc *mlxreg_lc = handle; + u32 regval; + int i, err; + + /* Update I2C devices feeding by auxiliary power. */ + aux_dev = mlxreg_lc->aux_devs; + for (i = 0; i < mlxreg_lc->aux_devs_num; i++, aux_dev++) { + aux_dev->adapter = adapters[aux_dev->nr]; + aux_dev->nr = adapters[aux_dev->nr]->nr; + } + + err = mlxreg_lc_create_static_devices(mlxreg_lc, mlxreg_lc->aux_devs, + mlxreg_lc->aux_devs_num); + if (err) + return err; + + /* Update I2C devices feeding by main power. */ + main_dev = mlxreg_lc->main_devs; + for (i = 0; i < mlxreg_lc->main_devs_num; i++, main_dev++) { + main_dev->adapter = adapters[main_dev->nr]; + main_dev->nr = adapters[main_dev->nr]->nr; + } + + /* Verify if line card is powered. */ + err = regmap_read(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_pwr, ®val); + if (err) + goto mlxreg_lc_regmap_read_power_fail; + + if (regval & mlxreg_lc->data->mask) { + err = mlxreg_lc_create_static_devices(mlxreg_lc, mlxreg_lc->main_devs, + mlxreg_lc->main_devs_num); + if (err) + goto mlxreg_lc_create_static_devices_failed; + + mlxreg_lc_state_update_locked(mlxreg_lc, MLXREG_LC_POWERED, 1); + } + + /* Verify if line card is synchronized. */ + err = regmap_read(mlxreg_lc->par_regmap, mlxreg_lc->data->reg_sync, ®val); + if (err) + goto mlxreg_lc_regmap_read_sync_fail; + + /* Power on line card if necessary. */ + if (regval & mlxreg_lc->data->mask) { + mlxreg_lc->state |= MLXREG_LC_SYNCED; + mlxreg_lc_state_update_locked(mlxreg_lc, MLXREG_LC_SYNCED, 1); + if (!(mlxreg_lc->state & MLXREG_LC_POWERED)) { + err = mlxreg_lc_power_on_off(mlxreg_lc, 1); + if (err) + goto mlxreg_lc_regmap_power_on_off_fail; + } + } + + mlxreg_lc_state_update_locked(mlxreg_lc, MLXREG_LC_INITIALIZED, 1); + + return 0; + +mlxreg_lc_regmap_power_on_off_fail: +mlxreg_lc_regmap_read_sync_fail: + if (mlxreg_lc->state & MLXREG_LC_POWERED) + mlxreg_lc_destroy_static_devices(mlxreg_lc, mlxreg_lc->main_devs, + mlxreg_lc->main_devs_num); +mlxreg_lc_create_static_devices_failed: + mlxreg_lc_destroy_static_devices(mlxreg_lc, mlxreg_lc->aux_devs, mlxreg_lc->aux_devs_num); +mlxreg_lc_regmap_read_power_fail: + return err; +} + +static int +mlxreg_lc_config_init(struct mlxreg_lc *mlxreg_lc, void *regmap, + struct mlxreg_core_data *data) +{ + struct device *dev = &data->hpdev.client->dev; + int lsb, err; + u32 regval; + + /* Validate line card type. */ + err = regmap_read(regmap, MLXREG_LC_REG_CONFIG_OFFSET, &lsb); + err = (!err) ? regmap_read(regmap, MLXREG_LC_REG_CONFIG_OFFSET, ®val) : err; + if (err) + return err; + regval = (regval & GENMASK(7, 0)) << 8 | (lsb & GENMASK(7, 0)); + switch (regval) { + case MLXREG_LC_SN4800_C16: + err = mlxreg_lc_sn4800_c16_config_init(mlxreg_lc, regmap, data); + if (err) { + dev_err(dev, "Failed to config client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, + data->hpdev.brdinfo->addr); + return err; + } + break; + default: + return -ENODEV; + } + + /* Create mux infrastructure. */ + mlxreg_lc->mux_data->handle = mlxreg_lc; + mlxreg_lc->mux_data->completion_notify = mlxreg_lc_completion_notify; + mlxreg_lc->mux_brdinfo->platform_data = mlxreg_lc->mux_data; + mlxreg_lc->mux = platform_device_register_resndata(dev, "i2c-mux-mlxcpld", data->hpdev.nr, + NULL, 0, mlxreg_lc->mux_data, + sizeof(*mlxreg_lc->mux_data)); + if (IS_ERR(mlxreg_lc->mux)) { + dev_err(dev, "Failed to create mux infra for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + return PTR_ERR(mlxreg_lc->mux); + } + + /* Register IO access driver. */ + if (mlxreg_lc->io_data) { + mlxreg_lc->io_data->regmap = regmap; + mlxreg_lc->io_regs = + platform_device_register_resndata(dev, "mlxreg-io", data->hpdev.nr, NULL, 0, + mlxreg_lc->io_data, sizeof(*mlxreg_lc->io_data)); + if (IS_ERR(mlxreg_lc->io_regs)) { + dev_err(dev, "Failed to create region for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, + data->hpdev.brdinfo->addr); + err = PTR_ERR(mlxreg_lc->io_regs); + goto fail_register_io; + } + } + + /* Register LED driver. */ + if (mlxreg_lc->led_data) { + mlxreg_lc->led_data->regmap = regmap; + mlxreg_lc->led = + platform_device_register_resndata(dev, "leds-mlxreg", data->hpdev.nr, NULL, 0, + mlxreg_lc->led_data, + sizeof(*mlxreg_lc->led_data)); + if (IS_ERR(mlxreg_lc->led)) { + dev_err(dev, "Failed to create LED objects for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, + data->hpdev.brdinfo->addr); + err = PTR_ERR(mlxreg_lc->led); + goto fail_register_led; + } + } + + return 0; + +fail_register_led: + if (mlxreg_lc->io_regs) + platform_device_unregister(mlxreg_lc->io_regs); +fail_register_io: + if (mlxreg_lc->mux) + platform_device_unregister(mlxreg_lc->mux); + + return err; +} + +static void mlxreg_lc_config_exit(struct mlxreg_lc *mlxreg_lc) +{ + /* Unregister LED driver. */ + if (mlxreg_lc->led) + platform_device_unregister(mlxreg_lc->led); + /* Unregister IO access driver. */ + if (mlxreg_lc->io_regs) + platform_device_unregister(mlxreg_lc->io_regs); + /* Remove mux infrastructure. */ + if (mlxreg_lc->mux) + platform_device_unregister(mlxreg_lc->mux); +} + +static int mlxreg_lc_probe(struct platform_device *pdev) +{ + struct mlxreg_core_hotplug_platform_data *par_pdata; + struct mlxreg_core_data *data; + struct mlxreg_lc *mlxreg_lc; + void *regmap; + int i, err; + + data = dev_get_platdata(&pdev->dev); + if (!data) + return -EINVAL; + + mlxreg_lc = devm_kzalloc(&pdev->dev, sizeof(*mlxreg_lc), GFP_KERNEL); + if (!mlxreg_lc) + return -ENOMEM; + + mutex_init(&mlxreg_lc->lock); + /* Set event notification callback. */ + data->notifier->user_handler = mlxreg_lc_event_handler; + data->notifier->handle = mlxreg_lc; + + data->hpdev.adapter = i2c_get_adapter(data->hpdev.nr); + if (!data->hpdev.adapter) { + dev_err(&pdev->dev, "Failed to get adapter for bus %d\n", + data->hpdev.nr); + err = -EFAULT; + goto i2c_get_adapter_fail; + } + + /* Create device at the top of line card I2C tree.*/ + data->hpdev.client = i2c_new_client_device(data->hpdev.adapter, + data->hpdev.brdinfo); + if (IS_ERR(data->hpdev.client)) { + dev_err(&pdev->dev, "Failed to create client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + err = PTR_ERR(data->hpdev.client); + goto i2c_new_device_fail; + } + + regmap = devm_regmap_init_i2c(data->hpdev.client, + &mlxreg_lc_regmap_conf); + if (IS_ERR(regmap)) { + dev_err(&pdev->dev, "Failed to create regmap for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + err = PTR_ERR(regmap); + goto devm_regmap_init_i2c_fail; + } + + /* Set default registers. */ + for (i = 0; i < mlxreg_lc_regmap_conf.num_reg_defaults; i++) { + err = regmap_write(regmap, mlxreg_lc_regmap_default[i].reg, + mlxreg_lc_regmap_default[i].def); + if (err) { + dev_err(&pdev->dev, "Failed to set default regmap %d for client %s at bus %d at addr 0x%02x\n", + i, data->hpdev.brdinfo->type, data->hpdev.nr, + data->hpdev.brdinfo->addr); + goto regmap_write_fail; + } + } + + /* Sync registers with hardware. */ + regcache_mark_dirty(regmap); + err = regcache_sync(regmap); + if (err) { + dev_err(&pdev->dev, "Failed to sync regmap for client %s at bus %d at addr 0x%02x\n", + data->hpdev.brdinfo->type, data->hpdev.nr, data->hpdev.brdinfo->addr); + goto regcache_sync_fail; + } + + par_pdata = data->hpdev.brdinfo->platform_data; + mlxreg_lc->par_regmap = par_pdata->regmap; + mlxreg_lc->data = data; + mlxreg_lc->dev = &pdev->dev; + platform_set_drvdata(pdev, mlxreg_lc); + + /* Configure line card. */ + err = mlxreg_lc_config_init(mlxreg_lc, regmap, data); + if (err) + goto mlxreg_lc_config_init_fail; + + return 0; + +mlxreg_lc_config_init_fail: +regcache_sync_fail: +regmap_write_fail: +devm_regmap_init_i2c_fail: + i2c_unregister_device(data->hpdev.client); + data->hpdev.client = NULL; +i2c_new_device_fail: + i2c_put_adapter(data->hpdev.adapter); + data->hpdev.adapter = NULL; +i2c_get_adapter_fail: + /* Clear event notification callback and handle. */ + if (data->notifier) { + data->notifier->user_handler = NULL; + data->notifier->handle = NULL; + } + return err; +} + +static void mlxreg_lc_remove(struct platform_device *pdev) +{ + struct mlxreg_core_data *data = dev_get_platdata(&pdev->dev); + struct mlxreg_lc *mlxreg_lc = platform_get_drvdata(pdev); + + mlxreg_lc_state_update_locked(mlxreg_lc, MLXREG_LC_INITIALIZED, 0); + + /* + * Probing and removing are invoked by hotplug events raised upon line card insertion and + * removing. If probing procedure fails all data is cleared. However, hotplug event still + * will be raised on line card removing and activate removing procedure. In this case there + * is nothing to remove. + */ + if (!data->notifier || !data->notifier->handle) + return; + + /* Clear event notification callback and handle. */ + data->notifier->user_handler = NULL; + data->notifier->handle = NULL; + + /* Destroy static I2C device feeding by main power. */ + mlxreg_lc_destroy_static_devices(mlxreg_lc, mlxreg_lc->main_devs, + mlxreg_lc->main_devs_num); + /* Destroy static I2C device feeding by auxiliary power. */ + mlxreg_lc_destroy_static_devices(mlxreg_lc, mlxreg_lc->aux_devs, mlxreg_lc->aux_devs_num); + /* Unregister underlying drivers. */ + mlxreg_lc_config_exit(mlxreg_lc); + if (data->hpdev.client) { + i2c_unregister_device(data->hpdev.client); + data->hpdev.client = NULL; + i2c_put_adapter(data->hpdev.adapter); + data->hpdev.adapter = NULL; + } +} + +static struct platform_driver mlxreg_lc_driver = { + .probe = mlxreg_lc_probe, + .remove = mlxreg_lc_remove, + .driver = { + .name = "mlxreg-lc", + }, +}; + +module_platform_driver(mlxreg_lc_driver); + +MODULE_AUTHOR("Vadim Pasternak "); +MODULE_DESCRIPTION("Nvidia line card platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:mlxreg-lc"); diff --git a/drivers/platform/mellanox/nvsw-sn2201.c b/drivers/platform/mellanox/nvsw-sn2201.c new file mode 100644 index 000000000..92b58ba8f --- /dev/null +++ b/drivers/platform/mellanox/nvsw-sn2201.c @@ -0,0 +1,1371 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Nvidia sn2201 driver + * + * Copyright (C) 2022 Nvidia Technologies Ltd. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* SN2201 CPLD register offset. */ +#define NVSW_SN2201_CPLD_LPC_I2C_BASE_ADRR 0x2000 +#define NVSW_SN2201_CPLD_LPC_IO_RANGE 0x100 +#define NVSW_SN2201_HW_VER_ID_OFFSET 0x00 +#define NVSW_SN2201_BOARD_ID_OFFSET 0x01 +#define NVSW_SN2201_CPLD_VER_OFFSET 0x02 +#define NVSW_SN2201_CPLD_MVER_OFFSET 0x03 +#define NVSW_SN2201_CPLD_ID_OFFSET 0x04 +#define NVSW_SN2201_CPLD_PN_OFFSET 0x05 +#define NVSW_SN2201_CPLD_PN1_OFFSET 0x06 +#define NVSW_SN2201_PSU_CTRL_OFFSET 0x0a +#define NVSW_SN2201_QSFP28_STATUS_OFFSET 0x0b +#define NVSW_SN2201_QSFP28_INT_STATUS_OFFSET 0x0c +#define NVSW_SN2201_QSFP28_LP_STATUS_OFFSET 0x0d +#define NVSW_SN2201_QSFP28_RST_STATUS_OFFSET 0x0e +#define NVSW_SN2201_SYS_STATUS_OFFSET 0x0f +#define NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET 0x10 +#define NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET 0x12 +#define NVSW_SN2201_FRONT_UID_LED_CTRL_OFFSET 0x13 +#define NVSW_SN2201_QSFP28_LED_TEST_STATUS_OFFSET 0x14 +#define NVSW_SN2201_SYS_RST_STATUS_OFFSET 0x15 +#define NVSW_SN2201_SYS_INT_STATUS_OFFSET 0x21 +#define NVSW_SN2201_SYS_INT_MASK_OFFSET 0x22 +#define NVSW_SN2201_ASIC_STATUS_OFFSET 0x24 +#define NVSW_SN2201_ASIC_EVENT_OFFSET 0x25 +#define NVSW_SN2201_ASIC_MAKS_OFFSET 0x26 +#define NVSW_SN2201_THML_STATUS_OFFSET 0x27 +#define NVSW_SN2201_THML_EVENT_OFFSET 0x28 +#define NVSW_SN2201_THML_MASK_OFFSET 0x29 +#define NVSW_SN2201_PS_ALT_STATUS_OFFSET 0x2a +#define NVSW_SN2201_PS_ALT_EVENT_OFFSET 0x2b +#define NVSW_SN2201_PS_ALT_MASK_OFFSET 0x2c +#define NVSW_SN2201_PS_PRSNT_STATUS_OFFSET 0x30 +#define NVSW_SN2201_PS_PRSNT_EVENT_OFFSET 0x31 +#define NVSW_SN2201_PS_PRSNT_MASK_OFFSET 0x32 +#define NVSW_SN2201_PS_DC_OK_STATUS_OFFSET 0x33 +#define NVSW_SN2201_PS_DC_OK_EVENT_OFFSET 0x34 +#define NVSW_SN2201_PS_DC_OK_MASK_OFFSET 0x35 +#define NVSW_SN2201_RST_CAUSE1_OFFSET 0x36 +#define NVSW_SN2201_RST_CAUSE2_OFFSET 0x37 +#define NVSW_SN2201_RST_SW_CTRL_OFFSET 0x38 +#define NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET 0x3a +#define NVSW_SN2201_FAN_PRSNT_EVENT_OFFSET 0x3b +#define NVSW_SN2201_FAN_PRSNT_MASK_OFFSET 0x3c +#define NVSW_SN2201_WD_TMR_OFFSET_LSB 0x40 +#define NVSW_SN2201_WD_TMR_OFFSET_MSB 0x41 +#define NVSW_SN2201_WD_ACT_OFFSET 0x42 +#define NVSW_SN2201_FAN_LED1_CTRL_OFFSET 0x50 +#define NVSW_SN2201_FAN_LED2_CTRL_OFFSET 0x51 +#define NVSW_SN2201_REG_MAX 0x52 + +/* Number of physical I2C busses. */ +#define NVSW_SN2201_PHY_I2C_BUS_NUM 2 +/* Number of main mux channels. */ +#define NVSW_SN2201_MAIN_MUX_CHNL_NUM 8 + +#define NVSW_SN2201_MAIN_NR 0 +#define NVSW_SN2201_MAIN_MUX_NR 1 +#define NVSW_SN2201_MAIN_MUX_DEFER_NR (NVSW_SN2201_PHY_I2C_BUS_NUM + \ + NVSW_SN2201_MAIN_MUX_CHNL_NUM - 1) + +#define NVSW_SN2201_MAIN_MUX_CH0_NR NVSW_SN2201_PHY_I2C_BUS_NUM +#define NVSW_SN2201_MAIN_MUX_CH1_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 1) +#define NVSW_SN2201_MAIN_MUX_CH2_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 2) +#define NVSW_SN2201_MAIN_MUX_CH3_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 3) +#define NVSW_SN2201_MAIN_MUX_CH5_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 5) +#define NVSW_SN2201_MAIN_MUX_CH6_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 6) +#define NVSW_SN2201_MAIN_MUX_CH7_NR (NVSW_SN2201_MAIN_MUX_CH0_NR + 7) +#define NVSW_SN2201_2ND_MUX_CH0_NR (NVSW_SN2201_MAIN_MUX_CH7_NR + 1) +#define NVSW_SN2201_2ND_MUX_CH1_NR (NVSW_SN2201_MAIN_MUX_CH7_NR + 2) +#define NVSW_SN2201_2ND_MUX_CH2_NR (NVSW_SN2201_MAIN_MUX_CH7_NR + 3) +#define NVSW_SN2201_2ND_MUX_CH3_NR (NVSW_SN2201_MAIN_MUX_CH7_NR + 4) + +#define NVSW_SN2201_CPLD_NR NVSW_SN2201_MAIN_MUX_CH0_NR +#define NVSW_SN2201_NR_NONE -1 + +/* Masks for aggregation, PSU presence and power, ASIC events + * in CPLD related registers. + */ +#define NVSW_SN2201_CPLD_AGGR_ASIC_MASK_DEF 0xe0 +#define NVSW_SN2201_CPLD_AGGR_PSU_MASK_DEF 0x04 +#define NVSW_SN2201_CPLD_AGGR_PWR_MASK_DEF 0x02 +#define NVSW_SN2201_CPLD_AGGR_FAN_MASK_DEF 0x10 +#define NVSW_SN2201_CPLD_AGGR_MASK_DEF \ + (NVSW_SN2201_CPLD_AGGR_ASIC_MASK_DEF \ + | NVSW_SN2201_CPLD_AGGR_PSU_MASK_DEF \ + | NVSW_SN2201_CPLD_AGGR_PWR_MASK_DEF \ + | NVSW_SN2201_CPLD_AGGR_FAN_MASK_DEF) +#define NVSW_SN2201_CPLD_AGGR_BUSBAR_MASK_DEF \ + (NVSW_SN2201_CPLD_AGGR_ASIC_MASK_DEF \ + | NVSW_SN2201_CPLD_AGGR_FAN_MASK_DEF) + +#define NVSW_SN2201_CPLD_ASIC_MASK GENMASK(3, 1) +#define NVSW_SN2201_CPLD_PSU_MASK GENMASK(1, 0) +#define NVSW_SN2201_CPLD_PWR_MASK GENMASK(1, 0) +#define NVSW_SN2201_CPLD_FAN_MASK GENMASK(3, 0) + +#define NVSW_SN2201_CPLD_SYSIRQ 26 +#define NVSW_SN2201_LPC_SYSIRQ 28 +#define NVSW_SN2201_CPLD_I2CADDR 0x41 + +#define NVSW_SN2201_WD_DFLT_TIMEOUT 600 + +/* nvsw_sn2201 - device private data + * @dev: platform device; + * @io_data: register access platform data; + * @led_data: LED platform data; + * @hotplug_data: hotplug platform data; + * @i2c_data: I2C controller platform data; + * @led: LED device; + * @io_regs: register access device; + * @pdev_hotplug: hotplug device; + * @sn2201_devs: I2C devices for sn2201 devices; + * @sn2201_devs_num: number of I2C devices for sn2201 device; + * @main_mux_devs: I2C devices for main mux; + * @main_mux_devs_num: number of I2C devices for main mux; + * @cpld_devs: I2C devices for cpld; + * @cpld_devs_num: number of I2C devices for cpld; + * @main_mux_deferred_nr: I2C adapter number must be exist prior creating devices execution; + * @ext_pwr_source: true if system powered by external power supply; false - by internal; + */ +struct nvsw_sn2201 { + struct device *dev; + struct mlxreg_core_platform_data *io_data; + struct mlxreg_core_platform_data *led_data; + struct mlxreg_core_platform_data *wd_data; + struct mlxreg_core_hotplug_platform_data *hotplug_data; + struct mlxreg_core_hotplug_platform_data *i2c_data; + struct platform_device *led; + struct platform_device *wd; + struct platform_device *io_regs; + struct platform_device *pdev_hotplug; + struct platform_device *pdev_i2c; + struct mlxreg_hotplug_device *sn2201_devs; + int sn2201_devs_num; + struct mlxreg_hotplug_device *main_mux_devs; + int main_mux_devs_num; + struct mlxreg_hotplug_device *cpld_devs; + int cpld_devs_num; + int main_mux_deferred_nr; + bool ext_pwr_source; +}; + +static bool nvsw_sn2201_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case NVSW_SN2201_PSU_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_LP_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_RST_STATUS_OFFSET: + case NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_UID_LED_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_LED_TEST_STATUS_OFFSET: + case NVSW_SN2201_SYS_RST_STATUS_OFFSET: + case NVSW_SN2201_SYS_INT_MASK_OFFSET: + case NVSW_SN2201_ASIC_EVENT_OFFSET: + case NVSW_SN2201_ASIC_MAKS_OFFSET: + case NVSW_SN2201_THML_EVENT_OFFSET: + case NVSW_SN2201_THML_MASK_OFFSET: + case NVSW_SN2201_PS_ALT_EVENT_OFFSET: + case NVSW_SN2201_PS_ALT_MASK_OFFSET: + case NVSW_SN2201_PS_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_PS_PRSNT_MASK_OFFSET: + case NVSW_SN2201_PS_DC_OK_EVENT_OFFSET: + case NVSW_SN2201_PS_DC_OK_MASK_OFFSET: + case NVSW_SN2201_RST_SW_CTRL_OFFSET: + case NVSW_SN2201_FAN_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_FAN_PRSNT_MASK_OFFSET: + case NVSW_SN2201_WD_TMR_OFFSET_LSB: + case NVSW_SN2201_WD_TMR_OFFSET_MSB: + case NVSW_SN2201_WD_ACT_OFFSET: + case NVSW_SN2201_FAN_LED1_CTRL_OFFSET: + case NVSW_SN2201_FAN_LED2_CTRL_OFFSET: + return true; + } + return false; +} + +static bool nvsw_sn2201_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case NVSW_SN2201_HW_VER_ID_OFFSET: + case NVSW_SN2201_BOARD_ID_OFFSET: + case NVSW_SN2201_CPLD_VER_OFFSET: + case NVSW_SN2201_CPLD_MVER_OFFSET: + case NVSW_SN2201_CPLD_ID_OFFSET: + case NVSW_SN2201_CPLD_PN_OFFSET: + case NVSW_SN2201_CPLD_PN1_OFFSET: + case NVSW_SN2201_PSU_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_INT_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_LP_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_RST_STATUS_OFFSET: + case NVSW_SN2201_SYS_STATUS_OFFSET: + case NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_UID_LED_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_LED_TEST_STATUS_OFFSET: + case NVSW_SN2201_SYS_RST_STATUS_OFFSET: + case NVSW_SN2201_RST_CAUSE1_OFFSET: + case NVSW_SN2201_RST_CAUSE2_OFFSET: + case NVSW_SN2201_SYS_INT_STATUS_OFFSET: + case NVSW_SN2201_SYS_INT_MASK_OFFSET: + case NVSW_SN2201_ASIC_STATUS_OFFSET: + case NVSW_SN2201_ASIC_EVENT_OFFSET: + case NVSW_SN2201_ASIC_MAKS_OFFSET: + case NVSW_SN2201_THML_STATUS_OFFSET: + case NVSW_SN2201_THML_EVENT_OFFSET: + case NVSW_SN2201_THML_MASK_OFFSET: + case NVSW_SN2201_PS_ALT_STATUS_OFFSET: + case NVSW_SN2201_PS_ALT_EVENT_OFFSET: + case NVSW_SN2201_PS_ALT_MASK_OFFSET: + case NVSW_SN2201_PS_PRSNT_STATUS_OFFSET: + case NVSW_SN2201_PS_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_PS_PRSNT_MASK_OFFSET: + case NVSW_SN2201_PS_DC_OK_STATUS_OFFSET: + case NVSW_SN2201_PS_DC_OK_EVENT_OFFSET: + case NVSW_SN2201_PS_DC_OK_MASK_OFFSET: + case NVSW_SN2201_RST_SW_CTRL_OFFSET: + case NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET: + case NVSW_SN2201_FAN_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_FAN_PRSNT_MASK_OFFSET: + case NVSW_SN2201_WD_TMR_OFFSET_LSB: + case NVSW_SN2201_WD_TMR_OFFSET_MSB: + case NVSW_SN2201_WD_ACT_OFFSET: + case NVSW_SN2201_FAN_LED1_CTRL_OFFSET: + case NVSW_SN2201_FAN_LED2_CTRL_OFFSET: + return true; + } + return false; +} + +static bool nvsw_sn2201_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case NVSW_SN2201_HW_VER_ID_OFFSET: + case NVSW_SN2201_BOARD_ID_OFFSET: + case NVSW_SN2201_CPLD_VER_OFFSET: + case NVSW_SN2201_CPLD_MVER_OFFSET: + case NVSW_SN2201_CPLD_ID_OFFSET: + case NVSW_SN2201_CPLD_PN_OFFSET: + case NVSW_SN2201_CPLD_PN1_OFFSET: + case NVSW_SN2201_PSU_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_INT_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_LP_STATUS_OFFSET: + case NVSW_SN2201_QSFP28_RST_STATUS_OFFSET: + case NVSW_SN2201_SYS_STATUS_OFFSET: + case NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET: + case NVSW_SN2201_FRONT_UID_LED_CTRL_OFFSET: + case NVSW_SN2201_QSFP28_LED_TEST_STATUS_OFFSET: + case NVSW_SN2201_SYS_RST_STATUS_OFFSET: + case NVSW_SN2201_RST_CAUSE1_OFFSET: + case NVSW_SN2201_RST_CAUSE2_OFFSET: + case NVSW_SN2201_SYS_INT_STATUS_OFFSET: + case NVSW_SN2201_SYS_INT_MASK_OFFSET: + case NVSW_SN2201_ASIC_STATUS_OFFSET: + case NVSW_SN2201_ASIC_EVENT_OFFSET: + case NVSW_SN2201_ASIC_MAKS_OFFSET: + case NVSW_SN2201_THML_STATUS_OFFSET: + case NVSW_SN2201_THML_EVENT_OFFSET: + case NVSW_SN2201_THML_MASK_OFFSET: + case NVSW_SN2201_PS_ALT_STATUS_OFFSET: + case NVSW_SN2201_PS_ALT_EVENT_OFFSET: + case NVSW_SN2201_PS_ALT_MASK_OFFSET: + case NVSW_SN2201_PS_PRSNT_STATUS_OFFSET: + case NVSW_SN2201_PS_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_PS_PRSNT_MASK_OFFSET: + case NVSW_SN2201_PS_DC_OK_STATUS_OFFSET: + case NVSW_SN2201_PS_DC_OK_EVENT_OFFSET: + case NVSW_SN2201_PS_DC_OK_MASK_OFFSET: + case NVSW_SN2201_RST_SW_CTRL_OFFSET: + case NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET: + case NVSW_SN2201_FAN_PRSNT_EVENT_OFFSET: + case NVSW_SN2201_FAN_PRSNT_MASK_OFFSET: + case NVSW_SN2201_WD_TMR_OFFSET_LSB: + case NVSW_SN2201_WD_TMR_OFFSET_MSB: + case NVSW_SN2201_FAN_LED1_CTRL_OFFSET: + case NVSW_SN2201_FAN_LED2_CTRL_OFFSET: + return true; + } + return false; +} + +static const struct reg_default nvsw_sn2201_regmap_default[] = { + { NVSW_SN2201_QSFP28_LED_TEST_STATUS_OFFSET, 0x00 }, + { NVSW_SN2201_WD_ACT_OFFSET, 0x00 }, +}; + +/* Configuration for the register map of a device with 1 bytes address space. */ +static const struct regmap_config nvsw_sn2201_regmap_conf = { + .reg_bits = 8, + .val_bits = 8, + .max_register = NVSW_SN2201_REG_MAX, + .cache_type = REGCACHE_FLAT, + .writeable_reg = nvsw_sn2201_writeable_reg, + .readable_reg = nvsw_sn2201_readable_reg, + .volatile_reg = nvsw_sn2201_volatile_reg, + .reg_defaults = nvsw_sn2201_regmap_default, + .num_reg_defaults = ARRAY_SIZE(nvsw_sn2201_regmap_default), +}; + +/* Regions for LPC I2C controller and LPC base register space. */ +static const struct resource nvsw_sn2201_lpc_io_resources[] = { + [0] = DEFINE_RES_NAMED(NVSW_SN2201_CPLD_LPC_I2C_BASE_ADRR, + NVSW_SN2201_CPLD_LPC_IO_RANGE, + "mlxplat_cpld_lpc_i2c_ctrl", IORESOURCE_IO), +}; + +static struct resource nvsw_sn2201_cpld_res[] = { + [0] = DEFINE_RES_IRQ_NAMED(NVSW_SN2201_CPLD_SYSIRQ, "mlxreg-hotplug"), +}; + +static struct resource nvsw_sn2201_lpc_res[] = { + [0] = DEFINE_RES_IRQ_NAMED(NVSW_SN2201_LPC_SYSIRQ, "i2c-mlxcpld"), +}; + +/* SN2201 I2C platform data. */ +static struct mlxreg_core_hotplug_platform_data nvsw_sn2201_i2c_data = { + .irq = NVSW_SN2201_CPLD_SYSIRQ, +}; + +/* SN2201 CPLD device. */ +static struct i2c_board_info nvsw_sn2201_cpld_devices[] = { + { + I2C_BOARD_INFO("nvsw-sn2201", 0x41), + }, +}; + +/* SN2201 CPLD board info. */ +static struct mlxreg_hotplug_device nvsw_sn2201_cpld_brdinfo[] = { + { + .brdinfo = &nvsw_sn2201_cpld_devices[0], + .nr = NVSW_SN2201_CPLD_NR, + }, +}; + +/* SN2201 main mux device. */ +static struct i2c_board_info nvsw_sn2201_main_mux_devices[] = { + { + I2C_BOARD_INFO("pca9548", 0x70), + }, +}; + +/* SN2201 main mux board info. */ +static struct mlxreg_hotplug_device nvsw_sn2201_main_mux_brdinfo[] = { + { + .brdinfo = &nvsw_sn2201_main_mux_devices[0], + .nr = NVSW_SN2201_MAIN_MUX_NR, + }, +}; + +/* SN2201 power devices. */ +static struct i2c_board_info nvsw_sn2201_pwr_devices[] = { + { + I2C_BOARD_INFO("pmbus", 0x58), + }, + { + I2C_BOARD_INFO("pmbus", 0x58), + }, +}; + +/* SN2201 fan devices. */ +static struct i2c_board_info nvsw_sn2201_fan_devices[] = { + { + I2C_BOARD_INFO("24c02", 0x50), + }, + { + I2C_BOARD_INFO("24c02", 0x51), + }, + { + I2C_BOARD_INFO("24c02", 0x52), + }, + { + I2C_BOARD_INFO("24c02", 0x53), + }, +}; + +/* SN2201 hotplug default data. */ +static struct mlxreg_core_data nvsw_sn2201_psu_items_data[] = { + { + .label = "psu1", + .reg = NVSW_SN2201_PS_PRSNT_STATUS_OFFSET, + .mask = BIT(0), + .hpdev.nr = NVSW_SN2201_NR_NONE, + }, + { + .label = "psu2", + .reg = NVSW_SN2201_PS_PRSNT_STATUS_OFFSET, + .mask = BIT(1), + .hpdev.nr = NVSW_SN2201_NR_NONE, + }, +}; + +static struct mlxreg_core_data nvsw_sn2201_pwr_items_data[] = { + { + .label = "pwr1", + .reg = NVSW_SN2201_PS_DC_OK_STATUS_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &nvsw_sn2201_pwr_devices[0], + .hpdev.nr = NVSW_SN2201_MAIN_MUX_CH1_NR, + }, + { + .label = "pwr2", + .reg = NVSW_SN2201_PS_DC_OK_STATUS_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &nvsw_sn2201_pwr_devices[1], + .hpdev.nr = NVSW_SN2201_MAIN_MUX_CH2_NR, + }, +}; + +static struct mlxreg_core_data nvsw_sn2201_fan_items_data[] = { + { + .label = "fan1", + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = BIT(0), + .hpdev.brdinfo = &nvsw_sn2201_fan_devices[0], + .hpdev.nr = NVSW_SN2201_2ND_MUX_CH0_NR, + }, + { + .label = "fan2", + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = BIT(1), + .hpdev.brdinfo = &nvsw_sn2201_fan_devices[1], + .hpdev.nr = NVSW_SN2201_2ND_MUX_CH1_NR, + }, + { + .label = "fan3", + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = BIT(2), + .hpdev.brdinfo = &nvsw_sn2201_fan_devices[2], + .hpdev.nr = NVSW_SN2201_2ND_MUX_CH2_NR, + }, + { + .label = "fan4", + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = BIT(3), + .hpdev.brdinfo = &nvsw_sn2201_fan_devices[3], + .hpdev.nr = NVSW_SN2201_2ND_MUX_CH3_NR, + }, +}; + +static struct mlxreg_core_data nvsw_sn2201_sys_items_data[] = { + { + .label = "nic_smb_alert", + .reg = NVSW_SN2201_ASIC_STATUS_OFFSET, + .mask = BIT(1), + .hpdev.nr = NVSW_SN2201_NR_NONE, + }, + { + .label = "cpu_sd", + .reg = NVSW_SN2201_ASIC_STATUS_OFFSET, + .mask = BIT(2), + .hpdev.nr = NVSW_SN2201_NR_NONE, + }, + { + .label = "mac_health", + .reg = NVSW_SN2201_ASIC_STATUS_OFFSET, + .mask = BIT(3), + .hpdev.nr = NVSW_SN2201_NR_NONE, + }, +}; + +static struct mlxreg_core_item nvsw_sn2201_items[] = { + { + .data = nvsw_sn2201_psu_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_PSU_MASK_DEF, + .reg = NVSW_SN2201_PS_PRSNT_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_PSU_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_psu_items_data), + .inversed = 1, + .health = false, + }, + { + .data = nvsw_sn2201_pwr_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_PWR_MASK_DEF, + .reg = NVSW_SN2201_PS_DC_OK_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_PWR_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_pwr_items_data), + .inversed = 0, + .health = false, + }, + { + .data = nvsw_sn2201_fan_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_FAN_MASK_DEF, + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_FAN_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = nvsw_sn2201_sys_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_ASIC_MASK_DEF, + .reg = NVSW_SN2201_ASIC_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_sys_items_data), + .inversed = 1, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data nvsw_sn2201_hotplug = { + .items = nvsw_sn2201_items, + .count = ARRAY_SIZE(nvsw_sn2201_items), + .cell = NVSW_SN2201_SYS_INT_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_AGGR_MASK_DEF, +}; + +static struct mlxreg_core_item nvsw_sn2201_busbar_items[] = { + { + .data = nvsw_sn2201_fan_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_FAN_MASK_DEF, + .reg = NVSW_SN2201_FAN_PRSNT_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_FAN_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_fan_items_data), + .inversed = 1, + .health = false, + }, + { + .data = nvsw_sn2201_sys_items_data, + .aggr_mask = NVSW_SN2201_CPLD_AGGR_ASIC_MASK_DEF, + .reg = NVSW_SN2201_ASIC_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_ASIC_MASK, + .count = ARRAY_SIZE(nvsw_sn2201_sys_items_data), + .inversed = 1, + .health = false, + }, +}; + +static +struct mlxreg_core_hotplug_platform_data nvsw_sn2201_busbar_hotplug = { + .items = nvsw_sn2201_busbar_items, + .count = ARRAY_SIZE(nvsw_sn2201_busbar_items), + .cell = NVSW_SN2201_SYS_INT_STATUS_OFFSET, + .mask = NVSW_SN2201_CPLD_AGGR_BUSBAR_MASK_DEF, +}; + +/* SN2201 static devices. */ +static struct i2c_board_info nvsw_sn2201_static_devices[] = { + { + I2C_BOARD_INFO("24c02", 0x57), + }, + { + I2C_BOARD_INFO("lm75", 0x4b), + }, + { + I2C_BOARD_INFO("24c64", 0x56), + }, + { + I2C_BOARD_INFO("ads1015", 0x49), + }, + { + I2C_BOARD_INFO("pca9546", 0x71), + }, + { + I2C_BOARD_INFO("emc2305", 0x4d), + }, + { + I2C_BOARD_INFO("lm75", 0x49), + }, + { + I2C_BOARD_INFO("pca9555", 0x27), + }, + { + I2C_BOARD_INFO("powr1014", 0x37), + }, + { + I2C_BOARD_INFO("lm75", 0x4f), + }, + { + I2C_BOARD_INFO("pmbus", 0x40), + }, + { + I2C_BOARD_INFO("lm5066i", 0x15), + }, +}; + +/* SN2201 default static board info. */ +static struct mlxreg_hotplug_device nvsw_sn2201_static_brdinfo[] = { + { + .brdinfo = &nvsw_sn2201_static_devices[0], + .nr = NVSW_SN2201_MAIN_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[1], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[2], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[3], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[4], + .nr = NVSW_SN2201_MAIN_MUX_CH3_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[5], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[6], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[7], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[8], + .nr = NVSW_SN2201_MAIN_MUX_CH6_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[9], + .nr = NVSW_SN2201_MAIN_MUX_CH6_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[10], + .nr = NVSW_SN2201_MAIN_MUX_CH7_NR, + }, +}; + +/* SN2201 default busbar static board info. */ +static struct mlxreg_hotplug_device nvsw_sn2201_busbar_static_brdinfo[] = { + { + .brdinfo = &nvsw_sn2201_static_devices[0], + .nr = NVSW_SN2201_MAIN_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[1], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[2], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[3], + .nr = NVSW_SN2201_MAIN_MUX_CH0_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[4], + .nr = NVSW_SN2201_MAIN_MUX_CH3_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[5], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[6], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[7], + .nr = NVSW_SN2201_MAIN_MUX_CH5_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[8], + .nr = NVSW_SN2201_MAIN_MUX_CH6_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[9], + .nr = NVSW_SN2201_MAIN_MUX_CH6_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[10], + .nr = NVSW_SN2201_MAIN_MUX_CH7_NR, + }, + { + .brdinfo = &nvsw_sn2201_static_devices[11], + .nr = NVSW_SN2201_MAIN_MUX_CH1_NR, + }, +}; + +/* LED default data. */ +static struct mlxreg_core_data nvsw_sn2201_led_data[] = { + { + .label = "status:green", + .reg = NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "status:orange", + .reg = NVSW_SN2201_FRONT_SYS_LED_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "psu:green", + .reg = NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "psu:orange", + .reg = NVSW_SN2201_FRONT_PSU_LED_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "uid:blue", + .reg = NVSW_SN2201_FRONT_UID_LED_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "fan1:green", + .reg = NVSW_SN2201_FAN_LED1_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "fan1:orange", + .reg = NVSW_SN2201_FAN_LED1_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "fan2:green", + .reg = NVSW_SN2201_FAN_LED1_CTRL_OFFSET, + .mask = GENMASK(3, 0), + }, + { + .label = "fan2:orange", + .reg = NVSW_SN2201_FAN_LED1_CTRL_OFFSET, + .mask = GENMASK(3, 0), + }, + { + .label = "fan3:green", + .reg = NVSW_SN2201_FAN_LED2_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "fan3:orange", + .reg = NVSW_SN2201_FAN_LED2_CTRL_OFFSET, + .mask = GENMASK(7, 4), + }, + { + .label = "fan4:green", + .reg = NVSW_SN2201_FAN_LED2_CTRL_OFFSET, + .mask = GENMASK(3, 0), + }, + { + .label = "fan4:orange", + .reg = NVSW_SN2201_FAN_LED2_CTRL_OFFSET, + .mask = GENMASK(3, 0), + }, +}; + +static struct mlxreg_core_platform_data nvsw_sn2201_led = { + .data = nvsw_sn2201_led_data, + .counter = ARRAY_SIZE(nvsw_sn2201_led_data), +}; + +/* Default register access data. */ +static struct mlxreg_core_data nvsw_sn2201_io_data[] = { + { + .label = "cpld1_version", + .reg = NVSW_SN2201_CPLD_VER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_version_min", + .reg = NVSW_SN2201_CPLD_MVER_OFFSET, + .bit = GENMASK(7, 0), + .mode = 0444, + }, + { + .label = "cpld1_pn", + .reg = NVSW_SN2201_CPLD_PN_OFFSET, + .bit = GENMASK(15, 0), + .mode = 0444, + .regnum = 2, + }, + { + .label = "psu1_on", + .reg = NVSW_SN2201_PSU_CTRL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "psu2_on", + .reg = NVSW_SN2201_PSU_CTRL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0644, + }, + { + .label = "pwr_cycle", + .reg = NVSW_SN2201_PSU_CTRL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0644, + }, + { + .label = "asic_health", + .reg = NVSW_SN2201_SYS_STATUS_OFFSET, + .mask = GENMASK(4, 3), + .bit = 4, + .mode = 0444, + }, + { + .label = "qsfp_pwr_good", + .reg = NVSW_SN2201_SYS_STATUS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "phy_reset", + .reg = NVSW_SN2201_SYS_RST_STATUS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0644, + }, + { + .label = "mac_reset", + .reg = NVSW_SN2201_SYS_RST_STATUS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0644, + }, + { + .label = "pwr_down", + .reg = NVSW_SN2201_RST_SW_CTRL_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0644, + }, + { + .label = "reset_long_pb", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "reset_short_pb", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_aux_pwr_or_fu", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_swb_dc_dc_pwr_fail", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(3), + .mode = 0444, + }, + { + .label = "reset_sw_reset", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_fw_reset", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_swb_wd", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "reset_asic_thermal", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(7), + .mode = 0444, + }, + { + .label = "reset_system", + .reg = NVSW_SN2201_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, + { + .label = "reset_sw_pwr_off", + .reg = NVSW_SN2201_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(2), + .mode = 0444, + }, + { + .label = "reset_cpu_pwr_fail_thermal", + .reg = NVSW_SN2201_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(4), + .mode = 0444, + }, + { + .label = "reset_reload_bios", + .reg = NVSW_SN2201_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(5), + .mode = 0444, + }, + { + .label = "reset_ac_pwr_fail", + .reg = NVSW_SN2201_RST_CAUSE2_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .mode = 0444, + }, + { + .label = "psu1", + .reg = NVSW_SN2201_PS_PRSNT_STATUS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(0), + .mode = 0444, + }, + { + .label = "psu2", + .reg = NVSW_SN2201_PS_PRSNT_STATUS_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(1), + .mode = 0444, + }, +}; + +static struct mlxreg_core_platform_data nvsw_sn2201_regs_io = { + .data = nvsw_sn2201_io_data, + .counter = ARRAY_SIZE(nvsw_sn2201_io_data), +}; + +/* Default watchdog data. */ +static struct mlxreg_core_data nvsw_sn2201_wd_data[] = { + { + .label = "action", + .reg = NVSW_SN2201_WD_ACT_OFFSET, + .mask = GENMASK(7, 1), + .bit = 0, + }, + { + .label = "timeout", + .reg = NVSW_SN2201_WD_TMR_OFFSET_LSB, + .mask = 0, + .health_cntr = NVSW_SN2201_WD_DFLT_TIMEOUT, + }, + { + .label = "timeleft", + .reg = NVSW_SN2201_WD_TMR_OFFSET_LSB, + .mask = 0, + }, + { + .label = "ping", + .reg = NVSW_SN2201_WD_ACT_OFFSET, + .mask = GENMASK(7, 1), + .bit = 0, + }, + { + .label = "reset", + .reg = NVSW_SN2201_RST_CAUSE1_OFFSET, + .mask = GENMASK(7, 0) & ~BIT(6), + .bit = 6, + }, +}; + +static struct mlxreg_core_platform_data nvsw_sn2201_wd = { + .data = nvsw_sn2201_wd_data, + .counter = ARRAY_SIZE(nvsw_sn2201_wd_data), + .version = MLX_WDT_TYPE3, + .identity = "mlx-wdt-main", +}; + +static int +nvsw_sn2201_create_static_devices(struct nvsw_sn2201 *nvsw_sn2201, + struct mlxreg_hotplug_device *devs, + int size) +{ + struct mlxreg_hotplug_device *dev = devs; + int ret; + int i; + + /* Create I2C static devices. */ + for (i = 0; i < size; i++, dev++) { + dev->client = i2c_new_client_device(dev->adapter, dev->brdinfo); + if (IS_ERR(dev->client)) { + dev_err(nvsw_sn2201->dev, "Failed to create client %s at bus %d at addr 0x%02x\n", + dev->brdinfo->type, + dev->nr, dev->brdinfo->addr); + + dev->adapter = NULL; + ret = PTR_ERR(dev->client); + goto fail_create_static_devices; + } + } + + return 0; + +fail_create_static_devices: + while (--i >= 0) { + dev = devs + i; + i2c_unregister_device(dev->client); + dev->client = NULL; + dev->adapter = NULL; + } + return ret; +} + +static void nvsw_sn2201_destroy_static_devices(struct nvsw_sn2201 *nvsw_sn2201, + struct mlxreg_hotplug_device *devs, int size) +{ + struct mlxreg_hotplug_device *dev = devs; + int i; + + /* Destroy static I2C device for SN2201 static devices. */ + for (i = 0; i < size; i++, dev++) { + if (dev->client) { + i2c_unregister_device(dev->client); + dev->client = NULL; + i2c_put_adapter(dev->adapter); + dev->adapter = NULL; + } + } +} + +static int nvsw_sn2201_config_post_init(struct nvsw_sn2201 *nvsw_sn2201) +{ + struct mlxreg_hotplug_device *sn2201_dev; + struct i2c_adapter *adap; + struct device *dev; + int i, err; + + dev = nvsw_sn2201->dev; + adap = i2c_get_adapter(nvsw_sn2201->main_mux_deferred_nr); + if (!adap) { + dev_err(dev, "Failed to get adapter for bus %d\n", + nvsw_sn2201->main_mux_deferred_nr); + return -ENODEV; + } + i2c_put_adapter(adap); + + /* Update board info. */ + sn2201_dev = nvsw_sn2201->sn2201_devs; + for (i = 0; i < nvsw_sn2201->sn2201_devs_num; i++, sn2201_dev++) { + sn2201_dev->adapter = i2c_get_adapter(sn2201_dev->nr); + if (!sn2201_dev->adapter) + return -ENODEV; + i2c_put_adapter(sn2201_dev->adapter); + } + + err = nvsw_sn2201_create_static_devices(nvsw_sn2201, nvsw_sn2201->sn2201_devs, + nvsw_sn2201->sn2201_devs_num); + if (err) + dev_err(dev, "Failed to create static devices\n"); + + return err; +} + +static int nvsw_sn2201_config_init(struct nvsw_sn2201 *nvsw_sn2201, void *regmap) +{ + struct device *dev = nvsw_sn2201->dev; + int err; + + nvsw_sn2201->io_data = &nvsw_sn2201_regs_io; + nvsw_sn2201->led_data = &nvsw_sn2201_led; + nvsw_sn2201->wd_data = &nvsw_sn2201_wd; + if (nvsw_sn2201->ext_pwr_source) + nvsw_sn2201->hotplug_data = &nvsw_sn2201_busbar_hotplug; + else + nvsw_sn2201->hotplug_data = &nvsw_sn2201_hotplug; + + /* Register IO access driver. */ + if (nvsw_sn2201->io_data) { + nvsw_sn2201->io_data->regmap = regmap; + nvsw_sn2201->io_regs = + platform_device_register_resndata(dev, "mlxreg-io", PLATFORM_DEVID_NONE, NULL, 0, + nvsw_sn2201->io_data, + sizeof(*nvsw_sn2201->io_data)); + if (IS_ERR(nvsw_sn2201->io_regs)) { + err = PTR_ERR(nvsw_sn2201->io_regs); + goto fail_register_io; + } + } + + /* Register LED driver. */ + if (nvsw_sn2201->led_data) { + nvsw_sn2201->led_data->regmap = regmap; + nvsw_sn2201->led = + platform_device_register_resndata(dev, "leds-mlxreg", PLATFORM_DEVID_NONE, NULL, 0, + nvsw_sn2201->led_data, + sizeof(*nvsw_sn2201->led_data)); + if (IS_ERR(nvsw_sn2201->led)) { + err = PTR_ERR(nvsw_sn2201->led); + goto fail_register_led; + } + } + + /* Register WD driver. */ + if (nvsw_sn2201->wd_data) { + nvsw_sn2201->wd_data->regmap = regmap; + nvsw_sn2201->wd = + platform_device_register_resndata(dev, "mlx-wdt", PLATFORM_DEVID_NONE, NULL, 0, + nvsw_sn2201->wd_data, + sizeof(*nvsw_sn2201->wd_data)); + if (IS_ERR(nvsw_sn2201->wd)) { + err = PTR_ERR(nvsw_sn2201->wd); + goto fail_register_wd; + } + } + + /* Register hotplug driver. */ + if (nvsw_sn2201->hotplug_data) { + nvsw_sn2201->hotplug_data->regmap = regmap; + nvsw_sn2201->pdev_hotplug = + platform_device_register_resndata(dev, "mlxreg-hotplug", PLATFORM_DEVID_NONE, + nvsw_sn2201_cpld_res, + ARRAY_SIZE(nvsw_sn2201_cpld_res), + nvsw_sn2201->hotplug_data, + sizeof(*nvsw_sn2201->hotplug_data)); + if (IS_ERR(nvsw_sn2201->pdev_hotplug)) { + err = PTR_ERR(nvsw_sn2201->pdev_hotplug); + goto fail_register_hotplug; + } + } + + return nvsw_sn2201_config_post_init(nvsw_sn2201); + +fail_register_hotplug: + if (nvsw_sn2201->wd) + platform_device_unregister(nvsw_sn2201->wd); +fail_register_wd: + if (nvsw_sn2201->led) + platform_device_unregister(nvsw_sn2201->led); +fail_register_led: + if (nvsw_sn2201->io_regs) + platform_device_unregister(nvsw_sn2201->io_regs); +fail_register_io: + + return err; +} + +static void nvsw_sn2201_config_exit(struct nvsw_sn2201 *nvsw_sn2201) +{ + /* Unregister hotplug driver. */ + if (nvsw_sn2201->pdev_hotplug) + platform_device_unregister(nvsw_sn2201->pdev_hotplug); + /* Unregister WD driver. */ + if (nvsw_sn2201->wd) + platform_device_unregister(nvsw_sn2201->wd); + /* Unregister LED driver. */ + if (nvsw_sn2201->led) + platform_device_unregister(nvsw_sn2201->led); + /* Unregister IO access driver. */ + if (nvsw_sn2201->io_regs) + platform_device_unregister(nvsw_sn2201->io_regs); +} + +/* + * Initialization is divided into two parts: + * - I2C main bus init. + * - Mux creation and attaching devices to the mux, + * which assumes that the main bus is already created. + * This separation is required for synchronization between these two parts. + * Completion notify callback is used to make this flow synchronized. + */ +static int nvsw_sn2201_i2c_completion_notify(void *handle, int id) +{ + struct nvsw_sn2201 *nvsw_sn2201 = handle; + void *regmap; + int i, err; + + /* Create main mux. */ + nvsw_sn2201->main_mux_devs->adapter = i2c_get_adapter(nvsw_sn2201->main_mux_devs->nr); + if (!nvsw_sn2201->main_mux_devs->adapter) { + err = -ENODEV; + dev_err(nvsw_sn2201->dev, "Failed to get adapter for bus %d\n", + nvsw_sn2201->main_mux_devs->nr); + goto i2c_get_adapter_main_fail; + } + + nvsw_sn2201->main_mux_devs_num = ARRAY_SIZE(nvsw_sn2201_main_mux_brdinfo); + err = nvsw_sn2201_create_static_devices(nvsw_sn2201, nvsw_sn2201->main_mux_devs, + nvsw_sn2201->main_mux_devs_num); + if (err) { + dev_err(nvsw_sn2201->dev, "Failed to create main mux devices\n"); + goto nvsw_sn2201_create_static_devices_fail; + } + + nvsw_sn2201->cpld_devs->adapter = i2c_get_adapter(nvsw_sn2201->cpld_devs->nr); + if (!nvsw_sn2201->cpld_devs->adapter) { + err = -ENODEV; + dev_err(nvsw_sn2201->dev, "Failed to get adapter for bus %d\n", + nvsw_sn2201->cpld_devs->nr); + goto i2c_get_adapter_fail; + } + + /* Create CPLD device. */ + nvsw_sn2201->cpld_devs->client = i2c_new_dummy_device(nvsw_sn2201->cpld_devs->adapter, + NVSW_SN2201_CPLD_I2CADDR); + if (IS_ERR(nvsw_sn2201->cpld_devs->client)) { + err = PTR_ERR(nvsw_sn2201->cpld_devs->client); + dev_err(nvsw_sn2201->dev, "Failed to create %s cpld device at bus %d at addr 0x%02x\n", + nvsw_sn2201->cpld_devs->brdinfo->type, nvsw_sn2201->cpld_devs->nr, + nvsw_sn2201->cpld_devs->brdinfo->addr); + goto i2c_new_dummy_fail; + } + + regmap = devm_regmap_init_i2c(nvsw_sn2201->cpld_devs->client, &nvsw_sn2201_regmap_conf); + if (IS_ERR(regmap)) { + err = PTR_ERR(regmap); + dev_err(nvsw_sn2201->dev, "Failed to initialise managed register map\n"); + goto devm_regmap_init_i2c_fail; + } + + /* Set default registers. */ + for (i = 0; i < nvsw_sn2201_regmap_conf.num_reg_defaults; i++) { + err = regmap_write(regmap, nvsw_sn2201_regmap_default[i].reg, + nvsw_sn2201_regmap_default[i].def); + if (err) { + dev_err(nvsw_sn2201->dev, "Failed to set register at offset 0x%02x to default value: 0x%02x\n", + nvsw_sn2201_regmap_default[i].reg, + nvsw_sn2201_regmap_default[i].def); + goto regmap_write_fail; + } + } + + /* Sync registers with hardware. */ + regcache_mark_dirty(regmap); + err = regcache_sync(regmap); + if (err) { + dev_err(nvsw_sn2201->dev, "Failed to Sync registers with hardware\n"); + goto regcache_sync_fail; + } + + /* Configure SN2201 board. */ + err = nvsw_sn2201_config_init(nvsw_sn2201, regmap); + if (err) { + dev_err(nvsw_sn2201->dev, "Failed to configure board\n"); + goto nvsw_sn2201_config_init_fail; + } + + return 0; + +nvsw_sn2201_config_init_fail: + nvsw_sn2201_config_exit(nvsw_sn2201); +regcache_sync_fail: +regmap_write_fail: +devm_regmap_init_i2c_fail: +i2c_new_dummy_fail: + i2c_put_adapter(nvsw_sn2201->cpld_devs->adapter); + nvsw_sn2201->cpld_devs->adapter = NULL; +i2c_get_adapter_fail: + /* Destroy SN2201 static I2C devices. */ + nvsw_sn2201_destroy_static_devices(nvsw_sn2201, nvsw_sn2201->sn2201_devs, + nvsw_sn2201->sn2201_devs_num); + /* Destroy main mux device. */ + nvsw_sn2201_destroy_static_devices(nvsw_sn2201, nvsw_sn2201->main_mux_devs, + nvsw_sn2201->main_mux_devs_num); +nvsw_sn2201_create_static_devices_fail: + i2c_put_adapter(nvsw_sn2201->main_mux_devs->adapter); +i2c_get_adapter_main_fail: + return err; +} + +static int nvsw_sn2201_config_pre_init(struct nvsw_sn2201 *nvsw_sn2201) +{ + nvsw_sn2201->i2c_data = &nvsw_sn2201_i2c_data; + + /* Register I2C controller. */ + nvsw_sn2201->i2c_data->handle = nvsw_sn2201; + nvsw_sn2201->i2c_data->completion_notify = nvsw_sn2201_i2c_completion_notify; + nvsw_sn2201->pdev_i2c = platform_device_register_resndata(nvsw_sn2201->dev, "i2c_mlxcpld", + NVSW_SN2201_MAIN_MUX_NR, + nvsw_sn2201_lpc_res, + ARRAY_SIZE(nvsw_sn2201_lpc_res), + nvsw_sn2201->i2c_data, + sizeof(*nvsw_sn2201->i2c_data)); + if (IS_ERR(nvsw_sn2201->pdev_i2c)) + return PTR_ERR(nvsw_sn2201->pdev_i2c); + + return 0; +} + +static int nvsw_sn2201_probe(struct platform_device *pdev) +{ + struct nvsw_sn2201 *nvsw_sn2201; + const char *sku; + int ret; + + nvsw_sn2201 = devm_kzalloc(&pdev->dev, sizeof(*nvsw_sn2201), GFP_KERNEL); + if (!nvsw_sn2201) + return -ENOMEM; + + /* Validate system powering type - only HI168 SKU supports external power. */ + sku = dmi_get_system_info(DMI_PRODUCT_SKU); + if (sku && !strcmp(sku, "HI168")) + nvsw_sn2201->ext_pwr_source = true; + + nvsw_sn2201->dev = &pdev->dev; + platform_set_drvdata(pdev, nvsw_sn2201); + ret = platform_device_add_resources(pdev, nvsw_sn2201_lpc_io_resources, + ARRAY_SIZE(nvsw_sn2201_lpc_io_resources)); + if (ret) + return ret; + + nvsw_sn2201->main_mux_deferred_nr = NVSW_SN2201_MAIN_MUX_DEFER_NR; + nvsw_sn2201->main_mux_devs = nvsw_sn2201_main_mux_brdinfo; + nvsw_sn2201->cpld_devs = nvsw_sn2201_cpld_brdinfo; + if (nvsw_sn2201->ext_pwr_source) { + nvsw_sn2201->sn2201_devs = nvsw_sn2201_busbar_static_brdinfo; + nvsw_sn2201->sn2201_devs_num = ARRAY_SIZE(nvsw_sn2201_busbar_static_brdinfo); + } else { + nvsw_sn2201->sn2201_devs = nvsw_sn2201_static_brdinfo; + nvsw_sn2201->sn2201_devs_num = ARRAY_SIZE(nvsw_sn2201_static_brdinfo); + } + + return nvsw_sn2201_config_pre_init(nvsw_sn2201); +} + +static void nvsw_sn2201_remove(struct platform_device *pdev) +{ + struct nvsw_sn2201 *nvsw_sn2201 = platform_get_drvdata(pdev); + + /* Unregister underlying drivers. */ + nvsw_sn2201_config_exit(nvsw_sn2201); + + /* Destroy SN2201 static I2C devices. */ + nvsw_sn2201_destroy_static_devices(nvsw_sn2201, + nvsw_sn2201->sn2201_devs, + nvsw_sn2201->sn2201_devs_num); + + i2c_put_adapter(nvsw_sn2201->cpld_devs->adapter); + nvsw_sn2201->cpld_devs->adapter = NULL; + /* Destroy main mux device. */ + nvsw_sn2201_destroy_static_devices(nvsw_sn2201, + nvsw_sn2201->main_mux_devs, + nvsw_sn2201->main_mux_devs_num); + + /* Unregister I2C controller. */ + if (nvsw_sn2201->pdev_i2c) + platform_device_unregister(nvsw_sn2201->pdev_i2c); +} + +static const struct acpi_device_id nvsw_sn2201_acpi_ids[] = { + {"NVSN2201", 0}, + {} +}; + +MODULE_DEVICE_TABLE(acpi, nvsw_sn2201_acpi_ids); + +static struct platform_driver nvsw_sn2201_driver = { + .probe = nvsw_sn2201_probe, + .remove = nvsw_sn2201_remove, + .driver = { + .name = "nvsw-sn2201", + .acpi_match_table = nvsw_sn2201_acpi_ids, + }, +}; + +module_platform_driver(nvsw_sn2201_driver); + +MODULE_AUTHOR("Nvidia"); +MODULE_DESCRIPTION("Nvidia sn2201 platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:nvsw-sn2201"); diff --git a/drivers/platform/mips/Kconfig b/drivers/platform/mips/Kconfig new file mode 100644 index 000000000..fb4ac4b08 --- /dev/null +++ b/drivers/platform/mips/Kconfig @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# MIPS Platform Specific Drivers +# + +menuconfig MIPS_PLATFORM_DEVICES + bool "MIPS Platform Specific Device Drivers" + default y + depends on MIPS + help + Say Y here to get to see options for device drivers of various + MIPS platforms, including vendor-specific netbook/laptop/desktop + extension and hardware monitor drivers. This option itself does + not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if MIPS_PLATFORM_DEVICES + +config CPU_HWMON + bool "Loongson-3 CPU HWMon Driver" + depends on MACH_LOONGSON64 + select HWMON + default y + help + Loongson-3A/3B CPU Hwmon (temperature sensor) driver. + +config RS780E_ACPI + bool "Loongson RS780E ACPI Controller" + depends on MACH_LOONGSON64 || COMPILE_TEST + help + Loongson RS780E PCH ACPI Controller driver. + +config LS2K_RESET + bool "Loongson-2K1000 Reset Controller" + depends on MACH_LOONGSON64 || COMPILE_TEST + help + Loongson-2K1000 Reset Controller driver. + +endif # MIPS_PLATFORM_DEVICES diff --git a/drivers/platform/mips/Makefile b/drivers/platform/mips/Makefile new file mode 100644 index 000000000..4c71444e4 --- /dev/null +++ b/drivers/platform/mips/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_CPU_HWMON) += cpu_hwmon.o +obj-$(CONFIG_RS780E_ACPI) += rs780e-acpi.o +obj-$(CONFIG_LS2K_RESET) += ls2k-reset.o diff --git a/drivers/platform/mips/cpu_hwmon.c b/drivers/platform/mips/cpu_hwmon.c new file mode 100644 index 000000000..2ac2f3109 --- /dev/null +++ b/drivers/platform/mips/cpu_hwmon.c @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static int csr_temp_enable; + +/* + * Loongson-3 series cpu has two sensors inside, + * each of them from 0 to 255, + * if more than 127, that is dangerous. + * here only provide sensor1 data, because it always hot than sensor0 + */ +int loongson3_cpu_temp(int cpu) +{ + u32 reg, prid_rev; + + if (csr_temp_enable) { + reg = (csr_readl(LOONGSON_CSR_CPUTEMP) & 0xff); + goto out; + } + + reg = LOONGSON_CHIPTEMP(cpu); + prid_rev = read_c0_prid() & PRID_REV_MASK; + + switch (prid_rev) { + case PRID_REV_LOONGSON3A_R1: + reg = (reg >> 8) & 0xff; + break; + case PRID_REV_LOONGSON3B_R1: + case PRID_REV_LOONGSON3B_R2: + case PRID_REV_LOONGSON3A_R2_0: + case PRID_REV_LOONGSON3A_R2_1: + reg = ((reg >> 8) & 0xff) - 100; + break; + case PRID_REV_LOONGSON3A_R3_0: + case PRID_REV_LOONGSON3A_R3_1: + default: + reg = (reg & 0xffff) * 731 / 0x4000 - 273; + break; + } + +out: + return (int)reg * 1000; +} + +static int nr_packages; +static struct device *cpu_hwmon_dev; + +static ssize_t cpu_temp_label(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int id = (to_sensor_dev_attr(attr))->index - 1; + + return sprintf(buf, "CPU %d Temperature\n", id); +} + +static ssize_t get_cpu_temp(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int id = (to_sensor_dev_attr(attr))->index - 1; + int value = loongson3_cpu_temp(id); + + return sprintf(buf, "%d\n", value); +} + +static SENSOR_DEVICE_ATTR(temp1_input, 0444, get_cpu_temp, NULL, 1); +static SENSOR_DEVICE_ATTR(temp1_label, 0444, cpu_temp_label, NULL, 1); +static SENSOR_DEVICE_ATTR(temp2_input, 0444, get_cpu_temp, NULL, 2); +static SENSOR_DEVICE_ATTR(temp2_label, 0444, cpu_temp_label, NULL, 2); +static SENSOR_DEVICE_ATTR(temp3_input, 0444, get_cpu_temp, NULL, 3); +static SENSOR_DEVICE_ATTR(temp3_label, 0444, cpu_temp_label, NULL, 3); +static SENSOR_DEVICE_ATTR(temp4_input, 0444, get_cpu_temp, NULL, 4); +static SENSOR_DEVICE_ATTR(temp4_label, 0444, cpu_temp_label, NULL, 4); + +static struct attribute *cpu_hwmon_attributes[] = { + &sensor_dev_attr_temp1_input.dev_attr.attr, + &sensor_dev_attr_temp1_label.dev_attr.attr, + &sensor_dev_attr_temp2_input.dev_attr.attr, + &sensor_dev_attr_temp2_label.dev_attr.attr, + &sensor_dev_attr_temp3_input.dev_attr.attr, + &sensor_dev_attr_temp3_label.dev_attr.attr, + &sensor_dev_attr_temp4_input.dev_attr.attr, + &sensor_dev_attr_temp4_label.dev_attr.attr, + NULL +}; + +static umode_t cpu_hwmon_is_visible(struct kobject *kobj, + struct attribute *attr, int i) +{ + int id = i / 2; + + if (id < nr_packages) + return attr->mode; + return 0; +} + +static struct attribute_group cpu_hwmon_group = { + .attrs = cpu_hwmon_attributes, + .is_visible = cpu_hwmon_is_visible, +}; + +static const struct attribute_group *cpu_hwmon_groups[] = { + &cpu_hwmon_group, + NULL +}; + +#define CPU_THERMAL_THRESHOLD 90000 +static struct delayed_work thermal_work; + +static void do_thermal_timer(struct work_struct *work) +{ + int i, value; + + for (i = 0; i < nr_packages; i++) { + value = loongson3_cpu_temp(i); + if (value > CPU_THERMAL_THRESHOLD) { + pr_emerg("Power off due to high temp: %d\n", value); + orderly_poweroff(true); + } + } + + schedule_delayed_work(&thermal_work, msecs_to_jiffies(5000)); +} + +static int __init loongson_hwmon_init(void) +{ + pr_info("Loongson Hwmon Enter...\n"); + + if (cpu_has_csr()) + csr_temp_enable = csr_readl(LOONGSON_CSR_FEATURES) & + LOONGSON_CSRF_TEMP; + + if (!csr_temp_enable && !loongson_chiptemp[0]) + return -ENODEV; + + nr_packages = loongson_sysconf.nr_cpus / + loongson_sysconf.cores_per_package; + + cpu_hwmon_dev = hwmon_device_register_with_groups(NULL, "cpu_hwmon", + NULL, cpu_hwmon_groups); + if (IS_ERR(cpu_hwmon_dev)) { + pr_err("hwmon_device_register fail!\n"); + return PTR_ERR(cpu_hwmon_dev); + } + + INIT_DEFERRABLE_WORK(&thermal_work, do_thermal_timer); + schedule_delayed_work(&thermal_work, msecs_to_jiffies(20000)); + + return 0; +} + +static void __exit loongson_hwmon_exit(void) +{ + cancel_delayed_work_sync(&thermal_work); + hwmon_device_unregister(cpu_hwmon_dev); +} + +module_init(loongson_hwmon_init); +module_exit(loongson_hwmon_exit); + +MODULE_AUTHOR("Yu Xiang "); +MODULE_AUTHOR("Huacai Chen "); +MODULE_DESCRIPTION("Loongson CPU Hwmon driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/mips/ls2k-reset.c b/drivers/platform/mips/ls2k-reset.c new file mode 100644 index 000000000..8f42d5d16 --- /dev/null +++ b/drivers/platform/mips/ls2k-reset.c @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2021, Qing Zhang + * Loongson-2K1000 reset support + */ + +#include +#include +#include + +#define PM1_STS 0x0c /* Power Management 1 Status Register */ +#define PM1_CNT 0x14 /* Power Management 1 Control Register */ +#define RST_CNT 0x30 /* Reset Control Register */ + +static void __iomem *base; + +static void ls2k_restart(char *command) +{ + writel(0x1, base + RST_CNT); +} + +static void ls2k_poweroff(void) +{ + /* Clear */ + writel((readl(base + PM1_STS) & 0xffffffff), base + PM1_STS); + /* Sleep Enable | Soft Off*/ + writel(GENMASK(12, 10) | BIT(13), base + PM1_CNT); +} + +static int ls2k_reset_init(void) +{ + struct device_node *np; + + np = of_find_compatible_node(NULL, NULL, "loongson,ls2k-pm"); + if (!np) { + pr_info("Failed to get PM node\n"); + return -ENODEV; + } + + base = of_iomap(np, 0); + of_node_put(np); + if (!base) { + pr_info("Failed to map PM register base address\n"); + return -ENOMEM; + } + + _machine_restart = ls2k_restart; + pm_power_off = ls2k_poweroff; + + return 0; +} + +arch_initcall(ls2k_reset_init); diff --git a/drivers/platform/mips/rs780e-acpi.c b/drivers/platform/mips/rs780e-acpi.c new file mode 100644 index 000000000..5b8f9cc32 --- /dev/null +++ b/drivers/platform/mips/rs780e-acpi.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include + +static unsigned long acpi_iobase; + +#define ACPI_PM_EVT_BLK (acpi_iobase + 0x00) /* 4 bytes */ +#define ACPI_PM_CNT_BLK (acpi_iobase + 0x04) /* 2 bytes */ +#define ACPI_PMA_CNT_BLK (acpi_iobase + 0x0F) /* 1 byte */ +#define ACPI_PM_TMR_BLK (acpi_iobase + 0x18) /* 4 bytes */ +#define ACPI_GPE0_BLK (acpi_iobase + 0x10) /* 8 bytes */ +#define ACPI_END (acpi_iobase + 0x80) + +#define PM_INDEX 0xCD6 +#define PM_DATA 0xCD7 +#define PM2_INDEX 0xCD0 +#define PM2_DATA 0xCD1 + +static void pmio_write_index(u16 index, u8 reg, u8 value) +{ + outb(reg, index); + outb(value, index + 1); +} + +static u8 pmio_read_index(u16 index, u8 reg) +{ + outb(reg, index); + return inb(index + 1); +} + +static void pm_iowrite(u8 reg, u8 value) +{ + pmio_write_index(PM_INDEX, reg, value); +} + +static u8 pm_ioread(u8 reg) +{ + return pmio_read_index(PM_INDEX, reg); +} + +static void pm2_iowrite(u8 reg, u8 value) +{ + pmio_write_index(PM2_INDEX, reg, value); +} + +static u8 pm2_ioread(u8 reg) +{ + return pmio_read_index(PM2_INDEX, reg); +} + +static void acpi_hw_clear_status(void) +{ + u16 value; + + /* PMStatus: Clear WakeStatus/PwrBtnStatus */ + value = inw(ACPI_PM_EVT_BLK); + value |= (1 << 8 | 1 << 15); + outw(value, ACPI_PM_EVT_BLK); + + /* GPEStatus: Clear all generated events */ + outl(inl(ACPI_GPE0_BLK), ACPI_GPE0_BLK); +} + +static void acpi_registers_setup(void) +{ + u32 value; + + /* PM Status Base */ + pm_iowrite(0x20, ACPI_PM_EVT_BLK & 0xff); + pm_iowrite(0x21, ACPI_PM_EVT_BLK >> 8); + + /* PM Control Base */ + pm_iowrite(0x22, ACPI_PM_CNT_BLK & 0xff); + pm_iowrite(0x23, ACPI_PM_CNT_BLK >> 8); + + /* GPM Base */ + pm_iowrite(0x28, ACPI_GPE0_BLK & 0xff); + pm_iowrite(0x29, ACPI_GPE0_BLK >> 8); + + /* ACPI End */ + pm_iowrite(0x2e, ACPI_END & 0xff); + pm_iowrite(0x2f, ACPI_END >> 8); + + /* IO Decode: When AcpiDecodeEnable set, South-Bridge uses the contents + * of the PM registers at index 0x20~0x2B to decode ACPI I/O address. */ + pm_iowrite(0x0e, 1 << 3); + + /* SCI_EN set */ + outw(1, ACPI_PM_CNT_BLK); + + /* Enable to generate SCI */ + pm_iowrite(0x10, pm_ioread(0x10) | 1); + + /* GPM3/GPM9 enable */ + value = inl(ACPI_GPE0_BLK + 4); + outl(value | (1 << 14) | (1 << 22), ACPI_GPE0_BLK + 4); + + /* Set GPM9 as input */ + pm_iowrite(0x8d, pm_ioread(0x8d) & (~(1 << 1))); + + /* Set GPM9 as non-output */ + pm_iowrite(0x94, pm_ioread(0x94) | (1 << 3)); + + /* GPM3 config ACPI trigger SCIOUT */ + pm_iowrite(0x33, pm_ioread(0x33) & (~(3 << 4))); + + /* GPM9 config ACPI trigger SCIOUT */ + pm_iowrite(0x3d, pm_ioread(0x3d) & (~(3 << 2))); + + /* GPM3 config falling edge trigger */ + pm_iowrite(0x37, pm_ioread(0x37) & (~(1 << 6))); + + /* No wait for STPGNT# in ACPI Sx state */ + pm_iowrite(0x7c, pm_ioread(0x7c) | (1 << 6)); + + /* Set GPM3 pull-down enable */ + value = pm2_ioread(0xf6); + value |= ((1 << 7) | (1 << 3)); + pm2_iowrite(0xf6, value); + + /* Set GPM9 pull-down enable */ + value = pm2_ioread(0xf8); + value |= ((1 << 5) | (1 << 1)); + pm2_iowrite(0xf8, value); +} + +static int rs780e_acpi_probe(struct platform_device *pdev) +{ + struct resource *res; + + res = platform_get_resource(pdev, IORESOURCE_IO, 0); + if (!res) + return -ENODEV; + + /* SCI interrupt need acpi space, allocate here */ + if (!request_region(res->start, resource_size(res), "acpi")) { + pr_err("RS780E-ACPI: Failed to request IO Region\n"); + return -EBUSY; + } + + acpi_iobase = res->start; + + acpi_registers_setup(); + acpi_hw_clear_status(); + + return 0; +} + +static const struct of_device_id rs780e_acpi_match[] = { + { .compatible = "loongson,rs780e-acpi" }, + {}, +}; + +static struct platform_driver rs780e_acpi_driver = { + .probe = rs780e_acpi_probe, + .driver = { + .name = "RS780E-ACPI", + .of_match_table = rs780e_acpi_match, + }, +}; +builtin_platform_driver(rs780e_acpi_driver); diff --git a/drivers/platform/olpc/Kconfig b/drivers/platform/olpc/Kconfig new file mode 100644 index 000000000..919b489e2 --- /dev/null +++ b/drivers/platform/olpc/Kconfig @@ -0,0 +1,29 @@ +config OLPC_EC + select REGULATOR + bool + +menuconfig OLPC_XO175 + bool "Platform support for OLPC XO 1.75 hardware" + depends on ARCH_MMP || COMPILE_TEST + help + Say Y here to get to see options for the ARM-based OLPC platform. + This option alone does not add any kernel code. + + Unless you have an OLPC XO laptop, you will want to say N. + +if OLPC_XO175 + +config OLPC_XO175_EC + tristate "OLPC XO 1.75 Embedded Controller" + depends on SPI_SLAVE + depends on INPUT + depends on POWER_SUPPLY + select OLPC_EC + help + Include support for the OLPC XO Embedded Controller (EC). The EC + provides various platform services, including support for the power, + button, restart, shutdown and battery charging status. + + Unless you have an OLPC XO laptop, you will want to say N. + +endif # OLPC_XO175 diff --git a/drivers/platform/olpc/Makefile b/drivers/platform/olpc/Makefile new file mode 100644 index 000000000..e9b67000c --- /dev/null +++ b/drivers/platform/olpc/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# OLPC XO platform-specific drivers +# +obj-$(CONFIG_OLPC_EC) += olpc-ec.o +obj-$(CONFIG_OLPC_XO175_EC) += olpc-xo175-ec.o diff --git a/drivers/platform/olpc/olpc-ec.c b/drivers/platform/olpc/olpc-ec.c new file mode 100644 index 000000000..4a2d36f73 --- /dev/null +++ b/drivers/platform/olpc/olpc-ec.c @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic driver for the OLPC Embedded Controller. + * + * Author: Andres Salomon + * + * Copyright (C) 2011-2012 One Laptop per Child Foundation. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct ec_cmd_desc { + u8 cmd; + u8 *inbuf, *outbuf; + size_t inlen, outlen; + + int err; + struct completion finished; + struct list_head node; + + void *priv; +}; + +struct olpc_ec_priv { + struct olpc_ec_driver *drv; + u8 version; + struct work_struct worker; + struct mutex cmd_lock; + + /* DCON regulator */ + bool dcon_enabled; + + /* Pending EC commands */ + struct list_head cmd_q; + spinlock_t cmd_q_lock; + + struct dentry *dbgfs_dir; + + /* + * EC event mask to be applied during suspend (defining wakeup + * sources). + */ + u16 ec_wakeup_mask; + + /* + * Running an EC command while suspending means we don't always finish + * the command before the machine suspends. This means that the EC + * is expecting the command protocol to finish, but we after a period + * of time (while the OS is asleep) the EC times out and restarts its + * idle loop. Meanwhile, the OS wakes up, thinks it's still in the + * middle of the command protocol, starts throwing random things at + * the EC... and everyone's uphappy. + */ + bool suspended; +}; + +static struct olpc_ec_driver *ec_driver; +static struct olpc_ec_priv *ec_priv; +static void *ec_cb_arg; + +void olpc_ec_driver_register(struct olpc_ec_driver *drv, void *arg) +{ + ec_driver = drv; + ec_cb_arg = arg; +} +EXPORT_SYMBOL_GPL(olpc_ec_driver_register); + +static void olpc_ec_worker(struct work_struct *w) +{ + struct olpc_ec_priv *ec = container_of(w, struct olpc_ec_priv, worker); + struct ec_cmd_desc *desc = NULL; + unsigned long flags; + + /* Grab the first pending command from the queue */ + spin_lock_irqsave(&ec->cmd_q_lock, flags); + if (!list_empty(&ec->cmd_q)) { + desc = list_first_entry(&ec->cmd_q, struct ec_cmd_desc, node); + list_del(&desc->node); + } + spin_unlock_irqrestore(&ec->cmd_q_lock, flags); + + /* Do we actually have anything to do? */ + if (!desc) + return; + + /* Protect the EC hw with a mutex; only run one cmd at a time */ + mutex_lock(&ec->cmd_lock); + desc->err = ec_driver->ec_cmd(desc->cmd, desc->inbuf, desc->inlen, + desc->outbuf, desc->outlen, ec_cb_arg); + mutex_unlock(&ec->cmd_lock); + + /* Finished, wake up olpc_ec_cmd() */ + complete(&desc->finished); + + /* Run the worker thread again in case there are more cmds pending */ + schedule_work(&ec->worker); +} + +/* + * Throw a cmd descripter onto the list. We now have SMP OLPC machines, so + * locking is pretty critical. + */ +static void queue_ec_descriptor(struct ec_cmd_desc *desc, + struct olpc_ec_priv *ec) +{ + unsigned long flags; + + INIT_LIST_HEAD(&desc->node); + + spin_lock_irqsave(&ec->cmd_q_lock, flags); + list_add_tail(&desc->node, &ec->cmd_q); + spin_unlock_irqrestore(&ec->cmd_q_lock, flags); + + schedule_work(&ec->worker); +} + +int olpc_ec_cmd(u8 cmd, u8 *inbuf, size_t inlen, u8 *outbuf, size_t outlen) +{ + struct olpc_ec_priv *ec = ec_priv; + struct ec_cmd_desc desc; + + /* Driver not yet registered. */ + if (!ec_driver) + return -EPROBE_DEFER; + + if (WARN_ON(!ec_driver->ec_cmd)) + return -ENODEV; + + if (!ec) + return -ENOMEM; + + /* Suspending in the middle of a command hoses things really badly */ + if (WARN_ON(ec->suspended)) + return -EBUSY; + + might_sleep(); + + desc.cmd = cmd; + desc.inbuf = inbuf; + desc.outbuf = outbuf; + desc.inlen = inlen; + desc.outlen = outlen; + desc.err = 0; + init_completion(&desc.finished); + + queue_ec_descriptor(&desc, ec); + + /* Timeouts must be handled in the platform-specific EC hook */ + wait_for_completion(&desc.finished); + + /* The worker thread dequeues the cmd; no need to do anything here */ + return desc.err; +} +EXPORT_SYMBOL_GPL(olpc_ec_cmd); + +void olpc_ec_wakeup_set(u16 value) +{ + struct olpc_ec_priv *ec = ec_priv; + + if (WARN_ON(!ec)) + return; + + ec->ec_wakeup_mask |= value; +} +EXPORT_SYMBOL_GPL(olpc_ec_wakeup_set); + +void olpc_ec_wakeup_clear(u16 value) +{ + struct olpc_ec_priv *ec = ec_priv; + + if (WARN_ON(!ec)) + return; + + ec->ec_wakeup_mask &= ~value; +} +EXPORT_SYMBOL_GPL(olpc_ec_wakeup_clear); + +int olpc_ec_mask_write(u16 bits) +{ + struct olpc_ec_priv *ec = ec_priv; + + if (WARN_ON(!ec)) + return -ENODEV; + + /* EC version 0x5f adds support for wide SCI mask */ + if (ec->version >= 0x5f) { + __be16 ec_word = cpu_to_be16(bits); + + return olpc_ec_cmd(EC_WRITE_EXT_SCI_MASK, (void *)&ec_word, 2, NULL, 0); + } else { + u8 ec_byte = bits & 0xff; + + return olpc_ec_cmd(EC_WRITE_SCI_MASK, &ec_byte, 1, NULL, 0); + } +} +EXPORT_SYMBOL_GPL(olpc_ec_mask_write); + +/* + * Returns true if the compile and runtime configurations allow for EC events + * to wake the system. + */ +bool olpc_ec_wakeup_available(void) +{ + if (WARN_ON(!ec_driver)) + return false; + + return ec_driver->wakeup_available; +} +EXPORT_SYMBOL_GPL(olpc_ec_wakeup_available); + +int olpc_ec_sci_query(u16 *sci_value) +{ + struct olpc_ec_priv *ec = ec_priv; + int ret; + + if (WARN_ON(!ec)) + return -ENODEV; + + /* EC version 0x5f adds support for wide SCI mask */ + if (ec->version >= 0x5f) { + __be16 ec_word; + + ret = olpc_ec_cmd(EC_EXT_SCI_QUERY, NULL, 0, (void *)&ec_word, 2); + if (ret == 0) + *sci_value = be16_to_cpu(ec_word); + } else { + u8 ec_byte; + + ret = olpc_ec_cmd(EC_SCI_QUERY, NULL, 0, &ec_byte, 1); + if (ret == 0) + *sci_value = ec_byte; + } + + return ret; +} +EXPORT_SYMBOL_GPL(olpc_ec_sci_query); + +#ifdef CONFIG_DEBUG_FS + +/* + * debugfs support for "generic commands", to allow sending + * arbitrary EC commands from userspace. + */ + +#define EC_MAX_CMD_ARGS (5 + 1) /* cmd byte + 5 args */ +#define EC_MAX_CMD_REPLY (8) + +static DEFINE_MUTEX(ec_dbgfs_lock); +static unsigned char ec_dbgfs_resp[EC_MAX_CMD_REPLY]; +static unsigned int ec_dbgfs_resp_bytes; + +static ssize_t ec_dbgfs_cmd_write(struct file *file, const char __user *buf, + size_t size, loff_t *ppos) +{ + int i, m; + unsigned char ec_cmd[EC_MAX_CMD_ARGS]; + unsigned int ec_cmd_int[EC_MAX_CMD_ARGS]; + char cmdbuf[64] = ""; + int ec_cmd_bytes; + + mutex_lock(&ec_dbgfs_lock); + + size = simple_write_to_buffer(cmdbuf, sizeof(cmdbuf), ppos, buf, size); + + m = sscanf(cmdbuf, "%x:%u %x %x %x %x %x", &ec_cmd_int[0], + &ec_dbgfs_resp_bytes, &ec_cmd_int[1], &ec_cmd_int[2], + &ec_cmd_int[3], &ec_cmd_int[4], &ec_cmd_int[5]); + if (m < 2 || ec_dbgfs_resp_bytes > EC_MAX_CMD_REPLY) { + /* reset to prevent overflow on read */ + ec_dbgfs_resp_bytes = 0; + + pr_debug("olpc-ec: bad ec cmd: cmd:response-count [arg1 [arg2 ...]]\n"); + size = -EINVAL; + goto out; + } + + /* convert scanf'd ints to char */ + ec_cmd_bytes = m - 2; + for (i = 0; i <= ec_cmd_bytes; i++) + ec_cmd[i] = ec_cmd_int[i]; + + pr_debug("olpc-ec: debugfs cmd 0x%02x with %d args %5ph, want %d returns\n", + ec_cmd[0], ec_cmd_bytes, ec_cmd + 1, + ec_dbgfs_resp_bytes); + + olpc_ec_cmd(ec_cmd[0], (ec_cmd_bytes == 0) ? NULL : &ec_cmd[1], + ec_cmd_bytes, ec_dbgfs_resp, ec_dbgfs_resp_bytes); + + pr_debug("olpc-ec: response %8ph (%d bytes expected)\n", + ec_dbgfs_resp, ec_dbgfs_resp_bytes); + +out: + mutex_unlock(&ec_dbgfs_lock); + return size; +} + +static ssize_t ec_dbgfs_cmd_read(struct file *file, char __user *buf, + size_t size, loff_t *ppos) +{ + unsigned int i, r; + char *rp; + char respbuf[64]; + + mutex_lock(&ec_dbgfs_lock); + rp = respbuf; + rp += sprintf(rp, "%02x", ec_dbgfs_resp[0]); + for (i = 1; i < ec_dbgfs_resp_bytes; i++) + rp += sprintf(rp, ", %02x", ec_dbgfs_resp[i]); + mutex_unlock(&ec_dbgfs_lock); + rp += sprintf(rp, "\n"); + + r = rp - respbuf; + return simple_read_from_buffer(buf, size, ppos, respbuf, r); +} + +static const struct file_operations ec_dbgfs_ops = { + .write = ec_dbgfs_cmd_write, + .read = ec_dbgfs_cmd_read, +}; + +static struct dentry *olpc_ec_setup_debugfs(void) +{ + struct dentry *dbgfs_dir; + + dbgfs_dir = debugfs_create_dir("olpc-ec", NULL); + debugfs_create_file("cmd", 0600, dbgfs_dir, NULL, &ec_dbgfs_ops); + + return dbgfs_dir; +} + +#else + +static struct dentry *olpc_ec_setup_debugfs(void) +{ + return NULL; +} + +#endif /* CONFIG_DEBUG_FS */ + +static int olpc_ec_set_dcon_power(struct olpc_ec_priv *ec, bool state) +{ + unsigned char ec_byte = state; + int ret; + + if (ec->dcon_enabled == state) + return 0; + + ret = olpc_ec_cmd(EC_DCON_POWER_MODE, &ec_byte, 1, NULL, 0); + if (ret) + return ret; + + ec->dcon_enabled = state; + return 0; +} + +static int dcon_regulator_enable(struct regulator_dev *rdev) +{ + struct olpc_ec_priv *ec = rdev_get_drvdata(rdev); + + return olpc_ec_set_dcon_power(ec, true); +} + +static int dcon_regulator_disable(struct regulator_dev *rdev) +{ + struct olpc_ec_priv *ec = rdev_get_drvdata(rdev); + + return olpc_ec_set_dcon_power(ec, false); +} + +static int dcon_regulator_is_enabled(struct regulator_dev *rdev) +{ + struct olpc_ec_priv *ec = rdev_get_drvdata(rdev); + + return ec->dcon_enabled ? 1 : 0; +} + +static const struct regulator_ops dcon_regulator_ops = { + .enable = dcon_regulator_enable, + .disable = dcon_regulator_disable, + .is_enabled = dcon_regulator_is_enabled, +}; + +static const struct regulator_desc dcon_desc = { + .name = "dcon", + .id = 0, + .ops = &dcon_regulator_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_time = 25000, +}; + +static int olpc_ec_probe(struct platform_device *pdev) +{ + struct olpc_ec_priv *ec; + struct regulator_config config = { }; + struct regulator_dev *regulator; + int err; + + if (!ec_driver) + return -ENODEV; + + ec = kzalloc_obj(*ec); + if (!ec) + return -ENOMEM; + + ec->drv = ec_driver; + INIT_WORK(&ec->worker, olpc_ec_worker); + mutex_init(&ec->cmd_lock); + + INIT_LIST_HEAD(&ec->cmd_q); + spin_lock_init(&ec->cmd_q_lock); + + ec_priv = ec; + platform_set_drvdata(pdev, ec); + + /* get the EC revision */ + err = olpc_ec_cmd(EC_FIRMWARE_REV, NULL, 0, &ec->version, 1); + if (err) + goto error; + + config.dev = pdev->dev.parent; + config.driver_data = ec; + ec->dcon_enabled = true; + regulator = devm_regulator_register(&pdev->dev, &dcon_desc, &config); + if (IS_ERR(regulator)) { + dev_err(&pdev->dev, "failed to register DCON regulator\n"); + err = PTR_ERR(regulator); + goto error; + } + + ec->dbgfs_dir = olpc_ec_setup_debugfs(); + + return 0; + +error: + ec_priv = NULL; + kfree(ec); + return err; +} + +static int olpc_ec_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct olpc_ec_priv *ec = platform_get_drvdata(pdev); + int err = 0; + + olpc_ec_mask_write(ec->ec_wakeup_mask); + + if (ec_driver->suspend) + err = ec_driver->suspend(pdev); + if (!err) + ec->suspended = true; + + return err; +} + +static int olpc_ec_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct olpc_ec_priv *ec = platform_get_drvdata(pdev); + + ec->suspended = false; + return ec_driver->resume ? ec_driver->resume(pdev) : 0; +} + +static const struct dev_pm_ops olpc_ec_pm_ops = { + .suspend_late = olpc_ec_suspend, + .resume_early = olpc_ec_resume, +}; + +static struct platform_driver olpc_ec_plat_driver = { + .probe = olpc_ec_probe, + .driver = { + .name = "olpc-ec", + .pm = &olpc_ec_pm_ops, + }, +}; + +static int __init olpc_ec_init_module(void) +{ + return platform_driver_register(&olpc_ec_plat_driver); +} +arch_initcall(olpc_ec_init_module); diff --git a/drivers/platform/olpc/olpc-xo175-ec.c b/drivers/platform/olpc/olpc-xo175-ec.c new file mode 100644 index 000000000..bee271a4f --- /dev/null +++ b/drivers/platform/olpc/olpc-xo175-ec.c @@ -0,0 +1,758 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for the OLPC XO-1.75 Embedded Controller. + * + * The EC protocol is documented at: + * http://wiki.laptop.org/go/XO_1.75_HOST_to_EC_Protocol + * + * Copyright (C) 2010 One Laptop per Child Foundation. + * Copyright (C) 2018 Lubomir Rintel + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct ec_cmd_t { + u8 cmd; + u8 bytes_returned; +}; + +enum ec_chan_t { + CHAN_NONE = 0, + CHAN_SWITCH, + CHAN_CMD_RESP, + CHAN_KEYBOARD, + CHAN_TOUCHPAD, + CHAN_EVENT, + CHAN_DEBUG, + CHAN_CMD_ERROR, +}; + +/* + * EC events + */ +#define EVENT_AC_CHANGE 1 /* AC plugged/unplugged */ +#define EVENT_BATTERY_STATUS 2 /* Battery low/full/error/gone */ +#define EVENT_BATTERY_CRITICAL 3 /* Battery critical voltage */ +#define EVENT_BATTERY_SOC_CHANGE 4 /* 1% SOC Change */ +#define EVENT_BATTERY_ERROR 5 /* Abnormal error, query for cause */ +#define EVENT_POWER_PRESSED 6 /* Power button was pressed */ +#define EVENT_POWER_PRESS_WAKE 7 /* Woken up with a power button */ +#define EVENT_TIMED_HOST_WAKE 8 /* Host wake timer */ +#define EVENT_OLS_HIGH_LIMIT 9 /* OLS crossed dark threshold */ +#define EVENT_OLS_LOW_LIMIT 10 /* OLS crossed light threshold */ + +/* + * EC commands + * (from http://dev.laptop.org/git/users/rsmith/ec-1.75/tree/ec_cmd.h) + */ +#define CMD_GET_API_VERSION 0x08 /* out: u8 */ +#define CMD_READ_VOLTAGE 0x10 /* out: u16, *9.76/32, mV */ +#define CMD_READ_CURRENT 0x11 /* out: s16, *15.625/120, mA */ +#define CMD_READ_ACR 0x12 /* out: s16, *6250/15, uAh */ +#define CMD_READ_BATT_TEMPERATURE 0x13 /* out: u16, *100/256, deg C */ +#define CMD_READ_AMBIENT_TEMPERATURE 0x14 /* unimplemented, no hardware */ +#define CMD_READ_BATTERY_STATUS 0x15 /* out: u8, bitmask */ +#define CMD_READ_SOC 0x16 /* out: u8, percentage */ +#define CMD_READ_GAUGE_ID 0x17 /* out: u8 * 8 */ +#define CMD_READ_GAUGE_DATA 0x18 /* in: u8 addr, out: u8 data */ +#define CMD_READ_BOARD_ID 0x19 /* out: u16 (platform id) */ +#define CMD_READ_BATT_ERR_CODE 0x1f /* out: u8, error bitmask */ +#define CMD_SET_DCON_POWER 0x26 /* in: u8 */ +#define CMD_RESET_EC 0x28 /* none */ +#define CMD_READ_BATTERY_TYPE 0x2c /* out: u8 */ +#define CMD_SET_AUTOWAK 0x33 /* out: u8 */ +#define CMD_SET_EC_WAKEUP_TIMER 0x36 /* in: u32, out: ? */ +#define CMD_READ_EXT_SCI_MASK 0x37 /* ? */ +#define CMD_WRITE_EXT_SCI_MASK 0x38 /* ? */ +#define CMD_CLEAR_EC_WAKEUP_TIMER 0x39 /* none */ +#define CMD_ENABLE_RUNIN_DISCHARGE 0x3B /* none */ +#define CMD_DISABLE_RUNIN_DISCHARGE 0x3C /* none */ +#define CMD_READ_MPPT_ACTIVE 0x3d /* out: u8 */ +#define CMD_READ_MPPT_LIMIT 0x3e /* out: u8 */ +#define CMD_SET_MPPT_LIMIT 0x3f /* in: u8 */ +#define CMD_DISABLE_MPPT 0x40 /* none */ +#define CMD_ENABLE_MPPT 0x41 /* none */ +#define CMD_READ_VIN 0x42 /* out: u16 */ +#define CMD_EXT_SCI_QUERY 0x43 /* ? */ +#define RSP_KEYBOARD_DATA 0x48 /* ? */ +#define RSP_TOUCHPAD_DATA 0x49 /* ? */ +#define CMD_GET_FW_VERSION 0x4a /* out: u8 * 16 */ +#define CMD_POWER_CYCLE 0x4b /* none */ +#define CMD_POWER_OFF 0x4c /* none */ +#define CMD_RESET_EC_SOFT 0x4d /* none */ +#define CMD_READ_GAUGE_U16 0x4e /* ? */ +#define CMD_ENABLE_MOUSE 0x4f /* ? */ +#define CMD_ECHO 0x52 /* in: u8 * 5, out: u8 * 5 */ +#define CMD_GET_FW_DATE 0x53 /* out: u8 * 16 */ +#define CMD_GET_FW_USER 0x54 /* out: u8 * 16 */ +#define CMD_TURN_OFF_POWER 0x55 /* none (same as 0x4c) */ +#define CMD_READ_OLS 0x56 /* out: u16 */ +#define CMD_OLS_SMT_LEDON 0x57 /* none */ +#define CMD_OLS_SMT_LEDOFF 0x58 /* none */ +#define CMD_START_OLS_ASSY 0x59 /* none */ +#define CMD_STOP_OLS_ASSY 0x5a /* none */ +#define CMD_OLS_SMTTEST_STOP 0x5b /* none */ +#define CMD_READ_VIN_SCALED 0x5c /* out: u16 */ +#define CMD_READ_BAT_MIN_W 0x5d /* out: u16 */ +#define CMD_READ_BAR_MAX_W 0x5e /* out: u16 */ +#define CMD_RESET_BAT_MINMAX_W 0x5f /* none */ +#define CMD_READ_LOCATION 0x60 /* in: u16 addr, out: u8 data */ +#define CMD_WRITE_LOCATION 0x61 /* in: u16 addr, u8 data */ +#define CMD_KEYBOARD_CMD 0x62 /* in: u8, out: ? */ +#define CMD_TOUCHPAD_CMD 0x63 /* in: u8, out: ? */ +#define CMD_GET_FW_HASH 0x64 /* out: u8 * 16 */ +#define CMD_SUSPEND_HINT 0x65 /* in: u8 */ +#define CMD_ENABLE_WAKE_TIMER 0x66 /* in: u8 */ +#define CMD_SET_WAKE_TIMER 0x67 /* in: 32 */ +#define CMD_ENABLE_WAKE_AUTORESET 0x68 /* in: u8 */ +#define CMD_OLS_SET_LIMITS 0x69 /* in: u16, u16 */ +#define CMD_OLS_GET_LIMITS 0x6a /* out: u16, u16 */ +#define CMD_OLS_SET_CEILING 0x6b /* in: u16 */ +#define CMD_OLS_GET_CEILING 0x6c /* out: u16 */ + +/* + * Accepted EC commands, and how many bytes they return. There are plenty + * of EC commands that are no longer implemented, or are implemented only on + * certain older boards. + */ +static const struct ec_cmd_t olpc_xo175_ec_cmds[] = { + { CMD_GET_API_VERSION, 1 }, + { CMD_READ_VOLTAGE, 2 }, + { CMD_READ_CURRENT, 2 }, + { CMD_READ_ACR, 2 }, + { CMD_READ_BATT_TEMPERATURE, 2 }, + { CMD_READ_BATTERY_STATUS, 1 }, + { CMD_READ_SOC, 1 }, + { CMD_READ_GAUGE_ID, 8 }, + { CMD_READ_GAUGE_DATA, 1 }, + { CMD_READ_BOARD_ID, 2 }, + { CMD_READ_BATT_ERR_CODE, 1 }, + { CMD_SET_DCON_POWER, 0 }, + { CMD_RESET_EC, 0 }, + { CMD_READ_BATTERY_TYPE, 1 }, + { CMD_ENABLE_RUNIN_DISCHARGE, 0 }, + { CMD_DISABLE_RUNIN_DISCHARGE, 0 }, + { CMD_READ_MPPT_ACTIVE, 1 }, + { CMD_READ_MPPT_LIMIT, 1 }, + { CMD_SET_MPPT_LIMIT, 0 }, + { CMD_DISABLE_MPPT, 0 }, + { CMD_ENABLE_MPPT, 0 }, + { CMD_READ_VIN, 2 }, + { CMD_GET_FW_VERSION, 16 }, + { CMD_POWER_CYCLE, 0 }, + { CMD_POWER_OFF, 0 }, + { CMD_RESET_EC_SOFT, 0 }, + { CMD_ECHO, 5 }, + { CMD_GET_FW_DATE, 16 }, + { CMD_GET_FW_USER, 16 }, + { CMD_TURN_OFF_POWER, 0 }, + { CMD_READ_OLS, 2 }, + { CMD_OLS_SMT_LEDON, 0 }, + { CMD_OLS_SMT_LEDOFF, 0 }, + { CMD_START_OLS_ASSY, 0 }, + { CMD_STOP_OLS_ASSY, 0 }, + { CMD_OLS_SMTTEST_STOP, 0 }, + { CMD_READ_VIN_SCALED, 2 }, + { CMD_READ_BAT_MIN_W, 2 }, + { CMD_READ_BAR_MAX_W, 2 }, + { CMD_RESET_BAT_MINMAX_W, 0 }, + { CMD_READ_LOCATION, 1 }, + { CMD_WRITE_LOCATION, 0 }, + { CMD_GET_FW_HASH, 16 }, + { CMD_SUSPEND_HINT, 0 }, + { CMD_ENABLE_WAKE_TIMER, 0 }, + { CMD_SET_WAKE_TIMER, 0 }, + { CMD_ENABLE_WAKE_AUTORESET, 0 }, + { CMD_OLS_SET_LIMITS, 0 }, + { CMD_OLS_GET_LIMITS, 4 }, + { CMD_OLS_SET_CEILING, 0 }, + { CMD_OLS_GET_CEILING, 2 }, + { CMD_READ_EXT_SCI_MASK, 2 }, + { CMD_WRITE_EXT_SCI_MASK, 0 }, + + { } +}; + +#define EC_MAX_CMD_DATA_LEN 5 +#define EC_MAX_RESP_LEN 16 + +#define LOG_BUF_SIZE 128 + +#define PM_WAKEUP_TIME 1000 + +#define EC_ALL_EVENTS GENMASK(15, 0) + +enum ec_state_t { + CMD_STATE_IDLE = 0, + CMD_STATE_WAITING_FOR_SWITCH, + CMD_STATE_CMD_IN_TX_FIFO, + CMD_STATE_CMD_SENT, + CMD_STATE_RESP_RECEIVED, + CMD_STATE_ERROR_RECEIVED, +}; + +struct olpc_xo175_ec_cmd { + u8 command; + u8 nr_args; + u8 data_len; + u8 args[EC_MAX_CMD_DATA_LEN]; +}; + +struct olpc_xo175_ec_resp { + u8 channel; + u8 byte; +}; + +struct olpc_xo175_ec { + bool suspended; + + /* SPI related stuff. */ + struct spi_device *spi; + struct spi_transfer xfer; + struct spi_message msg; + union { + struct olpc_xo175_ec_cmd cmd; + struct olpc_xo175_ec_resp resp; + } tx_buf, rx_buf; + + /* GPIO for the CMD signals. */ + struct gpio_desc *gpio_cmd; + + /* Command handling related state. */ + spinlock_t cmd_state_lock; + int cmd_state; + bool cmd_running; + struct completion cmd_done; + struct olpc_xo175_ec_cmd cmd; + u8 resp_data[EC_MAX_RESP_LEN]; + int expected_resp_len; + int resp_len; + + /* Power button. */ + struct input_dev *pwrbtn; + + /* Debug handling. */ + char logbuf[LOG_BUF_SIZE]; + int logbuf_len; +}; + +static struct platform_device *olpc_ec; + +static int olpc_xo175_ec_resp_len(u8 cmd) +{ + const struct ec_cmd_t *p; + + for (p = olpc_xo175_ec_cmds; p->cmd; p++) { + if (p->cmd == cmd) + return p->bytes_returned; + } + + return -EINVAL; +} + +static void olpc_xo175_ec_flush_logbuf(struct olpc_xo175_ec *priv) +{ + dev_dbg(&priv->spi->dev, "got debug string [%*pE]\n", + priv->logbuf_len, priv->logbuf); + priv->logbuf_len = 0; +} + +static void olpc_xo175_ec_complete(void *arg); + +static void olpc_xo175_ec_send_command(struct olpc_xo175_ec *priv, void *cmd, + size_t cmdlen) +{ + int ret; + + memcpy(&priv->tx_buf, cmd, cmdlen); + priv->xfer.len = cmdlen; + + spi_message_init_with_transfers(&priv->msg, &priv->xfer, 1); + + priv->msg.complete = olpc_xo175_ec_complete; + priv->msg.context = priv; + + ret = spi_async(priv->spi, &priv->msg); + if (ret) + dev_err(&priv->spi->dev, "spi_async() failed %d\n", ret); +} + +static void olpc_xo175_ec_read_packet(struct olpc_xo175_ec *priv) +{ + u8 nonce[] = {0xA5, 0x5A}; + + olpc_xo175_ec_send_command(priv, nonce, sizeof(nonce)); +} + +static void olpc_xo175_ec_complete(void *arg) +{ + struct olpc_xo175_ec *priv = arg; + struct device *dev = &priv->spi->dev; + struct power_supply *psy; + unsigned long flags; + u8 channel; + u8 byte; + int ret; + + ret = priv->msg.status; + if (ret) { + dev_err(dev, "SPI transfer failed: %d\n", ret); + + spin_lock_irqsave(&priv->cmd_state_lock, flags); + if (priv->cmd_running) { + priv->resp_len = 0; + priv->cmd_state = CMD_STATE_ERROR_RECEIVED; + complete(&priv->cmd_done); + } + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + + if (ret != -EINTR) + olpc_xo175_ec_read_packet(priv); + + return; + } + + channel = priv->rx_buf.resp.channel; + byte = priv->rx_buf.resp.byte; + + switch (channel) { + case CHAN_NONE: + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + if (!priv->cmd_running) { + /* We can safely ignore these */ + dev_err(dev, "spurious FIFO read packet\n"); + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + return; + } + + priv->cmd_state = CMD_STATE_CMD_SENT; + if (!priv->expected_resp_len) + complete(&priv->cmd_done); + olpc_xo175_ec_read_packet(priv); + + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + return; + + case CHAN_SWITCH: + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + if (!priv->cmd_running) { + /* Just go with the flow */ + dev_err(dev, "spurious SWITCH packet\n"); + memset(&priv->cmd, 0, sizeof(priv->cmd)); + priv->cmd.command = CMD_ECHO; + } + + priv->cmd_state = CMD_STATE_CMD_IN_TX_FIFO; + + /* Throw command into TxFIFO */ + gpiod_set_value_cansleep(priv->gpio_cmd, 0); + olpc_xo175_ec_send_command(priv, &priv->cmd, sizeof(priv->cmd)); + + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + return; + + case CHAN_CMD_RESP: + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + if (!priv->cmd_running) { + dev_err(dev, "spurious response packet\n"); + } else if (priv->resp_len >= priv->expected_resp_len) { + dev_err(dev, "too many response packets\n"); + } else { + priv->resp_data[priv->resp_len++] = byte; + if (priv->resp_len == priv->expected_resp_len) { + priv->cmd_state = CMD_STATE_RESP_RECEIVED; + complete(&priv->cmd_done); + } + } + + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + break; + + case CHAN_CMD_ERROR: + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + if (!priv->cmd_running) { + dev_err(dev, "spurious cmd error packet\n"); + } else { + priv->resp_data[0] = byte; + priv->resp_len = 1; + priv->cmd_state = CMD_STATE_ERROR_RECEIVED; + complete(&priv->cmd_done); + } + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + break; + + case CHAN_KEYBOARD: + dev_warn(dev, "keyboard is not supported\n"); + break; + + case CHAN_TOUCHPAD: + dev_warn(dev, "touchpad is not supported\n"); + break; + + case CHAN_EVENT: + dev_dbg(dev, "got event %.2x\n", byte); + switch (byte) { + case EVENT_AC_CHANGE: + psy = power_supply_get_by_name("olpc_ac"); + if (psy) { + power_supply_changed(psy); + power_supply_put(psy); + } + break; + case EVENT_BATTERY_STATUS: + case EVENT_BATTERY_CRITICAL: + case EVENT_BATTERY_SOC_CHANGE: + case EVENT_BATTERY_ERROR: + psy = power_supply_get_by_name("olpc_battery"); + if (psy) { + power_supply_changed(psy); + power_supply_put(psy); + } + break; + case EVENT_POWER_PRESSED: + input_report_key(priv->pwrbtn, KEY_POWER, 1); + input_sync(priv->pwrbtn); + input_report_key(priv->pwrbtn, KEY_POWER, 0); + input_sync(priv->pwrbtn); + fallthrough; + case EVENT_POWER_PRESS_WAKE: + case EVENT_TIMED_HOST_WAKE: + pm_wakeup_event(priv->pwrbtn->dev.parent, + PM_WAKEUP_TIME); + break; + default: + dev_dbg(dev, "ignored unknown event %.2x\n", byte); + break; + } + break; + + case CHAN_DEBUG: + if (byte == '\n') { + olpc_xo175_ec_flush_logbuf(priv); + } else if (isprint(byte)) { + priv->logbuf[priv->logbuf_len++] = byte; + if (priv->logbuf_len == LOG_BUF_SIZE) + olpc_xo175_ec_flush_logbuf(priv); + } + break; + + default: + dev_warn(dev, "unknown channel: %d, %.2x\n", channel, byte); + break; + } + + /* Most non-command packets get the TxFIFO refilled and an ACK. */ + olpc_xo175_ec_read_packet(priv); +} + +/* + * This function is protected with a mutex. We can safely assume that + * there will be only one instance of this function running at a time. + * One of the ways in which we enforce this is by waiting until we get + * all response bytes back from the EC, rather than just the number that + * the caller requests (otherwise, we might start a new command while an + * old command's response bytes are still incoming). + */ +static int olpc_xo175_ec_cmd(u8 cmd, u8 *inbuf, size_t inlen, u8 *resp, + size_t resp_len, void *ec_cb_arg) +{ + struct olpc_xo175_ec *priv = ec_cb_arg; + struct device *dev = &priv->spi->dev; + unsigned long flags; + size_t nr_bytes; + int ret = 0; + + dev_dbg(dev, "CMD %x, %zd bytes expected\n", cmd, resp_len); + + if (inlen > 5) { + dev_err(dev, "command len %zd too big!\n", inlen); + return -EOVERFLOW; + } + + /* Suspending in the middle of an EC command hoses things badly! */ + if (WARN_ON(priv->suspended)) + return -EBUSY; + + /* Ensure a valid command and return bytes */ + ret = olpc_xo175_ec_resp_len(cmd); + if (ret < 0) { + dev_err_ratelimited(dev, "unknown command 0x%x\n", cmd); + + /* + * Assume the best in our callers, and allow unknown commands + * through. I'm not the charitable type, but it was beaten + * into me. Just maintain a minimum standard of sanity. + */ + if (resp_len > sizeof(priv->resp_data)) { + dev_err(dev, "response too big: %zd!\n", resp_len); + return -EOVERFLOW; + } + nr_bytes = resp_len; + } else { + nr_bytes = (size_t)ret; + ret = 0; + } + resp_len = min(resp_len, nr_bytes); + + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + /* Initialize the state machine */ + init_completion(&priv->cmd_done); + priv->cmd_running = true; + priv->cmd_state = CMD_STATE_WAITING_FOR_SWITCH; + memset(&priv->cmd, 0, sizeof(priv->cmd)); + priv->cmd.command = cmd; + priv->cmd.nr_args = inlen; + priv->cmd.data_len = 0; + memcpy(priv->cmd.args, inbuf, inlen); + priv->expected_resp_len = nr_bytes; + priv->resp_len = 0; + + /* Tickle the cmd gpio to get things started */ + gpiod_set_value_cansleep(priv->gpio_cmd, 1); + + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + + /* The irq handler should do the rest */ + if (!wait_for_completion_timeout(&priv->cmd_done, + msecs_to_jiffies(4000))) { + dev_err(dev, "EC cmd error: timeout in STATE %d\n", + priv->cmd_state); + gpiod_set_value_cansleep(priv->gpio_cmd, 0); + spi_target_abort(priv->spi); + olpc_xo175_ec_read_packet(priv); + return -ETIMEDOUT; + } + + spin_lock_irqsave(&priv->cmd_state_lock, flags); + + /* Deal with the results. */ + if (priv->cmd_state == CMD_STATE_ERROR_RECEIVED) { + /* EC-provided error is in the single response byte */ + dev_err(dev, "command 0x%x returned error 0x%x\n", + cmd, priv->resp_data[0]); + ret = -EREMOTEIO; + } else if (priv->resp_len != nr_bytes) { + dev_err(dev, "command 0x%x returned %d bytes, expected %zd bytes\n", + cmd, priv->resp_len, nr_bytes); + ret = -EREMOTEIO; + } else { + /* + * We may have 8 bytes in priv->resp, but we only care about + * what we've been asked for. If the caller asked for only 2 + * bytes, give them that. We've guaranteed that + * resp_len <= priv->resp_len and priv->resp_len == nr_bytes. + */ + memcpy(resp, priv->resp_data, resp_len); + } + + /* This should already be low, but just in case. */ + gpiod_set_value_cansleep(priv->gpio_cmd, 0); + priv->cmd_running = false; + + spin_unlock_irqrestore(&priv->cmd_state_lock, flags); + + return ret; +} + +static int olpc_xo175_ec_set_event_mask(unsigned int mask) +{ + u8 args[2]; + + args[0] = mask >> 0; + args[1] = mask >> 8; + return olpc_ec_cmd(CMD_WRITE_EXT_SCI_MASK, args, 2, NULL, 0); +} + +static void olpc_xo175_ec_power_off(void) +{ + while (1) { + olpc_ec_cmd(CMD_POWER_OFF, NULL, 0, NULL, 0); + mdelay(1000); + } +} + +static int __maybe_unused olpc_xo175_ec_suspend(struct device *dev) +{ + struct olpc_xo175_ec *priv = dev_get_drvdata(dev); + static struct { + u8 suspend; + u32 suspend_count; + } __packed hintargs; + static unsigned int suspend_count; + + /* + * SOC_SLEEP is not wired to the EC on B3 and earlier boards. + * This command lets the EC know instead. The suspend count doesn't seem + * to be used anywhere but in the EC debug output. + */ + hintargs.suspend = 1; + hintargs.suspend_count = suspend_count++; + olpc_ec_cmd(CMD_SUSPEND_HINT, (void *)&hintargs, sizeof(hintargs), + NULL, 0); + + /* + * After we've sent the suspend hint, don't allow further EC commands + * to be run until we've resumed. Userspace tasks should be frozen, + * but kernel threads and interrupts could still schedule EC commands. + */ + priv->suspended = true; + + return 0; +} + +static int __maybe_unused olpc_xo175_ec_resume_noirq(struct device *dev) +{ + struct olpc_xo175_ec *priv = dev_get_drvdata(dev); + + priv->suspended = false; + + return 0; +} + +static int __maybe_unused olpc_xo175_ec_resume(struct device *dev) +{ + u8 x = 0; + + /* + * The resume hint is only needed if no other commands are + * being sent during resume. all it does is tell the EC + * the SoC is definitely awake. + */ + olpc_ec_cmd(CMD_SUSPEND_HINT, &x, 1, NULL, 0); + + /* Enable all EC events while we're awake */ + olpc_xo175_ec_set_event_mask(EC_ALL_EVENTS); + + return 0; +} + +static struct olpc_ec_driver olpc_xo175_ec_driver = { + .ec_cmd = olpc_xo175_ec_cmd, +}; + +static void olpc_xo175_ec_remove(struct spi_device *spi) +{ + if (pm_power_off == olpc_xo175_ec_power_off) + pm_power_off = NULL; + + spi_target_abort(spi); + + platform_device_unregister(olpc_ec); + olpc_ec = NULL; +} + +static int olpc_xo175_ec_probe(struct spi_device *spi) +{ + struct olpc_xo175_ec *priv; + int ret; + + if (olpc_ec) { + dev_err(&spi->dev, "OLPC EC already registered.\n"); + return -EBUSY; + } + + priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->gpio_cmd = devm_gpiod_get(&spi->dev, "cmd", GPIOD_OUT_LOW); + if (IS_ERR(priv->gpio_cmd)) { + dev_err(&spi->dev, "failed to get cmd gpio: %ld\n", + PTR_ERR(priv->gpio_cmd)); + return PTR_ERR(priv->gpio_cmd); + } + + priv->spi = spi; + + spin_lock_init(&priv->cmd_state_lock); + priv->cmd_state = CMD_STATE_IDLE; + init_completion(&priv->cmd_done); + + priv->logbuf_len = 0; + + /* Set up power button input device */ + priv->pwrbtn = devm_input_allocate_device(&spi->dev); + if (!priv->pwrbtn) + return -ENOMEM; + priv->pwrbtn->name = "Power Button"; + priv->pwrbtn->dev.parent = &spi->dev; + input_set_capability(priv->pwrbtn, EV_KEY, KEY_POWER); + ret = input_register_device(priv->pwrbtn); + if (ret) { + dev_err(&spi->dev, "error registering input device: %d\n", ret); + return ret; + } + + spi_set_drvdata(spi, priv); + + priv->xfer.rx_buf = &priv->rx_buf; + priv->xfer.tx_buf = &priv->tx_buf; + + olpc_xo175_ec_read_packet(priv); + + olpc_ec_driver_register(&olpc_xo175_ec_driver, priv); + olpc_ec = platform_device_register_resndata(&spi->dev, "olpc-ec", -1, + NULL, 0, NULL, 0); + + /* Enable all EC events while we're awake */ + olpc_xo175_ec_set_event_mask(EC_ALL_EVENTS); + + if (pm_power_off == NULL) + pm_power_off = olpc_xo175_ec_power_off; + + dev_info(&spi->dev, "OLPC XO-1.75 Embedded Controller driver\n"); + + return 0; +} + +static const struct dev_pm_ops olpc_xo175_ec_pm_ops = { + SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(NULL, olpc_xo175_ec_resume_noirq) + SET_RUNTIME_PM_OPS(olpc_xo175_ec_suspend, olpc_xo175_ec_resume, NULL) +}; + +static const struct of_device_id olpc_xo175_ec_of_match[] = { + { .compatible = "olpc,xo1.75-ec" }, + { } +}; +MODULE_DEVICE_TABLE(of, olpc_xo175_ec_of_match); + +static const struct spi_device_id olpc_xo175_ec_id_table[] = { + { "xo1.75-ec", 0 }, + {} +}; +MODULE_DEVICE_TABLE(spi, olpc_xo175_ec_id_table); + +static struct spi_driver olpc_xo175_ec_spi_driver = { + .driver = { + .name = "olpc-xo175-ec", + .of_match_table = olpc_xo175_ec_of_match, + .pm = &olpc_xo175_ec_pm_ops, + }, + .id_table = olpc_xo175_ec_id_table, + .probe = olpc_xo175_ec_probe, + .remove = olpc_xo175_ec_remove, +}; +module_spi_driver(olpc_xo175_ec_spi_driver); + +MODULE_DESCRIPTION("OLPC XO-1.75 Embedded Controller driver"); +MODULE_AUTHOR("Lennert Buytenhek "); /* Functionality */ +MODULE_AUTHOR("Lubomir Rintel "); /* Bugs */ +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/raspberrypi/Kconfig b/drivers/platform/raspberrypi/Kconfig new file mode 100644 index 000000000..2c928440a --- /dev/null +++ b/drivers/platform/raspberrypi/Kconfig @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0 + +menuconfig BCM_VIDEOCORE + tristate "Broadcom VideoCore support" + depends on OF + depends on RASPBERRYPI_FIRMWARE || (COMPILE_TEST && !RASPBERRYPI_FIRMWARE) + default y + help + Support for Broadcom VideoCore services including + the BCM2835 family of products which is used + by the Raspberry PI. + +if BCM_VIDEOCORE + +config BCM2835_VCHIQ + tristate "BCM2835 VCHIQ" + depends on HAS_DMA + imply VCHIQ_CDEV + help + Broadcom BCM2835 and similar SoCs have a VPU called VideoCore. + This config enables the VCHIQ driver, which implements a + messaging interface between the kernel and the firmware running + on VideoCore. Other drivers use this interface to communicate to + the VPU. More specifically, the VCHIQ driver is used by + audio/video and camera drivers as well as for implementing MMAL + API, which is in turn used by several multimedia services on the + BCM2835 family of SoCs. + + Defaults to Y when the Broadcom Videocore services are included + in the build, N otherwise. + +if BCM2835_VCHIQ + +config VCHIQ_CDEV + bool "VCHIQ Character Driver" + help + Enable the creation of VCHIQ character driver. The cdev exposes + ioctls used by userspace libraries and testing tools to interact + with VideoCore, via the VCHIQ core driver (Check BCM2835_VCHIQ + for more info). + + This can be set to 'N' if the VideoCore communication is not + needed by userspace but only by other kernel modules + (like bcm2835-audio). + + If not sure, set this to 'Y'. + +endif + +source "drivers/platform/raspberrypi/vchiq-mmal/Kconfig" + +endif diff --git a/drivers/platform/raspberrypi/Makefile b/drivers/platform/raspberrypi/Makefile new file mode 100644 index 000000000..2a7c9511e --- /dev/null +++ b/drivers/platform/raspberrypi/Makefile @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0 + +obj-$(CONFIG_BCM2835_VCHIQ) += vchiq.o + +vchiq-objs := \ + vchiq-interface/vchiq_core.o \ + vchiq-interface/vchiq_arm.o \ + vchiq-interface/vchiq_bus.o \ + vchiq-interface/vchiq_debugfs.o \ + +ifdef CONFIG_VCHIQ_CDEV +vchiq-objs += vchiq-interface/vchiq_dev.o +endif + +obj-$(CONFIG_BCM2835_VCHIQ_MMAL) += vchiq-mmal/ diff --git a/drivers/platform/raspberrypi/vchiq-interface/TESTING b/drivers/platform/raspberrypi/vchiq-interface/TESTING new file mode 100644 index 000000000..c98f688b0 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/TESTING @@ -0,0 +1,125 @@ +This document contains some hints to test the function of the VCHIQ driver +without having additional hardware to the Raspberry Pi. + +* Requirements & limitations + +Testing the VCHIQ driver requires a Raspberry Pi with one of the following SoC: + - BCM2835 ( e.g. Raspberry Pi Zero W ) + - BCM2836 ( e.g. Raspberry Pi 2 ) + - BCM2837 ( e.g. Raspberry Pi 3 B+ ) + +The BCM2711 used in the Raspberry Pi 4 is currently not supported in the +mainline kernel. + +There are no specific requirements to the VideoCore firmware to get VCHIQ +working. + +The test scenarios described in this document based on the tool vchiq_test. +Its source code is available here: https://github.com/raspberrypi/userland + +* Configuration + +Here are the most common kernel configurations: + + 1. BCM2835 target SoC (ARM 32 bit) + + Just use bcm2835_defconfig which already has VCHIQ enabled. + + 2. BCM2836/7 target SoC (ARM 32 bit) + + Use the multi_v7_defconfig as a base and then enable all VCHIQ options. + + 3. BCM2837 target SoC (ARM 64 bit) + + Use the defconfig which has most of the VCHIQ options enabled. + +* Scenarios + + * Initial test + + Check the driver is probed and /dev/vchiq is created + + * Functional test + + Command: vchiq_test -f 10 + + Expected output: + Functional test - iters:10 + ======== iteration 1 ======== + Testing bulk transfer for alignment. + Testing bulk transfer at PAGE_SIZE. + ... + + * Ping test + + Command: vchiq_test -p + + Expected output: + Ping test - service:echo, iters:1000, version 3 + vchi ping (size 0) -> 57.000000us + vchi ping (size 0, 0 async, 0 oneway) -> 122.000000us + vchi bulk (size 0, 0 async, 0 oneway) -> 546.000000us + vchi bulk (size 0, 0 oneway) -> 230.000000us + vchi ping (size 0) -> 49.000000us + vchi ping (size 0, 0 async, 0 oneway) -> 70.000000us + vchi bulk (size 0, 0 async, 0 oneway) -> 296.000000us + vchi bulk (size 0, 0 oneway) -> 266.000000us + vchi ping (size 0, 1 async, 0 oneway) -> 65.000000us + vchi bulk (size 0, 0 oneway) -> 456.000000us + vchi ping (size 0, 2 async, 0 oneway) -> 74.000000us + vchi bulk (size 0, 0 oneway) -> 640.000000us + vchi ping (size 0, 10 async, 0 oneway) -> 125.000000us + vchi bulk (size 0, 0 oneway) -> 2309.000000us + vchi ping (size 0, 0 async, 1 oneway) -> 70.000000us + vchi ping (size 0, 0 async, 2 oneway) -> 76.000000us + vchi ping (size 0, 0 async, 10 oneway) -> 105.000000us + vchi ping (size 0, 10 async, 10 oneway) -> 165.000000us + vchi ping (size 0, 100 async, 0 oneway) -> nanus + vchi bulk (size 0, 0 oneway) -> nanus + vchi ping (size 0, 0 async, 100 oneway) -> nanus + vchi ping (size 0, 100 async, 100 oneway) -> infus + vchi ping (size 0, 200 async, 0 oneway) -> infus + ... + + * Debugfs test + + Command: cat /sys/kernel/debug/vchiq/state + + Example output: + State 0: CONNECTED + tx_pos=0x1e8(@43b0acda), rx_pos=0x170(@05493af8) + Version: 8 (min 3) + Stats: ctrl_tx_count=7, ctrl_rx_count=7, error_count=0 + Slots: 30 available (29 data), 0 recyclable, 0 stalls (0 data) + Platform: 2835 (VC master) + Local: slots 34-64 tx_pos=0x1e8 recycle=0x1f + Slots claimed: + DEBUG: SLOT_HANDLER_COUNT = 20(0x14) + DEBUG: SLOT_HANDLER_LINE = 1937(0x791) + DEBUG: PARSE_LINE = 1864(0x748) + DEBUG: PARSE_HEADER = -249155224(0xf1263168) + DEBUG: PARSE_MSGID = 67362817(0x403e001) + DEBUG: AWAIT_COMPLETION_LINE = 0(0x0) + DEBUG: DEQUEUE_MESSAGE_LINE = 0(0x0) + DEBUG: SERVICE_CALLBACK_LINE = 0(0x0) + DEBUG: MSG_QUEUE_FULL_COUNT = 0(0x0) + DEBUG: COMPLETION_QUEUE_FULL_COUNT = 0(0x0) + Remote: slots 2-32 tx_pos=0x170 recycle=0x1f + Slots claimed: + 2: 10/9 + DEBUG: SLOT_HANDLER_COUNT = 20(0x14) + DEBUG: SLOT_HANDLER_LINE = 1851(0x73b) + DEBUG: PARSE_LINE = 1827(0x723) + DEBUG: PARSE_HEADER = -150330912(0xf70a21e0) + DEBUG: PARSE_MSGID = 67113022(0x400103e) + DEBUG: AWAIT_COMPLETION_LINE = 0(0x0) + DEBUG: DEQUEUE_MESSAGE_LINE = 0(0x0) + DEBUG: SERVICE_CALLBACK_LINE = 0(0x0) + DEBUG: MSG_QUEUE_FULL_COUNT = 0(0x0) + DEBUG: COMPLETION_QUEUE_FULL_COUNT = 0(0x0) + Service 0: LISTENING (ref 1) 'PEEK little-endian (0x4b454550)' remote n/a (msg use 0/3840, slot use 0/15) + Bulk: tx_pending=0 (size 0), rx_pending=0 (size 0) + Ctrl: tx_count=0, tx_bytes=0, rx_count=0, rx_bytes=0 + Bulk: tx_count=0, tx_bytes=0, rx_count=0, rx_bytes=0 + 0 quota stalls, 0 slot stalls, 0 bulk stalls, 0 aborted, 0 errors + instance b511f60b diff --git a/drivers/platform/raspberrypi/vchiq-interface/TODO b/drivers/platform/raspberrypi/vchiq-interface/TODO new file mode 100644 index 000000000..2357dae41 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/TODO @@ -0,0 +1,4 @@ +* Documentation + +A short top-down description of this driver's architecture (function of +kthreads, userspace, limitations) could be very helpful for reviewers. diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_arm.c b/drivers/platform/raspberrypi/vchiq-interface/vchiq_arm.c new file mode 100644 index 000000000..e78641703 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_arm.c @@ -0,0 +1,1476 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +/* + * Copyright (c) 2014 Raspberry Pi (Trading) Ltd. All rights reserved. + * Copyright (c) 2010-2012 Broadcom. All rights reserved. + */ + +#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 "vchiq_ioctl.h" + +#define DEVICE_NAME "vchiq" + +#define TOTAL_SLOTS (VCHIQ_SLOT_ZERO_SLOTS + 2 * 32) + +#define MAX_FRAGMENTS (VCHIQ_NUM_CURRENT_BULKS * 2) + +#define VCHIQ_PLATFORM_FRAGMENTS_OFFSET_IDX 0 +#define VCHIQ_PLATFORM_FRAGMENTS_COUNT_IDX 1 + +#define BELL0 0x00 + +#define ARM_DS_ACTIVE BIT(2) + +/* Override the default prefix, which would be vchiq_arm (from the filename) */ +#undef MODULE_PARAM_PREFIX +#define MODULE_PARAM_PREFIX DEVICE_NAME "." + +#define KEEPALIVE_VER 1 +#define KEEPALIVE_VER_MIN KEEPALIVE_VER + +/* + * The devices implemented in the VCHIQ firmware are not discoverable, + * so we need to maintain a list of them in order to register them with + * the interface. + */ +static struct vchiq_device *bcm2835_audio; + +static const struct vchiq_platform_info bcm2835_info = { + .cache_line_size = 32, +}; + +static const struct vchiq_platform_info bcm2836_info = { + .cache_line_size = 64, +}; + +struct vchiq_arm_state { + /* + * Keepalive-related data + * + * The keepalive mechanism was retro-fitted to VCHIQ to allow active + * services to prevent the system from suspending. + * This feature is not used on Raspberry Pi devices. + */ + struct task_struct *ka_thread; + struct completion ka_evt; + atomic_t ka_use_count; + atomic_t ka_use_ack_count; + atomic_t ka_release_count; + + rwlock_t susp_res_lock; + + struct vchiq_state *state; + + /* + * Global use count for videocore. + * This is equal to the sum of the use counts for all services. When + * this hits zero the videocore suspend procedure will be initiated. + */ + int videocore_use_count; + + /* + * Use count to track requests from videocore peer. + * This use count is not associated with a service, so needs to be + * tracked separately with the state. + */ + int peer_use_count; + + /* + * Flag to indicate that the first vchiq connect has made it through. + * This means that both sides should be fully ready, and we should + * be able to suspend after this point. + */ + int first_connect; +}; + +static int +vchiq_blocking_bulk_transfer(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_bulk *bulk_params); + +static irqreturn_t +vchiq_doorbell_irq(int irq, void *dev_id) +{ + struct vchiq_state *state = dev_id; + struct vchiq_drv_mgmt *mgmt; + irqreturn_t ret = IRQ_NONE; + unsigned int status; + + mgmt = dev_get_drvdata(state->dev); + + /* Read (and clear) the doorbell */ + status = readl(mgmt->regs + BELL0); + + if (status & ARM_DS_ACTIVE) { /* Was the doorbell rung? */ + remote_event_pollall(state); + ret = IRQ_HANDLED; + } + + return ret; +} + +/* + * This function is called by the vchiq stack once it has been connected to + * the videocore and clients can start to use the stack. + */ +static void vchiq_call_connected_callbacks(struct vchiq_drv_mgmt *drv_mgmt) +{ + int i; + + if (mutex_lock_killable(&drv_mgmt->connected_mutex)) + return; + + for (i = 0; i < drv_mgmt->num_deferred_callbacks; i++) + drv_mgmt->deferred_callback[i](); + + drv_mgmt->num_deferred_callbacks = 0; + drv_mgmt->connected = true; + mutex_unlock(&drv_mgmt->connected_mutex); +} + +/* + * This function is used to defer initialization until the vchiq stack is + * initialized. If the stack is already initialized, then the callback will + * be made immediately, otherwise it will be deferred until + * vchiq_call_connected_callbacks is called. + */ +void vchiq_add_connected_callback(struct vchiq_device *device, void (*callback)(void)) +{ + struct vchiq_drv_mgmt *drv_mgmt = device->drv_mgmt; + + if (mutex_lock_killable(&drv_mgmt->connected_mutex)) + return; + + if (drv_mgmt->connected) { + /* We're already connected. Call the callback immediately. */ + callback(); + } else { + if (drv_mgmt->num_deferred_callbacks >= VCHIQ_DRV_MAX_CALLBACKS) { + dev_err(&device->dev, + "core: deferred callbacks(%d) exceeded the maximum limit(%d)\n", + drv_mgmt->num_deferred_callbacks, VCHIQ_DRV_MAX_CALLBACKS); + } else { + drv_mgmt->deferred_callback[drv_mgmt->num_deferred_callbacks] = + callback; + drv_mgmt->num_deferred_callbacks++; + } + } + mutex_unlock(&drv_mgmt->connected_mutex); +} +EXPORT_SYMBOL(vchiq_add_connected_callback); + +static int vchiq_platform_init(struct platform_device *pdev, struct vchiq_state *state) +{ + struct device *dev = &pdev->dev; + struct vchiq_drv_mgmt *drv_mgmt = platform_get_drvdata(pdev); + struct rpi_firmware *fw = drv_mgmt->fw; + struct vchiq_slot_zero *vchiq_slot_zero; + void *slot_mem; + dma_addr_t slot_phys; + u32 channelbase; + int slot_mem_size, frag_mem_size; + int err, irq, i; + + /* + * VCHI messages between the CPU and firmware use + * 32-bit bus addresses. + */ + err = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)); + + if (err < 0) + return err; + + drv_mgmt->fragments_size = 2 * drv_mgmt->info->cache_line_size; + + /* Allocate space for the channels in coherent memory */ + slot_mem_size = PAGE_ALIGN(TOTAL_SLOTS * VCHIQ_SLOT_SIZE); + frag_mem_size = PAGE_ALIGN(drv_mgmt->fragments_size * MAX_FRAGMENTS); + + slot_mem = dmam_alloc_coherent(dev, slot_mem_size + frag_mem_size, + &slot_phys, GFP_KERNEL); + if (!slot_mem) { + dev_err(dev, "could not allocate DMA memory\n"); + return -ENOMEM; + } + + WARN_ON(((unsigned long)slot_mem & (PAGE_SIZE - 1)) != 0); + + vchiq_slot_zero = vchiq_init_slots(dev, slot_mem, slot_mem_size); + if (!vchiq_slot_zero) + return -ENOMEM; + + vchiq_slot_zero->platform_data[VCHIQ_PLATFORM_FRAGMENTS_OFFSET_IDX] = + (int)slot_phys + slot_mem_size; + vchiq_slot_zero->platform_data[VCHIQ_PLATFORM_FRAGMENTS_COUNT_IDX] = + MAX_FRAGMENTS; + + drv_mgmt->fragments_base = (char *)slot_mem + slot_mem_size; + + drv_mgmt->free_fragments = drv_mgmt->fragments_base; + for (i = 0; i < (MAX_FRAGMENTS - 1); i++) { + *(char **)&drv_mgmt->fragments_base[i * drv_mgmt->fragments_size] = + &drv_mgmt->fragments_base[(i + 1) * drv_mgmt->fragments_size]; + } + *(char **)&drv_mgmt->fragments_base[i * drv_mgmt->fragments_size] = NULL; + sema_init(&drv_mgmt->free_fragments_sema, MAX_FRAGMENTS); + sema_init(&drv_mgmt->free_fragments_mutex, 1); + + err = vchiq_init_state(state, vchiq_slot_zero, dev); + if (err) + return err; + + drv_mgmt->regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(drv_mgmt->regs)) + return PTR_ERR(drv_mgmt->regs); + + irq = platform_get_irq(pdev, 0); + if (irq <= 0) + return irq; + + err = devm_request_irq(dev, irq, vchiq_doorbell_irq, IRQF_IRQPOLL, + "VCHIQ doorbell", state); + if (err) { + dev_err(dev, "failed to register irq=%d\n", irq); + return err; + } + + /* Send the base address of the slots to VideoCore */ + channelbase = slot_phys; + err = rpi_firmware_property(fw, RPI_FIRMWARE_VCHIQ_INIT, + &channelbase, sizeof(channelbase)); + if (err) { + dev_err(dev, "failed to send firmware property: %d\n", err); + return err; + } + + if (channelbase) { + dev_err(dev, "failed to set channelbase (response: %x)\n", + channelbase); + return -ENXIO; + } + + dev_dbg(&pdev->dev, "arm: vchiq_init - done (slots %p, phys %pad)\n", + vchiq_slot_zero, &slot_phys); + + mutex_init(&drv_mgmt->connected_mutex); + vchiq_call_connected_callbacks(drv_mgmt); + + return 0; +} + +int +vchiq_platform_init_state(struct vchiq_state *state) +{ + struct vchiq_arm_state *platform_state; + + platform_state = devm_kzalloc(state->dev, sizeof(*platform_state), GFP_KERNEL); + if (!platform_state) + return -ENOMEM; + + rwlock_init(&platform_state->susp_res_lock); + + init_completion(&platform_state->ka_evt); + atomic_set(&platform_state->ka_use_count, 0); + atomic_set(&platform_state->ka_use_ack_count, 0); + atomic_set(&platform_state->ka_release_count, 0); + + platform_state->state = state; + + state->platform_state = (struct opaque_platform_state *)platform_state; + + return 0; +} + +static struct vchiq_arm_state *vchiq_platform_get_arm_state(struct vchiq_state *state) +{ + return (struct vchiq_arm_state *)state->platform_state; +} + +static void +vchiq_platform_uninit(struct vchiq_drv_mgmt *mgmt) +{ + struct vchiq_arm_state *arm_state; + + kthread_stop(mgmt->state.sync_thread); + kthread_stop(mgmt->state.recycle_thread); + kthread_stop(mgmt->state.slot_handler_thread); + + arm_state = vchiq_platform_get_arm_state(&mgmt->state); + if (!IS_ERR_OR_NULL(arm_state->ka_thread)) + kthread_stop(arm_state->ka_thread); +} + +void vchiq_dump_platform_state(struct seq_file *f) +{ + seq_puts(f, " Platform: 2835 (VC master)\n"); +} + +#define VCHIQ_INIT_RETRIES 10 +int vchiq_initialise(struct vchiq_state *state, struct vchiq_instance **instance_out) +{ + struct vchiq_instance *instance = NULL; + int i, ret; + + /* + * VideoCore may not be ready due to boot up timing. + * It may never be ready if kernel and firmware are mismatched,so don't + * block forever. + */ + for (i = 0; i < VCHIQ_INIT_RETRIES; i++) { + if (vchiq_remote_initialised(state)) + break; + usleep_range(500, 600); + } + if (i == VCHIQ_INIT_RETRIES) { + dev_err(state->dev, "core: %s: Videocore not initialized\n", __func__); + ret = -ENOTCONN; + goto failed; + } else if (i > 0) { + dev_warn(state->dev, "core: %s: videocore initialized after %d retries\n", + __func__, i); + } + + instance = kzalloc_obj(*instance); + if (!instance) { + ret = -ENOMEM; + goto failed; + } + + instance->connected = 0; + instance->state = state; + mutex_init(&instance->bulk_waiter_list_mutex); + INIT_LIST_HEAD(&instance->bulk_waiter_list); + + *instance_out = instance; + + ret = 0; + +failed: + dev_dbg(state->dev, "core: (%p): returning %d\n", instance, ret); + + return ret; +} +EXPORT_SYMBOL(vchiq_initialise); + +void free_bulk_waiter(struct vchiq_instance *instance) +{ + struct bulk_waiter_node *waiter, *next; + + list_for_each_entry_safe(waiter, next, + &instance->bulk_waiter_list, list) { + list_del(&waiter->list); + dev_dbg(instance->state->dev, + "arm: bulk_waiter - cleaned up %p for pid %d\n", + waiter, waiter->pid); + kfree(waiter); + } +} + +int vchiq_shutdown(struct vchiq_instance *instance) +{ + struct vchiq_state *state = instance->state; + int ret = 0; + + mutex_lock(&state->mutex); + + /* Remove all services */ + vchiq_shutdown_internal(state, instance); + + mutex_unlock(&state->mutex); + + dev_dbg(state->dev, "core: (%p): returning %d\n", instance, ret); + + free_bulk_waiter(instance); + kfree(instance); + + return ret; +} +EXPORT_SYMBOL(vchiq_shutdown); + +static int vchiq_is_connected(struct vchiq_instance *instance) +{ + return instance->connected; +} + +int vchiq_connect(struct vchiq_instance *instance) +{ + struct vchiq_state *state = instance->state; + int ret; + + if (mutex_lock_killable(&state->mutex)) { + dev_dbg(state->dev, + "core: call to mutex_lock failed\n"); + ret = -EAGAIN; + goto failed; + } + ret = vchiq_connect_internal(state, instance); + + if (!ret) + instance->connected = 1; + + mutex_unlock(&state->mutex); + +failed: + dev_dbg(state->dev, "core: (%p): returning %d\n", instance, ret); + + return ret; +} +EXPORT_SYMBOL(vchiq_connect); + +static int +vchiq_add_service(struct vchiq_instance *instance, + const struct vchiq_service_params_kernel *params, + unsigned int *phandle) +{ + struct vchiq_state *state = instance->state; + struct vchiq_service *service = NULL; + int srvstate, ret; + + *phandle = VCHIQ_SERVICE_HANDLE_INVALID; + + srvstate = vchiq_is_connected(instance) + ? VCHIQ_SRVSTATE_LISTENING + : VCHIQ_SRVSTATE_HIDDEN; + + service = vchiq_add_service_internal(state, params, srvstate, instance, NULL); + + if (service) { + *phandle = service->handle; + ret = 0; + } else { + ret = -EINVAL; + } + + dev_dbg(state->dev, "core: (%p): returning %d\n", instance, ret); + + return ret; +} + +int +vchiq_open_service(struct vchiq_instance *instance, + const struct vchiq_service_params_kernel *params, + unsigned int *phandle) +{ + struct vchiq_state *state = instance->state; + struct vchiq_service *service = NULL; + int ret = -EINVAL; + + *phandle = VCHIQ_SERVICE_HANDLE_INVALID; + + if (!vchiq_is_connected(instance)) + goto failed; + + service = vchiq_add_service_internal(state, params, VCHIQ_SRVSTATE_OPENING, instance, NULL); + + if (service) { + *phandle = service->handle; + ret = vchiq_open_service_internal(service, current->pid); + if (ret) { + vchiq_remove_service(instance, service->handle); + *phandle = VCHIQ_SERVICE_HANDLE_INVALID; + } + } + +failed: + dev_dbg(state->dev, "core: (%p): returning %d\n", instance, ret); + + return ret; +} +EXPORT_SYMBOL(vchiq_open_service); + +int +vchiq_bulk_transmit(struct vchiq_instance *instance, unsigned int handle, const void *data, + unsigned int size, void *userdata, enum vchiq_bulk_mode mode) +{ + struct vchiq_bulk bulk_params = {}; + int ret; + + switch (mode) { + case VCHIQ_BULK_MODE_NOCALLBACK: + case VCHIQ_BULK_MODE_CALLBACK: + + bulk_params.offset = (void *)data; + bulk_params.mode = mode; + bulk_params.size = size; + bulk_params.cb_data = userdata; + bulk_params.dir = VCHIQ_BULK_TRANSMIT; + + ret = vchiq_bulk_xfer_callback(instance, handle, &bulk_params); + break; + case VCHIQ_BULK_MODE_BLOCKING: + bulk_params.offset = (void *)data; + bulk_params.mode = mode; + bulk_params.size = size; + bulk_params.dir = VCHIQ_BULK_TRANSMIT; + + ret = vchiq_blocking_bulk_transfer(instance, handle, &bulk_params); + break; + default: + return -EINVAL; + } + + return ret; +} +EXPORT_SYMBOL(vchiq_bulk_transmit); + +int vchiq_bulk_receive(struct vchiq_instance *instance, unsigned int handle, + void *data, unsigned int size, void *userdata, + enum vchiq_bulk_mode mode) +{ + struct vchiq_bulk bulk_params = {}; + int ret; + + switch (mode) { + case VCHIQ_BULK_MODE_NOCALLBACK: + case VCHIQ_BULK_MODE_CALLBACK: + + bulk_params.offset = (void *)data; + bulk_params.mode = mode; + bulk_params.size = size; + bulk_params.cb_data = userdata; + bulk_params.dir = VCHIQ_BULK_RECEIVE; + + ret = vchiq_bulk_xfer_callback(instance, handle, &bulk_params); + break; + case VCHIQ_BULK_MODE_BLOCKING: + bulk_params.offset = (void *)data; + bulk_params.mode = mode; + bulk_params.size = size; + bulk_params.dir = VCHIQ_BULK_RECEIVE; + + ret = vchiq_blocking_bulk_transfer(instance, handle, &bulk_params); + break; + default: + return -EINVAL; + } + + return ret; +} +EXPORT_SYMBOL(vchiq_bulk_receive); + +static int +vchiq_blocking_bulk_transfer(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_bulk *bulk_params) +{ + struct vchiq_service *service; + struct bulk_waiter_node *waiter = NULL, *iter; + int ret; + + service = find_service_by_handle(instance, handle); + if (!service) + return -EINVAL; + + vchiq_service_put(service); + + mutex_lock(&instance->bulk_waiter_list_mutex); + list_for_each_entry(iter, &instance->bulk_waiter_list, list) { + if (iter->pid == current->pid) { + list_del(&iter->list); + waiter = iter; + break; + } + } + mutex_unlock(&instance->bulk_waiter_list_mutex); + + if (waiter) { + struct vchiq_bulk *bulk = waiter->bulk_waiter.bulk; + + if (bulk) { + /* This thread has an outstanding bulk transfer. */ + /* FIXME: why compare a dma address to a pointer? */ + if ((bulk->dma_addr != (dma_addr_t)(uintptr_t)bulk_params->dma_addr) || + (bulk->size != bulk_params->size)) { + /* + * This is not a retry of the previous one. + * Cancel the signal when the transfer completes. + */ + spin_lock(&service->state->bulk_waiter_spinlock); + bulk->waiter = NULL; + spin_unlock(&service->state->bulk_waiter_spinlock); + } + } + } else { + waiter = kzalloc_obj(*waiter); + if (!waiter) + return -ENOMEM; + } + + bulk_params->waiter = &waiter->bulk_waiter; + + ret = vchiq_bulk_xfer_blocking(instance, handle, bulk_params); + if ((ret != -EAGAIN) || fatal_signal_pending(current) || !waiter->bulk_waiter.bulk) { + struct vchiq_bulk *bulk = waiter->bulk_waiter.bulk; + + if (bulk) { + /* Cancel the signal when the transfer completes. */ + spin_lock(&service->state->bulk_waiter_spinlock); + bulk->waiter = NULL; + spin_unlock(&service->state->bulk_waiter_spinlock); + } + kfree(waiter); + } else { + waiter->pid = current->pid; + mutex_lock(&instance->bulk_waiter_list_mutex); + list_add(&waiter->list, &instance->bulk_waiter_list); + mutex_unlock(&instance->bulk_waiter_list_mutex); + dev_dbg(instance->state->dev, "arm: saved bulk_waiter %p for pid %d\n", + waiter, current->pid); + } + + return ret; +} + +static int +add_completion(struct vchiq_instance *instance, enum vchiq_reason reason, + struct vchiq_header *header, struct user_service *user_service, + void *cb_data, void __user *cb_userdata) +{ + struct vchiq_completion_data_kernel *completion; + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(instance->state->dev); + int insert; + + DEBUG_INITIALISE(mgmt->state.local); + + insert = instance->completion_insert; + while ((insert - instance->completion_remove) >= MAX_COMPLETIONS) { + /* Out of space - wait for the client */ + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + dev_dbg(instance->state->dev, "core: completion queue full\n"); + DEBUG_COUNT(COMPLETION_QUEUE_FULL_COUNT); + if (wait_for_completion_interruptible(&instance->remove_event)) { + dev_dbg(instance->state->dev, "arm: service_callback interrupted\n"); + return -EAGAIN; + } else if (instance->closing) { + dev_dbg(instance->state->dev, "arm: service_callback closing\n"); + return 0; + } + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + } + + completion = &instance->completions[insert & (MAX_COMPLETIONS - 1)]; + + completion->header = header; + completion->reason = reason; + /* N.B. service_userdata is updated while processing AWAIT_COMPLETION */ + completion->service_userdata = user_service->service; + completion->cb_data = cb_data; + completion->cb_userdata = cb_userdata; + + if (reason == VCHIQ_SERVICE_CLOSED) { + /* + * Take an extra reference, to be held until + * this CLOSED notification is delivered. + */ + vchiq_service_get(user_service->service); + if (instance->use_close_delivered) + user_service->close_pending = 1; + } + + /* + * A write barrier is needed here to ensure that the entire completion + * record is written out before the insert point. + */ + wmb(); + + if (reason == VCHIQ_MESSAGE_AVAILABLE) + user_service->message_available_pos = insert; + + insert++; + instance->completion_insert = insert; + + complete(&instance->insert_event); + + return 0; +} + +static int +service_single_message(struct vchiq_instance *instance, + enum vchiq_reason reason, struct vchiq_service *service, + void *cb_data, void __user *cb_userdata) +{ + struct user_service *user_service; + + user_service = (struct user_service *)service->base.userdata; + + dev_dbg(service->state->dev, "arm: msg queue full\n"); + /* + * If there is no MESSAGE_AVAILABLE in the completion + * queue, add one + */ + if ((user_service->message_available_pos - + instance->completion_remove) < 0) { + int ret; + + dev_dbg(instance->state->dev, + "arm: Inserting extra MESSAGE_AVAILABLE\n"); + ret = add_completion(instance, reason, NULL, user_service, + cb_data, cb_userdata); + if (ret) + return ret; + } + + if (wait_for_completion_interruptible(&user_service->remove_event)) { + dev_dbg(instance->state->dev, "arm: interrupted\n"); + return -EAGAIN; + } else if (instance->closing) { + dev_dbg(instance->state->dev, "arm: closing\n"); + return -EINVAL; + } + + return 0; +} + +int +service_callback(struct vchiq_instance *instance, enum vchiq_reason reason, + struct vchiq_header *header, unsigned int handle, + void *cb_data, void __user *cb_userdata) +{ + /* + * How do we ensure the callback goes to the right client? + * The service_user data points to a user_service record + * containing the original callback and the user state structure, which + * contains a circular buffer for completion records. + */ + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(instance->state->dev); + struct user_service *user_service; + struct vchiq_service *service; + bool skip_completion = false; + + DEBUG_INITIALISE(mgmt->state.local); + + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + + rcu_read_lock(); + service = handle_to_service(instance, handle); + if (WARN_ON(!service)) { + rcu_read_unlock(); + return 0; + } + + user_service = (struct user_service *)service->base.userdata; + + if (instance->closing) { + rcu_read_unlock(); + return 0; + } + + /* + * As hopping around different synchronization mechanism, + * taking an extra reference results in simpler implementation. + */ + vchiq_service_get(service); + rcu_read_unlock(); + + dev_dbg(service->state->dev, + "arm: service %p(%d,%p), reason %d, header %p, instance %p, cb_data %p, cb_userdata %p\n", + user_service, service->localport, user_service->userdata, + reason, header, instance, cb_data, cb_userdata); + + if (header && user_service->is_vchi) { + spin_lock(&service->state->msg_queue_spinlock); + while (user_service->msg_insert == + (user_service->msg_remove + MSG_QUEUE_SIZE)) { + int ret; + + spin_unlock(&service->state->msg_queue_spinlock); + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + DEBUG_COUNT(MSG_QUEUE_FULL_COUNT); + + ret = service_single_message(instance, reason, service, + cb_data, cb_userdata); + if (ret) { + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + vchiq_service_put(service); + return ret; + } + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + spin_lock(&service->state->msg_queue_spinlock); + } + + user_service->msg_queue[user_service->msg_insert & + (MSG_QUEUE_SIZE - 1)] = header; + user_service->msg_insert++; + + /* + * If there is a thread waiting in DEQUEUE_MESSAGE, or if + * there is a MESSAGE_AVAILABLE in the completion queue then + * bypass the completion queue. + */ + if (((user_service->message_available_pos - + instance->completion_remove) >= 0) || + user_service->dequeue_pending) { + user_service->dequeue_pending = 0; + skip_completion = true; + } + + spin_unlock(&service->state->msg_queue_spinlock); + complete(&user_service->insert_event); + + header = NULL; + } + DEBUG_TRACE(SERVICE_CALLBACK_LINE); + vchiq_service_put(service); + + if (skip_completion) + return 0; + + return add_completion(instance, reason, header, user_service, + cb_data, cb_userdata); +} + +void vchiq_dump_platform_instances(struct vchiq_state *state, struct seq_file *f) +{ + int i; + + if (!vchiq_remote_initialised(state)) + return; + + /* + * There is no list of instances, so instead scan all services, + * marking those that have been dumped. + */ + + rcu_read_lock(); + for (i = 0; i < state->unused_service; i++) { + struct vchiq_service *service; + struct vchiq_instance *instance; + + service = rcu_dereference(state->services[i]); + if (!service || service->base.callback != service_callback) + continue; + + instance = service->instance; + if (instance) + instance->mark = 0; + } + rcu_read_unlock(); + + for (i = 0; i < state->unused_service; i++) { + struct vchiq_service *service; + struct vchiq_instance *instance; + + rcu_read_lock(); + service = rcu_dereference(state->services[i]); + if (!service || service->base.callback != service_callback) { + rcu_read_unlock(); + continue; + } + + instance = service->instance; + if (!instance || instance->mark) { + rcu_read_unlock(); + continue; + } + rcu_read_unlock(); + + seq_printf(f, "Instance %pK: pid %d,%s completions %d/%d\n", + instance, instance->pid, + instance->connected ? " connected, " : + "", + instance->completion_insert - + instance->completion_remove, + MAX_COMPLETIONS); + instance->mark = 1; + } +} + +void vchiq_dump_platform_service_state(struct seq_file *f, + struct vchiq_service *service) +{ + struct user_service *user_service = + (struct user_service *)service->base.userdata; + + seq_printf(f, " instance %pK", service->instance); + + if ((service->base.callback == service_callback) && user_service->is_vchi) { + seq_printf(f, ", %d/%d messages", + user_service->msg_insert - user_service->msg_remove, + MSG_QUEUE_SIZE); + + if (user_service->dequeue_pending) + seq_puts(f, " (dequeue pending)"); + } + + seq_puts(f, "\n"); +} + +/* + * Autosuspend related functionality + */ + +static int +vchiq_keepalive_vchiq_callback(struct vchiq_instance *instance, + enum vchiq_reason reason, + struct vchiq_header *header, + unsigned int service_user, + void *cb_data, void __user *cb_userdata) +{ + dev_err(instance->state->dev, "suspend: %s: callback reason %d\n", + __func__, reason); + return 0; +} + +static int +vchiq_keepalive_thread_func(void *v) +{ + struct vchiq_state *state = (struct vchiq_state *)v; + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + struct vchiq_instance *instance; + unsigned int ka_handle; + int ret; + + struct vchiq_service_params_kernel params = { + .fourcc = VCHIQ_MAKE_FOURCC('K', 'E', 'E', 'P'), + .callback = vchiq_keepalive_vchiq_callback, + .version = KEEPALIVE_VER, + .version_min = KEEPALIVE_VER_MIN + }; + + ret = vchiq_initialise(state, &instance); + if (ret) { + dev_err(state->dev, "suspend: %s: vchiq_initialise failed %d\n", __func__, ret); + goto exit; + } + + ret = vchiq_connect(instance); + if (ret) { + dev_err(state->dev, "suspend: %s: vchiq_connect failed %d\n", __func__, ret); + goto shutdown; + } + + ret = vchiq_add_service(instance, ¶ms, &ka_handle); + if (ret) { + dev_err(state->dev, "suspend: %s: vchiq_open_service failed %d\n", + __func__, ret); + goto shutdown; + } + + while (!kthread_should_stop()) { + long rc = 0, uc = 0; + + if (wait_for_completion_interruptible(&arm_state->ka_evt)) { + dev_dbg(state->dev, "suspend: %s: interrupted\n", __func__); + flush_signals(current); + continue; + } + + /* + * read and clear counters. Do release_count then use_count to + * prevent getting more releases than uses + */ + rc = atomic_xchg(&arm_state->ka_release_count, 0); + uc = atomic_xchg(&arm_state->ka_use_count, 0); + + /* + * Call use/release service the requisite number of times. + * Process use before release so use counts don't go negative + */ + while (uc--) { + atomic_inc(&arm_state->ka_use_ack_count); + ret = vchiq_use_service(instance, ka_handle); + if (ret) { + dev_err(state->dev, "suspend: %s: vchiq_use_service error %d\n", + __func__, ret); + } + } + while (rc--) { + ret = vchiq_release_service(instance, ka_handle); + if (ret) { + dev_err(state->dev, "suspend: %s: vchiq_release_service error %d\n", + __func__, ret); + } + } + } + +shutdown: + vchiq_shutdown(instance); +exit: + return 0; +} + +int +vchiq_use_internal(struct vchiq_state *state, struct vchiq_service *service, + enum USE_TYPE_E use_type) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + int ret = 0; + char entity[64]; + int *entity_uc; + int local_uc; + + if (!arm_state) { + ret = -EINVAL; + goto out; + } + + if (use_type == USE_TYPE_VCHIQ) { + snprintf(entity, sizeof(entity), "VCHIQ: "); + entity_uc = &arm_state->peer_use_count; + } else if (service) { + snprintf(entity, sizeof(entity), "%p4cc:%03d", + &service->base.fourcc, + service->client_id); + entity_uc = &service->service_use_count; + } else { + dev_err(state->dev, "suspend: %s: null service ptr\n", __func__); + ret = -EINVAL; + goto out; + } + + write_lock_bh(&arm_state->susp_res_lock); + local_uc = ++arm_state->videocore_use_count; + ++(*entity_uc); + + dev_dbg(state->dev, "suspend: %s count %d, state count %d\n", + entity, *entity_uc, local_uc); + + write_unlock_bh(&arm_state->susp_res_lock); + + if (!ret) { + int ret = 0; + long ack_cnt = atomic_xchg(&arm_state->ka_use_ack_count, 0); + + while (ack_cnt && !ret) { + /* Send the use notify to videocore */ + ret = vchiq_send_remote_use_active(state); + if (!ret) + ack_cnt--; + else + atomic_add(ack_cnt, &arm_state->ka_use_ack_count); + } + } + +out: + dev_dbg(state->dev, "suspend: exit %d\n", ret); + return ret; +} + +int +vchiq_release_internal(struct vchiq_state *state, struct vchiq_service *service) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + int ret = 0; + char entity[64]; + int *entity_uc; + + if (!arm_state) { + ret = -EINVAL; + goto out; + } + + if (service) { + snprintf(entity, sizeof(entity), "%p4cc:%03d", + &service->base.fourcc, + service->client_id); + entity_uc = &service->service_use_count; + } else { + snprintf(entity, sizeof(entity), "PEER: "); + entity_uc = &arm_state->peer_use_count; + } + + write_lock_bh(&arm_state->susp_res_lock); + if (!arm_state->videocore_use_count || !(*entity_uc)) { + WARN_ON(!arm_state->videocore_use_count); + WARN_ON(!(*entity_uc)); + ret = -EINVAL; + goto unlock; + } + --arm_state->videocore_use_count; + --(*entity_uc); + + dev_dbg(state->dev, "suspend: %s count %d, state count %d\n", + entity, *entity_uc, arm_state->videocore_use_count); + +unlock: + write_unlock_bh(&arm_state->susp_res_lock); + +out: + dev_dbg(state->dev, "suspend: exit %d\n", ret); + return ret; +} + +void +vchiq_on_remote_use(struct vchiq_state *state) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + + atomic_inc(&arm_state->ka_use_count); + complete(&arm_state->ka_evt); +} + +void +vchiq_on_remote_release(struct vchiq_state *state) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + + atomic_inc(&arm_state->ka_release_count); + complete(&arm_state->ka_evt); +} + +int +vchiq_use_service_internal(struct vchiq_service *service) +{ + return vchiq_use_internal(service->state, service, USE_TYPE_SERVICE); +} + +int +vchiq_release_service_internal(struct vchiq_service *service) +{ + return vchiq_release_internal(service->state, service); +} + +struct vchiq_debugfs_node * +vchiq_instance_get_debugfs_node(struct vchiq_instance *instance) +{ + return &instance->debugfs_node; +} + +int +vchiq_instance_get_use_count(struct vchiq_instance *instance) +{ + struct vchiq_service *service; + int use_count = 0, i; + + i = 0; + rcu_read_lock(); + while ((service = __next_service_by_instance(instance->state, + instance, &i))) + use_count += service->service_use_count; + rcu_read_unlock(); + return use_count; +} + +int +vchiq_instance_get_pid(struct vchiq_instance *instance) +{ + return instance->pid; +} + +int +vchiq_instance_get_trace(struct vchiq_instance *instance) +{ + return instance->trace; +} + +void +vchiq_instance_set_trace(struct vchiq_instance *instance, int trace) +{ + struct vchiq_service *service; + int i; + + i = 0; + rcu_read_lock(); + while ((service = __next_service_by_instance(instance->state, + instance, &i))) + service->trace = trace; + rcu_read_unlock(); + instance->trace = (trace != 0); +} + +int +vchiq_use_service(struct vchiq_instance *instance, unsigned int handle) +{ + int ret = -EINVAL; + struct vchiq_service *service = find_service_by_handle(instance, handle); + + if (service) { + ret = vchiq_use_internal(service->state, service, USE_TYPE_SERVICE); + vchiq_service_put(service); + } + return ret; +} +EXPORT_SYMBOL(vchiq_use_service); + +int +vchiq_release_service(struct vchiq_instance *instance, unsigned int handle) +{ + int ret = -EINVAL; + struct vchiq_service *service = find_service_by_handle(instance, handle); + + if (service) { + ret = vchiq_release_internal(service->state, service); + vchiq_service_put(service); + } + return ret; +} +EXPORT_SYMBOL(vchiq_release_service); + +struct service_data_struct { + int fourcc; + int clientid; + int use_count; +}; + +void +vchiq_dump_service_use_state(struct vchiq_state *state) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + struct service_data_struct *service_data; + int i, found = 0; + /* + * If there's more than 64 services, only dump ones with + * non-zero counts + */ + int only_nonzero = 0; + static const char *nz = "<-- preventing suspend"; + + int peer_count; + int vc_use_count; + int active_services; + + if (!arm_state) + return; + + service_data = kmalloc_objs(*service_data, MAX_SERVICES); + if (!service_data) + return; + + read_lock_bh(&arm_state->susp_res_lock); + peer_count = arm_state->peer_use_count; + vc_use_count = arm_state->videocore_use_count; + active_services = state->unused_service; + if (active_services > MAX_SERVICES) + only_nonzero = 1; + + rcu_read_lock(); + for (i = 0; i < active_services; i++) { + struct vchiq_service *service_ptr = + rcu_dereference(state->services[i]); + + if (!service_ptr) + continue; + + if (only_nonzero && !service_ptr->service_use_count) + continue; + + if (service_ptr->srvstate == VCHIQ_SRVSTATE_FREE) + continue; + + service_data[found].fourcc = service_ptr->base.fourcc; + service_data[found].clientid = service_ptr->client_id; + service_data[found].use_count = service_ptr->service_use_count; + found++; + if (found >= MAX_SERVICES) + break; + } + rcu_read_unlock(); + + read_unlock_bh(&arm_state->susp_res_lock); + + if (only_nonzero) + dev_warn(state->dev, + "suspend: Too many active services (%d). Only dumping up to first %d services with non-zero use-count\n", + active_services, found); + + for (i = 0; i < found; i++) { + dev_warn(state->dev, + "suspend: %p4cc:%d service count %d %s\n", + &service_data[i].fourcc, + service_data[i].clientid, service_data[i].use_count, + service_data[i].use_count ? nz : ""); + } + dev_warn(state->dev, "suspend: VCHIQ use count %d\n", peer_count); + dev_warn(state->dev, "suspend: Overall vchiq instance use count %d\n", vc_use_count); + + kfree(service_data); +} + +int +vchiq_check_service(struct vchiq_service *service) +{ + struct vchiq_arm_state *arm_state; + int ret = -EINVAL; + + if (!service || !service->state) + goto out; + + arm_state = vchiq_platform_get_arm_state(service->state); + + read_lock_bh(&arm_state->susp_res_lock); + if (service->service_use_count) + ret = 0; + read_unlock_bh(&arm_state->susp_res_lock); + + if (ret) { + dev_err(service->state->dev, + "suspend: %s: %p4cc:%d service count %d, state count %d\n", + __func__, &service->base.fourcc, service->client_id, + service->service_use_count, arm_state->videocore_use_count); + vchiq_dump_service_use_state(service->state); + } +out: + return ret; +} + +void vchiq_platform_conn_state_changed(struct vchiq_state *state, + enum vchiq_connstate oldstate, + enum vchiq_connstate newstate) +{ + struct vchiq_arm_state *arm_state = vchiq_platform_get_arm_state(state); + char threadname[16]; + + dev_dbg(state->dev, "suspend: %d: %s->%s\n", + state->id, get_conn_state_name(oldstate), get_conn_state_name(newstate)); + if (state->conn_state != VCHIQ_CONNSTATE_CONNECTED) + return; + + write_lock_bh(&arm_state->susp_res_lock); + if (arm_state->first_connect) { + write_unlock_bh(&arm_state->susp_res_lock); + return; + } + + arm_state->first_connect = 1; + write_unlock_bh(&arm_state->susp_res_lock); + snprintf(threadname, sizeof(threadname), "vchiq-keep/%d", + state->id); + arm_state->ka_thread = kthread_create(&vchiq_keepalive_thread_func, + (void *)state, + threadname); + if (IS_ERR(arm_state->ka_thread)) { + dev_err(state->dev, "suspend: Couldn't create thread %s\n", + threadname); + } else { + wake_up_process(arm_state->ka_thread); + } +} + +static const struct of_device_id vchiq_of_match[] = { + { .compatible = "brcm,bcm2835-vchiq", .data = &bcm2835_info }, + { .compatible = "brcm,bcm2836-vchiq", .data = &bcm2836_info }, + {}, +}; +MODULE_DEVICE_TABLE(of, vchiq_of_match); + +static int vchiq_probe(struct platform_device *pdev) +{ + const struct vchiq_platform_info *info; + struct vchiq_drv_mgmt *mgmt; + int ret; + + info = of_device_get_match_data(&pdev->dev); + if (!info) + return -EINVAL; + + struct device_node *fw_node __free(device_node) = + of_find_compatible_node(NULL, NULL, "raspberrypi,bcm2835-firmware"); + if (!fw_node) { + dev_err(&pdev->dev, "Missing firmware node\n"); + return -ENOENT; + } + + mgmt = devm_kzalloc(&pdev->dev, sizeof(*mgmt), GFP_KERNEL); + if (!mgmt) + return -ENOMEM; + + mgmt->fw = devm_rpi_firmware_get(&pdev->dev, fw_node); + if (!mgmt->fw) + return -EPROBE_DEFER; + + mgmt->info = info; + platform_set_drvdata(pdev, mgmt); + + ret = vchiq_platform_init(pdev, &mgmt->state); + if (ret) { + dev_err(&pdev->dev, "arm: Could not initialize vchiq platform\n"); + return ret; + } + + dev_dbg(&pdev->dev, "arm: platform initialised - version %d (min %d)\n", + VCHIQ_VERSION, VCHIQ_VERSION_MIN); + + /* + * Simply exit on error since the function handles cleanup in + * cases of failure. + */ + ret = vchiq_register_chrdev(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "arm: Failed to initialize vchiq cdev\n"); + vchiq_platform_uninit(mgmt); + return ret; + } + + vchiq_debugfs_init(&mgmt->state); + + bcm2835_audio = vchiq_device_register(&pdev->dev, "bcm2835-audio"); + + return 0; +} + +static void vchiq_remove(struct platform_device *pdev) +{ + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(&pdev->dev); + + vchiq_device_unregister(bcm2835_audio); + vchiq_debugfs_deinit(); + vchiq_deregister_chrdev(); + vchiq_platform_uninit(mgmt); +} + +static struct platform_driver vchiq_driver = { + .driver = { + .name = "bcm2835_vchiq", + .of_match_table = vchiq_of_match, + }, + .probe = vchiq_probe, + .remove = vchiq_remove, +}; + +static int __init vchiq_driver_init(void) +{ + int ret; + + ret = bus_register(&vchiq_bus_type); + if (ret) { + pr_err("Failed to register %s\n", vchiq_bus_type.name); + return ret; + } + + ret = platform_driver_register(&vchiq_driver); + if (ret) { + pr_err("Failed to register vchiq driver\n"); + bus_unregister(&vchiq_bus_type); + } + + return ret; +} +module_init(vchiq_driver_init); + +static void __exit vchiq_driver_exit(void) +{ + bus_unregister(&vchiq_bus_type); + platform_driver_unregister(&vchiq_driver); +} +module_exit(vchiq_driver_exit); + +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_DESCRIPTION("Videocore VCHIQ driver"); +MODULE_AUTHOR("Broadcom Corporation"); diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_bus.c b/drivers/platform/raspberrypi/vchiq-interface/vchiq_bus.c new file mode 100644 index 000000000..011105f13 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_bus.c @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * vchiq_device.c - VCHIQ generic device and bus-type + * + * Copyright (c) 2023 Ideas On Board Oy + */ + +#include +#include +#include +#include +#include + +#include +#include + +static int vchiq_bus_type_match(struct device *dev, const struct device_driver *drv) +{ + if (dev->bus == &vchiq_bus_type && + strcmp(dev_name(dev), drv->name) == 0) + return true; + + return false; +} + +static int vchiq_bus_uevent(const struct device *dev, struct kobj_uevent_env *env) +{ + const struct vchiq_device *device = container_of_const(dev, struct vchiq_device, dev); + + return add_uevent_var(env, "MODALIAS=vchiq:%s", dev_name(&device->dev)); +} + +static int vchiq_bus_probe(struct device *dev) +{ + struct vchiq_device *device = to_vchiq_device(dev); + struct vchiq_driver *driver = to_vchiq_driver(dev->driver); + + return driver->probe(device); +} + +static void vchiq_bus_remove(struct device *dev) +{ + struct vchiq_device *device = to_vchiq_device(dev); + struct vchiq_driver *driver = to_vchiq_driver(dev->driver); + + if (driver->remove) + driver->remove(device); +} + +const struct bus_type vchiq_bus_type = { + .name = "vchiq-bus", + .match = vchiq_bus_type_match, + .uevent = vchiq_bus_uevent, + .probe = vchiq_bus_probe, + .remove = vchiq_bus_remove, +}; + +static void vchiq_device_release(struct device *dev) +{ + struct vchiq_device *device = to_vchiq_device(dev); + + kfree(device); +} + +struct vchiq_device * +vchiq_device_register(struct device *parent, const char *name) +{ + struct vchiq_device *device; + int ret; + + device = kzalloc_obj(*device); + if (!device) + return NULL; + + device->dev.init_name = name; + device->dev.parent = parent; + device->dev.bus = &vchiq_bus_type; + device->dev.dma_mask = &device->dev.coherent_dma_mask; + device->dev.release = vchiq_device_release; + + device->drv_mgmt = dev_get_drvdata(parent); + + of_dma_configure(&device->dev, parent->of_node, true); + + ret = device_register(&device->dev); + if (ret) { + dev_err(parent, "Cannot register %s: %d\n", name, ret); + put_device(&device->dev); + return NULL; + } + + return device; +} + +void vchiq_device_unregister(struct vchiq_device *vchiq_dev) +{ + device_unregister(&vchiq_dev->dev); +} + +int vchiq_driver_register(struct vchiq_driver *vchiq_drv) +{ + vchiq_drv->driver.bus = &vchiq_bus_type; + + return driver_register(&vchiq_drv->driver); +} +EXPORT_SYMBOL_GPL(vchiq_driver_register); + +void vchiq_driver_unregister(struct vchiq_driver *vchiq_drv) +{ + driver_unregister(&vchiq_drv->driver); +} +EXPORT_SYMBOL_GPL(vchiq_driver_unregister); diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_core.c b/drivers/platform/raspberrypi/vchiq-interface/vchiq_core.c new file mode 100644 index 000000000..48d6b1d74 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_core.c @@ -0,0 +1,4013 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +/* Copyright (c) 2010-2012 Broadcom. All rights reserved. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define VCHIQ_SLOT_HANDLER_STACK 8192 + +#define VCHIQ_MSG_PADDING 0 /* - */ +#define VCHIQ_MSG_CONNECT 1 /* - */ +#define VCHIQ_MSG_OPEN 2 /* + (srcport, -), fourcc, client_id */ +#define VCHIQ_MSG_OPENACK 3 /* + (srcport, dstport) */ +#define VCHIQ_MSG_CLOSE 4 /* + (srcport, dstport) */ +#define VCHIQ_MSG_DATA 5 /* + (srcport, dstport) */ +#define VCHIQ_MSG_BULK_RX 6 /* + (srcport, dstport), data, size */ +#define VCHIQ_MSG_BULK_TX 7 /* + (srcport, dstport), data, size */ +#define VCHIQ_MSG_BULK_RX_DONE 8 /* + (srcport, dstport), actual */ +#define VCHIQ_MSG_BULK_TX_DONE 9 /* + (srcport, dstport), actual */ +#define VCHIQ_MSG_PAUSE 10 /* - */ +#define VCHIQ_MSG_RESUME 11 /* - */ +#define VCHIQ_MSG_REMOTE_USE 12 /* - */ +#define VCHIQ_MSG_REMOTE_RELEASE 13 /* - */ +#define VCHIQ_MSG_REMOTE_USE_ACTIVE 14 /* - */ + +#define TYPE_SHIFT 24 + +#define VCHIQ_PORT_MAX (VCHIQ_MAX_SERVICES - 1) +#define VCHIQ_PORT_FREE 0x1000 +#define VCHIQ_PORT_IS_VALID(port) ((port) < VCHIQ_PORT_FREE) +#define VCHIQ_MAKE_MSG(type, srcport, dstport) \ + (((type) << TYPE_SHIFT) | ((srcport) << 12) | ((dstport) << 0)) +#define VCHIQ_MSG_TYPE(msgid) ((unsigned int)(msgid) >> TYPE_SHIFT) +#define VCHIQ_MSG_SRCPORT(msgid) \ + ((unsigned short)(((unsigned int)(msgid) >> 12) & 0xfff)) +#define VCHIQ_MSG_DSTPORT(msgid) \ + ((unsigned short)(msgid) & 0xfff) + +#define MAKE_CONNECT (VCHIQ_MSG_CONNECT << TYPE_SHIFT) +#define MAKE_OPEN(srcport) \ + ((VCHIQ_MSG_OPEN << TYPE_SHIFT) | ((srcport) << 12)) +#define MAKE_OPENACK(srcport, dstport) \ + ((VCHIQ_MSG_OPENACK << TYPE_SHIFT) | ((srcport) << 12) | ((dstport) << 0)) +#define MAKE_CLOSE(srcport, dstport) \ + ((VCHIQ_MSG_CLOSE << TYPE_SHIFT) | ((srcport) << 12) | ((dstport) << 0)) +#define MAKE_DATA(srcport, dstport) \ + ((VCHIQ_MSG_DATA << TYPE_SHIFT) | ((srcport) << 12) | ((dstport) << 0)) +#define MAKE_PAUSE (VCHIQ_MSG_PAUSE << TYPE_SHIFT) +#define MAKE_RESUME (VCHIQ_MSG_RESUME << TYPE_SHIFT) +#define MAKE_REMOTE_USE (VCHIQ_MSG_REMOTE_USE << TYPE_SHIFT) +#define MAKE_REMOTE_USE_ACTIVE (VCHIQ_MSG_REMOTE_USE_ACTIVE << TYPE_SHIFT) + +#define PAGELIST_WRITE 0 +#define PAGELIST_READ 1 +#define PAGELIST_READ_WITH_FRAGMENTS 2 + +#define BELL2 0x08 + +/* Ensure the fields are wide enough */ +static_assert(VCHIQ_MSG_SRCPORT(VCHIQ_MAKE_MSG(0, 0, VCHIQ_PORT_MAX)) == 0); +static_assert(VCHIQ_MSG_TYPE(VCHIQ_MAKE_MSG(0, VCHIQ_PORT_MAX, 0)) == 0); +static_assert((unsigned int)VCHIQ_PORT_MAX < (unsigned int)VCHIQ_PORT_FREE); + +#define VCHIQ_MSGID_PADDING VCHIQ_MAKE_MSG(VCHIQ_MSG_PADDING, 0, 0) +#define VCHIQ_MSGID_CLAIMED 0x40000000 + +#define VCHIQ_FOURCC_INVALID 0x00000000 +#define VCHIQ_FOURCC_IS_LEGAL(fourcc) ((fourcc) != VCHIQ_FOURCC_INVALID) + +#define VCHIQ_BULK_ACTUAL_ABORTED -1 + +#if VCHIQ_ENABLE_STATS +#define VCHIQ_STATS_INC(state, stat) (state->stats. stat++) +#define VCHIQ_SERVICE_STATS_INC(service, stat) (service->stats. stat++) +#define VCHIQ_SERVICE_STATS_ADD(service, stat, addend) \ + (service->stats. stat += addend) +#else +#define VCHIQ_STATS_INC(state, stat) ((void)0) +#define VCHIQ_SERVICE_STATS_INC(service, stat) ((void)0) +#define VCHIQ_SERVICE_STATS_ADD(service, stat, addend) ((void)0) +#endif + +#define HANDLE_STATE_SHIFT 12 + +#define SLOT_INFO_FROM_INDEX(state, index) (state->slot_info + (index)) +#define SLOT_DATA_FROM_INDEX(state, index) (state->slot_data + (index)) +#define SLOT_INDEX_FROM_DATA(state, data) \ + (((unsigned int)((char *)data - (char *)state->slot_data)) / \ + VCHIQ_SLOT_SIZE) +#define SLOT_INDEX_FROM_INFO(state, info) \ + ((unsigned int)(info - state->slot_info)) +#define SLOT_QUEUE_INDEX_FROM_POS(pos) \ + ((int)((unsigned int)(pos) / VCHIQ_SLOT_SIZE)) +#define SLOT_QUEUE_INDEX_FROM_POS_MASKED(pos) \ + (SLOT_QUEUE_INDEX_FROM_POS(pos) & VCHIQ_SLOT_QUEUE_MASK) + +#define BULK_INDEX(x) ((x) & (VCHIQ_NUM_SERVICE_BULKS - 1)) + +#define NO_CLOSE_RECVD 0 +#define CLOSE_RECVD 1 + +#define NO_RETRY_POLL 0 +#define RETRY_POLL 1 + +struct vchiq_open_payload { + int fourcc; + int client_id; + short version; + short version_min; +}; + +struct vchiq_openack_payload { + short version; +}; + +enum { + QMFLAGS_IS_BLOCKING = BIT(0), + QMFLAGS_NO_MUTEX_LOCK = BIT(1), + QMFLAGS_NO_MUTEX_UNLOCK = BIT(2) +}; + +enum { + VCHIQ_POLL_TERMINATE, + VCHIQ_POLL_REMOVE, + VCHIQ_POLL_TXNOTIFY, + VCHIQ_POLL_RXNOTIFY, + VCHIQ_POLL_COUNT +}; + +/* we require this for consistency between endpoints */ +static_assert(sizeof(struct vchiq_header) == 8); +static_assert(VCHIQ_VERSION >= VCHIQ_VERSION_MIN); + +static inline void check_sizes(void) +{ + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_SLOT_SIZE); + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_MAX_SLOTS); + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_MAX_SLOTS_PER_SIDE); + BUILD_BUG_ON_NOT_POWER_OF_2(sizeof(struct vchiq_header)); + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_NUM_CURRENT_BULKS); + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_NUM_SERVICE_BULKS); + BUILD_BUG_ON_NOT_POWER_OF_2(VCHIQ_MAX_SERVICES); +} + +static unsigned int handle_seq; + +static const char *const srvstate_names[] = { + "FREE", + "HIDDEN", + "LISTENING", + "OPENING", + "OPEN", + "OPENSYNC", + "CLOSESENT", + "CLOSERECVD", + "CLOSEWAIT", + "CLOSED" +}; + +static const char *const reason_names[] = { + "SERVICE_OPENED", + "SERVICE_CLOSED", + "MESSAGE_AVAILABLE", + "BULK_TRANSMIT_DONE", + "BULK_RECEIVE_DONE", + "BULK_TRANSMIT_ABORTED", + "BULK_RECEIVE_ABORTED" +}; + +static const char *const conn_state_names[] = { + "DISCONNECTED", + "CONNECTING", + "CONNECTED", + "PAUSING", + "PAUSE_SENT", + "PAUSED", + "RESUMING", + "PAUSE_TIMEOUT", + "RESUME_TIMEOUT" +}; + +static void +release_message_sync(struct vchiq_state *state, struct vchiq_header *header); + +static const char *msg_type_str(unsigned int msg_type) +{ + switch (msg_type) { + case VCHIQ_MSG_PADDING: return "PADDING"; + case VCHIQ_MSG_CONNECT: return "CONNECT"; + case VCHIQ_MSG_OPEN: return "OPEN"; + case VCHIQ_MSG_OPENACK: return "OPENACK"; + case VCHIQ_MSG_CLOSE: return "CLOSE"; + case VCHIQ_MSG_DATA: return "DATA"; + case VCHIQ_MSG_BULK_RX: return "BULK_RX"; + case VCHIQ_MSG_BULK_TX: return "BULK_TX"; + case VCHIQ_MSG_BULK_RX_DONE: return "BULK_RX_DONE"; + case VCHIQ_MSG_BULK_TX_DONE: return "BULK_TX_DONE"; + case VCHIQ_MSG_PAUSE: return "PAUSE"; + case VCHIQ_MSG_RESUME: return "RESUME"; + case VCHIQ_MSG_REMOTE_USE: return "REMOTE_USE"; + case VCHIQ_MSG_REMOTE_RELEASE: return "REMOTE_RELEASE"; + case VCHIQ_MSG_REMOTE_USE_ACTIVE: return "REMOTE_USE_ACTIVE"; + } + return "???"; +} + +static inline void +set_service_state(struct vchiq_service *service, int newstate) +{ + dev_dbg(service->state->dev, "core: %d: srv:%d %s->%s\n", + service->state->id, service->localport, + srvstate_names[service->srvstate], + srvstate_names[newstate]); + service->srvstate = newstate; +} + +struct vchiq_service *handle_to_service(struct vchiq_instance *instance, unsigned int handle) +{ + int idx = handle & (VCHIQ_MAX_SERVICES - 1); + + return rcu_dereference(instance->state->services[idx]); +} + +struct vchiq_service * +find_service_by_handle(struct vchiq_instance *instance, unsigned int handle) +{ + struct vchiq_service *service; + + rcu_read_lock(); + service = handle_to_service(instance, handle); + if (service && service->srvstate != VCHIQ_SRVSTATE_FREE && + service->handle == handle && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + rcu_read_unlock(); + dev_dbg(instance->state->dev, "core: Invalid service handle 0x%x\n", handle); + return NULL; +} + +struct vchiq_service * +find_service_by_port(struct vchiq_state *state, unsigned int localport) +{ + if (localport <= VCHIQ_PORT_MAX) { + struct vchiq_service *service; + + rcu_read_lock(); + service = rcu_dereference(state->services[localport]); + if (service && service->srvstate != VCHIQ_SRVSTATE_FREE && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + rcu_read_unlock(); + } + dev_dbg(state->dev, "core: Invalid port %u\n", localport); + return NULL; +} + +struct vchiq_service * +find_service_for_instance(struct vchiq_instance *instance, unsigned int handle) +{ + struct vchiq_service *service; + + rcu_read_lock(); + service = handle_to_service(instance, handle); + if (service && service->srvstate != VCHIQ_SRVSTATE_FREE && + service->handle == handle && + service->instance == instance && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + rcu_read_unlock(); + dev_dbg(instance->state->dev, "core: Invalid service handle 0x%x\n", handle); + return NULL; +} + +struct vchiq_service * +find_closed_service_for_instance(struct vchiq_instance *instance, unsigned int handle) +{ + struct vchiq_service *service; + + rcu_read_lock(); + service = handle_to_service(instance, handle); + if (service && + (service->srvstate == VCHIQ_SRVSTATE_FREE || + service->srvstate == VCHIQ_SRVSTATE_CLOSED) && + service->handle == handle && + service->instance == instance && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + rcu_read_unlock(); + dev_dbg(instance->state->dev, "core: Invalid service handle 0x%x\n", handle); + return service; +} + +struct vchiq_service * +__next_service_by_instance(struct vchiq_state *state, + struct vchiq_instance *instance, + int *pidx) +{ + struct vchiq_service *service = NULL; + int idx = *pidx; + + while (idx < state->unused_service) { + struct vchiq_service *srv; + + srv = rcu_dereference(state->services[idx]); + idx++; + if (srv && srv->srvstate != VCHIQ_SRVSTATE_FREE && + srv->instance == instance) { + service = srv; + break; + } + } + + *pidx = idx; + return service; +} + +struct vchiq_service * +next_service_by_instance(struct vchiq_state *state, + struct vchiq_instance *instance, + int *pidx) +{ + struct vchiq_service *service; + + rcu_read_lock(); + while (1) { + service = __next_service_by_instance(state, instance, pidx); + if (!service) + break; + if (kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + break; + } + } + rcu_read_unlock(); + return service; +} + +void +vchiq_service_get(struct vchiq_service *service) +{ + if (!service) { + WARN(1, "%s service is NULL\n", __func__); + return; + } + kref_get(&service->ref_count); +} + +static void service_release(struct kref *kref) +{ + struct vchiq_service *service = + container_of(kref, struct vchiq_service, ref_count); + struct vchiq_state *state = service->state; + + WARN_ON(service->srvstate != VCHIQ_SRVSTATE_FREE); + rcu_assign_pointer(state->services[service->localport], NULL); + if (service->userdata_term) + service->userdata_term(service->base.userdata); + kfree_rcu(service, rcu); +} + +void +vchiq_service_put(struct vchiq_service *service) +{ + if (!service) { + WARN(1, "%s: service is NULL\n", __func__); + return; + } + kref_put(&service->ref_count, service_release); +} + +int +vchiq_get_client_id(struct vchiq_instance *instance, unsigned int handle) +{ + struct vchiq_service *service; + int id; + + rcu_read_lock(); + service = handle_to_service(instance, handle); + id = service ? service->client_id : 0; + rcu_read_unlock(); + return id; +} + +void * +vchiq_get_service_userdata(struct vchiq_instance *instance, unsigned int handle) +{ + void *userdata; + struct vchiq_service *service; + + rcu_read_lock(); + service = handle_to_service(instance, handle); + userdata = service ? service->base.userdata : NULL; + rcu_read_unlock(); + return userdata; +} +EXPORT_SYMBOL(vchiq_get_service_userdata); + +static void +mark_service_closing_internal(struct vchiq_service *service, int sh_thread) +{ + struct vchiq_state *state = service->state; + struct vchiq_service_quota *quota; + + service->closing = 1; + + /* Synchronise with other threads. */ + mutex_lock(&state->recycle_mutex); + mutex_unlock(&state->recycle_mutex); + if (!sh_thread || (state->conn_state != VCHIQ_CONNSTATE_PAUSE_SENT)) { + /* + * If we're pausing then the slot_mutex is held until resume + * by the slot handler. Therefore don't try to acquire this + * mutex if we're the slot handler and in the pause sent state. + * We don't need to in this case anyway. + */ + mutex_lock(&state->slot_mutex); + mutex_unlock(&state->slot_mutex); + } + + /* Unblock any sending thread. */ + quota = &state->service_quotas[service->localport]; + complete("a->quota_event); +} + +static void +mark_service_closing(struct vchiq_service *service) +{ + mark_service_closing_internal(service, 0); +} + +static inline int +make_service_callback(struct vchiq_service *service, enum vchiq_reason reason, + struct vchiq_header *header, struct vchiq_bulk *bulk) +{ + void *cb_data = NULL; + void __user *cb_userdata = NULL; + int status; + + /* + * If a bulk transfer is in progress, pass bulk->cb_*data to the + * callback function. + */ + if (bulk) { + cb_data = bulk->cb_data; + cb_userdata = bulk->cb_userdata; + } + + dev_dbg(service->state->dev, "core: %d: callback:%d (%s, %p, %p %p)\n", + service->state->id, service->localport, reason_names[reason], + header, cb_data, cb_userdata); + status = service->base.callback(service->instance, reason, header, service->handle, + cb_data, cb_userdata); + if (status && (status != -EAGAIN)) { + dev_warn(service->state->dev, + "core: %d: ignoring ERROR from callback to service %x\n", + service->state->id, service->handle); + status = 0; + } + + if (reason != VCHIQ_MESSAGE_AVAILABLE) + vchiq_release_message(service->instance, service->handle, header); + + return status; +} + +inline void +vchiq_set_conn_state(struct vchiq_state *state, enum vchiq_connstate newstate) +{ + enum vchiq_connstate oldstate = state->conn_state; + + dev_dbg(state->dev, "core: %d: %s->%s\n", + state->id, conn_state_names[oldstate], conn_state_names[newstate]); + state->conn_state = newstate; + vchiq_platform_conn_state_changed(state, oldstate, newstate); +} + +/* This initialises a single remote_event, and the associated wait_queue. */ +static inline void +remote_event_create(wait_queue_head_t *wq, struct remote_event *event) +{ + event->armed = 0; + /* + * Don't clear the 'fired' flag because it may already have been set + * by the other side. + */ + init_waitqueue_head(wq); +} + +/* + * All the event waiting routines in VCHIQ used a custom semaphore + * implementation that filtered most signals. This achieved a behaviour similar + * to the "killable" family of functions. While cleaning up this code all the + * routines where switched to the "interruptible" family of functions, as the + * former was deemed unjustified and the use "killable" set all VCHIQ's + * threads in D state. + * + * Returns: 0 on success, a negative error code on failure + */ +static inline int +remote_event_wait(wait_queue_head_t *wq, struct remote_event *event) +{ + int ret = 0; + + if (!event->fired) { + event->armed = 1; + dsb(sy); + ret = wait_event_interruptible(*wq, event->fired); + if (ret) { + event->armed = 0; + return ret; + } + event->armed = 0; + /* Ensure that the peer sees that we are not waiting (armed == 0). */ + wmb(); + } + + event->fired = 0; + return ret; +} + +static void +remote_event_signal(struct vchiq_state *state, struct remote_event *event) +{ + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(state->dev); + + /* + * Ensure that all writes to shared data structures have completed + * before signalling the peer. + */ + wmb(); + + event->fired = 1; + + dsb(sy); /* data barrier operation */ + + if (event->armed) + writel(0, mgmt->regs + BELL2); /* trigger vc interrupt */ +} + +/* + * Acknowledge that the event has been signalled, and wake any waiters. Usually + * called as a result of the doorbell being rung. + */ +static inline void +remote_event_signal_local(wait_queue_head_t *wq, struct remote_event *event) +{ + event->fired = 1; + event->armed = 0; + wake_up_all(wq); +} + +/* Check if a single event has been signalled, waking the waiters if it has. */ +static inline void +remote_event_poll(wait_queue_head_t *wq, struct remote_event *event) +{ + if (event->fired && event->armed) + remote_event_signal_local(wq, event); +} + +/* + * VCHIQ used a small, fixed number of remote events. It is simplest to + * enumerate them here for polling. + */ +void +remote_event_pollall(struct vchiq_state *state) +{ + remote_event_poll(&state->sync_trigger_event, &state->local->sync_trigger); + remote_event_poll(&state->sync_release_event, &state->local->sync_release); + remote_event_poll(&state->trigger_event, &state->local->trigger); + remote_event_poll(&state->recycle_event, &state->local->recycle); +} + +/* + * Round up message sizes so that any space at the end of a slot is always big + * enough for a header. This relies on header size being a power of two, which + * has been verified earlier by a static assertion. + */ + +static inline size_t +calc_stride(size_t size) +{ + /* Allow room for the header */ + size += sizeof(struct vchiq_header); + + /* Round up */ + return (size + sizeof(struct vchiq_header) - 1) & + ~(sizeof(struct vchiq_header) - 1); +} + +/* Called by the slot handler thread */ +static struct vchiq_service * +get_listening_service(struct vchiq_state *state, int fourcc) +{ + int i; + + WARN_ON(fourcc == VCHIQ_FOURCC_INVALID); + + rcu_read_lock(); + for (i = 0; i < state->unused_service; i++) { + struct vchiq_service *service; + + service = rcu_dereference(state->services[i]); + if (service && + service->public_fourcc == fourcc && + (service->srvstate == VCHIQ_SRVSTATE_LISTENING || + (service->srvstate == VCHIQ_SRVSTATE_OPEN && + service->remoteport == VCHIQ_PORT_FREE)) && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + } + rcu_read_unlock(); + return NULL; +} + +/* Called by the slot handler thread */ +static struct vchiq_service * +get_connected_service(struct vchiq_state *state, unsigned int port) +{ + int i; + + rcu_read_lock(); + for (i = 0; i < state->unused_service; i++) { + struct vchiq_service *service = + rcu_dereference(state->services[i]); + + if (service && service->srvstate == VCHIQ_SRVSTATE_OPEN && + service->remoteport == port && + kref_get_unless_zero(&service->ref_count)) { + service = rcu_pointer_handoff(service); + rcu_read_unlock(); + return service; + } + } + rcu_read_unlock(); + return NULL; +} + +inline void +request_poll(struct vchiq_state *state, struct vchiq_service *service, + int poll_type) +{ + u32 value; + int index; + + if (!service) + goto skip_service; + + do { + value = atomic_read(&service->poll_flags); + } while (atomic_cmpxchg(&service->poll_flags, value, + value | BIT(poll_type)) != value); + + index = BITSET_WORD(service->localport); + do { + value = atomic_read(&state->poll_services[index]); + } while (atomic_cmpxchg(&state->poll_services[index], + value, value | BIT(service->localport & 0x1f)) != value); + +skip_service: + state->poll_needed = 1; + /* Ensure the slot handler thread sees the poll_needed flag. */ + wmb(); + + /* ... and ensure the slot handler runs. */ + remote_event_signal_local(&state->trigger_event, &state->local->trigger); +} + +/* + * Called from queue_message, by the slot handler and application threads, + * with slot_mutex held + */ +static struct vchiq_header * +reserve_space(struct vchiq_state *state, size_t space, int is_blocking) +{ + struct vchiq_shared_state *local = state->local; + int tx_pos = state->local_tx_pos; + int slot_space = VCHIQ_SLOT_SIZE - (tx_pos & VCHIQ_SLOT_MASK); + + if (space > slot_space) { + struct vchiq_header *header; + /* Fill the remaining space with padding */ + WARN_ON(!state->tx_data); + header = (struct vchiq_header *) + (state->tx_data + (tx_pos & VCHIQ_SLOT_MASK)); + header->msgid = VCHIQ_MSGID_PADDING; + header->size = slot_space - sizeof(struct vchiq_header); + + tx_pos += slot_space; + } + + /* If necessary, get the next slot. */ + if ((tx_pos & VCHIQ_SLOT_MASK) == 0) { + int slot_index; + + /* If there is no free slot... */ + + if (!try_wait_for_completion(&state->slot_available_event)) { + /* ...wait for one. */ + + VCHIQ_STATS_INC(state, slot_stalls); + + /* But first, flush through the last slot. */ + state->local_tx_pos = tx_pos; + local->tx_pos = tx_pos; + remote_event_signal(state, &state->remote->trigger); + + if (!is_blocking || + (wait_for_completion_interruptible(&state->slot_available_event))) + return NULL; /* No space available */ + } + + if (tx_pos == (state->slot_queue_available * VCHIQ_SLOT_SIZE)) { + complete(&state->slot_available_event); + dev_warn(state->dev, "%s: invalid tx_pos: %d\n", + __func__, tx_pos); + return NULL; + } + + slot_index = local->slot_queue[SLOT_QUEUE_INDEX_FROM_POS_MASKED(tx_pos)]; + state->tx_data = + (char *)SLOT_DATA_FROM_INDEX(state, slot_index); + } + + state->local_tx_pos = tx_pos + space; + + return (struct vchiq_header *)(state->tx_data + + (tx_pos & VCHIQ_SLOT_MASK)); +} + +static void +process_free_data_message(struct vchiq_state *state, u32 *service_found, + struct vchiq_header *header) +{ + int msgid = header->msgid; + int port = VCHIQ_MSG_SRCPORT(msgid); + struct vchiq_service_quota *quota = &state->service_quotas[port]; + int count; + + spin_lock(&state->quota_spinlock); + count = quota->message_use_count; + if (count > 0) + quota->message_use_count = count - 1; + spin_unlock(&state->quota_spinlock); + + if (count == quota->message_quota) { + /* + * Signal the service that it + * has dropped below its quota + */ + complete("a->quota_event); + } else if (count == 0) { + dev_err(state->dev, + "core: service %d message_use_count=%d (header %p, msgid %x, header->msgid %x, header->size %x)\n", + port, quota->message_use_count, header, msgid, + header->msgid, header->size); + WARN(1, "invalid message use count\n"); + } + if (!BITSET_IS_SET(service_found, port)) { + /* Set the found bit for this service */ + BITSET_SET(service_found, port); + + spin_lock(&state->quota_spinlock); + count = quota->slot_use_count; + if (count > 0) + quota->slot_use_count = count - 1; + spin_unlock(&state->quota_spinlock); + + if (count > 0) { + /* + * Signal the service in case + * it has dropped below its quota + */ + complete("a->quota_event); + dev_dbg(state->dev, "core: %d: pfq:%d %x@%p - slot_use->%d\n", + state->id, port, header->size, header, count - 1); + } else { + dev_err(state->dev, + "core: service %d slot_use_count=%d (header %p, msgid %x, header->msgid %x, header->size %x)\n", + port, count, header, msgid, header->msgid, header->size); + WARN(1, "bad slot use count\n"); + } + } +} + +/* Called by the recycle thread. */ +static void +process_free_queue(struct vchiq_state *state, u32 *service_found, + size_t length) +{ + struct vchiq_shared_state *local = state->local; + int slot_queue_available; + + /* + * Find slots which have been freed by the other side, and return them + * to the available queue. + */ + slot_queue_available = state->slot_queue_available; + + /* + * Use a memory barrier to ensure that any state that may have been + * modified by another thread is not masked by stale prefetched + * values. + */ + mb(); + + while (slot_queue_available != local->slot_queue_recycle) { + unsigned int pos; + int slot_index = local->slot_queue[slot_queue_available & + VCHIQ_SLOT_QUEUE_MASK]; + char *data = (char *)SLOT_DATA_FROM_INDEX(state, slot_index); + int data_found = 0; + + slot_queue_available++; + /* + * Beware of the address dependency - data is calculated + * using an index written by the other side. + */ + rmb(); + + dev_dbg(state->dev, "core: %d: pfq %d=%p %x %x\n", + state->id, slot_index, data, local->slot_queue_recycle, + slot_queue_available); + + /* Initialise the bitmask for services which have used this slot */ + memset(service_found, 0, length); + + pos = 0; + + while (pos < VCHIQ_SLOT_SIZE) { + struct vchiq_header *header = + (struct vchiq_header *)(data + pos); + int msgid = header->msgid; + + if (VCHIQ_MSG_TYPE(msgid) == VCHIQ_MSG_DATA) { + process_free_data_message(state, service_found, + header); + data_found = 1; + } + + pos += calc_stride(header->size); + if (pos > VCHIQ_SLOT_SIZE) { + dev_err(state->dev, + "core: pfq - pos %x: header %p, msgid %x, header->msgid %x, header->size %x\n", + pos, header, msgid, header->msgid, header->size); + WARN(1, "invalid slot position\n"); + } + } + + if (data_found) { + int count; + + spin_lock(&state->quota_spinlock); + count = state->data_use_count; + if (count > 0) + state->data_use_count = count - 1; + spin_unlock(&state->quota_spinlock); + if (count == state->data_quota) + complete(&state->data_quota_event); + } + + /* + * Don't allow the slot to be reused until we are no + * longer interested in it. + */ + mb(); + + state->slot_queue_available = slot_queue_available; + complete(&state->slot_available_event); + } +} + +static ssize_t +memcpy_copy_callback(void *context, void *dest, size_t offset, size_t maxsize) +{ + memcpy(dest + offset, context + offset, maxsize); + return maxsize; +} + +static ssize_t +copy_message_data(ssize_t (*copy_callback)(void *context, void *dest, size_t offset, + size_t maxsize), + void *context, + void *dest, + size_t size) +{ + size_t pos = 0; + + while (pos < size) { + ssize_t callback_result; + size_t max_bytes = size - pos; + + callback_result = copy_callback(context, dest + pos, pos, + max_bytes); + + if (callback_result < 0) + return callback_result; + + if (!callback_result) + return -EIO; + + if (callback_result > max_bytes) + return -EIO; + + pos += callback_result; + } + + return size; +} + +/* Called by the slot handler and application threads */ +static int +queue_message(struct vchiq_state *state, struct vchiq_service *service, + int msgid, + ssize_t (*copy_callback)(void *context, void *dest, + size_t offset, size_t maxsize), + void *context, size_t size, int flags) +{ + struct vchiq_shared_state *local; + struct vchiq_service_quota *quota = NULL; + struct vchiq_header *header; + int type = VCHIQ_MSG_TYPE(msgid); + int svc_fourcc; + + size_t stride; + + local = state->local; + + stride = calc_stride(size); + + WARN_ON(stride > VCHIQ_SLOT_SIZE); + + if (!(flags & QMFLAGS_NO_MUTEX_LOCK) && + mutex_lock_killable(&state->slot_mutex)) + return -EINTR; + + if (type == VCHIQ_MSG_DATA) { + int tx_end_index; + + if (!service) { + WARN(1, "%s: service is NULL\n", __func__); + mutex_unlock(&state->slot_mutex); + return -EINVAL; + } + + WARN_ON(flags & (QMFLAGS_NO_MUTEX_LOCK | + QMFLAGS_NO_MUTEX_UNLOCK)); + + if (service->closing) { + /* The service has been closed */ + mutex_unlock(&state->slot_mutex); + return -EHOSTDOWN; + } + + quota = &state->service_quotas[service->localport]; + + spin_lock(&state->quota_spinlock); + + /* + * Ensure this service doesn't use more than its quota of + * messages or slots + */ + tx_end_index = SLOT_QUEUE_INDEX_FROM_POS(state->local_tx_pos + stride - 1); + + /* + * Ensure data messages don't use more than their quota of + * slots + */ + while ((tx_end_index != state->previous_data_index) && + (state->data_use_count == state->data_quota)) { + VCHIQ_STATS_INC(state, data_stalls); + spin_unlock(&state->quota_spinlock); + mutex_unlock(&state->slot_mutex); + + if (wait_for_completion_killable(&state->data_quota_event)) + return -EINTR; + + mutex_lock(&state->slot_mutex); + spin_lock(&state->quota_spinlock); + tx_end_index = SLOT_QUEUE_INDEX_FROM_POS(state->local_tx_pos + stride - 1); + if ((tx_end_index == state->previous_data_index) || + (state->data_use_count < state->data_quota)) { + /* Pass the signal on to other waiters */ + complete(&state->data_quota_event); + break; + } + } + + while ((quota->message_use_count == quota->message_quota) || + ((tx_end_index != quota->previous_tx_index) && + (quota->slot_use_count == quota->slot_quota))) { + spin_unlock(&state->quota_spinlock); + dev_dbg(state->dev, + "core: %d: qm:%d %s,%zx - quota stall (msg %d, slot %d)\n", + state->id, service->localport, msg_type_str(type), size, + quota->message_use_count, quota->slot_use_count); + VCHIQ_SERVICE_STATS_INC(service, quota_stalls); + mutex_unlock(&state->slot_mutex); + if (wait_for_completion_killable("a->quota_event)) + return -EINTR; + if (service->closing) + return -EHOSTDOWN; + if (mutex_lock_killable(&state->slot_mutex)) + return -EINTR; + if (service->srvstate != VCHIQ_SRVSTATE_OPEN) { + /* The service has been closed */ + mutex_unlock(&state->slot_mutex); + return -EHOSTDOWN; + } + spin_lock(&state->quota_spinlock); + tx_end_index = SLOT_QUEUE_INDEX_FROM_POS(state->local_tx_pos + stride - 1); + } + + spin_unlock(&state->quota_spinlock); + } + + header = reserve_space(state, stride, flags & QMFLAGS_IS_BLOCKING); + + if (!header) { + if (service) + VCHIQ_SERVICE_STATS_INC(service, slot_stalls); + /* + * In the event of a failure, return the mutex to the + * state it was in + */ + if (!(flags & QMFLAGS_NO_MUTEX_LOCK)) + mutex_unlock(&state->slot_mutex); + return -EAGAIN; + } + + if (type == VCHIQ_MSG_DATA) { + ssize_t callback_result; + int tx_end_index; + int slot_use_count; + + dev_dbg(state->dev, "core: %d: qm %s@%p,%zx (%d->%d)\n", + state->id, msg_type_str(VCHIQ_MSG_TYPE(msgid)), header, size, + VCHIQ_MSG_SRCPORT(msgid), VCHIQ_MSG_DSTPORT(msgid)); + + WARN_ON(flags & (QMFLAGS_NO_MUTEX_LOCK | + QMFLAGS_NO_MUTEX_UNLOCK)); + + callback_result = + copy_message_data(copy_callback, context, + header->data, size); + + if (callback_result < 0) { + mutex_unlock(&state->slot_mutex); + VCHIQ_SERVICE_STATS_INC(service, error_count); + return -EINVAL; + } + + vchiq_log_dump_mem(state->dev, "Sent", 0, + header->data, + min_t(size_t, 16, callback_result)); + + spin_lock(&state->quota_spinlock); + quota->message_use_count++; + + tx_end_index = + SLOT_QUEUE_INDEX_FROM_POS(state->local_tx_pos - 1); + + /* + * If this transmission can't fit in the last slot used by any + * service, the data_use_count must be increased. + */ + if (tx_end_index != state->previous_data_index) { + state->previous_data_index = tx_end_index; + state->data_use_count++; + } + + /* + * If this isn't the same slot last used by this service, + * the service's slot_use_count must be increased. + */ + if (tx_end_index != quota->previous_tx_index) { + quota->previous_tx_index = tx_end_index; + slot_use_count = ++quota->slot_use_count; + } else { + slot_use_count = 0; + } + + spin_unlock(&state->quota_spinlock); + + if (slot_use_count) + dev_dbg(state->dev, "core: %d: qm:%d %s,%zx - slot_use->%d (hdr %p)\n", + state->id, service->localport, msg_type_str(VCHIQ_MSG_TYPE(msgid)), + size, slot_use_count, header); + + VCHIQ_SERVICE_STATS_INC(service, ctrl_tx_count); + VCHIQ_SERVICE_STATS_ADD(service, ctrl_tx_bytes, size); + } else { + dev_dbg(state->dev, "core: %d: qm %s@%p,%zx (%d->%d)\n", + state->id, msg_type_str(VCHIQ_MSG_TYPE(msgid)), header, size, + VCHIQ_MSG_SRCPORT(msgid), VCHIQ_MSG_DSTPORT(msgid)); + if (size != 0) { + /* + * It is assumed for now that this code path + * only happens from calls inside this file. + * + * External callers are through the vchiq_queue_message + * path which always sets the type to be VCHIQ_MSG_DATA + * + * At first glance this appears to be correct but + * more review is needed. + */ + copy_message_data(copy_callback, context, + header->data, size); + } + VCHIQ_STATS_INC(state, ctrl_tx_count); + } + + header->msgid = msgid; + header->size = size; + + svc_fourcc = service ? service->base.fourcc + : VCHIQ_MAKE_FOURCC('?', '?', '?', '?'); + + dev_dbg(state->dev, "core_msg: Sent Msg %s(%u) to %p4cc s:%u d:%d len:%zu\n", + msg_type_str(VCHIQ_MSG_TYPE(msgid)), + VCHIQ_MSG_TYPE(msgid), &svc_fourcc, + VCHIQ_MSG_SRCPORT(msgid), VCHIQ_MSG_DSTPORT(msgid), size); + + /* Make sure the new header is visible to the peer. */ + wmb(); + + /* Make the new tx_pos visible to the peer. */ + local->tx_pos = state->local_tx_pos; + wmb(); + + if (service && (type == VCHIQ_MSG_CLOSE)) + set_service_state(service, VCHIQ_SRVSTATE_CLOSESENT); + + if (!(flags & QMFLAGS_NO_MUTEX_UNLOCK)) + mutex_unlock(&state->slot_mutex); + + remote_event_signal(state, &state->remote->trigger); + + return 0; +} + +/* Called by the slot handler and application threads */ +static int +queue_message_sync(struct vchiq_state *state, struct vchiq_service *service, + int msgid, + ssize_t (*copy_callback)(void *context, void *dest, + size_t offset, size_t maxsize), + void *context, int size) +{ + struct vchiq_shared_state *local; + struct vchiq_header *header; + ssize_t callback_result; + int svc_fourcc; + int ret; + + local = state->local; + + if (VCHIQ_MSG_TYPE(msgid) != VCHIQ_MSG_RESUME && + mutex_lock_killable(&state->sync_mutex)) + return -EAGAIN; + + ret = remote_event_wait(&state->sync_release_event, &local->sync_release); + if (ret) + return ret; + + /* Ensure that reads don't overtake the remote_event_wait. */ + rmb(); + + header = (struct vchiq_header *)SLOT_DATA_FROM_INDEX(state, + local->slot_sync); + + { + int oldmsgid = header->msgid; + + if (oldmsgid != VCHIQ_MSGID_PADDING) + dev_err(state->dev, "core: %d: qms - msgid %x, not PADDING\n", + state->id, oldmsgid); + } + + dev_dbg(state->dev, "sync: %d: qms %s@%p,%x (%d->%d)\n", + state->id, msg_type_str(VCHIQ_MSG_TYPE(msgid)), header, size, + VCHIQ_MSG_SRCPORT(msgid), VCHIQ_MSG_DSTPORT(msgid)); + + callback_result = copy_message_data(copy_callback, context, + header->data, size); + + if (callback_result < 0) { + mutex_unlock(&state->slot_mutex); + VCHIQ_SERVICE_STATS_INC(service, error_count); + return -EINVAL; + } + + if (service) { + vchiq_log_dump_mem(state->dev, "Sent", 0, + header->data, + min_t(size_t, 16, callback_result)); + + VCHIQ_SERVICE_STATS_INC(service, ctrl_tx_count); + VCHIQ_SERVICE_STATS_ADD(service, ctrl_tx_bytes, size); + } else { + VCHIQ_STATS_INC(state, ctrl_tx_count); + } + + header->size = size; + header->msgid = msgid; + + svc_fourcc = service ? service->base.fourcc + : VCHIQ_MAKE_FOURCC('?', '?', '?', '?'); + + dev_dbg(state->dev, + "sync: Sent Sync Msg %s(%u) to %p4cc s:%u d:%d len:%d\n", + msg_type_str(VCHIQ_MSG_TYPE(msgid)), VCHIQ_MSG_TYPE(msgid), + &svc_fourcc, VCHIQ_MSG_SRCPORT(msgid), + VCHIQ_MSG_DSTPORT(msgid), size); + + remote_event_signal(state, &state->remote->sync_trigger); + + if (VCHIQ_MSG_TYPE(msgid) != VCHIQ_MSG_PAUSE) + mutex_unlock(&state->sync_mutex); + + return 0; +} + +static inline void +claim_slot(struct vchiq_slot_info *slot) +{ + slot->use_count++; +} + +static void +release_slot(struct vchiq_state *state, struct vchiq_slot_info *slot_info, + struct vchiq_header *header, struct vchiq_service *service) +{ + mutex_lock(&state->recycle_mutex); + + if (header) { + int msgid = header->msgid; + + if (((msgid & VCHIQ_MSGID_CLAIMED) == 0) || (service && service->closing)) { + mutex_unlock(&state->recycle_mutex); + return; + } + + /* Rewrite the message header to prevent a double release */ + header->msgid = msgid & ~VCHIQ_MSGID_CLAIMED; + } + + slot_info->release_count++; + + if (slot_info->release_count == slot_info->use_count) { + int slot_queue_recycle; + /* Add to the freed queue */ + + /* + * A read barrier is necessary here to prevent speculative + * fetches of remote->slot_queue_recycle from overtaking the + * mutex. + */ + rmb(); + + slot_queue_recycle = state->remote->slot_queue_recycle; + state->remote->slot_queue[slot_queue_recycle & + VCHIQ_SLOT_QUEUE_MASK] = + SLOT_INDEX_FROM_INFO(state, slot_info); + state->remote->slot_queue_recycle = slot_queue_recycle + 1; + dev_dbg(state->dev, "core: %d: %d - recycle->%x\n", + state->id, SLOT_INDEX_FROM_INFO(state, slot_info), + state->remote->slot_queue_recycle); + + /* + * A write barrier is necessary, but remote_event_signal + * contains one. + */ + remote_event_signal(state, &state->remote->recycle); + } + + mutex_unlock(&state->recycle_mutex); +} + +static inline enum vchiq_reason +get_bulk_reason(struct vchiq_bulk *bulk) +{ + if (bulk->dir == VCHIQ_BULK_TRANSMIT) { + if (bulk->actual == VCHIQ_BULK_ACTUAL_ABORTED) + return VCHIQ_BULK_TRANSMIT_ABORTED; + + return VCHIQ_BULK_TRANSMIT_DONE; + } + + if (bulk->actual == VCHIQ_BULK_ACTUAL_ABORTED) + return VCHIQ_BULK_RECEIVE_ABORTED; + + return VCHIQ_BULK_RECEIVE_DONE; +} + +static int service_notify_bulk(struct vchiq_service *service, + struct vchiq_bulk *bulk) +{ + if (bulk->actual != VCHIQ_BULK_ACTUAL_ABORTED) { + if (bulk->dir == VCHIQ_BULK_TRANSMIT) { + VCHIQ_SERVICE_STATS_INC(service, bulk_tx_count); + VCHIQ_SERVICE_STATS_ADD(service, bulk_tx_bytes, + bulk->actual); + } else { + VCHIQ_SERVICE_STATS_INC(service, bulk_rx_count); + VCHIQ_SERVICE_STATS_ADD(service, bulk_rx_bytes, + bulk->actual); + } + } else { + VCHIQ_SERVICE_STATS_INC(service, bulk_aborted_count); + } + + if (bulk->mode == VCHIQ_BULK_MODE_BLOCKING) { + struct bulk_waiter *waiter; + + spin_lock(&service->state->bulk_waiter_spinlock); + waiter = bulk->waiter; + if (waiter) { + waiter->actual = bulk->actual; + complete(&waiter->event); + } + spin_unlock(&service->state->bulk_waiter_spinlock); + } else if (bulk->mode == VCHIQ_BULK_MODE_CALLBACK) { + enum vchiq_reason reason = get_bulk_reason(bulk); + + return make_service_callback(service, reason, NULL, bulk); + } + + return 0; +} + +/* Called by the slot handler - don't hold the bulk mutex */ +static int +notify_bulks(struct vchiq_service *service, struct vchiq_bulk_queue *queue, + int retry_poll) +{ + int status = 0; + + dev_dbg(service->state->dev, + "core: %d: nb:%d %cx - p=%x rn=%x r=%x\n", + service->state->id, service->localport, + (queue == &service->bulk_tx) ? 't' : 'r', + queue->process, queue->remote_notify, queue->remove); + + queue->remote_notify = queue->process; + + while (queue->remove != queue->remote_notify) { + struct vchiq_bulk *bulk = + &queue->bulks[BULK_INDEX(queue->remove)]; + + /* + * Only generate callbacks for non-dummy bulk + * requests, and non-terminated services + */ + if (bulk->dma_addr && service->instance) { + status = service_notify_bulk(service, bulk); + if (status == -EAGAIN) + break; + } + + queue->remove++; + complete(&service->bulk_remove_event); + } + if (!retry_poll) + status = 0; + + if (status == -EAGAIN) + request_poll(service->state, service, (queue == &service->bulk_tx) ? + VCHIQ_POLL_TXNOTIFY : VCHIQ_POLL_RXNOTIFY); + + return status; +} + +static void +poll_services_of_group(struct vchiq_state *state, int group) +{ + u32 flags = atomic_xchg(&state->poll_services[group], 0); + int i; + + for (i = 0; flags; i++) { + struct vchiq_service *service; + u32 service_flags; + + if ((flags & BIT(i)) == 0) + continue; + + service = find_service_by_port(state, (group << 5) + i); + flags &= ~BIT(i); + + if (!service) + continue; + + service_flags = atomic_xchg(&service->poll_flags, 0); + if (service_flags & BIT(VCHIQ_POLL_REMOVE)) { + dev_dbg(state->dev, "core: %d: ps - remove %d<->%d\n", + state->id, service->localport, service->remoteport); + + /* + * Make it look like a client, because + * it must be removed and not left in + * the LISTENING state. + */ + service->public_fourcc = VCHIQ_FOURCC_INVALID; + + if (vchiq_close_service_internal(service, NO_CLOSE_RECVD)) + request_poll(state, service, VCHIQ_POLL_REMOVE); + } else if (service_flags & BIT(VCHIQ_POLL_TERMINATE)) { + dev_dbg(state->dev, "core: %d: ps - terminate %d<->%d\n", + state->id, service->localport, service->remoteport); + if (vchiq_close_service_internal(service, NO_CLOSE_RECVD)) + request_poll(state, service, VCHIQ_POLL_TERMINATE); + } + if (service_flags & BIT(VCHIQ_POLL_TXNOTIFY)) + notify_bulks(service, &service->bulk_tx, RETRY_POLL); + if (service_flags & BIT(VCHIQ_POLL_RXNOTIFY)) + notify_bulks(service, &service->bulk_rx, RETRY_POLL); + vchiq_service_put(service); + } +} + +/* Called by the slot handler thread */ +static void +poll_services(struct vchiq_state *state) +{ + int group; + + for (group = 0; group < BITSET_SIZE(state->unused_service); group++) + poll_services_of_group(state, group); +} + +static void +cleanup_pagelistinfo(struct vchiq_instance *instance, struct vchiq_pagelist_info *pagelistinfo) +{ + if (pagelistinfo->scatterlist_mapped) { + dma_unmap_sg(instance->state->dev, pagelistinfo->scatterlist, + pagelistinfo->num_pages, pagelistinfo->dma_dir); + } + + if (pagelistinfo->pages_need_release) + unpin_user_pages(pagelistinfo->pages, pagelistinfo->num_pages); + + dma_free_coherent(instance->state->dev, pagelistinfo->pagelist_buffer_size, + pagelistinfo->pagelist, pagelistinfo->dma_addr); +} + +static inline bool +is_adjacent_block(u32 *addrs, dma_addr_t addr, unsigned int k) +{ + u32 tmp; + + if (!k) + return false; + + tmp = (addrs[k - 1] & PAGE_MASK) + + (((addrs[k - 1] & ~PAGE_MASK) + 1) << PAGE_SHIFT); + + return tmp == (addr & PAGE_MASK); +} + +/* There is a potential problem with partial cache lines (pages?) + * at the ends of the block when reading. If the CPU accessed anything in + * the same line (page?) then it may have pulled old data into the cache, + * obscuring the new data underneath. We can solve this by transferring the + * partial cache lines separately, and allowing the ARM to copy into the + * cached area. + */ +static struct vchiq_pagelist_info * +create_pagelist(struct vchiq_instance *instance, struct vchiq_bulk *bulk) +{ + struct vchiq_drv_mgmt *drv_mgmt; + struct pagelist *pagelist; + struct vchiq_pagelist_info *pagelistinfo; + struct page **pages; + u32 *addrs; + unsigned int num_pages, offset, i, k; + int actual_pages; + size_t pagelist_size; + struct scatterlist *scatterlist, *sg; + int dma_buffers; + unsigned int cache_line_size; + dma_addr_t dma_addr; + size_t count = bulk->size; + unsigned short type = (bulk->dir == VCHIQ_BULK_RECEIVE) + ? PAGELIST_READ : PAGELIST_WRITE; + + if (count >= INT_MAX - PAGE_SIZE) + return NULL; + + drv_mgmt = dev_get_drvdata(instance->state->dev); + + if (bulk->offset) + offset = (uintptr_t)bulk->offset & (PAGE_SIZE - 1); + else + offset = (uintptr_t)bulk->uoffset & (PAGE_SIZE - 1); + num_pages = DIV_ROUND_UP(count + offset, PAGE_SIZE); + + if ((size_t)num_pages > (SIZE_MAX - sizeof(struct pagelist) - + sizeof(struct vchiq_pagelist_info)) / + (sizeof(u32) + sizeof(pages[0]) + + sizeof(struct scatterlist))) + return NULL; + + pagelist_size = sizeof(struct pagelist) + + (num_pages * sizeof(u32)) + + (num_pages * sizeof(pages[0]) + + (num_pages * sizeof(struct scatterlist))) + + sizeof(struct vchiq_pagelist_info); + + /* Allocate enough storage to hold the page pointers and the page + * list + */ + pagelist = dma_alloc_coherent(instance->state->dev, pagelist_size, &dma_addr, + GFP_KERNEL); + + dev_dbg(instance->state->dev, "arm: %p\n", pagelist); + + if (!pagelist) + return NULL; + + addrs = pagelist->addrs; + pages = (struct page **)(addrs + num_pages); + scatterlist = (struct scatterlist *)(pages + num_pages); + pagelistinfo = (struct vchiq_pagelist_info *) + (scatterlist + num_pages); + + pagelist->length = count; + pagelist->type = type; + pagelist->offset = offset; + + /* Populate the fields of the pagelistinfo structure */ + pagelistinfo->pagelist = pagelist; + pagelistinfo->pagelist_buffer_size = pagelist_size; + pagelistinfo->dma_addr = dma_addr; + pagelistinfo->dma_dir = (type == PAGELIST_WRITE) ? + DMA_TO_DEVICE : DMA_FROM_DEVICE; + pagelistinfo->num_pages = num_pages; + pagelistinfo->pages_need_release = 0; + pagelistinfo->pages = pages; + pagelistinfo->scatterlist = scatterlist; + pagelistinfo->scatterlist_mapped = 0; + + if (bulk->offset) { + unsigned long length = count; + unsigned int off = offset; + + for (actual_pages = 0; actual_pages < num_pages; + actual_pages++) { + struct page *pg = + vmalloc_to_page(((unsigned int *)bulk->offset + + (actual_pages * PAGE_SIZE))); + size_t bytes = PAGE_SIZE - off; + + if (!pg) { + cleanup_pagelistinfo(instance, pagelistinfo); + return NULL; + } + + if (bytes > length) + bytes = length; + pages[actual_pages] = pg; + length -= bytes; + off = 0; + } + /* do not try and release vmalloc pages */ + } else { + actual_pages = + pin_user_pages_fast((unsigned long)bulk->uoffset & PAGE_MASK, num_pages, + type == PAGELIST_READ, pages); + + if (actual_pages != num_pages) { + dev_dbg(instance->state->dev, "arm: Only %d/%d pages locked\n", + actual_pages, num_pages); + + /* This is probably due to the process being killed */ + if (actual_pages > 0) + unpin_user_pages(pages, actual_pages); + cleanup_pagelistinfo(instance, pagelistinfo); + return NULL; + } + /* release user pages */ + pagelistinfo->pages_need_release = 1; + } + + /* + * Initialize the scatterlist so that the magic cookie + * is filled if debugging is enabled + */ + sg_init_table(scatterlist, num_pages); + /* Now set the pages for each scatterlist */ + for (i = 0; i < num_pages; i++) { + unsigned int len = PAGE_SIZE - offset; + + if (len > count) + len = count; + sg_set_page(scatterlist + i, pages[i], len, offset); + offset = 0; + count -= len; + } + + dma_buffers = dma_map_sg(instance->state->dev, + scatterlist, + num_pages, + pagelistinfo->dma_dir); + + if (dma_buffers == 0) { + cleanup_pagelistinfo(instance, pagelistinfo); + return NULL; + } + + pagelistinfo->scatterlist_mapped = 1; + + /* Combine adjacent blocks for performance */ + k = 0; + for_each_sg(scatterlist, sg, dma_buffers, i) { + unsigned int len = sg_dma_len(sg); + dma_addr_t addr = sg_dma_address(sg); + + /* Note: addrs is the address + page_count - 1 + * The firmware expects blocks after the first to be page- + * aligned and a multiple of the page size + */ + WARN_ON(len == 0); + WARN_ON(i && (i != (dma_buffers - 1)) && (len & ~PAGE_MASK)); + WARN_ON(i && (addr & ~PAGE_MASK)); + if (is_adjacent_block(addrs, addr, k)) + addrs[k - 1] += ((len + PAGE_SIZE - 1) >> PAGE_SHIFT); + else + addrs[k++] = (addr & PAGE_MASK) | + (((len + PAGE_SIZE - 1) >> PAGE_SHIFT) - 1); + } + + /* Partial cache lines (fragments) require special measures */ + cache_line_size = drv_mgmt->info->cache_line_size; + if ((type == PAGELIST_READ) && + ((pagelist->offset & (cache_line_size - 1)) || + ((pagelist->offset + pagelist->length) & (cache_line_size - 1)))) { + char *fragments; + + if (down_interruptible(&drv_mgmt->free_fragments_sema)) { + cleanup_pagelistinfo(instance, pagelistinfo); + return NULL; + } + + WARN_ON(!drv_mgmt->free_fragments); + + down(&drv_mgmt->free_fragments_mutex); + fragments = drv_mgmt->free_fragments; + WARN_ON(!fragments); + drv_mgmt->free_fragments = *(char **)drv_mgmt->free_fragments; + up(&drv_mgmt->free_fragments_mutex); + pagelist->type = PAGELIST_READ_WITH_FRAGMENTS + + (fragments - drv_mgmt->fragments_base) / drv_mgmt->fragments_size; + } + + return pagelistinfo; +} + +static void +free_pagelist(struct vchiq_instance *instance, struct vchiq_pagelist_info *pagelistinfo, + int actual) +{ + struct vchiq_drv_mgmt *drv_mgmt; + struct pagelist *pagelist = pagelistinfo->pagelist; + struct page **pages = pagelistinfo->pages; + unsigned int num_pages = pagelistinfo->num_pages; + unsigned int cache_line_size; + + dev_dbg(instance->state->dev, "arm: %p, %d\n", pagelistinfo->pagelist, actual); + + drv_mgmt = dev_get_drvdata(instance->state->dev); + + /* + * NOTE: dma_unmap_sg must be called before the + * cpu can touch any of the data/pages. + */ + dma_unmap_sg(instance->state->dev, pagelistinfo->scatterlist, + pagelistinfo->num_pages, pagelistinfo->dma_dir); + pagelistinfo->scatterlist_mapped = 0; + + /* Deal with any partial cache lines (fragments) */ + cache_line_size = drv_mgmt->info->cache_line_size; + if (pagelist->type >= PAGELIST_READ_WITH_FRAGMENTS && drv_mgmt->fragments_base) { + char *fragments = drv_mgmt->fragments_base + + (pagelist->type - PAGELIST_READ_WITH_FRAGMENTS) * + drv_mgmt->fragments_size; + int head_bytes, tail_bytes; + + head_bytes = (cache_line_size - pagelist->offset) & + (cache_line_size - 1); + tail_bytes = (pagelist->offset + actual) & + (cache_line_size - 1); + + if ((actual >= 0) && (head_bytes != 0)) { + if (head_bytes > actual) + head_bytes = actual; + + memcpy_to_page(pages[0], pagelist->offset, + fragments, head_bytes); + } + if ((actual >= 0) && (head_bytes < actual) && + (tail_bytes != 0)) + memcpy_to_page(pages[num_pages - 1], + (pagelist->offset + actual) & + (PAGE_SIZE - 1) & ~(cache_line_size - 1), + fragments + cache_line_size, + tail_bytes); + + down(&drv_mgmt->free_fragments_mutex); + *(char **)fragments = drv_mgmt->free_fragments; + drv_mgmt->free_fragments = fragments; + up(&drv_mgmt->free_fragments_mutex); + up(&drv_mgmt->free_fragments_sema); + } + + /* Need to mark all the pages dirty. */ + if (pagelist->type != PAGELIST_WRITE && + pagelistinfo->pages_need_release) { + unsigned int i; + + for (i = 0; i < num_pages; i++) + set_page_dirty(pages[i]); + } + + cleanup_pagelistinfo(instance, pagelistinfo); +} + +static int +vchiq_prepare_bulk_data(struct vchiq_instance *instance, struct vchiq_bulk *bulk) +{ + struct vchiq_pagelist_info *pagelistinfo; + + pagelistinfo = create_pagelist(instance, bulk); + + if (!pagelistinfo) + return -ENOMEM; + + bulk->dma_addr = pagelistinfo->dma_addr; + + /* + * Store the pagelistinfo address in remote_data, + * which isn't used by the slave. + */ + bulk->remote_data = pagelistinfo; + + return 0; +} + +static void +vchiq_complete_bulk(struct vchiq_instance *instance, struct vchiq_bulk *bulk) +{ + if (bulk && bulk->remote_data && bulk->actual) + free_pagelist(instance, (struct vchiq_pagelist_info *)bulk->remote_data, + bulk->actual); +} + +/* Called with the bulk_mutex held */ +static void +abort_outstanding_bulks(struct vchiq_service *service, + struct vchiq_bulk_queue *queue) +{ + int is_tx = (queue == &service->bulk_tx); + + dev_dbg(service->state->dev, + "core: %d: aob:%d %cx - li=%x ri=%x p=%x\n", + service->state->id, service->localport, + is_tx ? 't' : 'r', queue->local_insert, + queue->remote_insert, queue->process); + + WARN_ON((int)(queue->local_insert - queue->process) < 0); + WARN_ON((int)(queue->remote_insert - queue->process) < 0); + + while ((queue->process != queue->local_insert) || + (queue->process != queue->remote_insert)) { + struct vchiq_bulk *bulk = &queue->bulks[BULK_INDEX(queue->process)]; + + if (queue->process == queue->remote_insert) { + /* fabricate a matching dummy bulk */ + bulk->remote_data = NULL; + bulk->remote_size = 0; + queue->remote_insert++; + } + + if (queue->process != queue->local_insert) { + vchiq_complete_bulk(service->instance, bulk); + + dev_dbg(service->state->dev, + "core_msg: %s %p4cc d:%d ABORTED - tx len:%d, rx len:%d\n", + is_tx ? "Send Bulk to" : "Recv Bulk from", + &service->base.fourcc, + service->remoteport, bulk->size, bulk->remote_size); + } else { + /* fabricate a matching dummy bulk */ + bulk->dma_addr = 0; + bulk->size = 0; + bulk->actual = VCHIQ_BULK_ACTUAL_ABORTED; + bulk->dir = is_tx ? VCHIQ_BULK_TRANSMIT : + VCHIQ_BULK_RECEIVE; + queue->local_insert++; + } + + queue->process++; + } +} + +static int +parse_open(struct vchiq_state *state, struct vchiq_header *header) +{ + const struct vchiq_open_payload *payload; + struct vchiq_openack_payload ack_payload; + struct vchiq_service *service = NULL; + int msgid, size; + int openack_id; + unsigned int localport, remoteport, fourcc; + short version, version_min; + + msgid = header->msgid; + size = header->size; + localport = VCHIQ_MSG_DSTPORT(msgid); + remoteport = VCHIQ_MSG_SRCPORT(msgid); + if (size < sizeof(struct vchiq_open_payload)) + goto fail_open; + + payload = (struct vchiq_open_payload *)header->data; + fourcc = payload->fourcc; + dev_dbg(state->dev, "core: %d: prs OPEN@%p (%d->'%p4cc')\n", + state->id, header, localport, &fourcc); + + service = get_listening_service(state, fourcc); + if (!service) + goto fail_open; + + /* A matching service exists */ + version = payload->version; + version_min = payload->version_min; + + if ((service->version < version_min) || (version < service->version_min)) { + /* Version mismatch */ + dev_err(state->dev, "%d: service %d (%p4cc) version mismatch - local (%d, min %d) vs. remote (%d, min %d)", + state->id, service->localport, &fourcc, + service->version, service->version_min, version, version_min); + vchiq_service_put(service); + service = NULL; + goto fail_open; + } + service->peer_version = version; + + if (service->srvstate != VCHIQ_SRVSTATE_LISTENING) + goto done; + + ack_payload.version = service->version; + openack_id = MAKE_OPENACK(service->localport, remoteport); + + if (state->version_common < VCHIQ_VERSION_SYNCHRONOUS_MODE) + service->sync = 0; + + /* Acknowledge the OPEN */ + if (service->sync) { + if (queue_message_sync(state, NULL, openack_id, + memcpy_copy_callback, + &ack_payload, + sizeof(ack_payload)) == -EAGAIN) + goto bail_not_ready; + + /* The service is now open */ + set_service_state(service, VCHIQ_SRVSTATE_OPENSYNC); + } else { + if (queue_message(state, NULL, openack_id, + memcpy_copy_callback, &ack_payload, + sizeof(ack_payload), 0) == -EINTR) + goto bail_not_ready; + + /* The service is now open */ + set_service_state(service, VCHIQ_SRVSTATE_OPEN); + } + +done: + /* Success - the message has been dealt with */ + vchiq_service_put(service); + return 1; + +fail_open: + /* No available service, or an invalid request - send a CLOSE */ + if (queue_message(state, NULL, MAKE_CLOSE(0, VCHIQ_MSG_SRCPORT(msgid)), + NULL, NULL, 0, 0) == -EINTR) + goto bail_not_ready; + + return 1; + +bail_not_ready: + if (service) + vchiq_service_put(service); + + return 0; +} + +/** + * parse_message() - parses a single message from the rx slot + * @state: vchiq state struct + * @header: message header + * + * Context: Process context + * + * Return: + * * >= 0 - size of the parsed message payload (without header) + * * -EINVAL - fatal error occurred, bail out is required + */ +static int +parse_message(struct vchiq_state *state, struct vchiq_header *header) +{ + struct vchiq_service *service = NULL; + unsigned int localport, remoteport; + int msgid, size, type, ret = -EINVAL; + int svc_fourcc; + + DEBUG_INITIALISE(state->local); + + DEBUG_VALUE(PARSE_HEADER, (int)(long)header); + msgid = header->msgid; + DEBUG_VALUE(PARSE_MSGID, msgid); + size = header->size; + type = VCHIQ_MSG_TYPE(msgid); + localport = VCHIQ_MSG_DSTPORT(msgid); + remoteport = VCHIQ_MSG_SRCPORT(msgid); + + if (type != VCHIQ_MSG_DATA) + VCHIQ_STATS_INC(state, ctrl_rx_count); + + switch (type) { + case VCHIQ_MSG_OPENACK: + case VCHIQ_MSG_CLOSE: + case VCHIQ_MSG_DATA: + case VCHIQ_MSG_BULK_RX: + case VCHIQ_MSG_BULK_TX: + case VCHIQ_MSG_BULK_RX_DONE: + case VCHIQ_MSG_BULK_TX_DONE: + service = find_service_by_port(state, localport); + if ((!service || + ((service->remoteport != remoteport) && + (service->remoteport != VCHIQ_PORT_FREE))) && + (localport == 0) && + (type == VCHIQ_MSG_CLOSE)) { + /* + * This could be a CLOSE from a client which + * hadn't yet received the OPENACK - look for + * the connected service + */ + if (service) + vchiq_service_put(service); + service = get_connected_service(state, remoteport); + if (service) + dev_warn(state->dev, + "core: %d: prs %s@%p (%d->%d) - found connected service %d\n", + state->id, msg_type_str(type), header, + remoteport, localport, service->localport); + } + + if (!service) { + dev_err(state->dev, + "core: %d: prs %s@%p (%d->%d) - invalid/closed service %d\n", + state->id, msg_type_str(type), header, remoteport, + localport, localport); + goto skip_message; + } + break; + default: + break; + } + + svc_fourcc = service ? service->base.fourcc + : VCHIQ_MAKE_FOURCC('?', '?', '?', '?'); + + dev_dbg(state->dev, "core_msg: Rcvd Msg %s(%u) from %p4cc s:%d d:%d len:%d\n", + msg_type_str(type), type, &svc_fourcc, remoteport, localport, size); + if (size > 0) + vchiq_log_dump_mem(state->dev, "Rcvd", 0, header->data, min(16, size)); + + if (((unsigned long)header & VCHIQ_SLOT_MASK) + + calc_stride(size) > VCHIQ_SLOT_SIZE) { + dev_err(state->dev, "core: header %p (msgid %x) - size %x too big for slot\n", + header, (unsigned int)msgid, (unsigned int)size); + WARN(1, "oversized for slot\n"); + } + + switch (type) { + case VCHIQ_MSG_OPEN: + WARN_ON(VCHIQ_MSG_DSTPORT(msgid)); + if (!parse_open(state, header)) + goto bail_not_ready; + break; + case VCHIQ_MSG_OPENACK: + if (size >= sizeof(struct vchiq_openack_payload)) { + const struct vchiq_openack_payload *payload = + (struct vchiq_openack_payload *) + header->data; + service->peer_version = payload->version; + } + dev_dbg(state->dev, + "core: %d: prs OPENACK@%p,%x (%d->%d) v:%d\n", + state->id, header, size, remoteport, localport, + service->peer_version); + if (service->srvstate == VCHIQ_SRVSTATE_OPENING) { + service->remoteport = remoteport; + set_service_state(service, VCHIQ_SRVSTATE_OPEN); + complete(&service->remove_event); + } else { + dev_err(state->dev, "core: OPENACK received in state %s\n", + srvstate_names[service->srvstate]); + } + break; + case VCHIQ_MSG_CLOSE: + WARN_ON(size); /* There should be no data */ + + dev_dbg(state->dev, "core: %d: prs CLOSE@%p (%d->%d)\n", + state->id, header, remoteport, localport); + + mark_service_closing_internal(service, 1); + + if (vchiq_close_service_internal(service, CLOSE_RECVD) == -EAGAIN) + goto bail_not_ready; + + dev_dbg(state->dev, "core: Close Service %p4cc s:%u d:%d\n", + &service->base.fourcc, service->localport, service->remoteport); + break; + case VCHIQ_MSG_DATA: + dev_dbg(state->dev, "core: %d: prs DATA@%p,%x (%d->%d)\n", + state->id, header, size, remoteport, localport); + + if ((service->remoteport == remoteport) && + (service->srvstate == VCHIQ_SRVSTATE_OPEN)) { + header->msgid = msgid | VCHIQ_MSGID_CLAIMED; + claim_slot(state->rx_info); + DEBUG_TRACE(PARSE_LINE); + if (make_service_callback(service, VCHIQ_MESSAGE_AVAILABLE, header, + NULL) == -EAGAIN) { + DEBUG_TRACE(PARSE_LINE); + goto bail_not_ready; + } + VCHIQ_SERVICE_STATS_INC(service, ctrl_rx_count); + VCHIQ_SERVICE_STATS_ADD(service, ctrl_rx_bytes, size); + } else { + VCHIQ_STATS_INC(state, error_count); + } + break; + case VCHIQ_MSG_CONNECT: + dev_dbg(state->dev, "core: %d: prs CONNECT@%p\n", + state->id, header); + state->version_common = ((struct vchiq_slot_zero *) + state->slot_data)->version; + complete(&state->connect); + break; + case VCHIQ_MSG_BULK_RX: + case VCHIQ_MSG_BULK_TX: + /* + * We should never receive a bulk request from the + * other side since we're not setup to perform as the + * master. + */ + WARN_ON(1); + break; + case VCHIQ_MSG_BULK_RX_DONE: + case VCHIQ_MSG_BULK_TX_DONE: + if ((service->remoteport == remoteport) && + (service->srvstate != VCHIQ_SRVSTATE_FREE)) { + struct vchiq_bulk_queue *queue; + struct vchiq_bulk *bulk; + + queue = (type == VCHIQ_MSG_BULK_RX_DONE) ? + &service->bulk_rx : &service->bulk_tx; + + DEBUG_TRACE(PARSE_LINE); + if (mutex_lock_killable(&service->bulk_mutex)) { + DEBUG_TRACE(PARSE_LINE); + goto bail_not_ready; + } + if ((int)(queue->remote_insert - + queue->local_insert) >= 0) { + dev_err(state->dev, + "core: %d: prs %s@%p (%d->%d) unexpected (ri=%d,li=%d)\n", + state->id, msg_type_str(type), header, remoteport, + localport, queue->remote_insert, queue->local_insert); + mutex_unlock(&service->bulk_mutex); + break; + } + if (queue->process != queue->remote_insert) { + dev_err(state->dev, "%s: p %x != ri %x\n", + __func__, queue->process, + queue->remote_insert); + mutex_unlock(&service->bulk_mutex); + goto bail_not_ready; + } + + bulk = &queue->bulks[BULK_INDEX(queue->remote_insert)]; + bulk->actual = *(int *)header->data; + queue->remote_insert++; + + dev_dbg(state->dev, "core: %d: prs %s@%p (%d->%d) %x@%pad\n", + state->id, msg_type_str(type), header, remoteport, + localport, bulk->actual, &bulk->dma_addr); + + dev_dbg(state->dev, "core: %d: prs:%d %cx li=%x ri=%x p=%x\n", + state->id, localport, + (type == VCHIQ_MSG_BULK_RX_DONE) ? 'r' : 't', + queue->local_insert, queue->remote_insert, queue->process); + + DEBUG_TRACE(PARSE_LINE); + WARN_ON(queue->process == queue->local_insert); + vchiq_complete_bulk(service->instance, bulk); + queue->process++; + mutex_unlock(&service->bulk_mutex); + DEBUG_TRACE(PARSE_LINE); + notify_bulks(service, queue, RETRY_POLL); + DEBUG_TRACE(PARSE_LINE); + } + break; + case VCHIQ_MSG_PADDING: + dev_dbg(state->dev, "core: %d: prs PADDING@%p,%x\n", + state->id, header, size); + break; + case VCHIQ_MSG_PAUSE: + /* If initiated, signal the application thread */ + dev_dbg(state->dev, "core: %d: prs PAUSE@%p,%x\n", + state->id, header, size); + if (state->conn_state == VCHIQ_CONNSTATE_PAUSED) { + dev_err(state->dev, "core: %d: PAUSE received in state PAUSED\n", + state->id); + break; + } + if (state->conn_state != VCHIQ_CONNSTATE_PAUSE_SENT) { + /* Send a PAUSE in response */ + if (queue_message(state, NULL, MAKE_PAUSE, NULL, NULL, 0, + QMFLAGS_NO_MUTEX_UNLOCK) == -EINTR) + goto bail_not_ready; + } + /* At this point slot_mutex is held */ + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_PAUSED); + break; + case VCHIQ_MSG_RESUME: + dev_dbg(state->dev, "core: %d: prs RESUME@%p,%x\n", + state->id, header, size); + /* Release the slot mutex */ + mutex_unlock(&state->slot_mutex); + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_CONNECTED); + break; + + case VCHIQ_MSG_REMOTE_USE: + vchiq_on_remote_use(state); + break; + case VCHIQ_MSG_REMOTE_RELEASE: + vchiq_on_remote_release(state); + break; + case VCHIQ_MSG_REMOTE_USE_ACTIVE: + break; + + default: + dev_err(state->dev, "core: %d: prs invalid msgid %x@%p,%x\n", + state->id, msgid, header, size); + WARN(1, "invalid message\n"); + break; + } + +skip_message: + ret = size; + +bail_not_ready: + if (service) + vchiq_service_put(service); + + return ret; +} + +/* Called by the slot handler thread */ +static void +parse_rx_slots(struct vchiq_state *state) +{ + struct vchiq_shared_state *remote = state->remote; + int tx_pos; + + DEBUG_INITIALISE(state->local); + + tx_pos = remote->tx_pos; + + while (state->rx_pos != tx_pos) { + struct vchiq_header *header; + int size; + + DEBUG_TRACE(PARSE_LINE); + if (!state->rx_data) { + int rx_index; + + WARN_ON(state->rx_pos & VCHIQ_SLOT_MASK); + rx_index = remote->slot_queue[ + SLOT_QUEUE_INDEX_FROM_POS_MASKED(state->rx_pos)]; + state->rx_data = (char *)SLOT_DATA_FROM_INDEX(state, + rx_index); + state->rx_info = SLOT_INFO_FROM_INDEX(state, rx_index); + + /* + * Initialise use_count to one, and increment + * release_count at the end of the slot to avoid + * releasing the slot prematurely. + */ + state->rx_info->use_count = 1; + state->rx_info->release_count = 0; + } + + header = (struct vchiq_header *)(state->rx_data + + (state->rx_pos & VCHIQ_SLOT_MASK)); + size = parse_message(state, header); + if (size < 0) + return; + + state->rx_pos += calc_stride(size); + + DEBUG_TRACE(PARSE_LINE); + /* + * Perform some housekeeping when the end of the slot is + * reached. + */ + if ((state->rx_pos & VCHIQ_SLOT_MASK) == 0) { + /* Remove the extra reference count. */ + release_slot(state, state->rx_info, NULL, NULL); + state->rx_data = NULL; + } + } +} + +/** + * handle_poll() - handle service polling and other rare conditions + * @state: vchiq state struct + * + * Context: Process context + * + * Return: + * * 0 - poll handled successful + * * -EAGAIN - retry later + */ +static int +handle_poll(struct vchiq_state *state) +{ + switch (state->conn_state) { + case VCHIQ_CONNSTATE_CONNECTED: + /* Poll the services as requested */ + poll_services(state); + break; + + case VCHIQ_CONNSTATE_PAUSING: + if (queue_message(state, NULL, MAKE_PAUSE, NULL, NULL, 0, + QMFLAGS_NO_MUTEX_UNLOCK) != -EINTR) { + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_PAUSE_SENT); + } else { + /* Retry later */ + return -EAGAIN; + } + break; + + case VCHIQ_CONNSTATE_RESUMING: + if (queue_message(state, NULL, MAKE_RESUME, NULL, NULL, 0, + QMFLAGS_NO_MUTEX_LOCK) != -EINTR) { + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_CONNECTED); + } else { + /* + * This should really be impossible, + * since the PAUSE should have flushed + * through outstanding messages. + */ + dev_err(state->dev, "core: Failed to send RESUME message\n"); + } + break; + default: + break; + } + + return 0; +} + +/* Called by the slot handler thread */ +static int +slot_handler_func(void *v) +{ + struct vchiq_state *state = v; + struct vchiq_shared_state *local = state->local; + int ret; + + DEBUG_INITIALISE(local); + + while (!kthread_should_stop()) { + DEBUG_COUNT(SLOT_HANDLER_COUNT); + DEBUG_TRACE(SLOT_HANDLER_LINE); + ret = remote_event_wait(&state->trigger_event, &local->trigger); + if (ret) + return ret; + + /* Ensure that reads don't overtake the remote_event_wait. */ + rmb(); + + DEBUG_TRACE(SLOT_HANDLER_LINE); + if (state->poll_needed) { + state->poll_needed = 0; + + /* + * Handle service polling and other rare conditions here + * out of the mainline code + */ + if (handle_poll(state) == -EAGAIN) + state->poll_needed = 1; + } + + DEBUG_TRACE(SLOT_HANDLER_LINE); + parse_rx_slots(state); + } + return 0; +} + +/* Called by the recycle thread */ +static int +recycle_func(void *v) +{ + struct vchiq_state *state = v; + struct vchiq_shared_state *local = state->local; + u32 *found; + size_t length; + int ret; + + length = sizeof(*found) * BITSET_SIZE(VCHIQ_MAX_SERVICES); + + found = kmalloc_array(BITSET_SIZE(VCHIQ_MAX_SERVICES), sizeof(*found), + GFP_KERNEL); + if (!found) + return -ENOMEM; + + while (!kthread_should_stop()) { + ret = remote_event_wait(&state->recycle_event, &local->recycle); + if (ret) + return ret; + + process_free_queue(state, found, length); + } + return 0; +} + +/* Called by the sync thread */ +static int +sync_func(void *v) +{ + struct vchiq_state *state = v; + struct vchiq_shared_state *local = state->local; + struct vchiq_header *header = + (struct vchiq_header *)SLOT_DATA_FROM_INDEX(state, + state->remote->slot_sync); + int svc_fourcc; + int ret; + + while (!kthread_should_stop()) { + struct vchiq_service *service; + int msgid, size; + int type; + unsigned int localport, remoteport; + + ret = remote_event_wait(&state->sync_trigger_event, &local->sync_trigger); + if (ret) + return ret; + + /* Ensure that reads don't overtake the remote_event_wait. */ + rmb(); + + msgid = header->msgid; + size = header->size; + type = VCHIQ_MSG_TYPE(msgid); + localport = VCHIQ_MSG_DSTPORT(msgid); + remoteport = VCHIQ_MSG_SRCPORT(msgid); + + service = find_service_by_port(state, localport); + + if (!service) { + dev_err(state->dev, + "sync: %d: sf %s@%p (%d->%d) - invalid/closed service %d\n", + state->id, msg_type_str(type), header, remoteport, + localport, localport); + release_message_sync(state, header); + continue; + } + + svc_fourcc = service->base.fourcc; + + dev_dbg(state->dev, "sync: Rcvd Msg %s from %p4cc s:%d d:%d len:%d\n", + msg_type_str(type), &svc_fourcc, remoteport, localport, size); + if (size > 0) + vchiq_log_dump_mem(state->dev, "Rcvd", 0, header->data, min(16, size)); + + switch (type) { + case VCHIQ_MSG_OPENACK: + if (size >= sizeof(struct vchiq_openack_payload)) { + const struct vchiq_openack_payload *payload = + (struct vchiq_openack_payload *) + header->data; + service->peer_version = payload->version; + } + dev_err(state->dev, "sync: %d: sf OPENACK@%p,%x (%d->%d) v:%d\n", + state->id, header, size, remoteport, localport, + service->peer_version); + if (service->srvstate == VCHIQ_SRVSTATE_OPENING) { + service->remoteport = remoteport; + set_service_state(service, VCHIQ_SRVSTATE_OPENSYNC); + service->sync = 1; + complete(&service->remove_event); + } + release_message_sync(state, header); + break; + + case VCHIQ_MSG_DATA: + dev_dbg(state->dev, "sync: %d: sf DATA@%p,%x (%d->%d)\n", + state->id, header, size, remoteport, localport); + + if ((service->remoteport == remoteport) && + (service->srvstate == VCHIQ_SRVSTATE_OPENSYNC)) { + if (make_service_callback(service, VCHIQ_MESSAGE_AVAILABLE, header, + NULL) == -EAGAIN) + dev_err(state->dev, + "sync: error: synchronous callback to service %d returns -EAGAIN\n", + localport); + } + break; + + default: + dev_err(state->dev, "sync: error: %d: sf unexpected msgid %x@%p,%x\n", + state->id, msgid, header, size); + release_message_sync(state, header); + break; + } + + vchiq_service_put(service); + } + + return 0; +} + +inline const char * +get_conn_state_name(enum vchiq_connstate conn_state) +{ + return conn_state_names[conn_state]; +} + +struct vchiq_slot_zero * +vchiq_init_slots(struct device *dev, void *mem_base, int mem_size) +{ + int mem_align = + (int)((VCHIQ_SLOT_SIZE - (long)mem_base) & VCHIQ_SLOT_MASK); + struct vchiq_slot_zero *slot_zero = + (struct vchiq_slot_zero *)(mem_base + mem_align); + int num_slots = (mem_size - mem_align) / VCHIQ_SLOT_SIZE; + int first_data_slot = VCHIQ_SLOT_ZERO_SLOTS; + + check_sizes(); + + /* Ensure there is enough memory to run an absolutely minimum system */ + num_slots -= first_data_slot; + + if (num_slots < 4) { + dev_err(dev, "core: %s: Insufficient memory %x bytes\n", + __func__, mem_size); + return NULL; + } + + memset(slot_zero, 0, sizeof(struct vchiq_slot_zero)); + + slot_zero->magic = VCHIQ_MAGIC; + slot_zero->version = VCHIQ_VERSION; + slot_zero->version_min = VCHIQ_VERSION_MIN; + slot_zero->slot_zero_size = sizeof(struct vchiq_slot_zero); + slot_zero->slot_size = VCHIQ_SLOT_SIZE; + slot_zero->max_slots = VCHIQ_MAX_SLOTS; + slot_zero->max_slots_per_side = VCHIQ_MAX_SLOTS_PER_SIDE; + + slot_zero->master.slot_sync = first_data_slot; + slot_zero->master.slot_first = first_data_slot + 1; + slot_zero->master.slot_last = first_data_slot + (num_slots / 2) - 1; + slot_zero->slave.slot_sync = first_data_slot + (num_slots / 2); + slot_zero->slave.slot_first = first_data_slot + (num_slots / 2) + 1; + slot_zero->slave.slot_last = first_data_slot + num_slots - 1; + + return slot_zero; +} + +int +vchiq_init_state(struct vchiq_state *state, struct vchiq_slot_zero *slot_zero, struct device *dev) +{ + struct vchiq_shared_state *local; + struct vchiq_shared_state *remote; + char threadname[16]; + int i, ret; + + local = &slot_zero->slave; + remote = &slot_zero->master; + + if (local->initialised) { + if (remote->initialised) + dev_err(dev, "local state has already been initialised\n"); + else + dev_err(dev, "master/slave mismatch two slaves\n"); + + return -EINVAL; + } + + memset(state, 0, sizeof(struct vchiq_state)); + + state->dev = dev; + + /* + * initialize shared state pointers + */ + + state->local = local; + state->remote = remote; + state->slot_data = (struct vchiq_slot *)slot_zero; + + /* + * initialize events and mutexes + */ + + init_completion(&state->connect); + mutex_init(&state->mutex); + mutex_init(&state->slot_mutex); + mutex_init(&state->recycle_mutex); + mutex_init(&state->sync_mutex); + + spin_lock_init(&state->msg_queue_spinlock); + spin_lock_init(&state->bulk_waiter_spinlock); + spin_lock_init(&state->quota_spinlock); + + init_completion(&state->slot_available_event); + init_completion(&state->data_quota_event); + + state->slot_queue_available = 0; + + for (i = 0; i < VCHIQ_MAX_SERVICES; i++) { + struct vchiq_service_quota *quota = &state->service_quotas[i]; + + init_completion("a->quota_event); + } + + for (i = local->slot_first; i <= local->slot_last; i++) { + local->slot_queue[state->slot_queue_available] = i; + state->slot_queue_available++; + complete(&state->slot_available_event); + } + + state->default_slot_quota = state->slot_queue_available / 2; + state->default_message_quota = + min_t(unsigned short, state->default_slot_quota * 256, ~0); + + state->previous_data_index = -1; + state->data_use_count = 0; + state->data_quota = state->slot_queue_available - 1; + + remote_event_create(&state->trigger_event, &local->trigger); + local->tx_pos = 0; + remote_event_create(&state->recycle_event, &local->recycle); + local->slot_queue_recycle = state->slot_queue_available; + remote_event_create(&state->sync_trigger_event, &local->sync_trigger); + remote_event_create(&state->sync_release_event, &local->sync_release); + + /* At start-of-day, the slot is empty and available */ + ((struct vchiq_header *) + SLOT_DATA_FROM_INDEX(state, local->slot_sync))->msgid = + VCHIQ_MSGID_PADDING; + remote_event_signal_local(&state->sync_release_event, &local->sync_release); + + local->debug[DEBUG_ENTRIES] = DEBUG_MAX; + + ret = vchiq_platform_init_state(state); + if (ret) + return ret; + + /* + * bring up slot handler thread + */ + snprintf(threadname, sizeof(threadname), "vchiq-slot/%d", state->id); + state->slot_handler_thread = kthread_create(&slot_handler_func, (void *)state, threadname); + + if (IS_ERR(state->slot_handler_thread)) { + dev_err(state->dev, "couldn't create thread %s\n", threadname); + return PTR_ERR(state->slot_handler_thread); + } + set_user_nice(state->slot_handler_thread, -19); + + snprintf(threadname, sizeof(threadname), "vchiq-recy/%d", state->id); + state->recycle_thread = kthread_create(&recycle_func, (void *)state, threadname); + if (IS_ERR(state->recycle_thread)) { + dev_err(state->dev, "couldn't create thread %s\n", threadname); + ret = PTR_ERR(state->recycle_thread); + goto fail_free_handler_thread; + } + set_user_nice(state->recycle_thread, -19); + + snprintf(threadname, sizeof(threadname), "vchiq-sync/%d", state->id); + state->sync_thread = kthread_create(&sync_func, (void *)state, threadname); + if (IS_ERR(state->sync_thread)) { + dev_err(state->dev, "couldn't create thread %s\n", threadname); + ret = PTR_ERR(state->sync_thread); + goto fail_free_recycle_thread; + } + set_user_nice(state->sync_thread, -20); + + wake_up_process(state->slot_handler_thread); + wake_up_process(state->recycle_thread); + wake_up_process(state->sync_thread); + + /* Indicate readiness to the other side */ + local->initialised = 1; + + return 0; + +fail_free_recycle_thread: + kthread_stop(state->recycle_thread); +fail_free_handler_thread: + kthread_stop(state->slot_handler_thread); + + return ret; +} + +void vchiq_msg_queue_push(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_header *header) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int pos; + + if (!service) + return; + + while (service->msg_queue_write == service->msg_queue_read + + VCHIQ_MAX_SLOTS) { + if (wait_for_completion_interruptible(&service->msg_queue_pop)) + flush_signals(current); + } + + pos = service->msg_queue_write & (VCHIQ_MAX_SLOTS - 1); + service->msg_queue_write++; + service->msg_queue[pos] = header; + + complete(&service->msg_queue_push); +} +EXPORT_SYMBOL(vchiq_msg_queue_push); + +struct vchiq_header *vchiq_msg_hold(struct vchiq_instance *instance, unsigned int handle) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + struct vchiq_header *header; + int pos; + + if (!service) + return NULL; + + if (service->msg_queue_write == service->msg_queue_read) + return NULL; + + while (service->msg_queue_write == service->msg_queue_read) { + if (wait_for_completion_interruptible(&service->msg_queue_push)) + flush_signals(current); + } + + pos = service->msg_queue_read & (VCHIQ_MAX_SLOTS - 1); + service->msg_queue_read++; + header = service->msg_queue[pos]; + + complete(&service->msg_queue_pop); + + return header; +} +EXPORT_SYMBOL(vchiq_msg_hold); + +static int vchiq_validate_params(struct vchiq_state *state, + const struct vchiq_service_params_kernel *params) +{ + if (!params->callback || !params->fourcc) { + dev_err(state->dev, "Can't add service, invalid params\n"); + return -EINVAL; + } + + return 0; +} + +/* Called from application thread when a client or server service is created. */ +struct vchiq_service * +vchiq_add_service_internal(struct vchiq_state *state, + const struct vchiq_service_params_kernel *params, + int srvstate, struct vchiq_instance *instance, + void (*userdata_term)(void *userdata)) +{ + struct vchiq_service *service; + struct vchiq_service __rcu **pservice = NULL; + struct vchiq_service_quota *quota; + int ret; + int i; + + ret = vchiq_validate_params(state, params); + if (ret) + return NULL; + + service = kzalloc_obj(*service); + if (!service) + return service; + + service->base.fourcc = params->fourcc; + service->base.callback = params->callback; + service->base.userdata = params->userdata; + service->handle = VCHIQ_SERVICE_HANDLE_INVALID; + kref_init(&service->ref_count); + service->srvstate = VCHIQ_SRVSTATE_FREE; + service->userdata_term = userdata_term; + service->localport = VCHIQ_PORT_FREE; + service->remoteport = VCHIQ_PORT_FREE; + + service->public_fourcc = (srvstate == VCHIQ_SRVSTATE_OPENING) ? + VCHIQ_FOURCC_INVALID : params->fourcc; + service->auto_close = 1; + atomic_set(&service->poll_flags, 0); + service->version = params->version; + service->version_min = params->version_min; + service->state = state; + service->instance = instance; + init_completion(&service->remove_event); + init_completion(&service->bulk_remove_event); + init_completion(&service->msg_queue_pop); + init_completion(&service->msg_queue_push); + mutex_init(&service->bulk_mutex); + + /* + * Although it is perfectly possible to use a spinlock + * to protect the creation of services, it is overkill as it + * disables interrupts while the array is searched. + * The only danger is of another thread trying to create a + * service - service deletion is safe. + * Therefore it is preferable to use state->mutex which, + * although slower to claim, doesn't block interrupts while + * it is held. + */ + + mutex_lock(&state->mutex); + + /* Prepare to use a previously unused service */ + if (state->unused_service < VCHIQ_MAX_SERVICES) + pservice = &state->services[state->unused_service]; + + if (srvstate == VCHIQ_SRVSTATE_OPENING) { + for (i = 0; i < state->unused_service; i++) { + if (!rcu_access_pointer(state->services[i])) { + pservice = &state->services[i]; + break; + } + } + } else { + rcu_read_lock(); + for (i = (state->unused_service - 1); i >= 0; i--) { + struct vchiq_service *srv; + + srv = rcu_dereference(state->services[i]); + if (!srv) { + pservice = &state->services[i]; + } else if ((srv->public_fourcc == params->fourcc) && + ((srv->instance != instance) || + (srv->base.callback != params->callback))) { + /* + * There is another server using this + * fourcc which doesn't match. + */ + pservice = NULL; + break; + } + } + rcu_read_unlock(); + } + + if (pservice) { + service->localport = (pservice - state->services); + if (!handle_seq) + handle_seq = VCHIQ_MAX_STATES * + VCHIQ_MAX_SERVICES; + service->handle = handle_seq | + (state->id * VCHIQ_MAX_SERVICES) | + service->localport; + handle_seq += VCHIQ_MAX_STATES * VCHIQ_MAX_SERVICES; + rcu_assign_pointer(*pservice, service); + if (pservice == &state->services[state->unused_service]) + state->unused_service++; + } + + mutex_unlock(&state->mutex); + + if (!pservice) { + kfree(service); + return NULL; + } + + quota = &state->service_quotas[service->localport]; + quota->slot_quota = state->default_slot_quota; + quota->message_quota = state->default_message_quota; + if (quota->slot_use_count == 0) + quota->previous_tx_index = + SLOT_QUEUE_INDEX_FROM_POS(state->local_tx_pos) + - 1; + + /* Bring this service online */ + set_service_state(service, srvstate); + + dev_dbg(state->dev, "core_msg: %s Service %p4cc SrcPort:%d\n", + (srvstate == VCHIQ_SRVSTATE_OPENING) ? "Open" : "Add", + ¶ms->fourcc, service->localport); + + /* Don't unlock the service - leave it with a ref_count of 1. */ + + return service; +} + +int +vchiq_open_service_internal(struct vchiq_service *service, int client_id) +{ + struct vchiq_open_payload payload = { + service->base.fourcc, + client_id, + service->version, + service->version_min + }; + int status = 0; + + service->client_id = client_id; + vchiq_use_service_internal(service); + status = queue_message(service->state, + NULL, MAKE_OPEN(service->localport), + memcpy_copy_callback, + &payload, + sizeof(payload), + QMFLAGS_IS_BLOCKING); + + if (status) + return status; + + /* Wait for the ACK/NAK */ + if (wait_for_completion_interruptible(&service->remove_event)) { + status = -EAGAIN; + vchiq_release_service_internal(service); + } else if ((service->srvstate != VCHIQ_SRVSTATE_OPEN) && + (service->srvstate != VCHIQ_SRVSTATE_OPENSYNC)) { + if (service->srvstate != VCHIQ_SRVSTATE_CLOSEWAIT) + dev_err(service->state->dev, + "core: %d: osi - srvstate = %s (ref %u)\n", + service->state->id, srvstate_names[service->srvstate], + kref_read(&service->ref_count)); + status = -EINVAL; + VCHIQ_SERVICE_STATS_INC(service, error_count); + vchiq_release_service_internal(service); + } + + return status; +} + +static void +release_service_messages(struct vchiq_service *service) +{ + struct vchiq_state *state = service->state; + int slot_last = state->remote->slot_last; + int i; + + /* Release any claimed messages aimed at this service */ + + if (service->sync) { + struct vchiq_header *header = + (struct vchiq_header *)SLOT_DATA_FROM_INDEX(state, + state->remote->slot_sync); + if (VCHIQ_MSG_DSTPORT(header->msgid) == service->localport) + release_message_sync(state, header); + + return; + } + + for (i = state->remote->slot_first; i <= slot_last; i++) { + struct vchiq_slot_info *slot_info = + SLOT_INFO_FROM_INDEX(state, i); + unsigned int pos, end; + char *data; + + if (slot_info->release_count == slot_info->use_count) + continue; + + data = (char *)SLOT_DATA_FROM_INDEX(state, i); + end = VCHIQ_SLOT_SIZE; + if (data == state->rx_data) + /* + * This buffer is still being read from - stop + * at the current read position + */ + end = state->rx_pos & VCHIQ_SLOT_MASK; + + pos = 0; + + while (pos < end) { + struct vchiq_header *header = + (struct vchiq_header *)(data + pos); + int msgid = header->msgid; + int port = VCHIQ_MSG_DSTPORT(msgid); + + if ((port == service->localport) && (msgid & VCHIQ_MSGID_CLAIMED)) { + dev_dbg(state->dev, "core: fsi - hdr %p\n", header); + release_slot(state, slot_info, header, NULL); + } + pos += calc_stride(header->size); + if (pos > VCHIQ_SLOT_SIZE) { + dev_err(state->dev, + "core: fsi - pos %x: header %p, msgid %x, header->msgid %x, header->size %x\n", + pos, header, msgid, header->msgid, header->size); + WARN(1, "invalid slot position\n"); + } + } + } +} + +static int +do_abort_bulks(struct vchiq_service *service) +{ + int status; + + /* Abort any outstanding bulk transfers */ + if (mutex_lock_killable(&service->bulk_mutex)) + return 0; + abort_outstanding_bulks(service, &service->bulk_tx); + abort_outstanding_bulks(service, &service->bulk_rx); + mutex_unlock(&service->bulk_mutex); + + status = notify_bulks(service, &service->bulk_tx, NO_RETRY_POLL); + if (status) + return 0; + + status = notify_bulks(service, &service->bulk_rx, NO_RETRY_POLL); + return !status; +} + +static int +close_service_complete(struct vchiq_service *service, int failstate) +{ + int status; + int is_server = (service->public_fourcc != VCHIQ_FOURCC_INVALID); + int newstate; + + switch (service->srvstate) { + case VCHIQ_SRVSTATE_OPEN: + case VCHIQ_SRVSTATE_CLOSESENT: + case VCHIQ_SRVSTATE_CLOSERECVD: + if (is_server) { + if (service->auto_close) { + service->client_id = 0; + service->remoteport = VCHIQ_PORT_FREE; + newstate = VCHIQ_SRVSTATE_LISTENING; + } else { + newstate = VCHIQ_SRVSTATE_CLOSEWAIT; + } + } else { + newstate = VCHIQ_SRVSTATE_CLOSED; + } + set_service_state(service, newstate); + break; + case VCHIQ_SRVSTATE_LISTENING: + break; + default: + dev_err(service->state->dev, "core: (%x) called in state %s\n", + service->handle, srvstate_names[service->srvstate]); + WARN(1, "%s in unexpected state\n", __func__); + return -EINVAL; + } + + status = make_service_callback(service, VCHIQ_SERVICE_CLOSED, NULL, NULL); + + if (status != -EAGAIN) { + int uc = service->service_use_count; + int i; + /* Complete the close process */ + for (i = 0; i < uc; i++) + /* + * cater for cases where close is forced and the + * client may not close all it's handles + */ + vchiq_release_service_internal(service); + + service->client_id = 0; + service->remoteport = VCHIQ_PORT_FREE; + + if (service->srvstate == VCHIQ_SRVSTATE_CLOSED) { + vchiq_free_service_internal(service); + } else if (service->srvstate != VCHIQ_SRVSTATE_CLOSEWAIT) { + if (is_server) + service->closing = 0; + + complete(&service->remove_event); + } + } else { + set_service_state(service, failstate); + } + + return status; +} + +/* + * Prepares a bulk transfer to be queued. The function is interruptible and is + * intended to be called from user threads. It may return -EAGAIN to indicate + * that a signal has been received and the call should be retried after being + * returned to user context. + */ +static int +vchiq_bulk_xfer_queue_msg_killable(struct vchiq_service *service, + struct vchiq_bulk *bulk_params) +{ + struct vchiq_bulk_queue *queue; + struct bulk_waiter *bulk_waiter = NULL; + struct vchiq_bulk *bulk; + struct vchiq_state *state = service->state; + const char dir_char = (bulk_params->dir == VCHIQ_BULK_TRANSMIT) ? 't' : 'r'; + const int dir_msgtype = (bulk_params->dir == VCHIQ_BULK_TRANSMIT) ? + VCHIQ_MSG_BULK_TX : VCHIQ_MSG_BULK_RX; + int status = -EINVAL; + int payload[2]; + + if (bulk_params->mode == VCHIQ_BULK_MODE_BLOCKING) { + bulk_waiter = bulk_params->waiter; + init_completion(&bulk_waiter->event); + bulk_waiter->actual = 0; + bulk_waiter->bulk = NULL; + } + + queue = (bulk_params->dir == VCHIQ_BULK_TRANSMIT) ? + &service->bulk_tx : &service->bulk_rx; + + if (mutex_lock_killable(&service->bulk_mutex)) + return -EINTR; + + if (queue->local_insert == queue->remove + VCHIQ_NUM_SERVICE_BULKS) { + VCHIQ_SERVICE_STATS_INC(service, bulk_stalls); + do { + mutex_unlock(&service->bulk_mutex); + if (wait_for_completion_killable(&service->bulk_remove_event)) + return -EINTR; + if (mutex_lock_killable(&service->bulk_mutex)) + return -EINTR; + } while (queue->local_insert == queue->remove + + VCHIQ_NUM_SERVICE_BULKS); + } + + bulk = &queue->bulks[BULK_INDEX(queue->local_insert)]; + + /* Initiliaze the 'bulk' slot with bulk parameters passed in. */ + bulk->mode = bulk_params->mode; + bulk->dir = bulk_params->dir; + bulk->waiter = bulk_params->waiter; + bulk->cb_data = bulk_params->cb_data; + bulk->cb_userdata = bulk_params->cb_userdata; + bulk->size = bulk_params->size; + bulk->offset = bulk_params->offset; + bulk->uoffset = bulk_params->uoffset; + bulk->actual = VCHIQ_BULK_ACTUAL_ABORTED; + + if (vchiq_prepare_bulk_data(service->instance, bulk)) + goto unlock_error_exit; + + /* + * Ensure that the bulk data record is visible to the peer + * before proceeding. + */ + wmb(); + + dev_dbg(state->dev, "core: %d: bt (%d->%d) %cx %x@%pad %p\n", + state->id, service->localport, service->remoteport, + dir_char, bulk->size, &bulk->dma_addr, bulk->cb_data); + + /* + * The slot mutex must be held when the service is being closed, so + * claim it here to ensure that isn't happening + */ + if (mutex_lock_killable(&state->slot_mutex)) { + status = -EINTR; + goto cancel_bulk_error_exit; + } + + if (service->srvstate != VCHIQ_SRVSTATE_OPEN) + goto unlock_both_error_exit; + + payload[0] = lower_32_bits(bulk->dma_addr); + payload[1] = bulk->size; + status = queue_message(state, + NULL, + VCHIQ_MAKE_MSG(dir_msgtype, + service->localport, + service->remoteport), + memcpy_copy_callback, + &payload, + sizeof(payload), + QMFLAGS_IS_BLOCKING | + QMFLAGS_NO_MUTEX_LOCK | + QMFLAGS_NO_MUTEX_UNLOCK); + if (status) + goto unlock_both_error_exit; + + queue->local_insert++; + + mutex_unlock(&state->slot_mutex); + mutex_unlock(&service->bulk_mutex); + + dev_dbg(state->dev, "core: %d: bt:%d %cx li=%x ri=%x p=%x\n", + state->id, service->localport, dir_char, queue->local_insert, + queue->remote_insert, queue->process); + + if (bulk_waiter) { + bulk_waiter->bulk = bulk; + if (wait_for_completion_killable(&bulk_waiter->event)) + status = -EINTR; + else if (bulk_waiter->actual == VCHIQ_BULK_ACTUAL_ABORTED) + status = -EINVAL; + } + + return status; + +unlock_both_error_exit: + mutex_unlock(&state->slot_mutex); +cancel_bulk_error_exit: + vchiq_complete_bulk(service->instance, bulk); +unlock_error_exit: + mutex_unlock(&service->bulk_mutex); + + return status; +} + +/* Called by the slot handler */ +int +vchiq_close_service_internal(struct vchiq_service *service, int close_recvd) +{ + struct vchiq_state *state = service->state; + int status = 0; + int is_server = (service->public_fourcc != VCHIQ_FOURCC_INVALID); + int close_id = MAKE_CLOSE(service->localport, + VCHIQ_MSG_DSTPORT(service->remoteport)); + + dev_dbg(state->dev, "core: %d: csi:%d,%d (%s)\n", + service->state->id, service->localport, close_recvd, + srvstate_names[service->srvstate]); + + switch (service->srvstate) { + case VCHIQ_SRVSTATE_CLOSED: + case VCHIQ_SRVSTATE_HIDDEN: + case VCHIQ_SRVSTATE_LISTENING: + case VCHIQ_SRVSTATE_CLOSEWAIT: + if (close_recvd) { + dev_err(state->dev, "core: (1) called in state %s\n", + srvstate_names[service->srvstate]); + break; + } else if (!is_server) { + vchiq_free_service_internal(service); + break; + } + + if (service->srvstate == VCHIQ_SRVSTATE_LISTENING) { + status = -EINVAL; + } else { + service->client_id = 0; + service->remoteport = VCHIQ_PORT_FREE; + if (service->srvstate == VCHIQ_SRVSTATE_CLOSEWAIT) + set_service_state(service, VCHIQ_SRVSTATE_LISTENING); + } + complete(&service->remove_event); + break; + case VCHIQ_SRVSTATE_OPENING: + if (close_recvd) { + /* The open was rejected - tell the user */ + set_service_state(service, VCHIQ_SRVSTATE_CLOSEWAIT); + complete(&service->remove_event); + } else { + /* Shutdown mid-open - let the other side know */ + status = queue_message(state, service, close_id, NULL, NULL, 0, 0); + } + break; + + case VCHIQ_SRVSTATE_OPENSYNC: + mutex_lock(&state->sync_mutex); + fallthrough; + case VCHIQ_SRVSTATE_OPEN: + if (close_recvd) { + if (!do_abort_bulks(service)) + status = -EAGAIN; + } + + release_service_messages(service); + + if (!status) + status = queue_message(state, service, close_id, NULL, + NULL, 0, QMFLAGS_NO_MUTEX_UNLOCK); + + if (status) { + if (service->srvstate == VCHIQ_SRVSTATE_OPENSYNC) + mutex_unlock(&state->sync_mutex); + break; + } + + if (!close_recvd) { + /* Change the state while the mutex is still held */ + set_service_state(service, VCHIQ_SRVSTATE_CLOSESENT); + mutex_unlock(&state->slot_mutex); + if (service->sync) + mutex_unlock(&state->sync_mutex); + break; + } + + /* Change the state while the mutex is still held */ + set_service_state(service, VCHIQ_SRVSTATE_CLOSERECVD); + mutex_unlock(&state->slot_mutex); + if (service->sync) + mutex_unlock(&state->sync_mutex); + + status = close_service_complete(service, VCHIQ_SRVSTATE_CLOSERECVD); + break; + + case VCHIQ_SRVSTATE_CLOSESENT: + if (!close_recvd) + /* This happens when a process is killed mid-close */ + break; + + if (!do_abort_bulks(service)) { + status = -EAGAIN; + break; + } + + if (!status) + status = close_service_complete(service, VCHIQ_SRVSTATE_CLOSERECVD); + break; + + case VCHIQ_SRVSTATE_CLOSERECVD: + if (!close_recvd && is_server) + /* Force into LISTENING mode */ + set_service_state(service, VCHIQ_SRVSTATE_LISTENING); + status = close_service_complete(service, VCHIQ_SRVSTATE_CLOSERECVD); + break; + + default: + dev_err(state->dev, "core: (%d) called in state %s\n", + close_recvd, srvstate_names[service->srvstate]); + break; + } + + return status; +} + +/* Called from the application process upon process death */ +void +vchiq_terminate_service_internal(struct vchiq_service *service) +{ + struct vchiq_state *state = service->state; + + dev_dbg(state->dev, "core: %d: tsi - (%d<->%d)\n", + state->id, service->localport, service->remoteport); + + mark_service_closing(service); + + /* Mark the service for removal by the slot handler */ + request_poll(state, service, VCHIQ_POLL_REMOVE); +} + +/* Called from the slot handler */ +void +vchiq_free_service_internal(struct vchiq_service *service) +{ + struct vchiq_state *state = service->state; + + dev_dbg(state->dev, "core: %d: fsi - (%d)\n", state->id, service->localport); + + switch (service->srvstate) { + case VCHIQ_SRVSTATE_OPENING: + case VCHIQ_SRVSTATE_CLOSED: + case VCHIQ_SRVSTATE_HIDDEN: + case VCHIQ_SRVSTATE_LISTENING: + case VCHIQ_SRVSTATE_CLOSEWAIT: + break; + default: + dev_err(state->dev, "core: %d: fsi - (%d) in state %s\n", + state->id, service->localport, srvstate_names[service->srvstate]); + return; + } + + set_service_state(service, VCHIQ_SRVSTATE_FREE); + + complete(&service->remove_event); + + /* Release the initial lock */ + vchiq_service_put(service); +} + +int +vchiq_connect_internal(struct vchiq_state *state, struct vchiq_instance *instance) +{ + struct vchiq_service *service; + int status = 0; + int i; + + /* Find all services registered to this client and enable them. */ + i = 0; + while ((service = next_service_by_instance(state, instance, &i)) != NULL) { + if (service->srvstate == VCHIQ_SRVSTATE_HIDDEN) + set_service_state(service, VCHIQ_SRVSTATE_LISTENING); + vchiq_service_put(service); + } + + if (state->conn_state == VCHIQ_CONNSTATE_DISCONNECTED) { + status = queue_message(state, NULL, MAKE_CONNECT, NULL, NULL, 0, + QMFLAGS_IS_BLOCKING); + if (status) + return status; + + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_CONNECTING); + } + + if (state->conn_state == VCHIQ_CONNSTATE_CONNECTING) { + if (wait_for_completion_interruptible(&state->connect)) + return -EAGAIN; + + vchiq_set_conn_state(state, VCHIQ_CONNSTATE_CONNECTED); + complete(&state->connect); + } + + return status; +} + +void +vchiq_shutdown_internal(struct vchiq_state *state, struct vchiq_instance *instance) +{ + struct vchiq_service *service; + int i; + + /* Find all services registered to this client and remove them. */ + i = 0; + while ((service = next_service_by_instance(state, instance, &i)) != NULL) { + (void)vchiq_remove_service(instance, service->handle); + vchiq_service_put(service); + } +} + +int +vchiq_close_service(struct vchiq_instance *instance, unsigned int handle) +{ + /* Unregister the service */ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int status = 0; + + if (!service) + return -EINVAL; + + dev_dbg(service->state->dev, "core: %d: close_service:%d\n", + service->state->id, service->localport); + + if ((service->srvstate == VCHIQ_SRVSTATE_FREE) || + (service->srvstate == VCHIQ_SRVSTATE_LISTENING) || + (service->srvstate == VCHIQ_SRVSTATE_HIDDEN)) { + vchiq_service_put(service); + return -EINVAL; + } + + mark_service_closing(service); + + if (current == service->state->slot_handler_thread) { + status = vchiq_close_service_internal(service, NO_CLOSE_RECVD); + WARN_ON(status == -EAGAIN); + } else { + /* Mark the service for termination by the slot handler */ + request_poll(service->state, service, VCHIQ_POLL_TERMINATE); + } + + while (1) { + if (wait_for_completion_interruptible(&service->remove_event)) { + status = -EAGAIN; + break; + } + + if ((service->srvstate == VCHIQ_SRVSTATE_FREE) || + (service->srvstate == VCHIQ_SRVSTATE_LISTENING) || + (service->srvstate == VCHIQ_SRVSTATE_OPEN)) + break; + + dev_warn(service->state->dev, + "core: %d: close_service:%d - waiting in state %s\n", + service->state->id, service->localport, + srvstate_names[service->srvstate]); + } + + if (!status && + (service->srvstate != VCHIQ_SRVSTATE_FREE) && + (service->srvstate != VCHIQ_SRVSTATE_LISTENING)) + status = -EINVAL; + + vchiq_service_put(service); + + return status; +} +EXPORT_SYMBOL(vchiq_close_service); + +int +vchiq_remove_service(struct vchiq_instance *instance, unsigned int handle) +{ + /* Unregister the service */ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int status = 0; + + if (!service) + return -EINVAL; + + dev_dbg(service->state->dev, "core: %d: remove_service:%d\n", + service->state->id, service->localport); + + if (service->srvstate == VCHIQ_SRVSTATE_FREE) { + vchiq_service_put(service); + return -EINVAL; + } + + mark_service_closing(service); + + if ((service->srvstate == VCHIQ_SRVSTATE_HIDDEN) || + (current == service->state->slot_handler_thread)) { + /* + * Make it look like a client, because it must be removed and + * not left in the LISTENING state. + */ + service->public_fourcc = VCHIQ_FOURCC_INVALID; + + status = vchiq_close_service_internal(service, NO_CLOSE_RECVD); + WARN_ON(status == -EAGAIN); + } else { + /* Mark the service for removal by the slot handler */ + request_poll(service->state, service, VCHIQ_POLL_REMOVE); + } + while (1) { + if (wait_for_completion_interruptible(&service->remove_event)) { + status = -EAGAIN; + break; + } + + if ((service->srvstate == VCHIQ_SRVSTATE_FREE) || + (service->srvstate == VCHIQ_SRVSTATE_OPEN)) + break; + + dev_warn(service->state->dev, + "core: %d: remove_service:%d - waiting in state %s\n", + service->state->id, service->localport, + srvstate_names[service->srvstate]); + } + + if (!status && (service->srvstate != VCHIQ_SRVSTATE_FREE)) + status = -EINVAL; + + vchiq_service_put(service); + + return status; +} + +int +vchiq_bulk_xfer_blocking(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_bulk *bulk_params) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int status = -EINVAL; + + if (!service) + return -EINVAL; + + if (service->srvstate != VCHIQ_SRVSTATE_OPEN) + goto error_exit; + + if (!bulk_params->offset && !bulk_params->uoffset) + goto error_exit; + + if (vchiq_check_service(service)) + goto error_exit; + + status = vchiq_bulk_xfer_queue_msg_killable(service, bulk_params); + +error_exit: + vchiq_service_put(service); + + return status; +} + +int +vchiq_bulk_xfer_callback(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_bulk *bulk_params) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int status = -EINVAL; + + if (!service) + return -EINVAL; + + if (bulk_params->mode != VCHIQ_BULK_MODE_CALLBACK && + bulk_params->mode != VCHIQ_BULK_MODE_NOCALLBACK) + goto error_exit; + + if (service->srvstate != VCHIQ_SRVSTATE_OPEN) + goto error_exit; + + if (!bulk_params->offset && !bulk_params->uoffset) + goto error_exit; + + if (vchiq_check_service(service)) + goto error_exit; + + status = vchiq_bulk_xfer_queue_msg_killable(service, bulk_params); + +error_exit: + vchiq_service_put(service); + + return status; +} + +/* + * This function is called by VCHIQ ioctl interface and is interruptible. + * It may receive -EAGAIN to indicate that a signal has been received + * and the call should be retried after being returned to user context. + */ +int +vchiq_bulk_xfer_waiting(struct vchiq_instance *instance, + unsigned int handle, struct bulk_waiter *waiter) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + struct bulk_waiter *bulk_waiter; + int status = -EINVAL; + + if (!service) + return -EINVAL; + + if (!waiter) + goto error_exit; + + if (service->srvstate != VCHIQ_SRVSTATE_OPEN) + goto error_exit; + + if (vchiq_check_service(service)) + goto error_exit; + + bulk_waiter = waiter; + + vchiq_service_put(service); + + status = 0; + + if (wait_for_completion_killable(&bulk_waiter->event)) + return -EINTR; + else if (bulk_waiter->actual == VCHIQ_BULK_ACTUAL_ABORTED) + return -EINVAL; + + return status; + +error_exit: + vchiq_service_put(service); + + return status; +} + +int +vchiq_queue_message(struct vchiq_instance *instance, unsigned int handle, + ssize_t (*copy_callback)(void *context, void *dest, + size_t offset, size_t maxsize), + void *context, + size_t size) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + int status = -EINVAL; + int data_id; + + if (!service) + goto error_exit; + + if (vchiq_check_service(service)) + goto error_exit; + + if (!size) { + VCHIQ_SERVICE_STATS_INC(service, error_count); + goto error_exit; + } + + if (size > VCHIQ_MAX_MSG_SIZE) { + VCHIQ_SERVICE_STATS_INC(service, error_count); + goto error_exit; + } + + data_id = MAKE_DATA(service->localport, service->remoteport); + + switch (service->srvstate) { + case VCHIQ_SRVSTATE_OPEN: + status = queue_message(service->state, service, data_id, + copy_callback, context, size, + QMFLAGS_IS_BLOCKING); + break; + case VCHIQ_SRVSTATE_OPENSYNC: + status = queue_message_sync(service->state, service, data_id, + copy_callback, context, size); + break; + default: + status = -EINVAL; + break; + } + +error_exit: + if (service) + vchiq_service_put(service); + + return status; +} + +int vchiq_queue_kernel_message(struct vchiq_instance *instance, unsigned int handle, void *data, + unsigned int size) +{ + return vchiq_queue_message(instance, handle, memcpy_copy_callback, + data, size); +} +EXPORT_SYMBOL(vchiq_queue_kernel_message); + +void +vchiq_release_message(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_header *header) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + struct vchiq_shared_state *remote; + struct vchiq_state *state; + int slot_index; + + if (!service) + return; + + state = service->state; + remote = state->remote; + + slot_index = SLOT_INDEX_FROM_DATA(state, (void *)header); + + if ((slot_index >= remote->slot_first) && + (slot_index <= remote->slot_last)) { + int msgid = header->msgid; + + if (msgid & VCHIQ_MSGID_CLAIMED) { + struct vchiq_slot_info *slot_info = + SLOT_INFO_FROM_INDEX(state, slot_index); + + release_slot(state, slot_info, header, service); + } + } else if (slot_index == remote->slot_sync) { + release_message_sync(state, header); + } + + vchiq_service_put(service); +} +EXPORT_SYMBOL(vchiq_release_message); + +static void +release_message_sync(struct vchiq_state *state, struct vchiq_header *header) +{ + header->msgid = VCHIQ_MSGID_PADDING; + remote_event_signal(state, &state->remote->sync_release); +} + +int +vchiq_get_peer_version(struct vchiq_instance *instance, unsigned int handle, short *peer_version) +{ + int status = -EINVAL; + struct vchiq_service *service = find_service_by_handle(instance, handle); + + if (!service) + goto exit; + + if (vchiq_check_service(service)) + goto exit; + + if (!peer_version) + goto exit; + + *peer_version = service->peer_version; + status = 0; + +exit: + if (service) + vchiq_service_put(service); + return status; +} +EXPORT_SYMBOL(vchiq_get_peer_version); + +void vchiq_get_config(struct vchiq_config *config) +{ + config->max_msg_size = VCHIQ_MAX_MSG_SIZE; + config->bulk_threshold = VCHIQ_MAX_MSG_SIZE; + config->max_outstanding_bulks = VCHIQ_NUM_SERVICE_BULKS; + config->max_services = VCHIQ_MAX_SERVICES; + config->version = VCHIQ_VERSION; + config->version_min = VCHIQ_VERSION_MIN; +} + +int +vchiq_set_service_option(struct vchiq_instance *instance, unsigned int handle, + enum vchiq_service_option option, int value) +{ + struct vchiq_service *service = find_service_by_handle(instance, handle); + struct vchiq_service_quota *quota; + int ret = -EINVAL; + + if (!service) + return -EINVAL; + + switch (option) { + case VCHIQ_SERVICE_OPTION_AUTOCLOSE: + service->auto_close = value; + ret = 0; + break; + + case VCHIQ_SERVICE_OPTION_SLOT_QUOTA: + quota = &service->state->service_quotas[service->localport]; + if (value == 0) + value = service->state->default_slot_quota; + if ((value >= quota->slot_use_count) && + (value < (unsigned short)~0)) { + quota->slot_quota = value; + if ((value >= quota->slot_use_count) && + (quota->message_quota >= quota->message_use_count)) + /* + * Signal the service that it may have + * dropped below its quota + */ + complete("a->quota_event); + ret = 0; + } + break; + + case VCHIQ_SERVICE_OPTION_MESSAGE_QUOTA: + quota = &service->state->service_quotas[service->localport]; + if (value == 0) + value = service->state->default_message_quota; + if ((value >= quota->message_use_count) && + (value < (unsigned short)~0)) { + quota->message_quota = value; + if ((value >= quota->message_use_count) && + (quota->slot_quota >= quota->slot_use_count)) + /* + * Signal the service that it may have + * dropped below its quota + */ + complete("a->quota_event); + ret = 0; + } + break; + + case VCHIQ_SERVICE_OPTION_SYNCHRONOUS: + if ((service->srvstate == VCHIQ_SRVSTATE_HIDDEN) || + (service->srvstate == VCHIQ_SRVSTATE_LISTENING)) { + service->sync = value; + ret = 0; + } + break; + + case VCHIQ_SERVICE_OPTION_TRACE: + service->trace = value; + ret = 0; + break; + + default: + break; + } + vchiq_service_put(service); + + return ret; +} + +static void +vchiq_dump_shared_state(struct seq_file *f, struct vchiq_state *state, + struct vchiq_shared_state *shared, const char *label) +{ + static const char *const debug_names[] = { + "", + "SLOT_HANDLER_COUNT", + "SLOT_HANDLER_LINE", + "PARSE_LINE", + "PARSE_HEADER", + "PARSE_MSGID", + "AWAIT_COMPLETION_LINE", + "DEQUEUE_MESSAGE_LINE", + "SERVICE_CALLBACK_LINE", + "MSG_QUEUE_FULL_COUNT", + "COMPLETION_QUEUE_FULL_COUNT" + }; + int i; + + seq_printf(f, " %s: slots %d-%d tx_pos=0x%x recycle=0x%x\n", + label, shared->slot_first, shared->slot_last, + shared->tx_pos, shared->slot_queue_recycle); + + seq_puts(f, " Slots claimed:\n"); + + for (i = shared->slot_first; i <= shared->slot_last; i++) { + struct vchiq_slot_info slot_info = + *SLOT_INFO_FROM_INDEX(state, i); + if (slot_info.use_count != slot_info.release_count) { + seq_printf(f, " %d: %d/%d\n", i, slot_info.use_count, + slot_info.release_count); + } + } + + for (i = 1; i < shared->debug[DEBUG_ENTRIES]; i++) { + seq_printf(f, " DEBUG: %s = %d(0x%x)\n", + debug_names[i], shared->debug[i], shared->debug[i]); + } +} + +static void +vchiq_dump_service_state(struct seq_file *f, struct vchiq_service *service) +{ + unsigned int ref_count; + + /*Don't include the lock just taken*/ + ref_count = kref_read(&service->ref_count) - 1; + seq_printf(f, "Service %u: %s (ref %u)", service->localport, + srvstate_names[service->srvstate], ref_count); + + if (service->srvstate != VCHIQ_SRVSTATE_FREE) { + char remoteport[30]; + struct vchiq_service_quota *quota = + &service->state->service_quotas[service->localport]; + int fourcc = service->base.fourcc; + int tx_pending, rx_pending, tx_size = 0, rx_size = 0; + + if (service->remoteport != VCHIQ_PORT_FREE) { + int len2 = scnprintf(remoteport, sizeof(remoteport), + "%u", service->remoteport); + + if (service->public_fourcc != VCHIQ_FOURCC_INVALID) + scnprintf(remoteport + len2, sizeof(remoteport) - len2, + " (client 0x%x)", service->client_id); + } else { + strscpy(remoteport, "n/a", sizeof(remoteport)); + } + + seq_printf(f, " '%p4cc' remote %s (msg use %d/%d, slot use %d/%d)\n", + &fourcc, remoteport, + quota->message_use_count, quota->message_quota, + quota->slot_use_count, quota->slot_quota); + + tx_pending = service->bulk_tx.local_insert - + service->bulk_tx.remote_insert; + if (tx_pending) { + unsigned int i = BULK_INDEX(service->bulk_tx.remove); + + tx_size = service->bulk_tx.bulks[i].size; + } + + rx_pending = service->bulk_rx.local_insert - + service->bulk_rx.remote_insert; + if (rx_pending) { + unsigned int i = BULK_INDEX(service->bulk_rx.remove); + + rx_size = service->bulk_rx.bulks[i].size; + } + + seq_printf(f, " Bulk: tx_pending=%d (size %d), rx_pending=%d (size %d)\n", + tx_pending, tx_size, rx_pending, rx_size); + + if (VCHIQ_ENABLE_STATS) { + seq_printf(f, " Ctrl: tx_count=%d, tx_bytes=%llu, rx_count=%d, rx_bytes=%llu\n", + service->stats.ctrl_tx_count, + service->stats.ctrl_tx_bytes, + service->stats.ctrl_rx_count, + service->stats.ctrl_rx_bytes); + + seq_printf(f, " Bulk: tx_count=%d, tx_bytes=%llu, rx_count=%d, rx_bytes=%llu\n", + service->stats.bulk_tx_count, + service->stats.bulk_tx_bytes, + service->stats.bulk_rx_count, + service->stats.bulk_rx_bytes); + + seq_printf(f, " %d quota stalls, %d slot stalls, %d bulk stalls, %d aborted, %d errors\n", + service->stats.quota_stalls, + service->stats.slot_stalls, + service->stats.bulk_stalls, + service->stats.bulk_aborted_count, + service->stats.error_count); + } + } + + vchiq_dump_platform_service_state(f, service); +} + +void vchiq_dump_state(struct seq_file *f, struct vchiq_state *state) +{ + int i; + + seq_printf(f, "State %d: %s\n", state->id, + conn_state_names[state->conn_state]); + + seq_printf(f, " tx_pos=0x%x(@%pK), rx_pos=0x%x(@%pK)\n", + state->local->tx_pos, + state->tx_data + (state->local_tx_pos & VCHIQ_SLOT_MASK), + state->rx_pos, + state->rx_data + (state->rx_pos & VCHIQ_SLOT_MASK)); + + seq_printf(f, " Version: %d (min %d)\n", VCHIQ_VERSION, + VCHIQ_VERSION_MIN); + + if (VCHIQ_ENABLE_STATS) { + seq_printf(f, " Stats: ctrl_tx_count=%d, ctrl_rx_count=%d, error_count=%d\n", + state->stats.ctrl_tx_count, state->stats.ctrl_rx_count, + state->stats.error_count); + } + + seq_printf(f, " Slots: %d available (%d data), %d recyclable, %d stalls (%d data)\n", + ((state->slot_queue_available * VCHIQ_SLOT_SIZE) - + state->local_tx_pos) / VCHIQ_SLOT_SIZE, + state->data_quota - state->data_use_count, + state->local->slot_queue_recycle - state->slot_queue_available, + state->stats.slot_stalls, state->stats.data_stalls); + + vchiq_dump_platform_state(f); + + vchiq_dump_shared_state(f, state, state->local, "Local"); + + vchiq_dump_shared_state(f, state, state->remote, "Remote"); + + vchiq_dump_platform_instances(state, f); + + for (i = 0; i < state->unused_service; i++) { + struct vchiq_service *service = find_service_by_port(state, i); + + if (service) { + vchiq_dump_service_state(f, service); + vchiq_service_put(service); + } + } +} + +int vchiq_send_remote_use(struct vchiq_state *state) +{ + if (state->conn_state == VCHIQ_CONNSTATE_DISCONNECTED) + return -ENOTCONN; + + return queue_message(state, NULL, MAKE_REMOTE_USE, NULL, NULL, 0, 0); +} + +int vchiq_send_remote_use_active(struct vchiq_state *state) +{ + if (state->conn_state == VCHIQ_CONNSTATE_DISCONNECTED) + return -ENOTCONN; + + return queue_message(state, NULL, MAKE_REMOTE_USE_ACTIVE, + NULL, NULL, 0, 0); +} + +void vchiq_log_dump_mem(struct device *dev, const char *label, u32 addr, + const void *void_mem, size_t num_bytes) +{ + const u8 *mem = void_mem; + size_t offset; + char line_buf[100]; + char *s; + + while (num_bytes > 0) { + s = line_buf; + + for (offset = 0; offset < 16; offset++) { + if (offset < num_bytes) + s += scnprintf(s, 4, "%02x ", mem[offset]); + else + s += scnprintf(s, 4, " "); + } + + for (offset = 0; offset < 16; offset++) { + if (offset < num_bytes) { + u8 ch = mem[offset]; + + if ((ch < ' ') || (ch > '~')) + ch = '.'; + *s++ = (char)ch; + } + } + *s++ = '\0'; + + dev_dbg(dev, "core: %s: %08x: %s\n", label, addr, line_buf); + + addr += 16; + mem += 16; + if (num_bytes > 16) + num_bytes -= 16; + else + num_bytes = 0; + } +} diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_debugfs.c b/drivers/platform/raspberrypi/vchiq-interface/vchiq_debugfs.c new file mode 100644 index 000000000..c82326a9b --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_debugfs.c @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +/* + * Copyright (c) 2014 Raspberry Pi (Trading) Ltd. All rights reserved. + * Copyright (c) 2010-2012 Broadcom. All rights reserved. + */ + +#include +#include +#include +#include + +#ifdef CONFIG_DEBUG_FS + +#define DEBUGFS_WRITE_BUF_SIZE 256 + +/* Global 'vchiq' debugfs and clients entry used by all instances */ +static struct dentry *vchiq_dbg_dir; +static struct dentry *vchiq_dbg_clients; + +static int debugfs_usecount_show(struct seq_file *f, void *offset) +{ + struct vchiq_instance *instance = f->private; + int use_count; + + use_count = vchiq_instance_get_use_count(instance); + seq_printf(f, "%d\n", use_count); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(debugfs_usecount); + +static int debugfs_trace_show(struct seq_file *f, void *offset) +{ + struct vchiq_instance *instance = f->private; + int trace; + + trace = vchiq_instance_get_trace(instance); + seq_printf(f, "%s\n", trace ? "Y" : "N"); + + return 0; +} + +static int vchiq_dump_show(struct seq_file *f, void *offset) +{ + struct vchiq_state *state = f->private; + + vchiq_dump_state(f, state); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(vchiq_dump); + +static int debugfs_trace_open(struct inode *inode, struct file *file) +{ + return single_open(file, debugfs_trace_show, inode->i_private); +} + +static ssize_t debugfs_trace_write(struct file *file, + const char __user *buffer, + size_t count, loff_t *ppos) +{ + struct seq_file *f = (struct seq_file *)file->private_data; + struct vchiq_instance *instance = f->private; + char firstchar; + + if (copy_from_user(&firstchar, buffer, 1)) + return -EFAULT; + + switch (firstchar) { + case 'Y': + case 'y': + case '1': + vchiq_instance_set_trace(instance, 1); + break; + case 'N': + case 'n': + case '0': + vchiq_instance_set_trace(instance, 0); + break; + default: + break; + } + + *ppos += count; + + return count; +} + +static const struct file_operations debugfs_trace_fops = { + .owner = THIS_MODULE, + .open = debugfs_trace_open, + .write = debugfs_trace_write, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +/* add an instance (process) to the debugfs entries */ +void vchiq_debugfs_add_instance(struct vchiq_instance *instance) +{ + char pidstr[16]; + struct dentry *top; + + snprintf(pidstr, sizeof(pidstr), "%d", + vchiq_instance_get_pid(instance)); + + top = debugfs_create_dir(pidstr, vchiq_dbg_clients); + + debugfs_create_file("use_count", 0444, top, instance, + &debugfs_usecount_fops); + debugfs_create_file("trace", 0644, top, instance, &debugfs_trace_fops); + + vchiq_instance_get_debugfs_node(instance)->dentry = top; +} + +void vchiq_debugfs_remove_instance(struct vchiq_instance *instance) +{ + struct vchiq_debugfs_node *node = + vchiq_instance_get_debugfs_node(instance); + + debugfs_remove_recursive(node->dentry); +} + +void vchiq_debugfs_init(struct vchiq_state *state) +{ + vchiq_dbg_dir = debugfs_create_dir("vchiq", NULL); + vchiq_dbg_clients = debugfs_create_dir("clients", vchiq_dbg_dir); + + debugfs_create_file("state", S_IFREG | 0444, vchiq_dbg_dir, state, + &vchiq_dump_fops); +} + +/* remove all the debugfs entries */ +void vchiq_debugfs_deinit(void) +{ + debugfs_remove_recursive(vchiq_dbg_dir); +} + +#else /* CONFIG_DEBUG_FS */ + +void vchiq_debugfs_init(struct vchiq_state *state) +{ +} + +void vchiq_debugfs_deinit(void) +{ +} + +void vchiq_debugfs_add_instance(struct vchiq_instance *instance) +{ +} + +void vchiq_debugfs_remove_instance(struct vchiq_instance *instance) +{ +} + +#endif /* CONFIG_DEBUG_FS */ diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_dev.c b/drivers/platform/raspberrypi/vchiq-interface/vchiq_dev.c new file mode 100644 index 000000000..f1c9c0d4b --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_dev.c @@ -0,0 +1,1355 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +/* + * Copyright (c) 2014 Raspberry Pi (Trading) Ltd. All rights reserved. + * Copyright (c) 2010-2012 Broadcom. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "vchiq_ioctl.h" + +static const char *const ioctl_names[] = { + "CONNECT", + "SHUTDOWN", + "CREATE_SERVICE", + "REMOVE_SERVICE", + "QUEUE_MESSAGE", + "QUEUE_BULK_TRANSMIT", + "QUEUE_BULK_RECEIVE", + "AWAIT_COMPLETION", + "DEQUEUE_MESSAGE", + "GET_CLIENT_ID", + "GET_CONFIG", + "CLOSE_SERVICE", + "USE_SERVICE", + "RELEASE_SERVICE", + "SET_SERVICE_OPTION", + "DUMP_PHYS_MEM", + "LIB_VERSION", + "CLOSE_DELIVERED" +}; + +static_assert(ARRAY_SIZE(ioctl_names) == (VCHIQ_IOC_MAX + 1)); + +static void +user_service_free(void *userdata) +{ + kfree(userdata); +} + +static void close_delivered(struct user_service *user_service) +{ + dev_dbg(user_service->service->state->dev, + "arm: (handle=%x)\n", user_service->service->handle); + + if (user_service->close_pending) { + /* Allow the underlying service to be culled */ + vchiq_service_put(user_service->service); + + /* Wake the user-thread blocked in close_ or remove_service */ + complete(&user_service->close_event); + + user_service->close_pending = 0; + } +} + +struct vchiq_io_copy_callback_context { + struct vchiq_element *element; + size_t element_offset; + unsigned long elements_to_go; +}; + +static ssize_t vchiq_ioc_copy_element_data(void *context, void *dest, + size_t offset, size_t maxsize) +{ + struct vchiq_io_copy_callback_context *cc = context; + size_t total_bytes_copied = 0; + size_t bytes_this_round; + + while (total_bytes_copied < maxsize) { + if (!cc->elements_to_go) + return total_bytes_copied; + + if (!cc->element->size) { + cc->elements_to_go--; + cc->element++; + cc->element_offset = 0; + continue; + } + + bytes_this_round = min(cc->element->size - cc->element_offset, + maxsize - total_bytes_copied); + + if (copy_from_user(dest + total_bytes_copied, + cc->element->data + cc->element_offset, + bytes_this_round)) + return -EFAULT; + + cc->element_offset += bytes_this_round; + total_bytes_copied += bytes_this_round; + + if (cc->element_offset == cc->element->size) { + cc->elements_to_go--; + cc->element++; + cc->element_offset = 0; + } + } + + return maxsize; +} + +static int +vchiq_ioc_queue_message(struct vchiq_instance *instance, unsigned int handle, + struct vchiq_element *elements, unsigned long count) +{ + struct vchiq_io_copy_callback_context context; + int status = 0; + unsigned long i; + size_t total_size = 0; + + context.element = elements; + context.element_offset = 0; + context.elements_to_go = count; + + for (i = 0; i < count; i++) { + if (!elements[i].data && elements[i].size != 0) + return -EFAULT; + + total_size += elements[i].size; + } + + status = vchiq_queue_message(instance, handle, vchiq_ioc_copy_element_data, + &context, total_size); + + if (status == -EINVAL) + return -EIO; + else if (status == -EAGAIN) + return -EINTR; + return 0; +} + +static int vchiq_ioc_create_service(struct vchiq_instance *instance, + struct vchiq_create_service *args) +{ + struct user_service *user_service = NULL; + struct vchiq_service *service; + int status = 0; + struct vchiq_service_params_kernel params; + int srvstate; + + if (args->is_open && !instance->connected) + return -ENOTCONN; + + user_service = kmalloc_obj(*user_service); + if (!user_service) + return -ENOMEM; + + if (args->is_open) { + srvstate = VCHIQ_SRVSTATE_OPENING; + } else { + srvstate = instance->connected ? + VCHIQ_SRVSTATE_LISTENING : VCHIQ_SRVSTATE_HIDDEN; + } + + params = (struct vchiq_service_params_kernel) { + .fourcc = args->params.fourcc, + .callback = service_callback, + .userdata = user_service, + .version = args->params.version, + .version_min = args->params.version_min, + }; + service = vchiq_add_service_internal(instance->state, ¶ms, + srvstate, instance, + user_service_free); + if (!service) { + kfree(user_service); + return -EEXIST; + } + + user_service->service = service; + user_service->userdata = args->params.userdata; + user_service->instance = instance; + user_service->is_vchi = (args->is_vchi != 0); + user_service->dequeue_pending = 0; + user_service->close_pending = 0; + user_service->message_available_pos = instance->completion_remove - 1; + user_service->msg_insert = 0; + user_service->msg_remove = 0; + init_completion(&user_service->insert_event); + init_completion(&user_service->remove_event); + init_completion(&user_service->close_event); + + if (args->is_open) { + status = vchiq_open_service_internal(service, instance->pid); + if (status) { + vchiq_remove_service(instance, service->handle); + return (status == -EAGAIN) ? + -EINTR : -EIO; + } + } + args->handle = service->handle; + + return 0; +} + +static int vchiq_ioc_dequeue_message(struct vchiq_instance *instance, + struct vchiq_dequeue_message *args) +{ + struct user_service *user_service; + struct vchiq_service *service; + struct vchiq_header *header; + int ret; + + DEBUG_INITIALISE(instance->state->local); + DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); + service = find_service_for_instance(instance, args->handle); + if (!service) + return -EINVAL; + + user_service = (struct user_service *)service->base.userdata; + if (user_service->is_vchi == 0) { + ret = -EINVAL; + goto out; + } + + spin_lock(&service->state->msg_queue_spinlock); + if (user_service->msg_remove == user_service->msg_insert) { + if (!args->blocking) { + spin_unlock(&service->state->msg_queue_spinlock); + DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); + ret = -EWOULDBLOCK; + goto out; + } + user_service->dequeue_pending = 1; + ret = 0; + do { + spin_unlock(&service->state->msg_queue_spinlock); + DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); + if (wait_for_completion_interruptible(&user_service->insert_event)) { + dev_dbg(service->state->dev, "arm: DEQUEUE_MESSAGE interrupted\n"); + ret = -EINTR; + break; + } + spin_lock(&service->state->msg_queue_spinlock); + } while (user_service->msg_remove == user_service->msg_insert); + + if (ret) + goto out; + } + + if (WARN_ON_ONCE((int)(user_service->msg_insert - + user_service->msg_remove) < 0)) { + spin_unlock(&service->state->msg_queue_spinlock); + ret = -EINVAL; + goto out; + } + + header = user_service->msg_queue[user_service->msg_remove & + (MSG_QUEUE_SIZE - 1)]; + user_service->msg_remove++; + spin_unlock(&service->state->msg_queue_spinlock); + + complete(&user_service->remove_event); + if (!header) { + ret = -ENOTCONN; + } else if (header->size <= args->bufsize) { + /* Copy to user space if msgbuf is not NULL */ + if (!args->buf || (copy_to_user(args->buf, header->data, header->size) == 0)) { + ret = header->size; + vchiq_release_message(instance, service->handle, header); + } else { + ret = -EFAULT; + } + } else { + dev_err(service->state->dev, + "arm: header %p: bufsize %x < size %x\n", + header, args->bufsize, header->size); + WARN(1, "invalid size\n"); + ret = -EMSGSIZE; + } + DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); +out: + vchiq_service_put(service); + return ret; +} + +static int vchiq_irq_queue_bulk_tx_rx(struct vchiq_instance *instance, + struct vchiq_queue_bulk_transfer *args, + enum vchiq_bulk_dir dir, + enum vchiq_bulk_mode __user *mode) +{ + struct vchiq_service *service; + struct bulk_waiter_node *waiter = NULL, *iter; + struct vchiq_bulk bulk_params = {}; + int status = 0; + int ret; + + service = find_service_for_instance(instance, args->handle); + if (!service) + return -EINVAL; + + if (args->mode == VCHIQ_BULK_MODE_BLOCKING) { + waiter = kzalloc_obj(*waiter); + if (!waiter) { + ret = -ENOMEM; + goto out; + } + + bulk_params.uoffset = args->data; + bulk_params.mode = args->mode; + bulk_params.size = args->size; + bulk_params.dir = dir; + bulk_params.waiter = &waiter->bulk_waiter; + + status = vchiq_bulk_xfer_blocking(instance, args->handle, + &bulk_params); + } else if (args->mode == VCHIQ_BULK_MODE_WAITING) { + mutex_lock(&instance->bulk_waiter_list_mutex); + list_for_each_entry(iter, &instance->bulk_waiter_list, + list) { + if (iter->pid == current->pid) { + list_del(&iter->list); + waiter = iter; + break; + } + } + mutex_unlock(&instance->bulk_waiter_list_mutex); + if (!waiter) { + dev_err(service->state->dev, + "arm: no bulk_waiter found for pid %d\n", current->pid); + ret = -ESRCH; + goto out; + } + dev_dbg(service->state->dev, "arm: found bulk_waiter %p for pid %d\n", + waiter, current->pid); + + status = vchiq_bulk_xfer_waiting(instance, args->handle, + &waiter->bulk_waiter); + } else { + bulk_params.uoffset = args->data; + bulk_params.mode = args->mode; + bulk_params.size = args->size; + bulk_params.dir = dir; + bulk_params.cb_userdata = args->userdata; + + status = vchiq_bulk_xfer_callback(instance, args->handle, + &bulk_params); + } + + if (!waiter) { + ret = 0; + goto out; + } + + if ((status != -EAGAIN) || fatal_signal_pending(current) || + !waiter->bulk_waiter.bulk) { + if (waiter->bulk_waiter.bulk) { + /* Cancel the signal when the transfer completes. */ + spin_lock(&service->state->bulk_waiter_spinlock); + waiter->bulk_waiter.bulk->waiter = NULL; + spin_unlock(&service->state->bulk_waiter_spinlock); + } + kfree(waiter); + ret = 0; + } else { + const enum vchiq_bulk_mode mode_waiting = + VCHIQ_BULK_MODE_WAITING; + waiter->pid = current->pid; + mutex_lock(&instance->bulk_waiter_list_mutex); + list_add(&waiter->list, &instance->bulk_waiter_list); + mutex_unlock(&instance->bulk_waiter_list_mutex); + dev_dbg(service->state->dev, "arm: saved bulk_waiter %p for pid %d\n", + waiter, current->pid); + + ret = put_user(mode_waiting, mode); + } +out: + vchiq_service_put(service); + if (ret) + return ret; + else if (status == -EINVAL) + return -EIO; + else if (status == -EAGAIN) + return -EINTR; + return 0; +} + +/* read a user pointer value from an array pointers in user space */ +static inline int vchiq_get_user_ptr(void __user **buf, void __user *ubuf, int index) +{ + int ret; + + if (in_compat_syscall()) { + compat_uptr_t ptr32; + compat_uptr_t __user *uptr = ubuf; + + ret = get_user(ptr32, uptr + index); + if (ret) + return ret; + + *buf = compat_ptr(ptr32); + } else { + uintptr_t ptr, __user *uptr = ubuf; + + ret = get_user(ptr, uptr + index); + + if (ret) + return ret; + + *buf = (void __user *)ptr; + } + + return 0; +} + +struct vchiq_completion_data32 { + enum vchiq_reason reason; + compat_uptr_t header; + compat_uptr_t service_userdata; + compat_uptr_t cb_data; +}; + +static int vchiq_put_completion(struct vchiq_completion_data __user *buf, + struct vchiq_completion_data *completion, + int index) +{ + struct vchiq_completion_data32 __user *buf32 = (void __user *)buf; + + if (in_compat_syscall()) { + struct vchiq_completion_data32 tmp = { + .reason = completion->reason, + .header = ptr_to_compat(completion->header), + .service_userdata = ptr_to_compat(completion->service_userdata), + .cb_data = ptr_to_compat(completion->cb_userdata), + }; + if (copy_to_user(&buf32[index], &tmp, sizeof(tmp))) + return -EFAULT; + } else { + if (copy_to_user(&buf[index], completion, sizeof(*completion))) + return -EFAULT; + } + + return 0; +} + +static int vchiq_ioc_await_completion(struct vchiq_instance *instance, + struct vchiq_await_completion *args, + int __user *msgbufcountp) +{ + int msgbufcount; + int remove; + int ret; + + DEBUG_INITIALISE(instance->state->local); + + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + if (!instance->connected) + return -ENOTCONN; + + mutex_lock(&instance->completion_mutex); + + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + while ((instance->completion_remove == instance->completion_insert) && !instance->closing) { + int rc; + + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + mutex_unlock(&instance->completion_mutex); + rc = wait_for_completion_interruptible(&instance->insert_event); + mutex_lock(&instance->completion_mutex); + if (rc) { + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + dev_dbg(instance->state->dev, "arm: AWAIT_COMPLETION interrupted\n"); + ret = -EINTR; + goto out; + } + } + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + + msgbufcount = args->msgbufcount; + remove = instance->completion_remove; + + for (ret = 0; ret < args->count; ret++) { + struct vchiq_completion_data_kernel *completion; + struct vchiq_completion_data user_completion; + struct vchiq_service *service; + struct user_service *user_service; + struct vchiq_header *header; + + if (remove == instance->completion_insert) + break; + + completion = &instance->completions[remove & (MAX_COMPLETIONS - 1)]; + + /* + * A read memory barrier is needed to stop + * prefetch of a stale completion record + */ + rmb(); + + service = completion->service_userdata; + user_service = service->base.userdata; + + memset(&user_completion, 0, sizeof(user_completion)); + user_completion = (struct vchiq_completion_data) { + .reason = completion->reason, + .service_userdata = user_service->userdata, + }; + + header = completion->header; + if (header) { + void __user *msgbuf; + int msglen; + + msglen = header->size + sizeof(struct vchiq_header); + /* This must be a VCHIQ-style service */ + if (args->msgbufsize < msglen) { + dev_err(service->state->dev, + "arm: header %p: msgbufsize %x < msglen %x\n", + header, args->msgbufsize, msglen); + WARN(1, "invalid message size\n"); + if (ret == 0) + ret = -EMSGSIZE; + break; + } + if (msgbufcount <= 0) + /* Stall here for lack of a buffer for the message. */ + break; + /* Get the pointer from user space */ + msgbufcount--; + if (vchiq_get_user_ptr(&msgbuf, args->msgbufs, + msgbufcount)) { + if (ret == 0) + ret = -EFAULT; + break; + } + + /* Copy the message to user space */ + if (copy_to_user(msgbuf, header, msglen)) { + if (ret == 0) + ret = -EFAULT; + break; + } + + /* Now it has been copied, the message can be released. */ + vchiq_release_message(instance, service->handle, header); + + /* The completion must point to the msgbuf. */ + user_completion.header = msgbuf; + } + + if ((completion->reason == VCHIQ_SERVICE_CLOSED) && + !instance->use_close_delivered) + vchiq_service_put(service); + + user_completion.cb_userdata = completion->cb_userdata; + + if (vchiq_put_completion(args->buf, &user_completion, ret)) { + if (ret == 0) + ret = -EFAULT; + break; + } + + /* + * Ensure that the above copy has completed + * before advancing the remove pointer. + */ + mb(); + remove++; + instance->completion_remove = remove; + } + + if (msgbufcount != args->msgbufcount) { + if (put_user(msgbufcount, msgbufcountp)) + ret = -EFAULT; + } +out: + if (ret) + complete(&instance->remove_event); + mutex_unlock(&instance->completion_mutex); + DEBUG_TRACE(AWAIT_COMPLETION_LINE); + + return ret; +} + +static long +vchiq_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct vchiq_instance *instance = file->private_data; + int status = 0; + struct vchiq_service *service = NULL; + long ret = 0; + int i, rc; + + dev_dbg(instance->state->dev, "arm: instance %p, cmd %s, arg %lx\n", instance, + ((_IOC_TYPE(cmd) == VCHIQ_IOC_MAGIC) && (_IOC_NR(cmd) <= VCHIQ_IOC_MAX)) ? + ioctl_names[_IOC_NR(cmd)] : "", arg); + + switch (cmd) { + case VCHIQ_IOC_SHUTDOWN: + if (!instance->connected) + break; + + /* Remove all services */ + i = 0; + while ((service = next_service_by_instance(instance->state, + instance, &i))) { + status = vchiq_remove_service(instance, service->handle); + vchiq_service_put(service); + if (status) + break; + } + service = NULL; + + if (!status) { + /* Wake the completion thread and ask it to exit */ + instance->closing = 1; + complete(&instance->insert_event); + } + + break; + + case VCHIQ_IOC_CONNECT: + if (instance->connected) { + ret = -EINVAL; + break; + } + rc = mutex_lock_killable(&instance->state->mutex); + if (rc) { + dev_err(instance->state->dev, + "arm: vchiq: connect: could not lock mutex for state %d: %d\n", + instance->state->id, rc); + ret = -EINTR; + break; + } + status = vchiq_connect_internal(instance->state, instance); + mutex_unlock(&instance->state->mutex); + + if (!status) + instance->connected = 1; + else + dev_err(instance->state->dev, + "arm: vchiq: could not connect: %d\n", status); + break; + + case VCHIQ_IOC_CREATE_SERVICE: { + struct vchiq_create_service __user *argp; + struct vchiq_create_service args; + + argp = (void __user *)arg; + if (copy_from_user(&args, argp, sizeof(args))) { + ret = -EFAULT; + break; + } + + ret = vchiq_ioc_create_service(instance, &args); + if (ret < 0) + break; + + if (put_user(args.handle, &argp->handle)) { + vchiq_remove_service(instance, args.handle); + ret = -EFAULT; + } + } break; + + case VCHIQ_IOC_CLOSE_SERVICE: + case VCHIQ_IOC_REMOVE_SERVICE: { + unsigned int handle = (unsigned int)arg; + struct user_service *user_service; + + service = find_service_for_instance(instance, handle); + if (!service) { + ret = -EINVAL; + break; + } + + user_service = service->base.userdata; + + /* + * close_pending is false on first entry, and when the + * wait in vchiq_close_service has been interrupted. + */ + if (!user_service->close_pending) { + status = (cmd == VCHIQ_IOC_CLOSE_SERVICE) ? + vchiq_close_service(instance, service->handle) : + vchiq_remove_service(instance, service->handle); + if (status) + break; + } + + /* + * close_pending is true once the underlying service + * has been closed until the client library calls the + * CLOSE_DELIVERED ioctl, signalling close_event. + */ + if (user_service->close_pending && + wait_for_completion_interruptible(&user_service->close_event)) + status = -EAGAIN; + break; + } + + case VCHIQ_IOC_USE_SERVICE: + case VCHIQ_IOC_RELEASE_SERVICE: { + unsigned int handle = (unsigned int)arg; + + service = find_service_for_instance(instance, handle); + if (service) { + ret = (cmd == VCHIQ_IOC_USE_SERVICE) ? + vchiq_use_service_internal(service) : + vchiq_release_service_internal(service); + if (ret) { + dev_err(instance->state->dev, + "suspend: cmd %s returned error %ld for service %p4cc:%03d\n", + (cmd == VCHIQ_IOC_USE_SERVICE) ? + "VCHIQ_IOC_USE_SERVICE" : + "VCHIQ_IOC_RELEASE_SERVICE", + ret, &service->base.fourcc, + service->client_id); + } + } else { + ret = -EINVAL; + } + } break; + + case VCHIQ_IOC_QUEUE_MESSAGE: { + struct vchiq_queue_message args; + + if (copy_from_user(&args, (const void __user *)arg, + sizeof(args))) { + ret = -EFAULT; + break; + } + + service = find_service_for_instance(instance, args.handle); + + if (service && (args.count <= MAX_ELEMENTS)) { + /* Copy elements into kernel space */ + struct vchiq_element elements[MAX_ELEMENTS]; + + if (copy_from_user(elements, args.elements, + args.count * sizeof(struct vchiq_element)) == 0) + ret = vchiq_ioc_queue_message(instance, args.handle, elements, + args.count); + else + ret = -EFAULT; + } else { + ret = -EINVAL; + } + } break; + + case VCHIQ_IOC_QUEUE_BULK_TRANSMIT: + case VCHIQ_IOC_QUEUE_BULK_RECEIVE: { + struct vchiq_queue_bulk_transfer args; + struct vchiq_queue_bulk_transfer __user *argp; + + enum vchiq_bulk_dir dir = + (cmd == VCHIQ_IOC_QUEUE_BULK_TRANSMIT) ? + VCHIQ_BULK_TRANSMIT : VCHIQ_BULK_RECEIVE; + + argp = (void __user *)arg; + if (copy_from_user(&args, argp, sizeof(args))) { + ret = -EFAULT; + break; + } + + ret = vchiq_irq_queue_bulk_tx_rx(instance, &args, + dir, &argp->mode); + } break; + + case VCHIQ_IOC_AWAIT_COMPLETION: { + struct vchiq_await_completion args; + struct vchiq_await_completion __user *argp; + + argp = (void __user *)arg; + if (copy_from_user(&args, argp, sizeof(args))) { + ret = -EFAULT; + break; + } + + ret = vchiq_ioc_await_completion(instance, &args, + &argp->msgbufcount); + } break; + + case VCHIQ_IOC_DEQUEUE_MESSAGE: { + struct vchiq_dequeue_message args; + + if (copy_from_user(&args, (const void __user *)arg, + sizeof(args))) { + ret = -EFAULT; + break; + } + + ret = vchiq_ioc_dequeue_message(instance, &args); + } break; + + case VCHIQ_IOC_GET_CLIENT_ID: { + unsigned int handle = (unsigned int)arg; + + ret = vchiq_get_client_id(instance, handle); + } break; + + case VCHIQ_IOC_GET_CONFIG: { + struct vchiq_get_config args; + struct vchiq_config config; + + if (copy_from_user(&args, (const void __user *)arg, + sizeof(args))) { + ret = -EFAULT; + break; + } + if (args.config_size > sizeof(config)) { + ret = -EINVAL; + break; + } + + vchiq_get_config(&config); + if (copy_to_user(args.pconfig, &config, args.config_size)) { + ret = -EFAULT; + break; + } + } break; + + case VCHIQ_IOC_SET_SERVICE_OPTION: { + struct vchiq_set_service_option args; + + if (copy_from_user(&args, (const void __user *)arg, + sizeof(args))) { + ret = -EFAULT; + break; + } + + service = find_service_for_instance(instance, args.handle); + if (!service) { + ret = -EINVAL; + break; + } + + ret = vchiq_set_service_option(instance, args.handle, args.option, + args.value); + } break; + + case VCHIQ_IOC_LIB_VERSION: { + unsigned int lib_version = (unsigned int)arg; + + if (lib_version < VCHIQ_VERSION_MIN) + ret = -EINVAL; + else if (lib_version >= VCHIQ_VERSION_CLOSE_DELIVERED) + instance->use_close_delivered = 1; + } break; + + case VCHIQ_IOC_CLOSE_DELIVERED: { + unsigned int handle = (unsigned int)arg; + + service = find_closed_service_for_instance(instance, handle); + if (service) { + struct user_service *user_service = + (struct user_service *)service->base.userdata; + close_delivered(user_service); + } else { + ret = -EINVAL; + } + } break; + + default: + ret = -ENOTTY; + break; + } + + if (service) + vchiq_service_put(service); + + if (ret == 0) { + if (status == -EINVAL) + ret = -EIO; + else if (status == -EAGAIN) + ret = -EINTR; + } + + if (!status && (ret < 0) && (ret != -EINTR) && (ret != -EWOULDBLOCK)) { + dev_dbg(instance->state->dev, + "arm: ioctl instance %p, cmd %s -> status %d, %ld\n", + instance, (_IOC_NR(cmd) <= VCHIQ_IOC_MAX) ? + ioctl_names[_IOC_NR(cmd)] : "", status, ret); + } else { + dev_dbg(instance->state->dev, + "arm: ioctl instance %p, cmd %s -> status %d\n, %ld\n", + instance, (_IOC_NR(cmd) <= VCHIQ_IOC_MAX) ? + ioctl_names[_IOC_NR(cmd)] : "", status, ret); + } + + return ret; +} + +#if defined(CONFIG_COMPAT) + +struct vchiq_service_params32 { + int fourcc; + compat_uptr_t callback; + compat_uptr_t userdata; + short version; /* Increment for non-trivial changes */ + short version_min; /* Update for incompatible changes */ +}; + +struct vchiq_create_service32 { + struct vchiq_service_params32 params; + int is_open; + int is_vchi; + unsigned int handle; /* OUT */ +}; + +#define VCHIQ_IOC_CREATE_SERVICE32 \ + _IOWR(VCHIQ_IOC_MAGIC, 2, struct vchiq_create_service32) + +static long +vchiq_compat_ioctl_create_service(struct file *file, unsigned int cmd, + struct vchiq_create_service32 __user *ptrargs32) +{ + struct vchiq_create_service args; + struct vchiq_create_service32 args32; + struct vchiq_instance *instance = file->private_data; + long ret; + + if (copy_from_user(&args32, ptrargs32, sizeof(args32))) + return -EFAULT; + + args = (struct vchiq_create_service) { + .params = { + .fourcc = args32.params.fourcc, + .callback = compat_ptr(args32.params.callback), + .userdata = compat_ptr(args32.params.userdata), + .version = args32.params.version, + .version_min = args32.params.version_min, + }, + .is_open = args32.is_open, + .is_vchi = args32.is_vchi, + .handle = args32.handle, + }; + + ret = vchiq_ioc_create_service(instance, &args); + if (ret < 0) + return ret; + + if (put_user(args.handle, &ptrargs32->handle)) { + vchiq_remove_service(instance, args.handle); + return -EFAULT; + } + + return 0; +} + +struct vchiq_element32 { + compat_uptr_t data; + unsigned int size; +}; + +struct vchiq_queue_message32 { + unsigned int handle; + unsigned int count; + compat_uptr_t elements; +}; + +#define VCHIQ_IOC_QUEUE_MESSAGE32 \ + _IOW(VCHIQ_IOC_MAGIC, 4, struct vchiq_queue_message32) + +static long +vchiq_compat_ioctl_queue_message(struct file *file, + unsigned int cmd, + struct vchiq_queue_message32 __user *arg) +{ + struct vchiq_queue_message args; + struct vchiq_queue_message32 args32; + struct vchiq_service *service; + struct vchiq_instance *instance = file->private_data; + int ret; + + if (copy_from_user(&args32, arg, sizeof(args32))) + return -EFAULT; + + args = (struct vchiq_queue_message) { + .handle = args32.handle, + .count = args32.count, + .elements = compat_ptr(args32.elements), + }; + + if (args32.count > MAX_ELEMENTS) + return -EINVAL; + + service = find_service_for_instance(instance, args.handle); + if (!service) + return -EINVAL; + + if (args32.elements && args32.count) { + struct vchiq_element32 element32[MAX_ELEMENTS]; + struct vchiq_element elements[MAX_ELEMENTS]; + unsigned int count; + + if (copy_from_user(&element32, args.elements, + sizeof(element32))) { + vchiq_service_put(service); + return -EFAULT; + } + + for (count = 0; count < args32.count; count++) { + elements[count].data = + compat_ptr(element32[count].data); + elements[count].size = element32[count].size; + } + ret = vchiq_ioc_queue_message(instance, args.handle, elements, + args.count); + } else { + ret = -EINVAL; + } + vchiq_service_put(service); + + return ret; +} + +struct vchiq_queue_bulk_transfer32 { + unsigned int handle; + compat_uptr_t data; + unsigned int size; + compat_uptr_t userdata; + enum vchiq_bulk_mode mode; +}; + +#define VCHIQ_IOC_QUEUE_BULK_TRANSMIT32 \ + _IOWR(VCHIQ_IOC_MAGIC, 5, struct vchiq_queue_bulk_transfer32) +#define VCHIQ_IOC_QUEUE_BULK_RECEIVE32 \ + _IOWR(VCHIQ_IOC_MAGIC, 6, struct vchiq_queue_bulk_transfer32) + +static long +vchiq_compat_ioctl_queue_bulk(struct file *file, + unsigned int cmd, + struct vchiq_queue_bulk_transfer32 __user *argp) +{ + struct vchiq_queue_bulk_transfer32 args32; + struct vchiq_queue_bulk_transfer args; + enum vchiq_bulk_dir dir = (cmd == VCHIQ_IOC_QUEUE_BULK_TRANSMIT32) ? + VCHIQ_BULK_TRANSMIT : VCHIQ_BULK_RECEIVE; + + if (copy_from_user(&args32, argp, sizeof(args32))) + return -EFAULT; + + args = (struct vchiq_queue_bulk_transfer) { + .handle = args32.handle, + .data = compat_ptr(args32.data), + .size = args32.size, + .userdata = compat_ptr(args32.userdata), + .mode = args32.mode, + }; + + return vchiq_irq_queue_bulk_tx_rx(file->private_data, &args, + dir, &argp->mode); +} + +struct vchiq_await_completion32 { + unsigned int count; + compat_uptr_t buf; + unsigned int msgbufsize; + unsigned int msgbufcount; /* IN/OUT */ + compat_uptr_t msgbufs; +}; + +#define VCHIQ_IOC_AWAIT_COMPLETION32 \ + _IOWR(VCHIQ_IOC_MAGIC, 7, struct vchiq_await_completion32) + +static long +vchiq_compat_ioctl_await_completion(struct file *file, + unsigned int cmd, + struct vchiq_await_completion32 __user *argp) +{ + struct vchiq_await_completion args; + struct vchiq_await_completion32 args32; + + if (copy_from_user(&args32, argp, sizeof(args32))) + return -EFAULT; + + args = (struct vchiq_await_completion) { + .count = args32.count, + .buf = compat_ptr(args32.buf), + .msgbufsize = args32.msgbufsize, + .msgbufcount = args32.msgbufcount, + .msgbufs = compat_ptr(args32.msgbufs), + }; + + return vchiq_ioc_await_completion(file->private_data, &args, + &argp->msgbufcount); +} + +struct vchiq_dequeue_message32 { + unsigned int handle; + int blocking; + unsigned int bufsize; + compat_uptr_t buf; +}; + +#define VCHIQ_IOC_DEQUEUE_MESSAGE32 \ + _IOWR(VCHIQ_IOC_MAGIC, 8, struct vchiq_dequeue_message32) + +static long +vchiq_compat_ioctl_dequeue_message(struct file *file, + unsigned int cmd, + struct vchiq_dequeue_message32 __user *arg) +{ + struct vchiq_dequeue_message32 args32; + struct vchiq_dequeue_message args; + + if (copy_from_user(&args32, arg, sizeof(args32))) + return -EFAULT; + + args = (struct vchiq_dequeue_message) { + .handle = args32.handle, + .blocking = args32.blocking, + .bufsize = args32.bufsize, + .buf = compat_ptr(args32.buf), + }; + + return vchiq_ioc_dequeue_message(file->private_data, &args); +} + +struct vchiq_get_config32 { + unsigned int config_size; + compat_uptr_t pconfig; +}; + +#define VCHIQ_IOC_GET_CONFIG32 \ + _IOWR(VCHIQ_IOC_MAGIC, 10, struct vchiq_get_config32) + +static long +vchiq_compat_ioctl_get_config(struct file *file, + unsigned int cmd, + struct vchiq_get_config32 __user *arg) +{ + struct vchiq_get_config32 args32; + struct vchiq_config config; + void __user *ptr; + + if (copy_from_user(&args32, arg, sizeof(args32))) + return -EFAULT; + if (args32.config_size > sizeof(config)) + return -EINVAL; + + vchiq_get_config(&config); + ptr = compat_ptr(args32.pconfig); + if (copy_to_user(ptr, &config, args32.config_size)) + return -EFAULT; + + return 0; +} + +static long +vchiq_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + void __user *argp = compat_ptr(arg); + + switch (cmd) { + case VCHIQ_IOC_CREATE_SERVICE32: + return vchiq_compat_ioctl_create_service(file, cmd, argp); + case VCHIQ_IOC_QUEUE_MESSAGE32: + return vchiq_compat_ioctl_queue_message(file, cmd, argp); + case VCHIQ_IOC_QUEUE_BULK_TRANSMIT32: + case VCHIQ_IOC_QUEUE_BULK_RECEIVE32: + return vchiq_compat_ioctl_queue_bulk(file, cmd, argp); + case VCHIQ_IOC_AWAIT_COMPLETION32: + return vchiq_compat_ioctl_await_completion(file, cmd, argp); + case VCHIQ_IOC_DEQUEUE_MESSAGE32: + return vchiq_compat_ioctl_dequeue_message(file, cmd, argp); + case VCHIQ_IOC_GET_CONFIG32: + return vchiq_compat_ioctl_get_config(file, cmd, argp); + default: + return vchiq_ioctl(file, cmd, (unsigned long)argp); + } +} + +#endif + +static int vchiq_open(struct inode *inode, struct file *file) +{ + struct miscdevice *vchiq_miscdev = file->private_data; + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(vchiq_miscdev->parent); + struct vchiq_state *state = &mgmt->state; + struct vchiq_instance *instance; + + dev_dbg(state->dev, "arm: vchiq open\n"); + + if (!vchiq_remote_initialised(state)) { + dev_dbg(state->dev, "arm: vchiq has no connection to VideoCore\n"); + return -ENOTCONN; + } + + instance = kzalloc_obj(*instance); + if (!instance) + return -ENOMEM; + + instance->state = state; + instance->pid = current->tgid; + + vchiq_debugfs_add_instance(instance); + + init_completion(&instance->insert_event); + init_completion(&instance->remove_event); + mutex_init(&instance->completion_mutex); + mutex_init(&instance->bulk_waiter_list_mutex); + INIT_LIST_HEAD(&instance->bulk_waiter_list); + + file->private_data = instance; + + return 0; +} + +static int vchiq_release(struct inode *inode, struct file *file) +{ + struct vchiq_instance *instance = file->private_data; + struct vchiq_state *state = instance->state; + struct vchiq_service *service; + int ret = 0; + int i; + + dev_dbg(state->dev, "arm: instance=%p\n", instance); + + if (!vchiq_remote_initialised(state)) { + ret = -EPERM; + goto out; + } + + /* Ensure videocore is awake to allow termination. */ + vchiq_use_internal(instance->state, NULL, USE_TYPE_VCHIQ); + + mutex_lock(&instance->completion_mutex); + + /* Wake the completion thread and ask it to exit */ + instance->closing = 1; + complete(&instance->insert_event); + + mutex_unlock(&instance->completion_mutex); + + /* Wake the slot handler if the completion queue is full. */ + complete(&instance->remove_event); + + /* Mark all services for termination... */ + i = 0; + while ((service = next_service_by_instance(state, instance, &i))) { + struct user_service *user_service = service->base.userdata; + + /* Wake the slot handler if the msg queue is full. */ + complete(&user_service->remove_event); + + vchiq_terminate_service_internal(service); + vchiq_service_put(service); + } + + /* ...and wait for them to die */ + i = 0; + while ((service = next_service_by_instance(state, instance, &i))) { + struct user_service *user_service = service->base.userdata; + + wait_for_completion(&service->remove_event); + + if (WARN_ON(service->srvstate != VCHIQ_SRVSTATE_FREE)) { + vchiq_service_put(service); + break; + } + + spin_lock(&service->state->msg_queue_spinlock); + + while (user_service->msg_remove != user_service->msg_insert) { + struct vchiq_header *header; + int m = user_service->msg_remove & (MSG_QUEUE_SIZE - 1); + + header = user_service->msg_queue[m]; + user_service->msg_remove++; + spin_unlock(&service->state->msg_queue_spinlock); + + if (header) + vchiq_release_message(instance, service->handle, header); + spin_lock(&service->state->msg_queue_spinlock); + } + + spin_unlock(&service->state->msg_queue_spinlock); + + vchiq_service_put(service); + } + + /* Release any closed services */ + while (instance->completion_remove != instance->completion_insert) { + struct vchiq_completion_data_kernel *completion; + struct vchiq_service *service; + + completion = &instance->completions[instance->completion_remove + & (MAX_COMPLETIONS - 1)]; + service = completion->service_userdata; + if (completion->reason == VCHIQ_SERVICE_CLOSED) { + struct user_service *user_service = + service->base.userdata; + + /* Wake any blocked user-thread */ + if (instance->use_close_delivered) + complete(&user_service->close_event); + vchiq_service_put(service); + } + instance->completion_remove++; + } + + /* Release the PEER service count. */ + vchiq_release_internal(instance->state, NULL); + + free_bulk_waiter(instance); + + vchiq_debugfs_remove_instance(instance); + + kfree(instance); + file->private_data = NULL; + +out: + return ret; +} + +static const struct file_operations +vchiq_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = vchiq_ioctl, +#if defined(CONFIG_COMPAT) + .compat_ioctl = vchiq_compat_ioctl, +#endif + .open = vchiq_open, + .release = vchiq_release, +}; + +static struct miscdevice vchiq_miscdev = { + .fops = &vchiq_fops, + .minor = MISC_DYNAMIC_MINOR, + .name = "vchiq", + +}; + +/** + * vchiq_register_chrdev - Register the char driver for vchiq + * and create the necessary class and + * device files in userspace. + * @parent: The parent of the char device. + * + * Returns 0 on success else returns the error code. + */ +int vchiq_register_chrdev(struct device *parent) +{ + vchiq_miscdev.parent = parent; + + return misc_register(&vchiq_miscdev); +} + +/** + * vchiq_deregister_chrdev - Deregister and cleanup the vchiq char + * driver and device files + */ +void vchiq_deregister_chrdev(void) +{ + misc_deregister(&vchiq_miscdev); +} diff --git a/drivers/platform/raspberrypi/vchiq-interface/vchiq_ioctl.h b/drivers/platform/raspberrypi/vchiq-interface/vchiq_ioctl.h new file mode 100644 index 000000000..d0c759f6d --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-interface/vchiq_ioctl.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */ +/* Copyright (c) 2010-2012 Broadcom. All rights reserved. */ + +#ifndef VCHIQ_IOCTLS_H +#define VCHIQ_IOCTLS_H + +#include +#include + +#define VCHIQ_IOC_MAGIC 0xc4 +#define VCHIQ_INVALID_HANDLE (~0) + +struct vchiq_service_params { + int fourcc; + int __user (*callback)(enum vchiq_reason reason, + struct vchiq_header *header, + unsigned int handle, + void *bulk_userdata); + void __user *userdata; + short version; /* Increment for non-trivial changes */ + short version_min; /* Update for incompatible changes */ +}; + +struct vchiq_create_service { + struct vchiq_service_params params; + int is_open; + int is_vchi; + unsigned int handle; /* OUT */ +}; + +struct vchiq_queue_message { + unsigned int handle; + unsigned int count; + const struct vchiq_element __user *elements; +}; + +struct vchiq_queue_bulk_transfer { + unsigned int handle; + void __user *data; + unsigned int size; + void __user *userdata; + enum vchiq_bulk_mode mode; +}; + +struct vchiq_completion_data { + enum vchiq_reason reason; + struct vchiq_header __user *header; + void __user *service_userdata; + void __user *cb_userdata; +}; + +struct vchiq_await_completion { + unsigned int count; + struct vchiq_completion_data __user *buf; + unsigned int msgbufsize; + unsigned int msgbufcount; /* IN/OUT */ + void * __user *msgbufs; +}; + +struct vchiq_dequeue_message { + unsigned int handle; + int blocking; + unsigned int bufsize; + void __user *buf; +}; + +struct vchiq_get_config { + unsigned int config_size; + struct vchiq_config __user *pconfig; +}; + +struct vchiq_set_service_option { + unsigned int handle; + enum vchiq_service_option option; + int value; +}; + +struct vchiq_dump_mem { + void __user *virt_addr; + size_t num_bytes; +}; + +#define VCHIQ_IOC_CONNECT _IO(VCHIQ_IOC_MAGIC, 0) +#define VCHIQ_IOC_SHUTDOWN _IO(VCHIQ_IOC_MAGIC, 1) +#define VCHIQ_IOC_CREATE_SERVICE \ + _IOWR(VCHIQ_IOC_MAGIC, 2, struct vchiq_create_service) +#define VCHIQ_IOC_REMOVE_SERVICE _IO(VCHIQ_IOC_MAGIC, 3) +#define VCHIQ_IOC_QUEUE_MESSAGE \ + _IOW(VCHIQ_IOC_MAGIC, 4, struct vchiq_queue_message) +#define VCHIQ_IOC_QUEUE_BULK_TRANSMIT \ + _IOWR(VCHIQ_IOC_MAGIC, 5, struct vchiq_queue_bulk_transfer) +#define VCHIQ_IOC_QUEUE_BULK_RECEIVE \ + _IOWR(VCHIQ_IOC_MAGIC, 6, struct vchiq_queue_bulk_transfer) +#define VCHIQ_IOC_AWAIT_COMPLETION \ + _IOWR(VCHIQ_IOC_MAGIC, 7, struct vchiq_await_completion) +#define VCHIQ_IOC_DEQUEUE_MESSAGE \ + _IOWR(VCHIQ_IOC_MAGIC, 8, struct vchiq_dequeue_message) +#define VCHIQ_IOC_GET_CLIENT_ID _IO(VCHIQ_IOC_MAGIC, 9) +#define VCHIQ_IOC_GET_CONFIG \ + _IOWR(VCHIQ_IOC_MAGIC, 10, struct vchiq_get_config) +#define VCHIQ_IOC_CLOSE_SERVICE _IO(VCHIQ_IOC_MAGIC, 11) +#define VCHIQ_IOC_USE_SERVICE _IO(VCHIQ_IOC_MAGIC, 12) +#define VCHIQ_IOC_RELEASE_SERVICE _IO(VCHIQ_IOC_MAGIC, 13) +#define VCHIQ_IOC_SET_SERVICE_OPTION \ + _IOW(VCHIQ_IOC_MAGIC, 14, struct vchiq_set_service_option) +#define VCHIQ_IOC_DUMP_PHYS_MEM \ + _IOW(VCHIQ_IOC_MAGIC, 15, struct vchiq_dump_mem) +#define VCHIQ_IOC_LIB_VERSION _IO(VCHIQ_IOC_MAGIC, 16) +#define VCHIQ_IOC_CLOSE_DELIVERED _IO(VCHIQ_IOC_MAGIC, 17) +#define VCHIQ_IOC_MAX 17 + +#endif diff --git a/drivers/platform/raspberrypi/vchiq-mmal/Kconfig b/drivers/platform/raspberrypi/vchiq-mmal/Kconfig new file mode 100644 index 000000000..c99525a0b --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/Kconfig @@ -0,0 +1,7 @@ +config BCM2835_VCHIQ_MMAL + tristate "BCM2835 MMAL VCHIQ service" + depends on BCM2835_VCHIQ + help + Enables the MMAL API over VCHIQ interface as used for the + majority of the multimedia services on VideoCore. + Defaults to Y when the Broadcomd BCM2835 camera host is selected. diff --git a/drivers/platform/raspberrypi/vchiq-mmal/Makefile b/drivers/platform/raspberrypi/vchiq-mmal/Makefile new file mode 100644 index 000000000..6937f6534 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +bcm2835-mmal-vchiq-objs := mmal-vchiq.o + +obj-$(CONFIG_BCM2835_VCHIQ_MMAL) += bcm2835-mmal-vchiq.o diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-common.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-common.h new file mode 100644 index 000000000..b33129403 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-common.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + * + * MMAL structures + * + */ +#ifndef MMAL_COMMON_H +#define MMAL_COMMON_H + +#define MMAL_FOURCC(a, b, c, d) ((a) | (b << 8) | (c << 16) | (d << 24)) +#define MMAL_MAGIC MMAL_FOURCC('m', 'm', 'a', 'l') + +/** Special value signalling that time is not known */ +#define MMAL_TIME_UNKNOWN BIT_ULL(63) + +struct mmal_msg_context; + +/* mapping between v4l and mmal video modes */ +struct mmal_fmt { + u32 fourcc; /* v4l2 format id */ + int flags; /* v4l2 flags field */ + u32 mmal; + int depth; + u32 mmal_component; /* MMAL component index to be used to encode */ + u32 ybbp; /* depth of first Y plane for planar formats */ + bool remove_padding; /* Does the GPU have to remove padding, + * or can we do hide padding via bytesperline. + */ +}; + +/* buffer for one video frame */ +struct mmal_buffer { + /* v4l buffer data -- must be first */ + struct vb2_v4l2_buffer vb; + + /* list of buffers available */ + struct list_head list; + + void *buffer; /* buffer pointer */ + unsigned long buffer_size; /* size of allocated buffer */ + + struct mmal_msg_context *msg_context; + + unsigned long length; + u32 mmal_flags; + s64 dts; + s64 pts; +}; + +/* */ +struct mmal_colourfx { + s32 enable; + u32 u; + u32 v; +}; +#endif diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-encodings.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-encodings.h new file mode 100644 index 000000000..e15ae7b24 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-encodings.h @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ +#ifndef MMAL_ENCODINGS_H +#define MMAL_ENCODINGS_H + +#define MMAL_ENCODING_H264 MMAL_FOURCC('H', '2', '6', '4') +#define MMAL_ENCODING_H263 MMAL_FOURCC('H', '2', '6', '3') +#define MMAL_ENCODING_MP4V MMAL_FOURCC('M', 'P', '4', 'V') +#define MMAL_ENCODING_MP2V MMAL_FOURCC('M', 'P', '2', 'V') +#define MMAL_ENCODING_MP1V MMAL_FOURCC('M', 'P', '1', 'V') +#define MMAL_ENCODING_WMV3 MMAL_FOURCC('W', 'M', 'V', '3') +#define MMAL_ENCODING_WMV2 MMAL_FOURCC('W', 'M', 'V', '2') +#define MMAL_ENCODING_WMV1 MMAL_FOURCC('W', 'M', 'V', '1') +#define MMAL_ENCODING_WVC1 MMAL_FOURCC('W', 'V', 'C', '1') +#define MMAL_ENCODING_VP8 MMAL_FOURCC('V', 'P', '8', ' ') +#define MMAL_ENCODING_VP7 MMAL_FOURCC('V', 'P', '7', ' ') +#define MMAL_ENCODING_VP6 MMAL_FOURCC('V', 'P', '6', ' ') +#define MMAL_ENCODING_THEORA MMAL_FOURCC('T', 'H', 'E', 'O') +#define MMAL_ENCODING_SPARK MMAL_FOURCC('S', 'P', 'R', 'K') +#define MMAL_ENCODING_MJPEG MMAL_FOURCC('M', 'J', 'P', 'G') + +#define MMAL_ENCODING_JPEG MMAL_FOURCC('J', 'P', 'E', 'G') +#define MMAL_ENCODING_GIF MMAL_FOURCC('G', 'I', 'F', ' ') +#define MMAL_ENCODING_PNG MMAL_FOURCC('P', 'N', 'G', ' ') +#define MMAL_ENCODING_PPM MMAL_FOURCC('P', 'P', 'M', ' ') +#define MMAL_ENCODING_TGA MMAL_FOURCC('T', 'G', 'A', ' ') +#define MMAL_ENCODING_BMP MMAL_FOURCC('B', 'M', 'P', ' ') + +#define MMAL_ENCODING_I420 MMAL_FOURCC('I', '4', '2', '0') +#define MMAL_ENCODING_I420_SLICE MMAL_FOURCC('S', '4', '2', '0') +#define MMAL_ENCODING_YV12 MMAL_FOURCC('Y', 'V', '1', '2') +#define MMAL_ENCODING_I422 MMAL_FOURCC('I', '4', '2', '2') +#define MMAL_ENCODING_I422_SLICE MMAL_FOURCC('S', '4', '2', '2') +#define MMAL_ENCODING_YUYV MMAL_FOURCC('Y', 'U', 'Y', 'V') +#define MMAL_ENCODING_YVYU MMAL_FOURCC('Y', 'V', 'Y', 'U') +#define MMAL_ENCODING_UYVY MMAL_FOURCC('U', 'Y', 'V', 'Y') +#define MMAL_ENCODING_VYUY MMAL_FOURCC('V', 'Y', 'U', 'Y') +#define MMAL_ENCODING_NV12 MMAL_FOURCC('N', 'V', '1', '2') +#define MMAL_ENCODING_NV21 MMAL_FOURCC('N', 'V', '2', '1') +#define MMAL_ENCODING_ARGB MMAL_FOURCC('A', 'R', 'G', 'B') +#define MMAL_ENCODING_RGBA MMAL_FOURCC('R', 'G', 'B', 'A') +#define MMAL_ENCODING_ABGR MMAL_FOURCC('A', 'B', 'G', 'R') +#define MMAL_ENCODING_BGRA MMAL_FOURCC('B', 'G', 'R', 'A') +#define MMAL_ENCODING_RGB16 MMAL_FOURCC('R', 'G', 'B', '2') +#define MMAL_ENCODING_RGB24 MMAL_FOURCC('R', 'G', 'B', '3') +#define MMAL_ENCODING_RGB32 MMAL_FOURCC('R', 'G', 'B', '4') +#define MMAL_ENCODING_BGR16 MMAL_FOURCC('B', 'G', 'R', '2') +#define MMAL_ENCODING_BGR24 MMAL_FOURCC('B', 'G', 'R', '3') +#define MMAL_ENCODING_BGR32 MMAL_FOURCC('B', 'G', 'R', '4') + +/** SAND Video (YUVUV128) format, native format understood by VideoCore. + * This format is *not* opaque - if requested you will receive full frames + * of YUV_UV video. + */ +#define MMAL_ENCODING_YUVUV128 MMAL_FOURCC('S', 'A', 'N', 'D') + +/** VideoCore opaque image format, image handles are returned to + * the host but not the actual image data. + */ +#define MMAL_ENCODING_OPAQUE MMAL_FOURCC('O', 'P', 'Q', 'V') + +/** An EGL image handle + */ +#define MMAL_ENCODING_EGL_IMAGE MMAL_FOURCC('E', 'G', 'L', 'I') + +/* }@ */ + +/** \name Pre-defined audio encodings */ +/* @{ */ +#define MMAL_ENCODING_PCM_UNSIGNED_BE MMAL_FOURCC('P', 'C', 'M', 'U') +#define MMAL_ENCODING_PCM_UNSIGNED_LE MMAL_FOURCC('p', 'c', 'm', 'u') +#define MMAL_ENCODING_PCM_SIGNED_BE MMAL_FOURCC('P', 'C', 'M', 'S') +#define MMAL_ENCODING_PCM_SIGNED_LE MMAL_FOURCC('p', 'c', 'm', 's') +#define MMAL_ENCODING_PCM_FLOAT_BE MMAL_FOURCC('P', 'C', 'M', 'F') +#define MMAL_ENCODING_PCM_FLOAT_LE MMAL_FOURCC('p', 'c', 'm', 'f') + +/* Pre-defined H264 encoding variants */ + +/** ISO 14496-10 Annex B byte stream format */ +#define MMAL_ENCODING_VARIANT_H264_DEFAULT 0 +/** ISO 14496-15 AVC stream format */ +#define MMAL_ENCODING_VARIANT_H264_AVC1 MMAL_FOURCC('A', 'V', 'C', '1') +/** Implicitly delineated NAL units without emulation prevention */ +#define MMAL_ENCODING_VARIANT_H264_RAW MMAL_FOURCC('R', 'A', 'W', ' ') + +/** \defgroup MmalColorSpace List of pre-defined video color spaces + * This defines a list of common color spaces. This list isn't exhaustive and + * is only provided as a convenience to avoid clients having to use FourCC + * codes directly. However components are allowed to define and use their own + * FourCC codes. + */ +/* @{ */ + +/** Unknown color space */ +#define MMAL_COLOR_SPACE_UNKNOWN 0 +/** ITU-R BT.601-5 [SDTV] */ +#define MMAL_COLOR_SPACE_ITUR_BT601 MMAL_FOURCC('Y', '6', '0', '1') +/** ITU-R BT.709-3 [HDTV] */ +#define MMAL_COLOR_SPACE_ITUR_BT709 MMAL_FOURCC('Y', '7', '0', '9') +/** JPEG JFIF */ +#define MMAL_COLOR_SPACE_JPEG_JFIF MMAL_FOURCC('Y', 'J', 'F', 'I') +/** Title 47 Code of Federal Regulations (2003) 73.682 (a) (20) */ +#define MMAL_COLOR_SPACE_FCC MMAL_FOURCC('Y', 'F', 'C', 'C') +/** Society of Motion Picture and Television Engineers 240M (1999) */ +#define MMAL_COLOR_SPACE_SMPTE240M MMAL_FOURCC('Y', '2', '4', '0') +/** ITU-R BT.470-2 System M */ +#define MMAL_COLOR_SPACE_BT470_2_M MMAL_FOURCC('Y', '_', '_', 'M') +/** ITU-R BT.470-2 System BG */ +#define MMAL_COLOR_SPACE_BT470_2_BG MMAL_FOURCC('Y', '_', 'B', 'G') +/** JPEG JFIF, but with 16..255 luma */ +#define MMAL_COLOR_SPACE_JFIF_Y16_255 MMAL_FOURCC('Y', 'Y', '1', '6') +/* @} MmalColorSpace List */ + +#endif /* MMAL_ENCODINGS_H */ diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-common.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-common.h new file mode 100644 index 000000000..492d4c5dc --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-common.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ + +#ifndef MMAL_MSG_COMMON_H +#define MMAL_MSG_COMMON_H + +#include + +enum mmal_msg_status { + MMAL_MSG_STATUS_SUCCESS = 0, /**< Success */ + MMAL_MSG_STATUS_ENOMEM, /**< Out of memory */ + MMAL_MSG_STATUS_ENOSPC, /**< Out of resources other than memory */ + MMAL_MSG_STATUS_EINVAL, /**< Argument is invalid */ + MMAL_MSG_STATUS_ENOSYS, /**< Function not implemented */ + MMAL_MSG_STATUS_ENOENT, /**< No such file or directory */ + MMAL_MSG_STATUS_ENXIO, /**< No such device or address */ + MMAL_MSG_STATUS_EIO, /**< I/O error */ + MMAL_MSG_STATUS_ESPIPE, /**< Illegal seek */ + MMAL_MSG_STATUS_ECORRUPT, /**< Data is corrupt \attention */ + MMAL_MSG_STATUS_ENOTREADY, /**< Component is not ready */ + MMAL_MSG_STATUS_ECONFIG, /**< Component is not configured */ + MMAL_MSG_STATUS_EISCONN, /**< Port is already connected */ + MMAL_MSG_STATUS_ENOTCONN, /**< Port is disconnected */ + MMAL_MSG_STATUS_EAGAIN, /**< Resource temporarily unavailable. */ + MMAL_MSG_STATUS_EFAULT, /**< Bad address */ +}; + +struct mmal_rect { + s32 x; /**< x coordinate (from left) */ + s32 y; /**< y coordinate (from top) */ + s32 width; /**< width */ + s32 height; /**< height */ +}; + +#endif /* MMAL_MSG_COMMON_H */ diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-format.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-format.h new file mode 100644 index 000000000..5569876d8 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-format.h @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ + +#ifndef MMAL_MSG_FORMAT_H +#define MMAL_MSG_FORMAT_H + +#include + +#include "mmal-msg-common.h" + +/* MMAL_ES_FORMAT_T */ + +struct mmal_audio_format { + u32 channels; /* Number of audio channels */ + u32 sample_rate; /* Sample rate */ + + u32 bits_per_sample; /* Bits per sample */ + u32 block_align; /* Size of a block of data */ +}; + +struct mmal_video_format { + u32 width; /* Width of frame in pixels */ + u32 height; /* Height of frame in rows of pixels */ + struct mmal_rect crop; /* Visible region of the frame */ + struct s32_fract frame_rate; /* Frame rate */ + struct s32_fract par; /* Pixel aspect ratio */ + + /* + * FourCC specifying the color space of the video stream. See the + * MmalColorSpace "pre-defined color spaces" for some examples. + */ + u32 color_space; +}; + +struct mmal_subpicture_format { + u32 x_offset; + u32 y_offset; +}; + +union mmal_es_specific_format { + struct mmal_audio_format audio; + struct mmal_video_format video; + struct mmal_subpicture_format subpicture; +}; + +/* Definition of an elementary stream format (MMAL_ES_FORMAT_T) */ +struct mmal_es_format_local { + u32 type; /* enum mmal_es_type */ + + u32 encoding; /* FourCC specifying encoding of the elementary + * stream. + */ + u32 encoding_variant; /* FourCC specifying the specific + * encoding variant of the elementary + * stream. + */ + + union mmal_es_specific_format *es; /* Type specific + * information for the + * elementary stream + */ + + u32 bitrate; /* Bitrate in bits per second */ + u32 flags; /* Flags describing properties of the elementary + * stream. + */ + + u32 extradata_size; /* Size of the codec specific data */ + u8 *extradata; /* Codec specific data */ +}; + +/* Remote definition of an elementary stream format (MMAL_ES_FORMAT_T) */ +struct mmal_es_format { + u32 type; /* enum mmal_es_type */ + + u32 encoding; /* FourCC specifying encoding of the elementary + * stream. + */ + u32 encoding_variant; /* FourCC specifying the specific + * encoding variant of the elementary + * stream. + */ + + u32 es; /* Type specific + * information for the + * elementary stream + */ + + u32 bitrate; /* Bitrate in bits per second */ + u32 flags; /* Flags describing properties of the elementary + * stream. + */ + + u32 extradata_size; /* Size of the codec specific data */ + u32 extradata; /* Codec specific data */ +}; + +#endif /* MMAL_MSG_FORMAT_H */ diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-port.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-port.h new file mode 100644 index 000000000..6ee4c1ed7 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg-port.h @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ + +/* MMAL_PORT_TYPE_T */ +enum mmal_port_type { + MMAL_PORT_TYPE_UNKNOWN = 0, /* Unknown port type */ + MMAL_PORT_TYPE_CONTROL, /* Control port */ + MMAL_PORT_TYPE_INPUT, /* Input port */ + MMAL_PORT_TYPE_OUTPUT, /* Output port */ + MMAL_PORT_TYPE_CLOCK, /* Clock port */ +}; + +/* The port is pass-through and doesn't need buffer headers allocated */ +#define MMAL_PORT_CAPABILITY_PASSTHROUGH 0x01 +/* + *The port wants to allocate the buffer payloads. + * This signals a preference that payload allocation should be done + * on this port for efficiency reasons. + */ +#define MMAL_PORT_CAPABILITY_ALLOCATION 0x02 +/* + * The port supports format change events. + * This applies to input ports and is used to let the client know + * whether the port supports being reconfigured via a format + * change event (i.e. without having to disable the port). + */ +#define MMAL_PORT_CAPABILITY_SUPPORTS_EVENT_FORMAT_CHANGE 0x04 + +/* + * mmal port structure (MMAL_PORT_T) + * + * most elements are informational only, the pointer values for + * interogation messages are generally provided as additional + * structures within the message. When used to set values only the + * buffer_num, buffer_size and userdata parameters are writable. + */ +struct mmal_port { + u32 priv; /* Private member used by the framework */ + u32 name; /* Port name. Used for debugging purposes (RO) */ + + u32 type; /* Type of the port (RO) enum mmal_port_type */ + u16 index; /* Index of the port in its type list (RO) */ + u16 index_all; /* Index of the port in the list of all ports (RO) */ + + u32 is_enabled; /* Indicates whether the port is enabled or not (RO) */ + u32 format; /* Format of the elementary stream */ + + u32 buffer_num_min; /* Minimum number of buffers the port + * requires (RO). This is set by the + * component. + */ + + u32 buffer_size_min; /* Minimum size of buffers the port + * requires (RO). This is set by the + * component. + */ + + u32 buffer_alignment_min;/* Minimum alignment requirement for + * the buffers (RO). A value of + * zero means no special alignment + * requirements. This is set by the + * component. + */ + + u32 buffer_num_recommended; /* Number of buffers the port + * recommends for optimal + * performance (RO). A value of + * zero means no special + * recommendation. This is set + * by the component. + */ + + u32 buffer_size_recommended; /* Size of buffers the port + * recommends for optimal + * performance (RO). A value of + * zero means no special + * recommendation. This is set + * by the component. + */ + + u32 buffer_num; /* Actual number of buffers the port will use. + * This is set by the client. + */ + + u32 buffer_size; /* Actual maximum size of the buffers that + * will be sent to the port. This is set by + * the client. + */ + + u32 component; /* Component this port belongs to (Read Only) */ + + u32 userdata; /* Field reserved for use by the client */ + + u32 capabilities; /* Flags describing the capabilities of a + * port (RO). Bitwise combination of \ref + * portcapabilities "Port capabilities" + * values. + */ +}; diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg.h new file mode 100644 index 000000000..188949442 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-msg.h @@ -0,0 +1,406 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ + +/* + * all the data structures which serialise the MMAL protocol. note + * these are directly mapped onto the received message data. + * + * BEWARE: They seem to *assume* pointers are u32 and that there is no + * structure padding! + * + * NOTE: this implementation uses kernel types to ensure sizes. Rather + * than assigning values to enums to force their size the + * implementation uses fixed size types and not the enums (though the + * comments have the actual enum type + */ +#ifndef MMAL_MSG_H +#define MMAL_MSG_H + +#define VC_MMAL_VER 15 +#define VC_MMAL_MIN_VER 10 + +/* max total message size is 512 bytes */ +#define MMAL_MSG_MAX_SIZE 512 +/* with six 32bit header elements max payload is therefore 488 bytes */ +#define MMAL_MSG_MAX_PAYLOAD 488 + +#include "mmal-msg-common.h" +#include "mmal-msg-format.h" +#include "mmal-msg-port.h" +#include "mmal-vchiq.h" + +enum mmal_msg_type { + MMAL_MSG_TYPE_QUIT = 1, + MMAL_MSG_TYPE_SERVICE_CLOSED, + MMAL_MSG_TYPE_GET_VERSION, + MMAL_MSG_TYPE_COMPONENT_CREATE, + MMAL_MSG_TYPE_COMPONENT_DESTROY, /* 5 */ + MMAL_MSG_TYPE_COMPONENT_ENABLE, + MMAL_MSG_TYPE_COMPONENT_DISABLE, + MMAL_MSG_TYPE_PORT_INFO_GET, + MMAL_MSG_TYPE_PORT_INFO_SET, + MMAL_MSG_TYPE_PORT_ACTION, /* 10 */ + MMAL_MSG_TYPE_BUFFER_FROM_HOST, + MMAL_MSG_TYPE_BUFFER_TO_HOST, + MMAL_MSG_TYPE_GET_STATS, + MMAL_MSG_TYPE_PORT_PARAMETER_SET, + MMAL_MSG_TYPE_PORT_PARAMETER_GET, /* 15 */ + MMAL_MSG_TYPE_EVENT_TO_HOST, + MMAL_MSG_TYPE_GET_CORE_STATS_FOR_PORT, + MMAL_MSG_TYPE_OPAQUE_ALLOCATOR, + MMAL_MSG_TYPE_CONSUME_MEM, + MMAL_MSG_TYPE_LMK, /* 20 */ + MMAL_MSG_TYPE_OPAQUE_ALLOCATOR_DESC, + MMAL_MSG_TYPE_DRM_GET_LHS32, + MMAL_MSG_TYPE_DRM_GET_TIME, + MMAL_MSG_TYPE_BUFFER_FROM_HOST_ZEROLEN, + MMAL_MSG_TYPE_PORT_FLUSH, /* 25 */ + MMAL_MSG_TYPE_HOST_LOG, + MMAL_MSG_TYPE_MSG_LAST +}; + +/* port action request messages differ depending on the action type */ +enum mmal_msg_port_action_type { + MMAL_MSG_PORT_ACTION_TYPE_UNKNOWN = 0, /* Unknown action */ + MMAL_MSG_PORT_ACTION_TYPE_ENABLE, /* Enable a port */ + MMAL_MSG_PORT_ACTION_TYPE_DISABLE, /* Disable a port */ + MMAL_MSG_PORT_ACTION_TYPE_FLUSH, /* Flush a port */ + MMAL_MSG_PORT_ACTION_TYPE_CONNECT, /* Connect ports */ + MMAL_MSG_PORT_ACTION_TYPE_DISCONNECT, /* Disconnect ports */ + MMAL_MSG_PORT_ACTION_TYPE_SET_REQUIREMENTS, /* Set buffer requirements*/ +}; + +struct mmal_msg_header { + u32 magic; + u32 type; /* enum mmal_msg_type */ + + /* Opaque handle to the control service */ + u32 control_service; + + u32 context; /* a u32 per message context */ + u32 status; /* The status of the vchiq operation */ + u32 padding; +}; + +/* Send from VC to host to report version */ +struct mmal_msg_version { + u32 flags; + u32 major; + u32 minor; + u32 minimum; +}; + +/* request to VC to create component */ +struct mmal_msg_component_create { + u32 client_component; /* component context */ + char name[128]; + u32 pid; /* For debug */ +}; + +/* reply from VC to component creation request */ +struct mmal_msg_component_create_reply { + u32 status; /* enum mmal_msg_status - how does this differ to + * the one in the header? + */ + u32 component_handle; /* VideoCore handle for component */ + u32 input_num; /* Number of input ports */ + u32 output_num; /* Number of output ports */ + u32 clock_num; /* Number of clock ports */ +}; + +/* request to VC to destroy a component */ +struct mmal_msg_component_destroy { + u32 component_handle; +}; + +struct mmal_msg_component_destroy_reply { + u32 status; /* The component destruction status */ +}; + +/* request and reply to VC to enable a component */ +struct mmal_msg_component_enable { + u32 component_handle; +}; + +struct mmal_msg_component_enable_reply { + u32 status; /* The component enable status */ +}; + +/* request and reply to VC to disable a component */ +struct mmal_msg_component_disable { + u32 component_handle; +}; + +struct mmal_msg_component_disable_reply { + u32 status; /* The component disable status */ +}; + +/* request to VC to get port information */ +struct mmal_msg_port_info_get { + u32 component_handle; /* component handle port is associated with */ + u32 port_type; /* enum mmal_msg_port_type */ + u32 index; /* port index to query */ +}; + +/* reply from VC to get port info request */ +struct mmal_msg_port_info_get_reply { + u32 status; /* enum mmal_msg_status */ + u32 component_handle; /* component handle port is associated with */ + u32 port_type; /* enum mmal_msg_port_type */ + u32 port_index; /* port indexed in query */ + s32 found; /* unused */ + u32 port_handle; /* Handle to use for this port */ + struct mmal_port port; + struct mmal_es_format format; /* elementary stream format */ + union mmal_es_specific_format es; /* es type specific data */ + u8 extradata[MMAL_FORMAT_EXTRADATA_MAX_SIZE]; /* es extra data */ +}; + +/* request to VC to set port information */ +struct mmal_msg_port_info_set { + u32 component_handle; + u32 port_type; /* enum mmal_msg_port_type */ + u32 port_index; /* port indexed in query */ + struct mmal_port port; + struct mmal_es_format format; + union mmal_es_specific_format es; + u8 extradata[MMAL_FORMAT_EXTRADATA_MAX_SIZE]; +}; + +/* reply from VC to port info set request */ +struct mmal_msg_port_info_set_reply { + u32 status; + u32 component_handle; /* component handle port is associated with */ + u32 port_type; /* enum mmal_msg_port_type */ + u32 index; /* port indexed in query */ + s32 found; /* unused */ + u32 port_handle; /* Handle to use for this port */ + struct mmal_port port; + struct mmal_es_format format; + union mmal_es_specific_format es; + u8 extradata[MMAL_FORMAT_EXTRADATA_MAX_SIZE]; +}; + +/* port action requests that take a mmal_port as a parameter */ +struct mmal_msg_port_action_port { + u32 component_handle; + u32 port_handle; + u32 action; /* enum mmal_msg_port_action_type */ + struct mmal_port port; +}; + +/* port action requests that take handles as a parameter */ +struct mmal_msg_port_action_handle { + u32 component_handle; + u32 port_handle; + u32 action; /* enum mmal_msg_port_action_type */ + u32 connect_component_handle; + u32 connect_port_handle; +}; + +struct mmal_msg_port_action_reply { + u32 status; /* The port action operation status */ +}; + +/* MMAL buffer transfer */ + +/* Size of space reserved in a buffer message for short messages. */ +#define MMAL_VC_SHORT_DATA 128 + +/* Signals that the current payload is the end of the stream of data */ +#define MMAL_BUFFER_HEADER_FLAG_EOS BIT(0) +/* Signals that the start of the current payload starts a frame */ +#define MMAL_BUFFER_HEADER_FLAG_FRAME_START BIT(1) +/* Signals that the end of the current payload ends a frame */ +#define MMAL_BUFFER_HEADER_FLAG_FRAME_END BIT(2) +/* Signals that the current payload contains only complete frames (>1) */ +#define MMAL_BUFFER_HEADER_FLAG_FRAME \ + (MMAL_BUFFER_HEADER_FLAG_FRAME_START | \ + MMAL_BUFFER_HEADER_FLAG_FRAME_END) +/* Signals that the current payload is a keyframe (i.e. self decodable) */ +#define MMAL_BUFFER_HEADER_FLAG_KEYFRAME BIT(3) +/* + * Signals a discontinuity in the stream of data (e.g. after a seek). + * Can be used for instance by a decoder to reset its state + */ +#define MMAL_BUFFER_HEADER_FLAG_DISCONTINUITY BIT(4) +/* + * Signals a buffer containing some kind of config data for the component + * (e.g. codec config data) + */ +#define MMAL_BUFFER_HEADER_FLAG_CONFIG BIT(5) +/* Signals an encrypted payload */ +#define MMAL_BUFFER_HEADER_FLAG_ENCRYPTED BIT(6) +/* Signals a buffer containing side information */ +#define MMAL_BUFFER_HEADER_FLAG_CODECSIDEINFO BIT(7) +/* + * Signals a buffer which is the snapshot/postview image from a stills + * capture + */ +#define MMAL_BUFFER_HEADER_FLAGS_SNAPSHOT BIT(8) +/* Signals a buffer which contains data known to be corrupted */ +#define MMAL_BUFFER_HEADER_FLAG_CORRUPTED BIT(9) +/* Signals that a buffer failed to be transmitted */ +#define MMAL_BUFFER_HEADER_FLAG_TRANSMISSION_FAILED BIT(10) + +struct mmal_driver_buffer { + u32 magic; + u32 component_handle; + u32 port_handle; + u32 client_context; +}; + +/* buffer header */ +struct mmal_buffer_header { + u32 next; /* next header */ + u32 priv; /* framework private data */ + u32 cmd; + u32 data; + u32 alloc_size; + u32 length; + u32 offset; + u32 flags; + s64 pts; + s64 dts; + u32 type; + u32 user_data; +}; + +struct mmal_buffer_header_type_specific { + union { + struct { + u32 planes; + u32 offset[4]; + u32 pitch[4]; + u32 flags; + } video; + } u; +}; + +struct mmal_msg_buffer_from_host { + /* + *The front 32 bytes of the buffer header are copied + * back to us in the reply to allow for context. This + * area is used to store two mmal_driver_buffer structures to + * allow for multiple concurrent service users. + */ + /* control data */ + struct mmal_driver_buffer drvbuf; + + /* referenced control data for passthrough buffer management */ + struct mmal_driver_buffer drvbuf_ref; + struct mmal_buffer_header buffer_header; /* buffer header itself */ + struct mmal_buffer_header_type_specific buffer_header_type_specific; + s32 is_zero_copy; + s32 has_reference; + + /* allows short data to be xfered in control message */ + u32 payload_in_message; + u8 short_data[MMAL_VC_SHORT_DATA]; +}; + +/* port parameter setting */ + +#define MMAL_WORKER_PORT_PARAMETER_SPACE 96 + +struct mmal_msg_port_parameter_set { + u32 component_handle; /* component */ + u32 port_handle; /* port */ + u32 id; /* Parameter ID */ + u32 size; /* Parameter size */ + u32 value[MMAL_WORKER_PORT_PARAMETER_SPACE]; +}; + +struct mmal_msg_port_parameter_set_reply { + u32 status; /* enum mmal_msg_status todo: how does this + * differ to the one in the header? + */ +}; + +/* port parameter getting */ + +struct mmal_msg_port_parameter_get { + u32 component_handle; /* component */ + u32 port_handle; /* port */ + u32 id; /* Parameter ID */ + u32 size; /* Parameter size */ +}; + +struct mmal_msg_port_parameter_get_reply { + u32 status; /* Status of mmal_port_parameter_get call */ + u32 id; /* Parameter ID */ + u32 size; /* Parameter size */ + u32 value[MMAL_WORKER_PORT_PARAMETER_SPACE]; +}; + +/* event messages */ +#define MMAL_WORKER_EVENT_SPACE 256 + +struct mmal_msg_event_to_host { + u32 client_component; /* component context */ + + u32 port_type; + u32 port_num; + + u32 cmd; + u32 length; + u8 data[MMAL_WORKER_EVENT_SPACE]; + u32 delayed_buffer; +}; + +/* all mmal messages are serialised through this structure */ +struct mmal_msg { + /* header */ + struct mmal_msg_header h; + /* payload */ + union { + struct mmal_msg_version version; + + struct mmal_msg_component_create component_create; + struct mmal_msg_component_create_reply component_create_reply; + + struct mmal_msg_component_destroy component_destroy; + struct mmal_msg_component_destroy_reply component_destroy_reply; + + struct mmal_msg_component_enable component_enable; + struct mmal_msg_component_enable_reply component_enable_reply; + + struct mmal_msg_component_disable component_disable; + struct mmal_msg_component_disable_reply component_disable_reply; + + struct mmal_msg_port_info_get port_info_get; + struct mmal_msg_port_info_get_reply port_info_get_reply; + + struct mmal_msg_port_info_set port_info_set; + struct mmal_msg_port_info_set_reply port_info_set_reply; + + struct mmal_msg_port_action_port port_action_port; + struct mmal_msg_port_action_handle port_action_handle; + struct mmal_msg_port_action_reply port_action_reply; + + struct mmal_msg_buffer_from_host buffer_from_host; + + struct mmal_msg_port_parameter_set port_parameter_set; + struct mmal_msg_port_parameter_set_reply + port_parameter_set_reply; + struct mmal_msg_port_parameter_get + port_parameter_get; + struct mmal_msg_port_parameter_get_reply + port_parameter_get_reply; + + struct mmal_msg_event_to_host event_to_host; + + u8 payload[MMAL_MSG_MAX_PAYLOAD]; + } u; +}; +#endif diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-parameters.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-parameters.h new file mode 100644 index 000000000..a0cdd2810 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-parameters.h @@ -0,0 +1,752 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + */ + +/* common parameters */ + +/** @name Parameter groups + * Parameters are divided into groups, and then allocated sequentially within + * a group using an enum. + * @{ + */ + +#ifndef MMAL_PARAMETERS_H +#define MMAL_PARAMETERS_H + +#include + +/** Common parameter ID group, used with many types of component. */ +#define MMAL_PARAMETER_GROUP_COMMON (0 << 16) +/** Camera-specific parameter ID group. */ +#define MMAL_PARAMETER_GROUP_CAMERA (1 << 16) +/** Video-specific parameter ID group. */ +#define MMAL_PARAMETER_GROUP_VIDEO (2 << 16) +/** Audio-specific parameter ID group. */ +#define MMAL_PARAMETER_GROUP_AUDIO (3 << 16) +/** Clock-specific parameter ID group. */ +#define MMAL_PARAMETER_GROUP_CLOCK (4 << 16) +/** Miracast-specific parameter ID group. */ +#define MMAL_PARAMETER_GROUP_MIRACAST (5 << 16) + +/* Common parameters */ +enum mmal_parameter_common_type { + /**< Never a valid parameter ID */ + MMAL_PARAMETER_UNUSED = MMAL_PARAMETER_GROUP_COMMON, + + /**< MMAL_PARAMETER_ENCODING_T */ + MMAL_PARAMETER_SUPPORTED_ENCODINGS, + /**< MMAL_PARAMETER_URI_T */ + MMAL_PARAMETER_URI, + /** MMAL_PARAMETER_CHANGE_EVENT_REQUEST_T */ + MMAL_PARAMETER_CHANGE_EVENT_REQUEST, + /** MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_ZERO_COPY, + /**< MMAL_PARAMETER_BUFFER_REQUIREMENTS_T */ + MMAL_PARAMETER_BUFFER_REQUIREMENTS, + /**< MMAL_PARAMETER_STATISTICS_T */ + MMAL_PARAMETER_STATISTICS, + /**< MMAL_PARAMETER_CORE_STATISTICS_T */ + MMAL_PARAMETER_CORE_STATISTICS, + /**< MMAL_PARAMETER_MEM_USAGE_T */ + MMAL_PARAMETER_MEM_USAGE, + /**< MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_BUFFER_FLAG_FILTER, + /**< MMAL_PARAMETER_SEEK_T */ + MMAL_PARAMETER_SEEK, + /**< MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_POWERMON_ENABLE, + /**< MMAL_PARAMETER_LOGGING_T */ + MMAL_PARAMETER_LOGGING, + /**< MMAL_PARAMETER_UINT64_T */ + MMAL_PARAMETER_SYSTEM_TIME, + /**< MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_NO_IMAGE_PADDING, +}; + +/* camera parameters */ + +enum mmal_parameter_camera_type { + /* 0 */ + /** @ref MMAL_PARAMETER_THUMBNAIL_CONFIG_T */ + MMAL_PARAMETER_THUMBNAIL_CONFIGURATION = + MMAL_PARAMETER_GROUP_CAMERA, + /**< Unused? */ + MMAL_PARAMETER_CAPTURE_QUALITY, + /**< @ref MMAL_PARAMETER_INT32_T */ + MMAL_PARAMETER_ROTATION, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_EXIF_DISABLE, + /**< @ref MMAL_PARAMETER_EXIF_T */ + MMAL_PARAMETER_EXIF, + /**< @ref MMAL_PARAM_AWBMODE_T */ + MMAL_PARAMETER_AWB_MODE, + /**< @ref MMAL_PARAMETER_IMAGEFX_T */ + MMAL_PARAMETER_IMAGE_EFFECT, + /**< @ref MMAL_PARAMETER_COLOURFX_T */ + MMAL_PARAMETER_COLOUR_EFFECT, + /**< @ref MMAL_PARAMETER_FLICKERAVOID_T */ + MMAL_PARAMETER_FLICKER_AVOID, + /**< @ref MMAL_PARAMETER_FLASH_T */ + MMAL_PARAMETER_FLASH, + /**< @ref MMAL_PARAMETER_REDEYE_T */ + MMAL_PARAMETER_REDEYE, + /**< @ref MMAL_PARAMETER_FOCUS_T */ + MMAL_PARAMETER_FOCUS, + /**< Unused? */ + MMAL_PARAMETER_FOCAL_LENGTHS, + /**< @ref MMAL_PARAMETER_INT32_T */ + MMAL_PARAMETER_EXPOSURE_COMP, + /**< @ref MMAL_PARAMETER_SCALEFACTOR_T */ + MMAL_PARAMETER_ZOOM, + /**< @ref MMAL_PARAMETER_MIRROR_T */ + MMAL_PARAMETER_MIRROR, + + /* 0x10 */ + /**< @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_CAMERA_NUM, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_CAPTURE, + /**< @ref MMAL_PARAMETER_EXPOSUREMODE_T */ + MMAL_PARAMETER_EXPOSURE_MODE, + /**< @ref MMAL_PARAMETER_EXPOSUREMETERINGMODE_T */ + MMAL_PARAMETER_EXP_METERING_MODE, + /**< @ref MMAL_PARAMETER_FOCUS_STATUS_T */ + MMAL_PARAMETER_FOCUS_STATUS, + /**< @ref MMAL_PARAMETER_CAMERA_CONFIG_T */ + MMAL_PARAMETER_CAMERA_CONFIG, + /**< @ref MMAL_PARAMETER_CAPTURE_STATUS_T */ + MMAL_PARAMETER_CAPTURE_STATUS, + /**< @ref MMAL_PARAMETER_FACE_TRACK_T */ + MMAL_PARAMETER_FACE_TRACK, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_DRAW_BOX_FACES_AND_FOCUS, + /**< @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_JPEG_Q_FACTOR, + /**< @ref MMAL_PARAMETER_FRAME_RATE_T */ + MMAL_PARAMETER_FRAME_RATE, + /**< @ref MMAL_PARAMETER_CAMERA_STC_MODE_T */ + MMAL_PARAMETER_USE_STC, + /**< @ref MMAL_PARAMETER_CAMERA_INFO_T */ + MMAL_PARAMETER_CAMERA_INFO, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_STABILISATION, + /**< @ref MMAL_PARAMETER_FACE_TRACK_RESULTS_T */ + MMAL_PARAMETER_FACE_TRACK_RESULTS, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_ENABLE_RAW_CAPTURE, + + /* 0x20 */ + /**< @ref MMAL_PARAMETER_URI_T */ + MMAL_PARAMETER_DPF_FILE, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_ENABLE_DPF_FILE, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_DPF_FAIL_IS_FATAL, + /**< @ref MMAL_PARAMETER_CAPTUREMODE_T */ + MMAL_PARAMETER_CAPTURE_MODE, + /**< @ref MMAL_PARAMETER_FOCUS_REGIONS_T */ + MMAL_PARAMETER_FOCUS_REGIONS, + /**< @ref MMAL_PARAMETER_INPUT_CROP_T */ + MMAL_PARAMETER_INPUT_CROP, + /**< @ref MMAL_PARAMETER_SENSOR_INFORMATION_T */ + MMAL_PARAMETER_SENSOR_INFORMATION, + /**< @ref MMAL_PARAMETER_FLASH_SELECT_T */ + MMAL_PARAMETER_FLASH_SELECT, + /**< @ref MMAL_PARAMETER_FIELD_OF_VIEW_T */ + MMAL_PARAMETER_FIELD_OF_VIEW, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_HIGH_DYNAMIC_RANGE, + /**< @ref MMAL_PARAMETER_DRC_T */ + MMAL_PARAMETER_DYNAMIC_RANGE_COMPRESSION, + /**< @ref MMAL_PARAMETER_ALGORITHM_CONTROL_T */ + MMAL_PARAMETER_ALGORITHM_CONTROL, + /**< @ref MMAL_PARAMETER_RATIONAL_T */ + MMAL_PARAMETER_SHARPNESS, + /**< @ref MMAL_PARAMETER_RATIONAL_T */ + MMAL_PARAMETER_CONTRAST, + /**< @ref MMAL_PARAMETER_RATIONAL_T */ + MMAL_PARAMETER_BRIGHTNESS, + /**< @ref MMAL_PARAMETER_RATIONAL_T */ + MMAL_PARAMETER_SATURATION, + + /* 0x30 */ + /**< @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_ISO, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_ANTISHAKE, + /** @ref MMAL_PARAMETER_IMAGEFX_PARAMETERS_T */ + MMAL_PARAMETER_IMAGE_EFFECT_PARAMETERS, + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_CAMERA_BURST_CAPTURE, + /** @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_CAMERA_MIN_ISO, + /** @ref MMAL_PARAMETER_CAMERA_USE_CASE_T */ + MMAL_PARAMETER_CAMERA_USE_CASE, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_CAPTURE_STATS_PASS, + /** @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_CAMERA_CUSTOM_SENSOR_CONFIG, + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_ENABLE_REGISTER_FILE, + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_REGISTER_FAIL_IS_FATAL, + /** @ref MMAL_PARAMETER_CONFIGFILE_T */ + MMAL_PARAMETER_CONFIGFILE_REGISTERS, + /** @ref MMAL_PARAMETER_CONFIGFILE_CHUNK_T */ + MMAL_PARAMETER_CONFIGFILE_CHUNK_REGISTERS, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_JPEG_ATTACH_LOG, + /**< @ref MMAL_PARAMETER_ZEROSHUTTERLAG_T */ + MMAL_PARAMETER_ZERO_SHUTTER_LAG, + /**< @ref MMAL_PARAMETER_FPS_RANGE_T */ + MMAL_PARAMETER_FPS_RANGE, + /**< @ref MMAL_PARAMETER_INT32_T */ + MMAL_PARAMETER_CAPTURE_EXPOSURE_COMP, + + /* 0x40 */ + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_SW_SHARPEN_DISABLE, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_FLASH_REQUIRED, + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_SW_SATURATION_DISABLE, + /**< Takes a @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_SHUTTER_SPEED, + /**< Takes a @ref MMAL_PARAMETER_AWB_GAINS_T */ + MMAL_PARAMETER_CUSTOM_AWB_GAINS, +}; + +enum mmal_parameter_camera_config_timestamp_mode { + MMAL_PARAM_TIMESTAMP_MODE_ZERO = 0, /* Always timestamp frames as 0 */ + MMAL_PARAM_TIMESTAMP_MODE_RAW_STC, /* Use the raw STC value + * for the frame timestamp + */ + MMAL_PARAM_TIMESTAMP_MODE_RESET_STC, /* Use the STC timestamp + * but subtract the + * timestamp of the first + * frame sent to give a + * zero based timestamp. + */ +}; + +struct mmal_parameter_fps_range { + /**< Low end of the permitted framerate range */ + struct s32_fract fps_low; + /**< High end of the permitted framerate range */ + struct s32_fract fps_high; +}; + +/* camera configuration parameter */ +struct mmal_parameter_camera_config { + /* Parameters for setting up the image pools */ + u32 max_stills_w; /* Max size of stills capture */ + u32 max_stills_h; + u32 stills_yuv422; /* Allow YUV422 stills capture */ + u32 one_shot_stills; /* Continuous or one shot stills captures. */ + + u32 max_preview_video_w; /* Max size of the preview or video + * capture frames + */ + u32 max_preview_video_h; + u32 num_preview_video_frames; + + /** Sets the height of the circular buffer for stills capture. */ + u32 stills_capture_circular_buffer_height; + + /** Allows preview/encode to resume as fast as possible after the stills + * input frame has been received, and then processes the still frame in + * the background whilst preview/encode has resumed. + * Actual mode is controlled by MMAL_PARAMETER_CAPTURE_MODE. + */ + u32 fast_preview_resume; + + /** Selects algorithm for timestamping frames if + * there is no clock component connected. + * enum mmal_parameter_camera_config_timestamp_mode + */ + s32 use_stc_timestamp; +}; + +enum mmal_parameter_exposuremode { + MMAL_PARAM_EXPOSUREMODE_OFF, + MMAL_PARAM_EXPOSUREMODE_AUTO, + MMAL_PARAM_EXPOSUREMODE_NIGHT, + MMAL_PARAM_EXPOSUREMODE_NIGHTPREVIEW, + MMAL_PARAM_EXPOSUREMODE_BACKLIGHT, + MMAL_PARAM_EXPOSUREMODE_SPOTLIGHT, + MMAL_PARAM_EXPOSUREMODE_SPORTS, + MMAL_PARAM_EXPOSUREMODE_SNOW, + MMAL_PARAM_EXPOSUREMODE_BEACH, + MMAL_PARAM_EXPOSUREMODE_VERYLONG, + MMAL_PARAM_EXPOSUREMODE_FIXEDFPS, + MMAL_PARAM_EXPOSUREMODE_ANTISHAKE, + MMAL_PARAM_EXPOSUREMODE_FIREWORKS, +}; + +enum mmal_parameter_exposuremeteringmode { + MMAL_PARAM_EXPOSUREMETERINGMODE_AVERAGE, + MMAL_PARAM_EXPOSUREMETERINGMODE_SPOT, + MMAL_PARAM_EXPOSUREMETERINGMODE_BACKLIT, + MMAL_PARAM_EXPOSUREMETERINGMODE_MATRIX, +}; + +enum mmal_parameter_awbmode { + MMAL_PARAM_AWBMODE_OFF, + MMAL_PARAM_AWBMODE_AUTO, + MMAL_PARAM_AWBMODE_SUNLIGHT, + MMAL_PARAM_AWBMODE_CLOUDY, + MMAL_PARAM_AWBMODE_SHADE, + MMAL_PARAM_AWBMODE_TUNGSTEN, + MMAL_PARAM_AWBMODE_FLUORESCENT, + MMAL_PARAM_AWBMODE_INCANDESCENT, + MMAL_PARAM_AWBMODE_FLASH, + MMAL_PARAM_AWBMODE_HORIZON, +}; + +enum mmal_parameter_imagefx { + MMAL_PARAM_IMAGEFX_NONE, + MMAL_PARAM_IMAGEFX_NEGATIVE, + MMAL_PARAM_IMAGEFX_SOLARIZE, + MMAL_PARAM_IMAGEFX_POSTERIZE, + MMAL_PARAM_IMAGEFX_WHITEBOARD, + MMAL_PARAM_IMAGEFX_BLACKBOARD, + MMAL_PARAM_IMAGEFX_SKETCH, + MMAL_PARAM_IMAGEFX_DENOISE, + MMAL_PARAM_IMAGEFX_EMBOSS, + MMAL_PARAM_IMAGEFX_OILPAINT, + MMAL_PARAM_IMAGEFX_HATCH, + MMAL_PARAM_IMAGEFX_GPEN, + MMAL_PARAM_IMAGEFX_PASTEL, + MMAL_PARAM_IMAGEFX_WATERCOLOUR, + MMAL_PARAM_IMAGEFX_FILM, + MMAL_PARAM_IMAGEFX_BLUR, + MMAL_PARAM_IMAGEFX_SATURATION, + MMAL_PARAM_IMAGEFX_COLOURSWAP, + MMAL_PARAM_IMAGEFX_WASHEDOUT, + MMAL_PARAM_IMAGEFX_POSTERISE, + MMAL_PARAM_IMAGEFX_COLOURPOINT, + MMAL_PARAM_IMAGEFX_COLOURBALANCE, + MMAL_PARAM_IMAGEFX_CARTOON, +}; + +enum MMAL_PARAM_FLICKERAVOID { + MMAL_PARAM_FLICKERAVOID_OFF, + MMAL_PARAM_FLICKERAVOID_AUTO, + MMAL_PARAM_FLICKERAVOID_50HZ, + MMAL_PARAM_FLICKERAVOID_60HZ, + MMAL_PARAM_FLICKERAVOID_MAX = 0x7FFFFFFF +}; + +struct mmal_parameter_awbgains { + struct s32_fract r_gain; /**< Red gain */ + struct s32_fract b_gain; /**< Blue gain */ +}; + +/** Manner of video rate control */ +enum mmal_parameter_rate_control_mode { + MMAL_VIDEO_RATECONTROL_DEFAULT, + MMAL_VIDEO_RATECONTROL_VARIABLE, + MMAL_VIDEO_RATECONTROL_CONSTANT, + MMAL_VIDEO_RATECONTROL_VARIABLE_SKIP_FRAMES, + MMAL_VIDEO_RATECONTROL_CONSTANT_SKIP_FRAMES +}; + +enum mmal_video_profile { + MMAL_VIDEO_PROFILE_H263_BASELINE, + MMAL_VIDEO_PROFILE_H263_H320CODING, + MMAL_VIDEO_PROFILE_H263_BACKWARDCOMPATIBLE, + MMAL_VIDEO_PROFILE_H263_ISWV2, + MMAL_VIDEO_PROFILE_H263_ISWV3, + MMAL_VIDEO_PROFILE_H263_HIGHCOMPRESSION, + MMAL_VIDEO_PROFILE_H263_INTERNET, + MMAL_VIDEO_PROFILE_H263_INTERLACE, + MMAL_VIDEO_PROFILE_H263_HIGHLATENCY, + MMAL_VIDEO_PROFILE_MP4V_SIMPLE, + MMAL_VIDEO_PROFILE_MP4V_SIMPLESCALABLE, + MMAL_VIDEO_PROFILE_MP4V_CORE, + MMAL_VIDEO_PROFILE_MP4V_MAIN, + MMAL_VIDEO_PROFILE_MP4V_NBIT, + MMAL_VIDEO_PROFILE_MP4V_SCALABLETEXTURE, + MMAL_VIDEO_PROFILE_MP4V_SIMPLEFACE, + MMAL_VIDEO_PROFILE_MP4V_SIMPLEFBA, + MMAL_VIDEO_PROFILE_MP4V_BASICANIMATED, + MMAL_VIDEO_PROFILE_MP4V_HYBRID, + MMAL_VIDEO_PROFILE_MP4V_ADVANCEDREALTIME, + MMAL_VIDEO_PROFILE_MP4V_CORESCALABLE, + MMAL_VIDEO_PROFILE_MP4V_ADVANCEDCODING, + MMAL_VIDEO_PROFILE_MP4V_ADVANCEDCORE, + MMAL_VIDEO_PROFILE_MP4V_ADVANCEDSCALABLE, + MMAL_VIDEO_PROFILE_MP4V_ADVANCEDSIMPLE, + MMAL_VIDEO_PROFILE_H264_BASELINE, + MMAL_VIDEO_PROFILE_H264_MAIN, + MMAL_VIDEO_PROFILE_H264_EXTENDED, + MMAL_VIDEO_PROFILE_H264_HIGH, + MMAL_VIDEO_PROFILE_H264_HIGH10, + MMAL_VIDEO_PROFILE_H264_HIGH422, + MMAL_VIDEO_PROFILE_H264_HIGH444, + MMAL_VIDEO_PROFILE_H264_CONSTRAINED_BASELINE, + MMAL_VIDEO_PROFILE_DUMMY = 0x7FFFFFFF +}; + +enum mmal_video_level { + MMAL_VIDEO_LEVEL_H263_10, + MMAL_VIDEO_LEVEL_H263_20, + MMAL_VIDEO_LEVEL_H263_30, + MMAL_VIDEO_LEVEL_H263_40, + MMAL_VIDEO_LEVEL_H263_45, + MMAL_VIDEO_LEVEL_H263_50, + MMAL_VIDEO_LEVEL_H263_60, + MMAL_VIDEO_LEVEL_H263_70, + MMAL_VIDEO_LEVEL_MP4V_0, + MMAL_VIDEO_LEVEL_MP4V_0b, + MMAL_VIDEO_LEVEL_MP4V_1, + MMAL_VIDEO_LEVEL_MP4V_2, + MMAL_VIDEO_LEVEL_MP4V_3, + MMAL_VIDEO_LEVEL_MP4V_4, + MMAL_VIDEO_LEVEL_MP4V_4a, + MMAL_VIDEO_LEVEL_MP4V_5, + MMAL_VIDEO_LEVEL_MP4V_6, + MMAL_VIDEO_LEVEL_H264_1, + MMAL_VIDEO_LEVEL_H264_1b, + MMAL_VIDEO_LEVEL_H264_11, + MMAL_VIDEO_LEVEL_H264_12, + MMAL_VIDEO_LEVEL_H264_13, + MMAL_VIDEO_LEVEL_H264_2, + MMAL_VIDEO_LEVEL_H264_21, + MMAL_VIDEO_LEVEL_H264_22, + MMAL_VIDEO_LEVEL_H264_3, + MMAL_VIDEO_LEVEL_H264_31, + MMAL_VIDEO_LEVEL_H264_32, + MMAL_VIDEO_LEVEL_H264_4, + MMAL_VIDEO_LEVEL_H264_41, + MMAL_VIDEO_LEVEL_H264_42, + MMAL_VIDEO_LEVEL_H264_5, + MMAL_VIDEO_LEVEL_H264_51, + MMAL_VIDEO_LEVEL_DUMMY = 0x7FFFFFFF +}; + +struct mmal_parameter_video_profile { + enum mmal_video_profile profile; + enum mmal_video_level level; +}; + +/* video parameters */ + +enum mmal_parameter_video_type { + /** @ref MMAL_DISPLAYREGION_T */ + MMAL_PARAMETER_DISPLAYREGION = MMAL_PARAMETER_GROUP_VIDEO, + + /** @ref MMAL_PARAMETER_VIDEO_PROFILE_T */ + MMAL_PARAMETER_SUPPORTED_PROFILES, + + /** @ref MMAL_PARAMETER_VIDEO_PROFILE_T */ + MMAL_PARAMETER_PROFILE, + + /** @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_INTRAPERIOD, + + /** @ref MMAL_PARAMETER_VIDEO_RATECONTROL_T */ + MMAL_PARAMETER_RATECONTROL, + + /** @ref MMAL_PARAMETER_VIDEO_NALUNITFORMAT_T */ + MMAL_PARAMETER_NALUNITFORMAT, + + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_MINIMISE_FRAGMENTATION, + + /** @ref MMAL_PARAMETER_UINT32_T. + * Setting the value to zero resets to the default (one slice per + * frame). + */ + MMAL_PARAMETER_MB_ROWS_PER_SLICE, + + /** @ref MMAL_PARAMETER_VIDEO_LEVEL_EXTENSION_T */ + MMAL_PARAMETER_VIDEO_LEVEL_EXTENSION, + + /** @ref MMAL_PARAMETER_VIDEO_EEDE_ENABLE_T */ + MMAL_PARAMETER_VIDEO_EEDE_ENABLE, + + /** @ref MMAL_PARAMETER_VIDEO_EEDE_LOSSRATE_T */ + MMAL_PARAMETER_VIDEO_EEDE_LOSSRATE, + + /** @ref MMAL_PARAMETER_BOOLEAN_T. Request an I-frame. */ + MMAL_PARAMETER_VIDEO_REQUEST_I_FRAME, + /** @ref MMAL_PARAMETER_VIDEO_INTRA_REFRESH_T */ + MMAL_PARAMETER_VIDEO_INTRA_REFRESH, + + /** @ref MMAL_PARAMETER_BOOLEAN_T. */ + MMAL_PARAMETER_VIDEO_IMMUTABLE_INPUT, + + /** @ref MMAL_PARAMETER_UINT32_T. Run-time bit rate control */ + MMAL_PARAMETER_VIDEO_BIT_RATE, + + /** @ref MMAL_PARAMETER_FRAME_RATE_T */ + MMAL_PARAMETER_VIDEO_FRAME_RATE, + + /** @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_MIN_QUANT, + + /** @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_MAX_QUANT, + + /** @ref MMAL_PARAMETER_VIDEO_ENCODE_RC_MODEL_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_RC_MODEL, + + MMAL_PARAMETER_EXTRA_BUFFERS, /**< @ref MMAL_PARAMETER_UINT32_T. */ + /** @ref MMAL_PARAMETER_UINT32_T. + * Changing this parameter from the default can reduce frame rate + * because image buffers need to be re-pitched. + */ + MMAL_PARAMETER_VIDEO_ALIGN_HORIZ, + + /** @ref MMAL_PARAMETER_UINT32_T. + * Changing this parameter from the default can reduce frame rate + * because image buffers need to be re-pitched. + */ + MMAL_PARAMETER_VIDEO_ALIGN_VERT, + + /** @ref MMAL_PARAMETER_BOOLEAN_T. */ + MMAL_PARAMETER_VIDEO_DROPPABLE_PFRAMES, + + /** @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_INITIAL_QUANT, + + /**< @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_QP_P, + + /**< @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_RC_SLICE_DQUANT, + + /** @ref MMAL_PARAMETER_UINT32_T */ + MMAL_PARAMETER_VIDEO_ENCODE_FRAME_LIMIT_BITS, + + /** @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_PEAK_RATE, + + /* H264 specific parameters */ + + /** @ref MMAL_PARAMETER_BOOLEAN_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_DISABLE_CABAC, + + /** @ref MMAL_PARAMETER_BOOLEAN_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_LOW_LATENCY, + + /** @ref MMAL_PARAMETER_BOOLEAN_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_AU_DELIMITERS, + + /** @ref MMAL_PARAMETER_UINT32_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_DEBLOCK_IDC, + + /** @ref MMAL_PARAMETER_VIDEO_ENCODER_H264_MB_INTRA_MODES_T. */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_MB_INTRA_MODE, + + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_ENCODE_HEADER_ON_OPEN, + + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_ENCODE_PRECODE_FOR_QP, + + /** @ref MMAL_PARAMETER_VIDEO_DRM_INIT_INFO_T. */ + MMAL_PARAMETER_VIDEO_DRM_INIT_INFO, + + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_TIMESTAMP_FIFO, + + /** @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_DECODE_ERROR_CONCEALMENT, + + /** @ref MMAL_PARAMETER_VIDEO_DRM_PROTECT_BUFFER_T. */ + MMAL_PARAMETER_VIDEO_DRM_PROTECT_BUFFER, + + /** @ref MMAL_PARAMETER_BYTES_T */ + MMAL_PARAMETER_VIDEO_DECODE_CONFIG_VD3, + + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_VCL_HRD_PARAMETERS, + + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_ENCODE_H264_LOW_DELAY_HRD_FLAG, + + /**< @ref MMAL_PARAMETER_BOOLEAN_T */ + MMAL_PARAMETER_VIDEO_ENCODE_INLINE_HEADER +}; + +/** Valid mirror modes */ +enum mmal_parameter_mirror { + MMAL_PARAM_MIRROR_NONE, + MMAL_PARAM_MIRROR_VERTICAL, + MMAL_PARAM_MIRROR_HORIZONTAL, + MMAL_PARAM_MIRROR_BOTH, +}; + +enum mmal_parameter_displaytransform { + MMAL_DISPLAY_ROT0 = 0, + MMAL_DISPLAY_MIRROR_ROT0 = 1, + MMAL_DISPLAY_MIRROR_ROT180 = 2, + MMAL_DISPLAY_ROT180 = 3, + MMAL_DISPLAY_MIRROR_ROT90 = 4, + MMAL_DISPLAY_ROT270 = 5, + MMAL_DISPLAY_ROT90 = 6, + MMAL_DISPLAY_MIRROR_ROT270 = 7, +}; + +enum mmal_parameter_displaymode { + MMAL_DISPLAY_MODE_FILL = 0, + MMAL_DISPLAY_MODE_LETTERBOX = 1, +}; + +enum mmal_parameter_displayset { + MMAL_DISPLAY_SET_NONE = 0, + MMAL_DISPLAY_SET_NUM = 1, + MMAL_DISPLAY_SET_FULLSCREEN = 2, + MMAL_DISPLAY_SET_TRANSFORM = 4, + MMAL_DISPLAY_SET_DEST_RECT = 8, + MMAL_DISPLAY_SET_SRC_RECT = 0x10, + MMAL_DISPLAY_SET_MODE = 0x20, + MMAL_DISPLAY_SET_PIXEL = 0x40, + MMAL_DISPLAY_SET_NOASPECT = 0x80, + MMAL_DISPLAY_SET_LAYER = 0x100, + MMAL_DISPLAY_SET_COPYPROTECT = 0x200, + MMAL_DISPLAY_SET_ALPHA = 0x400, +}; + +/* rectangle, used lots so it gets its own struct */ +struct vchiq_mmal_rect { + s32 x; + s32 y; + s32 width; + s32 height; +}; + +struct mmal_parameter_displayregion { + /** Bitfield that indicates which fields are set and should be + * used. All other fields will maintain their current value. + * \ref MMAL_DISPLAYSET_T defines the bits that can be + * combined. + */ + u32 set; + + /** Describes the display output device, with 0 typically + * being a directly connected LCD display. The actual values + * will depend on the hardware. Code using hard-wired numbers + * (e.g. 2) is certain to fail. + */ + + u32 display_num; + /** Indicates that we are using the full device screen area, + * rather than a window of the display. If zero, then + * dest_rect is used to specify a region of the display to + * use. + */ + + s32 fullscreen; + /** Indicates any rotation or flipping used to map frames onto + * the natural display orientation. + */ + u32 transform; /* enum mmal_parameter_displaytransform */ + + /** Where to display the frame within the screen, if + * fullscreen is zero. + */ + struct vchiq_mmal_rect dest_rect; + + /** Indicates which area of the frame to display. If all + * values are zero, the whole frame will be used. + */ + struct vchiq_mmal_rect src_rect; + + /** If set to non-zero, indicates that any display scaling + * should disregard the aspect ratio of the frame region being + * displayed. + */ + s32 noaspect; + + /** Indicates how the image should be scaled to fit the + * display. \code MMAL_DISPLAY_MODE_FILL \endcode indicates + * that the image should fill the screen by potentially + * cropping the frames. Setting \code mode \endcode to \code + * MMAL_DISPLAY_MODE_LETTERBOX \endcode indicates that all the + * source region should be displayed and black bars added if + * necessary. + */ + u32 mode; /* enum mmal_parameter_displaymode */ + + /** If non-zero, defines the width of a source pixel relative + * to \code pixel_y \endcode. If zero, then pixels default to + * being square. + */ + u32 pixel_x; + + /** If non-zero, defines the height of a source pixel relative + * to \code pixel_x \endcode. If zero, then pixels default to + * being square. + */ + u32 pixel_y; + + /** Sets the relative depth of the images, with greater values + * being in front of smaller values. + */ + u32 layer; + + /** Set to non-zero to ensure copy protection is used on + * output. + */ + s32 copyprotect_required; + + /** Level of opacity of the layer, where zero is fully + * transparent and 255 is fully opaque. + */ + u32 alpha; +}; + +#define MMAL_MAX_IMAGEFX_PARAMETERS 5 + +struct mmal_parameter_imagefx_parameters { + enum mmal_parameter_imagefx effect; + u32 num_effect_params; + u32 effect_parameter[MMAL_MAX_IMAGEFX_PARAMETERS]; +}; + +#define MMAL_PARAMETER_CAMERA_INFO_MAX_CAMERAS 4 +#define MMAL_PARAMETER_CAMERA_INFO_MAX_FLASHES 2 +#define MMAL_PARAMETER_CAMERA_INFO_MAX_STR_LEN 16 + +struct mmal_parameter_camera_info_camera { + u32 port_id; + u32 max_width; + u32 max_height; + u32 lens_present; + u8 camera_name[MMAL_PARAMETER_CAMERA_INFO_MAX_STR_LEN]; +}; + +enum mmal_parameter_camera_info_flash_type { + /* Make values explicit to ensure they match values in config ini */ + MMAL_PARAMETER_CAMERA_INFO_FLASH_TYPE_XENON = 0, + MMAL_PARAMETER_CAMERA_INFO_FLASH_TYPE_LED = 1, + MMAL_PARAMETER_CAMERA_INFO_FLASH_TYPE_OTHER = 2, + MMAL_PARAMETER_CAMERA_INFO_FLASH_TYPE_MAX = 0x7FFFFFFF +}; + +struct mmal_parameter_camera_info_flash { + enum mmal_parameter_camera_info_flash_type flash_type; +}; + +struct mmal_parameter_camera_info { + u32 num_cameras; + u32 num_flashes; + struct mmal_parameter_camera_info_camera + cameras[MMAL_PARAMETER_CAMERA_INFO_MAX_CAMERAS]; + struct mmal_parameter_camera_info_flash + flashes[MMAL_PARAMETER_CAMERA_INFO_MAX_FLASHES]; +}; + +#endif diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.c b/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.c new file mode 100644 index 000000000..c89c16fb8 --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.c @@ -0,0 +1,1949 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + * + * V4L2 driver MMAL vchiq interface code + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "mmal-common.h" +#include "mmal-vchiq.h" +#include "mmal-msg.h" + +/* + * maximum number of components supported. + * This matches the maximum permitted by default on the VPU + */ +#define VCHIQ_MMAL_MAX_COMPONENTS 64 + +/* + * Timeout for synchronous msg responses in seconds. + * Helpful to increase this if stopping in the VPU debugger. + */ +#define SYNC_MSG_TIMEOUT 3 + +/*#define FULL_MSG_DUMP 1*/ + +#ifdef DEBUG +static const char *const msg_type_names[] = { + "UNKNOWN", + "QUIT", + "SERVICE_CLOSED", + "GET_VERSION", + "COMPONENT_CREATE", + "COMPONENT_DESTROY", + "COMPONENT_ENABLE", + "COMPONENT_DISABLE", + "PORT_INFO_GET", + "PORT_INFO_SET", + "PORT_ACTION", + "BUFFER_FROM_HOST", + "BUFFER_TO_HOST", + "GET_STATS", + "PORT_PARAMETER_SET", + "PORT_PARAMETER_GET", + "EVENT_TO_HOST", + "GET_CORE_STATS_FOR_PORT", + "OPAQUE_ALLOCATOR", + "CONSUME_MEM", + "LMK", + "OPAQUE_ALLOCATOR_DESC", + "DRM_GET_LHS32", + "DRM_GET_TIME", + "BUFFER_FROM_HOST_ZEROLEN", + "PORT_FLUSH", + "HOST_LOG", +}; +#endif + +static const char *const port_action_type_names[] = { + "UNKNOWN", + "ENABLE", + "DISABLE", + "FLUSH", + "CONNECT", + "DISCONNECT", + "SET_REQUIREMENTS", +}; + +#if defined(DEBUG) +#if defined(FULL_MSG_DUMP) +#define DBG_DUMP_MSG(MSG, MSG_LEN, TITLE) \ + do { \ + pr_debug(TITLE" type:%s(%d) length:%d\n", \ + msg_type_names[(MSG)->h.type], \ + (MSG)->h.type, (MSG_LEN)); \ + print_hex_dump(KERN_DEBUG, "<h.type], \ + (MSG)->h.type, (MSG_LEN)); \ + } +#endif +#else +#define DBG_DUMP_MSG(MSG, MSG_LEN, TITLE) +#endif + +struct vchiq_mmal_instance; + +/* normal message context */ +struct mmal_msg_context { + struct vchiq_mmal_instance *instance; + + /* Index in the context_map idr so that we can find the + * mmal_msg_context again when servicing the VCHI reply. + */ + int handle; + + union { + struct { + /* work struct for buffer_cb callback */ + struct work_struct work; + /* work struct for deferred callback */ + struct work_struct buffer_to_host_work; + /* mmal instance */ + struct vchiq_mmal_instance *instance; + /* mmal port */ + struct vchiq_mmal_port *port; + /* actual buffer used to store bulk reply */ + struct mmal_buffer *buffer; + /* amount of buffer used */ + unsigned long buffer_used; + /* MMAL buffer flags */ + u32 mmal_flags; + /* Presentation and Decode timestamps */ + s64 pts; + s64 dts; + + int status; /* context status */ + + } bulk; /* bulk data */ + + struct { + /* message handle to release */ + struct vchiq_header *msg_handle; + /* pointer to received message */ + struct mmal_msg *msg; + /* received message length */ + u32 msg_len; + /* completion upon reply */ + struct completion cmplt; + } sync; /* synchronous response */ + } u; + +}; + +struct vchiq_mmal_instance { + unsigned int service_handle; + + /* ensure serialised access to service */ + struct mutex vchiq_mutex; + + struct idr context_map; + /* protect accesses to context_map */ + struct mutex context_map_lock; + + struct vchiq_mmal_component component[VCHIQ_MMAL_MAX_COMPONENTS]; + + /* ordered workqueue to process all bulk operations */ + struct workqueue_struct *bulk_wq; + + /* handle for a vchiq instance */ + struct vchiq_instance *vchiq_instance; +}; + +static struct mmal_msg_context * +get_msg_context(struct vchiq_mmal_instance *instance) +{ + struct mmal_msg_context *msg_context; + int handle; + + /* todo: should this be allocated from a pool to avoid kzalloc */ + msg_context = kzalloc_obj(*msg_context); + + if (!msg_context) + return ERR_PTR(-ENOMEM); + + /* Create an ID that will be passed along with our message so + * that when we service the VCHI reply, we can look up what + * message is being replied to. + */ + mutex_lock(&instance->context_map_lock); + handle = idr_alloc(&instance->context_map, msg_context, + 0, 0, GFP_KERNEL); + mutex_unlock(&instance->context_map_lock); + + if (handle < 0) { + kfree(msg_context); + return ERR_PTR(handle); + } + + msg_context->instance = instance; + msg_context->handle = handle; + + return msg_context; +} + +static struct mmal_msg_context * +lookup_msg_context(struct vchiq_mmal_instance *instance, int handle) +{ + return idr_find(&instance->context_map, handle); +} + +static void +release_msg_context(struct mmal_msg_context *msg_context) +{ + struct vchiq_mmal_instance *instance = msg_context->instance; + + mutex_lock(&instance->context_map_lock); + idr_remove(&instance->context_map, msg_context->handle); + mutex_unlock(&instance->context_map_lock); + kfree(msg_context); +} + +/* deals with receipt of event to host message */ +static void event_to_host_cb(struct vchiq_mmal_instance *instance, + struct mmal_msg *msg, u32 msg_len) +{ + pr_debug("unhandled event\n"); + pr_debug("component:%u port type:%d num:%d cmd:0x%x length:%d\n", + msg->u.event_to_host.client_component, + msg->u.event_to_host.port_type, + msg->u.event_to_host.port_num, + msg->u.event_to_host.cmd, msg->u.event_to_host.length); +} + +/* workqueue scheduled callback + * + * we do this because it is important we do not call any other vchiq + * sync calls from within the message delivery thread + */ +static void buffer_work_cb(struct work_struct *work) +{ + struct mmal_msg_context *msg_context = + container_of(work, struct mmal_msg_context, u.bulk.work); + struct mmal_buffer *buffer = msg_context->u.bulk.buffer; + + if (!buffer) { + pr_err("%s: ctx: %p, No mmal buffer to pass details\n", + __func__, msg_context); + return; + } + + buffer->length = msg_context->u.bulk.buffer_used; + buffer->mmal_flags = msg_context->u.bulk.mmal_flags; + buffer->dts = msg_context->u.bulk.dts; + buffer->pts = msg_context->u.bulk.pts; + + atomic_dec(&msg_context->u.bulk.port->buffers_with_vpu); + + msg_context->u.bulk.port->buffer_cb(msg_context->u.bulk.instance, + msg_context->u.bulk.port, + msg_context->u.bulk.status, + msg_context->u.bulk.buffer); +} + +/* workqueue scheduled callback to handle receiving buffers + * + * VCHI will allow up to 4 bulk receives to be scheduled before blocking. + * If we block in the service_callback context then we can't process the + * VCHI_CALLBACK_BULK_RECEIVED message that would otherwise allow the blocked + * vchiq_bulk_receive() call to complete. + */ +static void buffer_to_host_work_cb(struct work_struct *work) +{ + struct mmal_msg_context *msg_context = + container_of(work, struct mmal_msg_context, + u.bulk.buffer_to_host_work); + struct vchiq_mmal_instance *instance = msg_context->instance; + unsigned long len = msg_context->u.bulk.buffer_used; + int ret; + + if (!len) + /* Dummy receive to ensure the buffers remain in order */ + len = 8; + /* queue the bulk submission */ + vchiq_use_service(instance->vchiq_instance, instance->service_handle); + ret = vchiq_bulk_receive(instance->vchiq_instance, instance->service_handle, + msg_context->u.bulk.buffer->buffer, + /* Actual receive needs to be a multiple + * of 4 bytes + */ + (len + 3) & ~3, + msg_context, + VCHIQ_BULK_MODE_CALLBACK); + + vchiq_release_service(instance->vchiq_instance, instance->service_handle); + + if (ret != 0) + pr_err("%s: ctx: %p, vchiq_bulk_receive failed %d\n", + __func__, msg_context, ret); +} + +/* enqueue a bulk receive for a given message context */ +static int bulk_receive(struct vchiq_mmal_instance *instance, + struct mmal_msg *msg, + struct mmal_msg_context *msg_context) +{ + unsigned long rd_len; + + rd_len = msg->u.buffer_from_host.buffer_header.length; + + if (!msg_context->u.bulk.buffer) { + pr_err("bulk.buffer not configured - error in buffer_from_host\n"); + + /* todo: this is a serious error, we should never have + * committed a buffer_to_host operation to the mmal + * port without the buffer to back it up (underflow + * handling) and there is no obvious way to deal with + * this - how is the mmal service going to react when + * we fail to do the xfer and reschedule a buffer when + * it arrives? perhaps a starved flag to indicate a + * waiting bulk receive? + */ + + return -EINVAL; + } + + /* ensure we do not overrun the available buffer */ + if (rd_len > msg_context->u.bulk.buffer->buffer_size) { + rd_len = msg_context->u.bulk.buffer->buffer_size; + pr_warn("short read as not enough receive buffer space\n"); + /* todo: is this the correct response, what happens to + * the rest of the message data? + */ + } + + /* store length */ + msg_context->u.bulk.buffer_used = rd_len; + msg_context->u.bulk.dts = msg->u.buffer_from_host.buffer_header.dts; + msg_context->u.bulk.pts = msg->u.buffer_from_host.buffer_header.pts; + + queue_work(msg_context->instance->bulk_wq, + &msg_context->u.bulk.buffer_to_host_work); + + return 0; +} + +/* data in message, memcpy from packet into output buffer */ +static int inline_receive(struct vchiq_mmal_instance *instance, + struct mmal_msg *msg, + struct mmal_msg_context *msg_context) +{ + memcpy(msg_context->u.bulk.buffer->buffer, + msg->u.buffer_from_host.short_data, + msg->u.buffer_from_host.payload_in_message); + + msg_context->u.bulk.buffer_used = + msg->u.buffer_from_host.payload_in_message; + + return 0; +} + +/* queue the buffer availability with MMAL_MSG_TYPE_BUFFER_FROM_HOST */ +static int +buffer_from_host(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, struct mmal_buffer *buf) +{ + struct mmal_msg_context *msg_context; + struct mmal_msg m; + int ret; + + if (!port->enabled) + return -EINVAL; + + pr_debug("instance:%u buffer:%p\n", instance->service_handle, buf); + + /* get context */ + if (!buf->msg_context) { + pr_err("%s: msg_context not allocated, buf %p\n", __func__, + buf); + return -EINVAL; + } + msg_context = buf->msg_context; + + /* store bulk message context for when data arrives */ + msg_context->u.bulk.instance = instance; + msg_context->u.bulk.port = port; + msg_context->u.bulk.buffer = buf; + msg_context->u.bulk.buffer_used = 0; + + /* initialise work structure ready to schedule callback */ + INIT_WORK(&msg_context->u.bulk.work, buffer_work_cb); + INIT_WORK(&msg_context->u.bulk.buffer_to_host_work, + buffer_to_host_work_cb); + + atomic_inc(&port->buffers_with_vpu); + + /* prep the buffer from host message */ + memset(&m, 0xbc, sizeof(m)); /* just to make debug clearer */ + + m.h.type = MMAL_MSG_TYPE_BUFFER_FROM_HOST; + m.h.magic = MMAL_MAGIC; + m.h.context = msg_context->handle; + m.h.status = 0; + + /* drvbuf is our private data passed back */ + m.u.buffer_from_host.drvbuf.magic = MMAL_MAGIC; + m.u.buffer_from_host.drvbuf.component_handle = port->component->handle; + m.u.buffer_from_host.drvbuf.port_handle = port->handle; + m.u.buffer_from_host.drvbuf.client_context = msg_context->handle; + + /* buffer header */ + m.u.buffer_from_host.buffer_header.cmd = 0; + m.u.buffer_from_host.buffer_header.data = + (u32)(unsigned long)buf->buffer; + m.u.buffer_from_host.buffer_header.alloc_size = buf->buffer_size; + m.u.buffer_from_host.buffer_header.length = 0; /* nothing used yet */ + m.u.buffer_from_host.buffer_header.offset = 0; /* no offset */ + m.u.buffer_from_host.buffer_header.flags = 0; /* no flags */ + m.u.buffer_from_host.buffer_header.pts = MMAL_TIME_UNKNOWN; + m.u.buffer_from_host.buffer_header.dts = MMAL_TIME_UNKNOWN; + + /* clear buffer type specific data */ + memset(&m.u.buffer_from_host.buffer_header_type_specific, 0, + sizeof(m.u.buffer_from_host.buffer_header_type_specific)); + + /* no payload in message */ + m.u.buffer_from_host.payload_in_message = 0; + + vchiq_use_service(instance->vchiq_instance, instance->service_handle); + + ret = vchiq_queue_kernel_message(instance->vchiq_instance, instance->service_handle, &m, + sizeof(struct mmal_msg_header) + + sizeof(m.u.buffer_from_host)); + if (ret) + atomic_dec(&port->buffers_with_vpu); + + vchiq_release_service(instance->vchiq_instance, instance->service_handle); + + return ret; +} + +/* deals with receipt of buffer to host message */ +static void buffer_to_host_cb(struct vchiq_mmal_instance *instance, + struct mmal_msg *msg, u32 msg_len) +{ + struct mmal_msg_context *msg_context; + u32 handle; + + pr_debug("%s: instance:%p msg:%p msg_len:%d\n", + __func__, instance, msg, msg_len); + + if (msg->u.buffer_from_host.drvbuf.magic == MMAL_MAGIC) { + handle = msg->u.buffer_from_host.drvbuf.client_context; + msg_context = lookup_msg_context(instance, handle); + + if (!msg_context) { + pr_err("drvbuf.client_context(%u) is invalid\n", + handle); + return; + } + } else { + pr_err("MMAL_MSG_TYPE_BUFFER_TO_HOST with bad magic\n"); + return; + } + + msg_context->u.bulk.mmal_flags = + msg->u.buffer_from_host.buffer_header.flags; + + if (msg->h.status != MMAL_MSG_STATUS_SUCCESS) { + /* message reception had an error */ + pr_warn("error %d in reply\n", msg->h.status); + + msg_context->u.bulk.status = msg->h.status; + + } else if (msg->u.buffer_from_host.buffer_header.length == 0) { + /* empty buffer */ + if (msg->u.buffer_from_host.buffer_header.flags & + MMAL_BUFFER_HEADER_FLAG_EOS) { + msg_context->u.bulk.status = + bulk_receive(instance, msg, msg_context); + if (msg_context->u.bulk.status == 0) + return; /* successful bulk submission, bulk + * completion will trigger callback + */ + } else { + /* do callback with empty buffer - not EOS though */ + msg_context->u.bulk.status = 0; + msg_context->u.bulk.buffer_used = 0; + } + } else if (msg->u.buffer_from_host.payload_in_message == 0) { + /* data is not in message, queue a bulk receive */ + msg_context->u.bulk.status = + bulk_receive(instance, msg, msg_context); + if (msg_context->u.bulk.status == 0) + return; /* successful bulk submission, bulk + * completion will trigger callback + */ + + /* failed to submit buffer, this will end badly */ + pr_err("error %d on bulk submission\n", + msg_context->u.bulk.status); + + } else if (msg->u.buffer_from_host.payload_in_message <= + MMAL_VC_SHORT_DATA) { + /* data payload within message */ + msg_context->u.bulk.status = inline_receive(instance, msg, + msg_context); + } else { + pr_err("message with invalid short payload\n"); + + /* signal error */ + msg_context->u.bulk.status = -EINVAL; + msg_context->u.bulk.buffer_used = + msg->u.buffer_from_host.payload_in_message; + } + + /* schedule the port callback */ + schedule_work(&msg_context->u.bulk.work); +} + +static void bulk_receive_cb(struct vchiq_mmal_instance *instance, + struct mmal_msg_context *msg_context) +{ + msg_context->u.bulk.status = 0; + + /* schedule the port callback */ + schedule_work(&msg_context->u.bulk.work); +} + +static void bulk_abort_cb(struct vchiq_mmal_instance *instance, + struct mmal_msg_context *msg_context) +{ + pr_err("%s: bulk ABORTED msg_context:%p\n", __func__, msg_context); + + msg_context->u.bulk.status = -EINTR; + + schedule_work(&msg_context->u.bulk.work); +} + +/* incoming event service callback */ +static int mmal_service_callback(struct vchiq_instance *vchiq_instance, + enum vchiq_reason reason, struct vchiq_header *header, + unsigned int handle, void *cb_data, + void __user *cb_userdata) +{ + struct vchiq_mmal_instance *instance = vchiq_get_service_userdata(vchiq_instance, handle); + u32 msg_len; + struct mmal_msg *msg; + struct mmal_msg_context *msg_context; + + if (!instance) { + pr_err("Message callback passed NULL instance\n"); + return 0; + } + + switch (reason) { + case VCHIQ_MESSAGE_AVAILABLE: + msg = (void *)header->data; + msg_len = header->size; + + DBG_DUMP_MSG(msg, msg_len, "<<< reply message"); + + /* handling is different for buffer messages */ + switch (msg->h.type) { + case MMAL_MSG_TYPE_BUFFER_FROM_HOST: + vchiq_release_message(vchiq_instance, handle, header); + break; + + case MMAL_MSG_TYPE_EVENT_TO_HOST: + event_to_host_cb(instance, msg, msg_len); + vchiq_release_message(vchiq_instance, handle, header); + + break; + + case MMAL_MSG_TYPE_BUFFER_TO_HOST: + buffer_to_host_cb(instance, msg, msg_len); + vchiq_release_message(vchiq_instance, handle, header); + break; + + default: + /* messages dependent on header context to complete */ + if (!msg->h.context) { + pr_err("received message context was null!\n"); + vchiq_release_message(vchiq_instance, handle, header); + break; + } + + msg_context = lookup_msg_context(instance, + msg->h.context); + if (!msg_context) { + pr_err("received invalid message context %u!\n", + msg->h.context); + vchiq_release_message(vchiq_instance, handle, header); + break; + } + + /* fill in context values */ + msg_context->u.sync.msg_handle = header; + msg_context->u.sync.msg = msg; + msg_context->u.sync.msg_len = msg_len; + + /* todo: should this check (completion_done() + * == 1) for no one waiting? or do we need a + * flag to tell us the completion has been + * interrupted so we can free the message and + * its context. This probably also solves the + * message arriving after interruption todo + * below + */ + + /* complete message so caller knows it happened */ + complete(&msg_context->u.sync.cmplt); + break; + } + + break; + + case VCHIQ_BULK_RECEIVE_DONE: + bulk_receive_cb(instance, cb_data); + break; + + case VCHIQ_BULK_RECEIVE_ABORTED: + bulk_abort_cb(instance, cb_data); + break; + + case VCHIQ_SERVICE_CLOSED: + /* TODO: consider if this requires action if received when + * driver is not explicitly closing the service + */ + break; + + default: + pr_err("Received unhandled message reason %d\n", reason); + break; + } + + return 0; +} + +static int send_synchronous_mmal_msg(struct vchiq_mmal_instance *instance, + struct mmal_msg *msg, + unsigned int payload_len, + struct mmal_msg **msg_out, + struct vchiq_header **msg_handle) +{ + struct mmal_msg_context *msg_context; + int ret; + unsigned long time_left; + + /* payload size must not cause message to exceed max size */ + if (payload_len > + (MMAL_MSG_MAX_SIZE - sizeof(struct mmal_msg_header))) { + pr_err("payload length %d exceeds max:%d\n", payload_len, + (int)(MMAL_MSG_MAX_SIZE - + sizeof(struct mmal_msg_header))); + return -EINVAL; + } + + msg_context = get_msg_context(instance); + if (IS_ERR(msg_context)) + return PTR_ERR(msg_context); + + init_completion(&msg_context->u.sync.cmplt); + + msg->h.magic = MMAL_MAGIC; + msg->h.context = msg_context->handle; + msg->h.status = 0; + + DBG_DUMP_MSG(msg, (sizeof(struct mmal_msg_header) + payload_len), + ">>> sync message"); + + vchiq_use_service(instance->vchiq_instance, instance->service_handle); + + ret = vchiq_queue_kernel_message(instance->vchiq_instance, instance->service_handle, msg, + sizeof(struct mmal_msg_header) + + payload_len); + + vchiq_release_service(instance->vchiq_instance, instance->service_handle); + + if (ret) { + pr_err("error %d queuing message\n", ret); + release_msg_context(msg_context); + return ret; + } + + time_left = wait_for_completion_timeout(&msg_context->u.sync.cmplt, + SYNC_MSG_TIMEOUT * HZ); + if (time_left == 0) { + pr_err("timed out waiting for sync completion\n"); + ret = -ETIME; + /* todo: what happens if the message arrives after aborting */ + release_msg_context(msg_context); + return ret; + } + + *msg_out = msg_context->u.sync.msg; + *msg_handle = msg_context->u.sync.msg_handle; + release_msg_context(msg_context); + + return 0; +} + +static void dump_port_info(struct vchiq_mmal_port *port) +{ + pr_debug("port handle:0x%x enabled:%d\n", port->handle, port->enabled); + + pr_debug("buffer minimum num:%d size:%d align:%d\n", + port->minimum_buffer.num, + port->minimum_buffer.size, port->minimum_buffer.alignment); + + pr_debug("buffer recommended num:%d size:%d align:%d\n", + port->recommended_buffer.num, + port->recommended_buffer.size, + port->recommended_buffer.alignment); + + pr_debug("buffer current values num:%d size:%d align:%d\n", + port->current_buffer.num, + port->current_buffer.size, port->current_buffer.alignment); + + pr_debug("elementary stream: type:%d encoding:0x%x variant:0x%x\n", + port->format.type, + port->format.encoding, port->format.encoding_variant); + + pr_debug(" bitrate:%d flags:0x%x\n", + port->format.bitrate, port->format.flags); + + if (port->format.type == MMAL_ES_TYPE_VIDEO) { + pr_debug + ("es video format: width:%d height:%d colourspace:0x%x\n", + port->es.video.width, port->es.video.height, + port->es.video.color_space); + + pr_debug(" : crop xywh %d,%d,%d,%d\n", + port->es.video.crop.x, + port->es.video.crop.y, + port->es.video.crop.width, port->es.video.crop.height); + pr_debug(" : framerate %d/%d aspect %d/%d\n", + port->es.video.frame_rate.numerator, + port->es.video.frame_rate.denominator, + port->es.video.par.numerator, port->es.video.par.denominator); + } +} + +static void port_to_mmal_msg(struct vchiq_mmal_port *port, struct mmal_port *p) +{ + /* todo do readonly fields need setting at all? */ + p->type = port->type; + p->index = port->index; + p->index_all = 0; + p->is_enabled = port->enabled; + p->buffer_num_min = port->minimum_buffer.num; + p->buffer_size_min = port->minimum_buffer.size; + p->buffer_alignment_min = port->minimum_buffer.alignment; + p->buffer_num_recommended = port->recommended_buffer.num; + p->buffer_size_recommended = port->recommended_buffer.size; + + /* only three writable fields in a port */ + p->buffer_num = port->current_buffer.num; + p->buffer_size = port->current_buffer.size; + p->userdata = (u32)(unsigned long)port; +} + +static int port_info_set(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + pr_debug("setting port info port %p\n", port); + if (!port) + return -1; + dump_port_info(port); + + m.h.type = MMAL_MSG_TYPE_PORT_INFO_SET; + + m.u.port_info_set.component_handle = port->component->handle; + m.u.port_info_set.port_type = port->type; + m.u.port_info_set.port_index = port->index; + + port_to_mmal_msg(port, &m.u.port_info_set.port); + + /* elementary stream format setup */ + m.u.port_info_set.format.type = port->format.type; + m.u.port_info_set.format.encoding = port->format.encoding; + m.u.port_info_set.format.encoding_variant = + port->format.encoding_variant; + m.u.port_info_set.format.bitrate = port->format.bitrate; + m.u.port_info_set.format.flags = port->format.flags; + + memcpy(&m.u.port_info_set.es, &port->es, + sizeof(union mmal_es_specific_format)); + + m.u.port_info_set.format.extradata_size = port->format.extradata_size; + memcpy(&m.u.port_info_set.extradata, port->format.extradata, + port->format.extradata_size); + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.port_info_set), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_INFO_SET) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + /* return operation status */ + ret = -rmsg->u.port_info_get_reply.status; + + pr_debug("%s:result:%d component:0x%x port:%d\n", __func__, ret, + port->component->handle, port->handle); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* use port info get message to retrieve port information */ +static int port_info_get(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + /* port info time */ + m.h.type = MMAL_MSG_TYPE_PORT_INFO_GET; + m.u.port_info_get.component_handle = port->component->handle; + m.u.port_info_get.port_type = port->type; + m.u.port_info_get.index = port->index; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.port_info_get), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_INFO_GET) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + /* return operation status */ + ret = -rmsg->u.port_info_get_reply.status; + if (ret != MMAL_MSG_STATUS_SUCCESS) + goto release_msg; + + if (rmsg->u.port_info_get_reply.port.is_enabled == 0) + port->enabled = false; + else + port->enabled = true; + + /* copy the values out of the message */ + port->handle = rmsg->u.port_info_get_reply.port_handle; + + /* port type and index cached to use on port info set because + * it does not use a port handle + */ + port->type = rmsg->u.port_info_get_reply.port_type; + port->index = rmsg->u.port_info_get_reply.port_index; + + port->minimum_buffer.num = + rmsg->u.port_info_get_reply.port.buffer_num_min; + port->minimum_buffer.size = + rmsg->u.port_info_get_reply.port.buffer_size_min; + port->minimum_buffer.alignment = + rmsg->u.port_info_get_reply.port.buffer_alignment_min; + + port->recommended_buffer.alignment = + rmsg->u.port_info_get_reply.port.buffer_alignment_min; + port->recommended_buffer.num = + rmsg->u.port_info_get_reply.port.buffer_num_recommended; + + port->current_buffer.num = rmsg->u.port_info_get_reply.port.buffer_num; + port->current_buffer.size = + rmsg->u.port_info_get_reply.port.buffer_size; + + /* stream format */ + port->format.type = rmsg->u.port_info_get_reply.format.type; + port->format.encoding = rmsg->u.port_info_get_reply.format.encoding; + port->format.encoding_variant = + rmsg->u.port_info_get_reply.format.encoding_variant; + port->format.bitrate = rmsg->u.port_info_get_reply.format.bitrate; + port->format.flags = rmsg->u.port_info_get_reply.format.flags; + + /* elementary stream format */ + memcpy(&port->es, + &rmsg->u.port_info_get_reply.es, + sizeof(union mmal_es_specific_format)); + port->format.es = &port->es; + + port->format.extradata_size = + rmsg->u.port_info_get_reply.format.extradata_size; + memcpy(port->format.extradata, + rmsg->u.port_info_get_reply.extradata, + port->format.extradata_size); + + pr_debug("received port info\n"); + dump_port_info(port); + +release_msg: + + pr_debug("%s:result:%d component:0x%x port:%d\n", + __func__, ret, port->component->handle, port->handle); + + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* create component on vc */ +static int create_component(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component, + const char *name) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + /* build component create message */ + m.h.type = MMAL_MSG_TYPE_COMPONENT_CREATE; + m.u.component_create.client_component = component->client_component; + strscpy_pad(m.u.component_create.name, name, + sizeof(m.u.component_create.name)); + m.u.component_create.pid = 0; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.component_create), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != m.h.type) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.component_create_reply.status; + if (ret != MMAL_MSG_STATUS_SUCCESS) + goto release_msg; + + /* a valid component response received */ + component->handle = rmsg->u.component_create_reply.component_handle; + component->inputs = rmsg->u.component_create_reply.input_num; + component->outputs = rmsg->u.component_create_reply.output_num; + component->clocks = rmsg->u.component_create_reply.clock_num; + + pr_debug("Component handle:0x%x in:%d out:%d clock:%d\n", + component->handle, + component->inputs, component->outputs, component->clocks); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* destroys a component on vc */ +static int destroy_component(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_COMPONENT_DESTROY; + m.u.component_destroy.component_handle = component->handle; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.component_destroy), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != m.h.type) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.component_destroy_reply.status; + +release_msg: + + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* enable a component on vc */ +static int enable_component(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_COMPONENT_ENABLE; + m.u.component_enable.component_handle = component->handle; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.component_enable), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != m.h.type) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.component_enable_reply.status; + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* disable a component on vc */ +static int disable_component(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_COMPONENT_DISABLE; + m.u.component_disable.component_handle = component->handle; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.component_disable), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != m.h.type) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.component_disable_reply.status; + +release_msg: + + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* get version of mmal implementation */ +static int get_version(struct vchiq_mmal_instance *instance, + u32 *major_out, u32 *minor_out) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_GET_VERSION; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.version), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != m.h.type) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + *major_out = rmsg->u.version.major; + *minor_out = rmsg->u.version.minor; + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* do a port action with a port as a parameter */ +static int port_action_port(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + enum mmal_msg_port_action_type action_type) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_PORT_ACTION; + m.u.port_action_port.component_handle = port->component->handle; + m.u.port_action_port.port_handle = port->handle; + m.u.port_action_port.action = action_type; + + port_to_mmal_msg(port, &m.u.port_action_port.port); + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.port_action_port), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_ACTION) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.port_action_reply.status; + + pr_debug("%s:result:%d component:0x%x port:%d action:%s(%d)\n", + __func__, + ret, port->component->handle, port->handle, + port_action_type_names[action_type], action_type); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* do a port action with handles as parameters */ +static int port_action_handle(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + enum mmal_msg_port_action_type action_type, + u32 connect_component_handle, + u32 connect_port_handle) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_PORT_ACTION; + + m.u.port_action_handle.component_handle = port->component->handle; + m.u.port_action_handle.port_handle = port->handle; + m.u.port_action_handle.action = action_type; + + m.u.port_action_handle.connect_component_handle = + connect_component_handle; + m.u.port_action_handle.connect_port_handle = connect_port_handle; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(m.u.port_action_handle), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_ACTION) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.port_action_reply.status; + + pr_debug("%s:result:%d component:0x%x port:%d action:%s(%d) connect component:0x%x connect port:%d\n", + __func__, + ret, port->component->handle, port->handle, + port_action_type_names[action_type], + action_type, connect_component_handle, connect_port_handle); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +static int port_parameter_set(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter_id, void *value, u32 value_size) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_PORT_PARAMETER_SET; + + m.u.port_parameter_set.component_handle = port->component->handle; + m.u.port_parameter_set.port_handle = port->handle; + m.u.port_parameter_set.id = parameter_id; + m.u.port_parameter_set.size = (2 * sizeof(u32)) + value_size; + memcpy(&m.u.port_parameter_set.value, value, value_size); + + ret = send_synchronous_mmal_msg(instance, &m, + (4 * sizeof(u32)) + value_size, + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_PARAMETER_SET) { + /* got an unexpected message type in reply */ + ret = -EINVAL; + goto release_msg; + } + + ret = -rmsg->u.port_parameter_set_reply.status; + + pr_debug("%s:result:%d component:0x%x port:%d parameter:%d\n", + __func__, + ret, port->component->handle, port->handle, parameter_id); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +static int port_parameter_get(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter_id, void *value, u32 *value_size) +{ + int ret; + struct mmal_msg m; + struct mmal_msg *rmsg; + struct vchiq_header *rmsg_handle; + + m.h.type = MMAL_MSG_TYPE_PORT_PARAMETER_GET; + + m.u.port_parameter_get.component_handle = port->component->handle; + m.u.port_parameter_get.port_handle = port->handle; + m.u.port_parameter_get.id = parameter_id; + m.u.port_parameter_get.size = (2 * sizeof(u32)) + *value_size; + + ret = send_synchronous_mmal_msg(instance, &m, + sizeof(struct + mmal_msg_port_parameter_get), + &rmsg, &rmsg_handle); + if (ret) + return ret; + + if (rmsg->h.type != MMAL_MSG_TYPE_PORT_PARAMETER_GET) { + /* got an unexpected message type in reply */ + pr_err("Incorrect reply type %d\n", rmsg->h.type); + ret = -EINVAL; + goto release_msg; + } + + ret = rmsg->u.port_parameter_get_reply.status; + + /* port_parameter_get_reply.size includes the header, + * whilst *value_size doesn't. + */ + rmsg->u.port_parameter_get_reply.size -= (2 * sizeof(u32)); + + if (ret || rmsg->u.port_parameter_get_reply.size > *value_size) { + /* Copy only as much as we have space for + * but report true size of parameter + */ + memcpy(value, &rmsg->u.port_parameter_get_reply.value, + *value_size); + } else { + memcpy(value, &rmsg->u.port_parameter_get_reply.value, + rmsg->u.port_parameter_get_reply.size); + } + /* Always report the size of the returned parameter to the caller */ + *value_size = rmsg->u.port_parameter_get_reply.size; + + pr_debug("%s:result:%d component:0x%x port:%d parameter:%d\n", __func__, + ret, port->component->handle, port->handle, parameter_id); + +release_msg: + vchiq_release_message(instance->vchiq_instance, instance->service_handle, rmsg_handle); + + return ret; +} + +/* disables a port and drains buffers from it */ +static int port_disable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + int ret; + struct list_head *q, *buf_head; + unsigned long flags = 0; + + if (!port->enabled) + return 0; + + port->enabled = false; + + ret = port_action_port(instance, port, + MMAL_MSG_PORT_ACTION_TYPE_DISABLE); + if (ret == 0) { + /* + * Drain all queued buffers on port. This should only + * apply to buffers that have been queued before the port + * has been enabled. If the port has been enabled and buffers + * passed, then the buffers should have been removed from this + * list, and we should get the relevant callbacks via VCHIQ + * to release the buffers. + */ + spin_lock_irqsave(&port->slock, flags); + + list_for_each_safe(buf_head, q, &port->buffers) { + struct mmal_buffer *mmalbuf; + + mmalbuf = list_entry(buf_head, struct mmal_buffer, + list); + list_del(buf_head); + if (port->buffer_cb) { + mmalbuf->length = 0; + mmalbuf->mmal_flags = 0; + mmalbuf->dts = MMAL_TIME_UNKNOWN; + mmalbuf->pts = MMAL_TIME_UNKNOWN; + port->buffer_cb(instance, + port, 0, mmalbuf); + } + } + + spin_unlock_irqrestore(&port->slock, flags); + + ret = port_info_get(instance, port); + } + + return ret; +} + +/* enable a port */ +static int port_enable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + unsigned int hdr_count; + struct list_head *q, *buf_head; + int ret; + + if (port->enabled) + return 0; + + ret = port_action_port(instance, port, + MMAL_MSG_PORT_ACTION_TYPE_ENABLE); + if (ret) + goto done; + + port->enabled = true; + + if (port->buffer_cb) { + /* send buffer headers to videocore */ + hdr_count = 1; + list_for_each_safe(buf_head, q, &port->buffers) { + struct mmal_buffer *mmalbuf; + + mmalbuf = list_entry(buf_head, struct mmal_buffer, + list); + ret = buffer_from_host(instance, port, mmalbuf); + if (ret) + goto done; + + list_del(buf_head); + hdr_count++; + if (hdr_count > port->current_buffer.num) + break; + } + } + + ret = port_info_get(instance, port); + +done: + return ret; +} + +/* ------------------------------------------------------------------ + * Exported API + *------------------------------------------------------------------ + */ + +int vchiq_mmal_port_set_format(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + ret = port_info_set(instance, port); + if (ret) + goto release_unlock; + + /* read what has actually been set */ + ret = port_info_get(instance, port); + +release_unlock: + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_set_format); + +int vchiq_mmal_port_parameter_set(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter, void *value, u32 value_size) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + ret = port_parameter_set(instance, port, parameter, value, value_size); + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_parameter_set); + +int vchiq_mmal_port_parameter_get(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter, void *value, u32 *value_size) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + ret = port_parameter_get(instance, port, parameter, value, value_size); + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_parameter_get); + +/* enable a port + * + * enables a port and queues buffers for satisfying callbacks if we + * provide a callback handler + */ +int vchiq_mmal_port_enable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + vchiq_mmal_buffer_cb buffer_cb) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + /* already enabled - noop */ + if (port->enabled) { + ret = 0; + goto unlock; + } + + port->buffer_cb = buffer_cb; + + ret = port_enable(instance, port); + +unlock: + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_enable); + +int vchiq_mmal_port_disable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + if (!port->enabled) { + mutex_unlock(&instance->vchiq_mutex); + return 0; + } + + ret = port_disable(instance, port); + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_disable); + +/* ports will be connected in a tunneled manner so data buffers + * are not handled by client. + */ +int vchiq_mmal_port_connect_tunnel(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *src, + struct vchiq_mmal_port *dst) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + /* disconnect ports if connected */ + if (src->connected) { + ret = port_disable(instance, src); + if (ret) { + pr_err("failed disabling src port(%d)\n", ret); + goto release_unlock; + } + + /* do not need to disable the destination port as they + * are connected and it is done automatically + */ + + ret = port_action_handle(instance, src, + MMAL_MSG_PORT_ACTION_TYPE_DISCONNECT, + src->connected->component->handle, + src->connected->handle); + if (ret < 0) { + pr_err("failed disconnecting src port\n"); + goto release_unlock; + } + src->connected->enabled = false; + src->connected = NULL; + } + + if (!dst) { + /* do not make new connection */ + ret = 0; + pr_debug("not making new connection\n"); + goto release_unlock; + } + + /* copy src port format to dst */ + dst->format.encoding = src->format.encoding; + dst->es.video.width = src->es.video.width; + dst->es.video.height = src->es.video.height; + dst->es.video.crop.x = src->es.video.crop.x; + dst->es.video.crop.y = src->es.video.crop.y; + dst->es.video.crop.width = src->es.video.crop.width; + dst->es.video.crop.height = src->es.video.crop.height; + dst->es.video.frame_rate.numerator = src->es.video.frame_rate.numerator; + dst->es.video.frame_rate.denominator = src->es.video.frame_rate.denominator; + + /* set new format */ + ret = port_info_set(instance, dst); + if (ret) { + pr_debug("setting port info failed\n"); + goto release_unlock; + } + + /* read what has actually been set */ + ret = port_info_get(instance, dst); + if (ret) { + pr_debug("read back port info failed\n"); + goto release_unlock; + } + + /* connect two ports together */ + ret = port_action_handle(instance, src, + MMAL_MSG_PORT_ACTION_TYPE_CONNECT, + dst->component->handle, dst->handle); + if (ret < 0) { + pr_debug("connecting port %d:%d to %d:%d failed\n", + src->component->handle, src->handle, + dst->component->handle, dst->handle); + goto release_unlock; + } + src->connected = dst; + +release_unlock: + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_port_connect_tunnel); + +int vchiq_mmal_submit_buffer(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + struct mmal_buffer *buffer) +{ + unsigned long flags = 0; + int ret; + + ret = buffer_from_host(instance, port, buffer); + if (ret == -EINVAL) { + /* Port is disabled. Queue for when it is enabled. */ + spin_lock_irqsave(&port->slock, flags); + list_add_tail(&buffer->list, &port->buffers); + spin_unlock_irqrestore(&port->slock, flags); + } + + return 0; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_submit_buffer); + +int mmal_vchi_buffer_init(struct vchiq_mmal_instance *instance, + struct mmal_buffer *buf) +{ + struct mmal_msg_context *msg_context = get_msg_context(instance); + + if (IS_ERR(msg_context)) + return (PTR_ERR(msg_context)); + + buf->msg_context = msg_context; + return 0; +} +EXPORT_SYMBOL_GPL(mmal_vchi_buffer_init); + +int mmal_vchi_buffer_cleanup(struct mmal_buffer *buf) +{ + struct mmal_msg_context *msg_context = buf->msg_context; + + if (msg_context) + release_msg_context(msg_context); + buf->msg_context = NULL; + + return 0; +} +EXPORT_SYMBOL_GPL(mmal_vchi_buffer_cleanup); + +/* Initialise a mmal component and its ports + * + */ +int vchiq_mmal_component_init(struct vchiq_mmal_instance *instance, + const char *name, + struct vchiq_mmal_component **component_out) +{ + int ret; + int idx; /* port index */ + struct vchiq_mmal_component *component = NULL; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + for (idx = 0; idx < VCHIQ_MMAL_MAX_COMPONENTS; idx++) { + if (!instance->component[idx].in_use) { + component = &instance->component[idx]; + component->in_use = true; + break; + } + } + + if (!component) { + ret = -EINVAL; /* todo is this correct error? */ + goto unlock; + } + + /* We need a handle to reference back to our component structure. + * Use the array index in instance->component rather than rolling + * another IDR. + */ + component->client_component = idx; + + ret = create_component(instance, component, name); + if (ret < 0) { + pr_err("%s: failed to create component %d (Not enough GPU mem?)\n", + __func__, ret); + goto unlock; + } + + /* ports info needs gathering */ + component->control.type = MMAL_PORT_TYPE_CONTROL; + component->control.index = 0; + component->control.component = component; + spin_lock_init(&component->control.slock); + INIT_LIST_HEAD(&component->control.buffers); + ret = port_info_get(instance, &component->control); + if (ret < 0) + goto release_component; + + for (idx = 0; idx < component->inputs; idx++) { + component->input[idx].type = MMAL_PORT_TYPE_INPUT; + component->input[idx].index = idx; + component->input[idx].component = component; + spin_lock_init(&component->input[idx].slock); + INIT_LIST_HEAD(&component->input[idx].buffers); + ret = port_info_get(instance, &component->input[idx]); + if (ret < 0) + goto release_component; + } + + for (idx = 0; idx < component->outputs; idx++) { + component->output[idx].type = MMAL_PORT_TYPE_OUTPUT; + component->output[idx].index = idx; + component->output[idx].component = component; + spin_lock_init(&component->output[idx].slock); + INIT_LIST_HEAD(&component->output[idx].buffers); + ret = port_info_get(instance, &component->output[idx]); + if (ret < 0) + goto release_component; + } + + for (idx = 0; idx < component->clocks; idx++) { + component->clock[idx].type = MMAL_PORT_TYPE_CLOCK; + component->clock[idx].index = idx; + component->clock[idx].component = component; + spin_lock_init(&component->clock[idx].slock); + INIT_LIST_HEAD(&component->clock[idx].buffers); + ret = port_info_get(instance, &component->clock[idx]); + if (ret < 0) + goto release_component; + } + + *component_out = component; + + mutex_unlock(&instance->vchiq_mutex); + + return 0; + +release_component: + destroy_component(instance, component); +unlock: + if (component) + component->in_use = false; + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_component_init); + +/* + * cause a mmal component to be destroyed + */ +int vchiq_mmal_component_finalise(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + if (component->enabled) + ret = disable_component(instance, component); + + ret = destroy_component(instance, component); + + component->in_use = false; + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_component_finalise); + +/* + * cause a mmal component to be enabled + */ +int vchiq_mmal_component_enable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + if (component->enabled) { + mutex_unlock(&instance->vchiq_mutex); + return 0; + } + + ret = enable_component(instance, component); + if (ret == 0) + component->enabled = true; + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_component_enable); + +/* + * cause a mmal component to be enabled + */ +int vchiq_mmal_component_disable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + if (!component->enabled) { + mutex_unlock(&instance->vchiq_mutex); + return 0; + } + + ret = disable_component(instance, component); + if (ret == 0) + component->enabled = false; + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_component_disable); + +int vchiq_mmal_version(struct vchiq_mmal_instance *instance, + u32 *major_out, u32 *minor_out) +{ + int ret; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + ret = get_version(instance, major_out, minor_out); + + mutex_unlock(&instance->vchiq_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_version); + +int vchiq_mmal_finalise(struct vchiq_mmal_instance *instance) +{ + int status = 0; + + if (!instance) + return -EINVAL; + + if (mutex_lock_interruptible(&instance->vchiq_mutex)) + return -EINTR; + + vchiq_use_service(instance->vchiq_instance, instance->service_handle); + + status = vchiq_close_service(instance->vchiq_instance, instance->service_handle); + if (status != 0) + pr_err("mmal-vchiq: VCHIQ close failed\n"); + + mutex_unlock(&instance->vchiq_mutex); + + vchiq_shutdown(instance->vchiq_instance); + destroy_workqueue(instance->bulk_wq); + + idr_destroy(&instance->context_map); + + kfree(instance); + + return status; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_finalise); + +int vchiq_mmal_init(struct device *dev, struct vchiq_mmal_instance **out_instance) +{ + int status; + int err = -ENODEV; + struct vchiq_mmal_instance *instance; + struct vchiq_instance *vchiq_instance; + struct vchiq_service_params_kernel params = { + .version = VC_MMAL_VER, + .version_min = VC_MMAL_MIN_VER, + .fourcc = VCHIQ_MAKE_FOURCC('m', 'm', 'a', 'l'), + .callback = mmal_service_callback, + .userdata = NULL, + }; + struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(dev->parent); + + /* compile time checks to ensure structure size as they are + * directly (de)serialised from memory. + */ + + /* ensure the header structure has packed to the correct size */ + BUILD_BUG_ON(sizeof(struct mmal_msg_header) != 24); + + /* ensure message structure does not exceed maximum length */ + BUILD_BUG_ON(sizeof(struct mmal_msg) > MMAL_MSG_MAX_SIZE); + + /* mmal port struct is correct size */ + BUILD_BUG_ON(sizeof(struct mmal_port) != 64); + + /* create a vchi instance */ + status = vchiq_initialise(&mgmt->state, &vchiq_instance); + if (status) { + pr_err("Failed to initialise VCHI instance (status=%d)\n", + status); + return -EIO; + } + + status = vchiq_connect(vchiq_instance); + if (status) { + pr_err("Failed to connect VCHI instance (status=%d)\n", status); + err = -EIO; + goto err_shutdown_vchiq; + } + + instance = kzalloc_obj(*instance); + + if (!instance) { + err = -ENOMEM; + goto err_shutdown_vchiq; + } + + mutex_init(&instance->vchiq_mutex); + + instance->vchiq_instance = vchiq_instance; + + mutex_init(&instance->context_map_lock); + idr_init_base(&instance->context_map, 1); + + params.userdata = instance; + + instance->bulk_wq = alloc_ordered_workqueue("mmal-vchiq", + WQ_MEM_RECLAIM); + if (!instance->bulk_wq) + goto err_free; + + status = vchiq_open_service(vchiq_instance, ¶ms, + &instance->service_handle); + if (status) { + pr_err("Failed to open VCHI service connection (status=%d)\n", + status); + goto err_close_services; + } + + vchiq_release_service(instance->vchiq_instance, instance->service_handle); + + *out_instance = instance; + + return 0; + +err_close_services: + vchiq_close_service(instance->vchiq_instance, instance->service_handle); + destroy_workqueue(instance->bulk_wq); +err_free: + kfree(instance); +err_shutdown_vchiq: + vchiq_shutdown(vchiq_instance); + return err; +} +EXPORT_SYMBOL_GPL(vchiq_mmal_init); + +MODULE_DESCRIPTION("BCM2835 MMAL VCHIQ interface"); +MODULE_AUTHOR("Dave Stevenson, "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.h b/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.h new file mode 100644 index 000000000..8c3959f6f --- /dev/null +++ b/drivers/platform/raspberrypi/vchiq-mmal/mmal-vchiq.h @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Broadcom BCM2835 V4L2 driver + * + * Copyright © 2013 Raspberry Pi (Trading) Ltd. + * + * Authors: Vincent Sanders @ Collabora + * Dave Stevenson @ Broadcom + * (now dave.stevenson@raspberrypi.org) + * Simon Mellor @ Broadcom + * Luke Diamand @ Broadcom + * + * MMAL interface to VCHIQ message passing + */ + +#ifndef MMAL_VCHIQ_H +#define MMAL_VCHIQ_H + +#include "mmal-common.h" +#include "mmal-msg-format.h" + +#define MAX_PORT_COUNT 4 + +/* Maximum size of the format extradata. */ +#define MMAL_FORMAT_EXTRADATA_MAX_SIZE 128 + +struct vchiq_mmal_instance; +struct device; + +enum vchiq_mmal_es_type { + MMAL_ES_TYPE_UNKNOWN, /**< Unknown elementary stream type */ + MMAL_ES_TYPE_CONTROL, /**< Elementary stream of control commands */ + MMAL_ES_TYPE_AUDIO, /**< Audio elementary stream */ + MMAL_ES_TYPE_VIDEO, /**< Video elementary stream */ + MMAL_ES_TYPE_SUBPICTURE /**< Sub-picture elementary stream */ +}; + +struct vchiq_mmal_port_buffer { + unsigned int num; /* number of buffers */ + u32 size; /* size of buffers */ + u32 alignment; /* alignment of buffers */ +}; + +struct vchiq_mmal_port; + +typedef void (*vchiq_mmal_buffer_cb)(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + int status, struct mmal_buffer *buffer); + +struct vchiq_mmal_port { + bool enabled; + u32 handle; + u32 type; /* port type, cached to use on port info set */ + u32 index; /* port index, cached to use on port info set */ + + /* component port belongs to, allows simple deref */ + struct vchiq_mmal_component *component; + + struct vchiq_mmal_port *connected; /* port connected to */ + + /* buffer info */ + struct vchiq_mmal_port_buffer minimum_buffer; + struct vchiq_mmal_port_buffer recommended_buffer; + struct vchiq_mmal_port_buffer current_buffer; + + /* stream format */ + struct mmal_es_format_local format; + /* elementary stream format */ + union mmal_es_specific_format es; + + /* data buffers to fill */ + struct list_head buffers; + /* lock to serialise adding and removing buffers from list */ + spinlock_t slock; + + /* Count of buffers the VPU has yet to return */ + atomic_t buffers_with_vpu; + /* callback on buffer completion */ + vchiq_mmal_buffer_cb buffer_cb; + /* callback context */ + void *cb_ctx; +}; + +struct vchiq_mmal_component { + bool in_use; + bool enabled; + u32 handle; /* VideoCore handle for component */ + u32 inputs; /* Number of input ports */ + u32 outputs; /* Number of output ports */ + u32 clocks; /* Number of clock ports */ + struct vchiq_mmal_port control; /* control port */ + struct vchiq_mmal_port input[MAX_PORT_COUNT]; /* input ports */ + struct vchiq_mmal_port output[MAX_PORT_COUNT]; /* output ports */ + struct vchiq_mmal_port clock[MAX_PORT_COUNT]; /* clock ports */ + u32 client_component; /* Used to ref back to client struct */ +}; + +int vchiq_mmal_init(struct device *dev, struct vchiq_mmal_instance **out_instance); +int vchiq_mmal_finalise(struct vchiq_mmal_instance *instance); + +/* Initialise a mmal component and its ports + * + */ +int vchiq_mmal_component_init(struct vchiq_mmal_instance *instance, + const char *name, struct vchiq_mmal_component **component_out); + +int vchiq_mmal_component_finalise(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component); + +int vchiq_mmal_component_enable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component); + +int vchiq_mmal_component_disable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_component *component); + +/* enable a mmal port + * + * enables a port and, if a buffer callback provided, enqueues buffer + * headers as appropriate for the port. + */ +int vchiq_mmal_port_enable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + vchiq_mmal_buffer_cb buffer_cb); + +/* disable a port + * + * disable a port will dequeue any pending buffers + */ +int vchiq_mmal_port_disable(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port); + +int vchiq_mmal_port_parameter_set(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter, + void *value, + u32 value_size); + +int vchiq_mmal_port_parameter_get(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + u32 parameter, + void *value, + u32 *value_size); + +int vchiq_mmal_port_set_format(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port); + +int vchiq_mmal_port_connect_tunnel(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *src, + struct vchiq_mmal_port *dst); + +int vchiq_mmal_version(struct vchiq_mmal_instance *instance, + u32 *major_out, + u32 *minor_out); + +int vchiq_mmal_submit_buffer(struct vchiq_mmal_instance *instance, + struct vchiq_mmal_port *port, + struct mmal_buffer *buf); + +int mmal_vchi_buffer_init(struct vchiq_mmal_instance *instance, + struct mmal_buffer *buf); +int mmal_vchi_buffer_cleanup(struct mmal_buffer *buf); +#endif /* MMAL_VCHIQ_H */ diff --git a/drivers/platform/surface/Kconfig b/drivers/platform/surface/Kconfig new file mode 100644 index 000000000..f775c6ca1 --- /dev/null +++ b/drivers/platform/surface/Kconfig @@ -0,0 +1,234 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Microsoft Surface Platform-Specific Drivers +# + +menuconfig SURFACE_PLATFORMS + bool "Microsoft Surface Platform-Specific Device Drivers" + depends on ARM64 || X86 || COMPILE_TEST + default y if ARM64 || X86 + help + Say Y here to get to see options for platform-specific device drivers + for Microsoft Surface devices. This option alone does not add any + kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if SURFACE_PLATFORMS + +config SURFACE3_WMI + tristate "Surface 3 WMI Driver" + depends on ACPI_WMI + depends on DMI + depends on INPUT + depends on SPI + help + Say Y here if you have a Surface 3. + + To compile this driver as a module, choose M here: the module will + be called surface3-wmi. + +config SURFACE_3_POWER_OPREGION + tristate "Surface 3 battery platform operation region support" + depends on ACPI + depends on I2C + help + This driver provides support for ACPI operation + region of the Surface 3 battery platform driver. + +config SURFACE_ACPI_NOTIFY + tristate "Surface ACPI Notify Driver" + depends on SURFACE_AGGREGATOR + help + Surface ACPI Notify (SAN) driver for Microsoft Surface devices. + + This driver provides support for the ACPI interface (called SAN) of + the Surface System Aggregator Module (SSAM) EC. This interface is used + on 5th- and 6th-generation Microsoft Surface devices (including + Surface Pro 5 and 6, Surface Book 2, Surface Laptops 1 and 2, and in + reduced functionality on the Surface Laptop 3) to execute SSAM + requests directly from ACPI code, as well as receive SSAM events and + turn them into ACPI notifications. It essentially acts as a + translation layer between the SSAM controller and ACPI. + + Specifically, this driver may be needed for battery status reporting, + thermal sensor access, and real-time clock information, depending on + the Surface device in question. + +config SURFACE_AGGREGATOR_CDEV + tristate "Surface System Aggregator Module User-Space Interface" + depends on SURFACE_AGGREGATOR + help + Provides a misc-device interface to the Surface System Aggregator + Module (SSAM) controller. + + This option provides a module (called surface_aggregator_cdev), that, + when loaded, will add a client device (and its respective driver) to + the SSAM controller. Said client device manages a misc-device + interface (/dev/surface/aggregator), which can be used by user-space + tools to directly communicate with the SSAM EC by sending requests and + receiving the corresponding responses. + + The provided interface is intended for debugging and development only, + and should not be used otherwise. + +config SURFACE_AGGREGATOR_HUB + tristate "Surface System Aggregator Module Subsystem Device Hubs" + depends on SURFACE_AGGREGATOR + depends on SURFACE_AGGREGATOR_BUS + help + Device-hub drivers for Surface System Aggregator Module (SSAM) subsystem + devices. + + Provides subsystem hub drivers which manage client devices on various + SSAM subsystems. In some subsystems, notably the BAS subsystem managing + devices contained in the base of the Surface Book 3 and the KIP subsystem + managing type-cover devices in the Surface Pro 8 and Surface Pro X, + devices can be (hot-)removed. Hub devices and drivers are required to + manage these subdevices. + + Devices managed via these hubs are: + - Battery/AC devices (Surface Book 3). + - HID input devices (7th-generation and later models with detachable + input devices). + + Select M (recommended) or Y here if you want support for the above + mentioned devices on the corresponding Surface models. Without this + module, the respective devices mentioned above will not be instantiated + and thus any functionality provided by them will be missing, even when + drivers for these devices are present. This module only provides the + respective subsystem hubs. Both drivers and device specification (e.g. + via the Surface Aggregator Registry) for these devices still need to be + selected via other options. + +config SURFACE_AGGREGATOR_REGISTRY + tristate "Surface System Aggregator Module Device Registry" + depends on SURFACE_AGGREGATOR + depends on SURFACE_AGGREGATOR_BUS + help + Device-registry for Surface System Aggregator Module (SSAM) devices. + + Provides a module and driver which act as a device-registry for SSAM + client devices that cannot be detected automatically, e.g. via ACPI. + Such devices are instead provided and managed via this registry. + + Devices provided via this registry are: + - Platform profile (performance-/cooling-mode) device (5th- and later + generations). + - Battery/AC devices (7th-generation). + - HID input devices (7th-generation). + + Select M (recommended) or Y here if you want support for the above + mentioned devices on the corresponding Surface models. Without this + module, the respective devices will not be instantiated and thus any + functionality provided by them will be missing, even when drivers for + these devices are present. In other words, this module only provides + the respective client devices. Drivers for these devices still need to + be selected via the other options. + +config SURFACE_AGGREGATOR_TABLET_SWITCH + tristate "Surface Aggregator Generic Tablet-Mode Switch Driver" + depends on SURFACE_AGGREGATOR + depends on SURFACE_AGGREGATOR_BUS + depends on INPUT + help + Provides a tablet-mode switch input device on Microsoft Surface models + using the KIP subsystem for detachable keyboards (e.g. keyboard covers) + or the POS subsystem for device/screen posture changes. + + The KIP subsystem is used on newer Surface generations to handle + detachable input peripherals, specifically the keyboard cover (containing + keyboard and touchpad) on the Surface Pro 8 and Surface Pro X. The POS + subsystem is used for device posture change notifications on the Surface + Laptop Studio. This module provides a driver to let user-space know when + the device should be considered in tablet-mode due to the keyboard cover + being detached or folded back (essentially signaling when the keyboard is + not available for input). It does so by creating a tablet-mode switch + input device, sending the standard SW_TABLET_MODE event on mode change. + + Select M or Y here, if you want to provide tablet-mode switch input + events on the Surface Pro 8, Surface Pro X, and Surface Laptop Studio. + +config SURFACE_DTX + tristate "Surface DTX (Detachment System) Driver" + depends on SURFACE_AGGREGATOR + depends on INPUT + help + Driver for the Surface Book clipboard detachment system (DTX). + + On the Surface Book series devices, the display part containing the + CPU (called the clipboard) can be detached from the base (containing a + battery, the keyboard, and, optionally, a discrete GPU) by (if + necessary) unlocking and opening the latch connecting both parts. + + This driver provides a user-space interface that can influence the + behavior of this process, which includes the option to abort it in + case the base is still in use or speed it up in case it is not. + + Note that this module can be built without support for the Surface + Aggregator Bus (i.e. CONFIG_SURFACE_AGGREGATOR_BUS=n). In that case, + some devices, specifically the Surface Book 3, will not be supported. + +config SURFACE_GPE + tristate "Surface GPE/Lid Support Driver" + depends on ACPI + depends on DMI + help + This driver marks the GPEs related to the ACPI lid device found on + Microsoft Surface devices as wakeup sources and prepares them + accordingly. It is required on those devices to allow wake-ups from + suspend by opening the lid. + +config SURFACE_HOTPLUG + tristate "Surface Hot-Plug Driver" + depends on ACPI + depends on GPIOLIB + help + Driver for out-of-band hot-plug event signaling on Microsoft Surface + devices with hot-pluggable PCIe cards. + + This driver is used on Surface Book (2 and 3) devices with a + hot-pluggable discrete GPU (dGPU). When not in use, the dGPU on those + devices can enter D3cold, which prevents in-band (standard) PCIe + hot-plug signaling. Thus, without this driver, detaching the base + containing the dGPU will not correctly update the state of the + corresponding PCIe device if it is in D3cold. This driver adds support + for out-of-band hot-plug notifications, ensuring that the device state + is properly updated even when the device in question is in D3cold. + + Select M or Y here, if you want to (fully) support hot-plugging of + dGPU devices on the Surface Book 2 and/or 3 during D3cold. + +config SURFACE_PLATFORM_PROFILE + tristate "Surface Platform Profile Driver" + depends on ACPI + depends on SURFACE_AGGREGATOR_REGISTRY + select ACPI_PLATFORM_PROFILE + help + Provides support for the ACPI platform profile on 5th- and later + generation Microsoft Surface devices. + + More specifically, this driver provides ACPI platform profile support + on Microsoft Surface devices with a Surface System Aggregator Module + (SSAM) connected via the Surface Serial Hub (SSH / SAM-over-SSH). In + other words, this driver provides platform profile support on the + Surface Pro 5, Surface Book 2, Surface Laptop, Surface Laptop Go and + later. On those devices, the platform profile can significantly + influence cooling behavior, e.g. setting it to 'quiet' (default) or + 'low-power' can significantly limit performance of the discrete GPU on + Surface Books, while in turn leading to lower power consumption and/or + less fan noise. + + Select M or Y here, if you want to include ACPI platform profile + support on the above mentioned devices. + +config SURFACE_PRO3_BUTTON + tristate "Power/home/volume buttons driver for Microsoft Surface Pro 3/4 tablet" + depends on ACPI + depends on INPUT + help + This driver handles the power/home/volume buttons on the Microsoft Surface Pro 3/4 tablet. + +source "drivers/platform/surface/aggregator/Kconfig" + +endif # SURFACE_PLATFORMS diff --git a/drivers/platform/surface/Makefile b/drivers/platform/surface/Makefile new file mode 100644 index 000000000..533443309 --- /dev/null +++ b/drivers/platform/surface/Makefile @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/surface +# Microsoft Surface Platform-Specific Drivers +# + +obj-$(CONFIG_SURFACE3_WMI) += surface3-wmi.o +obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o +obj-$(CONFIG_SURFACE_ACPI_NOTIFY) += surface_acpi_notify.o +obj-$(CONFIG_SURFACE_AGGREGATOR) += aggregator/ +obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o +obj-$(CONFIG_SURFACE_AGGREGATOR_HUB) += surface_aggregator_hub.o +obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o +obj-$(CONFIG_SURFACE_AGGREGATOR_TABLET_SWITCH) += surface_aggregator_tabletsw.o +obj-$(CONFIG_SURFACE_DTX) += surface_dtx.o +obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o +obj-$(CONFIG_SURFACE_HOTPLUG) += surface_hotplug.o +obj-$(CONFIG_SURFACE_PLATFORM_PROFILE) += surface_platform_profile.o +obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o diff --git a/drivers/platform/surface/aggregator/Kconfig b/drivers/platform/surface/aggregator/Kconfig new file mode 100644 index 000000000..957c216c1 --- /dev/null +++ b/drivers/platform/surface/aggregator/Kconfig @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (C) 2019-2022 Maximilian Luz + +menuconfig SURFACE_AGGREGATOR + tristate "Microsoft Surface System Aggregator Module Subsystem and Drivers" + depends on SERIAL_DEV_BUS + depends on ACPI && !RISCV + select CRC_ITU_T + help + The Surface System Aggregator Module (Surface SAM or SSAM) is an + embedded controller (EC) found on 5th- and later-generation Microsoft + Surface devices (i.e. Surface Pro 5, Surface Book 2, Surface Laptop, + and newer, with exception of Surface Go series devices). + + Depending on the device in question, this EC provides varying + functionality, including: + - EC access from ACPI via Surface ACPI Notify (5th- and 6th-generation) + - battery status information (all devices) + - thermal sensor access (all devices) + - performance mode / cooling mode control (all devices) + - clipboard detachment system control (Surface Book 2 and 3) + - HID / keyboard input (Surface Laptops, Surface Book 3) + + This option controls whether the Surface SAM subsystem core will be + built. This includes a driver for the Surface Serial Hub (SSH), which + is the device responsible for the communication with the EC, and a + basic kernel interface exposing the EC functionality to other client + drivers, i.e. allowing them to make requests to the EC and receive + events from it. Selecting this option alone will not provide any + client drivers and therefore no functionality beyond the in-kernel + interface. Said functionality is the responsibility of the respective + client drivers. + + Note: While 4th-generation Surface devices also make use of a SAM EC, + due to a difference in the communication interface of the controller, + only 5th and later generations are currently supported. Specifically, + devices using SAM-over-SSH are supported, whereas devices using + SAM-over-HID, which is used on the 4th generation, are currently not + supported. + + Choose m if you want to build the SAM subsystem core and SSH driver as + module, y if you want to build it into the kernel and n if you don't + want it at all. + +config SURFACE_AGGREGATOR_BUS + bool "Surface System Aggregator Module Bus" + depends on SURFACE_AGGREGATOR + default y + help + Expands the Surface System Aggregator Module (SSAM) core driver by + providing a dedicated bus and client-device type. + + This bus and device type are intended to provide and simplify support + for non-platform and non-ACPI SSAM devices, i.e. SSAM devices that are + not auto-detectable via the conventional means (e.g. ACPI). + +config SURFACE_AGGREGATOR_ERROR_INJECTION + bool "Surface System Aggregator Module Error Injection Capabilities" + depends on SURFACE_AGGREGATOR + depends on FUNCTION_ERROR_INJECTION + help + Provides error-injection capabilities for the Surface System + Aggregator Module subsystem and Surface Serial Hub driver. + + Specifically, exports error injection hooks to be used with the + kernel's function error injection capabilities to simulate underlying + transport and communication problems, such as invalid data sent to or + received from the EC, dropped data, and communication timeouts. + Intended for development and debugging. diff --git a/drivers/platform/surface/aggregator/Makefile b/drivers/platform/surface/aggregator/Makefile new file mode 100644 index 000000000..fdf664a21 --- /dev/null +++ b/drivers/platform/surface/aggregator/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (C) 2019-2022 Maximilian Luz + +# For include/trace/define_trace.h to include trace.h +CFLAGS_core.o = -I$(src) + +obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator.o + +surface_aggregator-y := core.o +surface_aggregator-y += ssh_parser.o +surface_aggregator-y += ssh_packet_layer.o +surface_aggregator-y += ssh_request_layer.o +surface_aggregator-$(CONFIG_SURFACE_AGGREGATOR_BUS) += bus.o +surface_aggregator-y += controller.o diff --git a/drivers/platform/surface/aggregator/bus.c b/drivers/platform/surface/aggregator/bus.c new file mode 100644 index 000000000..4395331be --- /dev/null +++ b/drivers/platform/surface/aggregator/bus.c @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface System Aggregator Module bus and device integration. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include + +#include +#include + +#include "bus.h" +#include "controller.h" + + +/* -- Device and bus functions. --------------------------------------------- */ + +static ssize_t modalias_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ssam_device *sdev = to_ssam_device(dev); + + return sysfs_emit(buf, "ssam:d%02Xc%02Xt%02Xi%02Xf%02X\n", + sdev->uid.domain, sdev->uid.category, sdev->uid.target, + sdev->uid.instance, sdev->uid.function); +} +static DEVICE_ATTR_RO(modalias); + +static struct attribute *ssam_device_attrs[] = { + &dev_attr_modalias.attr, + NULL, +}; +ATTRIBUTE_GROUPS(ssam_device); + +static const struct bus_type ssam_bus_type; + +static int ssam_device_uevent(const struct device *dev, struct kobj_uevent_env *env) +{ + const struct ssam_device *sdev = to_ssam_device(dev); + + return add_uevent_var(env, "MODALIAS=ssam:d%02Xc%02Xt%02Xi%02Xf%02X", + sdev->uid.domain, sdev->uid.category, + sdev->uid.target, sdev->uid.instance, + sdev->uid.function); +} + +static void ssam_device_release(struct device *dev) +{ + struct ssam_device *sdev = to_ssam_device(dev); + + ssam_controller_put(sdev->ctrl); + fwnode_handle_put(sdev->dev.fwnode); + kfree(sdev); +} + +const struct device_type ssam_device_type = { + .name = "surface_aggregator_device", + .groups = ssam_device_groups, + .uevent = ssam_device_uevent, + .release = ssam_device_release, +}; +EXPORT_SYMBOL_GPL(ssam_device_type); + +/** + * ssam_device_alloc() - Allocate and initialize a SSAM client device. + * @ctrl: The controller under which the device should be added. + * @uid: The UID of the device to be added. + * + * Allocates and initializes a new client device. The parent of the device + * will be set to the controller device and the name will be set based on the + * UID. Note that the device still has to be added via ssam_device_add(). + * Refer to that function for more details. + * + * Return: Returns the newly allocated and initialized SSAM client device, or + * %NULL if it could not be allocated. + */ +struct ssam_device *ssam_device_alloc(struct ssam_controller *ctrl, + struct ssam_device_uid uid) +{ + struct ssam_device *sdev; + + sdev = kzalloc_obj(*sdev); + if (!sdev) + return NULL; + + device_initialize(&sdev->dev); + sdev->dev.bus = &ssam_bus_type; + sdev->dev.type = &ssam_device_type; + sdev->dev.parent = ssam_controller_device(ctrl); + sdev->ctrl = ssam_controller_get(ctrl); + sdev->uid = uid; + + dev_set_name(&sdev->dev, "%02x:%02x:%02x:%02x:%02x", + sdev->uid.domain, sdev->uid.category, sdev->uid.target, + sdev->uid.instance, sdev->uid.function); + + return sdev; +} +EXPORT_SYMBOL_GPL(ssam_device_alloc); + +/** + * ssam_device_add() - Add a SSAM client device. + * @sdev: The SSAM client device to be added. + * + * Added client devices must be guaranteed to always have a valid and active + * controller. Thus, this function will fail with %-ENODEV if the controller + * of the device has not been initialized yet, has been suspended, or has been + * shut down. + * + * The caller of this function should ensure that the corresponding call to + * ssam_device_remove() is issued before the controller is shut down. If the + * added device is a direct child of the controller device (default), it will + * be automatically removed when the controller is shut down. + * + * By default, the controller device will become the parent of the newly + * created client device. The parent may be changed before ssam_device_add is + * called, but care must be taken that a) the correct suspend/resume ordering + * is guaranteed and b) the client device does not outlive the controller, + * i.e. that the device is removed before the controller is being shut down. + * In case these guarantees have to be manually enforced, please refer to the + * ssam_client_link() and ssam_client_bind() functions, which are intended to + * set up device-links for this purpose. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssam_device_add(struct ssam_device *sdev) +{ + int status; + + /* + * Ensure that we can only add new devices to a controller if it has + * been started and is not going away soon. This works in combination + * with ssam_controller_remove_clients to ensure driver presence for the + * controller device, i.e. it ensures that the controller (sdev->ctrl) + * is always valid and can be used for requests as long as the client + * device we add here is registered as child under it. This essentially + * guarantees that the client driver can always expect the preconditions + * for functions like ssam_request_do_sync() (controller has to be + * started and is not suspended) to hold and thus does not have to check + * for them. + * + * Note that for this to work, the controller has to be a parent device. + * If it is not a direct parent, care has to be taken that the device is + * removed via ssam_device_remove(), as device_unregister does not + * remove child devices recursively. + */ + ssam_controller_statelock(sdev->ctrl); + + if (sdev->ctrl->state != SSAM_CONTROLLER_STARTED) { + ssam_controller_stateunlock(sdev->ctrl); + return -ENODEV; + } + + status = device_add(&sdev->dev); + + ssam_controller_stateunlock(sdev->ctrl); + return status; +} +EXPORT_SYMBOL_GPL(ssam_device_add); + +/** + * ssam_device_remove() - Remove a SSAM client device. + * @sdev: The device to remove. + * + * Removes and unregisters the provided SSAM client device. + */ +void ssam_device_remove(struct ssam_device *sdev) +{ + device_unregister(&sdev->dev); +} +EXPORT_SYMBOL_GPL(ssam_device_remove); + +/** + * ssam_device_id_compatible() - Check if a device ID matches a UID. + * @id: The device ID as potential match. + * @uid: The device UID matching against. + * + * Check if the given ID is a match for the given UID, i.e. if a device with + * the provided UID is compatible to the given ID following the match rules + * described in its &ssam_device_id.match_flags member. + * + * Return: Returns %true if the given UID is compatible to the match rule + * described by the given ID, %false otherwise. + */ +static bool ssam_device_id_compatible(const struct ssam_device_id *id, + struct ssam_device_uid uid) +{ + if (id->domain != uid.domain || id->category != uid.category) + return false; + + if ((id->match_flags & SSAM_MATCH_TARGET) && id->target != uid.target) + return false; + + if ((id->match_flags & SSAM_MATCH_INSTANCE) && id->instance != uid.instance) + return false; + + if ((id->match_flags & SSAM_MATCH_FUNCTION) && id->function != uid.function) + return false; + + return true; +} + +/** + * ssam_device_id_is_null() - Check if a device ID is null. + * @id: The device ID to check. + * + * Check if a given device ID is null, i.e. all zeros. Used to check for the + * end of ``MODULE_DEVICE_TABLE(ssam, ...)`` or similar lists. + * + * Return: Returns %true if the given ID represents a null ID, %false + * otherwise. + */ +static bool ssam_device_id_is_null(const struct ssam_device_id *id) +{ + return id->match_flags == 0 && + id->domain == 0 && + id->category == 0 && + id->target == 0 && + id->instance == 0 && + id->function == 0 && + id->driver_data == 0; +} + +/** + * ssam_device_id_match() - Find the matching ID table entry for the given UID. + * @table: The table to search in. + * @uid: The UID to matched against the individual table entries. + * + * Find the first match for the provided device UID in the provided ID table + * and return it. Returns %NULL if no match could be found. + */ +const struct ssam_device_id *ssam_device_id_match(const struct ssam_device_id *table, + const struct ssam_device_uid uid) +{ + const struct ssam_device_id *id; + + for (id = table; !ssam_device_id_is_null(id); ++id) + if (ssam_device_id_compatible(id, uid)) + return id; + + return NULL; +} +EXPORT_SYMBOL_GPL(ssam_device_id_match); + +/** + * ssam_device_get_match() - Find and return the ID matching the device in the + * ID table of the bound driver. + * @dev: The device for which to get the matching ID table entry. + * + * Find the fist match for the UID of the device in the ID table of the + * currently bound driver and return it. Returns %NULL if the device does not + * have a driver bound to it, the driver does not have match_table (i.e. it is + * %NULL), or there is no match in the driver's match_table. + * + * This function essentially calls ssam_device_id_match() with the ID table of + * the bound device driver and the UID of the device. + * + * Return: Returns the first match for the UID of the device in the device + * driver's match table, or %NULL if no such match could be found. + */ +const struct ssam_device_id *ssam_device_get_match(const struct ssam_device *dev) +{ + const struct ssam_device_driver *sdrv; + + sdrv = to_ssam_device_driver(dev->dev.driver); + if (!sdrv) + return NULL; + + if (!sdrv->match_table) + return NULL; + + return ssam_device_id_match(sdrv->match_table, dev->uid); +} +EXPORT_SYMBOL_GPL(ssam_device_get_match); + +/** + * ssam_device_get_match_data() - Find the ID matching the device in the + * ID table of the bound driver and return its ``driver_data`` member. + * @dev: The device for which to get the match data. + * + * Find the fist match for the UID of the device in the ID table of the + * corresponding driver and return its driver_data. Returns %NULL if the + * device does not have a driver bound to it, the driver does not have + * match_table (i.e. it is %NULL), there is no match in the driver's + * match_table, or the match does not have any driver_data. + * + * This function essentially calls ssam_device_get_match() and, if any match + * could be found, returns its ``struct ssam_device_id.driver_data`` member. + * + * Return: Returns the driver data associated with the first match for the UID + * of the device in the device driver's match table, or %NULL if no such match + * could be found. + */ +const void *ssam_device_get_match_data(const struct ssam_device *dev) +{ + const struct ssam_device_id *id; + + id = ssam_device_get_match(dev); + if (!id) + return NULL; + + return (const void *)id->driver_data; +} +EXPORT_SYMBOL_GPL(ssam_device_get_match_data); + +static int ssam_bus_match(struct device *dev, const struct device_driver *drv) +{ + const struct ssam_device_driver *sdrv = to_ssam_device_driver(drv); + struct ssam_device *sdev = to_ssam_device(dev); + + if (!is_ssam_device(dev)) + return 0; + + return !!ssam_device_id_match(sdrv->match_table, sdev->uid); +} + +static int ssam_bus_probe(struct device *dev) +{ + return to_ssam_device_driver(dev->driver) + ->probe(to_ssam_device(dev)); +} + +static void ssam_bus_remove(struct device *dev) +{ + struct ssam_device_driver *sdrv = to_ssam_device_driver(dev->driver); + + if (sdrv->remove) + sdrv->remove(to_ssam_device(dev)); +} + +static const struct bus_type ssam_bus_type = { + .name = "surface_aggregator", + .match = ssam_bus_match, + .probe = ssam_bus_probe, + .remove = ssam_bus_remove, +}; + +/** + * __ssam_device_driver_register() - Register a SSAM client device driver. + * @sdrv: The driver to register. + * @owner: The module owning the provided driver. + * + * Please refer to the ssam_device_driver_register() macro for the normal way + * to register a driver from inside its owning module. + */ +int __ssam_device_driver_register(struct ssam_device_driver *sdrv, + struct module *owner) +{ + sdrv->driver.owner = owner; + sdrv->driver.bus = &ssam_bus_type; + + /* force drivers to async probe so I/O is possible in probe */ + sdrv->driver.probe_type = PROBE_PREFER_ASYNCHRONOUS; + + return driver_register(&sdrv->driver); +} +EXPORT_SYMBOL_GPL(__ssam_device_driver_register); + +/** + * ssam_device_driver_unregister - Unregister a SSAM device driver. + * @sdrv: The driver to unregister. + */ +void ssam_device_driver_unregister(struct ssam_device_driver *sdrv) +{ + driver_unregister(&sdrv->driver); +} +EXPORT_SYMBOL_GPL(ssam_device_driver_unregister); + + +/* -- Bus registration. ----------------------------------------------------- */ + +/** + * ssam_bus_register() - Register and set-up the SSAM client device bus. + */ +int ssam_bus_register(void) +{ + return bus_register(&ssam_bus_type); +} + +/** + * ssam_bus_unregister() - Unregister the SSAM client device bus. + */ +void ssam_bus_unregister(void) +{ + return bus_unregister(&ssam_bus_type); +} + + +/* -- Helpers for controller and hub devices. ------------------------------- */ + +static int ssam_device_uid_from_string(const char *str, struct ssam_device_uid *uid) +{ + u8 d, tc, tid, iid, fn; + int n; + + n = sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx", &d, &tc, &tid, &iid, &fn); + if (n != 5) + return -EINVAL; + + uid->domain = d; + uid->category = tc; + uid->target = tid; + uid->instance = iid; + uid->function = fn; + + return 0; +} + +static int ssam_get_uid_for_node(struct fwnode_handle *node, struct ssam_device_uid *uid) +{ + const char *str = fwnode_get_name(node); + + /* + * To simplify definitions of firmware nodes, we set the device name + * based on the UID of the device, prefixed with "ssam:". + */ + if (strncmp(str, "ssam:", strlen("ssam:")) != 0) + return -ENODEV; + + str += strlen("ssam:"); + return ssam_device_uid_from_string(str, uid); +} + +static int ssam_add_client_device(struct device *parent, struct ssam_controller *ctrl, + struct fwnode_handle *node) +{ + struct ssam_device_uid uid; + struct ssam_device *sdev; + int status; + + status = ssam_get_uid_for_node(node, &uid); + if (status) + return status; + + sdev = ssam_device_alloc(ctrl, uid); + if (!sdev) + return -ENOMEM; + + sdev->dev.parent = parent; + sdev->dev.fwnode = fwnode_handle_get(node); + sdev->dev.of_node = to_of_node(node); + + status = ssam_device_add(sdev); + if (status) + ssam_device_put(sdev); + + return status; +} + +/** + * __ssam_register_clients() - Register client devices defined under the + * given firmware node as children of the given device. + * @parent: The parent device under which clients should be registered. + * @ctrl: The controller with which client should be registered. + * @node: The firmware node holding definitions of the devices to be added. + * + * Register all clients that have been defined as children of the given root + * firmware node as children of the given parent device. The respective child + * firmware nodes will be associated with the correspondingly created child + * devices. + * + * The given controller will be used to instantiate the new devices. See + * ssam_device_add() for details. + * + * Note that, generally, the use of either ssam_device_register_clients() or + * ssam_register_clients() should be preferred as they directly use the + * firmware node and/or controller associated with the given device. This + * function is only intended for use when different device specifications (e.g. + * ACPI and firmware nodes) need to be combined (as is done in the platform hub + * of the device registry). + * + * Return: Returns zero on success, nonzero on failure. + */ +int __ssam_register_clients(struct device *parent, struct ssam_controller *ctrl, + struct fwnode_handle *node) +{ + struct fwnode_handle *child; + int status; + + fwnode_for_each_child_node(node, child) { + /* + * Try to add the device specified in the firmware node. If + * this fails with -ENODEV, the node does not specify any SSAM + * device, so ignore it and continue with the next one. + */ + status = ssam_add_client_device(parent, ctrl, child); + if (status && status != -ENODEV) { + fwnode_handle_put(child); + goto err; + } + } + + return 0; +err: + ssam_remove_clients(parent); + return status; +} +EXPORT_SYMBOL_GPL(__ssam_register_clients); + +static int ssam_remove_device(struct device *dev, void *_data) +{ + struct ssam_device *sdev = to_ssam_device(dev); + + if (is_ssam_device(dev)) + ssam_device_remove(sdev); + + return 0; +} + +/** + * ssam_remove_clients() - Remove SSAM client devices registered as direct + * children under the given parent device. + * @dev: The (parent) device to remove all direct clients for. + * + * Remove all SSAM client devices registered as direct children under the given + * device. Note that this only accounts for direct children of the device. + * Refer to ssam_device_add()/ssam_device_remove() for more details. + */ +void ssam_remove_clients(struct device *dev) +{ + device_for_each_child_reverse(dev, NULL, ssam_remove_device); +} +EXPORT_SYMBOL_GPL(ssam_remove_clients); diff --git a/drivers/platform/surface/aggregator/bus.h b/drivers/platform/surface/aggregator/bus.h new file mode 100644 index 000000000..5b4dbf219 --- /dev/null +++ b/drivers/platform/surface/aggregator/bus.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Surface System Aggregator Module bus and device integration. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_BUS_H +#define _SURFACE_AGGREGATOR_BUS_H + +#include + +#ifdef CONFIG_SURFACE_AGGREGATOR_BUS + +int ssam_bus_register(void); +void ssam_bus_unregister(void); + +#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ + +static inline int ssam_bus_register(void) { return 0; } +static inline void ssam_bus_unregister(void) {} + +#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ +#endif /* _SURFACE_AGGREGATOR_BUS_H */ diff --git a/drivers/platform/surface/aggregator/controller.c b/drivers/platform/surface/aggregator/controller.c new file mode 100644 index 000000000..6a8430cb9 --- /dev/null +++ b/drivers/platform/surface/aggregator/controller.c @@ -0,0 +1,2847 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Main SSAM/SSH controller structure and functionality. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "controller.h" +#include "ssh_msgb.h" +#include "ssh_request_layer.h" + +#include "trace.h" + + +/* -- Safe counters. -------------------------------------------------------- */ + +/** + * ssh_seq_reset() - Reset/initialize sequence ID counter. + * @c: The counter to reset. + */ +static void ssh_seq_reset(struct ssh_seq_counter *c) +{ + WRITE_ONCE(c->value, 0); +} + +/** + * ssh_seq_next() - Get next sequence ID. + * @c: The counter providing the sequence IDs. + * + * Return: Returns the next sequence ID of the counter. + */ +static u8 ssh_seq_next(struct ssh_seq_counter *c) +{ + u8 old = READ_ONCE(c->value); + u8 new = old + 1; + u8 ret; + + while (unlikely((ret = cmpxchg(&c->value, old, new)) != old)) { + old = ret; + new = old + 1; + } + + return old; +} + +/** + * ssh_rqid_reset() - Reset/initialize request ID counter. + * @c: The counter to reset. + */ +static void ssh_rqid_reset(struct ssh_rqid_counter *c) +{ + WRITE_ONCE(c->value, 0); +} + +/** + * ssh_rqid_next() - Get next request ID. + * @c: The counter providing the request IDs. + * + * Return: Returns the next request ID of the counter, skipping any reserved + * request IDs. + */ +static u16 ssh_rqid_next(struct ssh_rqid_counter *c) +{ + u16 old = READ_ONCE(c->value); + u16 new = ssh_rqid_next_valid(old); + u16 ret; + + while (unlikely((ret = cmpxchg(&c->value, old, new)) != old)) { + old = ret; + new = ssh_rqid_next_valid(old); + } + + return old; +} + + +/* -- Event notifier/callbacks. --------------------------------------------- */ +/* + * The notifier system is based on linux/notifier.h, specifically the SRCU + * implementation. The difference to that is, that some bits of the notifier + * call return value can be tracked across multiple calls. This is done so + * that handling of events can be tracked and a warning can be issued in case + * an event goes unhandled. The idea of that warning is that it should help + * discover and identify new/currently unimplemented features. + */ + +/** + * ssam_event_matches_notifier() - Test if an event matches a notifier. + * @n: The event notifier to test against. + * @event: The event to test. + * + * Return: Returns %true if the given event matches the given notifier + * according to the rules set in the notifier's event mask, %false otherwise. + */ +static bool ssam_event_matches_notifier(const struct ssam_event_notifier *n, + const struct ssam_event *event) +{ + bool match = n->event.id.target_category == event->target_category; + + if (n->event.mask & SSAM_EVENT_MASK_TARGET) + match &= n->event.reg.target_id == event->target_id; + + if (n->event.mask & SSAM_EVENT_MASK_INSTANCE) + match &= n->event.id.instance == event->instance_id; + + return match; +} + +/** + * ssam_nfblk_call_chain() - Call event notifier callbacks of the given chain. + * @nh: The notifier head for which the notifier callbacks should be called. + * @event: The event data provided to the callbacks. + * + * Call all registered notifier callbacks in order of their priority until + * either no notifier is left or a notifier returns a value with the + * %SSAM_NOTIF_STOP bit set. Note that this bit is automatically set via + * ssam_notifier_from_errno() on any non-zero error value. + * + * Return: Returns the notifier status value, which contains the notifier + * status bits (%SSAM_NOTIF_HANDLED and %SSAM_NOTIF_STOP) as well as a + * potential error value returned from the last executed notifier callback. + * Use ssam_notifier_to_errno() to convert this value to the original error + * value. + */ +static int ssam_nfblk_call_chain(struct ssam_nf_head *nh, struct ssam_event *event) +{ + struct ssam_event_notifier *nf; + int ret = 0, idx; + + idx = srcu_read_lock(&nh->srcu); + + list_for_each_entry_rcu(nf, &nh->head, base.node, + srcu_read_lock_held(&nh->srcu)) { + if (ssam_event_matches_notifier(nf, event)) { + ret = (ret & SSAM_NOTIF_STATE_MASK) | nf->base.fn(nf, event); + if (ret & SSAM_NOTIF_STOP) + break; + } + } + + srcu_read_unlock(&nh->srcu, idx); + return ret; +} + +/** + * ssam_nfblk_insert() - Insert a new notifier block into the given notifier + * list. + * @nh: The notifier head into which the block should be inserted. + * @nb: The notifier block to add. + * + * Note: This function must be synchronized by the caller with respect to other + * insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * + * Return: Returns zero on success, %-EEXIST if the notifier block has already + * been registered. + */ +static int ssam_nfblk_insert(struct ssam_nf_head *nh, struct ssam_notifier_block *nb) +{ + struct ssam_notifier_block *p; + struct list_head *h; + + /* Runs under lock, no need for RCU variant. */ + list_for_each(h, &nh->head) { + p = list_entry(h, struct ssam_notifier_block, node); + + if (unlikely(p == nb)) { + WARN(1, "double register detected"); + return -EEXIST; + } + + if (nb->priority > p->priority) + break; + } + + list_add_tail_rcu(&nb->node, h); + return 0; +} + +/** + * ssam_nfblk_find() - Check if a notifier block is registered on the given + * notifier head. + * list. + * @nh: The notifier head on which to search. + * @nb: The notifier block to search for. + * + * Note: This function must be synchronized by the caller with respect to other + * insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * + * Return: Returns true if the given notifier block is registered on the given + * notifier head, false otherwise. + */ +static bool ssam_nfblk_find(struct ssam_nf_head *nh, struct ssam_notifier_block *nb) +{ + struct ssam_notifier_block *p; + + /* Runs under lock, no need for RCU variant. */ + list_for_each_entry(p, &nh->head, node) { + if (p == nb) + return true; + } + + return false; +} + +/** + * ssam_nfblk_remove() - Remove a notifier block from its notifier list. + * @nb: The notifier block to be removed. + * + * Note: This function must be synchronized by the caller with respect to + * other insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * Furthermore, the caller _must_ ensure SRCU synchronization by calling + * synchronize_srcu() with ``nh->srcu`` after leaving the critical section, to + * ensure that the removed notifier block is not in use any more. + */ +static void ssam_nfblk_remove(struct ssam_notifier_block *nb) +{ + list_del_rcu(&nb->node); +} + +/** + * ssam_nf_head_init() - Initialize the given notifier head. + * @nh: The notifier head to initialize. + */ +static int ssam_nf_head_init(struct ssam_nf_head *nh) +{ + int status; + + status = init_srcu_struct(&nh->srcu); + if (status) + return status; + + INIT_LIST_HEAD(&nh->head); + return 0; +} + +/** + * ssam_nf_head_destroy() - Deinitialize the given notifier head. + * @nh: The notifier head to deinitialize. + */ +static void ssam_nf_head_destroy(struct ssam_nf_head *nh) +{ + cleanup_srcu_struct(&nh->srcu); +} + + +/* -- Event/notification registry. ------------------------------------------ */ + +/** + * struct ssam_nf_refcount_key - Key used for event activation reference + * counting. + * @reg: The registry via which the event is enabled/disabled. + * @id: The ID uniquely describing the event. + */ +struct ssam_nf_refcount_key { + struct ssam_event_registry reg; + struct ssam_event_id id; +}; + +/** + * struct ssam_nf_refcount_entry - RB-tree entry for reference counting event + * activations. + * @node: The node of this entry in the rb-tree. + * @key: The key of the event. + * @refcount: The reference-count of the event. + * @flags: The flags used when enabling the event. + */ +struct ssam_nf_refcount_entry { + struct rb_node node; + struct ssam_nf_refcount_key key; + int refcount; + u8 flags; +}; + +/** + * ssam_nf_refcount_inc() - Increment reference-/activation-count of the given + * event. + * @nf: The notifier system reference. + * @reg: The registry used to enable/disable the event. + * @id: The event ID. + * + * Increments the reference-/activation-count associated with the specified + * event type/ID, allocating a new entry for this event ID if necessary. A + * newly allocated entry will have a refcount of one. + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns the refcount entry on success. Returns an error pointer + * with %-ENOSPC if there have already been %INT_MAX events of the specified + * ID and type registered, or %-ENOMEM if the entry could not be allocated. + */ +static struct ssam_nf_refcount_entry * +ssam_nf_refcount_inc(struct ssam_nf *nf, struct ssam_event_registry reg, + struct ssam_event_id id) +{ + struct ssam_nf_refcount_entry *entry; + struct ssam_nf_refcount_key key; + struct rb_node **link = &nf->refcount.rb_node; + struct rb_node *parent = NULL; + int cmp; + + lockdep_assert_held(&nf->lock); + + key.reg = reg; + key.id = id; + + while (*link) { + entry = rb_entry(*link, struct ssam_nf_refcount_entry, node); + parent = *link; + + cmp = memcmp(&key, &entry->key, sizeof(key)); + if (cmp < 0) { + link = &(*link)->rb_left; + } else if (cmp > 0) { + link = &(*link)->rb_right; + } else if (entry->refcount < INT_MAX) { + entry->refcount++; + return entry; + } else { + WARN_ON(1); + return ERR_PTR(-ENOSPC); + } + } + + entry = kzalloc_obj(*entry); + if (!entry) + return ERR_PTR(-ENOMEM); + + entry->key = key; + entry->refcount = 1; + + rb_link_node(&entry->node, parent, link); + rb_insert_color(&entry->node, &nf->refcount); + + return entry; +} + +/** + * ssam_nf_refcount_dec() - Decrement reference-/activation-count of the given + * event. + * @nf: The notifier system reference. + * @reg: The registry used to enable/disable the event. + * @id: The event ID. + * + * Decrements the reference-/activation-count of the specified event, + * returning its entry. If the returned entry has a refcount of zero, the + * caller is responsible for freeing it using kfree(). + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns the refcount entry on success or %NULL if the entry has not + * been found. + */ +static struct ssam_nf_refcount_entry * +ssam_nf_refcount_dec(struct ssam_nf *nf, struct ssam_event_registry reg, + struct ssam_event_id id) +{ + struct ssam_nf_refcount_entry *entry; + struct ssam_nf_refcount_key key; + struct rb_node *node = nf->refcount.rb_node; + int cmp; + + lockdep_assert_held(&nf->lock); + + key.reg = reg; + key.id = id; + + while (node) { + entry = rb_entry(node, struct ssam_nf_refcount_entry, node); + + cmp = memcmp(&key, &entry->key, sizeof(key)); + if (cmp < 0) { + node = node->rb_left; + } else if (cmp > 0) { + node = node->rb_right; + } else { + entry->refcount--; + if (entry->refcount == 0) + rb_erase(&entry->node, &nf->refcount); + + return entry; + } + } + + return NULL; +} + +/** + * ssam_nf_refcount_dec_free() - Decrement reference-/activation-count of the + * given event and free its entry if the reference count reaches zero. + * @nf: The notifier system reference. + * @reg: The registry used to enable/disable the event. + * @id: The event ID. + * + * Decrements the reference-/activation-count of the specified event, freeing + * its entry if it reaches zero. + * + * Note: ``nf->lock`` must be held when calling this function. + */ +static void ssam_nf_refcount_dec_free(struct ssam_nf *nf, + struct ssam_event_registry reg, + struct ssam_event_id id) +{ + struct ssam_nf_refcount_entry *entry; + + lockdep_assert_held(&nf->lock); + + entry = ssam_nf_refcount_dec(nf, reg, id); + if (entry && entry->refcount == 0) + kfree(entry); +} + +/** + * ssam_nf_refcount_empty() - Test if the notification system has any + * enabled/active events. + * @nf: The notification system. + */ +static bool ssam_nf_refcount_empty(struct ssam_nf *nf) +{ + return RB_EMPTY_ROOT(&nf->refcount); +} + +/** + * ssam_nf_call() - Call notification callbacks for the provided event. + * @nf: The notifier system + * @dev: The associated device, only used for logging. + * @rqid: The request ID of the event. + * @event: The event provided to the callbacks. + * + * Execute registered callbacks in order of their priority until either no + * callback is left or a callback returns a value with the %SSAM_NOTIF_STOP + * bit set. Note that this bit is set automatically when converting non-zero + * error values via ssam_notifier_from_errno() to notifier values. + * + * Also note that any callback that could handle an event should return a value + * with bit %SSAM_NOTIF_HANDLED set, indicating that the event does not go + * unhandled/ignored. In case no registered callback could handle an event, + * this function will emit a warning. + * + * In case a callback failed, this function will emit an error message. + */ +static void ssam_nf_call(struct ssam_nf *nf, struct device *dev, u16 rqid, + struct ssam_event *event) +{ + struct ssam_nf_head *nf_head; + int status, nf_ret; + + if (!ssh_rqid_is_event(rqid)) { + dev_warn(dev, "event: unsupported rqid: %#06x\n", rqid); + return; + } + + nf_head = &nf->head[ssh_rqid_to_event(rqid)]; + nf_ret = ssam_nfblk_call_chain(nf_head, event); + status = ssam_notifier_to_errno(nf_ret); + + if (status < 0) { + dev_err(dev, + "event: error handling event: %d (tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n", + status, event->target_category, event->target_id, + event->command_id, event->instance_id); + } else if (!(nf_ret & SSAM_NOTIF_HANDLED)) { + dev_warn(dev, + "event: unhandled event (rqid: %#04x, tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n", + rqid, event->target_category, event->target_id, + event->command_id, event->instance_id); + } +} + +/** + * ssam_nf_init() - Initialize the notifier system. + * @nf: The notifier system to initialize. + */ +static int ssam_nf_init(struct ssam_nf *nf) +{ + int i, status; + + for (i = 0; i < SSH_NUM_EVENTS; i++) { + status = ssam_nf_head_init(&nf->head[i]); + if (status) + break; + } + + if (status) { + while (i--) + ssam_nf_head_destroy(&nf->head[i]); + + return status; + } + + mutex_init(&nf->lock); + return 0; +} + +/** + * ssam_nf_destroy() - Deinitialize the notifier system. + * @nf: The notifier system to deinitialize. + */ +static void ssam_nf_destroy(struct ssam_nf *nf) +{ + int i; + + for (i = 0; i < SSH_NUM_EVENTS; i++) + ssam_nf_head_destroy(&nf->head[i]); + + mutex_destroy(&nf->lock); +} + + +/* -- Event/async request completion system. -------------------------------- */ + +#define SSAM_CPLT_WQ_NAME "ssam_cpltq" + +/* + * SSAM_CPLT_WQ_BATCH - Maximum number of event item completions executed per + * work execution. Used to prevent livelocking of the workqueue. Value chosen + * via educated guess, may be adjusted. + */ +#define SSAM_CPLT_WQ_BATCH 10 + +/* + * SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN - Maximum payload length for a cached + * &struct ssam_event_item. + * + * This length has been chosen to be accommodate standard touchpad and + * keyboard input events. Events with larger payloads will be allocated + * separately. + */ +#define SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN 32 + +static struct kmem_cache *ssam_event_item_cache; + +/** + * ssam_event_item_cache_init() - Initialize the event item cache. + */ +int ssam_event_item_cache_init(void) +{ + const unsigned int size = sizeof(struct ssam_event_item) + + SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN; + const unsigned int align = __alignof__(struct ssam_event_item); + struct kmem_cache *cache; + + cache = kmem_cache_create("ssam_event_item", size, align, 0, NULL); + if (!cache) + return -ENOMEM; + + ssam_event_item_cache = cache; + return 0; +} + +/** + * ssam_event_item_cache_destroy() - Deinitialize the event item cache. + */ +void ssam_event_item_cache_destroy(void) +{ + kmem_cache_destroy(ssam_event_item_cache); + ssam_event_item_cache = NULL; +} + +static void __ssam_event_item_free_cached(struct ssam_event_item *item) +{ + kmem_cache_free(ssam_event_item_cache, item); +} + +static void __ssam_event_item_free_generic(struct ssam_event_item *item) +{ + kfree(item); +} + +/** + * ssam_event_item_free() - Free the provided event item. + * @item: The event item to free. + */ +static void ssam_event_item_free(struct ssam_event_item *item) +{ + trace_ssam_event_item_free(item); + item->ops.free(item); +} + +/** + * ssam_event_item_alloc() - Allocate an event item with the given payload size. + * @len: The event payload length. + * @flags: The flags used for allocation. + * + * Allocate an event item with the given payload size, preferring allocation + * from the event item cache if the payload is small enough (i.e. smaller than + * %SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN). Sets the item operations and payload + * length values. The item free callback (``ops.free``) should not be + * overwritten after this call. + * + * Return: Returns the newly allocated event item. + */ +static struct ssam_event_item *ssam_event_item_alloc(size_t len, gfp_t flags) +{ + struct ssam_event_item *item; + + if (len <= SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN) { + item = kmem_cache_alloc(ssam_event_item_cache, flags); + if (!item) + return NULL; + + item->ops.free = __ssam_event_item_free_cached; + } else { + item = kzalloc_flex(*item, event.data, len, flags); + if (!item) + return NULL; + + item->ops.free = __ssam_event_item_free_generic; + } + + item->event.length = len; + + trace_ssam_event_item_alloc(item, len); + return item; +} + +/** + * ssam_event_queue_push() - Push an event item to the event queue. + * @q: The event queue. + * @item: The item to add. + */ +static void ssam_event_queue_push(struct ssam_event_queue *q, + struct ssam_event_item *item) +{ + spin_lock(&q->lock); + list_add_tail(&item->node, &q->head); + spin_unlock(&q->lock); +} + +/** + * ssam_event_queue_pop() - Pop the next event item from the event queue. + * @q: The event queue. + * + * Returns and removes the next event item from the queue. Returns %NULL If + * there is no event item left. + */ +static struct ssam_event_item *ssam_event_queue_pop(struct ssam_event_queue *q) +{ + struct ssam_event_item *item; + + spin_lock(&q->lock); + item = list_first_entry_or_null(&q->head, struct ssam_event_item, node); + if (item) + list_del(&item->node); + spin_unlock(&q->lock); + + return item; +} + +/** + * ssam_event_queue_is_empty() - Check if the event queue is empty. + * @q: The event queue. + */ +static bool ssam_event_queue_is_empty(struct ssam_event_queue *q) +{ + bool empty; + + spin_lock(&q->lock); + empty = list_empty(&q->head); + spin_unlock(&q->lock); + + return empty; +} + +/** + * ssam_cplt_get_event_queue() - Get the event queue for the given parameters. + * @cplt: The completion system on which to look for the queue. + * @tid: The target ID of the queue. + * @rqid: The request ID representing the event ID for which to get the queue. + * + * Return: Returns the event queue corresponding to the event type described + * by the given parameters. If the request ID does not represent an event, + * this function returns %NULL. If the target ID is not supported, this + * function will fall back to the default target ID (``tid = 1``). + */ +static +struct ssam_event_queue *ssam_cplt_get_event_queue(struct ssam_cplt *cplt, + u8 tid, u16 rqid) +{ + u16 event = ssh_rqid_to_event(rqid); + u16 tidx = ssh_tid_to_index(tid); + + if (!ssh_rqid_is_event(rqid)) { + dev_err(cplt->dev, "event: unsupported request ID: %#06x\n", rqid); + return NULL; + } + + if (!ssh_tid_is_valid(tid)) { + dev_warn(cplt->dev, "event: unsupported target ID: %u\n", tid); + tidx = 0; + } + + return &cplt->event.target[tidx].queue[event]; +} + +/** + * ssam_cplt_submit() - Submit a work item to the completion system workqueue. + * @cplt: The completion system. + * @work: The work item to submit. + */ +static bool ssam_cplt_submit(struct ssam_cplt *cplt, struct work_struct *work) +{ + return queue_work(cplt->wq, work); +} + +/** + * ssam_cplt_submit_event() - Submit an event to the completion system. + * @cplt: The completion system. + * @item: The event item to submit. + * + * Submits the event to the completion system by queuing it on the event item + * queue and queuing the respective event queue work item on the completion + * workqueue, which will eventually complete the event. + * + * Return: Returns zero on success, %-EINVAL if there is no event queue that + * can handle the given event item. + */ +static int ssam_cplt_submit_event(struct ssam_cplt *cplt, + struct ssam_event_item *item) +{ + struct ssam_event_queue *evq; + + evq = ssam_cplt_get_event_queue(cplt, item->event.target_id, item->rqid); + if (!evq) + return -EINVAL; + + ssam_event_queue_push(evq, item); + ssam_cplt_submit(cplt, &evq->work); + return 0; +} + +/** + * ssam_cplt_flush() - Flush the completion system. + * @cplt: The completion system. + * + * Flush the completion system by waiting until all currently submitted work + * items have been completed. + * + * Note: This function does not guarantee that all events will have been + * handled once this call terminates. In case of a larger number of + * to-be-completed events, the event queue work function may re-schedule its + * work item, which this flush operation will ignore. + * + * This operation is only intended to, during normal operation prior to + * shutdown, try to complete most events and requests to get them out of the + * system while the system is still fully operational. It does not aim to + * provide any guarantee that all of them have been handled. + */ +static void ssam_cplt_flush(struct ssam_cplt *cplt) +{ + flush_workqueue(cplt->wq); +} + +static void ssam_event_queue_work_fn(struct work_struct *work) +{ + struct ssam_event_queue *queue; + struct ssam_event_item *item; + struct ssam_nf *nf; + struct device *dev; + unsigned int iterations = SSAM_CPLT_WQ_BATCH; + + queue = container_of(work, struct ssam_event_queue, work); + nf = &queue->cplt->event.notif; + dev = queue->cplt->dev; + + /* Limit number of processed events to avoid livelocking. */ + do { + item = ssam_event_queue_pop(queue); + if (!item) + return; + + ssam_nf_call(nf, dev, item->rqid, &item->event); + ssam_event_item_free(item); + } while (--iterations); + + if (!ssam_event_queue_is_empty(queue)) + ssam_cplt_submit(queue->cplt, &queue->work); +} + +/** + * ssam_event_queue_init() - Initialize an event queue. + * @cplt: The completion system on which the queue resides. + * @evq: The event queue to initialize. + */ +static void ssam_event_queue_init(struct ssam_cplt *cplt, + struct ssam_event_queue *evq) +{ + evq->cplt = cplt; + spin_lock_init(&evq->lock); + INIT_LIST_HEAD(&evq->head); + INIT_WORK(&evq->work, ssam_event_queue_work_fn); +} + +/** + * ssam_cplt_init() - Initialize completion system. + * @cplt: The completion system to initialize. + * @dev: The device used for logging. + */ +static int ssam_cplt_init(struct ssam_cplt *cplt, struct device *dev) +{ + struct ssam_event_target *target; + int status, c, i; + + cplt->dev = dev; + + cplt->wq = alloc_workqueue(SSAM_CPLT_WQ_NAME, WQ_UNBOUND | WQ_MEM_RECLAIM, 0); + if (!cplt->wq) + return -ENOMEM; + + for (c = 0; c < ARRAY_SIZE(cplt->event.target); c++) { + target = &cplt->event.target[c]; + + for (i = 0; i < ARRAY_SIZE(target->queue); i++) + ssam_event_queue_init(cplt, &target->queue[i]); + } + + status = ssam_nf_init(&cplt->event.notif); + if (status) + destroy_workqueue(cplt->wq); + + return status; +} + +/** + * ssam_cplt_destroy() - Deinitialize the completion system. + * @cplt: The completion system to deinitialize. + * + * Deinitialize the given completion system and ensure that all pending, i.e. + * yet-to-be-completed, event items and requests have been handled. + */ +static void ssam_cplt_destroy(struct ssam_cplt *cplt) +{ + /* + * Note: destroy_workqueue ensures that all currently queued work will + * be fully completed and the workqueue drained. This means that this + * call will inherently also free any queued ssam_event_items, thus we + * don't have to take care of that here explicitly. + */ + destroy_workqueue(cplt->wq); + ssam_nf_destroy(&cplt->event.notif); +} + + +/* -- Main SSAM device structures. ------------------------------------------ */ + +/** + * ssam_controller_device() - Get the &struct device associated with this + * controller. + * @c: The controller for which to get the device. + * + * Return: Returns the &struct device associated with this controller, + * providing its lower-level transport. + */ +struct device *ssam_controller_device(struct ssam_controller *c) +{ + return ssh_rtl_get_device(&c->rtl); +} +EXPORT_SYMBOL_GPL(ssam_controller_device); + +static void __ssam_controller_release(struct kref *kref) +{ + struct ssam_controller *ctrl = to_ssam_controller(kref, kref); + + /* + * The lock-call here is to satisfy lockdep. At this point we really + * expect this to be the last remaining reference to the controller. + * Anything else is a bug. + */ + ssam_controller_lock(ctrl); + ssam_controller_destroy(ctrl); + ssam_controller_unlock(ctrl); + + kfree(ctrl); +} + +/** + * ssam_controller_get() - Increment reference count of controller. + * @c: The controller. + * + * Return: Returns the controller provided as input. + */ +struct ssam_controller *ssam_controller_get(struct ssam_controller *c) +{ + if (c) + kref_get(&c->kref); + return c; +} +EXPORT_SYMBOL_GPL(ssam_controller_get); + +/** + * ssam_controller_put() - Decrement reference count of controller. + * @c: The controller. + */ +void ssam_controller_put(struct ssam_controller *c) +{ + if (c) + kref_put(&c->kref, __ssam_controller_release); +} +EXPORT_SYMBOL_GPL(ssam_controller_put); + +/** + * ssam_controller_statelock() - Lock the controller against state transitions. + * @c: The controller to lock. + * + * Lock the controller against state transitions. Holding this lock guarantees + * that the controller will not transition between states, i.e. if the + * controller is in state "started", when this lock has been acquired, it will + * remain in this state at least until the lock has been released. + * + * Multiple clients may concurrently hold this lock. In other words: The + * ``statelock`` functions represent the read-lock part of a r/w-semaphore. + * Actions causing state transitions of the controller must be executed while + * holding the write-part of this r/w-semaphore (see ssam_controller_lock() + * and ssam_controller_unlock() for that). + * + * See ssam_controller_stateunlock() for the corresponding unlock function. + */ +void ssam_controller_statelock(struct ssam_controller *c) +{ + down_read(&c->lock); +} +EXPORT_SYMBOL_GPL(ssam_controller_statelock); + +/** + * ssam_controller_stateunlock() - Unlock controller state transitions. + * @c: The controller to unlock. + * + * See ssam_controller_statelock() for the corresponding lock function. + */ +void ssam_controller_stateunlock(struct ssam_controller *c) +{ + up_read(&c->lock); +} +EXPORT_SYMBOL_GPL(ssam_controller_stateunlock); + +/** + * ssam_controller_lock() - Acquire the main controller lock. + * @c: The controller to lock. + * + * This lock must be held for any state transitions, including transition to + * suspend/resumed states and during shutdown. See ssam_controller_statelock() + * for more details on controller locking. + * + * See ssam_controller_unlock() for the corresponding unlock function. + */ +void ssam_controller_lock(struct ssam_controller *c) +{ + down_write(&c->lock); +} + +/* + * ssam_controller_unlock() - Release the main controller lock. + * @c: The controller to unlock. + * + * See ssam_controller_lock() for the corresponding lock function. + */ +void ssam_controller_unlock(struct ssam_controller *c) +{ + up_write(&c->lock); +} + +static void ssam_handle_event(struct ssh_rtl *rtl, + const struct ssh_command *cmd, + const struct ssam_span *data) +{ + struct ssam_controller *ctrl = to_ssam_controller(rtl, rtl); + struct ssam_event_item *item; + + item = ssam_event_item_alloc(data->len, GFP_KERNEL); + if (!item) + return; + + item->rqid = get_unaligned_le16(&cmd->rqid); + item->event.target_category = cmd->tc; + item->event.target_id = cmd->sid; + item->event.command_id = cmd->cid; + item->event.instance_id = cmd->iid; + memcpy(&item->event.data[0], data->ptr, data->len); + + if (WARN_ON(ssam_cplt_submit_event(&ctrl->cplt, item))) + ssam_event_item_free(item); +} + +static const struct ssh_rtl_ops ssam_rtl_ops = { + .handle_event = ssam_handle_event, +}; + +static bool ssam_notifier_is_empty(struct ssam_controller *ctrl); +static void ssam_notifier_unregister_all(struct ssam_controller *ctrl); + +#define SSAM_SSH_DSM_REVISION 0 + +/* d5e383e1-d892-4a76-89fc-f6aaae7ed5b5 */ +static const guid_t SSAM_SSH_DSM_GUID = + GUID_INIT(0xd5e383e1, 0xd892, 0x4a76, + 0x89, 0xfc, 0xf6, 0xaa, 0xae, 0x7e, 0xd5, 0xb5); + +enum ssh_dsm_fn { + SSH_DSM_FN_SSH_POWER_PROFILE = 0x05, + SSH_DSM_FN_SCREEN_ON_SLEEP_IDLE_TIMEOUT = 0x06, + SSH_DSM_FN_SCREEN_OFF_SLEEP_IDLE_TIMEOUT = 0x07, + SSH_DSM_FN_D3_CLOSES_HANDLE = 0x08, + SSH_DSM_FN_SSH_BUFFER_SIZE = 0x09, +}; + +static int ssam_dsm_get_functions(acpi_handle handle, u64 *funcs) +{ + union acpi_object *obj; + u64 mask = 0; + int i; + + *funcs = 0; + + /* + * The _DSM function is only present on newer models. It is not + * present on 5th and 6th generation devices (i.e. up to and including + * Surface Pro 6, Surface Laptop 2, Surface Book 2). + * + * If the _DSM is not present, indicate that no function is supported. + * This will result in default values being set. + */ + if (!acpi_has_method(handle, "_DSM")) + return 0; + + obj = acpi_evaluate_dsm_typed(handle, &SSAM_SSH_DSM_GUID, + SSAM_SSH_DSM_REVISION, 0, NULL, + ACPI_TYPE_BUFFER); + if (!obj) + return -EIO; + + for (i = 0; i < obj->buffer.length && i < 8; i++) + mask |= (((u64)obj->buffer.pointer[i]) << (i * 8)); + + if (mask & BIT(0)) + *funcs = mask; + + ACPI_FREE(obj); + return 0; +} + +static int ssam_dsm_load_u32(acpi_handle handle, u64 funcs, u64 func, u32 *ret) +{ + union acpi_object *obj; + u64 val; + + if (!(funcs & BIT_ULL(func))) + return 0; /* Not supported, leave *ret at its default value */ + + obj = acpi_evaluate_dsm_typed(handle, &SSAM_SSH_DSM_GUID, + SSAM_SSH_DSM_REVISION, func, NULL, + ACPI_TYPE_INTEGER); + if (!obj) + return -EIO; + + val = obj->integer.value; + ACPI_FREE(obj); + + if (val > U32_MAX) + return -ERANGE; + + *ret = val; + return 0; +} + +/** + * ssam_controller_caps_load_from_acpi() - Load controller capabilities from + * ACPI _DSM. + * @handle: The handle of the ACPI controller/SSH device. + * @caps: Where to store the capabilities in. + * + * Initializes the given controller capabilities with default values, then + * checks and, if the respective _DSM functions are available, loads the + * actual capabilities from the _DSM. + * + * Return: Returns zero on success, a negative error code on failure. + */ +static +int ssam_controller_caps_load_from_acpi(acpi_handle handle, + struct ssam_controller_caps *caps) +{ + u32 d3_closes_handle = false; + u64 funcs; + int status; + + /* Pre-load supported DSM functions. */ + status = ssam_dsm_get_functions(handle, &funcs); + if (status) + return status; + + /* Load actual values from ACPI, if present. */ + status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_SSH_POWER_PROFILE, + &caps->ssh_power_profile); + if (status) + return status; + + status = ssam_dsm_load_u32(handle, funcs, + SSH_DSM_FN_SCREEN_ON_SLEEP_IDLE_TIMEOUT, + &caps->screen_on_sleep_idle_timeout); + if (status) + return status; + + status = ssam_dsm_load_u32(handle, funcs, + SSH_DSM_FN_SCREEN_OFF_SLEEP_IDLE_TIMEOUT, + &caps->screen_off_sleep_idle_timeout); + if (status) + return status; + + status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_D3_CLOSES_HANDLE, + &d3_closes_handle); + if (status) + return status; + + caps->d3_closes_handle = !!d3_closes_handle; + + status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_SSH_BUFFER_SIZE, + &caps->ssh_buffer_size); + if (status) + return status; + + return 0; +} + +/** + * ssam_controller_caps_load_from_of() - Load controller capabilities from OF/DT. + * @dev: A pointer to the controller device + * @caps: Where to store the capabilities in. + * + * Return: Returns zero on success, a negative error code on failure. + */ +static int ssam_controller_caps_load_from_of(struct device *dev, struct ssam_controller_caps *caps) +{ + /* + * Every device starting with Surface Pro X through Laptop 7 uses these + * identical values, which makes them good defaults. + */ + caps->d3_closes_handle = true; + caps->screen_on_sleep_idle_timeout = 5000; + caps->screen_off_sleep_idle_timeout = 30; + caps->ssh_buffer_size = 48; + /* TODO: figure out power profile */ + + return 0; +} + +/** + * ssam_controller_caps_load() - Load controller capabilities + * @dev: A pointer to the controller device + * @caps: Where to store the capabilities in. + * + * Return: Returns zero on success, a negative error code on failure. + */ +static int ssam_controller_caps_load(struct device *dev, struct ssam_controller_caps *caps) +{ + acpi_handle handle = ACPI_HANDLE(dev); + + /* Set defaults. */ + caps->ssh_power_profile = U32_MAX; + caps->screen_on_sleep_idle_timeout = U32_MAX; + caps->screen_off_sleep_idle_timeout = U32_MAX; + caps->d3_closes_handle = false; + caps->ssh_buffer_size = U32_MAX; + + if (handle) + return ssam_controller_caps_load_from_acpi(handle, caps); + else + return ssam_controller_caps_load_from_of(dev, caps); +} + +/** + * ssam_controller_init() - Initialize SSAM controller. + * @ctrl: The controller to initialize. + * @serdev: The serial device representing the underlying data transport. + * + * Initializes the given controller. Does neither start receiver nor + * transmitter threads. After this call, the controller has to be hooked up to + * the serdev core separately via &struct serdev_device_ops, relaying calls to + * ssam_controller_receive_buf() and ssam_controller_write_wakeup(). Once the + * controller has been hooked up, transmitter and receiver threads may be + * started via ssam_controller_start(). These setup steps need to be completed + * before controller can be used for requests. + */ +int ssam_controller_init(struct ssam_controller *ctrl, + struct serdev_device *serdev) +{ + int status; + + init_rwsem(&ctrl->lock); + kref_init(&ctrl->kref); + + status = ssam_controller_caps_load(&serdev->dev, &ctrl->caps); + if (status) + return status; + + dev_dbg(&serdev->dev, + "device capabilities:\n" + " ssh_power_profile: %u\n" + " ssh_buffer_size: %u\n" + " screen_on_sleep_idle_timeout: %u\n" + " screen_off_sleep_idle_timeout: %u\n" + " d3_closes_handle: %u\n", + ctrl->caps.ssh_power_profile, + ctrl->caps.ssh_buffer_size, + ctrl->caps.screen_on_sleep_idle_timeout, + ctrl->caps.screen_off_sleep_idle_timeout, + ctrl->caps.d3_closes_handle); + + ssh_seq_reset(&ctrl->counter.seq); + ssh_rqid_reset(&ctrl->counter.rqid); + + /* Initialize event/request completion system. */ + status = ssam_cplt_init(&ctrl->cplt, &serdev->dev); + if (status) + return status; + + /* Initialize request and packet transport layers. */ + status = ssh_rtl_init(&ctrl->rtl, serdev, &ssam_rtl_ops); + if (status) { + ssam_cplt_destroy(&ctrl->cplt); + return status; + } + + /* + * Set state via write_once even though we expect to be in an + * exclusive context, due to smoke-testing in + * ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_INITIALIZED); + return 0; +} + +/** + * ssam_controller_start() - Start the receiver and transmitter threads of the + * controller. + * @ctrl: The controller. + * + * Note: When this function is called, the controller should be properly + * hooked up to the serdev core via &struct serdev_device_ops. Please refer + * to ssam_controller_init() for more details on controller initialization. + * + * This function must be called with the main controller lock held (i.e. by + * calling ssam_controller_lock()). + */ +int ssam_controller_start(struct ssam_controller *ctrl) +{ + int status; + + lockdep_assert_held_write(&ctrl->lock); + + if (ctrl->state != SSAM_CONTROLLER_INITIALIZED) + return -EINVAL; + + status = ssh_rtl_start(&ctrl->rtl); + if (status) + return status; + + /* + * Set state via write_once even though we expect to be locked/in an + * exclusive context, due to smoke-testing in + * ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STARTED); + return 0; +} + +/* + * SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT - Timeout for flushing requests during + * shutdown. + * + * Chosen to be larger than one full request timeout, including packets timing + * out. This value should give ample time to complete any outstanding requests + * during normal operation and account for the odd package timeout. + */ +#define SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT msecs_to_jiffies(5000) + +/** + * ssam_controller_shutdown() - Shut down the controller. + * @ctrl: The controller. + * + * Shuts down the controller by flushing all pending requests and stopping the + * transmitter and receiver threads. All requests submitted after this call + * will fail with %-ESHUTDOWN. While it is discouraged to do so, this function + * is safe to use in parallel with ongoing request submission. + * + * In the course of this shutdown procedure, all currently registered + * notifiers will be unregistered. It is, however, strongly recommended to not + * rely on this behavior, and instead the party registering the notifier + * should unregister it before the controller gets shut down, e.g. via the + * SSAM bus which guarantees client devices to be removed before a shutdown. + * + * Note that events may still be pending after this call, but, due to the + * notifiers being unregistered, these events will be dropped when the + * controller is subsequently destroyed via ssam_controller_destroy(). + * + * This function must be called with the main controller lock held (i.e. by + * calling ssam_controller_lock()). + */ +void ssam_controller_shutdown(struct ssam_controller *ctrl) +{ + enum ssam_controller_state s = ctrl->state; + int status; + + lockdep_assert_held_write(&ctrl->lock); + + if (s == SSAM_CONTROLLER_UNINITIALIZED || s == SSAM_CONTROLLER_STOPPED) + return; + + /* + * Try to flush pending events and requests while everything still + * works. Note: There may still be packets and/or requests in the + * system after this call (e.g. via control packets submitted by the + * packet transport layer or flush timeout / failure, ...). Those will + * be handled with the ssh_rtl_shutdown() call below. + */ + status = ssh_rtl_flush(&ctrl->rtl, SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT); + if (status) { + ssam_err(ctrl, "failed to flush request transport layer: %d\n", + status); + } + + /* Try to flush all currently completing requests and events. */ + ssam_cplt_flush(&ctrl->cplt); + + /* + * We expect all notifiers to have been removed by the respective client + * driver that set them up at this point. If this warning occurs, some + * client driver has not done that... + */ + WARN_ON(!ssam_notifier_is_empty(ctrl)); + + /* + * Nevertheless, we should still take care of drivers that don't behave + * well. Thus disable all enabled events, unregister all notifiers. + */ + ssam_notifier_unregister_all(ctrl); + + /* + * Cancel remaining requests. Ensure no new ones can be queued and stop + * threads. + */ + ssh_rtl_shutdown(&ctrl->rtl); + + /* + * Set state via write_once even though we expect to be locked/in an + * exclusive context, due to smoke-testing in + * ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STOPPED); + ctrl->rtl.ptl.serdev = NULL; +} + +/** + * ssam_controller_destroy() - Destroy the controller and free its resources. + * @ctrl: The controller. + * + * Ensures that all resources associated with the controller get freed. This + * function should only be called after the controller has been stopped via + * ssam_controller_shutdown(). In general, this function should not be called + * directly. The only valid place to call this function directly is during + * initialization, before the controller has been fully initialized and passed + * to other processes. This function is called automatically when the + * reference count of the controller reaches zero. + * + * This function must be called with the main controller lock held (i.e. by + * calling ssam_controller_lock()). + */ +void ssam_controller_destroy(struct ssam_controller *ctrl) +{ + lockdep_assert_held_write(&ctrl->lock); + + if (ctrl->state == SSAM_CONTROLLER_UNINITIALIZED) + return; + + WARN_ON(ctrl->state != SSAM_CONTROLLER_STOPPED && + ctrl->state != SSAM_CONTROLLER_INITIALIZED); + + /* + * Note: New events could still have been received after the previous + * flush in ssam_controller_shutdown, before the request transport layer + * has been shut down. At this point, after the shutdown, we can be sure + * that no new events will be queued. The call to ssam_cplt_destroy will + * ensure that those remaining are being completed and freed. + */ + + /* Actually free resources. */ + ssam_cplt_destroy(&ctrl->cplt); + ssh_rtl_destroy(&ctrl->rtl); + + /* + * Set state via write_once even though we expect to be locked/in an + * exclusive context, due to smoke-testing in + * ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_UNINITIALIZED); +} + +/** + * ssam_controller_suspend() - Suspend the controller. + * @ctrl: The controller to suspend. + * + * Marks the controller as suspended. Note that display-off and D0-exit + * notifications have to be sent manually before transitioning the controller + * into the suspended state via this function. + * + * See ssam_controller_resume() for the corresponding resume function. + * + * Return: Returns %-EINVAL if the controller is currently not in the + * "started" state. + */ +int ssam_controller_suspend(struct ssam_controller *ctrl) +{ + ssam_controller_lock(ctrl); + + if (ctrl->state != SSAM_CONTROLLER_STARTED) { + ssam_controller_unlock(ctrl); + return -EINVAL; + } + + ssam_dbg(ctrl, "pm: suspending controller\n"); + + /* + * Set state via write_once even though we're locked, due to + * smoke-testing in ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_SUSPENDED); + + ssam_controller_unlock(ctrl); + return 0; +} + +/** + * ssam_controller_resume() - Resume the controller from suspend. + * @ctrl: The controller to resume. + * + * Resume the controller from the suspended state it was put into via + * ssam_controller_suspend(). This function does not issue display-on and + * D0-entry notifications. If required, those have to be sent manually after + * this call. + * + * Return: Returns %-EINVAL if the controller is currently not suspended. + */ +int ssam_controller_resume(struct ssam_controller *ctrl) +{ + ssam_controller_lock(ctrl); + + if (ctrl->state != SSAM_CONTROLLER_SUSPENDED) { + ssam_controller_unlock(ctrl); + return -EINVAL; + } + + ssam_dbg(ctrl, "pm: resuming controller\n"); + + /* + * Set state via write_once even though we're locked, due to + * smoke-testing in ssam_request_sync_submit(). + */ + WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STARTED); + + ssam_controller_unlock(ctrl); + return 0; +} + + +/* -- Top-level request interface ------------------------------------------- */ + +/** + * ssam_request_write_data() - Construct and write SAM request message to + * buffer. + * @buf: The buffer to write the data to. + * @ctrl: The controller via which the request will be sent. + * @spec: The request data and specification. + * + * Constructs a SAM/SSH request message and writes it to the provided buffer. + * The request and transport counters, specifically RQID and SEQ, will be set + * in this call. These counters are obtained from the controller. It is thus + * only valid to send the resulting message via the controller specified here. + * + * For calculation of the required buffer size, refer to the + * SSH_COMMAND_MESSAGE_LENGTH() macro. + * + * Return: Returns the number of bytes used in the buffer on success. Returns + * %-EINVAL if the payload length provided in the request specification is too + * large (larger than %SSH_COMMAND_MAX_PAYLOAD_SIZE) or if the provided buffer + * is too small. + */ +ssize_t ssam_request_write_data(struct ssam_span *buf, + struct ssam_controller *ctrl, + const struct ssam_request *spec) +{ + struct msgbuf msgb; + u16 rqid; + u8 seq; + + if (spec->length > SSH_COMMAND_MAX_PAYLOAD_SIZE) + return -EINVAL; + + if (SSH_COMMAND_MESSAGE_LENGTH(spec->length) > buf->len) + return -EINVAL; + + msgb_init(&msgb, buf->ptr, buf->len); + seq = ssh_seq_next(&ctrl->counter.seq); + rqid = ssh_rqid_next(&ctrl->counter.rqid); + msgb_push_cmd(&msgb, seq, rqid, spec); + + return msgb_bytes_used(&msgb); +} +EXPORT_SYMBOL_GPL(ssam_request_write_data); + +static void ssam_request_sync_complete(struct ssh_request *rqst, + const struct ssh_command *cmd, + const struct ssam_span *data, int status) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + struct ssam_request_sync *r; + + r = container_of(rqst, struct ssam_request_sync, base); + r->status = status; + + if (r->resp) + r->resp->length = 0; + + if (status) { + rtl_dbg_cond(rtl, "rsp: request failed: %d\n", status); + return; + } + + if (!data) /* Handle requests without a response. */ + return; + + if (!r->resp || !r->resp->pointer) { + if (data->len) + rtl_warn(rtl, "rsp: no response buffer provided, dropping data\n"); + return; + } + + if (data->len > r->resp->capacity) { + rtl_err(rtl, + "rsp: response buffer too small, capacity: %zu bytes, got: %zu bytes\n", + r->resp->capacity, data->len); + r->status = -ENOSPC; + return; + } + + r->resp->length = data->len; + memcpy(r->resp->pointer, data->ptr, data->len); +} + +static void ssam_request_sync_release(struct ssh_request *rqst) +{ + complete_all(&container_of(rqst, struct ssam_request_sync, base)->comp); +} + +static const struct ssh_request_ops ssam_request_sync_ops = { + .release = ssam_request_sync_release, + .complete = ssam_request_sync_complete, +}; + +/** + * ssam_request_sync_alloc() - Allocate a synchronous request. + * @payload_len: The length of the request payload. + * @flags: Flags used for allocation. + * @rqst: Where to store the pointer to the allocated request. + * @buffer: Where to store the buffer descriptor for the message buffer of + * the request. + * + * Allocates a synchronous request with corresponding message buffer. The + * request still needs to be initialized ssam_request_sync_init() before + * it can be submitted, and the message buffer data must still be set to the + * returned buffer via ssam_request_sync_set_data() after it has been filled, + * if need be with adjusted message length. + * + * After use, the request and its corresponding message buffer should be freed + * via ssam_request_sync_free(). The buffer must not be freed separately. + * + * Return: Returns zero on success, %-ENOMEM if the request could not be + * allocated. + */ +int ssam_request_sync_alloc(size_t payload_len, gfp_t flags, + struct ssam_request_sync **rqst, + struct ssam_span *buffer) +{ + size_t msglen = SSH_COMMAND_MESSAGE_LENGTH(payload_len); + + *rqst = kzalloc(sizeof(**rqst) + msglen, flags); + if (!*rqst) + return -ENOMEM; + + buffer->ptr = (u8 *)(*rqst + 1); + buffer->len = msglen; + + return 0; +} +EXPORT_SYMBOL_GPL(ssam_request_sync_alloc); + +/** + * ssam_request_sync_free() - Free a synchronous request. + * @rqst: The request to be freed. + * + * Free a synchronous request and its corresponding buffer allocated with + * ssam_request_sync_alloc(). Do not use for requests allocated on the stack + * or via any other function. + * + * Warning: The caller must ensure that the request is not in use any more. + * I.e. the caller must ensure that it has the only reference to the request + * and the request is not currently pending. This means that the caller has + * either never submitted the request, request submission has failed, or the + * caller has waited until the submitted request has been completed via + * ssam_request_sync_wait(). + */ +void ssam_request_sync_free(struct ssam_request_sync *rqst) +{ + kfree(rqst); +} +EXPORT_SYMBOL_GPL(ssam_request_sync_free); + +/** + * ssam_request_sync_init() - Initialize a synchronous request struct. + * @rqst: The request to initialize. + * @flags: The request flags. + * + * Initializes the given request struct. Does not initialize the request + * message data. This has to be done explicitly after this call via + * ssam_request_sync_set_data() and the actual message data has to be written + * via ssam_request_write_data(). + * + * Return: Returns zero on success or %-EINVAL if the given flags are invalid. + */ +int ssam_request_sync_init(struct ssam_request_sync *rqst, + enum ssam_request_flags flags) +{ + int status; + + status = ssh_request_init(&rqst->base, flags, &ssam_request_sync_ops); + if (status) + return status; + + init_completion(&rqst->comp); + rqst->resp = NULL; + rqst->status = 0; + + return 0; +} +EXPORT_SYMBOL_GPL(ssam_request_sync_init); + +/** + * ssam_request_sync_submit() - Submit a synchronous request. + * @ctrl: The controller with which to submit the request. + * @rqst: The request to submit. + * + * Submit a synchronous request. The request has to be initialized and + * properly set up, including response buffer (may be %NULL if no response is + * expected) and command message data. This function does not wait for the + * request to be completed. + * + * If this function succeeds, ssam_request_sync_wait() must be used to ensure + * that the request has been completed before the response data can be + * accessed and/or the request can be freed. On failure, the request may + * immediately be freed. + * + * This function may only be used if the controller is active, i.e. has been + * initialized and not suspended. + */ +int ssam_request_sync_submit(struct ssam_controller *ctrl, + struct ssam_request_sync *rqst) +{ + int status; + + /* + * This is only a superficial check. In general, the caller needs to + * ensure that the controller is initialized and is not (and does not + * get) suspended during use, i.e. until the request has been completed + * (if _absolutely_ necessary, by use of ssam_controller_statelock/ + * ssam_controller_stateunlock, but something like ssam_client_link + * should be preferred as this needs to last until the request has been + * completed). + * + * Note that it is actually safe to use this function while the + * controller is in the process of being shut down (as ssh_rtl_submit + * is safe with regards to this), but it is generally discouraged to do + * so. + */ + if (WARN_ON(READ_ONCE(ctrl->state) != SSAM_CONTROLLER_STARTED)) { + ssh_request_put(&rqst->base); + return -ENODEV; + } + + status = ssh_rtl_submit(&ctrl->rtl, &rqst->base); + ssh_request_put(&rqst->base); + + return status; +} +EXPORT_SYMBOL_GPL(ssam_request_sync_submit); + +/** + * ssam_request_do_sync() - Execute a synchronous request. + * @ctrl: The controller via which the request will be submitted. + * @spec: The request specification and payload. + * @rsp: The response buffer. + * + * Allocates a synchronous request with its message data buffer on the heap + * via ssam_request_sync_alloc(), fully initializes it via the provided + * request specification, submits it, and finally waits for its completion + * before freeing it and returning its status. + * + * Return: Returns the status of the request or any failure during setup. + */ +int ssam_request_do_sync(struct ssam_controller *ctrl, + const struct ssam_request *spec, + struct ssam_response *rsp) +{ + struct ssam_request_sync *rqst; + struct ssam_span buf; + ssize_t len; + int status; + + status = ssam_request_sync_alloc(spec->length, GFP_KERNEL, &rqst, &buf); + if (status) + return status; + + status = ssam_request_sync_init(rqst, spec->flags); + if (status) { + ssam_request_sync_free(rqst); + return status; + } + + ssam_request_sync_set_resp(rqst, rsp); + + len = ssam_request_write_data(&buf, ctrl, spec); + if (len < 0) { + ssam_request_sync_free(rqst); + return len; + } + + ssam_request_sync_set_data(rqst, buf.ptr, len); + + status = ssam_request_sync_submit(ctrl, rqst); + if (!status) + status = ssam_request_sync_wait(rqst); + + ssam_request_sync_free(rqst); + return status; +} +EXPORT_SYMBOL_GPL(ssam_request_do_sync); + +/** + * ssam_request_do_sync_with_buffer() - Execute a synchronous request with the + * provided buffer as back-end for the message buffer. + * @ctrl: The controller via which the request will be submitted. + * @spec: The request specification and payload. + * @rsp: The response buffer. + * @buf: The buffer for the request message data. + * + * Allocates a synchronous request struct on the stack, fully initializes it + * using the provided buffer as message data buffer, submits it, and then + * waits for its completion before returning its status. The + * SSH_COMMAND_MESSAGE_LENGTH() macro can be used to compute the required + * message buffer size. + * + * This function does essentially the same as ssam_request_do_sync(), but + * instead of dynamically allocating the request and message data buffer, it + * uses the provided message data buffer and stores the (small) request struct + * on the heap. + * + * Return: Returns the status of the request or any failure during setup. + */ +int ssam_request_do_sync_with_buffer(struct ssam_controller *ctrl, + const struct ssam_request *spec, + struct ssam_response *rsp, + struct ssam_span *buf) +{ + struct ssam_request_sync rqst; + ssize_t len; + int status; + + status = ssam_request_sync_init(&rqst, spec->flags); + if (status) + return status; + + ssam_request_sync_set_resp(&rqst, rsp); + + len = ssam_request_write_data(buf, ctrl, spec); + if (len < 0) + return len; + + ssam_request_sync_set_data(&rqst, buf->ptr, len); + + status = ssam_request_sync_submit(ctrl, &rqst); + if (!status) + status = ssam_request_sync_wait(&rqst); + + return status; +} +EXPORT_SYMBOL_GPL(ssam_request_do_sync_with_buffer); + + +/* -- Internal SAM requests. ------------------------------------------------ */ + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_get_firmware_version, __le32, { + .target_category = SSAM_SSH_TC_SAM, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x13, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_off, u8, { + .target_category = SSAM_SSH_TC_SAM, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x15, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_on, u8, { + .target_category = SSAM_SSH_TC_SAM, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x16, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_exit, u8, { + .target_category = SSAM_SSH_TC_SAM, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x33, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_entry, u8, { + .target_category = SSAM_SSH_TC_SAM, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x34, + .instance_id = 0x00, +}); + +/** + * struct ssh_notification_params - Command payload to enable/disable SSH + * notifications. + * @target_category: The target category for which notifications should be + * enabled/disabled. + * @flags: Flags determining how notifications are being sent. + * @request_id: The request ID that is used to send these notifications. + * @instance_id: The specific instance in the given target category for + * which notifications should be enabled. + */ +struct ssh_notification_params { + u8 target_category; + u8 flags; + __le16 request_id; + u8 instance_id; +} __packed; + +static_assert(sizeof(struct ssh_notification_params) == 5); + +static int __ssam_ssh_event_request(struct ssam_controller *ctrl, + struct ssam_event_registry reg, u8 cid, + struct ssam_event_id id, u8 flags) +{ + struct ssh_notification_params params; + struct ssam_request rqst; + struct ssam_response result; + int status; + + u16 rqid = ssh_tc_to_rqid(id.target_category); + u8 buf = 0; + + /* Only allow RQIDs that lie within the event spectrum. */ + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + params.target_category = id.target_category; + params.instance_id = id.instance; + params.flags = flags; + put_unaligned_le16(rqid, ¶ms.request_id); + + rqst.target_category = reg.target_category; + rqst.target_id = reg.target_id; + rqst.command_id = cid; + rqst.instance_id = 0x00; + rqst.flags = SSAM_REQUEST_HAS_RESPONSE; + rqst.length = sizeof(params); + rqst.payload = (u8 *)¶ms; + + result.capacity = sizeof(buf); + result.length = 0; + result.pointer = &buf; + + status = ssam_retry(ssam_request_do_sync_onstack, ctrl, &rqst, &result, + sizeof(params)); + + return status < 0 ? status : buf; +} + +/** + * ssam_ssh_event_enable() - Enable SSH event. + * @ctrl: The controller for which to enable the event. + * @reg: The event registry describing what request to use for enabling and + * disabling the event. + * @id: The event identifier. + * @flags: The event flags. + * + * Enables the specified event on the EC. This function does not manage + * reference counting of enabled events and is basically only a wrapper for + * the raw EC request. If the specified event is already enabled, the EC will + * ignore this request. + * + * Return: Returns the status of the executed SAM request (zero on success and + * negative on direct failure) or %-EPROTO if the request response indicates a + * failure. + */ +static int ssam_ssh_event_enable(struct ssam_controller *ctrl, + struct ssam_event_registry reg, + struct ssam_event_id id, u8 flags) +{ + int status; + + status = __ssam_ssh_event_request(ctrl, reg, reg.cid_enable, id, flags); + + if (status < 0 && status != -EINVAL) { + ssam_err(ctrl, + "failed to enable event source (tc: %#04x, iid: %#04x, reg: %#04x)\n", + id.target_category, id.instance, reg.target_category); + } + + if (status > 0) { + ssam_err(ctrl, + "unexpected result while enabling event source: %#04x (tc: %#04x, iid: %#04x, reg: %#04x)\n", + status, id.target_category, id.instance, reg.target_category); + return -EPROTO; + } + + return status; +} + +/** + * ssam_ssh_event_disable() - Disable SSH event. + * @ctrl: The controller for which to disable the event. + * @reg: The event registry describing what request to use for enabling and + * disabling the event (must be same as used when enabling the event). + * @id: The event identifier. + * @flags: The event flags (likely ignored for disabling of events). + * + * Disables the specified event on the EC. This function does not manage + * reference counting of enabled events and is basically only a wrapper for + * the raw EC request. If the specified event is already disabled, the EC will + * ignore this request. + * + * Return: Returns the status of the executed SAM request (zero on success and + * negative on direct failure) or %-EPROTO if the request response indicates a + * failure. + */ +static int ssam_ssh_event_disable(struct ssam_controller *ctrl, + struct ssam_event_registry reg, + struct ssam_event_id id, u8 flags) +{ + int status; + + status = __ssam_ssh_event_request(ctrl, reg, reg.cid_disable, id, flags); + + if (status < 0 && status != -EINVAL) { + ssam_err(ctrl, + "failed to disable event source (tc: %#04x, iid: %#04x, reg: %#04x)\n", + id.target_category, id.instance, reg.target_category); + } + + if (status > 0) { + ssam_err(ctrl, + "unexpected result while disabling event source: %#04x (tc: %#04x, iid: %#04x, reg: %#04x)\n", + status, id.target_category, id.instance, reg.target_category); + return -EPROTO; + } + + return status; +} + + +/* -- Wrappers for internal SAM requests. ----------------------------------- */ + +/** + * ssam_get_firmware_version() - Get the SAM/EC firmware version. + * @ctrl: The controller. + * @version: Where to store the version number. + * + * Return: Returns zero on success or the status of the executed SAM request + * if that request failed. + */ +int ssam_get_firmware_version(struct ssam_controller *ctrl, u32 *version) +{ + __le32 __version; + int status; + + status = ssam_retry(ssam_ssh_get_firmware_version, ctrl, &__version); + if (status) + return status; + + *version = le32_to_cpu(__version); + return 0; +} + +/** + * ssam_ctrl_notif_display_off() - Notify EC that the display has been turned + * off. + * @ctrl: The controller. + * + * Notify the EC that the display has been turned off and the driver may enter + * a lower-power state. This will prevent events from being sent directly. + * Rather, the EC signals an event by pulling the wakeup GPIO high for as long + * as there are pending events. The events then need to be manually released, + * one by one, via the GPIO callback request. All pending events accumulated + * during this state can also be released by issuing the display-on + * notification, e.g. via ssam_ctrl_notif_display_on(), which will also reset + * the GPIO. + * + * On some devices, specifically ones with an integrated keyboard, the keyboard + * backlight will be turned off by this call. + * + * This function will only send the display-off notification command if + * display notifications are supported by the EC. Currently all known devices + * support these notifications. + * + * Use ssam_ctrl_notif_display_on() to reverse the effects of this function. + * + * Return: Returns zero on success or if no request has been executed, the + * status of the executed SAM request if that request failed, or %-EPROTO if + * an unexpected response has been received. + */ +int ssam_ctrl_notif_display_off(struct ssam_controller *ctrl) +{ + int status; + u8 response; + + ssam_dbg(ctrl, "pm: notifying display off\n"); + + status = ssam_retry(ssam_ssh_notif_display_off, ctrl, &response); + if (status) + return status; + + if (response != 0) { + ssam_err(ctrl, "unexpected response from display-off notification: %#04x\n", + response); + return -EPROTO; + } + + return 0; +} + +/** + * ssam_ctrl_notif_display_on() - Notify EC that the display has been turned on. + * @ctrl: The controller. + * + * Notify the EC that the display has been turned back on and the driver has + * exited its lower-power state. This notification is the counterpart to the + * display-off notification sent via ssam_ctrl_notif_display_off() and will + * reverse its effects, including resetting events to their default behavior. + * + * This function will only send the display-on notification command if display + * notifications are supported by the EC. Currently all known devices support + * these notifications. + * + * See ssam_ctrl_notif_display_off() for more details. + * + * Return: Returns zero on success or if no request has been executed, the + * status of the executed SAM request if that request failed, or %-EPROTO if + * an unexpected response has been received. + */ +int ssam_ctrl_notif_display_on(struct ssam_controller *ctrl) +{ + int status; + u8 response; + + ssam_dbg(ctrl, "pm: notifying display on\n"); + + status = ssam_retry(ssam_ssh_notif_display_on, ctrl, &response); + if (status) + return status; + + if (response != 0) { + ssam_err(ctrl, "unexpected response from display-on notification: %#04x\n", + response); + return -EPROTO; + } + + return 0; +} + +/** + * ssam_ctrl_notif_d0_exit() - Notify EC that the driver/device exits the D0 + * power state. + * @ctrl: The controller + * + * Notifies the EC that the driver prepares to exit the D0 power state in + * favor of a lower-power state. Exact effects of this function related to the + * EC are currently unknown. + * + * This function will only send the D0-exit notification command if D0-state + * notifications are supported by the EC. Only newer Surface generations + * support these notifications. + * + * Use ssam_ctrl_notif_d0_entry() to reverse the effects of this function. + * + * Return: Returns zero on success or if no request has been executed, the + * status of the executed SAM request if that request failed, or %-EPROTO if + * an unexpected response has been received. + */ +int ssam_ctrl_notif_d0_exit(struct ssam_controller *ctrl) +{ + int status; + u8 response; + + if (!ctrl->caps.d3_closes_handle) + return 0; + + ssam_dbg(ctrl, "pm: notifying D0 exit\n"); + + status = ssam_retry(ssam_ssh_notif_d0_exit, ctrl, &response); + if (status) + return status; + + if (response != 0) { + ssam_err(ctrl, "unexpected response from D0-exit notification: %#04x\n", + response); + return -EPROTO; + } + + return 0; +} + +/** + * ssam_ctrl_notif_d0_entry() - Notify EC that the driver/device enters the D0 + * power state. + * @ctrl: The controller + * + * Notifies the EC that the driver has exited a lower-power state and entered + * the D0 power state. Exact effects of this function related to the EC are + * currently unknown. + * + * This function will only send the D0-entry notification command if D0-state + * notifications are supported by the EC. Only newer Surface generations + * support these notifications. + * + * See ssam_ctrl_notif_d0_exit() for more details. + * + * Return: Returns zero on success or if no request has been executed, the + * status of the executed SAM request if that request failed, or %-EPROTO if + * an unexpected response has been received. + */ +int ssam_ctrl_notif_d0_entry(struct ssam_controller *ctrl) +{ + int status; + u8 response; + + if (!ctrl->caps.d3_closes_handle) + return 0; + + ssam_dbg(ctrl, "pm: notifying D0 entry\n"); + + status = ssam_retry(ssam_ssh_notif_d0_entry, ctrl, &response); + if (status) + return status; + + if (response != 0) { + ssam_err(ctrl, "unexpected response from D0-entry notification: %#04x\n", + response); + return -EPROTO; + } + + return 0; +} + + +/* -- Top-level event registry interface. ----------------------------------- */ + +/** + * ssam_nf_refcount_enable() - Enable event for reference count entry if it has + * not already been enabled. + * @ctrl: The controller to enable the event on. + * @entry: The reference count entry for the event to be enabled. + * @flags: The flags used for enabling the event on the EC. + * + * Enable the event associated with the given reference count entry if the + * reference count equals one, i.e. the event has not previously been enabled. + * If the event has already been enabled (i.e. reference count not equal to + * one), check that the flags used for enabling match and warn about this if + * they do not. + * + * This does not modify the reference count itself, which is done with + * ssam_nf_refcount_inc() / ssam_nf_refcount_dec(). + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns zero on success. If the event is enabled by this call, + * returns the status of the event-enable EC command. + */ +static int ssam_nf_refcount_enable(struct ssam_controller *ctrl, + struct ssam_nf_refcount_entry *entry, u8 flags) +{ + const struct ssam_event_registry reg = entry->key.reg; + const struct ssam_event_id id = entry->key.id; + struct ssam_nf *nf = &ctrl->cplt.event.notif; + int status; + + lockdep_assert_held(&nf->lock); + + ssam_dbg(ctrl, "enabling event (reg: %#04x, tc: %#04x, iid: %#04x, rc: %d)\n", + reg.target_category, id.target_category, id.instance, entry->refcount); + + if (entry->refcount == 1) { + status = ssam_ssh_event_enable(ctrl, reg, id, flags); + if (status) + return status; + + entry->flags = flags; + + } else if (entry->flags != flags) { + ssam_warn(ctrl, + "inconsistent flags when enabling event: got %#04x, expected %#04x (reg: %#04x, tc: %#04x, iid: %#04x)\n", + flags, entry->flags, reg.target_category, id.target_category, + id.instance); + } + + return 0; +} + +/** + * ssam_nf_refcount_disable_free() - Disable event for reference count entry if + * it is no longer in use and free the corresponding entry. + * @ctrl: The controller to disable the event on. + * @entry: The reference count entry for the event to be disabled. + * @flags: The flags used for enabling the event on the EC. + * @ec: Flag specifying if the event should actually be disabled on the EC. + * + * If ``ec`` equals ``true`` and the reference count equals zero (i.e. the + * event is no longer requested by any client), the specified event will be + * disabled on the EC via the corresponding request. + * + * If ``ec`` equals ``false``, no request will be sent to the EC and the event + * can be considered in a detached state (i.e. no longer used but still + * enabled). Disabling an event via this method may be required for + * hot-removable devices, where event disable requests may time out after the + * device has been physically removed. + * + * In both cases, if the reference count equals zero, the corresponding + * reference count entry will be freed. The reference count entry must not be + * used any more after a call to this function. + * + * Also checks if the flags used for disabling the event match the flags used + * for enabling the event and warns if they do not (regardless of reference + * count). + * + * This does not modify the reference count itself, which is done with + * ssam_nf_refcount_inc() / ssam_nf_refcount_dec(). + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns zero on success. If the event is disabled by this call, + * returns the status of the event-enable EC command. + */ +static int ssam_nf_refcount_disable_free(struct ssam_controller *ctrl, + struct ssam_nf_refcount_entry *entry, u8 flags, bool ec) +{ + const struct ssam_event_registry reg = entry->key.reg; + const struct ssam_event_id id = entry->key.id; + struct ssam_nf *nf = &ctrl->cplt.event.notif; + int status = 0; + + lockdep_assert_held(&nf->lock); + + ssam_dbg(ctrl, "%s event (reg: %#04x, tc: %#04x, iid: %#04x, rc: %d)\n", + ec ? "disabling" : "detaching", reg.target_category, id.target_category, + id.instance, entry->refcount); + + if (entry->flags != flags) { + ssam_warn(ctrl, + "inconsistent flags when disabling event: got %#04x, expected %#04x (reg: %#04x, tc: %#04x, iid: %#04x)\n", + flags, entry->flags, reg.target_category, id.target_category, + id.instance); + } + + if (ec && entry->refcount == 0) { + status = ssam_ssh_event_disable(ctrl, reg, id, flags); + kfree(entry); + } + + return status; +} + +/** + * ssam_notifier_register() - Register an event notifier. + * @ctrl: The controller to register the notifier on. + * @n: The event notifier to register. + * + * Register an event notifier. Increment the usage counter of the associated + * SAM event if the notifier is not marked as an observer. If the event is not + * marked as an observer and is currently not enabled, it will be enabled + * during this call. If the notifier is marked as an observer, no attempt will + * be made at enabling any event and no reference count will be modified. + * + * Notifiers marked as observers do not need to be associated with one specific + * event, i.e. as long as no event matching is performed, only the event target + * category needs to be set. + * + * Return: Returns zero on success, %-ENOSPC if there have already been + * %INT_MAX notifiers for the event ID/type associated with the notifier block + * registered, %-ENOMEM if the corresponding event entry could not be + * allocated. If this is the first time that a notifier block is registered + * for the specific associated event, returns the status of the event-enable + * EC-command. + */ +int ssam_notifier_register(struct ssam_controller *ctrl, struct ssam_event_notifier *n) +{ + u16 rqid = ssh_tc_to_rqid(n->event.id.target_category); + struct ssam_nf_refcount_entry *entry = NULL; + struct ssam_nf_head *nf_head; + struct ssam_nf *nf; + int status; + + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + nf = &ctrl->cplt.event.notif; + nf_head = &nf->head[ssh_rqid_to_event(rqid)]; + + mutex_lock(&nf->lock); + + if (!(n->flags & SSAM_EVENT_NOTIFIER_OBSERVER)) { + entry = ssam_nf_refcount_inc(nf, n->event.reg, n->event.id); + if (IS_ERR(entry)) { + mutex_unlock(&nf->lock); + return PTR_ERR(entry); + } + } + + status = ssam_nfblk_insert(nf_head, &n->base); + if (status) { + if (entry) + ssam_nf_refcount_dec_free(nf, n->event.reg, n->event.id); + + mutex_unlock(&nf->lock); + return status; + } + + if (entry) { + status = ssam_nf_refcount_enable(ctrl, entry, n->event.flags); + if (status) { + ssam_nfblk_remove(&n->base); + ssam_nf_refcount_dec_free(nf, n->event.reg, n->event.id); + mutex_unlock(&nf->lock); + synchronize_srcu(&nf_head->srcu); + return status; + } + } + + mutex_unlock(&nf->lock); + return 0; +} +EXPORT_SYMBOL_GPL(ssam_notifier_register); + +/** + * __ssam_notifier_unregister() - Unregister an event notifier. + * @ctrl: The controller the notifier has been registered on. + * @n: The event notifier to unregister. + * @disable: Whether to disable the corresponding event on the EC. + * + * Unregister an event notifier. Decrement the usage counter of the associated + * SAM event if the notifier is not marked as an observer. If the usage counter + * reaches zero and ``disable`` equals ``true``, the event will be disabled. + * + * Useful for hot-removable devices, where communication may fail once the + * device has been physically removed. In that case, specifying ``disable`` as + * ``false`` avoids communication with the EC. + * + * Return: Returns zero on success, %-ENOENT if the given notifier block has + * not been registered on the controller. If the given notifier block was the + * last one associated with its specific event, returns the status of the + * event-disable EC-command. + */ +int __ssam_notifier_unregister(struct ssam_controller *ctrl, struct ssam_event_notifier *n, + bool disable) +{ + u16 rqid = ssh_tc_to_rqid(n->event.id.target_category); + struct ssam_nf_refcount_entry *entry; + struct ssam_nf_head *nf_head; + struct ssam_nf *nf; + int status = 0; + + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + nf = &ctrl->cplt.event.notif; + nf_head = &nf->head[ssh_rqid_to_event(rqid)]; + + mutex_lock(&nf->lock); + + if (!ssam_nfblk_find(nf_head, &n->base)) { + mutex_unlock(&nf->lock); + return -ENOENT; + } + + /* + * If this is an observer notifier, do not attempt to disable the + * event, just remove it. + */ + if (!(n->flags & SSAM_EVENT_NOTIFIER_OBSERVER)) { + entry = ssam_nf_refcount_dec(nf, n->event.reg, n->event.id); + if (WARN_ON(!entry)) { + /* + * If this does not return an entry, there's a logic + * error somewhere: The notifier block is registered, + * but the event refcount entry is not there. Remove + * the notifier block anyways. + */ + status = -ENOENT; + goto remove; + } + + status = ssam_nf_refcount_disable_free(ctrl, entry, n->event.flags, disable); + } + +remove: + ssam_nfblk_remove(&n->base); + mutex_unlock(&nf->lock); + synchronize_srcu(&nf_head->srcu); + + return status; +} +EXPORT_SYMBOL_GPL(__ssam_notifier_unregister); + +/** + * ssam_controller_event_enable() - Enable the specified event. + * @ctrl: The controller to enable the event for. + * @reg: The event registry to use for enabling the event. + * @id: The event ID specifying the event to be enabled. + * @flags: The SAM event flags used for enabling the event. + * + * Increment the event reference count of the specified event. If the event has + * not been enabled previously, it will be enabled by this call. + * + * Note: In general, ssam_notifier_register() with a non-observer notifier + * should be preferred for enabling/disabling events, as this will guarantee + * proper ordering and event forwarding in case of errors during event + * enabling/disabling. + * + * Return: Returns zero on success, %-ENOSPC if the reference count for the + * specified event has reached its maximum, %-ENOMEM if the corresponding event + * entry could not be allocated. If this is the first time that this event has + * been enabled (i.e. the reference count was incremented from zero to one by + * this call), returns the status of the event-enable EC-command. + */ +int ssam_controller_event_enable(struct ssam_controller *ctrl, + struct ssam_event_registry reg, + struct ssam_event_id id, u8 flags) +{ + u16 rqid = ssh_tc_to_rqid(id.target_category); + struct ssam_nf *nf = &ctrl->cplt.event.notif; + struct ssam_nf_refcount_entry *entry; + int status; + + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + mutex_lock(&nf->lock); + + entry = ssam_nf_refcount_inc(nf, reg, id); + if (IS_ERR(entry)) { + mutex_unlock(&nf->lock); + return PTR_ERR(entry); + } + + status = ssam_nf_refcount_enable(ctrl, entry, flags); + if (status) { + ssam_nf_refcount_dec_free(nf, reg, id); + mutex_unlock(&nf->lock); + return status; + } + + mutex_unlock(&nf->lock); + return 0; +} +EXPORT_SYMBOL_GPL(ssam_controller_event_enable); + +/** + * ssam_controller_event_disable() - Disable the specified event. + * @ctrl: The controller to disable the event for. + * @reg: The event registry to use for disabling the event. + * @id: The event ID specifying the event to be disabled. + * @flags: The flags used when enabling the event. + * + * Decrement the reference count of the specified event. If the reference count + * reaches zero, the event will be disabled. + * + * Note: In general, ssam_notifier_register()/ssam_notifier_unregister() with a + * non-observer notifier should be preferred for enabling/disabling events, as + * this will guarantee proper ordering and event forwarding in case of errors + * during event enabling/disabling. + * + * Return: Returns zero on success, %-ENOENT if the given event has not been + * enabled on the controller. If the reference count of the event reaches zero + * during this call, returns the status of the event-disable EC-command. + */ +int ssam_controller_event_disable(struct ssam_controller *ctrl, + struct ssam_event_registry reg, + struct ssam_event_id id, u8 flags) +{ + u16 rqid = ssh_tc_to_rqid(id.target_category); + struct ssam_nf *nf = &ctrl->cplt.event.notif; + struct ssam_nf_refcount_entry *entry; + int status; + + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + mutex_lock(&nf->lock); + + entry = ssam_nf_refcount_dec(nf, reg, id); + if (!entry) { + mutex_unlock(&nf->lock); + return -ENOENT; + } + + status = ssam_nf_refcount_disable_free(ctrl, entry, flags, true); + + mutex_unlock(&nf->lock); + return status; +} +EXPORT_SYMBOL_GPL(ssam_controller_event_disable); + +/** + * ssam_notifier_disable_registered() - Disable events for all registered + * notifiers. + * @ctrl: The controller for which to disable the notifiers/events. + * + * Disables events for all currently registered notifiers. In case of an error + * (EC command failing), all previously disabled events will be restored and + * the error code returned. + * + * This function is intended to disable all events prior to hibernation entry. + * See ssam_notifier_restore_registered() to restore/re-enable all events + * disabled with this function. + * + * Note that this function will not disable events for notifiers registered + * after calling this function. It should thus be made sure that no new + * notifiers are going to be added after this call and before the corresponding + * call to ssam_notifier_restore_registered(). + * + * Return: Returns zero on success. In case of failure returns the error code + * returned by the failed EC command to disable an event. + */ +int ssam_notifier_disable_registered(struct ssam_controller *ctrl) +{ + struct ssam_nf *nf = &ctrl->cplt.event.notif; + struct rb_node *n; + int status; + + mutex_lock(&nf->lock); + for (n = rb_first(&nf->refcount); n; n = rb_next(n)) { + struct ssam_nf_refcount_entry *e; + + e = rb_entry(n, struct ssam_nf_refcount_entry, node); + status = ssam_ssh_event_disable(ctrl, e->key.reg, + e->key.id, e->flags); + if (status) + goto err; + } + mutex_unlock(&nf->lock); + + return 0; + +err: + for (n = rb_prev(n); n; n = rb_prev(n)) { + struct ssam_nf_refcount_entry *e; + + e = rb_entry(n, struct ssam_nf_refcount_entry, node); + ssam_ssh_event_enable(ctrl, e->key.reg, e->key.id, e->flags); + } + mutex_unlock(&nf->lock); + + return status; +} + +/** + * ssam_notifier_restore_registered() - Restore/re-enable events for all + * registered notifiers. + * @ctrl: The controller for which to restore the notifiers/events. + * + * Restores/re-enables all events for which notifiers have been registered on + * the given controller. In case of a failure, the error is logged and the + * function continues to try and enable the remaining events. + * + * This function is intended to restore/re-enable all registered events after + * hibernation. See ssam_notifier_disable_registered() for the counter part + * disabling the events and more details. + */ +void ssam_notifier_restore_registered(struct ssam_controller *ctrl) +{ + struct ssam_nf *nf = &ctrl->cplt.event.notif; + struct rb_node *n; + + mutex_lock(&nf->lock); + for (n = rb_first(&nf->refcount); n; n = rb_next(n)) { + struct ssam_nf_refcount_entry *e; + + e = rb_entry(n, struct ssam_nf_refcount_entry, node); + + /* Ignore errors, will get logged in call. */ + ssam_ssh_event_enable(ctrl, e->key.reg, e->key.id, e->flags); + } + mutex_unlock(&nf->lock); +} + +/** + * ssam_notifier_is_empty() - Check if there are any registered notifiers. + * @ctrl: The controller to check on. + * + * Return: Returns %true if there are currently no notifiers registered on the + * controller, %false otherwise. + */ +static bool ssam_notifier_is_empty(struct ssam_controller *ctrl) +{ + struct ssam_nf *nf = &ctrl->cplt.event.notif; + bool result; + + mutex_lock(&nf->lock); + result = ssam_nf_refcount_empty(nf); + mutex_unlock(&nf->lock); + + return result; +} + +/** + * ssam_notifier_unregister_all() - Unregister all currently registered + * notifiers. + * @ctrl: The controller to unregister the notifiers on. + * + * Unregisters all currently registered notifiers. This function is used to + * ensure that all notifiers will be unregistered and associated + * entries/resources freed when the controller is being shut down. + */ +static void ssam_notifier_unregister_all(struct ssam_controller *ctrl) +{ + struct ssam_nf *nf = &ctrl->cplt.event.notif; + struct ssam_nf_refcount_entry *e, *n; + + mutex_lock(&nf->lock); + rbtree_postorder_for_each_entry_safe(e, n, &nf->refcount, node) { + /* Ignore errors, will get logged in call. */ + ssam_ssh_event_disable(ctrl, e->key.reg, e->key.id, e->flags); + kfree(e); + } + nf->refcount = RB_ROOT; + mutex_unlock(&nf->lock); +} + + +/* -- Wakeup IRQ. ----------------------------------------------------------- */ + +static irqreturn_t ssam_irq_handle(int irq, void *dev_id) +{ + struct ssam_controller *ctrl = dev_id; + + ssam_dbg(ctrl, "pm: wake irq triggered\n"); + + /* + * Note: Proper wakeup detection is currently unimplemented. + * When the EC is in display-off or any other non-D0 state, it + * does not send events/notifications to the host. Instead it + * signals that there are events available via the wakeup IRQ. + * This driver is responsible for calling back to the EC to + * release these events one-by-one. + * + * This IRQ should not cause a full system resume by its own. + * Instead, events should be handled by their respective subsystem + * drivers, which in turn should signal whether a full system + * resume should be performed. + * + * TODO: Send GPIO callback command repeatedly to EC until callback + * returns 0x00. Return flag of callback is "has more events". + * Each time the command is sent, one event is "released". Once + * all events have been released (return = 0x00), the GPIO is + * re-armed. Detect wakeup events during this process, go back to + * sleep if no wakeup event has been received. + */ + + return IRQ_HANDLED; +} + +/** + * ssam_irq_setup() - Set up SAM EC wakeup-GPIO interrupt. + * @ctrl: The controller for which the IRQ should be set up. + * + * Set up an IRQ for the wakeup-GPIO pin of the SAM EC. This IRQ can be used + * to wake the device from a low power state. + * + * Note that this IRQ can only be triggered while the EC is in the display-off + * state. In this state, events are not sent to the host in the usual way. + * Instead the wakeup-GPIO gets pulled to "high" as long as there are pending + * events and these events need to be released one-by-one via the GPIO + * callback request, either until there are no events left and the GPIO is + * reset, or all at once by transitioning the EC out of the display-off state, + * which will also clear the GPIO. + * + * Not all events, however, should trigger a full system wakeup. Instead the + * driver should, if necessary, inspect and forward each event to the + * corresponding subsystem, which in turn should decide if the system needs to + * be woken up. This logic has not been implemented yet, thus wakeup by this + * IRQ should be disabled by default to avoid spurious wake-ups, caused, for + * example, by the remaining battery percentage changing. Refer to comments in + * this function and comments in the corresponding IRQ handler for more + * details on how this should be implemented. + * + * See also ssam_ctrl_notif_display_off() and ssam_ctrl_notif_display_off() + * for functions to transition the EC into and out of the display-off state as + * well as more details on it. + * + * The IRQ is disabled by default and has to be enabled before it can wake up + * the device from suspend via ssam_irq_arm_for_wakeup(). On teardown, the IRQ + * should be freed via ssam_irq_free(). + */ +int ssam_irq_setup(struct ssam_controller *ctrl) +{ + struct device *dev = ssam_controller_device(ctrl); + struct gpio_desc *gpiod; + int irq; + int status; + + /* + * The actual GPIO interrupt is declared in ACPI as TRIGGER_HIGH. + * However, the GPIO line only gets reset by sending the GPIO callback + * command to SAM (or alternatively the display-on notification). As + * proper handling for this interrupt is not implemented yet, leaving + * the IRQ at TRIGGER_HIGH would cause an IRQ storm (as the callback + * never gets sent and thus the line never gets reset). To avoid this, + * mark the IRQ as TRIGGER_RISING for now, only creating a single + * interrupt, and let the SAM resume callback during the controller + * resume process clear it. + */ + const int irqf = IRQF_ONESHOT | IRQF_TRIGGER_RISING | IRQF_NO_AUTOEN; + + gpiod = gpiod_get(dev, "ssam_wakeup-int", GPIOD_ASIS); + if (IS_ERR(gpiod)) { + irq = fwnode_irq_get(dev_fwnode(dev), 0); + } else { + irq = gpiod_to_irq(gpiod); + gpiod_put(gpiod); + } + + if (irq < 0) + return irq; + + status = request_threaded_irq(irq, NULL, ssam_irq_handle, irqf, + "ssam_wakeup", ctrl); + if (status) + return status; + + ctrl->irq.num = irq; + return 0; +} + +/** + * ssam_irq_free() - Free SAM EC wakeup-GPIO interrupt. + * @ctrl: The controller for which the IRQ should be freed. + * + * Free the wakeup-GPIO IRQ previously set-up via ssam_irq_setup(). + */ +void ssam_irq_free(struct ssam_controller *ctrl) +{ + free_irq(ctrl->irq.num, ctrl); + ctrl->irq.num = -1; +} + +/** + * ssam_irq_arm_for_wakeup() - Arm the EC IRQ for wakeup, if enabled. + * @ctrl: The controller for which the IRQ should be armed. + * + * Sets up the IRQ so that it can be used to wake the device. Specifically, + * this function enables the irq and then, if the device is allowed to wake up + * the system, calls enable_irq_wake(). See ssam_irq_disarm_wakeup() for the + * corresponding function to disable the IRQ. + * + * This function is intended to arm the IRQ before entering S2idle suspend. + * + * Note: calls to ssam_irq_arm_for_wakeup() and ssam_irq_disarm_wakeup() must + * be balanced. + */ +int ssam_irq_arm_for_wakeup(struct ssam_controller *ctrl) +{ + struct device *dev = ssam_controller_device(ctrl); + int status; + + enable_irq(ctrl->irq.num); + if (device_may_wakeup(dev)) { + status = enable_irq_wake(ctrl->irq.num); + if (status) { + ssam_err(ctrl, "failed to enable wake IRQ: %d\n", status); + disable_irq(ctrl->irq.num); + return status; + } + + ctrl->irq.wakeup_enabled = true; + } else { + ctrl->irq.wakeup_enabled = false; + } + + return 0; +} + +/** + * ssam_irq_disarm_wakeup() - Disarm the wakeup IRQ. + * @ctrl: The controller for which the IRQ should be disarmed. + * + * Disarm the IRQ previously set up for wake via ssam_irq_arm_for_wakeup(). + * + * This function is intended to disarm the IRQ after exiting S2idle suspend. + * + * Note: calls to ssam_irq_arm_for_wakeup() and ssam_irq_disarm_wakeup() must + * be balanced. + */ +void ssam_irq_disarm_wakeup(struct ssam_controller *ctrl) +{ + int status; + + if (ctrl->irq.wakeup_enabled) { + status = disable_irq_wake(ctrl->irq.num); + if (status) + ssam_err(ctrl, "failed to disable wake IRQ: %d\n", status); + + ctrl->irq.wakeup_enabled = false; + } + disable_irq(ctrl->irq.num); +} diff --git a/drivers/platform/surface/aggregator/controller.h b/drivers/platform/surface/aggregator/controller.h new file mode 100644 index 000000000..f1638c208 --- /dev/null +++ b/drivers/platform/surface/aggregator/controller.h @@ -0,0 +1,285 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Main SSAM/SSH controller structure and functionality. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_CONTROLLER_H +#define _SURFACE_AGGREGATOR_CONTROLLER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ssh_request_layer.h" + + +/* -- Safe counters. -------------------------------------------------------- */ + +/** + * struct ssh_seq_counter - Safe counter for SSH sequence IDs. + * @value: The current counter value. + */ +struct ssh_seq_counter { + u8 value; +}; + +/** + * struct ssh_rqid_counter - Safe counter for SSH request IDs. + * @value: The current counter value. + */ +struct ssh_rqid_counter { + u16 value; +}; + + +/* -- Event/notification system. -------------------------------------------- */ + +/** + * struct ssam_nf_head - Notifier head for SSAM events. + * @srcu: The SRCU struct for synchronization. + * @head: List-head for notifier blocks registered under this head. + */ +struct ssam_nf_head { + struct srcu_struct srcu; + struct list_head head; +}; + +/** + * struct ssam_nf - Notifier callback- and activation-registry for SSAM events. + * @lock: Lock guarding (de-)registration of notifier blocks. Note: This + * lock does not need to be held for notifier calls, only + * registration and deregistration. + * @refcount: The root of the RB-tree used for reference-counting enabled + * events/notifications. + * @head: The list of notifier heads for event/notification callbacks. + */ +struct ssam_nf { + struct mutex lock; + struct rb_root refcount; + struct ssam_nf_head head[SSH_NUM_EVENTS]; +}; + + +/* -- Event/async request completion system. -------------------------------- */ + +struct ssam_cplt; + +/** + * struct ssam_event_item - Struct for event queuing and completion. + * @node: The node in the queue. + * @rqid: The request ID of the event. + * @ops: Instance specific functions. + * @ops.free: Callback for freeing this event item. + * @event: Actual event data. + */ +struct ssam_event_item { + struct list_head node; + u16 rqid; + + struct { + void (*free)(struct ssam_event_item *event); + } ops; + + struct ssam_event event; /* must be last */ +}; + +/** + * struct ssam_event_queue - Queue for completing received events. + * @cplt: Reference to the completion system on which this queue is active. + * @lock: The lock for any operation on the queue. + * @head: The list-head of the queue. + * @work: The &struct work_struct performing completion work for this queue. + */ +struct ssam_event_queue { + struct ssam_cplt *cplt; + + spinlock_t lock; + struct list_head head; + struct work_struct work; +}; + +/** + * struct ssam_event_target - Set of queues for a single SSH target ID. + * @queue: The array of queues, one queue per event ID. + */ +struct ssam_event_target { + struct ssam_event_queue queue[SSH_NUM_EVENTS]; +}; + +/** + * struct ssam_cplt - SSAM event/async request completion system. + * @dev: The device with which this system is associated. Only used + * for logging. + * @wq: The &struct workqueue_struct on which all completion work + * items are queued. + * @event: Event completion management. + * @event.target: Array of &struct ssam_event_target, one for each target. + * @event.notif: Notifier callbacks and event activation reference counting. + */ +struct ssam_cplt { + struct device *dev; + struct workqueue_struct *wq; + + struct { + struct ssam_event_target target[SSH_NUM_TARGETS]; + struct ssam_nf notif; + } event; +}; + + +/* -- Main SSAM device structures. ------------------------------------------ */ + +/** + * enum ssam_controller_state - State values for &struct ssam_controller. + * @SSAM_CONTROLLER_UNINITIALIZED: + * The controller has not been initialized yet or has been deinitialized. + * @SSAM_CONTROLLER_INITIALIZED: + * The controller is initialized, but has not been started yet. + * @SSAM_CONTROLLER_STARTED: + * The controller has been started and is ready to use. + * @SSAM_CONTROLLER_STOPPED: + * The controller has been stopped. + * @SSAM_CONTROLLER_SUSPENDED: + * The controller has been suspended. + */ +enum ssam_controller_state { + SSAM_CONTROLLER_UNINITIALIZED, + SSAM_CONTROLLER_INITIALIZED, + SSAM_CONTROLLER_STARTED, + SSAM_CONTROLLER_STOPPED, + SSAM_CONTROLLER_SUSPENDED, +}; + +/** + * struct ssam_controller_caps - Controller device capabilities. + * @ssh_power_profile: SSH power profile. + * @ssh_buffer_size: SSH driver UART buffer size. + * @screen_on_sleep_idle_timeout: SAM UART screen-on sleep idle timeout. + * @screen_off_sleep_idle_timeout: SAM UART screen-off sleep idle timeout. + * @d3_closes_handle: SAM closes UART handle in D3. + * + * Controller and SSH device capabilities found in ACPI. + */ +struct ssam_controller_caps { + u32 ssh_power_profile; + u32 ssh_buffer_size; + u32 screen_on_sleep_idle_timeout; + u32 screen_off_sleep_idle_timeout; + u32 d3_closes_handle:1; +}; + +/** + * struct ssam_controller - SSAM controller device. + * @kref: Reference count of the controller. + * @lock: Main lock for the controller, used to guard state changes. + * @state: Controller state. + * @rtl: Request transport layer for SSH I/O. + * @cplt: Completion system for SSH/SSAM events and asynchronous requests. + * @counter: Safe SSH message ID counters. + * @counter.seq: Sequence ID counter. + * @counter.rqid: Request ID counter. + * @irq: Wakeup IRQ resources. + * @irq.num: The wakeup IRQ number. + * @irq.wakeup_enabled: Whether wakeup by IRQ is enabled during suspend. + * @caps: The controller device capabilities. + */ +struct ssam_controller { + struct kref kref; + + struct rw_semaphore lock; + enum ssam_controller_state state; + + struct ssh_rtl rtl; + struct ssam_cplt cplt; + + struct { + struct ssh_seq_counter seq; + struct ssh_rqid_counter rqid; + } counter; + + struct { + int num; + bool wakeup_enabled; + } irq; + + struct ssam_controller_caps caps; +}; + +#define to_ssam_controller(ptr, member) \ + container_of(ptr, struct ssam_controller, member) + +#define ssam_dbg(ctrl, fmt, ...) rtl_dbg(&(ctrl)->rtl, fmt, ##__VA_ARGS__) +#define ssam_info(ctrl, fmt, ...) rtl_info(&(ctrl)->rtl, fmt, ##__VA_ARGS__) +#define ssam_warn(ctrl, fmt, ...) rtl_warn(&(ctrl)->rtl, fmt, ##__VA_ARGS__) +#define ssam_err(ctrl, fmt, ...) rtl_err(&(ctrl)->rtl, fmt, ##__VA_ARGS__) + +/** + * ssam_controller_receive_buf() - Provide input-data to the controller. + * @ctrl: The controller. + * @buf: The input buffer. + * @n: The number of bytes in the input buffer. + * + * Provide input data to be evaluated by the controller, which has been + * received via the lower-level transport. + * + * Return: Returns the number of bytes consumed, or, if the packet transport + * layer of the controller has been shut down, %-ESHUTDOWN. + */ +static inline +ssize_t ssam_controller_receive_buf(struct ssam_controller *ctrl, const u8 *buf, + size_t n) +{ + return ssh_ptl_rx_rcvbuf(&ctrl->rtl.ptl, buf, n); +} + +/** + * ssam_controller_write_wakeup() - Notify the controller that the underlying + * device has space available for data to be written. + * @ctrl: The controller. + */ +static inline void ssam_controller_write_wakeup(struct ssam_controller *ctrl) +{ + ssh_ptl_tx_wakeup_transfer(&ctrl->rtl.ptl); +} + +int ssam_controller_init(struct ssam_controller *ctrl, struct serdev_device *s); +int ssam_controller_start(struct ssam_controller *ctrl); +void ssam_controller_shutdown(struct ssam_controller *ctrl); +void ssam_controller_destroy(struct ssam_controller *ctrl); + +int ssam_notifier_disable_registered(struct ssam_controller *ctrl); +void ssam_notifier_restore_registered(struct ssam_controller *ctrl); + +int ssam_irq_setup(struct ssam_controller *ctrl); +void ssam_irq_free(struct ssam_controller *ctrl); +int ssam_irq_arm_for_wakeup(struct ssam_controller *ctrl); +void ssam_irq_disarm_wakeup(struct ssam_controller *ctrl); + +void ssam_controller_lock(struct ssam_controller *c); +void ssam_controller_unlock(struct ssam_controller *c); + +int ssam_get_firmware_version(struct ssam_controller *ctrl, u32 *version); +int ssam_ctrl_notif_display_off(struct ssam_controller *ctrl); +int ssam_ctrl_notif_display_on(struct ssam_controller *ctrl); +int ssam_ctrl_notif_d0_exit(struct ssam_controller *ctrl); +int ssam_ctrl_notif_d0_entry(struct ssam_controller *ctrl); + +int ssam_controller_suspend(struct ssam_controller *ctrl); +int ssam_controller_resume(struct ssam_controller *ctrl); + +int ssam_event_item_cache_init(void); +void ssam_event_item_cache_destroy(void); + +#endif /* _SURFACE_AGGREGATOR_CONTROLLER_H */ diff --git a/drivers/platform/surface/aggregator/core.c b/drivers/platform/surface/aggregator/core.c new file mode 100644 index 000000000..43d5988fb --- /dev/null +++ b/drivers/platform/surface/aggregator/core.c @@ -0,0 +1,894 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Serial Hub (SSH) driver for communication with the Surface/System + * Aggregator Module (SSAM/SAM). + * + * Provides access to a SAM-over-SSH connected EC via a controller device. + * Handles communication via requests as well as enabling, disabling, and + * relaying of events. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "bus.h" +#include "controller.h" + +#define CREATE_TRACE_POINTS +#include "trace.h" + + +/* -- Static controller reference. ------------------------------------------ */ + +/* + * Main controller reference. The corresponding lock must be held while + * accessing (reading/writing) the reference. + */ +static struct ssam_controller *__ssam_controller; +static DEFINE_SPINLOCK(__ssam_controller_lock); + +/** + * ssam_get_controller() - Get reference to SSAM controller. + * + * Returns a reference to the SSAM controller of the system or %NULL if there + * is none, it hasn't been set up yet, or it has already been unregistered. + * This function automatically increments the reference count of the + * controller, thus the calling party must ensure that ssam_controller_put() + * is called when it doesn't need the controller any more. + */ +struct ssam_controller *ssam_get_controller(void) +{ + struct ssam_controller *ctrl; + + spin_lock(&__ssam_controller_lock); + + ctrl = __ssam_controller; + if (!ctrl) + goto out; + + if (WARN_ON(!kref_get_unless_zero(&ctrl->kref))) + ctrl = NULL; + +out: + spin_unlock(&__ssam_controller_lock); + return ctrl; +} +EXPORT_SYMBOL_GPL(ssam_get_controller); + +/** + * ssam_try_set_controller() - Try to set the main controller reference. + * @ctrl: The controller to which the reference should point. + * + * Set the main controller reference to the given pointer if the reference + * hasn't been set already. + * + * Return: Returns zero on success or %-EEXIST if the reference has already + * been set. + */ +static int ssam_try_set_controller(struct ssam_controller *ctrl) +{ + int status = 0; + + spin_lock(&__ssam_controller_lock); + if (!__ssam_controller) + __ssam_controller = ctrl; + else + status = -EEXIST; + spin_unlock(&__ssam_controller_lock); + + return status; +} + +/** + * ssam_clear_controller() - Remove/clear the main controller reference. + * + * Clears the main controller reference, i.e. sets it to %NULL. This function + * should be called before the controller is shut down. + */ +static void ssam_clear_controller(void) +{ + spin_lock(&__ssam_controller_lock); + __ssam_controller = NULL; + spin_unlock(&__ssam_controller_lock); +} + +/** + * ssam_client_link() - Link an arbitrary client device to the controller. + * @c: The controller to link to. + * @client: The client device. + * + * Link an arbitrary client device to the controller by creating a device link + * between it as consumer and the controller device as provider. This function + * can be used for non-SSAM devices (or SSAM devices not registered as child + * under the controller) to guarantee that the controller is valid for as long + * as the driver of the client device is bound, and that proper suspend and + * resume ordering is guaranteed. + * + * The device link does not have to be destructed manually. It is removed + * automatically once the driver of the client device unbinds. + * + * Return: Returns zero on success, %-ENODEV if the controller is not ready or + * going to be removed soon, or %-ENOMEM if the device link could not be + * created for other reasons. + */ +int ssam_client_link(struct ssam_controller *c, struct device *client) +{ + const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_CONSUMER; + struct device_link *link; + struct device *ctrldev; + + ssam_controller_statelock(c); + + if (c->state != SSAM_CONTROLLER_STARTED) { + ssam_controller_stateunlock(c); + return -ENODEV; + } + + ctrldev = ssam_controller_device(c); + if (!ctrldev) { + ssam_controller_stateunlock(c); + return -ENODEV; + } + + link = device_link_add(client, ctrldev, flags); + if (!link) { + ssam_controller_stateunlock(c); + return -ENOMEM; + } + + /* + * Return -ENODEV if supplier driver is on its way to be removed. In + * this case, the controller won't be around for much longer and the + * device link is not going to save us any more, as unbinding is + * already in progress. + */ + if (READ_ONCE(link->status) == DL_STATE_SUPPLIER_UNBIND) { + ssam_controller_stateunlock(c); + return -ENODEV; + } + + ssam_controller_stateunlock(c); + return 0; +} +EXPORT_SYMBOL_GPL(ssam_client_link); + +/** + * ssam_client_bind() - Bind an arbitrary client device to the controller. + * @client: The client device. + * + * Link an arbitrary client device to the controller by creating a device link + * between it as consumer and the main controller device as provider. This + * function can be used for non-SSAM devices to guarantee that the controller + * returned by this function is valid for as long as the driver of the client + * device is bound, and that proper suspend and resume ordering is guaranteed. + * + * This function does essentially the same as ssam_client_link(), except that + * it first fetches the main controller reference, then creates the link, and + * finally returns this reference. Note that this function does not increment + * the reference counter of the controller, as, due to the link, the + * controller lifetime is assured as long as the driver of the client device + * is bound. + * + * It is not valid to use the controller reference obtained by this method + * outside of the driver bound to the client device at the time of calling + * this function, without first incrementing the reference count of the + * controller via ssam_controller_get(). Even after doing this, care must be + * taken that requests are only submitted and notifiers are only + * (un-)registered when the controller is active and not suspended. In other + * words: The device link only lives as long as the client driver is bound and + * any guarantees enforced by this link (e.g. active controller state) can + * only be relied upon as long as this link exists and may need to be enforced + * in other ways afterwards. + * + * The created device link does not have to be destructed manually. It is + * removed automatically once the driver of the client device unbinds. + * + * Return: Returns the controller on success, an error pointer with %-ENODEV + * if the controller is not present, not ready or going to be removed soon, or + * %-ENOMEM if the device link could not be created for other reasons. + */ +struct ssam_controller *ssam_client_bind(struct device *client) +{ + struct ssam_controller *c; + int status; + + c = ssam_get_controller(); + if (!c) + return ERR_PTR(-ENODEV); + + status = ssam_client_link(c, client); + + /* + * Note that we can drop our controller reference in both success and + * failure cases: On success, we have bound the controller lifetime + * inherently to the client driver lifetime, i.e. it the controller is + * now guaranteed to outlive the client driver. On failure, we're not + * going to use the controller any more. + */ + ssam_controller_put(c); + + return status >= 0 ? c : ERR_PTR(status); +} +EXPORT_SYMBOL_GPL(ssam_client_bind); + + +/* -- Glue layer (serdev_device -> ssam_controller). ------------------------ */ + +static size_t ssam_receive_buf(struct serdev_device *dev, const u8 *buf, + size_t n) +{ + struct ssam_controller *ctrl; + int ret; + + ctrl = serdev_device_get_drvdata(dev); + ret = ssam_controller_receive_buf(ctrl, buf, n); + + return ret < 0 ? 0 : ret; +} + +static void ssam_write_wakeup(struct serdev_device *dev) +{ + ssam_controller_write_wakeup(serdev_device_get_drvdata(dev)); +} + +static const struct serdev_device_ops ssam_serdev_ops = { + .receive_buf = ssam_receive_buf, + .write_wakeup = ssam_write_wakeup, +}; + + +/* -- SysFS and misc. ------------------------------------------------------- */ + +static int ssam_log_firmware_version(struct ssam_controller *ctrl) +{ + u32 version, a, b, c; + int status; + + status = ssam_get_firmware_version(ctrl, &version); + if (status) + return status; + + a = (version >> 24) & 0xff; + b = ((version >> 8) & 0xffff); + c = version & 0xff; + + ssam_info(ctrl, "SAM firmware version: %u.%u.%u\n", a, b, c); + return 0; +} + +static ssize_t firmware_version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ssam_controller *ctrl = dev_get_drvdata(dev); + u32 version, a, b, c; + int status; + + status = ssam_get_firmware_version(ctrl, &version); + if (status < 0) + return status; + + a = (version >> 24) & 0xff; + b = ((version >> 8) & 0xffff); + c = version & 0xff; + + return sysfs_emit(buf, "%u.%u.%u\n", a, b, c); +} +static DEVICE_ATTR_RO(firmware_version); + +static struct attribute *ssam_sam_attrs[] = { + &dev_attr_firmware_version.attr, + NULL +}; + +static const struct attribute_group ssam_sam_group = { + .name = "sam", + .attrs = ssam_sam_attrs, +}; + + +/* -- Serial device setup. -------------------------------------------------- */ + +static acpi_status ssam_serdev_setup_via_acpi_crs(struct acpi_resource *rsc, + void *ctx) +{ + struct serdev_device *serdev = ctx; + struct acpi_resource_uart_serialbus *uart; + bool flow_control; + int status = 0; + + if (!serdev_acpi_get_uart_resource(rsc, &uart)) + return AE_OK; + + /* Set up serdev device. */ + serdev_device_set_baudrate(serdev, uart->default_baud_rate); + + /* serdev currently only supports RTSCTS flow control. */ + if (uart->flow_control & (~((u8)ACPI_UART_FLOW_CONTROL_HW))) { + dev_warn(&serdev->dev, "setup: unsupported flow control (value: %#04x)\n", + uart->flow_control); + } + + /* Set RTSCTS flow control. */ + flow_control = uart->flow_control & ACPI_UART_FLOW_CONTROL_HW; + serdev_device_set_flow_control(serdev, flow_control); + + /* serdev currently only supports EVEN/ODD parity. */ + switch (uart->parity) { + case ACPI_UART_PARITY_NONE: + status = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); + break; + case ACPI_UART_PARITY_EVEN: + status = serdev_device_set_parity(serdev, SERDEV_PARITY_EVEN); + break; + case ACPI_UART_PARITY_ODD: + status = serdev_device_set_parity(serdev, SERDEV_PARITY_ODD); + break; + default: + dev_warn(&serdev->dev, "setup: unsupported parity (value: %#04x)\n", + uart->parity); + break; + } + + if (status) { + dev_err(&serdev->dev, "setup: failed to set parity (value: %#04x, error: %d)\n", + uart->parity, status); + return AE_ERROR; + } + + /* We've found the resource and are done. */ + return AE_CTRL_TERMINATE; +} + +static int ssam_serdev_setup_via_acpi(struct serdev_device *serdev, acpi_handle handle) +{ + acpi_status status; + + status = acpi_walk_resources(handle, METHOD_NAME__CRS, + ssam_serdev_setup_via_acpi_crs, serdev); + + return status ? -ENXIO : 0; +} + +static int ssam_serdev_setup(struct acpi_device *ssh, struct serdev_device *serdev) +{ + if (ssh) + return ssam_serdev_setup_via_acpi(serdev, ssh->handle); + + /* TODO: these values may differ per board/implementation */ + serdev_device_set_baudrate(serdev, 4 * HZ_PER_MHZ); + serdev_device_set_flow_control(serdev, true); + serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); + + return 0; +} + +/* -- Power management. ----------------------------------------------------- */ + +static void ssam_serial_hub_shutdown(struct serdev_device *serdev) +{ + struct ssam_controller *c = dev_get_drvdata(&serdev->dev); + int status; + + /* + * Try to disable notifiers, signal display-off and D0-exit, ignore any + * errors. + * + * Note: It has not been established yet if this is actually + * necessary/useful for shutdown. + */ + + status = ssam_notifier_disable_registered(c); + if (status) { + ssam_err(c, "pm: failed to disable notifiers for shutdown: %d\n", + status); + } + + status = ssam_ctrl_notif_display_off(c); + if (status) + ssam_err(c, "pm: display-off notification failed: %d\n", status); + + status = ssam_ctrl_notif_d0_exit(c); + if (status) + ssam_err(c, "pm: D0-exit notification failed: %d\n", status); +} + +#ifdef CONFIG_PM_SLEEP + +static int ssam_serial_hub_pm_prepare(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * Try to signal display-off, This will quiesce events. + * + * Note: Signaling display-off/display-on should normally be done from + * some sort of display state notifier. As that is not available, + * signal it here. + */ + + status = ssam_ctrl_notif_display_off(c); + if (status) + ssam_err(c, "pm: display-off notification failed: %d\n", status); + + return status; +} + +static void ssam_serial_hub_pm_complete(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * Try to signal display-on. This will restore events. + * + * Note: Signaling display-off/display-on should normally be done from + * some sort of display state notifier. As that is not available, + * signal it here. + */ + + status = ssam_ctrl_notif_display_on(c); + if (status) + ssam_err(c, "pm: display-on notification failed: %d\n", status); +} + +static int ssam_serial_hub_pm_suspend(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * Try to signal D0-exit, enable IRQ wakeup if specified. Abort on + * error. + */ + + status = ssam_ctrl_notif_d0_exit(c); + if (status) { + ssam_err(c, "pm: D0-exit notification failed: %d\n", status); + goto err_notif; + } + + status = ssam_irq_arm_for_wakeup(c); + if (status) + goto err_irq; + + WARN_ON(ssam_controller_suspend(c)); + return 0; + +err_irq: + ssam_ctrl_notif_d0_entry(c); +err_notif: + ssam_ctrl_notif_display_on(c); + return status; +} + +static int ssam_serial_hub_pm_resume(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + WARN_ON(ssam_controller_resume(c)); + + /* + * Try to disable IRQ wakeup (if specified) and signal D0-entry. In + * case of errors, log them and try to restore normal operation state + * as far as possible. + * + * Note: Signaling display-off/display-on should normally be done from + * some sort of display state notifier. As that is not available, + * signal it here. + */ + + ssam_irq_disarm_wakeup(c); + + status = ssam_ctrl_notif_d0_entry(c); + if (status) + ssam_err(c, "pm: D0-entry notification failed: %d\n", status); + + return 0; +} + +static int ssam_serial_hub_pm_freeze(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * During hibernation image creation, we only have to ensure that the + * EC doesn't send us any events. This is done via the display-off + * and D0-exit notifications. Note that this sets up the wakeup IRQ + * on the EC side, however, we have disabled it by default on our side + * and won't enable it here. + * + * See ssam_serial_hub_poweroff() for more details on the hibernation + * process. + */ + + status = ssam_ctrl_notif_d0_exit(c); + if (status) { + ssam_err(c, "pm: D0-exit notification failed: %d\n", status); + ssam_ctrl_notif_display_on(c); + return status; + } + + WARN_ON(ssam_controller_suspend(c)); + return 0; +} + +static int ssam_serial_hub_pm_thaw(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + WARN_ON(ssam_controller_resume(c)); + + status = ssam_ctrl_notif_d0_entry(c); + if (status) + ssam_err(c, "pm: D0-exit notification failed: %d\n", status); + + return status; +} + +static int ssam_serial_hub_pm_poweroff(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * When entering hibernation and powering off the system, the EC, at + * least on some models, may disable events. Without us taking care of + * that, this leads to events not being enabled/restored when the + * system resumes from hibernation, resulting SAM-HID subsystem devices + * (i.e. keyboard, touchpad) not working, AC-plug/AC-unplug events being + * gone, etc. + * + * To avoid these issues, we disable all registered events here (this is + * likely not actually required) and restore them during the drivers PM + * restore callback. + * + * Wakeup from the EC interrupt is not supported during hibernation, + * so don't arm the IRQ here. + */ + + status = ssam_notifier_disable_registered(c); + if (status) { + ssam_err(c, "pm: failed to disable notifiers for hibernation: %d\n", + status); + return status; + } + + status = ssam_ctrl_notif_d0_exit(c); + if (status) { + ssam_err(c, "pm: D0-exit notification failed: %d\n", status); + ssam_notifier_restore_registered(c); + return status; + } + + WARN_ON(ssam_controller_suspend(c)); + return 0; +} + +static int ssam_serial_hub_pm_restore(struct device *dev) +{ + struct ssam_controller *c = dev_get_drvdata(dev); + int status; + + /* + * Ignore but log errors, try to restore state as much as possible in + * case of failures. See ssam_serial_hub_poweroff() for more details on + * the hibernation process. + */ + + WARN_ON(ssam_controller_resume(c)); + + status = ssam_ctrl_notif_d0_entry(c); + if (status) + ssam_err(c, "pm: D0-entry notification failed: %d\n", status); + + ssam_notifier_restore_registered(c); + return 0; +} + +static const struct dev_pm_ops ssam_serial_hub_pm_ops = { + .prepare = ssam_serial_hub_pm_prepare, + .complete = ssam_serial_hub_pm_complete, + .suspend = ssam_serial_hub_pm_suspend, + .resume = ssam_serial_hub_pm_resume, + .freeze = ssam_serial_hub_pm_freeze, + .thaw = ssam_serial_hub_pm_thaw, + .poweroff = ssam_serial_hub_pm_poweroff, + .restore = ssam_serial_hub_pm_restore, +}; + +#else /* CONFIG_PM_SLEEP */ + +static const struct dev_pm_ops ssam_serial_hub_pm_ops = { }; + +#endif /* CONFIG_PM_SLEEP */ + + +/* -- Device/driver setup. -------------------------------------------------- */ + +static const struct acpi_gpio_params gpio_ssam_wakeup_int = { 0, 0, false }; +static const struct acpi_gpio_params gpio_ssam_wakeup = { 1, 0, false }; + +static const struct acpi_gpio_mapping ssam_acpi_gpios[] = { + { "ssam_wakeup-int-gpio", &gpio_ssam_wakeup_int, 1 }, + { "ssam_wakeup-gpio", &gpio_ssam_wakeup, 1 }, + { }, +}; + +static int ssam_serial_hub_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + struct acpi_device *ssh = ACPI_COMPANION(dev); + struct ssam_controller *ctrl; + int status; + + if (ssh) { + status = gpiod_count(dev, NULL); + if (status < 0) + return dev_err_probe(dev, status, "no GPIO found\n"); + + status = devm_acpi_dev_add_driver_gpios(dev, ssam_acpi_gpios); + if (status) + return status; + } + + /* Allocate controller. */ + ctrl = kzalloc_obj(*ctrl); + if (!ctrl) + return -ENOMEM; + + /* Initialize controller. */ + status = ssam_controller_init(ctrl, serdev); + if (status) { + dev_err_probe(dev, status, "failed to initialize ssam controller\n"); + goto err_ctrl_init; + } + + ssam_controller_lock(ctrl); + + /* Set up serdev device. */ + serdev_device_set_drvdata(serdev, ctrl); + serdev_device_set_client_ops(serdev, &ssam_serdev_ops); + status = serdev_device_open(serdev); + if (status) { + dev_err_probe(dev, status, "failed to open serdev device\n"); + goto err_devopen; + } + + status = ssam_serdev_setup(ssh, serdev); + if (status) { + dev_err_probe(dev, status, "failed to setup serdev\n"); + goto err_devinit; + } + + /* Start controller. */ + status = ssam_controller_start(ctrl); + if (status) + goto err_devinit; + + ssam_controller_unlock(ctrl); + + /* + * Initial SAM requests: Log version and notify default/init power + * states. + */ + status = ssam_log_firmware_version(ctrl); + if (status) { + dev_err_probe(dev, status, "failed to get firmware version\n"); + goto err_initrq; + } + + status = ssam_ctrl_notif_d0_entry(ctrl); + if (status) { + dev_err_probe(dev, status, "D0-entry notification failed\n"); + goto err_initrq; + } + + status = ssam_ctrl_notif_display_on(ctrl); + if (status) { + dev_err_probe(dev, status, "display-on notification failed\n"); + goto err_initrq; + } + + status = sysfs_create_group(&dev->kobj, &ssam_sam_group); + if (status) + goto err_initrq; + + /* Set up IRQ. */ + status = ssam_irq_setup(ctrl); + if (status) { + dev_err_probe(dev, status, "failed to setup IRQ\n"); + goto err_irq; + } + + /* Finally, set main controller reference. */ + status = ssam_try_set_controller(ctrl); + if (WARN_ON(status)) /* Currently, we're the only provider. */ + goto err_mainref; + + /* + * TODO: The EC can wake up the system via the associated GPIO interrupt + * in multiple situations. One of which is the remaining battery + * capacity falling below a certain threshold. Normally, we should + * use the device_init_wakeup function, however, the EC also seems + * to have other reasons for waking up the system and it seems + * that Windows has additional checks whether the system should be + * resumed. In short, this causes some spurious unwanted wake-ups. + * For now let's thus default power/wakeup to false. + */ + device_set_wakeup_capable(dev, true); + + /* + * When using DT, we have to register the platform hub driver manually, + * as it can't be matched based on top-level board compatible (like it + * does the ACPI case). + */ + if (!ssh) { + struct platform_device *ph_pdev = + platform_device_register_simple("surface_aggregator_platform_hub", + 0, NULL, 0); + if (IS_ERR(ph_pdev)) + return dev_err_probe(dev, PTR_ERR(ph_pdev), + "Failed to register the platform hub driver\n"); + } + + if (ssh) + acpi_dev_clear_dependencies(ssh); + + return 0; + +err_mainref: + ssam_irq_free(ctrl); +err_irq: + sysfs_remove_group(&dev->kobj, &ssam_sam_group); +err_initrq: + ssam_controller_lock(ctrl); + ssam_controller_shutdown(ctrl); +err_devinit: + serdev_device_close(serdev); +err_devopen: + ssam_controller_destroy(ctrl); + ssam_controller_unlock(ctrl); +err_ctrl_init: + kfree(ctrl); + return status; +} + +static void ssam_serial_hub_remove(struct serdev_device *serdev) +{ + struct ssam_controller *ctrl = serdev_device_get_drvdata(serdev); + int status; + + /* Clear static reference so that no one else can get a new one. */ + ssam_clear_controller(); + + /* Disable and free IRQ. */ + ssam_irq_free(ctrl); + + sysfs_remove_group(&serdev->dev.kobj, &ssam_sam_group); + ssam_controller_lock(ctrl); + + /* Remove all client devices. */ + ssam_remove_clients(&serdev->dev); + + /* Act as if suspending to silence events. */ + status = ssam_ctrl_notif_display_off(ctrl); + if (status) { + dev_err(&serdev->dev, "display-off notification failed: %d\n", + status); + } + + status = ssam_ctrl_notif_d0_exit(ctrl); + if (status) { + dev_err(&serdev->dev, "D0-exit notification failed: %d\n", + status); + } + + /* Shut down controller and remove serdev device reference from it. */ + ssam_controller_shutdown(ctrl); + + /* Shut down actual transport. */ + serdev_device_wait_until_sent(serdev, 0); + serdev_device_close(serdev); + + /* Drop our controller reference. */ + ssam_controller_unlock(ctrl); + ssam_controller_put(ctrl); + + device_set_wakeup_capable(&serdev->dev, false); +} + +static const struct acpi_device_id ssam_serial_hub_acpi_match[] = { + { "MSHW0084", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, ssam_serial_hub_acpi_match); + +#ifdef CONFIG_OF +static const struct of_device_id ssam_serial_hub_of_match[] = { + { .compatible = "microsoft,surface-sam", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ssam_serial_hub_of_match); +#endif + +static struct serdev_device_driver ssam_serial_hub = { + .probe = ssam_serial_hub_probe, + .remove = ssam_serial_hub_remove, + .shutdown = ssam_serial_hub_shutdown, + .driver = { + .name = "surface_serial_hub", + .acpi_match_table = ACPI_PTR(ssam_serial_hub_acpi_match), + .of_match_table = of_match_ptr(ssam_serial_hub_of_match), + .pm = &ssam_serial_hub_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + + +/* -- Module setup. --------------------------------------------------------- */ + +static int __init ssam_core_init(void) +{ + int status; + + status = ssam_bus_register(); + if (status) + goto err_bus; + + status = ssh_ctrl_packet_cache_init(); + if (status) + goto err_cpkg; + + status = ssam_event_item_cache_init(); + if (status) + goto err_evitem; + + status = serdev_device_driver_register(&ssam_serial_hub); + if (status) + goto err_register; + + return 0; + +err_register: + ssam_event_item_cache_destroy(); +err_evitem: + ssh_ctrl_packet_cache_destroy(); +err_cpkg: + ssam_bus_unregister(); +err_bus: + return status; +} +subsys_initcall(ssam_core_init); + +static void __exit ssam_core_exit(void) +{ + serdev_device_driver_unregister(&ssam_serial_hub); + ssam_event_item_cache_destroy(); + ssh_ctrl_packet_cache_destroy(); + ssam_bus_unregister(); +} +module_exit(ssam_core_exit); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Subsystem and Surface Serial Hub driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/aggregator/ssh_msgb.h b/drivers/platform/surface/aggregator/ssh_msgb.h new file mode 100644 index 000000000..80aa568a0 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_msgb.h @@ -0,0 +1,205 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * SSH message builder functions. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_SSH_MSGB_H +#define _SURFACE_AGGREGATOR_SSH_MSGB_H + +#include +#include + +#include +#include + +/** + * struct msgbuf - Buffer struct to construct SSH messages. + * @begin: Pointer to the beginning of the allocated buffer space. + * @end: Pointer to the end (one past last element) of the allocated buffer + * space. + * @ptr: Pointer to the first free element in the buffer. + */ +struct msgbuf { + u8 *begin; + u8 *end; + u8 *ptr; +}; + +/** + * msgb_init() - Initialize the given message buffer struct. + * @msgb: The buffer struct to initialize + * @ptr: Pointer to the underlying memory by which the buffer will be backed. + * @cap: Size of the underlying memory. + * + * Initialize the given message buffer struct using the provided memory as + * backing. + */ +static inline void msgb_init(struct msgbuf *msgb, u8 *ptr, size_t cap) +{ + msgb->begin = ptr; + msgb->end = ptr + cap; + msgb->ptr = ptr; +} + +/** + * msgb_bytes_used() - Return the current number of bytes used in the buffer. + * @msgb: The message buffer. + */ +static inline size_t msgb_bytes_used(const struct msgbuf *msgb) +{ + return msgb->ptr - msgb->begin; +} + +static inline void __msgb_push_u8(struct msgbuf *msgb, u8 value) +{ + *msgb->ptr = value; + msgb->ptr += sizeof(u8); +} + +static inline void __msgb_push_u16(struct msgbuf *msgb, u16 value) +{ + put_unaligned_le16(value, msgb->ptr); + msgb->ptr += sizeof(u16); +} + +/** + * msgb_push_u16() - Push a u16 value to the buffer. + * @msgb: The message buffer. + * @value: The value to push to the buffer. + */ +static inline void msgb_push_u16(struct msgbuf *msgb, u16 value) +{ + if (WARN_ON(msgb->ptr + sizeof(u16) > msgb->end)) + return; + + __msgb_push_u16(msgb, value); +} + +/** + * msgb_push_syn() - Push SSH SYN bytes to the buffer. + * @msgb: The message buffer. + */ +static inline void msgb_push_syn(struct msgbuf *msgb) +{ + msgb_push_u16(msgb, SSH_MSG_SYN); +} + +/** + * msgb_push_buf() - Push raw data to the buffer. + * @msgb: The message buffer. + * @buf: The data to push to the buffer. + * @len: The length of the data to push to the buffer. + */ +static inline void msgb_push_buf(struct msgbuf *msgb, const u8 *buf, size_t len) +{ + msgb->ptr = memcpy(msgb->ptr, buf, len) + len; +} + +/** + * msgb_push_crc() - Compute CRC and push it to the buffer. + * @msgb: The message buffer. + * @buf: The data for which the CRC should be computed. + * @len: The length of the data for which the CRC should be computed. + */ +static inline void msgb_push_crc(struct msgbuf *msgb, const u8 *buf, size_t len) +{ + msgb_push_u16(msgb, ssh_crc(buf, len)); +} + +/** + * msgb_push_frame() - Push a SSH message frame header to the buffer. + * @msgb: The message buffer + * @ty: The type of the frame. + * @len: The length of the payload of the frame. + * @seq: The sequence ID of the frame/packet. + */ +static inline void msgb_push_frame(struct msgbuf *msgb, u8 ty, u16 len, u8 seq) +{ + u8 *const begin = msgb->ptr; + + if (WARN_ON(msgb->ptr + sizeof(struct ssh_frame) > msgb->end)) + return; + + __msgb_push_u8(msgb, ty); /* Frame type. */ + __msgb_push_u16(msgb, len); /* Frame payload length. */ + __msgb_push_u8(msgb, seq); /* Frame sequence ID. */ + + msgb_push_crc(msgb, begin, msgb->ptr - begin); +} + +/** + * msgb_push_ack() - Push a SSH ACK frame to the buffer. + * @msgb: The message buffer + * @seq: The sequence ID of the frame/packet to be ACKed. + */ +static inline void msgb_push_ack(struct msgbuf *msgb, u8 seq) +{ + /* SYN. */ + msgb_push_syn(msgb); + + /* ACK-type frame + CRC. */ + msgb_push_frame(msgb, SSH_FRAME_TYPE_ACK, 0x00, seq); + + /* Payload CRC (ACK-type frames do not have a payload). */ + msgb_push_crc(msgb, msgb->ptr, 0); +} + +/** + * msgb_push_nak() - Push a SSH NAK frame to the buffer. + * @msgb: The message buffer + */ +static inline void msgb_push_nak(struct msgbuf *msgb) +{ + /* SYN. */ + msgb_push_syn(msgb); + + /* NAK-type frame + CRC. */ + msgb_push_frame(msgb, SSH_FRAME_TYPE_NAK, 0x00, 0x00); + + /* Payload CRC (ACK-type frames do not have a payload). */ + msgb_push_crc(msgb, msgb->ptr, 0); +} + +/** + * msgb_push_cmd() - Push a SSH command frame with payload to the buffer. + * @msgb: The message buffer. + * @seq: The sequence ID (SEQ) of the frame/packet. + * @rqid: The request ID (RQID) of the request contained in the frame. + * @rqst: The request to wrap in the frame. + */ +static inline void msgb_push_cmd(struct msgbuf *msgb, u8 seq, u16 rqid, + const struct ssam_request *rqst) +{ + const u8 type = SSH_FRAME_TYPE_DATA_SEQ; + u8 *cmd; + + /* SYN. */ + msgb_push_syn(msgb); + + /* Command frame + CRC. */ + msgb_push_frame(msgb, type, sizeof(struct ssh_command) + rqst->length, seq); + + /* Frame payload: Command struct + payload. */ + if (WARN_ON(msgb->ptr + sizeof(struct ssh_command) > msgb->end)) + return; + + cmd = msgb->ptr; + + __msgb_push_u8(msgb, SSH_PLD_TYPE_CMD); /* Payload type. */ + __msgb_push_u8(msgb, rqst->target_category); /* Target category. */ + __msgb_push_u8(msgb, rqst->target_id); /* Target ID. */ + __msgb_push_u8(msgb, SSAM_SSH_TID_HOST); /* Source ID. */ + __msgb_push_u8(msgb, rqst->instance_id); /* Instance ID. */ + __msgb_push_u16(msgb, rqid); /* Request ID. */ + __msgb_push_u8(msgb, rqst->command_id); /* Command ID. */ + + /* Command payload. */ + msgb_push_buf(msgb, rqst->payload, rqst->length); + + /* CRC for command struct + payload. */ + msgb_push_crc(msgb, cmd, msgb->ptr - cmd); +} + +#endif /* _SURFACE_AGGREGATOR_SSH_MSGB_H */ diff --git a/drivers/platform/surface/aggregator/ssh_packet_layer.c b/drivers/platform/surface/aggregator/ssh_packet_layer.c new file mode 100644 index 000000000..3dd228565 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_packet_layer.c @@ -0,0 +1,2086 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * SSH packet transport layer. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ssh_msgb.h" +#include "ssh_packet_layer.h" +#include "ssh_parser.h" + +#include "trace.h" + +/* + * To simplify reasoning about the code below, we define a few concepts. The + * system below is similar to a state-machine for packets, however, there are + * too many states to explicitly write them down. To (somewhat) manage the + * states and packets we rely on flags, reference counting, and some simple + * concepts. State transitions are triggered by actions. + * + * >> Actions << + * + * - submit + * - transmission start (process next item in queue) + * - transmission finished (guaranteed to never be parallel to transmission + * start) + * - ACK received + * - NAK received (this is equivalent to issuing re-submit for all pending + * packets) + * - timeout (this is equivalent to re-issuing a submit or canceling) + * - cancel (non-pending and pending) + * + * >> Data Structures, Packet Ownership, General Overview << + * + * The code below employs two main data structures: The packet queue, + * containing all packets scheduled for transmission, and the set of pending + * packets, containing all packets awaiting an ACK. + * + * Shared ownership of a packet is controlled via reference counting. Inside + * the transport system are a total of five packet owners: + * + * - the packet queue, + * - the pending set, + * - the transmitter thread, + * - the receiver thread (via ACKing), and + * - the timeout work item. + * + * Normal operation is as follows: The initial reference of the packet is + * obtained by submitting the packet and queuing it. The receiver thread takes + * packets from the queue. By doing this, it does not increment the refcount + * but takes over the reference (removing it from the queue). If the packet is + * sequenced (i.e. needs to be ACKed by the client), the transmitter thread + * sets-up the timeout and adds the packet to the pending set before starting + * to transmit it. As the timeout is handled by a reaper task, no additional + * reference for it is needed. After the transmit is done, the reference held + * by the transmitter thread is dropped. If the packet is unsequenced (i.e. + * does not need an ACK), the packet is completed by the transmitter thread + * before dropping that reference. + * + * On receival of an ACK, the receiver thread removes and obtains the + * reference to the packet from the pending set. The receiver thread will then + * complete the packet and drop its reference. + * + * On receival of a NAK, the receiver thread re-submits all currently pending + * packets. + * + * Packet timeouts are detected by the timeout reaper. This is a task, + * scheduled depending on the earliest packet timeout expiration date, + * checking all currently pending packets if their timeout has expired. If the + * timeout of a packet has expired, it is re-submitted and the number of tries + * of this packet is incremented. If this number reaches its limit, the packet + * will be completed with a failure. + * + * On transmission failure (such as repeated packet timeouts), the completion + * callback is immediately run by on thread on which the error was detected. + * + * To ensure that a packet eventually leaves the system it is marked as + * "locked" directly before it is going to be completed or when it is + * canceled. Marking a packet as "locked" has the effect that passing and + * creating new references of the packet is disallowed. This means that the + * packet cannot be added to the queue, the pending set, and the timeout, or + * be picked up by the transmitter thread or receiver thread. To remove a + * packet from the system it has to be marked as locked and subsequently all + * references from the data structures (queue, pending) have to be removed. + * References held by threads will eventually be dropped automatically as + * their execution progresses. + * + * Note that the packet completion callback is, in case of success and for a + * sequenced packet, guaranteed to run on the receiver thread, thus providing + * a way to reliably identify responses to the packet. The packet completion + * callback is only run once and it does not indicate that the packet has + * fully left the system (for this, one should rely on the release method, + * triggered when the reference count of the packet reaches zero). In case of + * re-submission (and with somewhat unlikely timing), it may be possible that + * the packet is being re-transmitted while the completion callback runs. + * Completion will occur both on success and internal error, as well as when + * the packet is canceled. + * + * >> Flags << + * + * Flags are used to indicate the state and progression of a packet. Some flags + * have stricter guarantees than other: + * + * - locked + * Indicates if the packet is locked. If the packet is locked, passing and/or + * creating additional references to the packet is forbidden. The packet thus + * may not be queued, dequeued, or removed or added to the pending set. Note + * that the packet state flags may still change (e.g. it may be marked as + * ACKed, transmitted, ...). + * + * - completed + * Indicates if the packet completion callback has been executed or is about + * to be executed. This flag is used to ensure that the packet completion + * callback is only run once. + * + * - queued + * Indicates if a packet is present in the submission queue or not. This flag + * must only be modified with the queue lock held, and must be coherent to the + * presence of the packet in the queue. + * + * - pending + * Indicates if a packet is present in the set of pending packets or not. + * This flag must only be modified with the pending lock held, and must be + * coherent to the presence of the packet in the pending set. + * + * - transmitting + * Indicates if the packet is currently transmitting. In case of + * re-transmissions, it is only safe to wait on the "transmitted" completion + * after this flag has been set. The completion will be set both in success + * and error case. + * + * - transmitted + * Indicates if the packet has been transmitted. This flag is not cleared by + * the system, thus it indicates the first transmission only. + * + * - acked + * Indicates if the packet has been acknowledged by the client. There are no + * other guarantees given. For example, the packet may still be canceled + * and/or the completion may be triggered an error even though this bit is + * set. Rely on the status provided to the completion callback instead. + * + * - canceled + * Indicates if the packet has been canceled from the outside. There are no + * other guarantees given. Specifically, the packet may be completed by + * another part of the system before the cancellation attempts to complete it. + * + * >> General Notes << + * + * - To avoid deadlocks, if both queue and pending locks are required, the + * pending lock must be acquired before the queue lock. + * + * - The packet priority must be accessed only while holding the queue lock. + * + * - The packet timestamp must be accessed only while holding the pending + * lock. + */ + +/* + * SSH_PTL_MAX_PACKET_TRIES - Maximum transmission attempts for packet. + * + * Maximum number of transmission attempts per sequenced packet in case of + * time-outs. Must be smaller than 16. If the packet times out after this + * amount of tries, the packet will be completed with %-ETIMEDOUT as status + * code. + */ +#define SSH_PTL_MAX_PACKET_TRIES 3 + +/* + * SSH_PTL_TX_TIMEOUT - Packet transmission timeout. + * + * Timeout in jiffies for packet transmission via the underlying serial + * device. If transmitting the packet takes longer than this timeout, the + * packet will be completed with -ETIMEDOUT. It will not be re-submitted. + */ +#define SSH_PTL_TX_TIMEOUT HZ + +/* + * SSH_PTL_PACKET_TIMEOUT - Packet response timeout. + * + * Timeout as ktime_t delta for ACKs. If we have not received an ACK in this + * time-frame after starting transmission, the packet will be re-submitted. + */ +#define SSH_PTL_PACKET_TIMEOUT ms_to_ktime(1000) + +/* + * SSH_PTL_PACKET_TIMEOUT_RESOLUTION - Packet timeout granularity. + * + * Time-resolution for timeouts. Should be larger than one jiffy to avoid + * direct re-scheduling of reaper work_struct. + */ +#define SSH_PTL_PACKET_TIMEOUT_RESOLUTION ms_to_ktime(max(2000 / HZ, 50)) + +/* + * SSH_PTL_MAX_PENDING - Maximum number of pending packets. + * + * Maximum number of sequenced packets concurrently waiting for an ACK. + * Packets marked as blocking will not be transmitted while this limit is + * reached. + */ +#define SSH_PTL_MAX_PENDING 1 + +/* + * SSH_PTL_RX_BUF_LEN - Evaluation-buffer size in bytes. + */ +#define SSH_PTL_RX_BUF_LEN 4096 + +/* + * SSH_PTL_RX_FIFO_LEN - Fifo input-buffer size in bytes. + */ +#define SSH_PTL_RX_FIFO_LEN 4096 + +#ifdef CONFIG_SURFACE_AGGREGATOR_ERROR_INJECTION + +/** + * ssh_ptl_should_drop_ack_packet() - Error injection hook to drop ACK packets. + * + * Useful to test detection and handling of automated re-transmits by the EC. + * Specifically of packets that the EC considers not-ACKed but the driver + * already considers ACKed (due to dropped ACK). In this case, the EC + * re-transmits the packet-to-be-ACKed and the driver should detect it as + * duplicate/already handled. Note that the driver should still send an ACK + * for the re-transmitted packet. + */ +static noinline bool ssh_ptl_should_drop_ack_packet(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_drop_ack_packet, TRUE); + +/** + * ssh_ptl_should_drop_nak_packet() - Error injection hook to drop NAK packets. + * + * Useful to test/force automated (timeout-based) re-transmit by the EC. + * Specifically, packets that have not reached the driver completely/with valid + * checksums. Only useful in combination with receival of (injected) bad data. + */ +static noinline bool ssh_ptl_should_drop_nak_packet(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_drop_nak_packet, TRUE); + +/** + * ssh_ptl_should_drop_dsq_packet() - Error injection hook to drop sequenced + * data packet. + * + * Useful to test re-transmit timeout of the driver. If the data packet has not + * been ACKed after a certain time, the driver should re-transmit the packet up + * to limited number of times defined in SSH_PTL_MAX_PACKET_TRIES. + */ +static noinline bool ssh_ptl_should_drop_dsq_packet(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_drop_dsq_packet, TRUE); + +/** + * ssh_ptl_should_fail_write() - Error injection hook to make + * serdev_device_write() fail. + * + * Hook to simulate errors in serdev_device_write when transmitting packets. + */ +static noinline int ssh_ptl_should_fail_write(void) +{ + return 0; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_fail_write, ERRNO); + +/** + * ssh_ptl_should_corrupt_tx_data() - Error injection hook to simulate invalid + * data being sent to the EC. + * + * Hook to simulate corrupt/invalid data being sent from host (driver) to EC. + * Causes the packet data to be actively corrupted by overwriting it with + * pre-defined values, such that it becomes invalid, causing the EC to respond + * with a NAK packet. Useful to test handling of NAK packets received by the + * driver. + */ +static noinline bool ssh_ptl_should_corrupt_tx_data(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_corrupt_tx_data, TRUE); + +/** + * ssh_ptl_should_corrupt_rx_syn() - Error injection hook to simulate invalid + * data being sent by the EC. + * + * Hook to simulate invalid SYN bytes, i.e. an invalid start of messages and + * test handling thereof in the driver. + */ +static noinline bool ssh_ptl_should_corrupt_rx_syn(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_corrupt_rx_syn, TRUE); + +/** + * ssh_ptl_should_corrupt_rx_data() - Error injection hook to simulate invalid + * data being sent by the EC. + * + * Hook to simulate invalid data/checksum of the message frame and test handling + * thereof in the driver. + */ +static noinline bool ssh_ptl_should_corrupt_rx_data(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_ptl_should_corrupt_rx_data, TRUE); + +static bool __ssh_ptl_should_drop_ack_packet(struct ssh_packet *packet) +{ + if (likely(!ssh_ptl_should_drop_ack_packet())) + return false; + + trace_ssam_ei_tx_drop_ack_packet(packet); + ptl_info(packet->ptl, "packet error injection: dropping ACK packet %p\n", + packet); + + return true; +} + +static bool __ssh_ptl_should_drop_nak_packet(struct ssh_packet *packet) +{ + if (likely(!ssh_ptl_should_drop_nak_packet())) + return false; + + trace_ssam_ei_tx_drop_nak_packet(packet); + ptl_info(packet->ptl, "packet error injection: dropping NAK packet %p\n", + packet); + + return true; +} + +static bool __ssh_ptl_should_drop_dsq_packet(struct ssh_packet *packet) +{ + if (likely(!ssh_ptl_should_drop_dsq_packet())) + return false; + + trace_ssam_ei_tx_drop_dsq_packet(packet); + ptl_info(packet->ptl, + "packet error injection: dropping sequenced data packet %p\n", + packet); + + return true; +} + +static bool ssh_ptl_should_drop_packet(struct ssh_packet *packet) +{ + /* Ignore packets that don't carry any data (i.e. flush). */ + if (!packet->data.ptr || !packet->data.len) + return false; + + switch (packet->data.ptr[SSH_MSGOFFSET_FRAME(type)]) { + case SSH_FRAME_TYPE_ACK: + return __ssh_ptl_should_drop_ack_packet(packet); + + case SSH_FRAME_TYPE_NAK: + return __ssh_ptl_should_drop_nak_packet(packet); + + case SSH_FRAME_TYPE_DATA_SEQ: + return __ssh_ptl_should_drop_dsq_packet(packet); + + default: + return false; + } +} + +static int ssh_ptl_write_buf(struct ssh_ptl *ptl, struct ssh_packet *packet, + const unsigned char *buf, size_t count) +{ + int status; + + status = ssh_ptl_should_fail_write(); + if (unlikely(status)) { + trace_ssam_ei_tx_fail_write(packet, status); + ptl_info(packet->ptl, + "packet error injection: simulating transmit error %d, packet %p\n", + status, packet); + + return status; + } + + return serdev_device_write_buf(ptl->serdev, buf, count); +} + +static void ssh_ptl_tx_inject_invalid_data(struct ssh_packet *packet) +{ + /* Ignore packets that don't carry any data (i.e. flush). */ + if (!packet->data.ptr || !packet->data.len) + return; + + /* Only allow sequenced data packets to be modified. */ + if (packet->data.ptr[SSH_MSGOFFSET_FRAME(type)] != SSH_FRAME_TYPE_DATA_SEQ) + return; + + if (likely(!ssh_ptl_should_corrupt_tx_data())) + return; + + trace_ssam_ei_tx_corrupt_data(packet); + ptl_info(packet->ptl, + "packet error injection: simulating invalid transmit data on packet %p\n", + packet); + + /* + * NB: The value 0xb3 has been chosen more or less randomly so that it + * doesn't have any (major) overlap with the SYN bytes (aa 55) and is + * non-trivial (i.e. non-zero, non-0xff). + */ + memset(packet->data.ptr, 0xb3, packet->data.len); +} + +static void ssh_ptl_rx_inject_invalid_syn(struct ssh_ptl *ptl, + struct ssam_span *data) +{ + struct ssam_span frame; + + /* Check if there actually is something to corrupt. */ + if (!sshp_find_syn(data, &frame)) + return; + + if (likely(!ssh_ptl_should_corrupt_rx_syn())) + return; + + trace_ssam_ei_rx_corrupt_syn(data->len); + + data->ptr[1] = 0xb3; /* Set second byte of SYN to "random" value. */ +} + +static void ssh_ptl_rx_inject_invalid_data(struct ssh_ptl *ptl, + struct ssam_span *frame) +{ + size_t payload_len, message_len; + struct ssh_frame *sshf; + + /* Ignore incomplete messages, will get handled once it's complete. */ + if (frame->len < SSH_MESSAGE_LENGTH(0)) + return; + + /* Ignore incomplete messages, part 2. */ + payload_len = get_unaligned_le16(&frame->ptr[SSH_MSGOFFSET_FRAME(len)]); + message_len = SSH_MESSAGE_LENGTH(payload_len); + if (frame->len < message_len) + return; + + if (likely(!ssh_ptl_should_corrupt_rx_data())) + return; + + sshf = (struct ssh_frame *)&frame->ptr[SSH_MSGOFFSET_FRAME(type)]; + trace_ssam_ei_rx_corrupt_data(sshf); + + /* + * Flip bits in first byte of payload checksum. This is basically + * equivalent to a payload/frame data error without us having to worry + * about (the, arguably pretty small, probability of) accidental + * checksum collisions. + */ + frame->ptr[frame->len - 2] = ~frame->ptr[frame->len - 2]; +} + +#else /* CONFIG_SURFACE_AGGREGATOR_ERROR_INJECTION */ + +static inline bool ssh_ptl_should_drop_packet(struct ssh_packet *packet) +{ + return false; +} + +static inline int ssh_ptl_write_buf(struct ssh_ptl *ptl, + struct ssh_packet *packet, + const unsigned char *buf, + size_t count) +{ + return serdev_device_write_buf(ptl->serdev, buf, count); +} + +static inline void ssh_ptl_tx_inject_invalid_data(struct ssh_packet *packet) +{ +} + +static inline void ssh_ptl_rx_inject_invalid_syn(struct ssh_ptl *ptl, + struct ssam_span *data) +{ +} + +static inline void ssh_ptl_rx_inject_invalid_data(struct ssh_ptl *ptl, + struct ssam_span *frame) +{ +} + +#endif /* CONFIG_SURFACE_AGGREGATOR_ERROR_INJECTION */ + +static void __ssh_ptl_packet_release(struct kref *kref) +{ + struct ssh_packet *p = container_of(kref, struct ssh_packet, refcnt); + + trace_ssam_packet_release(p); + + ptl_dbg_cond(p->ptl, "ptl: releasing packet %p\n", p); + p->ops->release(p); +} + +/** + * ssh_packet_get() - Increment reference count of packet. + * @packet: The packet to increment the reference count of. + * + * Increments the reference count of the given packet. See ssh_packet_put() + * for the counter-part of this function. + * + * Return: Returns the packet provided as input. + */ +struct ssh_packet *ssh_packet_get(struct ssh_packet *packet) +{ + if (packet) + kref_get(&packet->refcnt); + return packet; +} +EXPORT_SYMBOL_GPL(ssh_packet_get); + +/** + * ssh_packet_put() - Decrement reference count of packet. + * @packet: The packet to decrement the reference count of. + * + * If the reference count reaches zero, the ``release`` callback specified in + * the packet's &struct ssh_packet_ops, i.e. ``packet->ops->release``, will be + * called. + * + * See ssh_packet_get() for the counter-part of this function. + */ +void ssh_packet_put(struct ssh_packet *packet) +{ + if (packet) + kref_put(&packet->refcnt, __ssh_ptl_packet_release); +} +EXPORT_SYMBOL_GPL(ssh_packet_put); + +static u8 ssh_packet_get_seq(struct ssh_packet *packet) +{ + return packet->data.ptr[SSH_MSGOFFSET_FRAME(seq)]; +} + +/** + * ssh_packet_init() - Initialize SSH packet. + * @packet: The packet to initialize. + * @type: Type-flags of the packet. + * @priority: Priority of the packet. See SSH_PACKET_PRIORITY() for details. + * @ops: Packet operations. + * + * Initializes the given SSH packet. Sets the transmission buffer pointer to + * %NULL and the transmission buffer length to zero. For data-type packets, + * this buffer has to be set separately via ssh_packet_set_data() before + * submission, and must contain a valid SSH message, i.e. frame with optional + * payload of any type. + */ +void ssh_packet_init(struct ssh_packet *packet, unsigned long type, + u8 priority, const struct ssh_packet_ops *ops) +{ + kref_init(&packet->refcnt); + + packet->ptl = NULL; + INIT_LIST_HEAD(&packet->queue_node); + INIT_LIST_HEAD(&packet->pending_node); + + packet->state = type & SSH_PACKET_FLAGS_TY_MASK; + packet->priority = priority; + packet->timestamp = KTIME_MAX; + + packet->data.ptr = NULL; + packet->data.len = 0; + + packet->ops = ops; +} + +static struct kmem_cache *ssh_ctrl_packet_cache; + +/** + * ssh_ctrl_packet_cache_init() - Initialize the control packet cache. + */ +int ssh_ctrl_packet_cache_init(void) +{ + const unsigned int size = sizeof(struct ssh_packet) + SSH_MSG_LEN_CTRL; + const unsigned int align = __alignof__(struct ssh_packet); + struct kmem_cache *cache; + + cache = kmem_cache_create("ssam_ctrl_packet", size, align, 0, NULL); + if (!cache) + return -ENOMEM; + + ssh_ctrl_packet_cache = cache; + return 0; +} + +/** + * ssh_ctrl_packet_cache_destroy() - Deinitialize the control packet cache. + */ +void ssh_ctrl_packet_cache_destroy(void) +{ + kmem_cache_destroy(ssh_ctrl_packet_cache); + ssh_ctrl_packet_cache = NULL; +} + +/** + * ssh_ctrl_packet_alloc() - Allocate packet from control packet cache. + * @packet: Where the pointer to the newly allocated packet should be stored. + * @buffer: The buffer corresponding to this packet. + * @flags: Flags used for allocation. + * + * Allocates a packet and corresponding transport buffer from the control + * packet cache. Sets the packet's buffer reference to the allocated buffer. + * The packet must be freed via ssh_ctrl_packet_free(), which will also free + * the corresponding buffer. The corresponding buffer must not be freed + * separately. Intended to be used with %ssh_ptl_ctrl_packet_ops as packet + * operations. + * + * Return: Returns zero on success, %-ENOMEM if the allocation failed. + */ +static int ssh_ctrl_packet_alloc(struct ssh_packet **packet, + struct ssam_span *buffer, gfp_t flags) +{ + *packet = kmem_cache_alloc(ssh_ctrl_packet_cache, flags); + if (!*packet) + return -ENOMEM; + + buffer->ptr = (u8 *)(*packet + 1); + buffer->len = SSH_MSG_LEN_CTRL; + + trace_ssam_ctrl_packet_alloc(*packet, buffer->len); + return 0; +} + +/** + * ssh_ctrl_packet_free() - Free packet allocated from control packet cache. + * @p: The packet to free. + */ +static void ssh_ctrl_packet_free(struct ssh_packet *p) +{ + trace_ssam_ctrl_packet_free(p); + kmem_cache_free(ssh_ctrl_packet_cache, p); +} + +static const struct ssh_packet_ops ssh_ptl_ctrl_packet_ops = { + .complete = NULL, + .release = ssh_ctrl_packet_free, +}; + +static void ssh_ptl_timeout_reaper_mod(struct ssh_ptl *ptl, ktime_t now, + ktime_t expires) +{ + unsigned long delta = msecs_to_jiffies(ktime_ms_delta(expires, now)); + ktime_t aexp = ktime_add(expires, SSH_PTL_PACKET_TIMEOUT_RESOLUTION); + + spin_lock(&ptl->rtx_timeout.lock); + + /* Re-adjust / schedule reaper only if it is above resolution delta. */ + if (ktime_before(aexp, ptl->rtx_timeout.expires)) { + ptl->rtx_timeout.expires = expires; + mod_delayed_work(system_percpu_wq, &ptl->rtx_timeout.reaper, delta); + } + + spin_unlock(&ptl->rtx_timeout.lock); +} + +/* Must be called with queue lock held. */ +static void ssh_packet_next_try(struct ssh_packet *p) +{ + u8 base = ssh_packet_priority_get_base(p->priority); + u8 try = ssh_packet_priority_get_try(p->priority); + + lockdep_assert_held(&p->ptl->queue.lock); + + /* + * Ensure that we write the priority in one go via WRITE_ONCE() so we + * can access it via READ_ONCE() for tracing. Note that other access + * is guarded by the queue lock, so no need to use READ_ONCE() there. + */ + WRITE_ONCE(p->priority, __SSH_PACKET_PRIORITY(base, try + 1)); +} + +/* Must be called with queue lock held. */ +static struct list_head *__ssh_ptl_queue_find_entrypoint(struct ssh_packet *p) +{ + struct list_head *head; + struct ssh_packet *q; + + lockdep_assert_held(&p->ptl->queue.lock); + + /* + * We generally assume that there are less control (ACK/NAK) packets + * and re-submitted data packets as there are normal data packets (at + * least in situations in which many packets are queued; if there + * aren't many packets queued the decision on how to iterate should be + * basically irrelevant; the number of control/data packets is more or + * less limited via the maximum number of pending packets). Thus, when + * inserting a control or re-submitted data packet, (determined by + * their priority), we search from front to back. Normal data packets + * are, usually queued directly at the tail of the queue, so for those + * search from back to front. + */ + + if (p->priority > SSH_PACKET_PRIORITY(DATA, 0)) { + list_for_each(head, &p->ptl->queue.head) { + q = list_entry(head, struct ssh_packet, queue_node); + + if (q->priority < p->priority) + break; + } + } else { + list_for_each_prev(head, &p->ptl->queue.head) { + q = list_entry(head, struct ssh_packet, queue_node); + + if (q->priority >= p->priority) { + head = head->next; + break; + } + } + } + + return head; +} + +/* Must be called with queue lock held. */ +static int __ssh_ptl_queue_push(struct ssh_packet *packet) +{ + struct ssh_ptl *ptl = packet->ptl; + struct list_head *head; + + lockdep_assert_held(&ptl->queue.lock); + + if (test_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state)) + return -ESHUTDOWN; + + /* Avoid further transitions when canceling/completing. */ + if (test_bit(SSH_PACKET_SF_LOCKED_BIT, &packet->state)) + return -EINVAL; + + /* If this packet has already been queued, do not add it. */ + if (test_and_set_bit(SSH_PACKET_SF_QUEUED_BIT, &packet->state)) + return -EALREADY; + + head = __ssh_ptl_queue_find_entrypoint(packet); + + list_add_tail(&ssh_packet_get(packet)->queue_node, head); + return 0; +} + +static int ssh_ptl_queue_push(struct ssh_packet *packet) +{ + int status; + + spin_lock(&packet->ptl->queue.lock); + status = __ssh_ptl_queue_push(packet); + spin_unlock(&packet->ptl->queue.lock); + + return status; +} + +static void ssh_ptl_queue_remove(struct ssh_packet *packet) +{ + struct ssh_ptl *ptl = packet->ptl; + + spin_lock(&ptl->queue.lock); + + if (!test_and_clear_bit(SSH_PACKET_SF_QUEUED_BIT, &packet->state)) { + spin_unlock(&ptl->queue.lock); + return; + } + + list_del(&packet->queue_node); + + spin_unlock(&ptl->queue.lock); + ssh_packet_put(packet); +} + +static void ssh_ptl_pending_push(struct ssh_packet *p) +{ + struct ssh_ptl *ptl = p->ptl; + const ktime_t timestamp = ktime_get_coarse_boottime(); + const ktime_t timeout = ptl->rtx_timeout.timeout; + + /* + * Note: We can get the time for the timestamp before acquiring the + * lock as this is the only place we're setting it and this function + * is called only from the transmitter thread. Thus it is not possible + * to overwrite the timestamp with an outdated value below. + */ + + spin_lock(&ptl->pending.lock); + + /* If we are canceling/completing this packet, do not add it. */ + if (test_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state)) { + spin_unlock(&ptl->pending.lock); + return; + } + + /* + * On re-submission, the packet has already been added the pending + * set. We still need to update the timestamp as the packet timeout is + * reset for each (re-)submission. + */ + p->timestamp = timestamp; + + /* In case it is already pending (e.g. re-submission), do not add it. */ + if (!test_and_set_bit(SSH_PACKET_SF_PENDING_BIT, &p->state)) { + atomic_inc(&ptl->pending.count); + list_add_tail(&ssh_packet_get(p)->pending_node, &ptl->pending.head); + } + + spin_unlock(&ptl->pending.lock); + + /* Arm/update timeout reaper. */ + ssh_ptl_timeout_reaper_mod(ptl, timestamp, timestamp + timeout); +} + +static void ssh_ptl_pending_remove(struct ssh_packet *packet) +{ + struct ssh_ptl *ptl = packet->ptl; + + spin_lock(&ptl->pending.lock); + + if (!test_and_clear_bit(SSH_PACKET_SF_PENDING_BIT, &packet->state)) { + spin_unlock(&ptl->pending.lock); + return; + } + + list_del(&packet->pending_node); + atomic_dec(&ptl->pending.count); + + spin_unlock(&ptl->pending.lock); + + ssh_packet_put(packet); +} + +/* Warning: Does not check/set "completed" bit. */ +static void __ssh_ptl_complete(struct ssh_packet *p, int status) +{ + struct ssh_ptl *ptl = READ_ONCE(p->ptl); + + trace_ssam_packet_complete(p, status); + ptl_dbg_cond(ptl, "ptl: completing packet %p (status: %d)\n", p, status); + + if (p->ops->complete) + p->ops->complete(p, status); +} + +static void ssh_ptl_remove_and_complete(struct ssh_packet *p, int status) +{ + /* + * A call to this function should in general be preceded by + * set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->flags) to avoid re-adding the + * packet to the structures it's going to be removed from. + * + * The set_bit call does not need explicit memory barriers as the + * implicit barrier of the test_and_set_bit() call below ensure that the + * flag is visible before we actually attempt to remove the packet. + */ + + if (test_and_set_bit(SSH_PACKET_SF_COMPLETED_BIT, &p->state)) + return; + + ssh_ptl_queue_remove(p); + ssh_ptl_pending_remove(p); + + __ssh_ptl_complete(p, status); +} + +static bool ssh_ptl_tx_can_process(struct ssh_packet *packet) +{ + struct ssh_ptl *ptl = packet->ptl; + + if (test_bit(SSH_PACKET_TY_FLUSH_BIT, &packet->state)) + return !atomic_read(&ptl->pending.count); + + /* We can always process non-blocking packets. */ + if (!test_bit(SSH_PACKET_TY_BLOCKING_BIT, &packet->state)) + return true; + + /* If we are already waiting for this packet, send it again. */ + if (test_bit(SSH_PACKET_SF_PENDING_BIT, &packet->state)) + return true; + + /* Otherwise: Check if we have the capacity to send. */ + return atomic_read(&ptl->pending.count) < SSH_PTL_MAX_PENDING; +} + +static struct ssh_packet *ssh_ptl_tx_pop(struct ssh_ptl *ptl) +{ + struct ssh_packet *packet = ERR_PTR(-ENOENT); + struct ssh_packet *p, *n; + + spin_lock(&ptl->queue.lock); + list_for_each_entry_safe(p, n, &ptl->queue.head, queue_node) { + /* + * If we are canceling or completing this packet, ignore it. + * It's going to be removed from this queue shortly. + */ + if (test_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state)) + continue; + + /* + * Packets should be ordered non-blocking/to-be-resent first. + * If we cannot process this packet, assume that we can't + * process any following packet either and abort. + */ + if (!ssh_ptl_tx_can_process(p)) { + packet = ERR_PTR(-EBUSY); + break; + } + + /* + * We are allowed to change the state now. Remove it from the + * queue and mark it as being transmitted. + */ + + list_del(&p->queue_node); + + set_bit(SSH_PACKET_SF_TRANSMITTING_BIT, &p->state); + /* Ensure that state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_QUEUED_BIT, &p->state); + + /* + * Update number of tries. This directly influences the + * priority in case the packet is re-submitted (e.g. via + * timeout/NAK). Note that all reads and writes to the + * priority after the first submission are guarded by the + * queue lock. + */ + ssh_packet_next_try(p); + + packet = p; + break; + } + spin_unlock(&ptl->queue.lock); + + return packet; +} + +static struct ssh_packet *ssh_ptl_tx_next(struct ssh_ptl *ptl) +{ + struct ssh_packet *p; + + p = ssh_ptl_tx_pop(ptl); + if (IS_ERR(p)) + return p; + + if (test_bit(SSH_PACKET_TY_SEQUENCED_BIT, &p->state)) { + ptl_dbg(ptl, "ptl: transmitting sequenced packet %p\n", p); + ssh_ptl_pending_push(p); + } else { + ptl_dbg(ptl, "ptl: transmitting non-sequenced packet %p\n", p); + } + + return p; +} + +static void ssh_ptl_tx_compl_success(struct ssh_packet *packet) +{ + struct ssh_ptl *ptl = packet->ptl; + + ptl_dbg(ptl, "ptl: successfully transmitted packet %p\n", packet); + + /* Transition state to "transmitted". */ + set_bit(SSH_PACKET_SF_TRANSMITTED_BIT, &packet->state); + /* Ensure that state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_TRANSMITTING_BIT, &packet->state); + + /* If the packet is unsequenced, we're done: Lock and complete. */ + if (!test_bit(SSH_PACKET_TY_SEQUENCED_BIT, &packet->state)) { + set_bit(SSH_PACKET_SF_LOCKED_BIT, &packet->state); + ssh_ptl_remove_and_complete(packet, 0); + } + + /* + * Notify that a packet transmission has finished. In general we're only + * waiting for one packet (if any), so wake_up_all should be fine. + */ + wake_up_all(&ptl->tx.packet_wq); +} + +static void ssh_ptl_tx_compl_error(struct ssh_packet *packet, int status) +{ + /* Transmission failure: Lock the packet and try to complete it. */ + set_bit(SSH_PACKET_SF_LOCKED_BIT, &packet->state); + /* Ensure that state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_TRANSMITTING_BIT, &packet->state); + + ptl_err(packet->ptl, "ptl: transmission error: %d\n", status); + ptl_dbg(packet->ptl, "ptl: failed to transmit packet: %p\n", packet); + + ssh_ptl_remove_and_complete(packet, status); + + /* + * Notify that a packet transmission has finished. In general we're only + * waiting for one packet (if any), so wake_up_all should be fine. + */ + wake_up_all(&packet->ptl->tx.packet_wq); +} + +static long ssh_ptl_tx_wait_packet(struct ssh_ptl *ptl) +{ + int status; + + status = wait_for_completion_interruptible(&ptl->tx.thread_cplt_pkt); + reinit_completion(&ptl->tx.thread_cplt_pkt); + + /* + * Ensure completion is cleared before continuing to avoid lost update + * problems. + */ + smp_mb__after_atomic(); + + return status; +} + +static long ssh_ptl_tx_wait_transfer(struct ssh_ptl *ptl, long timeout) +{ + long status; + + status = wait_for_completion_interruptible_timeout(&ptl->tx.thread_cplt_tx, + timeout); + reinit_completion(&ptl->tx.thread_cplt_tx); + + /* + * Ensure completion is cleared before continuing to avoid lost update + * problems. + */ + smp_mb__after_atomic(); + + return status; +} + +static int ssh_ptl_tx_packet(struct ssh_ptl *ptl, struct ssh_packet *packet) +{ + long timeout = SSH_PTL_TX_TIMEOUT; + size_t offset = 0; + + /* Note: Flush-packets don't have any data. */ + if (unlikely(!packet->data.ptr)) + return 0; + + /* Error injection: drop packet to simulate transmission problem. */ + if (ssh_ptl_should_drop_packet(packet)) + return 0; + + /* Error injection: simulate invalid packet data. */ + ssh_ptl_tx_inject_invalid_data(packet); + + ptl_dbg(ptl, "tx: sending data (length: %zu)\n", packet->data.len); + print_hex_dump_debug("tx: ", DUMP_PREFIX_OFFSET, 16, 1, + packet->data.ptr, packet->data.len, false); + + do { + ssize_t status, len; + u8 *buf; + + buf = packet->data.ptr + offset; + len = packet->data.len - offset; + + status = ssh_ptl_write_buf(ptl, packet, buf, len); + if (status < 0) + return status; + + if (status == len) + return 0; + + offset += status; + + timeout = ssh_ptl_tx_wait_transfer(ptl, timeout); + if (kthread_should_stop() || !atomic_read(&ptl->tx.running)) + return -ESHUTDOWN; + + if (timeout < 0) + return -EINTR; + + if (timeout == 0) + return -ETIMEDOUT; + } while (true); +} + +static int ssh_ptl_tx_threadfn(void *data) +{ + struct ssh_ptl *ptl = data; + + while (!kthread_should_stop() && atomic_read(&ptl->tx.running)) { + struct ssh_packet *packet; + int status; + + /* Try to get the next packet. */ + packet = ssh_ptl_tx_next(ptl); + + /* If no packet can be processed, we are done. */ + if (IS_ERR(packet)) { + ssh_ptl_tx_wait_packet(ptl); + continue; + } + + /* Transfer and complete packet. */ + status = ssh_ptl_tx_packet(ptl, packet); + if (status) + ssh_ptl_tx_compl_error(packet, status); + else + ssh_ptl_tx_compl_success(packet); + + ssh_packet_put(packet); + } + + return 0; +} + +/** + * ssh_ptl_tx_wakeup_packet() - Wake up packet transmitter thread for new + * packet. + * @ptl: The packet transport layer. + * + * Wakes up the packet transmitter thread, notifying it that a new packet has + * arrived and is ready for transfer. If the packet transport layer has been + * shut down, calls to this function will be ignored. + */ +static void ssh_ptl_tx_wakeup_packet(struct ssh_ptl *ptl) +{ + if (test_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state)) + return; + + complete(&ptl->tx.thread_cplt_pkt); +} + +/** + * ssh_ptl_tx_start() - Start packet transmitter thread. + * @ptl: The packet transport layer. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssh_ptl_tx_start(struct ssh_ptl *ptl) +{ + atomic_set_release(&ptl->tx.running, 1); + + ptl->tx.thread = kthread_run(ssh_ptl_tx_threadfn, ptl, "ssam_serial_hub-tx"); + if (IS_ERR(ptl->tx.thread)) + return PTR_ERR(ptl->tx.thread); + + return 0; +} + +/** + * ssh_ptl_tx_stop() - Stop packet transmitter thread. + * @ptl: The packet transport layer. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssh_ptl_tx_stop(struct ssh_ptl *ptl) +{ + int status = 0; + + if (!IS_ERR_OR_NULL(ptl->tx.thread)) { + /* Tell thread to stop. */ + atomic_set_release(&ptl->tx.running, 0); + + /* + * Wake up thread in case it is paused. Do not use wakeup + * helpers as this may be called when the shutdown bit has + * already been set. + */ + complete(&ptl->tx.thread_cplt_pkt); + complete(&ptl->tx.thread_cplt_tx); + + /* Finally, wait for thread to stop. */ + status = kthread_stop(ptl->tx.thread); + ptl->tx.thread = NULL; + } + + return status; +} + +static struct ssh_packet *ssh_ptl_ack_pop(struct ssh_ptl *ptl, u8 seq_id) +{ + struct ssh_packet *packet = ERR_PTR(-ENOENT); + struct ssh_packet *p, *n; + + spin_lock(&ptl->pending.lock); + list_for_each_entry_safe(p, n, &ptl->pending.head, pending_node) { + /* + * We generally expect packets to be in order, so first packet + * to be added to pending is first to be sent, is first to be + * ACKed. + */ + if (unlikely(ssh_packet_get_seq(p) != seq_id)) + continue; + + /* + * In case we receive an ACK while handling a transmission + * error completion. The packet will be removed shortly. + */ + if (unlikely(test_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state))) { + packet = ERR_PTR(-EPERM); + break; + } + + /* + * Mark the packet as ACKed and remove it from pending by + * removing its node and decrementing the pending counter. + */ + set_bit(SSH_PACKET_SF_ACKED_BIT, &p->state); + /* Ensure that state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_PENDING_BIT, &p->state); + + atomic_dec(&ptl->pending.count); + list_del(&p->pending_node); + packet = p; + + break; + } + spin_unlock(&ptl->pending.lock); + + return packet; +} + +static void ssh_ptl_wait_until_transmitted(struct ssh_packet *packet) +{ + wait_event(packet->ptl->tx.packet_wq, + test_bit(SSH_PACKET_SF_TRANSMITTED_BIT, &packet->state) || + test_bit(SSH_PACKET_SF_LOCKED_BIT, &packet->state)); +} + +static void ssh_ptl_acknowledge(struct ssh_ptl *ptl, u8 seq) +{ + struct ssh_packet *p; + + p = ssh_ptl_ack_pop(ptl, seq); + if (IS_ERR(p)) { + if (PTR_ERR(p) == -ENOENT) { + /* + * The packet has not been found in the set of pending + * packets. + */ + ptl_warn(ptl, "ptl: received ACK for non-pending packet\n"); + } else { + /* + * The packet is pending, but we are not allowed to take + * it because it has been locked. + */ + WARN_ON(PTR_ERR(p) != -EPERM); + } + return; + } + + ptl_dbg(ptl, "ptl: received ACK for packet %p\n", p); + + /* + * It is possible that the packet has been transmitted, but the state + * has not been updated from "transmitting" to "transmitted" yet. + * In that case, we need to wait for this transition to occur in order + * to determine between success or failure. + * + * On transmission failure, the packet will be locked after this call. + * On success, the transmitted bit will be set. + */ + ssh_ptl_wait_until_transmitted(p); + + /* + * The packet will already be locked in case of a transmission error or + * cancellation. Let the transmitter or cancellation issuer complete the + * packet. + */ + if (unlikely(test_and_set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state))) { + if (unlikely(!test_bit(SSH_PACKET_SF_TRANSMITTED_BIT, &p->state))) + ptl_err(ptl, "ptl: received ACK before packet had been fully transmitted\n"); + + ssh_packet_put(p); + return; + } + + ssh_ptl_remove_and_complete(p, 0); + ssh_packet_put(p); + + if (atomic_read(&ptl->pending.count) < SSH_PTL_MAX_PENDING) + ssh_ptl_tx_wakeup_packet(ptl); +} + +/** + * ssh_ptl_submit() - Submit a packet to the transport layer. + * @ptl: The packet transport layer to submit the packet to. + * @p: The packet to submit. + * + * Submits a new packet to the transport layer, queuing it to be sent. This + * function should not be used for re-submission. + * + * Return: Returns zero on success, %-EINVAL if a packet field is invalid or + * the packet has been canceled prior to submission, %-EALREADY if the packet + * has already been submitted, or %-ESHUTDOWN if the packet transport layer + * has been shut down. + */ +int ssh_ptl_submit(struct ssh_ptl *ptl, struct ssh_packet *p) +{ + struct ssh_ptl *ptl_old; + int status; + + trace_ssam_packet_submit(p); + + /* Validate packet fields. */ + if (test_bit(SSH_PACKET_TY_FLUSH_BIT, &p->state)) { + if (p->data.ptr || test_bit(SSH_PACKET_TY_SEQUENCED_BIT, &p->state)) + return -EINVAL; + } else if (!p->data.ptr) { + return -EINVAL; + } + + /* + * The ptl reference only gets set on or before the first submission. + * After the first submission, it has to be read-only. + * + * Note that ptl may already be set from upper-layer request + * submission, thus we cannot expect it to be NULL. + */ + ptl_old = READ_ONCE(p->ptl); + if (!ptl_old) + WRITE_ONCE(p->ptl, ptl); + else if (WARN_ON(ptl_old != ptl)) + return -EALREADY; /* Submitted on different PTL. */ + + status = ssh_ptl_queue_push(p); + if (status) + return status; + + if (!test_bit(SSH_PACKET_TY_BLOCKING_BIT, &p->state) || + (atomic_read(&ptl->pending.count) < SSH_PTL_MAX_PENDING)) + ssh_ptl_tx_wakeup_packet(ptl); + + return 0; +} + +/* + * __ssh_ptl_resubmit() - Re-submit a packet to the transport layer. + * @packet: The packet to re-submit. + * + * Re-submits the given packet: Checks if it can be re-submitted and queues it + * if it can, resetting the packet timestamp in the process. Must be called + * with the pending lock held. + * + * Return: Returns %-ECANCELED if the packet has exceeded its number of tries, + * %-EINVAL if the packet has been locked, %-EALREADY if the packet is already + * on the queue, and %-ESHUTDOWN if the transmission layer has been shut down. + */ +static int __ssh_ptl_resubmit(struct ssh_packet *packet) +{ + int status; + u8 try; + + lockdep_assert_held(&packet->ptl->pending.lock); + + trace_ssam_packet_resubmit(packet); + + spin_lock(&packet->ptl->queue.lock); + + /* Check if the packet is out of tries. */ + try = ssh_packet_priority_get_try(packet->priority); + if (try >= SSH_PTL_MAX_PACKET_TRIES) { + spin_unlock(&packet->ptl->queue.lock); + return -ECANCELED; + } + + status = __ssh_ptl_queue_push(packet); + if (status) { + /* + * An error here indicates that the packet has either already + * been queued, been locked, or the transport layer is being + * shut down. In all cases: Ignore the error. + */ + spin_unlock(&packet->ptl->queue.lock); + return status; + } + + packet->timestamp = KTIME_MAX; + + spin_unlock(&packet->ptl->queue.lock); + return 0; +} + +static void ssh_ptl_resubmit_pending(struct ssh_ptl *ptl) +{ + struct ssh_packet *p; + bool resub = false; + + /* + * Note: We deliberately do not remove/attempt to cancel and complete + * packets that are out of tires in this function. The packet will be + * eventually canceled and completed by the timeout. Removing the packet + * here could lead to overly eager cancellation if the packet has not + * been re-transmitted yet but the tries-counter already updated (i.e + * ssh_ptl_tx_next() removed the packet from the queue and updated the + * counter, but re-transmission for the last try has not actually + * started yet). + */ + + spin_lock(&ptl->pending.lock); + + /* Re-queue all pending packets. */ + list_for_each_entry(p, &ptl->pending.head, pending_node) { + /* + * Re-submission fails if the packet is out of tries, has been + * locked, is already queued, or the layer is being shut down. + * No need to re-schedule tx-thread in those cases. + */ + if (!__ssh_ptl_resubmit(p)) + resub = true; + } + + spin_unlock(&ptl->pending.lock); + + if (resub) + ssh_ptl_tx_wakeup_packet(ptl); +} + +/** + * ssh_ptl_cancel() - Cancel a packet. + * @p: The packet to cancel. + * + * Cancels a packet. There are no guarantees on when completion and release + * callbacks will be called. This may occur during execution of this function + * or may occur at any point later. + * + * Note that it is not guaranteed that the packet will actually be canceled if + * the packet is concurrently completed by another process. The only guarantee + * of this function is that the packet will be completed (with success, + * failure, or cancellation) and released from the transport layer in a + * reasonable time-frame. + * + * May be called before the packet has been submitted, in which case any later + * packet submission fails. + */ +void ssh_ptl_cancel(struct ssh_packet *p) +{ + if (test_and_set_bit(SSH_PACKET_SF_CANCELED_BIT, &p->state)) + return; + + trace_ssam_packet_cancel(p); + + /* + * Lock packet and commit with memory barrier. If this packet has + * already been locked, it's going to be removed and completed by + * another party, which should have precedence. + */ + if (test_and_set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state)) + return; + + /* + * By marking the packet as locked and employing the implicit memory + * barrier of test_and_set_bit, we have guaranteed that, at this point, + * the packet cannot be added to the queue any more. + * + * In case the packet has never been submitted, packet->ptl is NULL. If + * the packet is currently being submitted, packet->ptl may be NULL or + * non-NULL. Due marking the packet as locked above and committing with + * the memory barrier, we have guaranteed that, if packet->ptl is NULL, + * the packet will never be added to the queue. If packet->ptl is + * non-NULL, we don't have any guarantees. + */ + + if (READ_ONCE(p->ptl)) { + ssh_ptl_remove_and_complete(p, -ECANCELED); + + if (atomic_read(&p->ptl->pending.count) < SSH_PTL_MAX_PENDING) + ssh_ptl_tx_wakeup_packet(p->ptl); + + } else if (!test_and_set_bit(SSH_PACKET_SF_COMPLETED_BIT, &p->state)) { + __ssh_ptl_complete(p, -ECANCELED); + } +} + +/* Must be called with pending lock held */ +static ktime_t ssh_packet_get_expiration(struct ssh_packet *p, ktime_t timeout) +{ + lockdep_assert_held(&p->ptl->pending.lock); + + if (p->timestamp != KTIME_MAX) + return ktime_add(p->timestamp, timeout); + else + return KTIME_MAX; +} + +static void ssh_ptl_timeout_reap(struct work_struct *work) +{ + struct ssh_ptl *ptl = to_ssh_ptl(work, rtx_timeout.reaper.work); + struct ssh_packet *p, *n; + LIST_HEAD(claimed); + ktime_t now = ktime_get_coarse_boottime(); + ktime_t timeout = ptl->rtx_timeout.timeout; + ktime_t next = KTIME_MAX; + bool resub = false; + int status; + + trace_ssam_ptl_timeout_reap(atomic_read(&ptl->pending.count)); + + /* + * Mark reaper as "not pending". This is done before checking any + * packets to avoid lost-update type problems. + */ + spin_lock(&ptl->rtx_timeout.lock); + ptl->rtx_timeout.expires = KTIME_MAX; + spin_unlock(&ptl->rtx_timeout.lock); + + spin_lock(&ptl->pending.lock); + + list_for_each_entry_safe(p, n, &ptl->pending.head, pending_node) { + ktime_t expires = ssh_packet_get_expiration(p, timeout); + + /* + * Check if the timeout hasn't expired yet. Find out next + * expiration date to be handled after this run. + */ + if (ktime_after(expires, now)) { + next = ktime_before(expires, next) ? expires : next; + continue; + } + + trace_ssam_packet_timeout(p); + + status = __ssh_ptl_resubmit(p); + + /* + * Re-submission fails if the packet is out of tries, has been + * locked, is already queued, or the layer is being shut down. + * No need to re-schedule tx-thread in those cases. + */ + if (!status) + resub = true; + + /* Go to next packet if this packet is not out of tries. */ + if (status != -ECANCELED) + continue; + + /* No more tries left: Cancel the packet. */ + + /* + * If someone else has locked the packet already, don't use it + * and let the other party complete it. + */ + if (test_and_set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state)) + continue; + + /* + * We have now marked the packet as locked. Thus it cannot be + * added to the pending list again after we've removed it here. + * We can therefore re-use the pending_node of this packet + * temporarily. + */ + + clear_bit(SSH_PACKET_SF_PENDING_BIT, &p->state); + + atomic_dec(&ptl->pending.count); + list_move_tail(&p->pending_node, &claimed); + } + + spin_unlock(&ptl->pending.lock); + + /* Cancel and complete the packet. */ + list_for_each_entry_safe(p, n, &claimed, pending_node) { + if (!test_and_set_bit(SSH_PACKET_SF_COMPLETED_BIT, &p->state)) { + ssh_ptl_queue_remove(p); + __ssh_ptl_complete(p, -ETIMEDOUT); + } + + /* + * Drop the reference we've obtained by removing it from + * the pending set. + */ + list_del(&p->pending_node); + ssh_packet_put(p); + } + + /* Ensure that reaper doesn't run again immediately. */ + next = max(next, ktime_add(now, SSH_PTL_PACKET_TIMEOUT_RESOLUTION)); + if (next != KTIME_MAX) + ssh_ptl_timeout_reaper_mod(ptl, now, next); + + if (resub) + ssh_ptl_tx_wakeup_packet(ptl); +} + +static bool ssh_ptl_rx_retransmit_check(struct ssh_ptl *ptl, const struct ssh_frame *frame) +{ + int i; + + /* + * Ignore unsequenced packets. On some devices (notably Surface Pro 9), + * unsequenced events will always be sent with SEQ=0x00. Attempting to + * detect retransmission would thus just block all events. + * + * While sequence numbers would also allow detection of retransmitted + * packets in unsequenced communication, they have only ever been used + * to cover edge-cases in sequenced transmission. In particular, the + * only instance of packets being retransmitted (that we are aware of) + * is due to an ACK timeout. As this does not happen in unsequenced + * communication, skip the retransmission check for those packets + * entirely. + */ + if (frame->type == SSH_FRAME_TYPE_DATA_NSQ) + return false; + + /* + * Check if SEQ has been seen recently (i.e. packet was + * re-transmitted and we should ignore it). + */ + for (i = 0; i < ARRAY_SIZE(ptl->rx.blocked.seqs); i++) { + if (likely(ptl->rx.blocked.seqs[i] != frame->seq)) + continue; + + ptl_dbg(ptl, "ptl: ignoring repeated data packet\n"); + return true; + } + + /* Update list of blocked sequence IDs. */ + ptl->rx.blocked.seqs[ptl->rx.blocked.offset] = frame->seq; + ptl->rx.blocked.offset = (ptl->rx.blocked.offset + 1) + % ARRAY_SIZE(ptl->rx.blocked.seqs); + + return false; +} + +static void ssh_ptl_rx_dataframe(struct ssh_ptl *ptl, + const struct ssh_frame *frame, + const struct ssam_span *payload) +{ + if (ssh_ptl_rx_retransmit_check(ptl, frame)) + return; + + ptl->ops.data_received(ptl, payload); +} + +static void ssh_ptl_send_ack(struct ssh_ptl *ptl, u8 seq) +{ + struct ssh_packet *packet; + struct ssam_span buf; + struct msgbuf msgb; + int status; + + status = ssh_ctrl_packet_alloc(&packet, &buf, GFP_KERNEL); + if (status) { + ptl_err(ptl, "ptl: failed to allocate ACK packet\n"); + return; + } + + ssh_packet_init(packet, 0, SSH_PACKET_PRIORITY(ACK, 0), + &ssh_ptl_ctrl_packet_ops); + + msgb_init(&msgb, buf.ptr, buf.len); + msgb_push_ack(&msgb, seq); + ssh_packet_set_data(packet, msgb.begin, msgb_bytes_used(&msgb)); + + ssh_ptl_submit(ptl, packet); + ssh_packet_put(packet); +} + +static void ssh_ptl_send_nak(struct ssh_ptl *ptl) +{ + struct ssh_packet *packet; + struct ssam_span buf; + struct msgbuf msgb; + int status; + + status = ssh_ctrl_packet_alloc(&packet, &buf, GFP_KERNEL); + if (status) { + ptl_err(ptl, "ptl: failed to allocate NAK packet\n"); + return; + } + + ssh_packet_init(packet, 0, SSH_PACKET_PRIORITY(NAK, 0), + &ssh_ptl_ctrl_packet_ops); + + msgb_init(&msgb, buf.ptr, buf.len); + msgb_push_nak(&msgb); + ssh_packet_set_data(packet, msgb.begin, msgb_bytes_used(&msgb)); + + ssh_ptl_submit(ptl, packet); + ssh_packet_put(packet); +} + +static size_t ssh_ptl_rx_eval(struct ssh_ptl *ptl, struct ssam_span *source) +{ + struct ssh_frame *frame; + struct ssam_span payload; + struct ssam_span aligned; + bool syn_found; + int status; + + /* Error injection: Modify data to simulate corrupt SYN bytes. */ + ssh_ptl_rx_inject_invalid_syn(ptl, source); + + /* Find SYN. */ + syn_found = sshp_find_syn(source, &aligned); + + if (unlikely(aligned.ptr != source->ptr)) { + /* + * We expect aligned.ptr == source->ptr. If this is not the + * case, then aligned.ptr > source->ptr and we've encountered + * some unexpected data where we'd expect the start of a new + * message (i.e. the SYN sequence). + * + * This can happen when a CRC check for the previous message + * failed and we start actively searching for the next one + * (via the call to sshp_find_syn() above), or the first bytes + * of a message got dropped or corrupted. + * + * In any case, we issue a warning, send a NAK to the EC to + * request re-transmission of any data we haven't acknowledged + * yet, and finally, skip everything up to the next SYN + * sequence. + */ + + ptl_warn(ptl, "rx: parser: invalid start of frame, skipping\n"); + + /* + * Notes: + * - This might send multiple NAKs in case the communication + * starts with an invalid SYN and is broken down into multiple + * pieces. This should generally be handled fine, we just + * might receive duplicate data in this case, which is + * detected when handling data frames. + * - This path will also be executed on invalid CRCs: When an + * invalid CRC is encountered, the code below will skip data + * until directly after the SYN. This causes the search for + * the next SYN, which is generally not placed directly after + * the last one. + * + * Open question: Should we send this in case of invalid + * payload CRCs if the frame-type is non-sequential (current + * implementation) or should we drop that frame without + * telling the EC? + */ + ssh_ptl_send_nak(ptl); + } + + if (unlikely(!syn_found)) + return aligned.ptr - source->ptr; + + /* Error injection: Modify data to simulate corruption. */ + ssh_ptl_rx_inject_invalid_data(ptl, &aligned); + + /* Parse and validate frame. */ + status = sshp_parse_frame(&ptl->serdev->dev, &aligned, &frame, &payload, + SSH_PTL_RX_BUF_LEN); + if (status) /* Invalid frame: skip to next SYN. */ + return aligned.ptr - source->ptr + sizeof(u16); + if (!frame) /* Not enough data. */ + return aligned.ptr - source->ptr; + + trace_ssam_rx_frame_received(frame); + + switch (frame->type) { + case SSH_FRAME_TYPE_ACK: + ssh_ptl_acknowledge(ptl, frame->seq); + break; + + case SSH_FRAME_TYPE_NAK: + ssh_ptl_resubmit_pending(ptl); + break; + + case SSH_FRAME_TYPE_DATA_SEQ: + ssh_ptl_send_ack(ptl, frame->seq); + fallthrough; + + case SSH_FRAME_TYPE_DATA_NSQ: + ssh_ptl_rx_dataframe(ptl, frame, &payload); + break; + + default: + ptl_warn(ptl, "ptl: received frame with unknown type %#04x\n", + frame->type); + break; + } + + return aligned.ptr - source->ptr + SSH_MESSAGE_LENGTH(payload.len); +} + +static int ssh_ptl_rx_threadfn(void *data) +{ + struct ssh_ptl *ptl = data; + + while (true) { + struct ssam_span span; + size_t offs = 0; + size_t n; + + wait_event_interruptible(ptl->rx.wq, + !kfifo_is_empty(&ptl->rx.fifo) || + kthread_should_stop()); + if (kthread_should_stop()) + break; + + /* Copy from fifo to evaluation buffer. */ + n = sshp_buf_read_from_fifo(&ptl->rx.buf, &ptl->rx.fifo); + + ptl_dbg(ptl, "rx: received data (size: %zu)\n", n); + print_hex_dump_debug("rx: ", DUMP_PREFIX_OFFSET, 16, 1, + ptl->rx.buf.ptr + ptl->rx.buf.len - n, + n, false); + + /* Parse until we need more bytes or buffer is empty. */ + while (offs < ptl->rx.buf.len) { + sshp_buf_span_from(&ptl->rx.buf, offs, &span); + n = ssh_ptl_rx_eval(ptl, &span); + if (n == 0) + break; /* Need more bytes. */ + + offs += n; + } + + /* Throw away the evaluated parts. */ + sshp_buf_drop(&ptl->rx.buf, offs); + } + + return 0; +} + +static void ssh_ptl_rx_wakeup(struct ssh_ptl *ptl) +{ + wake_up(&ptl->rx.wq); +} + +/** + * ssh_ptl_rx_start() - Start packet transport layer receiver thread. + * @ptl: The packet transport layer. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssh_ptl_rx_start(struct ssh_ptl *ptl) +{ + if (ptl->rx.thread) + return 0; + + ptl->rx.thread = kthread_run(ssh_ptl_rx_threadfn, ptl, + "ssam_serial_hub-rx"); + if (IS_ERR(ptl->rx.thread)) + return PTR_ERR(ptl->rx.thread); + + return 0; +} + +/** + * ssh_ptl_rx_stop() - Stop packet transport layer receiver thread. + * @ptl: The packet transport layer. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssh_ptl_rx_stop(struct ssh_ptl *ptl) +{ + int status = 0; + + if (ptl->rx.thread) { + status = kthread_stop(ptl->rx.thread); + ptl->rx.thread = NULL; + } + + return status; +} + +/** + * ssh_ptl_rx_rcvbuf() - Push data from lower-layer transport to the packet + * layer. + * @ptl: The packet transport layer. + * @buf: Pointer to the data to push to the layer. + * @n: Size of the data to push to the layer, in bytes. + * + * Pushes data from a lower-layer transport to the receiver fifo buffer of the + * packet layer and notifies the receiver thread. Calls to this function are + * ignored once the packet layer has been shut down. + * + * Return: Returns the number of bytes transferred (positive or zero) on + * success. Returns %-ESHUTDOWN if the packet layer has been shut down. + */ +ssize_t ssh_ptl_rx_rcvbuf(struct ssh_ptl *ptl, const u8 *buf, size_t n) +{ + size_t used; + + if (test_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state)) + return -ESHUTDOWN; + + used = kfifo_in(&ptl->rx.fifo, buf, n); + if (used) + ssh_ptl_rx_wakeup(ptl); + + return used; +} + +/** + * ssh_ptl_shutdown() - Shut down the packet transport layer. + * @ptl: The packet transport layer. + * + * Shuts down the packet transport layer, removing and canceling all queued + * and pending packets. Packets canceled by this operation will be completed + * with %-ESHUTDOWN as status. Receiver and transmitter threads will be + * stopped. + * + * As a result of this function, the transport layer will be marked as shut + * down. Submission of packets after the transport layer has been shut down + * will fail with %-ESHUTDOWN. + */ +void ssh_ptl_shutdown(struct ssh_ptl *ptl) +{ + LIST_HEAD(complete_q); + LIST_HEAD(complete_p); + struct ssh_packet *p, *n; + int status; + + /* Ensure that no new packets (including ACK/NAK) can be submitted. */ + set_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state); + /* + * Ensure that the layer gets marked as shut-down before actually + * stopping it. In combination with the check in ssh_ptl_queue_push(), + * this guarantees that no new packets can be added and all already + * queued packets are properly canceled. In combination with the check + * in ssh_ptl_rx_rcvbuf(), this guarantees that received data is + * properly cut off. + */ + smp_mb__after_atomic(); + + status = ssh_ptl_rx_stop(ptl); + if (status) + ptl_err(ptl, "ptl: failed to stop receiver thread\n"); + + status = ssh_ptl_tx_stop(ptl); + if (status) + ptl_err(ptl, "ptl: failed to stop transmitter thread\n"); + + cancel_delayed_work_sync(&ptl->rtx_timeout.reaper); + + /* + * At this point, all threads have been stopped. This means that the + * only references to packets from inside the system are in the queue + * and pending set. + * + * Note: We still need locks here because someone could still be + * canceling packets. + * + * Note 2: We can re-use queue_node (or pending_node) if we mark the + * packet as locked an then remove it from the queue (or pending set + * respectively). Marking the packet as locked avoids re-queuing + * (which should already be prevented by having stopped the treads...) + * and not setting QUEUED_BIT (or PENDING_BIT) prevents removal from a + * new list via other threads (e.g. cancellation). + * + * Note 3: There may be overlap between complete_p and complete_q. + * This is handled via test_and_set_bit() on the "completed" flag + * (also handles cancellation). + */ + + /* Mark queued packets as locked and move them to complete_q. */ + spin_lock(&ptl->queue.lock); + list_for_each_entry_safe(p, n, &ptl->queue.head, queue_node) { + set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state); + /* Ensure that state does not get zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_QUEUED_BIT, &p->state); + + list_move_tail(&p->queue_node, &complete_q); + } + spin_unlock(&ptl->queue.lock); + + /* Mark pending packets as locked and move them to complete_p. */ + spin_lock(&ptl->pending.lock); + list_for_each_entry_safe(p, n, &ptl->pending.head, pending_node) { + set_bit(SSH_PACKET_SF_LOCKED_BIT, &p->state); + /* Ensure that state does not get zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_PACKET_SF_PENDING_BIT, &p->state); + + list_move_tail(&p->pending_node, &complete_q); + } + atomic_set(&ptl->pending.count, 0); + spin_unlock(&ptl->pending.lock); + + /* Complete and drop packets on complete_q. */ + list_for_each_entry(p, &complete_q, queue_node) { + if (!test_and_set_bit(SSH_PACKET_SF_COMPLETED_BIT, &p->state)) + __ssh_ptl_complete(p, -ESHUTDOWN); + + ssh_packet_put(p); + } + + /* Complete and drop packets on complete_p. */ + list_for_each_entry(p, &complete_p, pending_node) { + if (!test_and_set_bit(SSH_PACKET_SF_COMPLETED_BIT, &p->state)) + __ssh_ptl_complete(p, -ESHUTDOWN); + + ssh_packet_put(p); + } + + /* + * At this point we have guaranteed that the system doesn't reference + * any packets any more. + */ +} + +/** + * ssh_ptl_init() - Initialize packet transport layer. + * @ptl: The packet transport layer to initialize. + * @serdev: The underlying serial device, i.e. the lower-level transport. + * @ops: Packet layer operations. + * + * Initializes the given packet transport layer. Transmitter and receiver + * threads must be started separately via ssh_ptl_tx_start() and + * ssh_ptl_rx_start(), after the packet-layer has been initialized and the + * lower-level transport layer has been set up. + * + * Return: Returns zero on success and a nonzero error code on failure. + */ +int ssh_ptl_init(struct ssh_ptl *ptl, struct serdev_device *serdev, + struct ssh_ptl_ops *ops) +{ + int i, status; + + ptl->serdev = serdev; + ptl->state = 0; + + spin_lock_init(&ptl->queue.lock); + INIT_LIST_HEAD(&ptl->queue.head); + + spin_lock_init(&ptl->pending.lock); + INIT_LIST_HEAD(&ptl->pending.head); + atomic_set_release(&ptl->pending.count, 0); + + ptl->tx.thread = NULL; + atomic_set(&ptl->tx.running, 0); + init_completion(&ptl->tx.thread_cplt_pkt); + init_completion(&ptl->tx.thread_cplt_tx); + init_waitqueue_head(&ptl->tx.packet_wq); + + ptl->rx.thread = NULL; + init_waitqueue_head(&ptl->rx.wq); + + spin_lock_init(&ptl->rtx_timeout.lock); + ptl->rtx_timeout.timeout = SSH_PTL_PACKET_TIMEOUT; + ptl->rtx_timeout.expires = KTIME_MAX; + INIT_DELAYED_WORK(&ptl->rtx_timeout.reaper, ssh_ptl_timeout_reap); + + ptl->ops = *ops; + + /* Initialize list of recent/blocked SEQs with invalid sequence IDs. */ + for (i = 0; i < ARRAY_SIZE(ptl->rx.blocked.seqs); i++) + ptl->rx.blocked.seqs[i] = U16_MAX; + ptl->rx.blocked.offset = 0; + + status = kfifo_alloc(&ptl->rx.fifo, SSH_PTL_RX_FIFO_LEN, GFP_KERNEL); + if (status) + return status; + + status = sshp_buf_alloc(&ptl->rx.buf, SSH_PTL_RX_BUF_LEN, GFP_KERNEL); + if (status) + kfifo_free(&ptl->rx.fifo); + + return status; +} + +/** + * ssh_ptl_destroy() - Deinitialize packet transport layer. + * @ptl: The packet transport layer to deinitialize. + * + * Deinitializes the given packet transport layer and frees resources + * associated with it. If receiver and/or transmitter threads have been + * started, the layer must first be shut down via ssh_ptl_shutdown() before + * this function can be called. + */ +void ssh_ptl_destroy(struct ssh_ptl *ptl) +{ + kfifo_free(&ptl->rx.fifo); + sshp_buf_free(&ptl->rx.buf); +} diff --git a/drivers/platform/surface/aggregator/ssh_packet_layer.h b/drivers/platform/surface/aggregator/ssh_packet_layer.h new file mode 100644 index 000000000..c80e82207 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_packet_layer.h @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * SSH packet transport layer. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H +#define _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "ssh_parser.h" + +/** + * enum ssh_ptl_state_flags - State-flags for &struct ssh_ptl. + * + * @SSH_PTL_SF_SHUTDOWN_BIT: + * Indicates that the packet transport layer has been shut down or is + * being shut down and should not accept any new packets/data. + */ +enum ssh_ptl_state_flags { + SSH_PTL_SF_SHUTDOWN_BIT, +}; + +/** + * struct ssh_ptl_ops - Callback operations for packet transport layer. + * @data_received: Function called when a data-packet has been received. Both, + * the packet layer on which the packet has been received and + * the packet's payload data are provided to this function. + */ +struct ssh_ptl_ops { + void (*data_received)(struct ssh_ptl *p, const struct ssam_span *data); +}; + +/** + * struct ssh_ptl - SSH packet transport layer. + * @serdev: Serial device providing the underlying data transport. + * @state: State(-flags) of the transport layer. + * @queue: Packet submission queue. + * @queue.lock: Lock for modifying the packet submission queue. + * @queue.head: List-head of the packet submission queue. + * @pending: Set/list of pending packets. + * @pending.lock: Lock for modifying the pending set. + * @pending.head: List-head of the pending set/list. + * @pending.count: Number of currently pending packets. + * @tx: Transmitter subsystem. + * @tx.running: Flag indicating (desired) transmitter thread state. + * @tx.thread: Transmitter thread. + * @tx.thread_cplt_tx: Completion for transmitter thread waiting on transfer. + * @tx.thread_cplt_pkt: Completion for transmitter thread waiting on packets. + * @tx.packet_wq: Waitqueue-head for packet transmit completion. + * @rx: Receiver subsystem. + * @rx.thread: Receiver thread. + * @rx.wq: Waitqueue-head for receiver thread. + * @rx.fifo: Buffer for receiving data/pushing data to receiver thread. + * @rx.buf: Buffer for evaluating data on receiver thread. + * @rx.blocked: List of recent/blocked sequence IDs to detect retransmission. + * @rx.blocked.seqs: Array of blocked sequence IDs. + * @rx.blocked.offset: Offset indicating where a new ID should be inserted. + * @rtx_timeout: Retransmission timeout subsystem. + * @rtx_timeout.lock: Lock for modifying the retransmission timeout reaper. + * @rtx_timeout.timeout: Timeout interval for retransmission. + * @rtx_timeout.expires: Time specifying when the reaper work is next scheduled. + * @rtx_timeout.reaper: Work performing timeout checks and subsequent actions. + * @ops: Packet layer operations. + */ +struct ssh_ptl { + struct serdev_device *serdev; + unsigned long state; + + struct { + spinlock_t lock; + struct list_head head; + } queue; + + struct { + spinlock_t lock; + struct list_head head; + atomic_t count; + } pending; + + struct { + atomic_t running; + struct task_struct *thread; + struct completion thread_cplt_tx; + struct completion thread_cplt_pkt; + struct wait_queue_head packet_wq; + } tx; + + struct { + struct task_struct *thread; + struct wait_queue_head wq; + struct kfifo fifo; + struct sshp_buf buf; + + struct { + u16 seqs[8]; + u16 offset; + } blocked; + } rx; + + struct { + spinlock_t lock; + ktime_t timeout; + ktime_t expires; + struct delayed_work reaper; + } rtx_timeout; + + struct ssh_ptl_ops ops; +}; + +#define __ssam_prcond(func, p, fmt, ...) \ + do { \ + typeof(p) __p = (p); \ + \ + if (__p) \ + func(__p, fmt, ##__VA_ARGS__); \ + } while (0) + +#define ptl_dbg(p, fmt, ...) dev_dbg(&(p)->serdev->dev, fmt, ##__VA_ARGS__) +#define ptl_info(p, fmt, ...) dev_info(&(p)->serdev->dev, fmt, ##__VA_ARGS__) +#define ptl_warn(p, fmt, ...) dev_warn(&(p)->serdev->dev, fmt, ##__VA_ARGS__) +#define ptl_err(p, fmt, ...) dev_err(&(p)->serdev->dev, fmt, ##__VA_ARGS__) +#define ptl_dbg_cond(p, fmt, ...) __ssam_prcond(ptl_dbg, p, fmt, ##__VA_ARGS__) + +#define to_ssh_ptl(ptr, member) \ + container_of(ptr, struct ssh_ptl, member) + +int ssh_ptl_init(struct ssh_ptl *ptl, struct serdev_device *serdev, + struct ssh_ptl_ops *ops); + +void ssh_ptl_destroy(struct ssh_ptl *ptl); + +/** + * ssh_ptl_get_device() - Get device associated with packet transport layer. + * @ptl: The packet transport layer. + * + * Return: Returns the device on which the given packet transport layer builds + * upon. + */ +static inline struct device *ssh_ptl_get_device(struct ssh_ptl *ptl) +{ + return ptl->serdev ? &ptl->serdev->dev : NULL; +} + +int ssh_ptl_tx_start(struct ssh_ptl *ptl); +int ssh_ptl_tx_stop(struct ssh_ptl *ptl); +int ssh_ptl_rx_start(struct ssh_ptl *ptl); +int ssh_ptl_rx_stop(struct ssh_ptl *ptl); +void ssh_ptl_shutdown(struct ssh_ptl *ptl); + +int ssh_ptl_submit(struct ssh_ptl *ptl, struct ssh_packet *p); +void ssh_ptl_cancel(struct ssh_packet *p); + +ssize_t ssh_ptl_rx_rcvbuf(struct ssh_ptl *ptl, const u8 *buf, size_t n); + +/** + * ssh_ptl_tx_wakeup_transfer() - Wake up packet transmitter thread for + * transfer. + * @ptl: The packet transport layer. + * + * Wakes up the packet transmitter thread, notifying it that the underlying + * transport has more space for data to be transmitted. If the packet + * transport layer has been shut down, calls to this function will be ignored. + */ +static inline void ssh_ptl_tx_wakeup_transfer(struct ssh_ptl *ptl) +{ + if (test_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state)) + return; + + complete(&ptl->tx.thread_cplt_tx); +} + +void ssh_packet_init(struct ssh_packet *packet, unsigned long type, + u8 priority, const struct ssh_packet_ops *ops); + +int ssh_ctrl_packet_cache_init(void); +void ssh_ctrl_packet_cache_destroy(void); + +#endif /* _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H */ diff --git a/drivers/platform/surface/aggregator/ssh_parser.c b/drivers/platform/surface/aggregator/ssh_parser.c new file mode 100644 index 000000000..6cfda85d3 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_parser.c @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * SSH message parser. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include + +#include +#include "ssh_parser.h" + +/** + * sshp_validate_crc() - Validate a CRC in raw message data. + * @src: The span of data over which the CRC should be computed. + * @crc: The pointer to the expected u16 CRC value. + * + * Computes the CRC of the provided data span (@src), compares it to the CRC + * stored at the given address (@crc), and returns the result of this + * comparison, i.e. %true if equal. This function is intended to run on raw + * input/message data. + * + * Return: Returns %true if the computed CRC matches the stored CRC, %false + * otherwise. + */ +static bool sshp_validate_crc(const struct ssam_span *src, const u8 *crc) +{ + u16 actual = ssh_crc(src->ptr, src->len); + u16 expected = get_unaligned_le16(crc); + + return actual == expected; +} + +/** + * sshp_starts_with_syn() - Check if the given data starts with SSH SYN bytes. + * @src: The data span to check the start of. + */ +static bool sshp_starts_with_syn(const struct ssam_span *src) +{ + return src->len >= 2 && get_unaligned_le16(src->ptr) == SSH_MSG_SYN; +} + +/** + * sshp_find_syn() - Find SSH SYN bytes in the given data span. + * @src: The data span to search in. + * @rem: The span (output) indicating the remaining data, starting with SSH + * SYN bytes, if found. + * + * Search for SSH SYN bytes in the given source span. If found, set the @rem + * span to the remaining data, starting with the first SYN bytes and capped by + * the source span length, and return %true. This function does not copy any + * data, but rather only sets pointers to the respective start addresses and + * length values. + * + * If no SSH SYN bytes could be found, set the @rem span to the zero-length + * span at the end of the source span and return %false. + * + * If partial SSH SYN bytes could be found at the end of the source span, set + * the @rem span to cover these partial SYN bytes, capped by the end of the + * source span, and return %false. This function should then be re-run once + * more data is available. + * + * Return: Returns %true if a complete SSH SYN sequence could be found, + * %false otherwise. + */ +bool sshp_find_syn(const struct ssam_span *src, struct ssam_span *rem) +{ + size_t i; + + for (i = 0; i < src->len - 1; i++) { + if (likely(get_unaligned_le16(src->ptr + i) == SSH_MSG_SYN)) { + rem->ptr = src->ptr + i; + rem->len = src->len - i; + return true; + } + } + + if (unlikely(src->ptr[src->len - 1] == (SSH_MSG_SYN & 0xff))) { + rem->ptr = src->ptr + src->len - 1; + rem->len = 1; + return false; + } + + rem->ptr = src->ptr + src->len; + rem->len = 0; + return false; +} + +/** + * sshp_parse_frame() - Parse SSH frame. + * @dev: The device used for logging. + * @source: The source to parse from. + * @frame: The parsed frame (output). + * @payload: The parsed payload (output). + * @maxlen: The maximum supported message length. + * + * Parses and validates a SSH frame, including its payload, from the given + * source. Sets the provided @frame pointer to the start of the frame and + * writes the limits of the frame payload to the provided @payload span + * pointer. + * + * This function does not copy any data, but rather only validates the message + * data and sets pointers (and length values) to indicate the respective parts. + * + * If no complete SSH frame could be found, the frame pointer will be set to + * the %NULL pointer and the payload span will be set to the null span (start + * pointer %NULL, size zero). + * + * Return: Returns zero on success or if the frame is incomplete, %-ENOMSG if + * the start of the message is invalid, %-EBADMSG if any (frame-header or + * payload) CRC is invalid, or %-EMSGSIZE if the SSH message is bigger than + * the maximum message length specified in the @maxlen parameter. + */ +int sshp_parse_frame(const struct device *dev, const struct ssam_span *source, + struct ssh_frame **frame, struct ssam_span *payload, + size_t maxlen) +{ + struct ssam_span sf; + struct ssam_span sp; + + /* Initialize output. */ + *frame = NULL; + payload->ptr = NULL; + payload->len = 0; + + if (!sshp_starts_with_syn(source)) { + dev_warn(dev, "rx: parser: invalid start of frame\n"); + return -ENOMSG; + } + + /* Check for minimum packet length. */ + if (unlikely(source->len < SSH_MESSAGE_LENGTH(0))) { + dev_dbg(dev, "rx: parser: not enough data for frame\n"); + return 0; + } + + /* Pin down frame. */ + sf.ptr = source->ptr + sizeof(u16); + sf.len = sizeof(struct ssh_frame); + + /* Validate frame CRC. */ + if (unlikely(!sshp_validate_crc(&sf, sf.ptr + sf.len))) { + dev_warn(dev, "rx: parser: invalid frame CRC\n"); + return -EBADMSG; + } + + /* Ensure packet does not exceed maximum length. */ + sp.len = get_unaligned_le16(&((struct ssh_frame *)sf.ptr)->len); + if (unlikely(SSH_MESSAGE_LENGTH(sp.len) > maxlen)) { + dev_warn(dev, "rx: parser: frame too large: %llu bytes\n", + SSH_MESSAGE_LENGTH(sp.len)); + return -EMSGSIZE; + } + + /* Pin down payload. */ + sp.ptr = sf.ptr + sf.len + sizeof(u16); + + /* Check for frame + payload length. */ + if (source->len < SSH_MESSAGE_LENGTH(sp.len)) { + dev_dbg(dev, "rx: parser: not enough data for payload\n"); + return 0; + } + + /* Validate payload CRC. */ + if (unlikely(!sshp_validate_crc(&sp, sp.ptr + sp.len))) { + dev_warn(dev, "rx: parser: invalid payload CRC\n"); + return -EBADMSG; + } + + *frame = (struct ssh_frame *)sf.ptr; + *payload = sp; + + dev_dbg(dev, "rx: parser: valid frame found (type: %#04x, len: %u)\n", + (*frame)->type, (*frame)->len); + + return 0; +} + +/** + * sshp_parse_command() - Parse SSH command frame payload. + * @dev: The device used for logging. + * @source: The source to parse from. + * @command: The parsed command (output). + * @command_data: The parsed command data/payload (output). + * + * Parses and validates a SSH command frame payload. Sets the @command pointer + * to the command header and the @command_data span to the command data (i.e. + * payload of the command). This will result in a zero-length span if the + * command does not have any associated data/payload. This function does not + * check the frame-payload-type field, which should be checked by the caller + * before calling this function. + * + * The @source parameter should be the complete frame payload, e.g. returned + * by the sshp_parse_frame() command. + * + * This function does not copy any data, but rather only validates the frame + * payload data and sets pointers (and length values) to indicate the + * respective parts. + * + * Return: Returns zero on success or %-ENOMSG if @source does not represent a + * valid command-type frame payload, i.e. is too short. + */ +int sshp_parse_command(const struct device *dev, const struct ssam_span *source, + struct ssh_command **command, + struct ssam_span *command_data) +{ + /* Check for minimum length. */ + if (unlikely(source->len < sizeof(struct ssh_command))) { + *command = NULL; + command_data->ptr = NULL; + command_data->len = 0; + + dev_err(dev, "rx: parser: command payload is too short\n"); + return -ENOMSG; + } + + *command = (struct ssh_command *)source->ptr; + command_data->ptr = source->ptr + sizeof(struct ssh_command); + command_data->len = source->len - sizeof(struct ssh_command); + + dev_dbg(dev, "rx: parser: valid command found (tc: %#04x, cid: %#04x)\n", + (*command)->tc, (*command)->cid); + + return 0; +} diff --git a/drivers/platform/surface/aggregator/ssh_parser.h b/drivers/platform/surface/aggregator/ssh_parser.h new file mode 100644 index 000000000..801d8fa69 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_parser.h @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * SSH message parser. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_SSH_PARSER_H +#define _SURFACE_AGGREGATOR_SSH_PARSER_H + +#include +#include +#include +#include + +#include + +/** + * struct sshp_buf - Parser buffer for SSH messages. + * @ptr: Pointer to the beginning of the buffer. + * @len: Number of bytes used in the buffer. + * @cap: Maximum capacity of the buffer. + */ +struct sshp_buf { + u8 *ptr; + size_t len; + size_t cap; +}; + +/** + * sshp_buf_init() - Initialize a SSH parser buffer. + * @buf: The buffer to initialize. + * @ptr: The memory backing the buffer. + * @cap: The length of the memory backing the buffer, i.e. its capacity. + * + * Initializes the buffer with the given memory as backing and set its used + * length to zero. + */ +static inline void sshp_buf_init(struct sshp_buf *buf, u8 *ptr, size_t cap) +{ + buf->ptr = ptr; + buf->len = 0; + buf->cap = cap; +} + +/** + * sshp_buf_alloc() - Allocate and initialize a SSH parser buffer. + * @buf: The buffer to initialize/allocate to. + * @cap: The desired capacity of the buffer. + * @flags: The flags used for allocating the memory. + * + * Allocates @cap bytes and initializes the provided buffer struct with the + * allocated memory. + * + * Return: Returns zero on success and %-ENOMEM if allocation failed. + */ +static inline int sshp_buf_alloc(struct sshp_buf *buf, size_t cap, gfp_t flags) +{ + u8 *ptr; + + ptr = kzalloc(cap, flags); + if (!ptr) + return -ENOMEM; + + sshp_buf_init(buf, ptr, cap); + return 0; +} + +/** + * sshp_buf_free() - Free a SSH parser buffer. + * @buf: The buffer to free. + * + * Frees a SSH parser buffer by freeing the memory backing it and then + * resetting its pointer to %NULL and length and capacity to zero. Intended to + * free a buffer previously allocated with sshp_buf_alloc(). + */ +static inline void sshp_buf_free(struct sshp_buf *buf) +{ + kfree(buf->ptr); + buf->ptr = NULL; + buf->len = 0; + buf->cap = 0; +} + +/** + * sshp_buf_drop() - Drop data from the beginning of the buffer. + * @buf: The buffer to drop data from. + * @n: The number of bytes to drop. + * + * Drops the first @n bytes from the buffer. Re-aligns any remaining data to + * the beginning of the buffer. + */ +static inline void sshp_buf_drop(struct sshp_buf *buf, size_t n) +{ + memmove(buf->ptr, buf->ptr + n, buf->len - n); + buf->len -= n; +} + +/** + * sshp_buf_read_from_fifo() - Transfer data from a fifo to the buffer. + * @buf: The buffer to write the data into. + * @fifo: The fifo to read the data from. + * + * Transfers the data contained in the fifo to the buffer, removing it from + * the fifo. This function will try to transfer as much data as possible, + * limited either by the remaining space in the buffer or by the number of + * bytes available in the fifo. + * + * Return: Returns the number of bytes transferred. + */ +static inline size_t sshp_buf_read_from_fifo(struct sshp_buf *buf, + struct kfifo *fifo) +{ + size_t n; + + n = kfifo_out(fifo, buf->ptr + buf->len, buf->cap - buf->len); + buf->len += n; + + return n; +} + +/** + * sshp_buf_span_from() - Initialize a span from the given buffer and offset. + * @buf: The buffer to create the span from. + * @offset: The offset in the buffer at which the span should start. + * @span: The span to initialize (output). + * + * Initializes the provided span to point to the memory at the given offset in + * the buffer, with the length of the span being capped by the number of bytes + * used in the buffer after the offset (i.e. bytes remaining after the + * offset). + * + * Warning: This function does not validate that @offset is less than or equal + * to the number of bytes used in the buffer or the buffer capacity. This must + * be guaranteed by the caller. + */ +static inline void sshp_buf_span_from(struct sshp_buf *buf, size_t offset, + struct ssam_span *span) +{ + span->ptr = buf->ptr + offset; + span->len = buf->len - offset; +} + +bool sshp_find_syn(const struct ssam_span *src, struct ssam_span *rem); + +int sshp_parse_frame(const struct device *dev, const struct ssam_span *source, + struct ssh_frame **frame, struct ssam_span *payload, + size_t maxlen); + +int sshp_parse_command(const struct device *dev, const struct ssam_span *source, + struct ssh_command **command, + struct ssam_span *command_data); + +#endif /* _SURFACE_AGGREGATOR_SSH_PARSER_h */ diff --git a/drivers/platform/surface/aggregator/ssh_request_layer.c b/drivers/platform/surface/aggregator/ssh_request_layer.c new file mode 100644 index 000000000..a356e4956 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_request_layer.c @@ -0,0 +1,1274 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * SSH request transport layer. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ssh_packet_layer.h" +#include "ssh_request_layer.h" + +#include "trace.h" + +/* + * SSH_RTL_REQUEST_TIMEOUT - Request timeout. + * + * Timeout as ktime_t delta for request responses. If we have not received a + * response in this time-frame after finishing the underlying packet + * transmission, the request will be completed with %-ETIMEDOUT as status + * code. + */ +#define SSH_RTL_REQUEST_TIMEOUT ms_to_ktime(3000) + +/* + * SSH_RTL_REQUEST_TIMEOUT_RESOLUTION - Request timeout granularity. + * + * Time-resolution for timeouts. Should be larger than one jiffy to avoid + * direct re-scheduling of reaper work_struct. + */ +#define SSH_RTL_REQUEST_TIMEOUT_RESOLUTION ms_to_ktime(max(2000 / HZ, 50)) + +/* + * SSH_RTL_MAX_PENDING - Maximum number of pending requests. + * + * Maximum number of requests concurrently waiting to be completed (i.e. + * waiting for the corresponding packet transmission to finish if they don't + * have a response or waiting for a response if they have one). + */ +#define SSH_RTL_MAX_PENDING 3 + +/* + * SSH_RTL_TX_BATCH - Maximum number of requests processed per work execution. + * Used to prevent livelocking of the workqueue. Value chosen via educated + * guess, may be adjusted. + */ +#define SSH_RTL_TX_BATCH 10 + +#ifdef CONFIG_SURFACE_AGGREGATOR_ERROR_INJECTION + +/** + * ssh_rtl_should_drop_response() - Error injection hook to drop request + * responses. + * + * Useful to cause request transmission timeouts in the driver by dropping the + * response to a request. + */ +static noinline bool ssh_rtl_should_drop_response(void) +{ + return false; +} +ALLOW_ERROR_INJECTION(ssh_rtl_should_drop_response, TRUE); + +#else + +static inline bool ssh_rtl_should_drop_response(void) +{ + return false; +} + +#endif + +static u16 ssh_request_get_rqid(struct ssh_request *rqst) +{ + return get_unaligned_le16(rqst->packet.data.ptr + + SSH_MSGOFFSET_COMMAND(rqid)); +} + +static u32 ssh_request_get_rqid_safe(struct ssh_request *rqst) +{ + if (!rqst->packet.data.ptr) + return U32_MAX; + + return ssh_request_get_rqid(rqst); +} + +static void ssh_rtl_queue_remove(struct ssh_request *rqst) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + spin_lock(&rtl->queue.lock); + + if (!test_and_clear_bit(SSH_REQUEST_SF_QUEUED_BIT, &rqst->state)) { + spin_unlock(&rtl->queue.lock); + return; + } + + list_del(&rqst->node); + + spin_unlock(&rtl->queue.lock); + ssh_request_put(rqst); +} + +static bool ssh_rtl_queue_empty(struct ssh_rtl *rtl) +{ + bool empty; + + spin_lock(&rtl->queue.lock); + empty = list_empty(&rtl->queue.head); + spin_unlock(&rtl->queue.lock); + + return empty; +} + +static void ssh_rtl_pending_remove(struct ssh_request *rqst) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + spin_lock(&rtl->pending.lock); + + if (!test_and_clear_bit(SSH_REQUEST_SF_PENDING_BIT, &rqst->state)) { + spin_unlock(&rtl->pending.lock); + return; + } + + atomic_dec(&rtl->pending.count); + list_del(&rqst->node); + + spin_unlock(&rtl->pending.lock); + + ssh_request_put(rqst); +} + +static int ssh_rtl_tx_pending_push(struct ssh_request *rqst) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + spin_lock(&rtl->pending.lock); + + if (test_bit(SSH_REQUEST_SF_LOCKED_BIT, &rqst->state)) { + spin_unlock(&rtl->pending.lock); + return -EINVAL; + } + + if (test_and_set_bit(SSH_REQUEST_SF_PENDING_BIT, &rqst->state)) { + spin_unlock(&rtl->pending.lock); + return -EALREADY; + } + + atomic_inc(&rtl->pending.count); + list_add_tail(&ssh_request_get(rqst)->node, &rtl->pending.head); + + spin_unlock(&rtl->pending.lock); + return 0; +} + +static void ssh_rtl_complete_with_status(struct ssh_request *rqst, int status) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + trace_ssam_request_complete(rqst, status); + + /* rtl/ptl may not be set if we're canceling before submitting. */ + rtl_dbg_cond(rtl, "rtl: completing request (rqid: %#06x, status: %d)\n", + ssh_request_get_rqid_safe(rqst), status); + + rqst->ops->complete(rqst, NULL, NULL, status); +} + +static void ssh_rtl_complete_with_rsp(struct ssh_request *rqst, + const struct ssh_command *cmd, + const struct ssam_span *data) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + trace_ssam_request_complete(rqst, 0); + + rtl_dbg(rtl, "rtl: completing request with response (rqid: %#06x)\n", + ssh_request_get_rqid(rqst)); + + rqst->ops->complete(rqst, cmd, data, 0); +} + +static bool ssh_rtl_tx_can_process(struct ssh_request *rqst) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + + if (test_bit(SSH_REQUEST_TY_FLUSH_BIT, &rqst->state)) + return !atomic_read(&rtl->pending.count); + + return atomic_read(&rtl->pending.count) < SSH_RTL_MAX_PENDING; +} + +static struct ssh_request *ssh_rtl_tx_next(struct ssh_rtl *rtl) +{ + struct ssh_request *rqst = ERR_PTR(-ENOENT); + struct ssh_request *p, *n; + + spin_lock(&rtl->queue.lock); + + /* Find first non-locked request and remove it. */ + list_for_each_entry_safe(p, n, &rtl->queue.head, node) { + if (unlikely(test_bit(SSH_REQUEST_SF_LOCKED_BIT, &p->state))) + continue; + + if (!ssh_rtl_tx_can_process(p)) { + rqst = ERR_PTR(-EBUSY); + break; + } + + /* Remove from queue and mark as transmitting. */ + set_bit(SSH_REQUEST_SF_TRANSMITTING_BIT, &p->state); + /* Ensure state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_REQUEST_SF_QUEUED_BIT, &p->state); + + list_del(&p->node); + + rqst = p; + break; + } + + spin_unlock(&rtl->queue.lock); + return rqst; +} + +static int ssh_rtl_tx_try_process_one(struct ssh_rtl *rtl) +{ + struct ssh_request *rqst; + int status; + + /* Get and prepare next request for transmit. */ + rqst = ssh_rtl_tx_next(rtl); + if (IS_ERR(rqst)) + return PTR_ERR(rqst); + + /* Add it to/mark it as pending. */ + status = ssh_rtl_tx_pending_push(rqst); + if (status) { + ssh_request_put(rqst); + return -EAGAIN; + } + + /* Submit packet. */ + status = ssh_ptl_submit(&rtl->ptl, &rqst->packet); + if (status == -ESHUTDOWN) { + /* + * Packet has been refused due to the packet layer shutting + * down. Complete it here. + */ + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &rqst->state); + /* + * Note: A barrier is not required here, as there are only two + * references in the system at this point: The one that we have, + * and the other one that belongs to the pending set. Due to the + * request being marked as "transmitting", our process is the + * only one allowed to remove the pending node and change the + * state. Normally, the task would fall to the packet callback, + * but as this is a path where submission failed, this callback + * will never be executed. + */ + + ssh_rtl_pending_remove(rqst); + ssh_rtl_complete_with_status(rqst, -ESHUTDOWN); + + ssh_request_put(rqst); + return -ESHUTDOWN; + + } else if (status) { + /* + * If submitting the packet failed and the packet layer isn't + * shutting down, the packet has either been submitted/queued + * before (-EALREADY, which cannot happen as we have + * guaranteed that requests cannot be re-submitted), or the + * packet was marked as locked (-EINVAL). To mark the packet + * locked at this stage, the request, and thus the packets + * itself, had to have been canceled. Simply drop the + * reference. Cancellation itself will remove it from the set + * of pending requests. + */ + + WARN_ON(status != -EINVAL); + + ssh_request_put(rqst); + return -EAGAIN; + } + + ssh_request_put(rqst); + return 0; +} + +static bool ssh_rtl_tx_schedule(struct ssh_rtl *rtl) +{ + if (atomic_read(&rtl->pending.count) >= SSH_RTL_MAX_PENDING) + return false; + + if (ssh_rtl_queue_empty(rtl)) + return false; + + return schedule_work(&rtl->tx.work); +} + +static void ssh_rtl_tx_work_fn(struct work_struct *work) +{ + struct ssh_rtl *rtl = to_ssh_rtl(work, tx.work); + unsigned int iterations = SSH_RTL_TX_BATCH; + int status; + + /* + * Try to be nice and not block/live-lock the workqueue: Run a maximum + * of 10 tries, then re-submit if necessary. This should not be + * necessary for normal execution, but guarantee it anyway. + */ + do { + status = ssh_rtl_tx_try_process_one(rtl); + if (status == -ENOENT || status == -EBUSY) + return; /* No more requests to process. */ + + if (status == -ESHUTDOWN) { + /* + * Packet system shutting down. No new packets can be + * transmitted. Return silently, the party initiating + * the shutdown should handle the rest. + */ + return; + } + + WARN_ON(status != 0 && status != -EAGAIN); + } while (--iterations); + + /* Out of tries, reschedule. */ + ssh_rtl_tx_schedule(rtl); +} + +/** + * ssh_rtl_submit() - Submit a request to the transport layer. + * @rtl: The request transport layer. + * @rqst: The request to submit. + * + * Submits a request to the transport layer. A single request may not be + * submitted multiple times without reinitializing it. + * + * Return: Returns zero on success, %-EINVAL if the request type is invalid or + * the request has been canceled prior to submission, %-EALREADY if the + * request has already been submitted, or %-ESHUTDOWN in case the request + * transport layer has been shut down. + */ +int ssh_rtl_submit(struct ssh_rtl *rtl, struct ssh_request *rqst) +{ + trace_ssam_request_submit(rqst); + + /* + * Ensure that requests expecting a response are sequenced. If this + * invariant ever changes, see the comment in ssh_rtl_complete() on what + * is required to be changed in the code. + */ + if (test_bit(SSH_REQUEST_TY_HAS_RESPONSE_BIT, &rqst->state)) + if (!test_bit(SSH_PACKET_TY_SEQUENCED_BIT, &rqst->packet.state)) + return -EINVAL; + + spin_lock(&rtl->queue.lock); + + /* + * Try to set ptl and check if this request has already been submitted. + * + * Must be inside lock as we might run into a lost update problem + * otherwise: If this were outside of the lock, cancellation in + * ssh_rtl_cancel_nonpending() may run after we've set the ptl + * reference but before we enter the lock. In that case, we'd detect + * that the request is being added to the queue and would try to remove + * it from that, but removal might fail because it hasn't actually been + * added yet. By putting this cmpxchg in the critical section, we + * ensure that the queuing detection only triggers when we are already + * in the critical section and the remove process will wait until the + * push operation has been completed (via lock) due to that. Only then, + * we can safely try to remove it. + */ + if (cmpxchg(&rqst->packet.ptl, NULL, &rtl->ptl)) { + spin_unlock(&rtl->queue.lock); + return -EALREADY; + } + + /* + * Ensure that we set ptl reference before we continue modifying state. + * This is required for non-pending cancellation. This barrier is paired + * with the one in ssh_rtl_cancel_nonpending(). + * + * By setting the ptl reference before we test for "locked", we can + * check if the "locked" test may have already run. See comments in + * ssh_rtl_cancel_nonpending() for more detail. + */ + smp_mb__after_atomic(); + + if (test_bit(SSH_RTL_SF_SHUTDOWN_BIT, &rtl->state)) { + spin_unlock(&rtl->queue.lock); + return -ESHUTDOWN; + } + + if (test_bit(SSH_REQUEST_SF_LOCKED_BIT, &rqst->state)) { + spin_unlock(&rtl->queue.lock); + return -EINVAL; + } + + set_bit(SSH_REQUEST_SF_QUEUED_BIT, &rqst->state); + list_add_tail(&ssh_request_get(rqst)->node, &rtl->queue.head); + + spin_unlock(&rtl->queue.lock); + + ssh_rtl_tx_schedule(rtl); + return 0; +} + +static void ssh_rtl_timeout_reaper_mod(struct ssh_rtl *rtl, ktime_t now, + ktime_t expires) +{ + unsigned long delta = msecs_to_jiffies(ktime_ms_delta(expires, now)); + ktime_t aexp = ktime_add(expires, SSH_RTL_REQUEST_TIMEOUT_RESOLUTION); + + spin_lock(&rtl->rtx_timeout.lock); + + /* Re-adjust / schedule reaper only if it is above resolution delta. */ + if (ktime_before(aexp, rtl->rtx_timeout.expires)) { + rtl->rtx_timeout.expires = expires; + mod_delayed_work(system_percpu_wq, &rtl->rtx_timeout.reaper, delta); + } + + spin_unlock(&rtl->rtx_timeout.lock); +} + +static void ssh_rtl_timeout_start(struct ssh_request *rqst) +{ + struct ssh_rtl *rtl = ssh_request_rtl(rqst); + ktime_t timestamp = ktime_get_coarse_boottime(); + ktime_t timeout = rtl->rtx_timeout.timeout; + + if (test_bit(SSH_REQUEST_SF_LOCKED_BIT, &rqst->state)) + return; + + /* + * Note: The timestamp gets set only once. This happens on the packet + * callback. All other access to it is read-only. + */ + WRITE_ONCE(rqst->timestamp, timestamp); + /* + * Ensure timestamp is set before starting the reaper. Paired with + * implicit barrier following check on ssh_request_get_expiration() in + * ssh_rtl_timeout_reap. + */ + smp_mb__after_atomic(); + + ssh_rtl_timeout_reaper_mod(rtl, timestamp, timestamp + timeout); +} + +static void ssh_rtl_complete(struct ssh_rtl *rtl, + const struct ssh_command *command, + const struct ssam_span *command_data) +{ + struct ssh_request *r = NULL; + struct ssh_request *p, *n; + u16 rqid = get_unaligned_le16(&command->rqid); + + trace_ssam_rx_response_received(command, command_data->len); + + /* + * Get request from pending based on request ID and mark it as response + * received and locked. + */ + spin_lock(&rtl->pending.lock); + list_for_each_entry_safe(p, n, &rtl->pending.head, node) { + /* We generally expect requests to be processed in order. */ + if (unlikely(ssh_request_get_rqid(p) != rqid)) + continue; + + /* Simulate response timeout. */ + if (ssh_rtl_should_drop_response()) { + spin_unlock(&rtl->pending.lock); + + trace_ssam_ei_rx_drop_response(p); + rtl_info(rtl, "request error injection: dropping response for request %p\n", + &p->packet); + return; + } + + /* + * Mark as "response received" and "locked" as we're going to + * complete it. + */ + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &p->state); + set_bit(SSH_REQUEST_SF_RSPRCVD_BIT, &p->state); + /* Ensure state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_REQUEST_SF_PENDING_BIT, &p->state); + + atomic_dec(&rtl->pending.count); + list_del(&p->node); + + r = p; + break; + } + spin_unlock(&rtl->pending.lock); + + if (!r) { + rtl_warn(rtl, "rtl: dropping unexpected command message (rqid = %#06x)\n", + rqid); + return; + } + + /* If the request hasn't been completed yet, we will do this now. */ + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) { + ssh_request_put(r); + ssh_rtl_tx_schedule(rtl); + return; + } + + /* + * Make sure the request has been transmitted. In case of a sequenced + * request, we are guaranteed that the completion callback will run on + * the receiver thread directly when the ACK for the packet has been + * received. Similarly, this function is guaranteed to run on the + * receiver thread. Thus we are guaranteed that if the packet has been + * successfully transmitted and received an ACK, the transmitted flag + * has been set and is visible here. + * + * We are currently not handling unsequenced packets here, as those + * should never expect a response as ensured in ssh_rtl_submit. If this + * ever changes, one would have to test for + * + * (r->state & (transmitting | transmitted)) + * + * on unsequenced packets to determine if they could have been + * transmitted. There are no synchronization guarantees as in the + * sequenced case, since, in this case, the callback function will not + * run on the same thread. Thus an exact determination is impossible. + */ + if (!test_bit(SSH_REQUEST_SF_TRANSMITTED_BIT, &r->state)) { + rtl_err(rtl, "rtl: received response before ACK for request (rqid = %#06x)\n", + rqid); + + /* + * NB: Timeout has already been canceled, request already been + * removed from pending and marked as locked and completed. As + * we receive a "false" response, the packet might still be + * queued though. + */ + ssh_rtl_queue_remove(r); + + ssh_rtl_complete_with_status(r, -EREMOTEIO); + ssh_request_put(r); + + ssh_rtl_tx_schedule(rtl); + return; + } + + /* + * NB: Timeout has already been canceled, request already been + * removed from pending and marked as locked and completed. The request + * can also not be queued any more, as it has been marked as + * transmitting and later transmitted. Thus no need to remove it from + * anywhere. + */ + + ssh_rtl_complete_with_rsp(r, command, command_data); + ssh_request_put(r); + + ssh_rtl_tx_schedule(rtl); +} + +static bool ssh_rtl_cancel_nonpending(struct ssh_request *r) +{ + struct ssh_rtl *rtl; + unsigned long flags, fixed; + bool remove; + + /* + * Handle unsubmitted request: Try to mark the packet as locked, + * expecting the state to be zero (i.e. unsubmitted). Note that, if + * setting the state worked, we might still be adding the packet to the + * queue in a currently executing submit call. In that case, however, + * ptl reference must have been set previously, as locked is checked + * after setting ptl. Furthermore, when the ptl reference is set, the + * submission process is guaranteed to have entered the critical + * section. Thus only if we successfully locked this request and ptl is + * NULL, we have successfully removed the request, i.e. we are + * guaranteed that, due to the "locked" check in ssh_rtl_submit(), the + * packet will never be added. Otherwise, we need to try and grab it + * from the queue, where we are now guaranteed that the packet is or has + * been due to the critical section. + * + * Note that if the cmpxchg() fails, we are guaranteed that ptl has + * been set and is non-NULL, as states can only be nonzero after this + * has been set. Also note that we need to fetch the static (type) + * flags to ensure that they don't cause the cmpxchg() to fail. + */ + fixed = READ_ONCE(r->state) & SSH_REQUEST_FLAGS_TY_MASK; + flags = cmpxchg(&r->state, fixed, SSH_REQUEST_SF_LOCKED_BIT); + + /* + * Force correct ordering with regards to state and ptl reference access + * to safe-guard cancellation to concurrent submission against a + * lost-update problem. First try to exchange state, then also check + * ptl if that worked. This barrier is paired with the + * one in ssh_rtl_submit(). + */ + smp_mb__after_atomic(); + + if (flags == fixed && !READ_ONCE(r->packet.ptl)) { + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return true; + + ssh_rtl_complete_with_status(r, -ECANCELED); + return true; + } + + rtl = ssh_request_rtl(r); + spin_lock(&rtl->queue.lock); + + /* + * Note: 1) Requests cannot be re-submitted. 2) If a request is + * queued, it cannot be "transmitting"/"pending" yet. Thus, if we + * successfully remove the request here, we have removed all its + * occurrences in the system. + */ + + remove = test_and_clear_bit(SSH_REQUEST_SF_QUEUED_BIT, &r->state); + if (!remove) { + spin_unlock(&rtl->queue.lock); + return false; + } + + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state); + list_del(&r->node); + + spin_unlock(&rtl->queue.lock); + + ssh_request_put(r); /* Drop reference obtained from queue. */ + + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return true; + + ssh_rtl_complete_with_status(r, -ECANCELED); + return true; +} + +static bool ssh_rtl_cancel_pending(struct ssh_request *r) +{ + /* If the packet is already locked, it's going to be removed shortly. */ + if (test_and_set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state)) + return true; + + /* + * Now that we have locked the packet, we have guaranteed that it can't + * be added to the system any more. If ptl is NULL, the locked + * check in ssh_rtl_submit() has not been run and any submission, + * currently in progress or called later, won't add the packet. Thus we + * can directly complete it. + * + * The implicit memory barrier of test_and_set_bit() should be enough + * to ensure that the correct order (first lock, then check ptl) is + * ensured. This is paired with the barrier in ssh_rtl_submit(). + */ + if (!READ_ONCE(r->packet.ptl)) { + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return true; + + ssh_rtl_complete_with_status(r, -ECANCELED); + return true; + } + + /* + * Try to cancel the packet. If the packet has not been completed yet, + * this will subsequently (and synchronously) call the completion + * callback of the packet, which will complete the request. + */ + ssh_ptl_cancel(&r->packet); + + /* + * If the packet has been completed with success, i.e. has not been + * canceled by the above call, the request may not have been completed + * yet (may be waiting for a response). Check if we need to do this + * here. + */ + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return true; + + ssh_rtl_queue_remove(r); + ssh_rtl_pending_remove(r); + ssh_rtl_complete_with_status(r, -ECANCELED); + + return true; +} + +/** + * ssh_rtl_cancel() - Cancel request. + * @rqst: The request to cancel. + * @pending: Whether to also cancel pending requests. + * + * Cancels the given request. If @pending is %false, this will not cancel + * pending requests, i.e. requests that have already been submitted to the + * packet layer but not been completed yet. If @pending is %true, this will + * cancel the given request regardless of the state it is in. + * + * If the request has been canceled by calling this function, both completion + * and release callbacks of the request will be executed in a reasonable + * time-frame. This may happen during execution of this function, however, + * there is no guarantee for this. For example, a request currently + * transmitting will be canceled/completed only after transmission has + * completed, and the respective callbacks will be executed on the transmitter + * thread, which may happen during, but also some time after execution of the + * cancel function. + * + * Return: Returns %true if the given request has been canceled or completed, + * either by this function or prior to calling this function, %false + * otherwise. If @pending is %true, this function will always return %true. + */ +bool ssh_rtl_cancel(struct ssh_request *rqst, bool pending) +{ + struct ssh_rtl *rtl; + bool canceled; + + if (test_and_set_bit(SSH_REQUEST_SF_CANCELED_BIT, &rqst->state)) + return true; + + trace_ssam_request_cancel(rqst); + + if (pending) + canceled = ssh_rtl_cancel_pending(rqst); + else + canceled = ssh_rtl_cancel_nonpending(rqst); + + /* Note: rtl may be NULL if request has not been submitted yet. */ + rtl = ssh_request_rtl(rqst); + if (canceled && rtl) + ssh_rtl_tx_schedule(rtl); + + return canceled; +} + +static void ssh_rtl_packet_callback(struct ssh_packet *p, int status) +{ + struct ssh_request *r = to_ssh_request(p); + + if (unlikely(status)) { + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state); + + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return; + + /* + * The packet may get canceled even though it has not been + * submitted yet. The request may still be queued. Check the + * queue and remove it if necessary. As the timeout would have + * been started in this function on success, there's no need + * to cancel it here. + */ + ssh_rtl_queue_remove(r); + ssh_rtl_pending_remove(r); + ssh_rtl_complete_with_status(r, status); + + ssh_rtl_tx_schedule(ssh_request_rtl(r)); + return; + } + + /* Update state: Mark as transmitted and clear transmitting. */ + set_bit(SSH_REQUEST_SF_TRANSMITTED_BIT, &r->state); + /* Ensure state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_REQUEST_SF_TRANSMITTING_BIT, &r->state); + + /* If we expect a response, we just need to start the timeout. */ + if (test_bit(SSH_REQUEST_TY_HAS_RESPONSE_BIT, &r->state)) { + /* + * Note: This is the only place where the timestamp gets set, + * all other access to it is read-only. + */ + ssh_rtl_timeout_start(r); + return; + } + + /* + * If we don't expect a response, lock, remove, and complete the + * request. Note that, at this point, the request is guaranteed to have + * left the queue and no timeout has been started. Thus we only need to + * remove it from pending. If the request has already been completed (it + * may have been canceled) return. + */ + + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state); + if (test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + return; + + ssh_rtl_pending_remove(r); + ssh_rtl_complete_with_status(r, 0); + + ssh_rtl_tx_schedule(ssh_request_rtl(r)); +} + +static ktime_t ssh_request_get_expiration(struct ssh_request *r, ktime_t timeout) +{ + ktime_t timestamp = READ_ONCE(r->timestamp); + + if (timestamp != KTIME_MAX) + return ktime_add(timestamp, timeout); + else + return KTIME_MAX; +} + +static void ssh_rtl_timeout_reap(struct work_struct *work) +{ + struct ssh_rtl *rtl = to_ssh_rtl(work, rtx_timeout.reaper.work); + struct ssh_request *r, *n; + LIST_HEAD(claimed); + ktime_t now = ktime_get_coarse_boottime(); + ktime_t timeout = rtl->rtx_timeout.timeout; + ktime_t next = KTIME_MAX; + + trace_ssam_rtl_timeout_reap(atomic_read(&rtl->pending.count)); + + /* + * Mark reaper as "not pending". This is done before checking any + * requests to avoid lost-update type problems. + */ + spin_lock(&rtl->rtx_timeout.lock); + rtl->rtx_timeout.expires = KTIME_MAX; + spin_unlock(&rtl->rtx_timeout.lock); + + spin_lock(&rtl->pending.lock); + list_for_each_entry_safe(r, n, &rtl->pending.head, node) { + ktime_t expires = ssh_request_get_expiration(r, timeout); + + /* + * Check if the timeout hasn't expired yet. Find out next + * expiration date to be handled after this run. + */ + if (ktime_after(expires, now)) { + next = ktime_before(expires, next) ? expires : next; + continue; + } + + /* Avoid further transitions if locked. */ + if (test_and_set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state)) + continue; + + /* + * We have now marked the packet as locked. Thus it cannot be + * added to the pending or queued lists again after we've + * removed it here. We can therefore re-use the node of this + * packet temporarily. + */ + + clear_bit(SSH_REQUEST_SF_PENDING_BIT, &r->state); + + atomic_dec(&rtl->pending.count); + list_move_tail(&r->node, &claimed); + } + spin_unlock(&rtl->pending.lock); + + /* Cancel and complete the request. */ + list_for_each_entry_safe(r, n, &claimed, node) { + trace_ssam_request_timeout(r); + + /* + * At this point we've removed the packet from pending. This + * means that we've obtained the last (only) reference of the + * system to it. Thus we can just complete it. + */ + if (!test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + ssh_rtl_complete_with_status(r, -ETIMEDOUT); + + /* + * Drop the reference we've obtained by removing it from the + * pending set. + */ + list_del(&r->node); + ssh_request_put(r); + } + + /* Ensure that the reaper doesn't run again immediately. */ + next = max(next, ktime_add(now, SSH_RTL_REQUEST_TIMEOUT_RESOLUTION)); + if (next != KTIME_MAX) + ssh_rtl_timeout_reaper_mod(rtl, now, next); + + ssh_rtl_tx_schedule(rtl); +} + +static void ssh_rtl_rx_event(struct ssh_rtl *rtl, const struct ssh_command *cmd, + const struct ssam_span *data) +{ + trace_ssam_rx_event_received(cmd, data->len); + + rtl_dbg(rtl, "rtl: handling event (rqid: %#06x)\n", + get_unaligned_le16(&cmd->rqid)); + + rtl->ops.handle_event(rtl, cmd, data); +} + +static void ssh_rtl_rx_command(struct ssh_ptl *p, const struct ssam_span *data) +{ + struct ssh_rtl *rtl = to_ssh_rtl(p, ptl); + struct device *dev = &p->serdev->dev; + struct ssh_command *command; + struct ssam_span command_data; + + if (sshp_parse_command(dev, data, &command, &command_data)) + return; + + /* + * Check if the message was intended for us. If not, drop it. + * + * Note: We will need to change this to handle debug messages. On newer + * generation devices, these seem to be sent to SSAM_SSH_TID_DEBUG. We + * as host can still receive them as they can be forwarded via an + * override option on SAM, but doing so does not change the target ID + * to SSAM_SSH_TID_HOST. + */ + if (command->tid != SSAM_SSH_TID_HOST) { + rtl_warn(rtl, "rtl: dropping message not intended for us (tid = %#04x)\n", + command->tid); + return; + } + + if (ssh_rqid_is_event(get_unaligned_le16(&command->rqid))) + ssh_rtl_rx_event(rtl, command, &command_data); + else + ssh_rtl_complete(rtl, command, &command_data); +} + +static void ssh_rtl_rx_data(struct ssh_ptl *p, const struct ssam_span *data) +{ + if (!data->len) { + ptl_err(p, "rtl: rx: no data frame payload\n"); + return; + } + + switch (data->ptr[0]) { + case SSH_PLD_TYPE_CMD: + ssh_rtl_rx_command(p, data); + break; + + default: + ptl_err(p, "rtl: rx: unknown frame payload type (type: %#04x)\n", + data->ptr[0]); + break; + } +} + +static void ssh_rtl_packet_release(struct ssh_packet *p) +{ + struct ssh_request *rqst; + + rqst = to_ssh_request(p); + rqst->ops->release(rqst); +} + +static const struct ssh_packet_ops ssh_rtl_packet_ops = { + .complete = ssh_rtl_packet_callback, + .release = ssh_rtl_packet_release, +}; + +/** + * ssh_request_init() - Initialize SSH request. + * @rqst: The request to initialize. + * @flags: Request flags, determining the type of the request. + * @ops: Request operations. + * + * Initializes the given SSH request and underlying packet. Sets the message + * buffer pointer to %NULL and the message buffer length to zero. This buffer + * has to be set separately via ssh_request_set_data() before submission and + * must contain a valid SSH request message. + * + * Return: Returns zero on success or %-EINVAL if the given flags are invalid. + */ +int ssh_request_init(struct ssh_request *rqst, enum ssam_request_flags flags, + const struct ssh_request_ops *ops) +{ + unsigned long type = BIT(SSH_PACKET_TY_BLOCKING_BIT); + + /* Unsequenced requests cannot have a response. */ + if (flags & SSAM_REQUEST_UNSEQUENCED && flags & SSAM_REQUEST_HAS_RESPONSE) + return -EINVAL; + + if (!(flags & SSAM_REQUEST_UNSEQUENCED)) + type |= BIT(SSH_PACKET_TY_SEQUENCED_BIT); + + ssh_packet_init(&rqst->packet, type, SSH_PACKET_PRIORITY(DATA, 0), + &ssh_rtl_packet_ops); + + INIT_LIST_HEAD(&rqst->node); + + rqst->state = 0; + if (flags & SSAM_REQUEST_HAS_RESPONSE) + rqst->state |= BIT(SSH_REQUEST_TY_HAS_RESPONSE_BIT); + + rqst->timestamp = KTIME_MAX; + rqst->ops = ops; + + return 0; +} + +/** + * ssh_rtl_init() - Initialize request transport layer. + * @rtl: The request transport layer to initialize. + * @serdev: The underlying serial device, i.e. the lower-level transport. + * @ops: Request transport layer operations. + * + * Initializes the given request transport layer and associated packet + * transport layer. Transmitter and receiver threads must be started + * separately via ssh_rtl_start(), after the request-layer has been + * initialized and the lower-level serial device layer has been set up. + * + * Return: Returns zero on success and a nonzero error code on failure. + */ +int ssh_rtl_init(struct ssh_rtl *rtl, struct serdev_device *serdev, + const struct ssh_rtl_ops *ops) +{ + struct ssh_ptl_ops ptl_ops; + int status; + + ptl_ops.data_received = ssh_rtl_rx_data; + + status = ssh_ptl_init(&rtl->ptl, serdev, &ptl_ops); + if (status) + return status; + + spin_lock_init(&rtl->queue.lock); + INIT_LIST_HEAD(&rtl->queue.head); + + spin_lock_init(&rtl->pending.lock); + INIT_LIST_HEAD(&rtl->pending.head); + atomic_set_release(&rtl->pending.count, 0); + + INIT_WORK(&rtl->tx.work, ssh_rtl_tx_work_fn); + + spin_lock_init(&rtl->rtx_timeout.lock); + rtl->rtx_timeout.timeout = SSH_RTL_REQUEST_TIMEOUT; + rtl->rtx_timeout.expires = KTIME_MAX; + INIT_DELAYED_WORK(&rtl->rtx_timeout.reaper, ssh_rtl_timeout_reap); + + rtl->ops = *ops; + + return 0; +} + +/** + * ssh_rtl_destroy() - Deinitialize request transport layer. + * @rtl: The request transport layer to deinitialize. + * + * Deinitializes the given request transport layer and frees resources + * associated with it. If receiver and/or transmitter threads have been + * started, the layer must first be shut down via ssh_rtl_shutdown() before + * this function can be called. + */ +void ssh_rtl_destroy(struct ssh_rtl *rtl) +{ + ssh_ptl_destroy(&rtl->ptl); +} + +/** + * ssh_rtl_start() - Start request transmitter and receiver. + * @rtl: The request transport layer. + * + * Return: Returns zero on success, a negative error code on failure. + */ +int ssh_rtl_start(struct ssh_rtl *rtl) +{ + int status; + + status = ssh_ptl_tx_start(&rtl->ptl); + if (status) + return status; + + ssh_rtl_tx_schedule(rtl); + + status = ssh_ptl_rx_start(&rtl->ptl); + if (status) { + ssh_rtl_flush(rtl, msecs_to_jiffies(5000)); + ssh_ptl_tx_stop(&rtl->ptl); + return status; + } + + return 0; +} + +struct ssh_flush_request { + struct ssh_request base; + struct completion completion; + int status; +}; + +static void ssh_rtl_flush_request_complete(struct ssh_request *r, + const struct ssh_command *cmd, + const struct ssam_span *data, + int status) +{ + struct ssh_flush_request *rqst; + + rqst = container_of(r, struct ssh_flush_request, base); + rqst->status = status; +} + +static void ssh_rtl_flush_request_release(struct ssh_request *r) +{ + struct ssh_flush_request *rqst; + + rqst = container_of(r, struct ssh_flush_request, base); + complete_all(&rqst->completion); +} + +static const struct ssh_request_ops ssh_rtl_flush_request_ops = { + .complete = ssh_rtl_flush_request_complete, + .release = ssh_rtl_flush_request_release, +}; + +/** + * ssh_rtl_flush() - Flush the request transport layer. + * @rtl: request transport layer + * @timeout: timeout for the flush operation in jiffies + * + * Queue a special flush request and wait for its completion. This request + * will be completed after all other currently queued and pending requests + * have been completed. Instead of a normal data packet, this request submits + * a special flush packet, meaning that upon completion, also the underlying + * packet transport layer has been flushed. + * + * Flushing the request layer guarantees that all previously submitted + * requests have been fully completed before this call returns. Additionally, + * flushing blocks execution of all later submitted requests until the flush + * has been completed. + * + * If the caller ensures that no new requests are submitted after a call to + * this function, the request transport layer is guaranteed to have no + * remaining requests when this call returns. The same guarantee does not hold + * for the packet layer, on which control packets may still be queued after + * this call. + * + * Return: Returns zero on success, %-ETIMEDOUT if the flush timed out and has + * been canceled as a result of the timeout, or %-ESHUTDOWN if the packet + * and/or request transport layer has been shut down before this call. May + * also return %-EINTR if the underlying packet transmission has been + * interrupted. + */ +int ssh_rtl_flush(struct ssh_rtl *rtl, unsigned long timeout) +{ + const unsigned int init_flags = SSAM_REQUEST_UNSEQUENCED; + struct ssh_flush_request rqst; + int status; + + ssh_request_init(&rqst.base, init_flags, &ssh_rtl_flush_request_ops); + rqst.base.packet.state |= BIT(SSH_PACKET_TY_FLUSH_BIT); + rqst.base.packet.priority = SSH_PACKET_PRIORITY(FLUSH, 0); + rqst.base.state |= BIT(SSH_REQUEST_TY_FLUSH_BIT); + + init_completion(&rqst.completion); + + status = ssh_rtl_submit(rtl, &rqst.base); + if (status) + return status; + + ssh_request_put(&rqst.base); + + if (!wait_for_completion_timeout(&rqst.completion, timeout)) { + ssh_rtl_cancel(&rqst.base, true); + wait_for_completion(&rqst.completion); + } + + WARN_ON(rqst.status != 0 && rqst.status != -ECANCELED && + rqst.status != -ESHUTDOWN && rqst.status != -EINTR); + + return rqst.status == -ECANCELED ? -ETIMEDOUT : rqst.status; +} + +/** + * ssh_rtl_shutdown() - Shut down request transport layer. + * @rtl: The request transport layer. + * + * Shuts down the request transport layer, removing and canceling all queued + * and pending requests. Requests canceled by this operation will be completed + * with %-ESHUTDOWN as status. Receiver and transmitter threads will be + * stopped, the lower-level packet layer will be shutdown. + * + * As a result of this function, the transport layer will be marked as shut + * down. Submission of requests after the transport layer has been shut down + * will fail with %-ESHUTDOWN. + */ +void ssh_rtl_shutdown(struct ssh_rtl *rtl) +{ + struct ssh_request *r, *n; + LIST_HEAD(claimed); + int pending; + + set_bit(SSH_RTL_SF_SHUTDOWN_BIT, &rtl->state); + /* + * Ensure that the layer gets marked as shut-down before actually + * stopping it. In combination with the check in ssh_rtl_submit(), + * this guarantees that no new requests can be added and all already + * queued requests are properly canceled. + */ + smp_mb__after_atomic(); + + /* Remove requests from queue. */ + spin_lock(&rtl->queue.lock); + list_for_each_entry_safe(r, n, &rtl->queue.head, node) { + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state); + /* Ensure state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_REQUEST_SF_QUEUED_BIT, &r->state); + + list_move_tail(&r->node, &claimed); + } + spin_unlock(&rtl->queue.lock); + + /* + * We have now guaranteed that the queue is empty and no more new + * requests can be submitted (i.e. it will stay empty). This means that + * calling ssh_rtl_tx_schedule() will not schedule tx.work any more. So + * we can simply call cancel_work_sync() on tx.work here and when that + * returns, we've locked it down. This also means that after this call, + * we don't submit any more packets to the underlying packet layer, so + * we can also shut that down. + */ + + cancel_work_sync(&rtl->tx.work); + ssh_ptl_shutdown(&rtl->ptl); + cancel_delayed_work_sync(&rtl->rtx_timeout.reaper); + + /* + * Shutting down the packet layer should also have canceled all + * requests. Thus the pending set should be empty. Attempt to handle + * this gracefully anyways, even though this should be dead code. + */ + + pending = atomic_read(&rtl->pending.count); + if (WARN_ON(pending)) { + spin_lock(&rtl->pending.lock); + list_for_each_entry_safe(r, n, &rtl->pending.head, node) { + set_bit(SSH_REQUEST_SF_LOCKED_BIT, &r->state); + /* Ensure state never gets zero. */ + smp_mb__before_atomic(); + clear_bit(SSH_REQUEST_SF_PENDING_BIT, &r->state); + + list_move_tail(&r->node, &claimed); + } + spin_unlock(&rtl->pending.lock); + } + + /* Finally, cancel and complete the requests we claimed before. */ + list_for_each_entry_safe(r, n, &claimed, node) { + /* + * We need test_and_set() because we still might compete with + * cancellation. + */ + if (!test_and_set_bit(SSH_REQUEST_SF_COMPLETED_BIT, &r->state)) + ssh_rtl_complete_with_status(r, -ESHUTDOWN); + + /* + * Drop the reference we've obtained by removing it from the + * lists. + */ + list_del(&r->node); + ssh_request_put(r); + } +} diff --git a/drivers/platform/surface/aggregator/ssh_request_layer.h b/drivers/platform/surface/aggregator/ssh_request_layer.h new file mode 100644 index 000000000..4e387a031 --- /dev/null +++ b/drivers/platform/surface/aggregator/ssh_request_layer.h @@ -0,0 +1,143 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * SSH request transport layer. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#ifndef _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H +#define _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H + +#include +#include +#include +#include +#include + +#include +#include + +#include "ssh_packet_layer.h" + +/** + * enum ssh_rtl_state_flags - State-flags for &struct ssh_rtl. + * + * @SSH_RTL_SF_SHUTDOWN_BIT: + * Indicates that the request transport layer has been shut down or is + * being shut down and should not accept any new requests. + */ +enum ssh_rtl_state_flags { + SSH_RTL_SF_SHUTDOWN_BIT, +}; + +/** + * struct ssh_rtl_ops - Callback operations for request transport layer. + * @handle_event: Function called when a SSH event has been received. The + * specified function takes the request layer, received command + * struct, and corresponding payload as arguments. If the event + * has no payload, the payload span is empty (not %NULL). + */ +struct ssh_rtl_ops { + void (*handle_event)(struct ssh_rtl *rtl, const struct ssh_command *cmd, + const struct ssam_span *data); +}; + +/** + * struct ssh_rtl - SSH request transport layer. + * @ptl: Underlying packet transport layer. + * @state: State(-flags) of the transport layer. + * @queue: Request submission queue. + * @queue.lock: Lock for modifying the request submission queue. + * @queue.head: List-head of the request submission queue. + * @pending: Set/list of pending requests. + * @pending.lock: Lock for modifying the request set. + * @pending.head: List-head of the pending set/list. + * @pending.count: Number of currently pending requests. + * @tx: Transmitter subsystem. + * @tx.work: Transmitter work item. + * @rtx_timeout: Retransmission timeout subsystem. + * @rtx_timeout.lock: Lock for modifying the retransmission timeout reaper. + * @rtx_timeout.timeout: Timeout interval for retransmission. + * @rtx_timeout.expires: Time specifying when the reaper work is next scheduled. + * @rtx_timeout.reaper: Work performing timeout checks and subsequent actions. + * @ops: Request layer operations. + */ +struct ssh_rtl { + struct ssh_ptl ptl; + unsigned long state; + + struct { + spinlock_t lock; + struct list_head head; + } queue; + + struct { + spinlock_t lock; + struct list_head head; + atomic_t count; + } pending; + + struct { + struct work_struct work; + } tx; + + struct { + spinlock_t lock; + ktime_t timeout; + ktime_t expires; + struct delayed_work reaper; + } rtx_timeout; + + struct ssh_rtl_ops ops; +}; + +#define rtl_dbg(r, fmt, ...) ptl_dbg(&(r)->ptl, fmt, ##__VA_ARGS__) +#define rtl_info(p, fmt, ...) ptl_info(&(p)->ptl, fmt, ##__VA_ARGS__) +#define rtl_warn(r, fmt, ...) ptl_warn(&(r)->ptl, fmt, ##__VA_ARGS__) +#define rtl_err(r, fmt, ...) ptl_err(&(r)->ptl, fmt, ##__VA_ARGS__) +#define rtl_dbg_cond(r, fmt, ...) __ssam_prcond(rtl_dbg, r, fmt, ##__VA_ARGS__) + +#define to_ssh_rtl(ptr, member) \ + container_of(ptr, struct ssh_rtl, member) + +/** + * ssh_rtl_get_device() - Get device associated with request transport layer. + * @rtl: The request transport layer. + * + * Return: Returns the device on which the given request transport layer + * builds upon. + */ +static inline struct device *ssh_rtl_get_device(struct ssh_rtl *rtl) +{ + return ssh_ptl_get_device(&rtl->ptl); +} + +/** + * ssh_request_rtl() - Get request transport layer associated with request. + * @rqst: The request to get the request transport layer reference for. + * + * Return: Returns the &struct ssh_rtl associated with the given SSH request. + */ +static inline struct ssh_rtl *ssh_request_rtl(struct ssh_request *rqst) +{ + struct ssh_ptl *ptl; + + ptl = READ_ONCE(rqst->packet.ptl); + return likely(ptl) ? to_ssh_rtl(ptl, ptl) : NULL; +} + +int ssh_rtl_submit(struct ssh_rtl *rtl, struct ssh_request *rqst); +bool ssh_rtl_cancel(struct ssh_request *rqst, bool pending); + +int ssh_rtl_init(struct ssh_rtl *rtl, struct serdev_device *serdev, + const struct ssh_rtl_ops *ops); + +int ssh_rtl_start(struct ssh_rtl *rtl); +int ssh_rtl_flush(struct ssh_rtl *rtl, unsigned long timeout); +void ssh_rtl_shutdown(struct ssh_rtl *rtl); +void ssh_rtl_destroy(struct ssh_rtl *rtl); + +int ssh_request_init(struct ssh_request *rqst, enum ssam_request_flags flags, + const struct ssh_request_ops *ops); + +#endif /* _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H */ diff --git a/drivers/platform/surface/aggregator/trace.h b/drivers/platform/surface/aggregator/trace.h new file mode 100644 index 000000000..caf7d3cb5 --- /dev/null +++ b/drivers/platform/surface/aggregator/trace.h @@ -0,0 +1,703 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Trace points for SSAM/SSH. + * + * Copyright (C) 2020-2022 Maximilian Luz + */ + +#undef TRACE_SYSTEM +#define TRACE_SYSTEM surface_aggregator + +#if !defined(_SURFACE_AGGREGATOR_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) +#define _SURFACE_AGGREGATOR_TRACE_H + +#include + +#include +#include + +TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_DATA_SEQ); +TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_DATA_NSQ); +TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_ACK); +TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_NAK); + +TRACE_DEFINE_ENUM(SSH_PACKET_SF_LOCKED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_QUEUED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_PENDING_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_TRANSMITTING_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_TRANSMITTED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_ACKED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_CANCELED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_SF_COMPLETED_BIT); + +TRACE_DEFINE_ENUM(SSH_PACKET_TY_FLUSH_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_TY_SEQUENCED_BIT); +TRACE_DEFINE_ENUM(SSH_PACKET_TY_BLOCKING_BIT); + +TRACE_DEFINE_ENUM(SSH_PACKET_FLAGS_SF_MASK); +TRACE_DEFINE_ENUM(SSH_PACKET_FLAGS_TY_MASK); + +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_LOCKED_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_QUEUED_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_PENDING_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_TRANSMITTING_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_TRANSMITTED_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_RSPRCVD_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_CANCELED_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_SF_COMPLETED_BIT); + +TRACE_DEFINE_ENUM(SSH_REQUEST_TY_FLUSH_BIT); +TRACE_DEFINE_ENUM(SSH_REQUEST_TY_HAS_RESPONSE_BIT); + +TRACE_DEFINE_ENUM(SSH_REQUEST_FLAGS_SF_MASK); +TRACE_DEFINE_ENUM(SSH_REQUEST_FLAGS_TY_MASK); + +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SAM); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_BAT); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_TMP); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_PMC); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_FAN); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_PoM); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_DBG); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_KBD); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_FWU); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_UNI); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_LPC); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_TCL); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SFL); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_KIP); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_EXT); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_BLD); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_BAS); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SEN); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SRQ); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_MCU); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_HID); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_TCH); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_BKL); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_TAM); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_ACC0); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_UFI); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_USC); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_PEN); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_VID); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_AUD); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SMC); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_KPD); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_REG); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SPT); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SYS); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_ACC1); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_SHB); +TRACE_DEFINE_ENUM(SSAM_SSH_TC_POS); + +#define SSAM_PTR_UID_LEN 9 +#define SSAM_U8_FIELD_NOT_APPLICABLE ((u16)-1) +#define SSAM_SEQ_NOT_APPLICABLE ((u16)-1) +#define SSAM_RQID_NOT_APPLICABLE ((u32)-1) +#define SSAM_SSH_TC_NOT_APPLICABLE 0 +#define SSAM_SSH_TID_NOT_APPLICABLE ((u8)-1) + +#ifndef _SURFACE_AGGREGATOR_TRACE_HELPERS +#define _SURFACE_AGGREGATOR_TRACE_HELPERS + +/** + * ssam_trace_ptr_uid() - Convert the pointer to a non-pointer UID string. + * @ptr: The pointer to convert. + * @uid_str: A buffer of length SSAM_PTR_UID_LEN where the UID will be stored. + * + * Converts the given pointer into a UID string that is safe to be shared + * with userspace and logs, i.e. doesn't give away the real memory location. + */ +static inline void ssam_trace_ptr_uid(const void *ptr, char *uid_str) +{ + char buf[2 * sizeof(void *) + 1]; + + BUILD_BUG_ON(ARRAY_SIZE(buf) < SSAM_PTR_UID_LEN); + + snprintf(buf, ARRAY_SIZE(buf), "%p", ptr); + memcpy(uid_str, &buf[ARRAY_SIZE(buf) - SSAM_PTR_UID_LEN], + SSAM_PTR_UID_LEN); +} + +/** + * ssam_trace_get_packet_seq() - Read the packet's sequence ID. + * @p: The packet. + * + * Return: Returns the packet's sequence ID (SEQ) field if present, or + * %SSAM_SEQ_NOT_APPLICABLE if not (e.g. flush packet). + */ +static inline u16 ssam_trace_get_packet_seq(const struct ssh_packet *p) +{ + if (!p->data.ptr || p->data.len < SSH_MESSAGE_LENGTH(0)) + return SSAM_SEQ_NOT_APPLICABLE; + + return p->data.ptr[SSH_MSGOFFSET_FRAME(seq)]; +} + +/** + * ssam_trace_get_request_id() - Read the packet's request ID. + * @p: The packet. + * + * Return: Returns the packet's request ID (RQID) field if the packet + * represents a request with command data, or %SSAM_RQID_NOT_APPLICABLE if not + * (e.g. flush request, control packet). + */ +static inline u32 ssam_trace_get_request_id(const struct ssh_packet *p) +{ + if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) + return SSAM_RQID_NOT_APPLICABLE; + + return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(rqid)]); +} + +/** + * ssam_trace_get_request_tid() - Read the packet's request target ID. + * @p: The packet. + * + * Return: Returns the packet's request target ID (TID) field if the packet + * represents a request with command data, or %SSAM_SSH_TID_NOT_APPLICABLE + * if not (e.g. flush request, control packet). + */ +static inline u32 ssam_trace_get_request_tid(const struct ssh_packet *p) +{ + if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) + return SSAM_SSH_TID_NOT_APPLICABLE; + + return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(tid)]); +} + +/** + * ssam_trace_get_request_sid() - Read the packet's request source ID. + * @p: The packet. + * + * Return: Returns the packet's request source ID (SID) field if the packet + * represents a request with command data, or %SSAM_SSH_TID_NOT_APPLICABLE + * if not (e.g. flush request, control packet). + */ +static inline u32 ssam_trace_get_request_sid(const struct ssh_packet *p) +{ + if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) + return SSAM_SSH_TID_NOT_APPLICABLE; + + return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(sid)]); +} + +/** + * ssam_trace_get_request_tc() - Read the packet's request target category. + * @p: The packet. + * + * Return: Returns the packet's request target category (TC) field if the + * packet represents a request with command data, or %SSAM_SSH_TC_NOT_APPLICABLE + * if not (e.g. flush request, control packet). + */ +static inline u32 ssam_trace_get_request_tc(const struct ssh_packet *p) +{ + if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) + return SSAM_SSH_TC_NOT_APPLICABLE; + + return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(tc)]); +} + +#endif /* _SURFACE_AGGREGATOR_TRACE_HELPERS */ + +#define ssam_trace_get_command_field_u8(packet, field) \ + ((!(packet) || (packet)->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) \ + ? 0 : (packet)->data.ptr[SSH_MSGOFFSET_COMMAND(field)]) + +#define ssam_show_generic_u8_field(value) \ + __print_symbolic(value, \ + { SSAM_U8_FIELD_NOT_APPLICABLE, "N/A" } \ + ) + +#define ssam_show_frame_type(ty) \ + __print_symbolic(ty, \ + { SSH_FRAME_TYPE_DATA_SEQ, "DSEQ" }, \ + { SSH_FRAME_TYPE_DATA_NSQ, "DNSQ" }, \ + { SSH_FRAME_TYPE_ACK, "ACK" }, \ + { SSH_FRAME_TYPE_NAK, "NAK" } \ + ) + +#define ssam_show_packet_type(type) \ + __print_flags(flags & SSH_PACKET_FLAGS_TY_MASK, "", \ + { BIT(SSH_PACKET_TY_FLUSH_BIT), "F" }, \ + { BIT(SSH_PACKET_TY_SEQUENCED_BIT), "S" }, \ + { BIT(SSH_PACKET_TY_BLOCKING_BIT), "B" } \ + ) + +#define ssam_show_packet_state(state) \ + __print_flags(flags & SSH_PACKET_FLAGS_SF_MASK, "", \ + { BIT(SSH_PACKET_SF_LOCKED_BIT), "L" }, \ + { BIT(SSH_PACKET_SF_QUEUED_BIT), "Q" }, \ + { BIT(SSH_PACKET_SF_PENDING_BIT), "P" }, \ + { BIT(SSH_PACKET_SF_TRANSMITTING_BIT), "S" }, \ + { BIT(SSH_PACKET_SF_TRANSMITTED_BIT), "T" }, \ + { BIT(SSH_PACKET_SF_ACKED_BIT), "A" }, \ + { BIT(SSH_PACKET_SF_CANCELED_BIT), "C" }, \ + { BIT(SSH_PACKET_SF_COMPLETED_BIT), "F" } \ + ) + +#define ssam_show_packet_seq(seq) \ + __print_symbolic(seq, \ + { SSAM_SEQ_NOT_APPLICABLE, "N/A" } \ + ) + +#define ssam_show_request_type(flags) \ + __print_flags((flags) & SSH_REQUEST_FLAGS_TY_MASK, "", \ + { BIT(SSH_REQUEST_TY_FLUSH_BIT), "F" }, \ + { BIT(SSH_REQUEST_TY_HAS_RESPONSE_BIT), "R" } \ + ) + +#define ssam_show_request_state(flags) \ + __print_flags((flags) & SSH_REQUEST_FLAGS_SF_MASK, "", \ + { BIT(SSH_REQUEST_SF_LOCKED_BIT), "L" }, \ + { BIT(SSH_REQUEST_SF_QUEUED_BIT), "Q" }, \ + { BIT(SSH_REQUEST_SF_PENDING_BIT), "P" }, \ + { BIT(SSH_REQUEST_SF_TRANSMITTING_BIT), "S" }, \ + { BIT(SSH_REQUEST_SF_TRANSMITTED_BIT), "T" }, \ + { BIT(SSH_REQUEST_SF_RSPRCVD_BIT), "A" }, \ + { BIT(SSH_REQUEST_SF_CANCELED_BIT), "C" }, \ + { BIT(SSH_REQUEST_SF_COMPLETED_BIT), "F" } \ + ) + +#define ssam_show_request_id(rqid) \ + __print_symbolic(rqid, \ + { SSAM_RQID_NOT_APPLICABLE, "N/A" } \ + ) + +#define ssam_show_ssh_tid(tid) \ + __print_symbolic(tid, \ + { SSAM_SSH_TID_NOT_APPLICABLE, "N/A" }, \ + { SSAM_SSH_TID_HOST, "Host" }, \ + { SSAM_SSH_TID_SAM, "SAM" }, \ + { SSAM_SSH_TID_KIP, "KIP" }, \ + { SSAM_SSH_TID_DEBUG, "Debug" }, \ + { SSAM_SSH_TID_SURFLINK, "SurfLink" } \ + ) + +#define ssam_show_ssh_tc(tc) \ + __print_symbolic(tc, \ + { SSAM_SSH_TC_NOT_APPLICABLE, "N/A" }, \ + { SSAM_SSH_TC_SAM, "SAM" }, \ + { SSAM_SSH_TC_BAT, "BAT" }, \ + { SSAM_SSH_TC_TMP, "TMP" }, \ + { SSAM_SSH_TC_PMC, "PMC" }, \ + { SSAM_SSH_TC_FAN, "FAN" }, \ + { SSAM_SSH_TC_PoM, "PoM" }, \ + { SSAM_SSH_TC_DBG, "DBG" }, \ + { SSAM_SSH_TC_KBD, "KBD" }, \ + { SSAM_SSH_TC_FWU, "FWU" }, \ + { SSAM_SSH_TC_UNI, "UNI" }, \ + { SSAM_SSH_TC_LPC, "LPC" }, \ + { SSAM_SSH_TC_TCL, "TCL" }, \ + { SSAM_SSH_TC_SFL, "SFL" }, \ + { SSAM_SSH_TC_KIP, "KIP" }, \ + { SSAM_SSH_TC_EXT, "EXT" }, \ + { SSAM_SSH_TC_BLD, "BLD" }, \ + { SSAM_SSH_TC_BAS, "BAS" }, \ + { SSAM_SSH_TC_SEN, "SEN" }, \ + { SSAM_SSH_TC_SRQ, "SRQ" }, \ + { SSAM_SSH_TC_MCU, "MCU" }, \ + { SSAM_SSH_TC_HID, "HID" }, \ + { SSAM_SSH_TC_TCH, "TCH" }, \ + { SSAM_SSH_TC_BKL, "BKL" }, \ + { SSAM_SSH_TC_TAM, "TAM" }, \ + { SSAM_SSH_TC_ACC0, "ACC0" }, \ + { SSAM_SSH_TC_UFI, "UFI" }, \ + { SSAM_SSH_TC_USC, "USC" }, \ + { SSAM_SSH_TC_PEN, "PEN" }, \ + { SSAM_SSH_TC_VID, "VID" }, \ + { SSAM_SSH_TC_AUD, "AUD" }, \ + { SSAM_SSH_TC_SMC, "SMC" }, \ + { SSAM_SSH_TC_KPD, "KPD" }, \ + { SSAM_SSH_TC_REG, "REG" }, \ + { SSAM_SSH_TC_SPT, "SPT" }, \ + { SSAM_SSH_TC_SYS, "SYS" }, \ + { SSAM_SSH_TC_ACC1, "ACC1" }, \ + { SSAM_SSH_TC_SHB, "SMB" }, \ + { SSAM_SSH_TC_POS, "POS" } \ + ) + +DECLARE_EVENT_CLASS(ssam_frame_class, + TP_PROTO(const struct ssh_frame *frame), + + TP_ARGS(frame), + + TP_STRUCT__entry( + __field(u8, type) + __field(u8, seq) + __field(u16, len) + ), + + TP_fast_assign( + __entry->type = frame->type; + __entry->seq = frame->seq; + __entry->len = get_unaligned_le16(&frame->len); + ), + + TP_printk("ty=%s, seq=%#04x, len=%u", + ssam_show_frame_type(__entry->type), + __entry->seq, + __entry->len + ) +); + +#define DEFINE_SSAM_FRAME_EVENT(name) \ + DEFINE_EVENT(ssam_frame_class, ssam_##name, \ + TP_PROTO(const struct ssh_frame *frame), \ + TP_ARGS(frame) \ + ) + +DECLARE_EVENT_CLASS(ssam_command_class, + TP_PROTO(const struct ssh_command *cmd, u16 len), + + TP_ARGS(cmd, len), + + TP_STRUCT__entry( + __field(u16, rqid) + __field(u16, len) + __field(u8, tid) + __field(u8, sid) + __field(u8, tc) + __field(u8, cid) + __field(u8, iid) + ), + + TP_fast_assign( + __entry->rqid = get_unaligned_le16(&cmd->rqid); + __entry->tid = cmd->tid; + __entry->sid = cmd->sid; + __entry->tc = cmd->tc; + __entry->cid = cmd->cid; + __entry->iid = cmd->iid; + __entry->len = len; + ), + + TP_printk("rqid=%#06x, tid=%s, sid=%s, tc=%s, cid=%#04x, iid=%#04x, len=%u", + __entry->rqid, + ssam_show_ssh_tid(__entry->tid), + ssam_show_ssh_tid(__entry->sid), + ssam_show_ssh_tc(__entry->tc), + __entry->cid, + __entry->iid, + __entry->len + ) +); + +#define DEFINE_SSAM_COMMAND_EVENT(name) \ + DEFINE_EVENT(ssam_command_class, ssam_##name, \ + TP_PROTO(const struct ssh_command *cmd, u16 len), \ + TP_ARGS(cmd, len) \ + ) + +DECLARE_EVENT_CLASS(ssam_packet_class, + TP_PROTO(const struct ssh_packet *packet), + + TP_ARGS(packet), + + TP_STRUCT__entry( + __field(unsigned long, state) + __array(char, uid, SSAM_PTR_UID_LEN) + __field(u8, priority) + __field(u16, length) + __field(u16, seq) + ), + + TP_fast_assign( + __entry->state = READ_ONCE(packet->state); + ssam_trace_ptr_uid(packet, __entry->uid); + __entry->priority = READ_ONCE(packet->priority); + __entry->length = packet->data.len; + __entry->seq = ssam_trace_get_packet_seq(packet); + ), + + TP_printk("uid=%s, seq=%s, ty=%s, pri=%#04x, len=%u, sta=%s", + __entry->uid, + ssam_show_packet_seq(__entry->seq), + ssam_show_packet_type(__entry->state), + __entry->priority, + __entry->length, + ssam_show_packet_state(__entry->state) + ) +); + +#define DEFINE_SSAM_PACKET_EVENT(name) \ + DEFINE_EVENT(ssam_packet_class, ssam_##name, \ + TP_PROTO(const struct ssh_packet *packet), \ + TP_ARGS(packet) \ + ) + +DECLARE_EVENT_CLASS(ssam_packet_status_class, + TP_PROTO(const struct ssh_packet *packet, int status), + + TP_ARGS(packet, status), + + TP_STRUCT__entry( + __field(unsigned long, state) + __field(int, status) + __array(char, uid, SSAM_PTR_UID_LEN) + __field(u8, priority) + __field(u16, length) + __field(u16, seq) + ), + + TP_fast_assign( + __entry->state = READ_ONCE(packet->state); + __entry->status = status; + ssam_trace_ptr_uid(packet, __entry->uid); + __entry->priority = READ_ONCE(packet->priority); + __entry->length = packet->data.len; + __entry->seq = ssam_trace_get_packet_seq(packet); + ), + + TP_printk("uid=%s, seq=%s, ty=%s, pri=%#04x, len=%u, sta=%s, status=%d", + __entry->uid, + ssam_show_packet_seq(__entry->seq), + ssam_show_packet_type(__entry->state), + __entry->priority, + __entry->length, + ssam_show_packet_state(__entry->state), + __entry->status + ) +); + +#define DEFINE_SSAM_PACKET_STATUS_EVENT(name) \ + DEFINE_EVENT(ssam_packet_status_class, ssam_##name, \ + TP_PROTO(const struct ssh_packet *packet, int status), \ + TP_ARGS(packet, status) \ + ) + +DECLARE_EVENT_CLASS(ssam_request_class, + TP_PROTO(const struct ssh_request *request), + + TP_ARGS(request), + + TP_STRUCT__entry( + __field(unsigned long, state) + __field(u32, rqid) + __array(char, uid, SSAM_PTR_UID_LEN) + __field(u8, tc) + __field(u16, cid) + __field(u16, iid) + __field(u8, tid) + __field(u8, sid) + ), + + TP_fast_assign( + const struct ssh_packet *p = &request->packet; + + /* Use packet for UID so we can match requests to packets. */ + __entry->state = READ_ONCE(request->state); + __entry->rqid = ssam_trace_get_request_id(p); + ssam_trace_ptr_uid(p, __entry->uid); + __entry->tid = ssam_trace_get_request_tid(p); + __entry->sid = ssam_trace_get_request_sid(p); + __entry->tc = ssam_trace_get_request_tc(p); + __entry->cid = ssam_trace_get_command_field_u8(p, cid); + __entry->iid = ssam_trace_get_command_field_u8(p, iid); + ), + + TP_printk("uid=%s, rqid=%s, ty=%s, sta=%s, tid=%s, sid=%s, tc=%s, cid=%s, iid=%s", + __entry->uid, + ssam_show_request_id(__entry->rqid), + ssam_show_request_type(__entry->state), + ssam_show_request_state(__entry->state), + ssam_show_ssh_tid(__entry->tid), + ssam_show_ssh_tid(__entry->sid), + ssam_show_ssh_tc(__entry->tc), + ssam_show_generic_u8_field(__entry->cid), + ssam_show_generic_u8_field(__entry->iid) + ) +); + +#define DEFINE_SSAM_REQUEST_EVENT(name) \ + DEFINE_EVENT(ssam_request_class, ssam_##name, \ + TP_PROTO(const struct ssh_request *request), \ + TP_ARGS(request) \ + ) + +DECLARE_EVENT_CLASS(ssam_request_status_class, + TP_PROTO(const struct ssh_request *request, int status), + + TP_ARGS(request, status), + + TP_STRUCT__entry( + __field(unsigned long, state) + __field(u32, rqid) + __field(int, status) + __array(char, uid, SSAM_PTR_UID_LEN) + __field(u8, tc) + __field(u16, cid) + __field(u16, iid) + __field(u8, tid) + __field(u8, sid) + ), + + TP_fast_assign( + const struct ssh_packet *p = &request->packet; + + /* Use packet for UID so we can match requests to packets. */ + __entry->state = READ_ONCE(request->state); + __entry->rqid = ssam_trace_get_request_id(p); + __entry->status = status; + ssam_trace_ptr_uid(p, __entry->uid); + __entry->tid = ssam_trace_get_request_tid(p); + __entry->sid = ssam_trace_get_request_sid(p); + __entry->tc = ssam_trace_get_request_tc(p); + __entry->cid = ssam_trace_get_command_field_u8(p, cid); + __entry->iid = ssam_trace_get_command_field_u8(p, iid); + ), + + TP_printk("uid=%s, rqid=%s, ty=%s, sta=%s, tid=%s, sid=%s, tc=%s, cid=%s, iid=%s, status=%d", + __entry->uid, + ssam_show_request_id(__entry->rqid), + ssam_show_request_type(__entry->state), + ssam_show_request_state(__entry->state), + ssam_show_ssh_tid(__entry->tid), + ssam_show_ssh_tid(__entry->sid), + ssam_show_ssh_tc(__entry->tc), + ssam_show_generic_u8_field(__entry->cid), + ssam_show_generic_u8_field(__entry->iid), + __entry->status + ) +); + +#define DEFINE_SSAM_REQUEST_STATUS_EVENT(name) \ + DEFINE_EVENT(ssam_request_status_class, ssam_##name, \ + TP_PROTO(const struct ssh_request *request, int status),\ + TP_ARGS(request, status) \ + ) + +DECLARE_EVENT_CLASS(ssam_alloc_class, + TP_PROTO(void *ptr, size_t len), + + TP_ARGS(ptr, len), + + TP_STRUCT__entry( + __field(size_t, len) + __array(char, uid, SSAM_PTR_UID_LEN) + ), + + TP_fast_assign( + __entry->len = len; + ssam_trace_ptr_uid(ptr, __entry->uid); + ), + + TP_printk("uid=%s, len=%zu", __entry->uid, __entry->len) +); + +#define DEFINE_SSAM_ALLOC_EVENT(name) \ + DEFINE_EVENT(ssam_alloc_class, ssam_##name, \ + TP_PROTO(void *ptr, size_t len), \ + TP_ARGS(ptr, len) \ + ) + +DECLARE_EVENT_CLASS(ssam_free_class, + TP_PROTO(void *ptr), + + TP_ARGS(ptr), + + TP_STRUCT__entry( + __array(char, uid, SSAM_PTR_UID_LEN) + ), + + TP_fast_assign( + ssam_trace_ptr_uid(ptr, __entry->uid); + ), + + TP_printk("uid=%s", __entry->uid) +); + +#define DEFINE_SSAM_FREE_EVENT(name) \ + DEFINE_EVENT(ssam_free_class, ssam_##name, \ + TP_PROTO(void *ptr), \ + TP_ARGS(ptr) \ + ) + +DECLARE_EVENT_CLASS(ssam_pending_class, + TP_PROTO(unsigned int pending), + + TP_ARGS(pending), + + TP_STRUCT__entry( + __field(unsigned int, pending) + ), + + TP_fast_assign( + __entry->pending = pending; + ), + + TP_printk("pending=%u", __entry->pending) +); + +#define DEFINE_SSAM_PENDING_EVENT(name) \ + DEFINE_EVENT(ssam_pending_class, ssam_##name, \ + TP_PROTO(unsigned int pending), \ + TP_ARGS(pending) \ + ) + +DECLARE_EVENT_CLASS(ssam_data_class, + TP_PROTO(size_t length), + + TP_ARGS(length), + + TP_STRUCT__entry( + __field(size_t, length) + ), + + TP_fast_assign( + __entry->length = length; + ), + + TP_printk("length=%zu", __entry->length) +); + +#define DEFINE_SSAM_DATA_EVENT(name) \ + DEFINE_EVENT(ssam_data_class, ssam_##name, \ + TP_PROTO(size_t length), \ + TP_ARGS(length) \ + ) + +DEFINE_SSAM_FRAME_EVENT(rx_frame_received); +DEFINE_SSAM_COMMAND_EVENT(rx_response_received); +DEFINE_SSAM_COMMAND_EVENT(rx_event_received); + +DEFINE_SSAM_PACKET_EVENT(packet_release); +DEFINE_SSAM_PACKET_EVENT(packet_submit); +DEFINE_SSAM_PACKET_EVENT(packet_resubmit); +DEFINE_SSAM_PACKET_EVENT(packet_timeout); +DEFINE_SSAM_PACKET_EVENT(packet_cancel); +DEFINE_SSAM_PACKET_STATUS_EVENT(packet_complete); +DEFINE_SSAM_PENDING_EVENT(ptl_timeout_reap); + +DEFINE_SSAM_REQUEST_EVENT(request_submit); +DEFINE_SSAM_REQUEST_EVENT(request_timeout); +DEFINE_SSAM_REQUEST_EVENT(request_cancel); +DEFINE_SSAM_REQUEST_STATUS_EVENT(request_complete); +DEFINE_SSAM_PENDING_EVENT(rtl_timeout_reap); + +DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_ack_packet); +DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_nak_packet); +DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_dsq_packet); +DEFINE_SSAM_PACKET_STATUS_EVENT(ei_tx_fail_write); +DEFINE_SSAM_PACKET_EVENT(ei_tx_corrupt_data); +DEFINE_SSAM_DATA_EVENT(ei_rx_corrupt_syn); +DEFINE_SSAM_FRAME_EVENT(ei_rx_corrupt_data); +DEFINE_SSAM_REQUEST_EVENT(ei_rx_drop_response); + +DEFINE_SSAM_ALLOC_EVENT(ctrl_packet_alloc); +DEFINE_SSAM_FREE_EVENT(ctrl_packet_free); + +DEFINE_SSAM_ALLOC_EVENT(event_item_alloc); +DEFINE_SSAM_FREE_EVENT(event_item_free); + +#endif /* _SURFACE_AGGREGATOR_TRACE_H */ + +/* This part must be outside protection */ +#undef TRACE_INCLUDE_PATH +#undef TRACE_INCLUDE_FILE + +#define TRACE_INCLUDE_PATH . +#define TRACE_INCLUDE_FILE trace + +#include diff --git a/drivers/platform/surface/surface3-wmi.c b/drivers/platform/surface/surface3-wmi.c new file mode 100644 index 000000000..6c8fb7a4d --- /dev/null +++ b/drivers/platform/surface/surface3-wmi.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for the LID cover switch of the Surface 3 + * + * Copyright (c) 2016 Red Hat Inc. + */ + + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Benjamin Tissoires "); +MODULE_DESCRIPTION("Surface 3 platform driver"); +MODULE_LICENSE("GPL"); + +#define ACPI_BUTTON_HID_LID "PNP0C0D" +#define SPI_CTL_OBJ_NAME "SPI" +#define SPI_TS_OBJ_NAME "NTRG" + +#define SURFACE3_LID_GUID "F7CC25EC-D20B-404C-8903-0ED4359C18AE" + +MODULE_ALIAS("wmi:" SURFACE3_LID_GUID); + +static const struct dmi_system_id surface3_dmi_table[] = { +#if defined(CONFIG_X86) + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "Surface 3"), + }, + }, +#endif + { } +}; + +struct surface3_wmi { + struct acpi_device *touchscreen_adev; + struct acpi_device *pnp0c0d_adev; + struct acpi_hotplug_context hp; + struct input_dev *input; +}; + +static struct platform_device *s3_wmi_pdev; + +static struct surface3_wmi s3_wmi; + +static DEFINE_MUTEX(s3_wmi_lock); + +static int s3_wmi_query_block(const char *guid, int instance, int *ret) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj = NULL; + acpi_status status; + int error = 0; + + mutex_lock(&s3_wmi_lock); + status = wmi_query_block(guid, instance, &output); + if (ACPI_FAILURE(status)) { + error = -EIO; + goto out_free_unlock; + } + + obj = output.pointer; + + if (!obj || obj->type != ACPI_TYPE_INTEGER) { + if (obj) { + pr_err("query block returned object type: %d - buffer length:%d\n", + obj->type, + obj->type == ACPI_TYPE_BUFFER ? + obj->buffer.length : 0); + } + error = -EINVAL; + goto out_free_unlock; + } + *ret = obj->integer.value; + out_free_unlock: + kfree(obj); + mutex_unlock(&s3_wmi_lock); + return error; +} + +static inline int s3_wmi_query_lid(int *ret) +{ + return s3_wmi_query_block(SURFACE3_LID_GUID, 0, ret); +} + +static int s3_wmi_send_lid_state(void) +{ + int ret, lid_sw; + + ret = s3_wmi_query_lid(&lid_sw); + if (ret) + return ret; + + input_report_switch(s3_wmi.input, SW_LID, lid_sw); + input_sync(s3_wmi.input); + + return 0; +} + +static int s3_wmi_hp_notify(struct acpi_device *adev, u32 value) +{ + return s3_wmi_send_lid_state(); +} + +static acpi_status s3_wmi_attach_spi_device(acpi_handle handle, + u32 level, + void *data, + void **return_value) +{ + struct acpi_device *adev = acpi_fetch_acpi_dev(handle); + struct acpi_device **ts_adev = data; + + if (!adev || strncmp(acpi_device_bid(adev), SPI_TS_OBJ_NAME, + strlen(SPI_TS_OBJ_NAME))) + return AE_OK; + + if (*ts_adev) { + pr_err("duplicate entry %s\n", SPI_TS_OBJ_NAME); + return AE_OK; + } + + *ts_adev = adev; + + return AE_OK; +} + +static int s3_wmi_check_platform_device(struct device *dev, void *data) +{ + struct acpi_device *adev = ACPI_COMPANION(dev); + struct acpi_device *ts_adev = NULL; + acpi_status status; + + /* ignore non ACPI devices */ + if (!adev) + return 0; + + /* check for LID ACPI switch */ + if (!strcmp(ACPI_BUTTON_HID_LID, acpi_device_hid(adev))) { + s3_wmi.pnp0c0d_adev = adev; + return 0; + } + + /* ignore non SPI controllers */ + if (strncmp(acpi_device_bid(adev), SPI_CTL_OBJ_NAME, + strlen(SPI_CTL_OBJ_NAME))) + return 0; + + status = acpi_walk_namespace(ACPI_TYPE_DEVICE, adev->handle, 1, + s3_wmi_attach_spi_device, NULL, + &ts_adev, NULL); + if (ACPI_FAILURE(status)) + dev_warn(dev, "failed to enumerate SPI slaves\n"); + + if (!ts_adev) + return 0; + + s3_wmi.touchscreen_adev = ts_adev; + + return 0; +} + +static int s3_wmi_create_and_register_input(struct platform_device *pdev) +{ + struct input_dev *input; + int error; + + input = devm_input_allocate_device(&pdev->dev); + if (!input) + return -ENOMEM; + + input->name = "Lid Switch"; + input->phys = "button/input0"; + input->id.bustype = BUS_HOST; + input->id.product = 0x0005; + + input_set_capability(input, EV_SW, SW_LID); + + error = input_register_device(input); + if (error) + return error; + + s3_wmi.input = input; + + return 0; +} + +static int __init s3_wmi_probe(struct platform_device *pdev) +{ + int error; + + if (!dmi_check_system(surface3_dmi_table)) + return -ENODEV; + + memset(&s3_wmi, 0, sizeof(s3_wmi)); + + bus_for_each_dev(&platform_bus_type, NULL, NULL, + s3_wmi_check_platform_device); + + if (!s3_wmi.touchscreen_adev) + return -ENODEV; + + acpi_bus_trim(s3_wmi.pnp0c0d_adev); + + error = s3_wmi_create_and_register_input(pdev); + if (error) + goto restore_acpi_lid; + + acpi_initialize_hp_context(s3_wmi.touchscreen_adev, &s3_wmi.hp, + s3_wmi_hp_notify, NULL); + + s3_wmi_send_lid_state(); + + return 0; + + restore_acpi_lid: + acpi_bus_scan(s3_wmi.pnp0c0d_adev->handle); + return error; +} + +static void s3_wmi_remove(struct platform_device *device) +{ + /* remove the hotplug context from the acpi device */ + s3_wmi.touchscreen_adev->hp = NULL; + + /* reinstall the actual PNPC0C0D LID default handle */ + acpi_bus_scan(s3_wmi.pnp0c0d_adev->handle); +} + +static int __maybe_unused s3_wmi_resume(struct device *dev) +{ + s3_wmi_send_lid_state(); + return 0; +} +static SIMPLE_DEV_PM_OPS(s3_wmi_pm, NULL, s3_wmi_resume); + +static struct platform_driver s3_wmi_driver = { + .driver = { + .name = "surface3-wmi", + .pm = &s3_wmi_pm, + }, + .remove = s3_wmi_remove, +}; + +static int __init s3_wmi_init(void) +{ + int error; + + s3_wmi_pdev = platform_device_alloc("surface3-wmi", -1); + if (!s3_wmi_pdev) + return -ENOMEM; + + error = platform_device_add(s3_wmi_pdev); + if (error) + goto err_device_put; + + error = platform_driver_probe(&s3_wmi_driver, s3_wmi_probe); + if (error) + goto err_device_del; + + pr_info("Surface 3 WMI Extras loaded\n"); + return 0; + + err_device_del: + platform_device_del(s3_wmi_pdev); + err_device_put: + platform_device_put(s3_wmi_pdev); + return error; +} + +static void __exit s3_wmi_exit(void) +{ + platform_device_unregister(s3_wmi_pdev); + platform_driver_unregister(&s3_wmi_driver); +} + +module_init(s3_wmi_init); +module_exit(s3_wmi_exit); diff --git a/drivers/platform/surface/surface3_power.c b/drivers/platform/surface/surface3_power.c new file mode 100644 index 000000000..94fdddc7f --- /dev/null +++ b/drivers/platform/surface/surface3_power.c @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Supports for the power IC on the Surface 3 tablet. + * + * (C) Copyright 2016-2018 Red Hat, Inc + * (C) Copyright 2016-2018 Benjamin Tissoires + * (C) Copyright 2016 Stephen Just + * + * This driver has been reverse-engineered by parsing the DSDT of the Surface 3 + * and looking at the registers of the chips. + * + * The DSDT allowed to find out that: + * - the driver is required for the ACPI BAT0 device to communicate to the chip + * through an operation region. + * - the various defines for the operation region functions to communicate with + * this driver + * - the DSM 3f99e367-6220-4955-8b0f-06ef2ae79412 allows to trigger ACPI + * events to BAT0 (the code is all available in the DSDT). + * + * Further findings regarding the 2 chips declared in the MSHW0011 are: + * - there are 2 chips declared: + * . 0x22 seems to control the ADP1 line status (and probably the charger) + * . 0x55 controls the battery directly + * - the battery chip uses a SMBus protocol (using plain SMBus allows non + * destructive commands): + * . the commands/registers used are in the range 0x00..0x7F + * . if bit 8 (0x80) is set in the SMBus command, the returned value is the + * same as when it is not set. There is a high chance this bit is the + * read/write + * . the various registers semantic as been deduced by observing the register + * dumps. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SURFACE_3_POLL_INTERVAL (2 * HZ) +#define SURFACE_3_STRLEN 10 + +struct mshw0011_data { + struct i2c_client *adp1; + struct i2c_client *bat0; + unsigned short notify_mask; + struct task_struct *poll_task; + bool kthread_running; + + bool charging; + bool bat_charging; + u8 trip_point; + s32 full_capacity; +}; + +struct mshw0011_handler_data { + struct acpi_connection_info info; + struct i2c_client *client; +}; + +struct bix { + u32 revision; + u32 power_unit; + u32 design_capacity; + u32 last_full_charg_capacity; + u32 battery_technology; + u32 design_voltage; + u32 design_capacity_of_warning; + u32 design_capacity_of_low; + u32 cycle_count; + u32 measurement_accuracy; + u32 max_sampling_time; + u32 min_sampling_time; + u32 max_average_interval; + u32 min_average_interval; + u32 battery_capacity_granularity_1; + u32 battery_capacity_granularity_2; + char model[SURFACE_3_STRLEN]; + char serial[SURFACE_3_STRLEN]; + char type[SURFACE_3_STRLEN]; + char OEM[SURFACE_3_STRLEN]; +} __packed; + +struct bst { + u32 battery_state; + s32 battery_present_rate; + u32 battery_remaining_capacity; + u32 battery_present_voltage; +} __packed; + +struct gsb_command { + u8 arg0; + u8 arg1; + u8 arg2; +} __packed; + +struct gsb_buffer { + u8 status; + u8 len; + u8 ret; + union { + struct gsb_command cmd; + struct bst bst; + struct bix bix; + } __packed; +} __packed; + +#define ACPI_BATTERY_STATE_DISCHARGING BIT(0) +#define ACPI_BATTERY_STATE_CHARGING BIT(1) +#define ACPI_BATTERY_STATE_CRITICAL BIT(2) + +#define MSHW0011_CMD_DEST_BAT0 0x01 +#define MSHW0011_CMD_DEST_ADP1 0x03 + +#define MSHW0011_CMD_BAT0_STA 0x01 +#define MSHW0011_CMD_BAT0_BIX 0x02 +#define MSHW0011_CMD_BAT0_BCT 0x03 +#define MSHW0011_CMD_BAT0_BTM 0x04 +#define MSHW0011_CMD_BAT0_BST 0x05 +#define MSHW0011_CMD_BAT0_BTP 0x06 +#define MSHW0011_CMD_ADP1_PSR 0x07 +#define MSHW0011_CMD_BAT0_PSOC 0x09 +#define MSHW0011_CMD_BAT0_PMAX 0x0a +#define MSHW0011_CMD_BAT0_PSRC 0x0b +#define MSHW0011_CMD_BAT0_CHGI 0x0c +#define MSHW0011_CMD_BAT0_ARTG 0x0d + +#define MSHW0011_NOTIFY_GET_VERSION 0x00 +#define MSHW0011_NOTIFY_ADP1 0x01 +#define MSHW0011_NOTIFY_BAT0_BST 0x02 +#define MSHW0011_NOTIFY_BAT0_BIX 0x05 + +#define MSHW0011_ADP1_REG_PSR 0x04 + +#define MSHW0011_BAT0_REG_CAPACITY 0x0c +#define MSHW0011_BAT0_REG_FULL_CHG_CAPACITY 0x0e +#define MSHW0011_BAT0_REG_DESIGN_CAPACITY 0x40 +#define MSHW0011_BAT0_REG_VOLTAGE 0x08 +#define MSHW0011_BAT0_REG_RATE 0x14 +#define MSHW0011_BAT0_REG_OEM 0x45 +#define MSHW0011_BAT0_REG_TYPE 0x4e +#define MSHW0011_BAT0_REG_SERIAL_NO 0x56 +#define MSHW0011_BAT0_REG_CYCLE_CNT 0x6e + +#define MSHW0011_EV_2_5_MASK GENMASK(8, 0) + +/* 3f99e367-6220-4955-8b0f-06ef2ae79412 */ +static const guid_t mshw0011_guid = + GUID_INIT(0x3F99E367, 0x6220, 0x4955, 0x8B, 0x0F, 0x06, 0xEF, + 0x2A, 0xE7, 0x94, 0x12); + +static int +mshw0011_notify(struct mshw0011_data *cdata, u8 arg1, u8 arg2, + unsigned int *ret_value) +{ + union acpi_object *obj; + acpi_handle handle; + unsigned int i; + + handle = ACPI_HANDLE(&cdata->adp1->dev); + if (!handle) + return -ENODEV; + + obj = acpi_evaluate_dsm_typed(handle, &mshw0011_guid, arg1, arg2, NULL, + ACPI_TYPE_BUFFER); + if (!obj) { + dev_err(&cdata->adp1->dev, "device _DSM execution failed\n"); + return -ENODEV; + } + + *ret_value = 0; + for (i = 0; i < obj->buffer.length; i++) + *ret_value |= obj->buffer.pointer[i] << (i * 8); + + ACPI_FREE(obj); + return 0; +} + +static const struct bix default_bix = { + .revision = 0x00, + .power_unit = 0x01, + .design_capacity = 0x1dca, + .last_full_charg_capacity = 0x1dca, + .battery_technology = 0x01, + .design_voltage = 0x10df, + .design_capacity_of_warning = 0x8f, + .design_capacity_of_low = 0x47, + .cycle_count = 0xffffffff, + .measurement_accuracy = 0x00015f90, + .max_sampling_time = 0x03e8, + .min_sampling_time = 0x03e8, + .max_average_interval = 0x03e8, + .min_average_interval = 0x03e8, + .battery_capacity_granularity_1 = 0x45, + .battery_capacity_granularity_2 = 0x11, + .model = "P11G8M", + .serial = "", + .type = "LION", + .OEM = "", +}; + +static int mshw0011_bix(struct mshw0011_data *cdata, struct bix *bix) +{ + struct i2c_client *client = cdata->bat0; + char buf[SURFACE_3_STRLEN]; + int ret; + + *bix = default_bix; + + /* get design capacity */ + ret = i2c_smbus_read_word_data(client, + MSHW0011_BAT0_REG_DESIGN_CAPACITY); + if (ret < 0) { + dev_err(&client->dev, "Error reading design capacity: %d\n", + ret); + return ret; + } + bix->design_capacity = ret; + + /* get last full charge capacity */ + ret = i2c_smbus_read_word_data(client, + MSHW0011_BAT0_REG_FULL_CHG_CAPACITY); + if (ret < 0) { + dev_err(&client->dev, + "Error reading last full charge capacity: %d\n", ret); + return ret; + } + bix->last_full_charg_capacity = ret; + + /* + * Get serial number, on some devices (with unofficial replacement + * battery?) reading any of the serial number range addresses gets + * nacked in this case just leave the serial number empty. + */ + ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_SERIAL_NO, + sizeof(buf), buf); + if (ret == -EREMOTEIO) { + /* no serial number available */ + } else if (ret != sizeof(buf)) { + dev_err(&client->dev, "Error reading serial no: %d\n", ret); + return ret; + } else { + snprintf(bix->serial, ARRAY_SIZE(bix->serial), "%3pE%6pE", buf + 7, buf); + } + + /* get cycle count */ + ret = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CYCLE_CNT); + if (ret < 0) { + dev_err(&client->dev, "Error reading cycle count: %d\n", ret); + return ret; + } + bix->cycle_count = ret; + + /* get OEM name */ + ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_OEM, + 4, buf); + if (ret != 4) { + dev_err(&client->dev, "Error reading cycle count: %d\n", ret); + return ret; + } + snprintf(bix->OEM, ARRAY_SIZE(bix->OEM), "%3pE", buf); + + return 0; +} + +static int mshw0011_bst(struct mshw0011_data *cdata, struct bst *bst) +{ + struct i2c_client *client = cdata->bat0; + int rate, capacity, voltage, state; + s16 tmp; + + rate = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_RATE); + if (rate < 0) + return rate; + + capacity = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CAPACITY); + if (capacity < 0) + return capacity; + + voltage = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_VOLTAGE); + if (voltage < 0) + return voltage; + + tmp = rate; + bst->battery_present_rate = abs((s32)tmp); + + state = 0; + if ((s32) tmp > 0) + state |= ACPI_BATTERY_STATE_CHARGING; + else if ((s32) tmp < 0) + state |= ACPI_BATTERY_STATE_DISCHARGING; + bst->battery_state = state; + + bst->battery_remaining_capacity = capacity; + bst->battery_present_voltage = voltage; + + return 0; +} + +static int mshw0011_adp_psr(struct mshw0011_data *cdata) +{ + return i2c_smbus_read_byte_data(cdata->adp1, MSHW0011_ADP1_REG_PSR); +} + +static int mshw0011_isr(struct mshw0011_data *cdata) +{ + struct bst bst; + struct bix bix; + int ret; + bool status, bat_status; + + ret = mshw0011_adp_psr(cdata); + if (ret < 0) + return ret; + + status = ret; + if (status != cdata->charging) + mshw0011_notify(cdata, cdata->notify_mask, + MSHW0011_NOTIFY_ADP1, &ret); + + cdata->charging = status; + + ret = mshw0011_bst(cdata, &bst); + if (ret < 0) + return ret; + + bat_status = bst.battery_state; + if (bat_status != cdata->bat_charging) + mshw0011_notify(cdata, cdata->notify_mask, + MSHW0011_NOTIFY_BAT0_BST, &ret); + + cdata->bat_charging = bat_status; + + ret = mshw0011_bix(cdata, &bix); + if (ret < 0) + return ret; + + if (bix.last_full_charg_capacity != cdata->full_capacity) + mshw0011_notify(cdata, cdata->notify_mask, + MSHW0011_NOTIFY_BAT0_BIX, &ret); + + cdata->full_capacity = bix.last_full_charg_capacity; + + return 0; +} + +static int mshw0011_poll_task(void *data) +{ + struct mshw0011_data *cdata = data; + int ret = 0; + + cdata->kthread_running = true; + + set_freezable(); + + while (!kthread_should_stop()) { + schedule_timeout_interruptible(SURFACE_3_POLL_INTERVAL); + try_to_freeze(); + ret = mshw0011_isr(data); + if (ret) + break; + } + + cdata->kthread_running = false; + return ret; +} + +static acpi_status +mshw0011_space_handler(u32 function, acpi_physical_address command, + u32 bits, u64 *value64, + void *handler_context, void *region_context) +{ + struct gsb_buffer *gsb = (struct gsb_buffer *)value64; + struct mshw0011_handler_data *data = handler_context; + struct acpi_connection_info *info = &data->info; + struct acpi_resource_i2c_serialbus *sb; + struct i2c_client *client = data->client; + struct mshw0011_data *cdata = i2c_get_clientdata(client); + struct acpi_resource *ares; + u32 accessor_type = function >> 16; + acpi_status ret; + int status = 1; + + ret = acpi_buffer_to_resource(info->connection, info->length, &ares); + if (ACPI_FAILURE(ret)) + return ret; + + if (!value64 || !i2c_acpi_get_i2c_resource(ares, &sb)) { + ret = AE_BAD_PARAMETER; + goto err; + } + + if (accessor_type != ACPI_GSB_ACCESS_ATTRIB_RAW_PROCESS) { + ret = AE_BAD_PARAMETER; + goto err; + } + + if (gsb->cmd.arg0 == MSHW0011_CMD_DEST_ADP1 && + gsb->cmd.arg1 == MSHW0011_CMD_ADP1_PSR) { + status = mshw0011_adp_psr(cdata); + if (status >= 0) { + ret = AE_OK; + goto out; + } else { + ret = AE_ERROR; + goto err; + } + } + + if (gsb->cmd.arg0 != MSHW0011_CMD_DEST_BAT0) { + ret = AE_BAD_PARAMETER; + goto err; + } + + switch (gsb->cmd.arg1) { + case MSHW0011_CMD_BAT0_STA: + break; + case MSHW0011_CMD_BAT0_BIX: + ret = mshw0011_bix(cdata, &gsb->bix); + break; + case MSHW0011_CMD_BAT0_BTP: + cdata->trip_point = gsb->cmd.arg2; + break; + case MSHW0011_CMD_BAT0_BST: + ret = mshw0011_bst(cdata, &gsb->bst); + break; + default: + dev_info(&cdata->bat0->dev, "command(0x%02x) is not supported.\n", gsb->cmd.arg1); + ret = AE_BAD_PARAMETER; + goto err; + } + + out: + gsb->ret = status; + gsb->status = 0; + + err: + ACPI_FREE(ares); + return ret; +} + +static int mshw0011_install_space_handler(struct i2c_client *client) +{ + struct acpi_device *adev; + struct mshw0011_handler_data *data; + acpi_status status; + + adev = ACPI_COMPANION(&client->dev); + if (!adev) + return -ENODEV; + + data = kzalloc_obj(struct mshw0011_handler_data); + if (!data) + return -ENOMEM; + + data->client = client; + status = acpi_bus_attach_private_data(adev->handle, (void *)data); + if (ACPI_FAILURE(status)) { + kfree(data); + return -ENOMEM; + } + + status = acpi_install_address_space_handler(adev->handle, + ACPI_ADR_SPACE_GSBUS, + &mshw0011_space_handler, + NULL, + data); + if (ACPI_FAILURE(status)) { + dev_err(&client->dev, "Error installing i2c space handler\n"); + acpi_bus_detach_private_data(adev->handle); + kfree(data); + return -ENOMEM; + } + + acpi_dev_clear_dependencies(adev); + return 0; +} + +static void mshw0011_remove_space_handler(struct i2c_client *client) +{ + struct mshw0011_handler_data *data; + acpi_handle handle; + acpi_status status; + + handle = ACPI_HANDLE(&client->dev); + if (!handle) + return; + + acpi_remove_address_space_handler(handle, + ACPI_ADR_SPACE_GSBUS, + &mshw0011_space_handler); + + status = acpi_bus_get_private_data(handle, (void **)&data); + if (ACPI_SUCCESS(status)) + kfree(data); + + acpi_bus_detach_private_data(handle); +} + +static int mshw0011_probe(struct i2c_client *client) +{ + struct i2c_board_info board_info; + struct device *dev = &client->dev; + struct i2c_client *bat0; + struct mshw0011_data *data; + int error, mask; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->adp1 = client; + i2c_set_clientdata(client, data); + + memset(&board_info, 0, sizeof(board_info)); + strscpy(board_info.type, "MSHW0011-bat0", I2C_NAME_SIZE); + + bat0 = i2c_acpi_new_device(dev, 1, &board_info); + if (IS_ERR(bat0)) + return PTR_ERR(bat0); + + data->bat0 = bat0; + i2c_set_clientdata(bat0, data); + + error = mshw0011_notify(data, 1, MSHW0011_NOTIFY_GET_VERSION, &mask); + if (error) + goto out_err; + + data->notify_mask = mask == MSHW0011_EV_2_5_MASK; + + data->poll_task = kthread_run(mshw0011_poll_task, data, "mshw0011_adp"); + if (IS_ERR(data->poll_task)) { + error = PTR_ERR(data->poll_task); + dev_err(&client->dev, "Unable to run kthread err %d\n", error); + goto out_err; + } + + error = mshw0011_install_space_handler(client); + if (error) + goto out_err; + + return 0; + +out_err: + if (data->kthread_running) + kthread_stop(data->poll_task); + i2c_unregister_device(data->bat0); + return error; +} + +static void mshw0011_remove(struct i2c_client *client) +{ + struct mshw0011_data *cdata = i2c_get_clientdata(client); + + mshw0011_remove_space_handler(client); + + if (cdata->kthread_running) + kthread_stop(cdata->poll_task); + + i2c_unregister_device(cdata->bat0); +} + +static const struct acpi_device_id mshw0011_acpi_match[] = { + { "MSHW0011", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, mshw0011_acpi_match); + +static struct i2c_driver mshw0011_driver = { + .probe = mshw0011_probe, + .remove = mshw0011_remove, + .driver = { + .name = "mshw0011", + .acpi_match_table = mshw0011_acpi_match, + }, +}; +module_i2c_driver(mshw0011_driver); + +MODULE_AUTHOR("Benjamin Tissoires "); +MODULE_DESCRIPTION("mshw0011 driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/surface/surface_acpi_notify.c b/drivers/platform/surface/surface_acpi_notify.c new file mode 100644 index 000000000..a9dcb0bbe --- /dev/null +++ b/drivers/platform/surface/surface_acpi_notify.c @@ -0,0 +1,884 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for the Surface ACPI Notify (SAN) interface/shim. + * + * Translates communication from ACPI to Surface System Aggregator Module + * (SSAM/SAM) requests and back, specifically SAM-over-SSH. Translates SSAM + * events back to ACPI notifications. Allows handling of discrete GPU + * notifications sent from ACPI via the SAN interface by providing them to any + * registered external driver. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +struct san_data { + struct device *dev; + struct ssam_controller *ctrl; + + struct acpi_connection_info info; + + struct ssam_event_notifier nf_bat; + struct ssam_event_notifier nf_tmp; +}; + +#define to_san_data(ptr, member) \ + container_of(ptr, struct san_data, member) + +static struct workqueue_struct *san_wq; + +/* -- dGPU notifier interface. ---------------------------------------------- */ + +struct san_rqsg_if { + struct rw_semaphore lock; + struct device *dev; + struct blocking_notifier_head nh; +}; + +static struct san_rqsg_if san_rqsg_if = { + .lock = __RWSEM_INITIALIZER(san_rqsg_if.lock), + .dev = NULL, + .nh = BLOCKING_NOTIFIER_INIT(san_rqsg_if.nh), +}; + +static int san_set_rqsg_interface_device(struct device *dev) +{ + int status = 0; + + down_write(&san_rqsg_if.lock); + if (!san_rqsg_if.dev && dev) + san_rqsg_if.dev = dev; + else + status = -EBUSY; + up_write(&san_rqsg_if.lock); + + return status; +} + +/** + * san_client_link() - Link client as consumer to SAN device. + * @client: The client to link. + * + * Sets up a device link between the provided client device as consumer and + * the SAN device as provider. This function can be used to ensure that the + * SAN interface has been set up and will be set up for as long as the driver + * of the client device is bound. This guarantees that, during that time, all + * dGPU events will be received by any registered notifier. + * + * The link will be automatically removed once the client device's driver is + * unbound. + * + * Return: Returns zero on success, %-ENXIO if the SAN interface has not been + * set up yet, and %-ENOMEM if device link creation failed. + */ +int san_client_link(struct device *client) +{ + const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_CONSUMER; + struct device_link *link; + + down_read(&san_rqsg_if.lock); + + if (!san_rqsg_if.dev) { + up_read(&san_rqsg_if.lock); + return -ENXIO; + } + + link = device_link_add(client, san_rqsg_if.dev, flags); + if (!link) { + up_read(&san_rqsg_if.lock); + return -ENOMEM; + } + + if (READ_ONCE(link->status) == DL_STATE_SUPPLIER_UNBIND) { + up_read(&san_rqsg_if.lock); + return -ENXIO; + } + + up_read(&san_rqsg_if.lock); + return 0; +} +EXPORT_SYMBOL_GPL(san_client_link); + +/** + * san_dgpu_notifier_register() - Register a SAN dGPU notifier. + * @nb: The notifier-block to register. + * + * Registers a SAN dGPU notifier, receiving any new SAN dGPU events sent from + * ACPI. The registered notifier will be called with &struct san_dgpu_event + * as notifier data and the command ID of that event as notifier action. + */ +int san_dgpu_notifier_register(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&san_rqsg_if.nh, nb); +} +EXPORT_SYMBOL_GPL(san_dgpu_notifier_register); + +/** + * san_dgpu_notifier_unregister() - Unregister a SAN dGPU notifier. + * @nb: The notifier-block to unregister. + */ +int san_dgpu_notifier_unregister(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&san_rqsg_if.nh, nb); +} +EXPORT_SYMBOL_GPL(san_dgpu_notifier_unregister); + +static int san_dgpu_notifier_call(struct san_dgpu_event *evt) +{ + int ret; + + ret = blocking_notifier_call_chain(&san_rqsg_if.nh, evt->command, evt); + return notifier_to_errno(ret); +} + + +/* -- ACPI _DSM event relay. ------------------------------------------------ */ + +#define SAN_DSM_REVISION 0 + +/* 93b666c5-70c6-469f-a215-3d487c91ab3c */ +static const guid_t SAN_DSM_UUID = + GUID_INIT(0x93b666c5, 0x70c6, 0x469f, 0xa2, 0x15, 0x3d, + 0x48, 0x7c, 0x91, 0xab, 0x3c); + +enum san_dsm_event_fn { + SAN_DSM_EVENT_FN_BAT1_STAT = 0x03, + SAN_DSM_EVENT_FN_BAT1_INFO = 0x04, + SAN_DSM_EVENT_FN_ADP1_STAT = 0x05, + SAN_DSM_EVENT_FN_ADP1_INFO = 0x06, + SAN_DSM_EVENT_FN_BAT2_STAT = 0x07, + SAN_DSM_EVENT_FN_BAT2_INFO = 0x08, + SAN_DSM_EVENT_FN_THERMAL = 0x09, + SAN_DSM_EVENT_FN_DPTF = 0x0a, +}; + +enum sam_event_cid_bat { + SAM_EVENT_CID_BAT_BIX = 0x15, + SAM_EVENT_CID_BAT_BST = 0x16, + SAM_EVENT_CID_BAT_ADP = 0x17, + SAM_EVENT_CID_BAT_PROT = 0x18, + SAM_EVENT_CID_BAT_DPTF = 0x4f, +}; + +enum sam_event_cid_tmp { + SAM_EVENT_CID_TMP_TRIP = 0x0b, +}; + +struct san_event_work { + struct delayed_work work; + struct device *dev; + struct ssam_event event; /* must be last */ +}; + +static int san_acpi_notify_event(struct device *dev, u64 func, + union acpi_object *param) +{ + acpi_handle san = ACPI_HANDLE(dev); + union acpi_object *obj; + int status = 0; + + if (!acpi_check_dsm(san, &SAN_DSM_UUID, SAN_DSM_REVISION, BIT_ULL(func))) + return 0; + + dev_dbg(dev, "notify event %#04llx\n", func); + + obj = acpi_evaluate_dsm_typed(san, &SAN_DSM_UUID, SAN_DSM_REVISION, + func, param, ACPI_TYPE_BUFFER); + if (!obj) + return -EFAULT; + + if (obj->buffer.length != 1 || obj->buffer.pointer[0] != 0) { + dev_err(dev, "got unexpected result from _DSM\n"); + status = -EPROTO; + } + + ACPI_FREE(obj); + return status; +} + +static int san_evt_bat_adp(struct device *dev, const struct ssam_event *event) +{ + int status; + + status = san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_ADP1_STAT, NULL); + if (status) + return status; + + /* + * Ensure that the battery states get updated correctly. When the + * battery is fully charged and an adapter is plugged in, it sometimes + * is not updated correctly, instead showing it as charging. + * Explicitly trigger battery updates to fix this. + */ + + status = san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_BAT1_STAT, NULL); + if (status) + return status; + + return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_BAT2_STAT, NULL); +} + +static int san_evt_bat_bix(struct device *dev, const struct ssam_event *event) +{ + enum san_dsm_event_fn fn; + + if (event->instance_id == 0x02) + fn = SAN_DSM_EVENT_FN_BAT2_INFO; + else + fn = SAN_DSM_EVENT_FN_BAT1_INFO; + + return san_acpi_notify_event(dev, fn, NULL); +} + +static int san_evt_bat_bst(struct device *dev, const struct ssam_event *event) +{ + enum san_dsm_event_fn fn; + + if (event->instance_id == 0x02) + fn = SAN_DSM_EVENT_FN_BAT2_STAT; + else + fn = SAN_DSM_EVENT_FN_BAT1_STAT; + + return san_acpi_notify_event(dev, fn, NULL); +} + +static int san_evt_bat_dptf(struct device *dev, const struct ssam_event *event) +{ + union acpi_object payload; + + /* + * The Surface ACPI expects a buffer and not a package. It specifically + * checks for ObjectType (Arg3) == 0x03. This will cause a warning in + * acpica/nsarguments.c, but that warning can be safely ignored. + */ + payload.type = ACPI_TYPE_BUFFER; + payload.buffer.length = event->length; + payload.buffer.pointer = (u8 *)&event->data[0]; + + return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_DPTF, &payload); +} + +static unsigned long san_evt_bat_delay(u8 cid) +{ + switch (cid) { + case SAM_EVENT_CID_BAT_ADP: + /* + * Wait for battery state to update before signaling adapter + * change. + */ + return msecs_to_jiffies(5000); + + case SAM_EVENT_CID_BAT_BST: + /* Ensure we do not miss anything important due to caching. */ + return msecs_to_jiffies(2000); + + default: + return 0; + } +} + +static bool san_evt_bat(const struct ssam_event *event, struct device *dev) +{ + int status; + + switch (event->command_id) { + case SAM_EVENT_CID_BAT_BIX: + status = san_evt_bat_bix(dev, event); + break; + + case SAM_EVENT_CID_BAT_BST: + status = san_evt_bat_bst(dev, event); + break; + + case SAM_EVENT_CID_BAT_ADP: + status = san_evt_bat_adp(dev, event); + break; + + case SAM_EVENT_CID_BAT_PROT: + /* + * TODO: Implement support for battery protection status change + * event. + */ + return true; + + case SAM_EVENT_CID_BAT_DPTF: + status = san_evt_bat_dptf(dev, event); + break; + + default: + return false; + } + + if (status) { + dev_err(dev, "error handling power event (cid = %#04x)\n", + event->command_id); + } + + return true; +} + +static void san_evt_bat_workfn(struct work_struct *work) +{ + struct san_event_work *ev; + + ev = container_of(work, struct san_event_work, work.work); + san_evt_bat(&ev->event, ev->dev); + kfree(ev); +} + +static u32 san_evt_bat_nf(struct ssam_event_notifier *nf, + const struct ssam_event *event) +{ + struct san_data *d = to_san_data(nf, nf_bat); + struct san_event_work *work; + unsigned long delay = san_evt_bat_delay(event->command_id); + + if (delay == 0) + return san_evt_bat(event, d->dev) ? SSAM_NOTIF_HANDLED : 0; + + work = kzalloc(sizeof(*work) + event->length, GFP_KERNEL); + if (!work) + return ssam_notifier_from_errno(-ENOMEM); + + INIT_DELAYED_WORK(&work->work, san_evt_bat_workfn); + work->dev = d->dev; + + work->event = *event; + memcpy(work->event.data, event->data, event->length); + + queue_delayed_work(san_wq, &work->work, delay); + return SSAM_NOTIF_HANDLED; +} + +static int san_evt_tmp_trip(struct device *dev, const struct ssam_event *event) +{ + union acpi_object param; + + /* + * The Surface ACPI expects an integer and not a package. This will + * cause a warning in acpica/nsarguments.c, but that warning can be + * safely ignored. + */ + param.type = ACPI_TYPE_INTEGER; + param.integer.value = event->instance_id; + + return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_THERMAL, ¶m); +} + +static bool san_evt_tmp(const struct ssam_event *event, struct device *dev) +{ + int status; + + switch (event->command_id) { + case SAM_EVENT_CID_TMP_TRIP: + status = san_evt_tmp_trip(dev, event); + break; + + default: + return false; + } + + if (status) { + dev_err(dev, "error handling thermal event (cid = %#04x)\n", + event->command_id); + } + + return true; +} + +static u32 san_evt_tmp_nf(struct ssam_event_notifier *nf, + const struct ssam_event *event) +{ + struct san_data *d = to_san_data(nf, nf_tmp); + + return san_evt_tmp(event, d->dev) ? SSAM_NOTIF_HANDLED : 0; +} + + +/* -- ACPI GSB OperationRegion handler -------------------------------------- */ + +struct gsb_data_in { + u8 cv; +} __packed; + +struct gsb_data_rqsx { + u8 cv; /* Command value (san_gsb_request_cv). */ + u8 tc; /* Target category. */ + u8 tid; /* Target ID. */ + u8 iid; /* Instance ID. */ + u8 snc; /* Expect-response-flag. */ + u8 cid; /* Command ID. */ + u16 cdl; /* Payload length. */ + u8 pld[]; /* Payload. */ +} __packed; + +struct gsb_data_etwl { + u8 cv; /* Command value (should be 0x02). */ + u8 etw3; /* Unknown. */ + u8 etw4; /* Unknown. */ + u8 msg[]; /* Error message (ASCIIZ). */ +} __packed; + +struct gsb_data_out { + u8 status; /* _SSH communication status. */ + u8 len; /* _SSH payload length. */ + u8 pld[]; /* _SSH payload. */ +} __packed; + +union gsb_buffer_data { + struct gsb_data_in in; /* Common input. */ + struct gsb_data_rqsx rqsx; /* RQSX input. */ + struct gsb_data_etwl etwl; /* ETWL input. */ + struct gsb_data_out out; /* Output. */ +}; + +struct gsb_buffer { + u8 status; /* GSB AttribRawProcess status. */ + u8 len; /* GSB AttribRawProcess length. */ + union gsb_buffer_data data; +} __packed; + +#define SAN_GSB_MAX_RQSX_PAYLOAD (U8_MAX - 2 - sizeof(struct gsb_data_rqsx)) +#define SAN_GSB_MAX_RESPONSE (U8_MAX - 2 - sizeof(struct gsb_data_out)) + +#define SAN_GSB_COMMAND 0 + +enum san_gsb_request_cv { + SAN_GSB_REQUEST_CV_RQST = 0x01, + SAN_GSB_REQUEST_CV_ETWL = 0x02, + SAN_GSB_REQUEST_CV_RQSG = 0x03, +}; + +#define SAN_REQUEST_NUM_TRIES 5 + +static acpi_status san_etwl(struct san_data *d, struct gsb_buffer *b) +{ + struct gsb_data_etwl *etwl = &b->data.etwl; + + if (b->len < sizeof(struct gsb_data_etwl)) { + dev_err(d->dev, "invalid ETWL package (len = %d)\n", b->len); + return AE_OK; + } + + dev_err(d->dev, "ETWL(%#04x, %#04x): %.*s\n", etwl->etw3, etwl->etw4, + (unsigned int)(b->len - sizeof(struct gsb_data_etwl)), + (char *)etwl->msg); + + /* Indicate success. */ + b->status = 0x00; + b->len = 0x00; + + return AE_OK; +} + +static +struct gsb_data_rqsx *san_validate_rqsx(struct device *dev, const char *type, + struct gsb_buffer *b) +{ + struct gsb_data_rqsx *rqsx = &b->data.rqsx; + + if (b->len < sizeof(struct gsb_data_rqsx)) { + dev_err(dev, "invalid %s package (len = %d)\n", type, b->len); + return NULL; + } + + if (get_unaligned(&rqsx->cdl) != b->len - sizeof(struct gsb_data_rqsx)) { + dev_err(dev, "bogus %s package (len = %d, cdl = %d)\n", + type, b->len, get_unaligned(&rqsx->cdl)); + return NULL; + } + + if (get_unaligned(&rqsx->cdl) > SAN_GSB_MAX_RQSX_PAYLOAD) { + dev_err(dev, "payload for %s package too large (cdl = %d)\n", + type, get_unaligned(&rqsx->cdl)); + return NULL; + } + + return rqsx; +} + +static void gsb_rqsx_response_error(struct gsb_buffer *gsb, int status) +{ + gsb->status = 0x00; + gsb->len = 0x02; + gsb->data.out.status = (u8)(-status); + gsb->data.out.len = 0x00; +} + +static void gsb_rqsx_response_success(struct gsb_buffer *gsb, u8 *ptr, size_t len) +{ + gsb->status = 0x00; + gsb->len = len + 2; + gsb->data.out.status = 0x00; + gsb->data.out.len = len; + + if (len) + memcpy(&gsb->data.out.pld[0], ptr, len); +} + +static acpi_status san_rqst_fixup_suspended(struct san_data *d, + struct ssam_request *rqst, + struct gsb_buffer *gsb) +{ + if (rqst->target_category == SSAM_SSH_TC_BAS && rqst->command_id == 0x0D) { + u8 base_state = 1; + + /* Base state quirk: + * The base state may be queried from ACPI when the EC is still + * suspended. In this case it will return '-EPERM'. This query + * will only be triggered from the ACPI lid GPE interrupt, thus + * we are either in laptop or studio mode (base status 0x01 or + * 0x02). Furthermore, we will only get here if the device (and + * EC) have been suspended. + * + * We now assume that the device is in laptop mode (0x01). This + * has the drawback that it will wake the device when unfolding + * it in studio mode, but it also allows us to avoid actively + * waiting for the EC to wake up, which may incur a notable + * delay. + */ + + dev_dbg(d->dev, "rqst: fixup: base-state quirk\n"); + + gsb_rqsx_response_success(gsb, &base_state, sizeof(base_state)); + return AE_OK; + } + + gsb_rqsx_response_error(gsb, -ENXIO); + return AE_OK; +} + +static acpi_status san_rqst(struct san_data *d, struct gsb_buffer *buffer) +{ + u8 rspbuf[SAN_GSB_MAX_RESPONSE]; + struct gsb_data_rqsx *gsb_rqst; + struct ssam_request rqst; + struct ssam_response rsp; + int status = 0; + + gsb_rqst = san_validate_rqsx(d->dev, "RQST", buffer); + if (!gsb_rqst) + return AE_OK; + + rqst.target_category = gsb_rqst->tc; + rqst.target_id = gsb_rqst->tid; + rqst.command_id = gsb_rqst->cid; + rqst.instance_id = gsb_rqst->iid; + rqst.flags = gsb_rqst->snc ? SSAM_REQUEST_HAS_RESPONSE : 0; + rqst.length = get_unaligned(&gsb_rqst->cdl); + rqst.payload = &gsb_rqst->pld[0]; + + rsp.capacity = ARRAY_SIZE(rspbuf); + rsp.length = 0; + rsp.pointer = &rspbuf[0]; + + /* Handle suspended device. */ + if (d->dev->power.is_suspended) { + dev_warn(d->dev, "rqst: device is suspended, not executing\n"); + return san_rqst_fixup_suspended(d, &rqst, buffer); + } + + status = __ssam_retry(ssam_request_do_sync_onstack, SAN_REQUEST_NUM_TRIES, + d->ctrl, &rqst, &rsp, SAN_GSB_MAX_RQSX_PAYLOAD); + + if (!status) { + gsb_rqsx_response_success(buffer, rsp.pointer, rsp.length); + } else { + dev_err(d->dev, "rqst: failed with error %d\n", status); + gsb_rqsx_response_error(buffer, status); + } + + return AE_OK; +} + +static acpi_status san_rqsg(struct san_data *d, struct gsb_buffer *buffer) +{ + struct gsb_data_rqsx *gsb_rqsg; + struct san_dgpu_event evt; + int status; + + gsb_rqsg = san_validate_rqsx(d->dev, "RQSG", buffer); + if (!gsb_rqsg) + return AE_OK; + + evt.category = gsb_rqsg->tc; + evt.target = gsb_rqsg->tid; + evt.command = gsb_rqsg->cid; + evt.instance = gsb_rqsg->iid; + evt.length = get_unaligned(&gsb_rqsg->cdl); + evt.payload = &gsb_rqsg->pld[0]; + + status = san_dgpu_notifier_call(&evt); + if (!status) { + gsb_rqsx_response_success(buffer, NULL, 0); + } else { + dev_err(d->dev, "rqsg: failed with error %d\n", status); + gsb_rqsx_response_error(buffer, status); + } + + return AE_OK; +} + +static acpi_status san_opreg_handler(u32 function, acpi_physical_address command, + u32 bits, u64 *value64, void *opreg_context, + void *region_context) +{ + struct san_data *d = to_san_data(opreg_context, info); + struct gsb_buffer *buffer = (struct gsb_buffer *)value64; + int accessor_type = (function & 0xFFFF0000) >> 16; + + if (command != SAN_GSB_COMMAND) { + dev_warn(d->dev, "unsupported command: %#04llx\n", command); + return AE_OK; + } + + if (accessor_type != ACPI_GSB_ACCESS_ATTRIB_RAW_PROCESS) { + dev_err(d->dev, "invalid access type: %#04x\n", accessor_type); + return AE_OK; + } + + /* Buffer must have at least contain the command-value. */ + if (buffer->len == 0) { + dev_err(d->dev, "request-package too small\n"); + return AE_OK; + } + + switch (buffer->data.in.cv) { + case SAN_GSB_REQUEST_CV_RQST: + return san_rqst(d, buffer); + + case SAN_GSB_REQUEST_CV_ETWL: + return san_etwl(d, buffer); + + case SAN_GSB_REQUEST_CV_RQSG: + return san_rqsg(d, buffer); + + default: + dev_warn(d->dev, "unsupported SAN0 request (cv: %#04x)\n", + buffer->data.in.cv); + return AE_OK; + } +} + + +/* -- Driver setup. --------------------------------------------------------- */ + +static int san_events_register(struct platform_device *pdev) +{ + struct san_data *d = platform_get_drvdata(pdev); + int status; + + d->nf_bat.base.priority = 1; + d->nf_bat.base.fn = san_evt_bat_nf; + d->nf_bat.event.reg = SSAM_EVENT_REGISTRY_SAM; + d->nf_bat.event.id.target_category = SSAM_SSH_TC_BAT; + d->nf_bat.event.id.instance = 0; + d->nf_bat.event.mask = SSAM_EVENT_MASK_TARGET; + d->nf_bat.event.flags = SSAM_EVENT_SEQUENCED; + + d->nf_tmp.base.priority = 1; + d->nf_tmp.base.fn = san_evt_tmp_nf; + d->nf_tmp.event.reg = SSAM_EVENT_REGISTRY_SAM; + d->nf_tmp.event.id.target_category = SSAM_SSH_TC_TMP; + d->nf_tmp.event.id.instance = 0; + d->nf_tmp.event.mask = SSAM_EVENT_MASK_TARGET; + d->nf_tmp.event.flags = SSAM_EVENT_SEQUENCED; + + status = ssam_notifier_register(d->ctrl, &d->nf_bat); + if (status) + return status; + + status = ssam_notifier_register(d->ctrl, &d->nf_tmp); + if (status) + ssam_notifier_unregister(d->ctrl, &d->nf_bat); + + return status; +} + +static void san_events_unregister(struct platform_device *pdev) +{ + struct san_data *d = platform_get_drvdata(pdev); + + ssam_notifier_unregister(d->ctrl, &d->nf_bat); + ssam_notifier_unregister(d->ctrl, &d->nf_tmp); +} + +#define san_consumer_printk(level, dev, handle, fmt, ...) \ +do { \ + char *path = ""; \ + struct acpi_buffer buffer = { \ + .length = ACPI_ALLOCATE_BUFFER, \ + .pointer = NULL, \ + }; \ + \ + if (ACPI_SUCCESS(acpi_get_name(handle, ACPI_FULL_PATHNAME, &buffer))) \ + path = buffer.pointer; \ + \ + dev_##level(dev, "[%s]: " fmt, path, ##__VA_ARGS__); \ + kfree(buffer.pointer); \ +} while (0) + +#define san_consumer_dbg(dev, handle, fmt, ...) \ + san_consumer_printk(dbg, dev, handle, fmt, ##__VA_ARGS__) + +#define san_consumer_warn(dev, handle, fmt, ...) \ + san_consumer_printk(warn, dev, handle, fmt, ##__VA_ARGS__) + +static acpi_status san_consumer_setup(acpi_handle handle, u32 lvl, + void *context, void **rv) +{ + const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_SUPPLIER; + struct platform_device *pdev = context; + struct acpi_device *adev; + struct device_link *link; + + if (!acpi_device_dep(handle, ACPI_HANDLE(&pdev->dev))) + return AE_OK; + + /* Ignore ACPI devices that are not present. */ + adev = acpi_fetch_acpi_dev(handle); + if (!adev) + return AE_OK; + + san_consumer_dbg(&pdev->dev, handle, "creating device link\n"); + + /* Try to set up device links, ignore but log errors. */ + link = device_link_add(&adev->dev, &pdev->dev, flags); + if (!link) { + san_consumer_warn(&pdev->dev, handle, "failed to create device link\n"); + return AE_OK; + } + + return AE_OK; +} + +static int san_consumer_links_setup(struct platform_device *pdev) +{ + acpi_status status; + + status = acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT, + ACPI_UINT32_MAX, san_consumer_setup, NULL, + pdev, NULL); + + return status ? -EFAULT : 0; +} + +static int san_probe(struct platform_device *pdev) +{ + struct acpi_device *san = ACPI_COMPANION(&pdev->dev); + struct ssam_controller *ctrl; + struct san_data *data; + acpi_status astatus; + int status; + + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + status = san_consumer_links_setup(pdev); + if (status) + return status; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = &pdev->dev; + data->ctrl = ctrl; + + platform_set_drvdata(pdev, data); + + astatus = acpi_install_address_space_handler(san->handle, + ACPI_ADR_SPACE_GSBUS, + &san_opreg_handler, NULL, + &data->info); + if (ACPI_FAILURE(astatus)) + return -ENXIO; + + status = san_events_register(pdev); + if (status) + goto err_enable_events; + + status = san_set_rqsg_interface_device(&pdev->dev); + if (status) + goto err_install_dev; + + acpi_dev_clear_dependencies(san); + return 0; + +err_install_dev: + san_events_unregister(pdev); +err_enable_events: + acpi_remove_address_space_handler(san, ACPI_ADR_SPACE_GSBUS, + &san_opreg_handler); + return status; +} + +static void san_remove(struct platform_device *pdev) +{ + acpi_handle san = ACPI_HANDLE(&pdev->dev); + + san_set_rqsg_interface_device(NULL); + acpi_remove_address_space_handler(san, ACPI_ADR_SPACE_GSBUS, + &san_opreg_handler); + san_events_unregister(pdev); + + /* + * We have unregistered our event sources. Now we need to ensure that + * all delayed works they may have spawned are run to completion. + */ + flush_workqueue(san_wq); +} + +static const struct acpi_device_id san_match[] = { + { "MSHW0091" }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, san_match); + +static struct platform_driver surface_acpi_notify = { + .probe = san_probe, + .remove = san_remove, + .driver = { + .name = "surface_acpi_notify", + .acpi_match_table = san_match, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static int __init san_init(void) +{ + int ret; + + san_wq = alloc_workqueue("san_wq", WQ_PERCPU, 0); + if (!san_wq) + return -ENOMEM; + ret = platform_driver_register(&surface_acpi_notify); + if (ret) + destroy_workqueue(san_wq); + return ret; +} +module_init(san_init); + +static void __exit san_exit(void) +{ + platform_driver_unregister(&surface_acpi_notify); + destroy_workqueue(san_wq); +} +module_exit(san_exit); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Surface ACPI Notify driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_aggregator_cdev.c b/drivers/platform/surface/surface_aggregator_cdev.c new file mode 100644 index 000000000..8929937ba --- /dev/null +++ b/drivers/platform/surface/surface_aggregator_cdev.c @@ -0,0 +1,808 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Provides user-space access to the SSAM EC via the /dev/surface/aggregator + * misc device. Intended for debugging and development. + * + * Copyright (C) 2020-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define SSAM_CDEV_DEVICE_NAME "surface_aggregator_cdev" + + +/* -- Main structures. ------------------------------------------------------ */ + +enum ssam_cdev_device_state { + SSAM_CDEV_DEVICE_SHUTDOWN_BIT = BIT(0), +}; + +struct ssam_cdev { + struct kref kref; + struct rw_semaphore lock; + + struct device *dev; + struct ssam_controller *ctrl; + struct miscdevice mdev; + unsigned long flags; + + struct rw_semaphore client_lock; /* Guards client list. */ + struct list_head client_list; +}; + +struct ssam_cdev_client; + +struct ssam_cdev_notifier { + struct ssam_cdev_client *client; + struct ssam_event_notifier nf; +}; + +struct ssam_cdev_client { + struct ssam_cdev *cdev; + struct list_head node; + + struct mutex notifier_lock; /* Guards notifier access for registration */ + struct ssam_cdev_notifier *notifier[SSH_NUM_EVENTS]; + + struct mutex read_lock; /* Guards FIFO buffer read access */ + struct mutex write_lock; /* Guards FIFO buffer write access */ + DECLARE_KFIFO(buffer, u8, 4096); + + wait_queue_head_t waitq; + struct fasync_struct *fasync; +}; + +static void __ssam_cdev_release(struct kref *kref) +{ + kfree(container_of(kref, struct ssam_cdev, kref)); +} + +static struct ssam_cdev *ssam_cdev_get(struct ssam_cdev *cdev) +{ + if (cdev) + kref_get(&cdev->kref); + + return cdev; +} + +static void ssam_cdev_put(struct ssam_cdev *cdev) +{ + if (cdev) + kref_put(&cdev->kref, __ssam_cdev_release); +} + + +/* -- Notifier handling. ---------------------------------------------------- */ + +static u32 ssam_cdev_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in) +{ + struct ssam_cdev_notifier *cdev_nf = container_of(nf, struct ssam_cdev_notifier, nf); + struct ssam_cdev_client *client = cdev_nf->client; + struct ssam_cdev_event event; + size_t n = struct_size(&event, data, in->length); + + /* Translate event. */ + event.target_category = in->target_category; + event.target_id = in->target_id; + event.command_id = in->command_id; + event.instance_id = in->instance_id; + event.length = in->length; + + mutex_lock(&client->write_lock); + + /* Make sure we have enough space. */ + if (kfifo_avail(&client->buffer) < n) { + dev_warn(client->cdev->dev, + "buffer full, dropping event (tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n", + in->target_category, in->target_id, in->command_id, in->instance_id); + mutex_unlock(&client->write_lock); + return 0; + } + + /* Copy event header and payload. */ + kfifo_in(&client->buffer, (const u8 *)&event, struct_size(&event, data, 0)); + kfifo_in(&client->buffer, &in->data[0], in->length); + + mutex_unlock(&client->write_lock); + + /* Notify waiting readers. */ + kill_fasync(&client->fasync, SIGIO, POLL_IN); + wake_up_interruptible(&client->waitq); + + /* + * Don't mark events as handled, this is the job of a proper driver and + * not the debugging interface. + */ + return 0; +} + +static int ssam_cdev_notifier_register(struct ssam_cdev_client *client, u8 tc, int priority) +{ + const u16 rqid = ssh_tc_to_rqid(tc); + const u16 event = ssh_rqid_to_event(rqid); + struct ssam_cdev_notifier *nf; + int status; + + lockdep_assert_held_read(&client->cdev->lock); + + /* Validate notifier target category. */ + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + mutex_lock(&client->notifier_lock); + + /* Check if the notifier has already been registered. */ + if (client->notifier[event]) { + mutex_unlock(&client->notifier_lock); + return -EEXIST; + } + + /* Allocate new notifier. */ + nf = kzalloc_obj(*nf); + if (!nf) { + mutex_unlock(&client->notifier_lock); + return -ENOMEM; + } + + /* + * Create a dummy notifier with the minimal required fields for + * observer registration. Note that we can skip fully specifying event + * and registry here as we do not need any matching and use silent + * registration, which does not enable the corresponding event. + */ + nf->client = client; + nf->nf.base.fn = ssam_cdev_notifier; + nf->nf.base.priority = priority; + nf->nf.event.id.target_category = tc; + nf->nf.event.mask = 0; /* Do not do any matching. */ + nf->nf.flags = SSAM_EVENT_NOTIFIER_OBSERVER; + + /* Register notifier. */ + status = ssam_notifier_register(client->cdev->ctrl, &nf->nf); + if (status) + kfree(nf); + else + client->notifier[event] = nf; + + mutex_unlock(&client->notifier_lock); + return status; +} + +static int ssam_cdev_notifier_unregister(struct ssam_cdev_client *client, u8 tc) +{ + const u16 rqid = ssh_tc_to_rqid(tc); + const u16 event = ssh_rqid_to_event(rqid); + int status; + + lockdep_assert_held_read(&client->cdev->lock); + + /* Validate notifier target category. */ + if (!ssh_rqid_is_event(rqid)) + return -EINVAL; + + mutex_lock(&client->notifier_lock); + + /* Check if the notifier is currently registered. */ + if (!client->notifier[event]) { + mutex_unlock(&client->notifier_lock); + return -ENOENT; + } + + /* Unregister and free notifier. */ + status = ssam_notifier_unregister(client->cdev->ctrl, &client->notifier[event]->nf); + kfree(client->notifier[event]); + client->notifier[event] = NULL; + + mutex_unlock(&client->notifier_lock); + return status; +} + +static void ssam_cdev_notifier_unregister_all(struct ssam_cdev_client *client) +{ + int i; + + down_read(&client->cdev->lock); + + /* + * This function may be used during shutdown, thus we need to test for + * cdev->ctrl instead of the SSAM_CDEV_DEVICE_SHUTDOWN_BIT bit. + */ + if (client->cdev->ctrl) { + for (i = 0; i < SSH_NUM_EVENTS; i++) + ssam_cdev_notifier_unregister(client, i + 1); + + } else { + int count = 0; + + /* + * Device has been shut down. Any notifier remaining is a bug, + * so warn about that as this would otherwise hardly be + * noticeable. Nevertheless, free them as well. + */ + mutex_lock(&client->notifier_lock); + for (i = 0; i < SSH_NUM_EVENTS; i++) { + count += !!(client->notifier[i]); + kfree(client->notifier[i]); + client->notifier[i] = NULL; + } + mutex_unlock(&client->notifier_lock); + + WARN_ON(count > 0); + } + + up_read(&client->cdev->lock); +} + + +/* -- IOCTL functions. ------------------------------------------------------ */ + +static long ssam_cdev_request(struct ssam_cdev_client *client, struct ssam_cdev_request __user *r) +{ + struct ssam_cdev_request rqst; + struct ssam_request spec = {}; + struct ssam_response rsp = {}; + const void __user *plddata; + void __user *rspdata; + int status = 0, ret = 0, tmp; + + lockdep_assert_held_read(&client->cdev->lock); + + ret = copy_struct_from_user(&rqst, sizeof(rqst), r, sizeof(*r)); + if (ret) + goto out; + + plddata = u64_to_user_ptr(rqst.payload.data); + rspdata = u64_to_user_ptr(rqst.response.data); + + /* Setup basic request fields. */ + spec.target_category = rqst.target_category; + spec.target_id = rqst.target_id; + spec.command_id = rqst.command_id; + spec.instance_id = rqst.instance_id; + spec.flags = 0; + spec.length = rqst.payload.length; + spec.payload = NULL; + + if (rqst.flags & SSAM_CDEV_REQUEST_HAS_RESPONSE) + spec.flags |= SSAM_REQUEST_HAS_RESPONSE; + + if (rqst.flags & SSAM_CDEV_REQUEST_UNSEQUENCED) + spec.flags |= SSAM_REQUEST_UNSEQUENCED; + + rsp.capacity = rqst.response.length; + rsp.length = 0; + rsp.pointer = NULL; + + /* Get request payload from user-space. */ + if (spec.length) { + if (!plddata) { + ret = -EINVAL; + goto out; + } + + /* + * Note: spec.length is limited to U16_MAX bytes via struct + * ssam_cdev_request. This is slightly larger than the + * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the + * underlying protocol (note that nothing remotely this size + * should ever be allocated in any normal case). This size is + * validated later in ssam_request_do_sync(), for allocation + * the bound imposed by u16 should be enough. + */ + spec.payload = kzalloc(spec.length, GFP_KERNEL); + if (!spec.payload) { + ret = -ENOMEM; + goto out; + } + + if (copy_from_user((void *)spec.payload, plddata, spec.length)) { + ret = -EFAULT; + goto out; + } + } + + /* Allocate response buffer. */ + if (rsp.capacity) { + if (!rspdata) { + ret = -EINVAL; + goto out; + } + + /* + * Note: rsp.capacity is limited to U16_MAX bytes via struct + * ssam_cdev_request. This is slightly larger than the + * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the + * underlying protocol (note that nothing remotely this size + * should ever be allocated in any normal case). In later use, + * this capacity does not have to be strictly bounded, as it + * is only used as an output buffer to be written to. For + * allocation the bound imposed by u16 should be enough. + */ + rsp.pointer = kzalloc(rsp.capacity, GFP_KERNEL); + if (!rsp.pointer) { + ret = -ENOMEM; + goto out; + } + } + + /* Perform request. */ + status = ssam_request_do_sync(client->cdev->ctrl, &spec, &rsp); + if (status) + goto out; + + /* Copy response to user-space. */ + if (rsp.length && copy_to_user(rspdata, rsp.pointer, rsp.length)) + ret = -EFAULT; + +out: + /* Always try to set response-length and status. */ + tmp = put_user(rsp.length, &r->response.length); + if (tmp) + ret = tmp; + + tmp = put_user(status, &r->status); + if (tmp) + ret = tmp; + + /* Cleanup. */ + kfree(spec.payload); + kfree(rsp.pointer); + + return ret; +} + +static long ssam_cdev_notif_register(struct ssam_cdev_client *client, + const struct ssam_cdev_notifier_desc __user *d) +{ + struct ssam_cdev_notifier_desc desc; + long ret; + + lockdep_assert_held_read(&client->cdev->lock); + + ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d)); + if (ret) + return ret; + + return ssam_cdev_notifier_register(client, desc.target_category, desc.priority); +} + +static long ssam_cdev_notif_unregister(struct ssam_cdev_client *client, + const struct ssam_cdev_notifier_desc __user *d) +{ + struct ssam_cdev_notifier_desc desc; + long ret; + + lockdep_assert_held_read(&client->cdev->lock); + + ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d)); + if (ret) + return ret; + + return ssam_cdev_notifier_unregister(client, desc.target_category); +} + +static long ssam_cdev_event_enable(struct ssam_cdev_client *client, + const struct ssam_cdev_event_desc __user *d) +{ + struct ssam_cdev_event_desc desc; + struct ssam_event_registry reg; + struct ssam_event_id id; + long ret; + + lockdep_assert_held_read(&client->cdev->lock); + + /* Read descriptor from user-space. */ + ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d)); + if (ret) + return ret; + + /* Translate descriptor. */ + reg.target_category = desc.reg.target_category; + reg.target_id = desc.reg.target_id; + reg.cid_enable = desc.reg.cid_enable; + reg.cid_disable = desc.reg.cid_disable; + + id.target_category = desc.id.target_category; + id.instance = desc.id.instance; + + /* Disable event. */ + return ssam_controller_event_enable(client->cdev->ctrl, reg, id, desc.flags); +} + +static long ssam_cdev_event_disable(struct ssam_cdev_client *client, + const struct ssam_cdev_event_desc __user *d) +{ + struct ssam_cdev_event_desc desc; + struct ssam_event_registry reg; + struct ssam_event_id id; + long ret; + + lockdep_assert_held_read(&client->cdev->lock); + + /* Read descriptor from user-space. */ + ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d)); + if (ret) + return ret; + + /* Translate descriptor. */ + reg.target_category = desc.reg.target_category; + reg.target_id = desc.reg.target_id; + reg.cid_enable = desc.reg.cid_enable; + reg.cid_disable = desc.reg.cid_disable; + + id.target_category = desc.id.target_category; + id.instance = desc.id.instance; + + /* Disable event. */ + return ssam_controller_event_disable(client->cdev->ctrl, reg, id, desc.flags); +} + + +/* -- File operations. ------------------------------------------------------ */ + +static int ssam_cdev_device_open(struct inode *inode, struct file *filp) +{ + struct miscdevice *mdev = filp->private_data; + struct ssam_cdev_client *client; + struct ssam_cdev *cdev = container_of(mdev, struct ssam_cdev, mdev); + + /* Initialize client */ + client = vzalloc(sizeof(*client)); + if (!client) + return -ENOMEM; + + client->cdev = ssam_cdev_get(cdev); + + INIT_LIST_HEAD(&client->node); + + mutex_init(&client->notifier_lock); + + mutex_init(&client->read_lock); + mutex_init(&client->write_lock); + INIT_KFIFO(client->buffer); + init_waitqueue_head(&client->waitq); + + filp->private_data = client; + + /* Attach client. */ + down_write(&cdev->client_lock); + + if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) { + up_write(&cdev->client_lock); + mutex_destroy(&client->write_lock); + mutex_destroy(&client->read_lock); + mutex_destroy(&client->notifier_lock); + ssam_cdev_put(client->cdev); + vfree(client); + return -ENODEV; + } + list_add_tail(&client->node, &cdev->client_list); + + up_write(&cdev->client_lock); + + stream_open(inode, filp); + return 0; +} + +static int ssam_cdev_device_release(struct inode *inode, struct file *filp) +{ + struct ssam_cdev_client *client = filp->private_data; + + /* Force-unregister all remaining notifiers of this client. */ + ssam_cdev_notifier_unregister_all(client); + + /* Detach client. */ + down_write(&client->cdev->client_lock); + list_del(&client->node); + up_write(&client->cdev->client_lock); + + /* Free client. */ + mutex_destroy(&client->write_lock); + mutex_destroy(&client->read_lock); + + mutex_destroy(&client->notifier_lock); + + ssam_cdev_put(client->cdev); + vfree(client); + + return 0; +} + +static long __ssam_cdev_device_ioctl(struct ssam_cdev_client *client, unsigned int cmd, + unsigned long arg) +{ + lockdep_assert_held_read(&client->cdev->lock); + + switch (cmd) { + case SSAM_CDEV_REQUEST: + return ssam_cdev_request(client, (struct ssam_cdev_request __user *)arg); + + case SSAM_CDEV_NOTIF_REGISTER: + return ssam_cdev_notif_register(client, + (struct ssam_cdev_notifier_desc __user *)arg); + + case SSAM_CDEV_NOTIF_UNREGISTER: + return ssam_cdev_notif_unregister(client, + (struct ssam_cdev_notifier_desc __user *)arg); + + case SSAM_CDEV_EVENT_ENABLE: + return ssam_cdev_event_enable(client, (struct ssam_cdev_event_desc __user *)arg); + + case SSAM_CDEV_EVENT_DISABLE: + return ssam_cdev_event_disable(client, (struct ssam_cdev_event_desc __user *)arg); + + default: + return -ENOTTY; + } +} + +static long ssam_cdev_device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct ssam_cdev_client *client = file->private_data; + long status; + + /* Ensure that controller is valid for as long as we need it. */ + if (down_read_killable(&client->cdev->lock)) + return -ERESTARTSYS; + + if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &client->cdev->flags)) { + up_read(&client->cdev->lock); + return -ENODEV; + } + + status = __ssam_cdev_device_ioctl(client, cmd, arg); + + up_read(&client->cdev->lock); + return status; +} + +static ssize_t ssam_cdev_read(struct file *file, char __user *buf, size_t count, loff_t *offs) +{ + struct ssam_cdev_client *client = file->private_data; + struct ssam_cdev *cdev = client->cdev; + unsigned int copied; + int status = 0; + + if (down_read_killable(&cdev->lock)) + return -ERESTARTSYS; + + /* Make sure we're not shut down. */ + if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) { + up_read(&cdev->lock); + return -ENODEV; + } + + do { + /* Check availability, wait if necessary. */ + if (kfifo_is_empty(&client->buffer)) { + up_read(&cdev->lock); + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + status = wait_event_interruptible(client->waitq, + !kfifo_is_empty(&client->buffer) || + test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, + &cdev->flags)); + if (status < 0) + return status; + + if (down_read_killable(&cdev->lock)) + return -ERESTARTSYS; + + /* Need to check that we're not shut down again. */ + if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) { + up_read(&cdev->lock); + return -ENODEV; + } + } + + /* Try to read from FIFO. */ + if (mutex_lock_interruptible(&client->read_lock)) { + up_read(&cdev->lock); + return -ERESTARTSYS; + } + + status = kfifo_to_user(&client->buffer, buf, count, &copied); + mutex_unlock(&client->read_lock); + + if (status < 0) { + up_read(&cdev->lock); + return status; + } + + /* We might not have gotten anything, check this here. */ + if (copied == 0 && (file->f_flags & O_NONBLOCK)) { + up_read(&cdev->lock); + return -EAGAIN; + } + } while (copied == 0); + + up_read(&cdev->lock); + return copied; +} + +static __poll_t ssam_cdev_poll(struct file *file, struct poll_table_struct *pt) +{ + struct ssam_cdev_client *client = file->private_data; + __poll_t events = 0; + + if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &client->cdev->flags)) + return EPOLLHUP | EPOLLERR; + + poll_wait(file, &client->waitq, pt); + + if (!kfifo_is_empty(&client->buffer)) + events |= EPOLLIN | EPOLLRDNORM; + + return events; +} + +static int ssam_cdev_fasync(int fd, struct file *file, int on) +{ + struct ssam_cdev_client *client = file->private_data; + + return fasync_helper(fd, file, on, &client->fasync); +} + +static const struct file_operations ssam_controller_fops = { + .owner = THIS_MODULE, + .open = ssam_cdev_device_open, + .release = ssam_cdev_device_release, + .read = ssam_cdev_read, + .poll = ssam_cdev_poll, + .fasync = ssam_cdev_fasync, + .unlocked_ioctl = ssam_cdev_device_ioctl, + .compat_ioctl = ssam_cdev_device_ioctl, +}; + + +/* -- Device and driver setup ----------------------------------------------- */ + +static int ssam_dbg_device_probe(struct platform_device *pdev) +{ + struct ssam_controller *ctrl; + struct ssam_cdev *cdev; + int status; + + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + cdev = kzalloc_obj(*cdev); + if (!cdev) + return -ENOMEM; + + kref_init(&cdev->kref); + init_rwsem(&cdev->lock); + cdev->ctrl = ctrl; + cdev->dev = &pdev->dev; + + cdev->mdev.parent = &pdev->dev; + cdev->mdev.minor = MISC_DYNAMIC_MINOR; + cdev->mdev.name = "surface_aggregator"; + cdev->mdev.nodename = "surface/aggregator"; + cdev->mdev.fops = &ssam_controller_fops; + + init_rwsem(&cdev->client_lock); + INIT_LIST_HEAD(&cdev->client_list); + + status = misc_register(&cdev->mdev); + if (status) { + kfree(cdev); + return status; + } + + platform_set_drvdata(pdev, cdev); + return 0; +} + +static void ssam_dbg_device_remove(struct platform_device *pdev) +{ + struct ssam_cdev *cdev = platform_get_drvdata(pdev); + struct ssam_cdev_client *client; + + /* + * Mark device as shut-down. Prevent new clients from being added and + * new operations from being executed. + */ + set_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags); + + down_write(&cdev->client_lock); + + /* Remove all notifiers registered by us. */ + list_for_each_entry(client, &cdev->client_list, node) { + ssam_cdev_notifier_unregister_all(client); + } + + /* Wake up async clients. */ + list_for_each_entry(client, &cdev->client_list, node) { + kill_fasync(&client->fasync, SIGIO, POLL_HUP); + } + + /* Wake up blocking clients. */ + list_for_each_entry(client, &cdev->client_list, node) { + wake_up_interruptible(&client->waitq); + } + + up_write(&cdev->client_lock); + + /* + * The controller is only guaranteed to be valid for as long as the + * driver is bound. Remove controller so that any lingering open files + * cannot access it any more after we're gone. + */ + down_write(&cdev->lock); + cdev->ctrl = NULL; + cdev->dev = NULL; + up_write(&cdev->lock); + + misc_deregister(&cdev->mdev); + + ssam_cdev_put(cdev); +} + +static struct platform_device *ssam_cdev_device; + +static struct platform_driver ssam_cdev_driver = { + .probe = ssam_dbg_device_probe, + .remove = ssam_dbg_device_remove, + .driver = { + .name = SSAM_CDEV_DEVICE_NAME, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static int __init ssam_debug_init(void) +{ + int status; + + ssam_cdev_device = platform_device_alloc(SSAM_CDEV_DEVICE_NAME, + PLATFORM_DEVID_NONE); + if (!ssam_cdev_device) + return -ENOMEM; + + status = platform_device_add(ssam_cdev_device); + if (status) + goto err_device; + + status = platform_driver_register(&ssam_cdev_driver); + if (status) + goto err_driver; + + return 0; + +err_driver: + platform_device_del(ssam_cdev_device); +err_device: + platform_device_put(ssam_cdev_device); + return status; +} +module_init(ssam_debug_init); + +static void __exit ssam_debug_exit(void) +{ + platform_driver_unregister(&ssam_cdev_driver); + platform_device_unregister(ssam_cdev_device); +} +module_exit(ssam_debug_exit); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("User-space interface for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_aggregator_hub.c b/drivers/platform/surface/surface_aggregator_hub.c new file mode 100644 index 000000000..8b8b80228 --- /dev/null +++ b/drivers/platform/surface/surface_aggregator_hub.c @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs. + * + * Provides a driver for SSAM subsystems device hubs. This driver performs + * instantiation of the devices managed by said hubs and takes care of + * (hot-)removal. + * + * Copyright (C) 2020-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include + +#include + + +/* -- SSAM generic subsystem hub driver framework. -------------------------- */ + +enum ssam_hub_state { + SSAM_HUB_UNINITIALIZED, /* Only set during initialization. */ + SSAM_HUB_CONNECTED, + SSAM_HUB_DISCONNECTED, +}; + +enum ssam_hub_flags { + SSAM_HUB_HOT_REMOVED, +}; + +struct ssam_hub; + +struct ssam_hub_ops { + int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state); +}; + +struct ssam_hub { + struct ssam_device *sdev; + + enum ssam_hub_state state; + unsigned long flags; + + struct delayed_work update_work; + unsigned long connect_delay; + + struct ssam_event_notifier notif; + struct ssam_hub_ops ops; +}; + +struct ssam_hub_desc { + struct { + struct ssam_event_registry reg; + struct ssam_event_id id; + enum ssam_event_mask mask; + } event; + + struct { + u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event); + int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state); + } ops; + + unsigned long connect_delay_ms; +}; + +static void ssam_hub_update_workfn(struct work_struct *work) +{ + struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work); + enum ssam_hub_state state; + int status = 0; + + status = hub->ops.get_state(hub, &state); + if (status) + return; + + /* + * There is a small possibility that hub devices were hot-removed and + * re-added before we were able to remove them here. In that case, both + * the state returned by get_state() and the state of the hub will + * equal SSAM_HUB_CONNECTED and we would bail early below, which would + * leave child devices without proper (re-)initialization and the + * hot-remove flag set. + * + * Therefore, we check whether devices have been hot-removed via an + * additional flag on the hub and, in this case, override the returned + * hub state. In case of a missed disconnect (i.e. get_state returned + * "connected"), we further need to re-schedule this work (with the + * appropriate delay) as the actual connect work submission might have + * been merged with this one. + * + * This then leads to one of two cases: Either we submit an unnecessary + * work item (which will get ignored via either the queue or the state + * checks) or, in the unlikely case that the work is actually required, + * double the normal connect delay. + */ + if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED, &hub->flags)) { + if (state == SSAM_HUB_CONNECTED) + schedule_delayed_work(&hub->update_work, hub->connect_delay); + + state = SSAM_HUB_DISCONNECTED; + } + + if (hub->state == state) + return; + hub->state = state; + + if (hub->state == SSAM_HUB_CONNECTED) + status = ssam_device_register_clients(hub->sdev); + else + ssam_remove_clients(&hub->sdev->dev); + + if (status) + dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status); +} + +static int ssam_hub_mark_hot_removed(struct device *dev, void *_data) +{ + struct ssam_device *sdev = to_ssam_device(dev); + + if (is_ssam_device(dev)) + ssam_device_mark_hot_removed(sdev); + + return 0; +} + +static void ssam_hub_update(struct ssam_hub *hub, bool connected) +{ + unsigned long delay; + + /* Mark devices as hot-removed before we remove any. */ + if (!connected) { + set_bit(SSAM_HUB_HOT_REMOVED, &hub->flags); + device_for_each_child_reverse(&hub->sdev->dev, NULL, ssam_hub_mark_hot_removed); + } + + /* + * Delay update when the base/keyboard cover is being connected to give + * devices/EC some time to set up. + */ + delay = connected ? hub->connect_delay : 0; + + schedule_delayed_work(&hub->update_work, delay); +} + +static int __maybe_unused ssam_hub_resume(struct device *dev) +{ + struct ssam_hub *hub = dev_get_drvdata(dev); + + schedule_delayed_work(&hub->update_work, 0); + return 0; +} +static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume); + +static int ssam_hub_probe(struct ssam_device *sdev) +{ + const struct ssam_hub_desc *desc; + struct ssam_hub *hub; + int status; + + desc = ssam_device_get_match_data(sdev); + if (!desc) { + WARN(1, "no driver match data specified"); + return -EINVAL; + } + + hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL); + if (!hub) + return -ENOMEM; + + hub->sdev = sdev; + hub->state = SSAM_HUB_UNINITIALIZED; + + hub->notif.base.priority = INT_MAX; /* This notifier should run first. */ + hub->notif.base.fn = desc->ops.notify; + hub->notif.event.reg = desc->event.reg; + hub->notif.event.id = desc->event.id; + hub->notif.event.mask = desc->event.mask; + hub->notif.event.flags = SSAM_EVENT_SEQUENCED; + + hub->connect_delay = msecs_to_jiffies(desc->connect_delay_ms); + hub->ops.get_state = desc->ops.get_state; + + INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn); + + ssam_device_set_drvdata(sdev, hub); + + status = ssam_device_notifier_register(sdev, &hub->notif); + if (status) + return status; + + schedule_delayed_work(&hub->update_work, 0); + return 0; +} + +static void ssam_hub_remove(struct ssam_device *sdev) +{ + struct ssam_hub *hub = ssam_device_get_drvdata(sdev); + + ssam_device_notifier_unregister(sdev, &hub->notif); + cancel_delayed_work_sync(&hub->update_work); + ssam_remove_clients(&sdev->dev); +} + + +/* -- SSAM base-subsystem hub driver. --------------------------------------- */ + +/* + * Some devices (especially battery) may need a bit of time to be fully usable + * after being (re-)connected. This delay has been determined via + * experimentation. + */ +#define SSAM_BASE_UPDATE_CONNECT_DELAY 2500 + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0d, + .instance_id = 0x00, +}); + +#define SSAM_BAS_OPMODE_TABLET 0x00 +#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c + +static int ssam_base_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state) +{ + u8 opmode; + int status; + + status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode); + if (status < 0) { + dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status); + return status; + } + + if (opmode != SSAM_BAS_OPMODE_TABLET) + *state = SSAM_HUB_CONNECTED; + else + *state = SSAM_HUB_DISCONNECTED; + + return 0; +} + +static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ + struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif); + + if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION) + return 0; + + if (event->length < 1) { + dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length); + return 0; + } + + ssam_hub_update(hub, event->data[0]); + + /* + * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and + * consumed by the detachment system driver. We're just a (more or less) + * silent observer. + */ + return 0; +} + +static const struct ssam_hub_desc base_hub = { + .event = { + .reg = SSAM_EVENT_REGISTRY_SAM, + .id = { + .target_category = SSAM_SSH_TC_BAS, + .instance = 0, + }, + .mask = SSAM_EVENT_MASK_NONE, + }, + .ops = { + .notify = ssam_base_hub_notif, + .get_state = ssam_base_hub_query_state, + }, + .connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY, +}; + + +/* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */ + +/* + * Some devices may need a bit of time to be fully usable after being + * (re-)connected. This delay has been determined via experimentation. + */ +#define SSAM_KIP_UPDATE_CONNECT_DELAY 250 + +#define SSAM_EVENT_KIP_CID_CONNECTION 0x2c + +SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, { + .target_category = SSAM_SSH_TC_KIP, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x2c, + .instance_id = 0x00, +}); + +static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state) +{ + int status; + u8 connected; + + status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected); + if (status < 0) { + dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status); + return status; + } + + *state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED; + return 0; +} + +static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ + struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif); + + if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION) + return 0; /* Return "unhandled". */ + + if (event->length < 1) { + dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length); + return 0; + } + + ssam_hub_update(hub, event->data[0]); + return SSAM_NOTIF_HANDLED; +} + +static const struct ssam_hub_desc kip_hub = { + .event = { + .reg = SSAM_EVENT_REGISTRY_SAM, + .id = { + .target_category = SSAM_SSH_TC_KIP, + .instance = 0, + }, + .mask = SSAM_EVENT_MASK_TARGET, + }, + .ops = { + .notify = ssam_kip_hub_notif, + .get_state = ssam_kip_hub_query_state, + }, + .connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY, +}; + + +/* -- Driver registration. -------------------------------------------------- */ + +static const struct ssam_device_id ssam_hub_match[] = { + { SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub }, + { SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_BAS, 0x00), (unsigned long)&base_hub }, + { } +}; +MODULE_DEVICE_TABLE(ssam, ssam_hub_match); + +static struct ssam_device_driver ssam_subsystem_hub_driver = { + .probe = ssam_hub_probe, + .remove = ssam_hub_remove, + .match_table = ssam_hub_match, + .driver = { + .name = "surface_aggregator_subsystem_hub", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .pm = &ssam_hub_pm_ops, + }, +}; +module_ssam_device_driver(ssam_subsystem_hub_driver); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c new file mode 100644 index 000000000..d37a7e4c6 --- /dev/null +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface System Aggregator Module (SSAM) client device registry. + * + * Registry for non-platform/non-ACPI SSAM client devices, i.e. devices that + * cannot be auto-detected. Provides device-hubs and performs instantiation + * for these devices. + * + * Copyright (C) 2020-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + + +/* -- Device registry. ------------------------------------------------------ */ + +/* + * SSAM device names follow the SSAM module alias, meaning they are prefixed + * with 'ssam:', followed by domain, category, target ID, instance ID, and + * function, each encoded as two-digit hexadecimal, separated by ':'. In other + * words, it follows the scheme + * + * ssam:dd:cc:tt:ii:ff + * + * Where, 'dd', 'cc', 'tt', 'ii', and 'ff' are the two-digit hexadecimal + * values mentioned above, respectively. + */ + +/* Root node. */ +static const struct software_node ssam_node_root = { + .name = "ssam_platform_hub", +}; + +/* KIP device hub (connects keyboard cover devices on Surface Pro 8). */ +static const struct software_node ssam_node_hub_kip = { + .name = "ssam:00:00:01:0e:00", + .parent = &ssam_node_root, +}; + +/* Base device hub (devices attached to Surface Book 3 base). */ +static const struct software_node ssam_node_hub_base = { + .name = "ssam:00:00:01:11:00", + .parent = &ssam_node_root, +}; + +/* AC adapter. */ +static const struct software_node ssam_node_bat_ac = { + .name = "ssam:01:02:01:01:01", + .parent = &ssam_node_root, +}; + +/* Primary battery. */ +static const struct software_node ssam_node_bat_main = { + .name = "ssam:01:02:01:01:00", + .parent = &ssam_node_root, +}; + +/* Secondary battery (Surface Book 3). */ +static const struct software_node ssam_node_bat_sb3base = { + .name = "ssam:01:02:02:01:00", + .parent = &ssam_node_hub_base, +}; + +/* Platform profile / performance-mode device without a fan. */ +static const struct software_node ssam_node_tmp_perf_profile = { + .name = "ssam:01:03:01:00:01", + .parent = &ssam_node_root, +}; + +/* Platform profile / performance-mode device with a fan, such that + * the fan controller profile can also be switched. + */ +static const struct property_entry ssam_node_tmp_perf_profile_has_fan[] = { + PROPERTY_ENTRY_BOOL("has_fan"), + { } +}; + +static const struct software_node ssam_node_tmp_perf_profile_with_fan = { + .name = "ssam:01:03:01:00:01", + .parent = &ssam_node_root, + .properties = ssam_node_tmp_perf_profile_has_fan, +}; + +/* Thermal sensors. */ +static const struct software_node ssam_node_tmp_sensors = { + .name = "ssam:01:03:01:00:02", + .parent = &ssam_node_root, +}; + +/* Fan speed function. */ +static const struct software_node ssam_node_fan_speed = { + .name = "ssam:01:05:01:01:01", + .parent = &ssam_node_root, +}; + +/* Tablet-mode switch via KIP subsystem. */ +static const struct software_node ssam_node_kip_tablet_switch = { + .name = "ssam:01:0e:01:00:01", + .parent = &ssam_node_root, +}; + +/* DTX / detachment-system device (Surface Book 3). */ +static const struct software_node ssam_node_bas_dtx = { + .name = "ssam:01:11:01:00:00", + .parent = &ssam_node_root, +}; + +/* HID keyboard (SAM, TID=1). */ +static const struct software_node ssam_node_hid_sam_keyboard = { + .name = "ssam:01:15:01:01:00", + .parent = &ssam_node_root, +}; + +/* HID pen stash (SAM, TID=1; pen taken / stashed away evens). */ +static const struct software_node ssam_node_hid_sam_penstash = { + .name = "ssam:01:15:01:02:00", + .parent = &ssam_node_root, +}; + +/* HID touchpad (SAM, TID=1). */ +static const struct software_node ssam_node_hid_sam_touchpad = { + .name = "ssam:01:15:01:03:00", + .parent = &ssam_node_root, +}; + +/* HID device instance 6 (SAM, TID=1, HID sensor collection). */ +static const struct software_node ssam_node_hid_sam_sensors = { + .name = "ssam:01:15:01:06:00", + .parent = &ssam_node_root, +}; + +/* HID device instance 7 (SAM, TID=1, UCM UCSI HID client). */ +static const struct software_node ssam_node_hid_sam_ucm_ucsi = { + .name = "ssam:01:15:01:07:00", + .parent = &ssam_node_root, +}; + +/* HID system controls (SAM, TID=1). */ +static const struct software_node ssam_node_hid_sam_sysctrl = { + .name = "ssam:01:15:01:08:00", + .parent = &ssam_node_root, +}; + +/* HID keyboard. */ +static const struct software_node ssam_node_hid_main_keyboard = { + .name = "ssam:01:15:02:01:00", + .parent = &ssam_node_root, +}; + +/* HID touchpad. */ +static const struct software_node ssam_node_hid_main_touchpad = { + .name = "ssam:01:15:02:03:00", + .parent = &ssam_node_root, +}; + +/* HID device instance 5 (unknown HID device). */ +static const struct software_node ssam_node_hid_main_iid5 = { + .name = "ssam:01:15:02:05:00", + .parent = &ssam_node_root, +}; + +/* HID keyboard (base hub). */ +static const struct software_node ssam_node_hid_base_keyboard = { + .name = "ssam:01:15:02:01:00", + .parent = &ssam_node_hub_base, +}; + +/* HID touchpad (base hub). */ +static const struct software_node ssam_node_hid_base_touchpad = { + .name = "ssam:01:15:02:03:00", + .parent = &ssam_node_hub_base, +}; + +/* HID device instance 5 (unknown HID device, base hub). */ +static const struct software_node ssam_node_hid_base_iid5 = { + .name = "ssam:01:15:02:05:00", + .parent = &ssam_node_hub_base, +}; + +/* HID device instance 6 (unknown HID device, base hub). */ +static const struct software_node ssam_node_hid_base_iid6 = { + .name = "ssam:01:15:02:06:00", + .parent = &ssam_node_hub_base, +}; + +/* HID keyboard (KIP hub). */ +static const struct software_node ssam_node_hid_kip_keyboard = { + .name = "ssam:01:15:02:01:00", + .parent = &ssam_node_hub_kip, +}; + +/* HID pen stash (KIP hub; pen taken / stashed away evens). */ +static const struct software_node ssam_node_hid_kip_penstash = { + .name = "ssam:01:15:02:02:00", + .parent = &ssam_node_hub_kip, +}; + +/* HID touchpad (KIP hub). */ +static const struct software_node ssam_node_hid_kip_touchpad = { + .name = "ssam:01:15:02:03:00", + .parent = &ssam_node_hub_kip, +}; + +/* HID device instance 5 (KIP hub, type-cover firmware update). */ +static const struct software_node ssam_node_hid_kip_fwupd = { + .name = "ssam:01:15:02:05:00", + .parent = &ssam_node_hub_kip, +}; + +/* Tablet-mode switch via POS subsystem. */ +static const struct software_node ssam_node_pos_tablet_switch = { + .name = "ssam:01:26:01:00:01", + .parent = &ssam_node_root, +}; + +/* + * Devices for 5th- and 6th-generations models: + * - Surface Book 2, + * - Surface Laptop 1 and 2, + * - Surface Pro 5 and 6. + */ +static const struct software_node *ssam_node_group_gen5[] = { + &ssam_node_root, + &ssam_node_tmp_perf_profile, + NULL, +}; + +/* Devices for Surface Book 3. */ +static const struct software_node *ssam_node_group_sb3[] = { + &ssam_node_root, + &ssam_node_hub_base, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_bat_sb3base, + &ssam_node_tmp_perf_profile, + &ssam_node_bas_dtx, + &ssam_node_hid_base_keyboard, + &ssam_node_hid_base_touchpad, + &ssam_node_hid_base_iid5, + &ssam_node_hid_base_iid6, + NULL, +}; + +/* Devices for Surface Laptop 3 and 4. */ +static const struct software_node *ssam_node_group_sl3[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile, + &ssam_node_hid_main_keyboard, + &ssam_node_hid_main_touchpad, + &ssam_node_hid_main_iid5, + NULL, +}; + +/* Devices for Surface Laptop 5. */ +static const struct software_node *ssam_node_group_sl5[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_tmp_sensors, + &ssam_node_fan_speed, + &ssam_node_hid_main_keyboard, + &ssam_node_hid_main_touchpad, + &ssam_node_hid_main_iid5, + &ssam_node_hid_sam_ucm_ucsi, + NULL, +}; + +/* Devices for Surface Laptop 6. */ +static const struct software_node *ssam_node_group_sl6[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_tmp_sensors, + &ssam_node_fan_speed, + &ssam_node_hid_main_keyboard, + &ssam_node_hid_main_touchpad, + &ssam_node_hid_main_iid5, + &ssam_node_hid_sam_sensors, + &ssam_node_hid_sam_ucm_ucsi, + NULL, +}; + +/* Devices for Surface Laptop 7. */ +static const struct software_node *ssam_node_group_sl7[] = { + &ssam_node_root, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_fan_speed, + &ssam_node_hid_sam_keyboard, + /* TODO: evaluate thermal sensors devices when we get a driver for that */ + NULL, +}; + +/* Devices for Surface Laptop Studio 1. */ +static const struct software_node *ssam_node_group_sls1[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile, + &ssam_node_pos_tablet_switch, + &ssam_node_hid_sam_keyboard, + &ssam_node_hid_sam_penstash, + &ssam_node_hid_sam_touchpad, + &ssam_node_hid_sam_sensors, + &ssam_node_hid_sam_ucm_ucsi, + &ssam_node_hid_sam_sysctrl, + NULL, +}; + +/* Devices for Surface Laptop Studio 2. */ +static const struct software_node *ssam_node_group_sls2[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_tmp_sensors, + &ssam_node_fan_speed, + &ssam_node_pos_tablet_switch, + &ssam_node_hid_sam_keyboard, + &ssam_node_hid_sam_penstash, + &ssam_node_hid_sam_sensors, + &ssam_node_hid_sam_ucm_ucsi, + NULL, +}; + +/* Devices for Surface Laptop Go. */ +static const struct software_node *ssam_node_group_slg1[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile, + NULL, +}; + +/* Devices for Surface Pro 7 and Surface Pro 7+. */ +static const struct software_node *ssam_node_group_sp7[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile, + NULL, +}; + +/* Devices for Surface Pro 8 */ +static const struct software_node *ssam_node_group_sp8[] = { + &ssam_node_root, + &ssam_node_hub_kip, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile, + &ssam_node_kip_tablet_switch, + &ssam_node_hid_kip_keyboard, + &ssam_node_hid_kip_penstash, + &ssam_node_hid_kip_touchpad, + &ssam_node_hid_kip_fwupd, + &ssam_node_hid_sam_sensors, + &ssam_node_hid_sam_ucm_ucsi, + NULL, +}; + +/* Devices for Surface Pro 9, 10 and 11 (Intel/x86) */ +static const struct software_node *ssam_node_group_sp9[] = { + &ssam_node_root, + &ssam_node_hub_kip, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_tmp_sensors, + &ssam_node_fan_speed, + &ssam_node_pos_tablet_switch, + &ssam_node_hid_kip_keyboard, + &ssam_node_hid_kip_penstash, + &ssam_node_hid_kip_touchpad, + &ssam_node_hid_kip_fwupd, + &ssam_node_hid_sam_sensors, + &ssam_node_hid_sam_ucm_ucsi, + NULL, +}; + +/* Devices for Surface Pro 9 5G (ARM/QCOM) */ +static const struct software_node *ssam_node_group_sp9_5g[] = { + &ssam_node_root, + &ssam_node_hub_kip, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_sensors, + &ssam_node_hid_kip_keyboard, + &ssam_node_hid_kip_penstash, + &ssam_node_hid_kip_touchpad, + &ssam_node_hid_kip_fwupd, + &ssam_node_hid_sam_sensors, + &ssam_node_kip_tablet_switch, + NULL, +}; + +/* Devices for Surface Pro 11 (ARM/QCOM) */ +static const struct software_node *ssam_node_group_sp11[] = { + &ssam_node_root, + &ssam_node_hub_kip, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_sensors, + &ssam_node_hid_kip_keyboard, + &ssam_node_hid_kip_penstash, + &ssam_node_hid_kip_touchpad, + &ssam_node_hid_kip_fwupd, + &ssam_node_hid_sam_sensors, + &ssam_node_kip_tablet_switch, + NULL, +}; + +/* Devices for Surface Pro 12" first edition (ARM/QCOM) */ +static const struct software_node *ssam_node_group_sp12in[] = { + &ssam_node_root, + &ssam_node_hub_kip, + &ssam_node_tmp_sensors, + &ssam_node_hid_kip_keyboard, + &ssam_node_hid_sam_penstash, + &ssam_node_hid_kip_touchpad, + &ssam_node_hid_kip_fwupd, + &ssam_node_pos_tablet_switch, + NULL, +}; + +/* -- SSAM platform/meta-hub driver. ---------------------------------------- */ + +static const struct acpi_device_id ssam_platform_hub_acpi_match[] = { + /* Surface Pro 4, 5, and 6 (OMBR < 0x10) */ + { "MSHW0081", (unsigned long)ssam_node_group_gen5 }, + + /* Surface Pro 6 (OMBR >= 0x10) */ + { "MSHW0111", (unsigned long)ssam_node_group_gen5 }, + + /* Surface Pro 7 */ + { "MSHW0116", (unsigned long)ssam_node_group_sp7 }, + + /* Surface Pro 7+ */ + { "MSHW0119", (unsigned long)ssam_node_group_sp7 }, + + /* Surface Pro 8 */ + { "MSHW0263", (unsigned long)ssam_node_group_sp8 }, + + /* Surface Pro 9 */ + { "MSHW0343", (unsigned long)ssam_node_group_sp9 }, + + /* Surface Pro 10 */ + { "MSHW0510", (unsigned long)ssam_node_group_sp9 }, + + /* Surface Pro 11 */ + { "MSHW0583", (unsigned long)ssam_node_group_sp9 }, + + /* Surface Book 2 */ + { "MSHW0107", (unsigned long)ssam_node_group_gen5 }, + + /* Surface Book 3 */ + { "MSHW0117", (unsigned long)ssam_node_group_sb3 }, + + /* Surface Laptop 1 */ + { "MSHW0086", (unsigned long)ssam_node_group_gen5 }, + + /* Surface Laptop 2 */ + { "MSHW0112", (unsigned long)ssam_node_group_gen5 }, + + /* Surface Laptop 3 (13", Intel) */ + { "MSHW0114", (unsigned long)ssam_node_group_sl3 }, + + /* Surface Laptop 3 (15", AMD) and 4 (15", AMD) */ + { "MSHW0110", (unsigned long)ssam_node_group_sl3 }, + + /* Surface Laptop 4 (13", Intel) */ + { "MSHW0250", (unsigned long)ssam_node_group_sl3 }, + + /* Surface Laptop 5 */ + { "MSHW0350", (unsigned long)ssam_node_group_sl5 }, + + /* Surface Laptop 6 */ + { "MSHW0530", (unsigned long)ssam_node_group_sl6 }, + + /* Surface Laptop Go 1 */ + { "MSHW0118", (unsigned long)ssam_node_group_slg1 }, + + /* Surface Laptop Go 2 */ + { "MSHW0290", (unsigned long)ssam_node_group_slg1 }, + + /* Surface Laptop Go 3 */ + { "MSHW0440", (unsigned long)ssam_node_group_slg1 }, + + /* Surface Laptop Studio 1 */ + { "MSHW0123", (unsigned long)ssam_node_group_sls1 }, + + /* Surface Laptop Studio 2 */ + { "MSHW0360", (unsigned long)ssam_node_group_sls2 }, + + { }, +}; +MODULE_DEVICE_TABLE(acpi, ssam_platform_hub_acpi_match); + +static const struct of_device_id ssam_platform_hub_of_match[] __maybe_unused = { + /* Surface Pro 9 5G (ARM/QCOM) */ + { .compatible = "microsoft,arcata", (void *)ssam_node_group_sp9_5g }, + /* Surface Pro 11 (ARM/QCOM) */ + { .compatible = "microsoft,denali", (void *)ssam_node_group_sp11 }, + /* Surface Pro 12in First Edition (ARM/QCOM) */ + { .compatible = "microsoft,surface-pro-12in", (void *)ssam_node_group_sp12in }, + /* Surface Laptop 7 */ + { .compatible = "microsoft,romulus13", (void *)ssam_node_group_sl7 }, + { .compatible = "microsoft,romulus15", (void *)ssam_node_group_sl7 }, + { }, +}; + +static int ssam_platform_hub_probe(struct platform_device *pdev) +{ + const struct software_node **nodes; + struct ssam_controller *ctrl; + struct fwnode_handle *root; + int status; + + nodes = (const struct software_node **)acpi_device_get_match_data(&pdev->dev); + if (!nodes) { + nodes = (const struct software_node **)of_machine_get_match_data(ssam_platform_hub_of_match); + if (!nodes) + return -ENODEV; + } + + /* + * As we're adding the SSAM client devices as children under this device + * and not the SSAM controller, we need to add a device link to the + * controller to ensure that we remove all of our devices before the + * controller is removed. This also guarantees proper ordering for + * suspend/resume of the devices on this hub. + */ + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + status = software_node_register_node_group(nodes); + if (status) + return status; + + root = software_node_fwnode(&ssam_node_root); + if (!root) { + software_node_unregister_node_group(nodes); + return -ENOENT; + } + + set_secondary_fwnode(&pdev->dev, root); + + status = __ssam_register_clients(&pdev->dev, ctrl, root); + if (status) { + set_secondary_fwnode(&pdev->dev, NULL); + software_node_unregister_node_group(nodes); + } + + platform_set_drvdata(pdev, nodes); + return status; +} + +static void ssam_platform_hub_remove(struct platform_device *pdev) +{ + const struct software_node **nodes = platform_get_drvdata(pdev); + + ssam_remove_clients(&pdev->dev); + set_secondary_fwnode(&pdev->dev, NULL); + software_node_unregister_node_group(nodes); +} + +static struct platform_driver ssam_platform_hub_driver = { + .probe = ssam_platform_hub_probe, + .remove = ssam_platform_hub_remove, + .driver = { + .name = "surface_aggregator_platform_hub", + .acpi_match_table = ssam_platform_hub_acpi_match, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; +module_platform_driver(ssam_platform_hub_driver); + +MODULE_ALIAS("platform:surface_aggregator_platform_hub"); +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Device-registry for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_aggregator_tabletsw.c b/drivers/platform/surface/surface_aggregator_tabletsw.c new file mode 100644 index 000000000..ffa36ed92 --- /dev/null +++ b/drivers/platform/surface/surface_aggregator_tabletsw.c @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface System Aggregator Module (SSAM) tablet mode switch driver. + * + * Copyright (C) 2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + + +/* -- SSAM generic tablet switch driver framework. -------------------------- */ + +struct ssam_tablet_sw; + +struct ssam_tablet_sw_state { + u32 source; + u32 state; +}; + +struct ssam_tablet_sw_ops { + int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state); + const char *(*state_name)(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state); + bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state); +}; + +struct ssam_tablet_sw { + struct ssam_device *sdev; + + struct ssam_tablet_sw_state state; + struct work_struct update_work; + struct input_dev *mode_switch; + + struct ssam_tablet_sw_ops ops; + struct ssam_event_notifier notif; +}; + +struct ssam_tablet_sw_desc { + struct { + const char *name; + const char *phys; + } dev; + + struct { + u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event); + int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state); + const char *(*state_name)(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state); + bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state); + } ops; + + struct { + struct ssam_event_registry reg; + struct ssam_event_id id; + enum ssam_event_mask mask; + u8 flags; + } event; +}; + +static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct ssam_tablet_sw *sw = dev_get_drvdata(dev); + const char *state = sw->ops.state_name(sw, &sw->state); + + return sysfs_emit(buf, "%s\n", state); +} +static DEVICE_ATTR_RO(state); + +static struct attribute *ssam_tablet_sw_attrs[] = { + &dev_attr_state.attr, + NULL, +}; + +static const struct attribute_group ssam_tablet_sw_group = { + .attrs = ssam_tablet_sw_attrs, +}; + +static void ssam_tablet_sw_update_workfn(struct work_struct *work) +{ + struct ssam_tablet_sw *sw = container_of(work, struct ssam_tablet_sw, update_work); + struct ssam_tablet_sw_state state; + int tablet, status; + + status = sw->ops.get_state(sw, &state); + if (status) + return; + + if (sw->state.source == state.source && sw->state.state == state.state) + return; + sw->state = state; + + /* Send SW_TABLET_MODE event. */ + tablet = sw->ops.state_is_tablet_mode(sw, &state); + input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet); + input_sync(sw->mode_switch); +} + +static int __maybe_unused ssam_tablet_sw_resume(struct device *dev) +{ + struct ssam_tablet_sw *sw = dev_get_drvdata(dev); + + schedule_work(&sw->update_work); + return 0; +} +static SIMPLE_DEV_PM_OPS(ssam_tablet_sw_pm_ops, NULL, ssam_tablet_sw_resume); + +static int ssam_tablet_sw_probe(struct ssam_device *sdev) +{ + const struct ssam_tablet_sw_desc *desc; + struct ssam_tablet_sw *sw; + int tablet, status; + + desc = ssam_device_get_match_data(sdev); + if (!desc) { + WARN(1, "no driver match data specified"); + return -EINVAL; + } + + sw = devm_kzalloc(&sdev->dev, sizeof(*sw), GFP_KERNEL); + if (!sw) + return -ENOMEM; + + sw->sdev = sdev; + + sw->ops.get_state = desc->ops.get_state; + sw->ops.state_name = desc->ops.state_name; + sw->ops.state_is_tablet_mode = desc->ops.state_is_tablet_mode; + + INIT_WORK(&sw->update_work, ssam_tablet_sw_update_workfn); + + ssam_device_set_drvdata(sdev, sw); + + /* Get initial state. */ + status = sw->ops.get_state(sw, &sw->state); + if (status) + return status; + + /* Set up tablet mode switch. */ + sw->mode_switch = devm_input_allocate_device(&sdev->dev); + if (!sw->mode_switch) + return -ENOMEM; + + sw->mode_switch->name = desc->dev.name; + sw->mode_switch->phys = desc->dev.phys; + sw->mode_switch->id.bustype = BUS_HOST; + sw->mode_switch->dev.parent = &sdev->dev; + + tablet = sw->ops.state_is_tablet_mode(sw, &sw->state); + input_set_capability(sw->mode_switch, EV_SW, SW_TABLET_MODE); + input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet); + + status = input_register_device(sw->mode_switch); + if (status) + return status; + + /* Set up notifier. */ + sw->notif.base.priority = 0; + sw->notif.base.fn = desc->ops.notify; + sw->notif.event.reg = desc->event.reg; + sw->notif.event.id = desc->event.id; + sw->notif.event.mask = desc->event.mask; + sw->notif.event.flags = SSAM_EVENT_SEQUENCED; + + status = ssam_device_notifier_register(sdev, &sw->notif); + if (status) + return status; + + status = sysfs_create_group(&sdev->dev.kobj, &ssam_tablet_sw_group); + if (status) + goto err; + + /* We might have missed events during setup, so check again. */ + schedule_work(&sw->update_work); + return 0; + +err: + ssam_device_notifier_unregister(sdev, &sw->notif); + cancel_work_sync(&sw->update_work); + return status; +} + +static void ssam_tablet_sw_remove(struct ssam_device *sdev) +{ + struct ssam_tablet_sw *sw = ssam_device_get_drvdata(sdev); + + sysfs_remove_group(&sdev->dev.kobj, &ssam_tablet_sw_group); + + ssam_device_notifier_unregister(sdev, &sw->notif); + cancel_work_sync(&sw->update_work); +} + + +/* -- SSAM KIP tablet switch implementation. -------------------------------- */ + +#define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED 0x1d + +enum ssam_kip_cover_state { + SSAM_KIP_COVER_STATE_DISCONNECTED = 0x01, + SSAM_KIP_COVER_STATE_CLOSED = 0x02, + SSAM_KIP_COVER_STATE_LAPTOP = 0x03, + SSAM_KIP_COVER_STATE_FOLDED_CANVAS = 0x04, + SSAM_KIP_COVER_STATE_FOLDED_BACK = 0x05, + SSAM_KIP_COVER_STATE_BOOK = 0x06, +}; + +static const char *ssam_kip_cover_state_name(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state) +{ + switch (state->state) { + case SSAM_KIP_COVER_STATE_DISCONNECTED: + return "disconnected"; + + case SSAM_KIP_COVER_STATE_CLOSED: + return "closed"; + + case SSAM_KIP_COVER_STATE_LAPTOP: + return "laptop"; + + case SSAM_KIP_COVER_STATE_FOLDED_CANVAS: + return "folded-canvas"; + + case SSAM_KIP_COVER_STATE_FOLDED_BACK: + return "folded-back"; + + case SSAM_KIP_COVER_STATE_BOOK: + return "book"; + + default: + dev_warn(&sw->sdev->dev, "unknown KIP cover state: %u\n", state->state); + return ""; + } +} + +static bool ssam_kip_cover_state_is_tablet_mode(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state) +{ + switch (state->state) { + case SSAM_KIP_COVER_STATE_DISCONNECTED: + case SSAM_KIP_COVER_STATE_FOLDED_CANVAS: + case SSAM_KIP_COVER_STATE_FOLDED_BACK: + case SSAM_KIP_COVER_STATE_BOOK: + return true; + + case SSAM_KIP_COVER_STATE_CLOSED: + case SSAM_KIP_COVER_STATE_LAPTOP: + return false; + + default: + dev_warn(&sw->sdev->dev, "unknown KIP cover state: %d\n", state->state); + return true; + } +} + +SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_get_cover_state, u8, { + .target_category = SSAM_SSH_TC_KIP, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x1d, + .instance_id = 0x00, +}); + +static int ssam_kip_get_cover_state(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state) +{ + int status; + u8 raw; + + status = ssam_retry(__ssam_kip_get_cover_state, sw->sdev->ctrl, &raw); + if (status < 0) { + dev_err(&sw->sdev->dev, "failed to query KIP lid state: %d\n", status); + return status; + } + + state->source = 0; /* Unused for KIP switch. */ + state->state = raw; + return 0; +} + +static u32 ssam_kip_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ + struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif); + + if (event->command_id != SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED) + return 0; /* Return "unhandled". */ + + if (event->length < 1) + dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length); + + schedule_work(&sw->update_work); + return SSAM_NOTIF_HANDLED; +} + +static const struct ssam_tablet_sw_desc ssam_kip_sw_desc = { + .dev = { + .name = "Microsoft Surface KIP Tablet Mode Switch", + .phys = "ssam/01:0e:01:00:01/input0", + }, + .ops = { + .notify = ssam_kip_sw_notif, + .get_state = ssam_kip_get_cover_state, + .state_name = ssam_kip_cover_state_name, + .state_is_tablet_mode = ssam_kip_cover_state_is_tablet_mode, + }, + .event = { + .reg = SSAM_EVENT_REGISTRY_SAM, + .id = { + .target_category = SSAM_SSH_TC_KIP, + .instance = 0, + }, + .mask = SSAM_EVENT_MASK_TARGET, + }, +}; + + +/* -- SSAM POS tablet switch implementation. -------------------------------- */ + +static bool tablet_mode_in_slate_state = true; +module_param(tablet_mode_in_slate_state, bool, 0644); +MODULE_PARM_DESC(tablet_mode_in_slate_state, "Enable tablet mode in slate device posture, default is 'true'"); + +#define SSAM_EVENT_POS_CID_POSTURE_CHANGED 0x03 +#define SSAM_POS_MAX_SOURCES 4 + +enum ssam_pos_source_id { + SSAM_POS_SOURCE_COVER = 0x00, + SSAM_POS_SOURCE_SLS = 0x03, +}; + +enum ssam_pos_state_cover { + SSAM_POS_COVER_DISCONNECTED = 0x01, + SSAM_POS_COVER_CLOSED = 0x02, + SSAM_POS_COVER_LAPTOP = 0x03, + SSAM_POS_COVER_FOLDED_CANVAS = 0x04, + SSAM_POS_COVER_FOLDED_BACK = 0x05, + SSAM_POS_COVER_BOOK = 0x06, +}; + +enum ssam_pos_state_sls { + SSAM_POS_SLS_LID_CLOSED = 0x00, + SSAM_POS_SLS_LAPTOP = 0x01, + SSAM_POS_SLS_SLATE = 0x02, + SSAM_POS_SLS_TABLET = 0x03, +}; + +struct ssam_sources_list { + __le32 count; + __le32 id[SSAM_POS_MAX_SOURCES]; +} __packed; + +static const char *ssam_pos_state_name_cover(struct ssam_tablet_sw *sw, u32 state) +{ + switch (state) { + case SSAM_POS_COVER_DISCONNECTED: + return "disconnected"; + + case SSAM_POS_COVER_CLOSED: + return "closed"; + + case SSAM_POS_COVER_LAPTOP: + return "laptop"; + + case SSAM_POS_COVER_FOLDED_CANVAS: + return "folded-canvas"; + + case SSAM_POS_COVER_FOLDED_BACK: + return "folded-back"; + + case SSAM_POS_COVER_BOOK: + return "book"; + + default: + dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state); + return ""; + } +} + +static const char *ssam_pos_state_name_sls(struct ssam_tablet_sw *sw, u32 state) +{ + switch (state) { + case SSAM_POS_SLS_LID_CLOSED: + return "closed"; + + case SSAM_POS_SLS_LAPTOP: + return "laptop"; + + case SSAM_POS_SLS_SLATE: + return "slate"; + + case SSAM_POS_SLS_TABLET: + return "tablet"; + + default: + dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state); + return ""; + } +} + +static const char *ssam_pos_state_name(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state) +{ + switch (state->source) { + case SSAM_POS_SOURCE_COVER: + return ssam_pos_state_name_cover(sw, state->state); + + case SSAM_POS_SOURCE_SLS: + return ssam_pos_state_name_sls(sw, state->state); + + default: + dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source); + return ""; + } +} + +static bool ssam_pos_state_is_tablet_mode_cover(struct ssam_tablet_sw *sw, u32 state) +{ + switch (state) { + case SSAM_POS_COVER_DISCONNECTED: + case SSAM_POS_COVER_FOLDED_CANVAS: + case SSAM_POS_COVER_FOLDED_BACK: + case SSAM_POS_COVER_BOOK: + return true; + + case SSAM_POS_COVER_CLOSED: + case SSAM_POS_COVER_LAPTOP: + return false; + + default: + dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state); + return true; + } +} + +static bool ssam_pos_state_is_tablet_mode_sls(struct ssam_tablet_sw *sw, u32 state) +{ + switch (state) { + case SSAM_POS_SLS_LAPTOP: + case SSAM_POS_SLS_LID_CLOSED: + return false; + + case SSAM_POS_SLS_SLATE: + return tablet_mode_in_slate_state; + + case SSAM_POS_SLS_TABLET: + return true; + + default: + dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state); + return true; + } +} + +static bool ssam_pos_state_is_tablet_mode(struct ssam_tablet_sw *sw, + const struct ssam_tablet_sw_state *state) +{ + switch (state->source) { + case SSAM_POS_SOURCE_COVER: + return ssam_pos_state_is_tablet_mode_cover(sw, state->state); + + case SSAM_POS_SOURCE_SLS: + return ssam_pos_state_is_tablet_mode_sls(sw, state->state); + + default: + dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source); + return true; + } +} + +static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources) +{ + struct ssam_request rqst; + struct ssam_response rsp; + int status; + + rqst.target_category = SSAM_SSH_TC_POS; + rqst.target_id = SSAM_SSH_TID_SAM; + rqst.command_id = 0x01; + rqst.instance_id = 0x00; + rqst.flags = SSAM_REQUEST_HAS_RESPONSE; + rqst.length = 0; + rqst.payload = NULL; + + rsp.capacity = sizeof(*sources); + rsp.length = 0; + rsp.pointer = (u8 *)sources; + + status = ssam_retry(ssam_request_do_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0); + if (status) + return status; + + /* We need at least the 'sources->count' field. */ + if (rsp.length < sizeof(__le32)) { + dev_err(&sw->sdev->dev, "received source list response is too small\n"); + return -EPROTO; + } + + /* Make sure 'sources->count' matches with the response length. */ + if (get_unaligned_le32(&sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) { + dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n"); + return -EPROTO; + } + + return 0; +} + +static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id) +{ + struct ssam_sources_list sources = {}; + int status; + + status = ssam_pos_get_sources_list(sw, &sources); + if (status) + return status; + + if (get_unaligned_le32(&sources.count) == 0) { + dev_err(&sw->sdev->dev, "no posture sources found\n"); + return -ENODEV; + } + + /* + * We currently don't know what to do with more than one posture + * source. At the moment, only one source seems to be used/provided. + * The WARN_ON() here should hopefully let us know quickly once there + * is a device that provides multiple sources, at which point we can + * then try to figure out how to handle them. + */ + WARN_ON(get_unaligned_le32(&sources.count) > 1); + + *source_id = get_unaligned_le32(&sources.id[0]); + return 0; +} + +SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, { + .target_category = SSAM_SSH_TC_POS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x02, + .instance_id = 0x00, +}); + +static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture) +{ + __le32 source_le = cpu_to_le32(source_id); + __le32 rspval_le = 0; + int status; + + status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl, + &source_le, &rspval_le); + if (status) + return status; + + *posture = le32_to_cpu(rspval_le); + return 0; +} + +static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state) +{ + u32 source_id; + u32 source_state; + int status; + + status = ssam_pos_get_source(sw, &source_id); + if (status) { + dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n", status); + return status; + } + + status = ssam_pos_get_posture_for_source(sw, source_id, &source_state); + if (status) { + dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n", + source_id, status); + return status; + } + + state->source = source_id; + state->state = source_state; + return 0; +} + +static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ + struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif); + + if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED) + return 0; /* Return "unhandled". */ + + if (event->length != sizeof(__le32) * 3) + dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length); + + schedule_work(&sw->update_work); + return SSAM_NOTIF_HANDLED; +} + +static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = { + .dev = { + .name = "Microsoft Surface POS Tablet Mode Switch", + .phys = "ssam/01:26:01:00:01/input0", + }, + .ops = { + .notify = ssam_pos_sw_notif, + .get_state = ssam_pos_get_posture, + .state_name = ssam_pos_state_name, + .state_is_tablet_mode = ssam_pos_state_is_tablet_mode, + }, + .event = { + .reg = SSAM_EVENT_REGISTRY_SAM, + .id = { + .target_category = SSAM_SSH_TC_POS, + .instance = 0, + }, + .mask = SSAM_EVENT_MASK_TARGET, + }, +}; + + +/* -- Driver registration. -------------------------------------------------- */ + +static const struct ssam_device_id ssam_tablet_sw_match[] = { + { SSAM_SDEV(KIP, SAM, 0x00, 0x01), (unsigned long)&ssam_kip_sw_desc }, + { SSAM_SDEV(POS, SAM, 0x00, 0x01), (unsigned long)&ssam_pos_sw_desc }, + { }, +}; +MODULE_DEVICE_TABLE(ssam, ssam_tablet_sw_match); + +static struct ssam_device_driver ssam_tablet_sw_driver = { + .probe = ssam_tablet_sw_probe, + .remove = ssam_tablet_sw_remove, + .match_table = ssam_tablet_sw_match, + .driver = { + .name = "surface_aggregator_tablet_mode_switch", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .pm = &ssam_tablet_sw_pm_ops, + }, +}; +module_ssam_device_driver(ssam_tablet_sw_driver); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Tablet mode switch driver for Surface devices using the Surface Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_dtx.c b/drivers/platform/surface/surface_dtx.c new file mode 100644 index 000000000..d6cd56970 --- /dev/null +++ b/drivers/platform/surface/surface_dtx.c @@ -0,0 +1,1282 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Book (gen. 2 and later) detachment system (DTX) driver. + * + * Provides a user-space interface to properly handle clipboard/tablet + * (containing screen and processor) detachment from the base of the device + * (containing the keyboard and optionally a discrete GPU). Allows to + * acknowledge (to speed things up), abort (e.g. in case the dGPU is still in + * use), or request detachment via user-space. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +/* -- SSAM interface. ------------------------------------------------------- */ + +enum sam_event_cid_bas { + SAM_EVENT_CID_DTX_CONNECTION = 0x0c, + SAM_EVENT_CID_DTX_REQUEST = 0x0e, + SAM_EVENT_CID_DTX_CANCEL = 0x0f, + SAM_EVENT_CID_DTX_LATCH_STATUS = 0x11, +}; + +enum ssam_bas_base_state { + SSAM_BAS_BASE_STATE_DETACH_SUCCESS = 0x00, + SSAM_BAS_BASE_STATE_ATTACHED = 0x01, + SSAM_BAS_BASE_STATE_NOT_FEASIBLE = 0x02, +}; + +enum ssam_bas_latch_status { + SSAM_BAS_LATCH_STATUS_CLOSED = 0x00, + SSAM_BAS_LATCH_STATUS_OPENED = 0x01, + SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN = 0x02, + SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN = 0x03, + SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE = 0x04, +}; + +enum ssam_bas_cancel_reason { + SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE = 0x00, /* Low battery. */ + SSAM_BAS_CANCEL_REASON_TIMEOUT = 0x02, + SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN = 0x03, + SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN = 0x04, + SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE = 0x05, +}; + +struct ssam_bas_base_info { + u8 state; + u8 base_id; +} __packed; + +static_assert(sizeof(struct ssam_bas_base_info) == 2); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_lock, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x06, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_unlock, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x07, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_request, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x08, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_confirm, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x09, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_heartbeat, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0a, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_cancel, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0b, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_base, struct ssam_bas_base_info, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0c, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_device_mode, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0d, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_latch_status, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x11, + .instance_id = 0x00, +}); + + +/* -- Main structures. ------------------------------------------------------ */ + +enum sdtx_device_state { + SDTX_DEVICE_SHUTDOWN_BIT = BIT(0), + SDTX_DEVICE_DIRTY_BASE_BIT = BIT(1), + SDTX_DEVICE_DIRTY_MODE_BIT = BIT(2), + SDTX_DEVICE_DIRTY_LATCH_BIT = BIT(3), +}; + +struct sdtx_device { + struct kref kref; + struct rw_semaphore lock; /* Guards device and controller reference. */ + + struct device *dev; + struct ssam_controller *ctrl; + unsigned long flags; + + struct miscdevice mdev; + wait_queue_head_t waitq; + struct mutex write_lock; /* Guards order of events/notifications. */ + struct rw_semaphore client_lock; /* Guards client list. */ + struct list_head client_list; + + struct delayed_work state_work; + struct { + struct ssam_bas_base_info base; + u8 device_mode; + u8 latch_status; + } state; + + struct delayed_work mode_work; + struct input_dev *mode_switch; + + struct ssam_event_notifier notif; +}; + +enum sdtx_client_state { + SDTX_CLIENT_EVENTS_ENABLED_BIT = BIT(0), +}; + +struct sdtx_client { + struct sdtx_device *ddev; + struct list_head node; + unsigned long flags; + + struct fasync_struct *fasync; + + struct mutex read_lock; /* Guards FIFO buffer read access. */ + DECLARE_KFIFO(buffer, u8, 512); +}; + +static void __sdtx_device_release(struct kref *kref) +{ + struct sdtx_device *ddev = container_of(kref, struct sdtx_device, kref); + + mutex_destroy(&ddev->write_lock); + kfree(ddev); +} + +static struct sdtx_device *sdtx_device_get(struct sdtx_device *ddev) +{ + if (ddev) + kref_get(&ddev->kref); + + return ddev; +} + +static void sdtx_device_put(struct sdtx_device *ddev) +{ + if (ddev) + kref_put(&ddev->kref, __sdtx_device_release); +} + + +/* -- Firmware value translations. ------------------------------------------ */ + +static u16 sdtx_translate_base_state(struct sdtx_device *ddev, u8 state) +{ + switch (state) { + case SSAM_BAS_BASE_STATE_ATTACHED: + return SDTX_BASE_ATTACHED; + + case SSAM_BAS_BASE_STATE_DETACH_SUCCESS: + return SDTX_BASE_DETACHED; + + case SSAM_BAS_BASE_STATE_NOT_FEASIBLE: + return SDTX_DETACH_NOT_FEASIBLE; + + default: + dev_err(ddev->dev, "unknown base state: %#04x\n", state); + return SDTX_UNKNOWN(state); + } +} + +static u16 sdtx_translate_latch_status(struct sdtx_device *ddev, u8 status) +{ + switch (status) { + case SSAM_BAS_LATCH_STATUS_CLOSED: + return SDTX_LATCH_CLOSED; + + case SSAM_BAS_LATCH_STATUS_OPENED: + return SDTX_LATCH_OPENED; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN: + return SDTX_ERR_FAILED_TO_OPEN; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN: + return SDTX_ERR_FAILED_TO_REMAIN_OPEN; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE: + return SDTX_ERR_FAILED_TO_CLOSE; + + default: + dev_err(ddev->dev, "unknown latch status: %#04x\n", status); + return SDTX_UNKNOWN(status); + } +} + +static u16 sdtx_translate_cancel_reason(struct sdtx_device *ddev, u8 reason) +{ + switch (reason) { + case SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE: + return SDTX_DETACH_NOT_FEASIBLE; + + case SSAM_BAS_CANCEL_REASON_TIMEOUT: + return SDTX_DETACH_TIMEDOUT; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN: + return SDTX_ERR_FAILED_TO_OPEN; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN: + return SDTX_ERR_FAILED_TO_REMAIN_OPEN; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE: + return SDTX_ERR_FAILED_TO_CLOSE; + + default: + dev_err(ddev->dev, "unknown cancel reason: %#04x\n", reason); + return SDTX_UNKNOWN(reason); + } +} + + +/* -- IOCTLs. --------------------------------------------------------------- */ + +static int sdtx_ioctl_get_base_info(struct sdtx_device *ddev, + struct sdtx_base_info __user *buf) +{ + struct ssam_bas_base_info raw; + struct sdtx_base_info info; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &raw); + if (status < 0) + return status; + + info.state = sdtx_translate_base_state(ddev, raw.state); + info.base_id = SDTX_BASE_TYPE_SSH(raw.base_id); + + if (copy_to_user(buf, &info, sizeof(info))) + return -EFAULT; + + return 0; +} + +static int sdtx_ioctl_get_device_mode(struct sdtx_device *ddev, u16 __user *buf) +{ + u8 mode; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status < 0) + return status; + + return put_user(mode, buf); +} + +static int sdtx_ioctl_get_latch_status(struct sdtx_device *ddev, u16 __user *buf) +{ + u8 latch; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); + if (status < 0) + return status; + + return put_user(sdtx_translate_latch_status(ddev, latch), buf); +} + +static long __surface_dtx_ioctl(struct sdtx_client *client, unsigned int cmd, unsigned long arg) +{ + struct sdtx_device *ddev = client->ddev; + + lockdep_assert_held_read(&ddev->lock); + + switch (cmd) { + case SDTX_IOCTL_EVENTS_ENABLE: + set_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); + return 0; + + case SDTX_IOCTL_EVENTS_DISABLE: + clear_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); + return 0; + + case SDTX_IOCTL_LATCH_LOCK: + return ssam_retry(ssam_bas_latch_lock, ddev->ctrl); + + case SDTX_IOCTL_LATCH_UNLOCK: + return ssam_retry(ssam_bas_latch_unlock, ddev->ctrl); + + case SDTX_IOCTL_LATCH_REQUEST: + return ssam_retry(ssam_bas_latch_request, ddev->ctrl); + + case SDTX_IOCTL_LATCH_CONFIRM: + return ssam_retry(ssam_bas_latch_confirm, ddev->ctrl); + + case SDTX_IOCTL_LATCH_HEARTBEAT: + return ssam_retry(ssam_bas_latch_heartbeat, ddev->ctrl); + + case SDTX_IOCTL_LATCH_CANCEL: + return ssam_retry(ssam_bas_latch_cancel, ddev->ctrl); + + case SDTX_IOCTL_GET_BASE_INFO: + return sdtx_ioctl_get_base_info(ddev, (struct sdtx_base_info __user *)arg); + + case SDTX_IOCTL_GET_DEVICE_MODE: + return sdtx_ioctl_get_device_mode(ddev, (u16 __user *)arg); + + case SDTX_IOCTL_GET_LATCH_STATUS: + return sdtx_ioctl_get_latch_status(ddev, (u16 __user *)arg); + + default: + return -EINVAL; + } +} + +static long surface_dtx_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct sdtx_client *client = file->private_data; + long status; + + if (down_read_killable(&client->ddev->lock)) + return -ERESTARTSYS; + + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { + up_read(&client->ddev->lock); + return -ENODEV; + } + + status = __surface_dtx_ioctl(client, cmd, arg); + + up_read(&client->ddev->lock); + return status; +} + + +/* -- File operations. ------------------------------------------------------ */ + +static int surface_dtx_open(struct inode *inode, struct file *file) +{ + struct sdtx_device *ddev = container_of(file->private_data, struct sdtx_device, mdev); + struct sdtx_client *client; + + /* Initialize client. */ + client = kzalloc_obj(*client); + if (!client) + return -ENOMEM; + + client->ddev = sdtx_device_get(ddev); + + INIT_LIST_HEAD(&client->node); + + mutex_init(&client->read_lock); + INIT_KFIFO(client->buffer); + + file->private_data = client; + + /* Attach client. */ + down_write(&ddev->client_lock); + + /* + * Do not add a new client if the device has been shut down. Note that + * it's enough to hold the client_lock here as, during shutdown, we + * only acquire that lock and remove clients after marking the device + * as shut down. + */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_write(&ddev->client_lock); + mutex_destroy(&client->read_lock); + sdtx_device_put(client->ddev); + kfree(client); + return -ENODEV; + } + + list_add_tail(&client->node, &ddev->client_list); + up_write(&ddev->client_lock); + + stream_open(inode, file); + return 0; +} + +static int surface_dtx_release(struct inode *inode, struct file *file) +{ + struct sdtx_client *client = file->private_data; + + /* Detach client. */ + down_write(&client->ddev->client_lock); + list_del(&client->node); + up_write(&client->ddev->client_lock); + + /* Free client. */ + sdtx_device_put(client->ddev); + mutex_destroy(&client->read_lock); + kfree(client); + + return 0; +} + +static ssize_t surface_dtx_read(struct file *file, char __user *buf, size_t count, loff_t *offs) +{ + struct sdtx_client *client = file->private_data; + struct sdtx_device *ddev = client->ddev; + unsigned int copied; + int status = 0; + + if (down_read_killable(&ddev->lock)) + return -ERESTARTSYS; + + /* Make sure we're not shut down. */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_read(&ddev->lock); + return -ENODEV; + } + + do { + /* Check availability, wait if necessary. */ + if (kfifo_is_empty(&client->buffer)) { + up_read(&ddev->lock); + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + status = wait_event_interruptible(ddev->waitq, + !kfifo_is_empty(&client->buffer) || + test_bit(SDTX_DEVICE_SHUTDOWN_BIT, + &ddev->flags)); + if (status < 0) + return status; + + if (down_read_killable(&ddev->lock)) + return -ERESTARTSYS; + + /* Need to check that we're not shut down again. */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_read(&ddev->lock); + return -ENODEV; + } + } + + /* Try to read from FIFO. */ + if (mutex_lock_interruptible(&client->read_lock)) { + up_read(&ddev->lock); + return -ERESTARTSYS; + } + + status = kfifo_to_user(&client->buffer, buf, count, &copied); + mutex_unlock(&client->read_lock); + + if (status < 0) { + up_read(&ddev->lock); + return status; + } + + /* We might not have gotten anything, check this here. */ + if (copied == 0 && (file->f_flags & O_NONBLOCK)) { + up_read(&ddev->lock); + return -EAGAIN; + } + } while (copied == 0); + + up_read(&ddev->lock); + return copied; +} + +static __poll_t surface_dtx_poll(struct file *file, struct poll_table_struct *pt) +{ + struct sdtx_client *client = file->private_data; + __poll_t events = 0; + + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) + return EPOLLHUP | EPOLLERR; + + poll_wait(file, &client->ddev->waitq, pt); + + if (!kfifo_is_empty(&client->buffer)) + events |= EPOLLIN | EPOLLRDNORM; + + return events; +} + +static int surface_dtx_fasync(int fd, struct file *file, int on) +{ + struct sdtx_client *client = file->private_data; + + return fasync_helper(fd, file, on, &client->fasync); +} + +static const struct file_operations surface_dtx_fops = { + .owner = THIS_MODULE, + .open = surface_dtx_open, + .release = surface_dtx_release, + .read = surface_dtx_read, + .poll = surface_dtx_poll, + .fasync = surface_dtx_fasync, + .unlocked_ioctl = surface_dtx_ioctl, + .compat_ioctl = surface_dtx_ioctl, +}; + + +/* -- Event handling/forwarding. -------------------------------------------- */ + +/* + * The device operation mode is not immediately updated on the EC when the + * base has been connected, i.e. querying the device mode inside the + * connection event callback yields an outdated value. Thus, we can only + * determine the new tablet-mode switch and device mode values after some + * time. + * + * These delays have been chosen by experimenting. We first delay on connect + * events, then check and validate the device mode against the base state and + * if invalid delay again by the "recheck" delay. + */ +#define SDTX_DEVICE_MODE_DELAY_CONNECT msecs_to_jiffies(100) +#define SDTX_DEVICE_MODE_DELAY_RECHECK msecs_to_jiffies(100) + +struct sdtx_status_event { + struct sdtx_event e; + __u16 v; +} __packed; + +struct sdtx_base_info_event { + struct sdtx_event e; + struct sdtx_base_info v; +} __packed; + +union sdtx_generic_event { + struct sdtx_event common; + struct sdtx_status_event status; + struct sdtx_base_info_event base; +}; + +static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay); + +/* Must be executed with ddev->write_lock held. */ +static void sdtx_push_event(struct sdtx_device *ddev, struct sdtx_event *evt) +{ + const size_t len = sizeof(struct sdtx_event) + evt->length; + struct sdtx_client *client; + + lockdep_assert_held(&ddev->write_lock); + + down_read(&ddev->client_lock); + list_for_each_entry(client, &ddev->client_list, node) { + if (!test_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags)) + continue; + + if (likely(kfifo_avail(&client->buffer) >= len)) + kfifo_in(&client->buffer, (const u8 *)evt, len); + else + dev_warn(ddev->dev, "event buffer overrun\n"); + + kill_fasync(&client->fasync, SIGIO, POLL_IN); + } + up_read(&ddev->client_lock); + + wake_up_interruptible(&ddev->waitq); +} + +static u32 sdtx_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in) +{ + struct sdtx_device *ddev = container_of(nf, struct sdtx_device, notif); + union sdtx_generic_event event; + size_t len; + + /* Validate event payload length. */ + switch (in->command_id) { + case SAM_EVENT_CID_DTX_CONNECTION: + len = 2 * sizeof(u8); + break; + + case SAM_EVENT_CID_DTX_REQUEST: + len = 0; + break; + + case SAM_EVENT_CID_DTX_CANCEL: + len = sizeof(u8); + break; + + case SAM_EVENT_CID_DTX_LATCH_STATUS: + len = sizeof(u8); + break; + + default: + return 0; + } + + if (in->length != len) { + dev_err(ddev->dev, + "unexpected payload size for event %#04x: got %u, expected %zu\n", + in->command_id, in->length, len); + return 0; + } + + mutex_lock(&ddev->write_lock); + + /* Translate event. */ + switch (in->command_id) { + case SAM_EVENT_CID_DTX_CONNECTION: + clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); + + /* If state has not changed: do not send new event. */ + if (ddev->state.base.state == in->data[0] && + ddev->state.base.base_id == in->data[1]) + goto out; + + ddev->state.base.state = in->data[0]; + ddev->state.base.base_id = in->data[1]; + + event.base.e.length = sizeof(struct sdtx_base_info); + event.base.e.code = SDTX_EVENT_BASE_CONNECTION; + event.base.v.state = sdtx_translate_base_state(ddev, in->data[0]); + event.base.v.base_id = SDTX_BASE_TYPE_SSH(in->data[1]); + break; + + case SAM_EVENT_CID_DTX_REQUEST: + event.common.code = SDTX_EVENT_REQUEST; + event.common.length = 0; + break; + + case SAM_EVENT_CID_DTX_CANCEL: + event.status.e.length = sizeof(u16); + event.status.e.code = SDTX_EVENT_CANCEL; + event.status.v = sdtx_translate_cancel_reason(ddev, in->data[0]); + break; + + case SAM_EVENT_CID_DTX_LATCH_STATUS: + clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); + + /* If state has not changed: do not send new event. */ + if (ddev->state.latch_status == in->data[0]) + goto out; + + ddev->state.latch_status = in->data[0]; + + event.status.e.length = sizeof(u16); + event.status.e.code = SDTX_EVENT_LATCH_STATUS; + event.status.v = sdtx_translate_latch_status(ddev, in->data[0]); + break; + } + + sdtx_push_event(ddev, &event.common); + + /* Update device mode on base connection change. */ + if (in->command_id == SAM_EVENT_CID_DTX_CONNECTION) { + unsigned long delay; + + delay = in->data[0] ? SDTX_DEVICE_MODE_DELAY_CONNECT : 0; + sdtx_update_device_mode(ddev, delay); + } + +out: + mutex_unlock(&ddev->write_lock); + return SSAM_NOTIF_HANDLED; +} + + +/* -- State update functions. ----------------------------------------------- */ + +static bool sdtx_device_mode_invalid(u8 mode, u8 base_state) +{ + return ((base_state == SSAM_BAS_BASE_STATE_ATTACHED) && + (mode == SDTX_DEVICE_MODE_TABLET)) || + ((base_state == SSAM_BAS_BASE_STATE_DETACH_SUCCESS) && + (mode != SDTX_DEVICE_MODE_TABLET)); +} + +static void sdtx_device_mode_workfn(struct work_struct *work) +{ + struct sdtx_device *ddev = container_of(work, struct sdtx_device, mode_work.work); + struct sdtx_status_event event; + struct ssam_bas_base_info base; + int status, tablet; + u8 mode; + + /* Get operation mode. */ + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status) { + dev_err(ddev->dev, "failed to get device mode: %d\n", status); + return; + } + + /* Get base info. */ + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); + if (status) { + dev_err(ddev->dev, "failed to get base info: %d\n", status); + return; + } + + /* + * In some cases (specifically when attaching the base), the device + * mode isn't updated right away. Thus we check if the device mode + * makes sense for the given base state and try again later if it + * doesn't. + */ + if (sdtx_device_mode_invalid(mode, base.state)) { + dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); + sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); + return; + } + + mutex_lock(&ddev->write_lock); + clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); + + /* Avoid sending duplicate device-mode events. */ + if (ddev->state.device_mode == mode) { + mutex_unlock(&ddev->write_lock); + return; + } + + ddev->state.device_mode = mode; + + event.e.length = sizeof(u16); + event.e.code = SDTX_EVENT_DEVICE_MODE; + event.v = mode; + + sdtx_push_event(ddev, &event.e); + + /* Send SW_TABLET_MODE event. */ + tablet = mode != SDTX_DEVICE_MODE_LAPTOP; + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); + input_sync(ddev->mode_switch); + + mutex_unlock(&ddev->write_lock); +} + +static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay) +{ + schedule_delayed_work(&ddev->mode_work, delay); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_base(struct sdtx_device *ddev, + struct ssam_bas_base_info info) +{ + struct sdtx_base_info_event event; + + lockdep_assert_held(&ddev->write_lock); + + /* Prevent duplicate events. */ + if (ddev->state.base.state == info.state && + ddev->state.base.base_id == info.base_id) + return; + + ddev->state.base = info; + + event.e.length = sizeof(struct sdtx_base_info); + event.e.code = SDTX_EVENT_BASE_CONNECTION; + event.v.state = sdtx_translate_base_state(ddev, info.state); + event.v.base_id = SDTX_BASE_TYPE_SSH(info.base_id); + + sdtx_push_event(ddev, &event.e); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_mode(struct sdtx_device *ddev, u8 mode) +{ + struct sdtx_status_event event; + int tablet; + + /* + * Note: This function must be called after updating the base state + * via __sdtx_device_state_update_base(), as we rely on the updated + * base state value in the validity check below. + */ + + lockdep_assert_held(&ddev->write_lock); + + if (sdtx_device_mode_invalid(mode, ddev->state.base.state)) { + dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); + sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); + return; + } + + /* Prevent duplicate events. */ + if (ddev->state.device_mode == mode) + return; + + ddev->state.device_mode = mode; + + /* Send event. */ + event.e.length = sizeof(u16); + event.e.code = SDTX_EVENT_DEVICE_MODE; + event.v = mode; + + sdtx_push_event(ddev, &event.e); + + /* Send SW_TABLET_MODE event. */ + tablet = mode != SDTX_DEVICE_MODE_LAPTOP; + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); + input_sync(ddev->mode_switch); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_latch(struct sdtx_device *ddev, u8 status) +{ + struct sdtx_status_event event; + + lockdep_assert_held(&ddev->write_lock); + + /* Prevent duplicate events. */ + if (ddev->state.latch_status == status) + return; + + ddev->state.latch_status = status; + + event.e.length = sizeof(struct sdtx_base_info); + event.e.code = SDTX_EVENT_BASE_CONNECTION; + event.v = sdtx_translate_latch_status(ddev, status); + + sdtx_push_event(ddev, &event.e); +} + +static void sdtx_device_state_workfn(struct work_struct *work) +{ + struct sdtx_device *ddev = container_of(work, struct sdtx_device, state_work.work); + struct ssam_bas_base_info base; + u8 mode, latch; + int status; + + /* Mark everything as dirty. */ + set_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); + set_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); + set_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); + + /* + * Ensure that the state gets marked as dirty before continuing to + * query it. Necessary to ensure that clear_bit() calls in + * sdtx_notifier() and sdtx_device_mode_workfn() actually clear these + * bits if an event is received while updating the state here. + */ + smp_mb__after_atomic(); + + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); + if (status) { + dev_err(ddev->dev, "failed to get base state: %d\n", status); + return; + } + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status) { + dev_err(ddev->dev, "failed to get device mode: %d\n", status); + return; + } + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); + if (status) { + dev_err(ddev->dev, "failed to get latch status: %d\n", status); + return; + } + + mutex_lock(&ddev->write_lock); + + /* + * If the respective dirty-bit has been cleared, an event has been + * received, updating this state. The queried state may thus be out of + * date. At this point, we can safely assume that the state provided + * by the event is either up to date, or we're about to receive + * another event updating it. + */ + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags)) + __sdtx_device_state_update_base(ddev, base); + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags)) + __sdtx_device_state_update_mode(ddev, mode); + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags)) + __sdtx_device_state_update_latch(ddev, latch); + + mutex_unlock(&ddev->write_lock); +} + +static void sdtx_update_device_state(struct sdtx_device *ddev, unsigned long delay) +{ + schedule_delayed_work(&ddev->state_work, delay); +} + + +/* -- Common device initialization. ----------------------------------------- */ + +static int sdtx_device_init(struct sdtx_device *ddev, struct device *dev, + struct ssam_controller *ctrl) +{ + int status, tablet_mode; + + /* Basic initialization. */ + kref_init(&ddev->kref); + init_rwsem(&ddev->lock); + ddev->dev = dev; + ddev->ctrl = ctrl; + + ddev->mdev.minor = MISC_DYNAMIC_MINOR; + ddev->mdev.name = "surface_dtx"; + ddev->mdev.nodename = "surface/dtx"; + ddev->mdev.fops = &surface_dtx_fops; + + ddev->notif.base.priority = 1; + ddev->notif.base.fn = sdtx_notifier; + ddev->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; + ddev->notif.event.id.target_category = SSAM_SSH_TC_BAS; + ddev->notif.event.id.instance = 0; + ddev->notif.event.mask = SSAM_EVENT_MASK_NONE; + ddev->notif.event.flags = SSAM_EVENT_SEQUENCED; + + init_waitqueue_head(&ddev->waitq); + mutex_init(&ddev->write_lock); + init_rwsem(&ddev->client_lock); + INIT_LIST_HEAD(&ddev->client_list); + + INIT_DELAYED_WORK(&ddev->mode_work, sdtx_device_mode_workfn); + INIT_DELAYED_WORK(&ddev->state_work, sdtx_device_state_workfn); + + /* + * Get current device state. We want to guarantee that events are only + * sent when state actually changes. Thus we cannot use special + * "uninitialized" values, as that would cause problems when manually + * querying the state in surface_dtx_pm_complete(). I.e. we would not + * be able to detect state changes there if no change event has been + * received between driver initialization and first device suspension. + * + * Note that we also need to do this before registering the event + * notifier, as that may access the state values. + */ + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &ddev->state.base); + if (status) + return status; + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &ddev->state.device_mode); + if (status) + return status; + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &ddev->state.latch_status); + if (status) + return status; + + /* Set up tablet mode switch. */ + ddev->mode_switch = input_allocate_device(); + if (!ddev->mode_switch) + return -ENOMEM; + + ddev->mode_switch->name = "Microsoft Surface DTX Device Mode Switch"; + ddev->mode_switch->phys = "ssam/01:11:01:00:00/input0"; + ddev->mode_switch->id.bustype = BUS_HOST; + ddev->mode_switch->dev.parent = ddev->dev; + + tablet_mode = (ddev->state.device_mode != SDTX_DEVICE_MODE_LAPTOP); + input_set_capability(ddev->mode_switch, EV_SW, SW_TABLET_MODE); + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet_mode); + + status = input_register_device(ddev->mode_switch); + if (status) { + input_free_device(ddev->mode_switch); + return status; + } + + /* Set up event notifier. */ + status = ssam_notifier_register(ddev->ctrl, &ddev->notif); + if (status) + goto err_notif; + + /* Register miscdevice. */ + status = misc_register(&ddev->mdev); + if (status) + goto err_mdev; + + /* + * Update device state in case it has changed between getting the + * initial mode and registering the event notifier. + */ + sdtx_update_device_state(ddev, 0); + return 0; + +err_notif: + ssam_notifier_unregister(ddev->ctrl, &ddev->notif); + cancel_delayed_work_sync(&ddev->mode_work); +err_mdev: + input_unregister_device(ddev->mode_switch); + return status; +} + +static struct sdtx_device *sdtx_device_create(struct device *dev, struct ssam_controller *ctrl) +{ + struct sdtx_device *ddev; + int status; + + ddev = kzalloc_obj(*ddev); + if (!ddev) + return ERR_PTR(-ENOMEM); + + status = sdtx_device_init(ddev, dev, ctrl); + if (status) { + sdtx_device_put(ddev); + return ERR_PTR(status); + } + + return ddev; +} + +static void sdtx_device_destroy(struct sdtx_device *ddev) +{ + struct sdtx_client *client; + + /* + * Mark device as shut-down. Prevent new clients from being added and + * new operations from being executed. + */ + set_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags); + + /* Disable notifiers, prevent new events from arriving. */ + ssam_notifier_unregister(ddev->ctrl, &ddev->notif); + + /* Stop mode_work, prevent access to mode_switch. */ + cancel_delayed_work_sync(&ddev->mode_work); + + /* Stop state_work. */ + cancel_delayed_work_sync(&ddev->state_work); + + /* With mode_work canceled, we can unregister the mode_switch. */ + input_unregister_device(ddev->mode_switch); + + /* Wake up async clients. */ + down_write(&ddev->client_lock); + list_for_each_entry(client, &ddev->client_list, node) { + kill_fasync(&client->fasync, SIGIO, POLL_HUP); + } + up_write(&ddev->client_lock); + + /* Wake up blocking clients. */ + wake_up_interruptible(&ddev->waitq); + + /* + * Wait for clients to finish their current operation. After this, the + * controller and device references are guaranteed to be no longer in + * use. + */ + down_write(&ddev->lock); + ddev->dev = NULL; + ddev->ctrl = NULL; + up_write(&ddev->lock); + + /* Finally remove the misc-device. */ + misc_deregister(&ddev->mdev); + + /* + * We're now guaranteed that sdtx_device_open() won't be called any + * more, so we can now drop out reference. + */ + sdtx_device_put(ddev); +} + + +/* -- PM ops. --------------------------------------------------------------- */ + +#ifdef CONFIG_PM_SLEEP + +static void surface_dtx_pm_complete(struct device *dev) +{ + struct sdtx_device *ddev = dev_get_drvdata(dev); + + /* + * Normally, the EC will store events while suspended (i.e. in + * display-off state) and release them when resumed (i.e. transitioned + * to display-on state). During hibernation, however, the EC will be + * shut down and does not store events. Furthermore, events might be + * dropped during prolonged suspension (it is currently unknown how + * big this event buffer is and how it behaves on overruns). + * + * To prevent any problems, we update the device state here. We do + * this delayed to ensure that any events sent by the EC directly + * after resuming will be handled first. The delay below has been + * chosen (experimentally), so that there should be ample time for + * these events to be handled, before we check and, if necessary, + * update the state. + */ + sdtx_update_device_state(ddev, msecs_to_jiffies(1000)); +} + +static const struct dev_pm_ops surface_dtx_pm_ops = { + .complete = surface_dtx_pm_complete, +}; + +#else /* CONFIG_PM_SLEEP */ + +static const struct dev_pm_ops surface_dtx_pm_ops = {}; + +#endif /* CONFIG_PM_SLEEP */ + + +/* -- Platform driver. ------------------------------------------------------ */ + +static int surface_dtx_platform_probe(struct platform_device *pdev) +{ + struct ssam_controller *ctrl; + struct sdtx_device *ddev; + + /* Link to EC. */ + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + ddev = sdtx_device_create(&pdev->dev, ctrl); + if (IS_ERR(ddev)) + return PTR_ERR(ddev); + + platform_set_drvdata(pdev, ddev); + return 0; +} + +static void surface_dtx_platform_remove(struct platform_device *pdev) +{ + sdtx_device_destroy(platform_get_drvdata(pdev)); +} + +static const struct acpi_device_id surface_dtx_acpi_match[] = { + { "MSHW0133", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, surface_dtx_acpi_match); + +static struct platform_driver surface_dtx_platform_driver = { + .probe = surface_dtx_platform_probe, + .remove = surface_dtx_platform_remove, + .driver = { + .name = "surface_dtx_pltf", + .acpi_match_table = surface_dtx_acpi_match, + .pm = &surface_dtx_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + + +/* -- SSAM device driver. --------------------------------------------------- */ + +#ifdef CONFIG_SURFACE_AGGREGATOR_BUS + +static int surface_dtx_ssam_probe(struct ssam_device *sdev) +{ + struct sdtx_device *ddev; + + ddev = sdtx_device_create(&sdev->dev, sdev->ctrl); + if (IS_ERR(ddev)) + return PTR_ERR(ddev); + + ssam_device_set_drvdata(sdev, ddev); + return 0; +} + +static void surface_dtx_ssam_remove(struct ssam_device *sdev) +{ + sdtx_device_destroy(ssam_device_get_drvdata(sdev)); +} + +static const struct ssam_device_id surface_dtx_ssam_match[] = { + { SSAM_SDEV(BAS, SAM, 0x00, 0x00) }, + { }, +}; +MODULE_DEVICE_TABLE(ssam, surface_dtx_ssam_match); + +static struct ssam_device_driver surface_dtx_ssam_driver = { + .probe = surface_dtx_ssam_probe, + .remove = surface_dtx_ssam_remove, + .match_table = surface_dtx_ssam_match, + .driver = { + .name = "surface_dtx", + .pm = &surface_dtx_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static int ssam_dtx_driver_register(void) +{ + return ssam_device_driver_register(&surface_dtx_ssam_driver); +} + +static void ssam_dtx_driver_unregister(void) +{ + ssam_device_driver_unregister(&surface_dtx_ssam_driver); +} + +#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ + +static int ssam_dtx_driver_register(void) +{ + return 0; +} + +static void ssam_dtx_driver_unregister(void) +{ +} + +#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ + + +/* -- Module setup. --------------------------------------------------------- */ + +static int __init surface_dtx_init(void) +{ + int status; + + status = ssam_dtx_driver_register(); + if (status) + return status; + + status = platform_driver_register(&surface_dtx_platform_driver); + if (status) + ssam_dtx_driver_unregister(); + + return status; +} +module_init(surface_dtx_init); + +static void __exit surface_dtx_exit(void) +{ + platform_driver_unregister(&surface_dtx_platform_driver); + ssam_dtx_driver_unregister(); +} +module_exit(surface_dtx_exit); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Detachment-system driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_gpe.c b/drivers/platform/surface/surface_gpe.c new file mode 100644 index 000000000..29b264f2a --- /dev/null +++ b/drivers/platform/surface/surface_gpe.c @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Surface GPE/Lid driver to enable wakeup from suspend via the lid by + * properly configuring the respective GPEs. Required for wakeup via lid on + * newer Intel-based Microsoft Surface devices. + * + * Copyright (C) 2020-2022 Maximilian Luz + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include + +/* + * Note: The GPE numbers for the lid devices found below have been obtained + * from ACPI/the DSDT table, specifically from the GPE handler for the + * lid. + */ + +static const struct property_entry lid_device_props_l17[] = { + PROPERTY_ENTRY_U32("gpe", 0x17), + {}, +}; + +static const struct property_entry lid_device_props_l4B[] = { + PROPERTY_ENTRY_U32("gpe", 0x4B), + {}, +}; + +static const struct property_entry lid_device_props_l4D[] = { + PROPERTY_ENTRY_U32("gpe", 0x4D), + {}, +}; + +static const struct property_entry lid_device_props_l4F[] = { + PROPERTY_ENTRY_U32("gpe", 0x4F), + {}, +}; + +static const struct property_entry lid_device_props_l57[] = { + PROPERTY_ENTRY_U32("gpe", 0x57), + {}, +}; + +/* + * Note: When changing this, don't forget to check that the MODULE_ALIAS below + * still fits. + */ +static const struct dmi_system_id dmi_lid_device_table[] = { + { + .ident = "Surface Pro 4", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 4"), + }, + .driver_data = (void *)lid_device_props_l17, + }, + { + .ident = "Surface Pro 5", + .matches = { + /* + * We match for SKU here due to generic product name + * "Surface Pro". + */ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1796"), + }, + .driver_data = (void *)lid_device_props_l4F, + }, + { + .ident = "Surface Pro 5 (LTE)", + .matches = { + /* + * We match for SKU here due to generic product name + * "Surface Pro" + */ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1807"), + }, + .driver_data = (void *)lid_device_props_l4F, + }, + { + .ident = "Surface Pro 6", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 6"), + }, + .driver_data = (void *)lid_device_props_l4F, + }, + { + .ident = "Surface Pro 7", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 7"), + }, + .driver_data = (void *)lid_device_props_l4D, + }, + { + .ident = "Surface Pro 8", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 8"), + }, + .driver_data = (void *)lid_device_props_l4B, + }, + { + .ident = "Surface Book 1", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book"), + }, + .driver_data = (void *)lid_device_props_l17, + }, + { + .ident = "Surface Book 2", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 2"), + }, + .driver_data = (void *)lid_device_props_l17, + }, + { + .ident = "Surface Book 3", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 3"), + }, + .driver_data = (void *)lid_device_props_l4D, + }, + { + .ident = "Surface Laptop 1", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop"), + }, + .driver_data = (void *)lid_device_props_l57, + }, + { + .ident = "Surface Laptop 2", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop 2"), + }, + .driver_data = (void *)lid_device_props_l57, + }, + { + .ident = "Surface Laptop 3 (Intel 13\")", + .matches = { + /* + * We match for SKU here due to different variants: The + * AMD (15") version does not rely on GPEs. + */ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1867:1868"), + }, + .driver_data = (void *)lid_device_props_l4D, + }, + { + .ident = "Surface Laptop 3 (Intel 15\")", + .matches = { + /* + * We match for SKU here due to different variants: The + * AMD (15") version does not rely on GPEs. + */ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1872"), + }, + .driver_data = (void *)lid_device_props_l4D, + }, + { + .ident = "Surface Laptop 4 (Intel 13\")", + .matches = { + /* + * We match for SKU here due to different variants: The + * AMD (15") version does not rely on GPEs. + */ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_4_1950:1951"), + }, + .driver_data = (void *)lid_device_props_l4B, + }, + { + .ident = "Surface Laptop Studio", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop Studio"), + }, + .driver_data = (void *)lid_device_props_l4B, + }, + { } +}; + +struct surface_lid_device { + u32 gpe_number; +}; + +static int surface_lid_enable_wakeup(struct device *dev, bool enable) +{ + const struct surface_lid_device *lid = dev_get_drvdata(dev); + int action = enable ? ACPI_GPE_ENABLE : ACPI_GPE_DISABLE; + acpi_status status; + + status = acpi_set_gpe_wake_mask(NULL, lid->gpe_number, action); + if (ACPI_FAILURE(status)) { + dev_err(dev, "failed to set GPE wake mask: %s\n", + acpi_format_exception(status)); + return -EINVAL; + } + + return 0; +} + +static int __maybe_unused surface_gpe_suspend(struct device *dev) +{ + return surface_lid_enable_wakeup(dev, true); +} + +static int __maybe_unused surface_gpe_resume(struct device *dev) +{ + return surface_lid_enable_wakeup(dev, false); +} + +static SIMPLE_DEV_PM_OPS(surface_gpe_pm, surface_gpe_suspend, surface_gpe_resume); + +static int surface_gpe_probe(struct platform_device *pdev) +{ + struct surface_lid_device *lid; + u32 gpe_number; + acpi_status status; + int ret; + + ret = device_property_read_u32(&pdev->dev, "gpe", &gpe_number); + if (ret) { + dev_err(&pdev->dev, "failed to read 'gpe' property: %d\n", ret); + return ret; + } + + lid = devm_kzalloc(&pdev->dev, sizeof(*lid), GFP_KERNEL); + if (!lid) + return -ENOMEM; + + lid->gpe_number = gpe_number; + platform_set_drvdata(pdev, lid); + + status = acpi_mark_gpe_for_wake(NULL, gpe_number); + if (ACPI_FAILURE(status)) { + dev_err(&pdev->dev, "failed to mark GPE for wake: %s\n", + acpi_format_exception(status)); + return -EINVAL; + } + + status = acpi_enable_gpe(NULL, gpe_number); + if (ACPI_FAILURE(status)) { + dev_err(&pdev->dev, "failed to enable GPE: %s\n", + acpi_format_exception(status)); + return -EINVAL; + } + + ret = surface_lid_enable_wakeup(&pdev->dev, false); + if (ret) + acpi_disable_gpe(NULL, gpe_number); + + return ret; +} + +static void surface_gpe_remove(struct platform_device *pdev) +{ + struct surface_lid_device *lid = dev_get_drvdata(&pdev->dev); + + /* restore default behavior without this module */ + surface_lid_enable_wakeup(&pdev->dev, false); + acpi_disable_gpe(NULL, lid->gpe_number); +} + +static struct platform_driver surface_gpe_driver = { + .probe = surface_gpe_probe, + .remove = surface_gpe_remove, + .driver = { + .name = "surface_gpe", + .pm = &surface_gpe_pm, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static struct platform_device *surface_gpe_device; + +static int __init surface_gpe_init(void) +{ + struct platform_device_info pdevinfo; + const struct dmi_system_id *match; + struct platform_device *pdev; + int status; + + match = dmi_first_match(dmi_lid_device_table); + if (!match) { + pr_info("no compatible Microsoft Surface device found, exiting\n"); + return -ENODEV; + } + + status = platform_driver_register(&surface_gpe_driver); + if (status) + return status; + + pdevinfo = (struct platform_device_info){ + .name = "surface_gpe", + .id = PLATFORM_DEVID_NONE, + .properties = match->driver_data, + }; + + pdev = platform_device_register_full(&pdevinfo); + if (IS_ERR(pdev)) { + platform_driver_unregister(&surface_gpe_driver); + return PTR_ERR(pdev); + } + + surface_gpe_device = pdev; + return 0; +} +module_init(surface_gpe_init); + +static void __exit surface_gpe_exit(void) +{ + platform_device_unregister(surface_gpe_device); + platform_driver_unregister(&surface_gpe_driver); +} +module_exit(surface_gpe_exit); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Surface GPE/Lid Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("dmi:*:svnMicrosoftCorporation:pnSurface*:*"); diff --git a/drivers/platform/surface/surface_hotplug.c b/drivers/platform/surface/surface_hotplug.c new file mode 100644 index 000000000..33a8a9d41 --- /dev/null +++ b/drivers/platform/surface/surface_hotplug.c @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Book (2 and later) hot-plug driver. + * + * Surface Book devices (can) have a hot-pluggable discrete GPU (dGPU). This + * driver is responsible for out-of-band hot-plug event signaling on these + * devices. It is specifically required when the hot-plug device is in D3cold + * and can thus not generate PCIe hot-plug events itself. + * + * Event signaling is handled via ACPI, which will generate the appropriate + * device-check notifications to be picked up by the PCIe hot-plug driver. + * + * Copyright (C) 2019-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include +#include +#include + +static const struct acpi_gpio_params shps_base_presence_int = { 0, 0, false }; +static const struct acpi_gpio_params shps_base_presence = { 1, 0, false }; +static const struct acpi_gpio_params shps_device_power_int = { 2, 0, false }; +static const struct acpi_gpio_params shps_device_power = { 3, 0, false }; +static const struct acpi_gpio_params shps_device_presence_int = { 4, 0, false }; +static const struct acpi_gpio_params shps_device_presence = { 5, 0, false }; + +static const struct acpi_gpio_mapping shps_acpi_gpios[] = { + { "base_presence-int-gpio", &shps_base_presence_int, 1 }, + { "base_presence-gpio", &shps_base_presence, 1 }, + { "device_power-int-gpio", &shps_device_power_int, 1 }, + { "device_power-gpio", &shps_device_power, 1 }, + { "device_presence-int-gpio", &shps_device_presence_int, 1 }, + { "device_presence-gpio", &shps_device_presence, 1 }, + { }, +}; + +/* 5515a847-ed55-4b27-8352-cd320e10360a */ +static const guid_t shps_dsm_guid = + GUID_INIT(0x5515a847, 0xed55, 0x4b27, 0x83, 0x52, 0xcd, 0x32, 0x0e, 0x10, 0x36, 0x0a); + +#define SHPS_DSM_REVISION 1 + +enum shps_dsm_fn { + SHPS_DSM_FN_PCI_NUM_ENTRIES = 0x01, + SHPS_DSM_FN_PCI_GET_ENTRIES = 0x02, + SHPS_DSM_FN_IRQ_BASE_PRESENCE = 0x03, + SHPS_DSM_FN_IRQ_DEVICE_POWER = 0x04, + SHPS_DSM_FN_IRQ_DEVICE_PRESENCE = 0x05, +}; + +enum shps_irq_type { + /* NOTE: Must be in order of enum shps_dsm_fn above. */ + SHPS_IRQ_TYPE_BASE_PRESENCE = 0, + SHPS_IRQ_TYPE_DEVICE_POWER = 1, + SHPS_IRQ_TYPE_DEVICE_PRESENCE = 2, + SHPS_NUM_IRQS, +}; + +static const char *const shps_gpio_names[] = { + [SHPS_IRQ_TYPE_BASE_PRESENCE] = "base_presence", + [SHPS_IRQ_TYPE_DEVICE_POWER] = "device_power", + [SHPS_IRQ_TYPE_DEVICE_PRESENCE] = "device_presence", +}; + +struct shps_device { + struct mutex lock[SHPS_NUM_IRQS]; /* Protects update in shps_dsm_notify_irq() */ + struct gpio_desc *gpio[SHPS_NUM_IRQS]; + unsigned int irq[SHPS_NUM_IRQS]; +}; + +#define SHPS_IRQ_NOT_PRESENT ((unsigned int)-1) + +static enum shps_dsm_fn shps_dsm_fn_for_irq(enum shps_irq_type type) +{ + return SHPS_DSM_FN_IRQ_BASE_PRESENCE + type; +} + +static void shps_dsm_notify_irq(struct platform_device *pdev, enum shps_irq_type type) +{ + struct shps_device *sdev = platform_get_drvdata(pdev); + acpi_handle handle = ACPI_HANDLE(&pdev->dev); + union acpi_object *result; + union acpi_object param; + int value; + + mutex_lock(&sdev->lock[type]); + + value = gpiod_get_value_cansleep(sdev->gpio[type]); + if (value < 0) { + mutex_unlock(&sdev->lock[type]); + dev_err(&pdev->dev, "failed to get gpio: %d (irq=%d)\n", type, value); + return; + } + + dev_dbg(&pdev->dev, "IRQ notification via DSM (irq=%d, value=%d)\n", type, value); + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = value; + + result = acpi_evaluate_dsm_typed(handle, &shps_dsm_guid, SHPS_DSM_REVISION, + shps_dsm_fn_for_irq(type), ¶m, ACPI_TYPE_BUFFER); + if (!result) { + dev_err(&pdev->dev, "IRQ notification via DSM failed (irq=%d, gpio=%d)\n", + type, value); + + } else if (result->buffer.length != 1 || result->buffer.pointer[0] != 0) { + dev_err(&pdev->dev, + "IRQ notification via DSM failed: unexpected result value (irq=%d, gpio=%d)\n", + type, value); + } + + mutex_unlock(&sdev->lock[type]); + + ACPI_FREE(result); +} + +static irqreturn_t shps_handle_irq(int irq, void *data) +{ + struct platform_device *pdev = data; + struct shps_device *sdev = platform_get_drvdata(pdev); + int type; + + /* Figure out which IRQ we're handling. */ + for (type = 0; type < SHPS_NUM_IRQS; type++) + if (irq == sdev->irq[type]) + break; + + /* We should have found our interrupt, if not: this is a bug. */ + if (WARN(type >= SHPS_NUM_IRQS, "invalid IRQ number: %d\n", irq)) + return IRQ_HANDLED; + + /* Forward interrupt to ACPI via DSM. */ + shps_dsm_notify_irq(pdev, type); + return IRQ_HANDLED; +} + +static int shps_setup_irq(struct platform_device *pdev, enum shps_irq_type type) +{ + unsigned long flags = IRQF_ONESHOT | IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING; + struct shps_device *sdev = platform_get_drvdata(pdev); + struct gpio_desc *gpiod; + acpi_handle handle = ACPI_HANDLE(&pdev->dev); + const char *irq_name; + const int dsm = shps_dsm_fn_for_irq(type); + int status, irq; + + /* + * Only set up interrupts that we actually need: The Surface Book 3 + * does not have a DSM for base presence, so don't set up an interrupt + * for that. + */ + if (!acpi_check_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, BIT(dsm))) { + dev_dbg(&pdev->dev, "IRQ notification via DSM not present (irq=%d)\n", type); + return 0; + } + + gpiod = devm_gpiod_get(&pdev->dev, shps_gpio_names[type], GPIOD_ASIS); + if (IS_ERR(gpiod)) + return PTR_ERR(gpiod); + + irq = gpiod_to_irq(gpiod); + if (irq < 0) + return irq; + + irq_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "shps-irq-%d", type); + if (!irq_name) + return -ENOMEM; + + status = devm_request_threaded_irq(&pdev->dev, irq, NULL, shps_handle_irq, + flags, irq_name, pdev); + if (status) + return status; + + dev_dbg(&pdev->dev, "set up irq %d as type %d\n", irq, type); + + sdev->gpio[type] = gpiod; + sdev->irq[type] = irq; + + return 0; +} + +static void surface_hotplug_remove(struct platform_device *pdev) +{ + struct shps_device *sdev = platform_get_drvdata(pdev); + int i; + + /* Ensure that IRQs have been fully handled and won't trigger any more. */ + for (i = 0; i < SHPS_NUM_IRQS; i++) { + if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) + disable_irq(sdev->irq[i]); + + mutex_destroy(&sdev->lock[i]); + } +} + +static int surface_hotplug_probe(struct platform_device *pdev) +{ + struct shps_device *sdev; + int status, i; + + /* + * The MSHW0153 device is also present on the Surface Laptop 3, + * however that doesn't have a hot-pluggable PCIe device. It also + * doesn't have any GPIO interrupts/pins under the MSHW0153, so filter + * it out here. + */ + if (gpiod_count(&pdev->dev, NULL) < 0) + return -ENODEV; + + status = devm_acpi_dev_add_driver_gpios(&pdev->dev, shps_acpi_gpios); + if (status) + return status; + + sdev = devm_kzalloc(&pdev->dev, sizeof(*sdev), GFP_KERNEL); + if (!sdev) + return -ENOMEM; + + platform_set_drvdata(pdev, sdev); + + /* + * Initialize IRQs so that we can safely call surface_hotplug_remove() + * on errors. + */ + for (i = 0; i < SHPS_NUM_IRQS; i++) + sdev->irq[i] = SHPS_IRQ_NOT_PRESENT; + + /* Set up IRQs. */ + for (i = 0; i < SHPS_NUM_IRQS; i++) { + mutex_init(&sdev->lock[i]); + + status = shps_setup_irq(pdev, i); + if (status) { + dev_err(&pdev->dev, "failed to set up IRQ %d: %d\n", i, status); + goto err; + } + } + + /* Ensure everything is up-to-date. */ + for (i = 0; i < SHPS_NUM_IRQS; i++) + if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) + shps_dsm_notify_irq(pdev, i); + + return 0; + +err: + surface_hotplug_remove(pdev); + return status; +} + +static const struct acpi_device_id surface_hotplug_acpi_match[] = { + { "MSHW0153", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, surface_hotplug_acpi_match); + +static struct platform_driver surface_hotplug_driver = { + .probe = surface_hotplug_probe, + .remove = surface_hotplug_remove, + .driver = { + .name = "surface_hotplug", + .acpi_match_table = surface_hotplug_acpi_match, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; +module_platform_driver(surface_hotplug_driver); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Surface Hot-Plug Signaling Driver for Surface Book Devices"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_platform_profile.c b/drivers/platform/surface/surface_platform_profile.c new file mode 100644 index 000000000..0e479e35e --- /dev/null +++ b/drivers/platform/surface/surface_platform_profile.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Platform Profile / Performance Mode driver for Surface System + * Aggregator Module (thermal and fan subsystem). + * + * Copyright (C) 2021-2022 Maximilian Luz + */ + +#include +#include +#include +#include +#include + +#include + +// Enum for the platform performance profile sent to the TMP module. +enum ssam_tmp_profile { + SSAM_TMP_PROFILE_NORMAL = 1, + SSAM_TMP_PROFILE_BATTERY_SAVER = 2, + SSAM_TMP_PROFILE_BETTER_PERFORMANCE = 3, + SSAM_TMP_PROFILE_BEST_PERFORMANCE = 4, +}; + +// Enum for the fan profile sent to the FAN module. This fan profile is +// only sent to the EC if the 'has_fan' property is set. The integers are +// not a typo, they differ from the performance profile indices. +enum ssam_fan_profile { + SSAM_FAN_PROFILE_NORMAL = 2, + SSAM_FAN_PROFILE_BATTERY_SAVER = 1, + SSAM_FAN_PROFILE_BETTER_PERFORMANCE = 3, + SSAM_FAN_PROFILE_BEST_PERFORMANCE = 4, +}; + +struct ssam_tmp_profile_info { + __le32 profile; + __le16 unknown1; + __le16 unknown2; +} __packed; + +struct ssam_platform_profile_device { + struct ssam_device *sdev; + struct device *ppdev; + bool has_fan; +}; + +SSAM_DEFINE_SYNC_REQUEST_CL_R(__ssam_tmp_profile_get, struct ssam_tmp_profile_info, { + .target_category = SSAM_SSH_TC_TMP, + .command_id = 0x02, +}); + +SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_profile_set, __le32, { + .target_category = SSAM_SSH_TC_TMP, + .command_id = 0x03, +}); + +SSAM_DEFINE_SYNC_REQUEST_W(__ssam_fan_profile_set, u8, { + .target_category = SSAM_SSH_TC_FAN, + .target_id = SSAM_SSH_TID_SAM, + .command_id = 0x0e, + .instance_id = 0x01, +}); + +static int ssam_tmp_profile_get(struct ssam_device *sdev, enum ssam_tmp_profile *p) +{ + struct ssam_tmp_profile_info info; + int status; + + status = ssam_retry(__ssam_tmp_profile_get, sdev, &info); + if (status < 0) + return status; + + *p = le32_to_cpu(info.profile); + return 0; +} + +static int ssam_tmp_profile_set(struct ssam_device *sdev, enum ssam_tmp_profile p) +{ + const __le32 profile_le = cpu_to_le32(p); + + return ssam_retry(__ssam_tmp_profile_set, sdev, &profile_le); +} + +static int ssam_fan_profile_set(struct ssam_device *sdev, enum ssam_fan_profile p) +{ + const u8 profile = p; + + return ssam_retry(__ssam_fan_profile_set, sdev->ctrl, &profile); +} + +static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p) +{ + switch (p) { + case SSAM_TMP_PROFILE_NORMAL: + return PLATFORM_PROFILE_BALANCED; + + case SSAM_TMP_PROFILE_BATTERY_SAVER: + return PLATFORM_PROFILE_LOW_POWER; + + case SSAM_TMP_PROFILE_BETTER_PERFORMANCE: + return PLATFORM_PROFILE_BALANCED_PERFORMANCE; + + case SSAM_TMP_PROFILE_BEST_PERFORMANCE: + return PLATFORM_PROFILE_PERFORMANCE; + + default: + dev_err(&sdev->dev, "invalid performance profile: %d", p); + return -EINVAL; + } +} + + +static int convert_profile_to_ssam_tmp(struct ssam_device *sdev, enum platform_profile_option p) +{ + switch (p) { + case PLATFORM_PROFILE_LOW_POWER: + return SSAM_TMP_PROFILE_BATTERY_SAVER; + + case PLATFORM_PROFILE_BALANCED: + return SSAM_TMP_PROFILE_NORMAL; + + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + return SSAM_TMP_PROFILE_BETTER_PERFORMANCE; + + case PLATFORM_PROFILE_PERFORMANCE: + return SSAM_TMP_PROFILE_BEST_PERFORMANCE; + + default: + /* This should have already been caught by platform_profile_store(). */ + WARN(true, "unsupported platform profile"); + return -EOPNOTSUPP; + } +} + +static int convert_profile_to_ssam_fan(struct ssam_device *sdev, enum platform_profile_option p) +{ + switch (p) { + case PLATFORM_PROFILE_LOW_POWER: + return SSAM_FAN_PROFILE_BATTERY_SAVER; + + case PLATFORM_PROFILE_BALANCED: + return SSAM_FAN_PROFILE_NORMAL; + + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + return SSAM_FAN_PROFILE_BETTER_PERFORMANCE; + + case PLATFORM_PROFILE_PERFORMANCE: + return SSAM_FAN_PROFILE_BEST_PERFORMANCE; + + default: + /* This should have already been caught by platform_profile_store(). */ + WARN(true, "unsupported platform profile"); + return -EOPNOTSUPP; + } +} + +static int ssam_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct ssam_platform_profile_device *tpd; + enum ssam_tmp_profile tp; + int status; + + tpd = dev_get_drvdata(dev); + + status = ssam_tmp_profile_get(tpd->sdev, &tp); + if (status) + return status; + + status = convert_ssam_tmp_to_profile(tpd->sdev, tp); + if (status < 0) + return status; + + *profile = status; + return 0; +} + +static int ssam_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ssam_platform_profile_device *tpd; + int tp; + + tpd = dev_get_drvdata(dev); + + tp = convert_profile_to_ssam_tmp(tpd->sdev, profile); + if (tp < 0) + return tp; + + tp = ssam_tmp_profile_set(tpd->sdev, tp); + if (tp < 0) + return tp; + + if (tpd->has_fan) { + tp = convert_profile_to_ssam_fan(tpd->sdev, profile); + if (tp < 0) + return tp; + tp = ssam_fan_profile_set(tpd->sdev, tp); + } + + return tp; +} + +static int ssam_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 const struct platform_profile_ops ssam_platform_profile_ops = { + .probe = ssam_platform_profile_probe, + .profile_get = ssam_platform_profile_get, + .profile_set = ssam_platform_profile_set, +}; + +static int surface_platform_profile_probe(struct ssam_device *sdev) +{ + struct ssam_platform_profile_device *tpd; + + tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL); + if (!tpd) + return -ENOMEM; + + tpd->sdev = sdev; + ssam_device_set_drvdata(sdev, tpd); + + tpd->has_fan = device_property_read_bool(&sdev->dev, "has_fan"); + + tpd->ppdev = devm_platform_profile_register(&sdev->dev, "Surface Platform Profile", + tpd, &ssam_platform_profile_ops); + + return PTR_ERR_OR_ZERO(tpd->ppdev); +} + +static const struct ssam_device_id ssam_platform_profile_match[] = { + { SSAM_SDEV(TMP, SAM, 0x00, 0x01) }, + { }, +}; +MODULE_DEVICE_TABLE(ssam, ssam_platform_profile_match); + +static struct ssam_device_driver surface_platform_profile = { + .probe = surface_platform_profile_probe, + .match_table = ssam_platform_profile_match, + .driver = { + .name = "surface_platform_profile", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; +module_ssam_device_driver(surface_platform_profile); + +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Platform Profile Support for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surfacepro3_button.c b/drivers/platform/surface/surfacepro3_button.c new file mode 100644 index 000000000..388a3e1a4 --- /dev/null +++ b/drivers/platform/surface/surfacepro3_button.c @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * power/home/volume button support for + * Microsoft Surface Pro 3/4 tablet. + * + * Copyright (c) 2015 Intel Corporation. + * All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SURFACE_PRO3_BUTTON_HID "MSHW0028" +#define SURFACE_PRO4_BUTTON_HID "MSHW0040" +#define SURFACE_BUTTON_OBJ_NAME "VGBI" +#define SURFACE_BUTTON_DEVICE_NAME "Surface Pro 3/4 Buttons" + +#define MSHW0040_DSM_REVISION 0x01 +#define MSHW0040_DSM_GET_OMPR 0x02 // get OEM Platform Revision +static const guid_t MSHW0040_DSM_UUID = + GUID_INIT(0x6fd05c69, 0xcde3, 0x49f4, 0x95, 0xed, 0xab, 0x16, 0x65, + 0x49, 0x80, 0x35); + +#define SURFACE_BUTTON_NOTIFY_TABLET_MODE 0xc8 + +#define SURFACE_BUTTON_NOTIFY_PRESS_POWER 0xc6 +#define SURFACE_BUTTON_NOTIFY_RELEASE_POWER 0xc7 + +#define SURFACE_BUTTON_NOTIFY_PRESS_HOME 0xc4 +#define SURFACE_BUTTON_NOTIFY_RELEASE_HOME 0xc5 + +#define SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_UP 0xc0 +#define SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_UP 0xc1 + +#define SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_DOWN 0xc2 +#define SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_DOWN 0xc3 + +MODULE_AUTHOR("Chen Yu"); +MODULE_DESCRIPTION("Surface Pro3 Button Driver"); +MODULE_LICENSE("GPL v2"); + +/* + * Power button, Home button, Volume buttons support is supposed to + * be covered by drivers/input/misc/soc_button_array.c, which is implemented + * according to "Windows ACPI Design Guide for SoC Platforms". + * However surface pro3 seems not to obey the specs, instead it uses + * device VGBI(MSHW0028) for dispatching the events. + * We choose acpi_driver rather than platform_driver/i2c_driver because + * although VGBI has an i2c resource connected to i2c controller, it + * is not embedded in any i2c controller's scope, thus neither platform_device + * will be created, nor i2c_client will be enumerated, we have to use + * acpi_driver. + */ +static const struct acpi_device_id surface_button_device_ids[] = { + {SURFACE_PRO3_BUTTON_HID, 0}, + {SURFACE_PRO4_BUTTON_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, surface_button_device_ids); + +struct surface_button { + unsigned int type; + struct input_dev *input; + char phys[32]; /* for input device */ + unsigned long pushed; + bool suspended; +}; + +static void surface_button_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct surface_button *button = dev_get_drvdata(dev); + struct input_dev *input; + int key_code = KEY_RESERVED; + bool pressed = false; + + switch (event) { + /* Power button press,release handle */ + case SURFACE_BUTTON_NOTIFY_PRESS_POWER: + pressed = true; + fallthrough; + case SURFACE_BUTTON_NOTIFY_RELEASE_POWER: + key_code = KEY_POWER; + break; + /* Home button press,release handle */ + case SURFACE_BUTTON_NOTIFY_PRESS_HOME: + pressed = true; + fallthrough; + case SURFACE_BUTTON_NOTIFY_RELEASE_HOME: + key_code = KEY_LEFTMETA; + break; + /* Volume up button press,release handle */ + case SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_UP: + pressed = true; + fallthrough; + case SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_UP: + key_code = KEY_VOLUMEUP; + break; + /* Volume down button press,release handle */ + case SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_DOWN: + pressed = true; + fallthrough; + case SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_DOWN: + key_code = KEY_VOLUMEDOWN; + break; + case SURFACE_BUTTON_NOTIFY_TABLET_MODE: + dev_warn_once(dev, "Tablet mode is not supported\n"); + break; + default: + dev_info_ratelimited(dev, "Unsupported event [0x%x]\n", event); + break; + } + input = button->input; + if (key_code == KEY_RESERVED) + return; + if (pressed) + pm_wakeup_dev_event(dev, 0, button->suspended); + if (button->suspended) + return; + input_report_key(input, key_code, pressed?1:0); + input_sync(input); +} + +#ifdef CONFIG_PM_SLEEP +static int surface_button_suspend(struct device *dev) +{ + struct surface_button *button = dev_get_drvdata(dev); + + button->suspended = true; + return 0; +} + +static int surface_button_resume(struct device *dev) +{ + struct surface_button *button = dev_get_drvdata(dev); + + button->suspended = false; + return 0; +} +#endif + +/* + * Surface Pro 4 and Surface Book 2 / Surface Pro 2017 use the same device + * ID (MSHW0040) for the power/volume buttons. Make sure this is the right + * device by checking for the _DSM method and OEM Platform Revision. + * + * Returns true if the driver should bind to this device, i.e. the device is + * either MSWH0028 (Pro 3) or MSHW0040 on a Pro 4 or Book 1. + */ +static bool surface_button_check_MSHW0040(struct device *dev, acpi_handle handle) +{ + union acpi_object *result; + u64 oem_platform_rev = 0; // valid revisions are nonzero + + // get OEM platform revision + result = acpi_evaluate_dsm_typed(handle, &MSHW0040_DSM_UUID, + MSHW0040_DSM_REVISION, + MSHW0040_DSM_GET_OMPR, + NULL, ACPI_TYPE_INTEGER); + + /* + * If evaluating the _DSM fails, the method is not present. This means + * that we have either MSHW0028 or MSHW0040 on Pro 4 or Book 1, so we + * should use this driver. We use revision 0 indicating it is + * unavailable. + */ + + if (result) { + oem_platform_rev = result->integer.value; + ACPI_FREE(result); + } + + dev_dbg(dev, "OEM Platform Revision %llu\n", oem_platform_rev); + + return oem_platform_rev == 0; +} + + +static int surface_button_probe(struct platform_device *pdev) +{ + struct surface_button *button; + struct acpi_device *device; + struct input_dev *input; + int error; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + if (strncmp(acpi_device_bid(device), SURFACE_BUTTON_OBJ_NAME, + strlen(SURFACE_BUTTON_OBJ_NAME))) + return -ENODEV; + + if (!surface_button_check_MSHW0040(&pdev->dev, device->handle)) + return -ENODEV; + + button = kzalloc_obj(struct surface_button); + if (!button) + return -ENOMEM; + + platform_set_drvdata(pdev, button); + button->input = input = input_allocate_device(); + if (!input) { + error = -ENOMEM; + goto err_free_button; + } + + strscpy(acpi_device_name(device), SURFACE_BUTTON_DEVICE_NAME); + snprintf(button->phys, sizeof(button->phys), "%s/buttons", + acpi_device_hid(device)); + + input->name = acpi_device_name(device); + input->phys = button->phys; + input->id.bustype = BUS_HOST; + input->dev.parent = &pdev->dev; + input_set_capability(input, EV_KEY, KEY_POWER); + input_set_capability(input, EV_KEY, KEY_LEFTMETA); + input_set_capability(input, EV_KEY, KEY_VOLUMEUP); + input_set_capability(input, EV_KEY, KEY_VOLUMEDOWN); + + error = input_register_device(input); + if (error) + goto err_free_input; + + device_init_wakeup(&pdev->dev, true); + + error = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + surface_button_notify, &pdev->dev); + if (error) { + device_init_wakeup(&pdev->dev, false); + input_unregister_device(input); + goto err_free_button; + } + + dev_info(&pdev->dev, "%s [%s]\n", acpi_device_name(device), + acpi_device_bid(device)); + return 0; + + err_free_input: + input_free_device(input); + err_free_button: + kfree(button); + return error; +} + +static void surface_button_remove(struct platform_device *pdev) +{ + struct surface_button *button = platform_get_drvdata(pdev); + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, surface_button_notify); + device_init_wakeup(&pdev->dev, false); + input_unregister_device(button->input); + kfree(button); +} + +static SIMPLE_DEV_PM_OPS(surface_button_pm, + surface_button_suspend, surface_button_resume); + +static struct platform_driver surface_button_driver = { + .probe = surface_button_probe, + .remove = surface_button_remove, + .driver = { + .name = "surface_pro3_button", + .acpi_match_table = surface_button_device_ids, + .pm = &surface_button_pm, + }, +}; + +module_platform_driver(surface_button_driver); diff --git a/drivers/platform/wmi/Kconfig b/drivers/platform/wmi/Kconfig new file mode 100644 index 000000000..d62f51ff3 --- /dev/null +++ b/drivers/platform/wmi/Kconfig @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# ACPI WMI Core +# + +menuconfig ACPI_WMI + tristate "ACPI-WMI support" + depends on ACPI && X86 + select NLS + help + This option enables support for the ACPI-WMI driver core. + + The ACPI-WMI interface is a proprietary extension of ACPI allowing + the platform firmware to expose WMI (Windows Management Instrumentation) + objects used for managing various aspects of the underlying system. + Mapping between ACPI control methods and WMI objects happens through + special mapper devices (PNP0C14) defined inside the ACPI tables. + + Enabling this option is necessary for building the vendor specific + ACPI-WMI client drivers for Acer, Dell an HP machines (among others). + + It is safe to enable this option even for machines that do not contain + any ACPI-WMI mapper devices at all. + +if ACPI_WMI + +config ACPI_WMI_LEGACY_DEVICE_NAMES + bool "Use legacy WMI device naming scheme" + help + Say Y here to force the WMI driver core to use the old WMI device naming + scheme when creating WMI devices. Doing so might be necessary for some + userspace applications but will cause the registration of WMI devices with + the same GUID to fail in some corner cases. + +source "drivers/platform/wmi/tests/Kconfig" + +endif # ACPI_WMI diff --git a/drivers/platform/wmi/Makefile b/drivers/platform/wmi/Makefile new file mode 100644 index 000000000..2feff94a5 --- /dev/null +++ b/drivers/platform/wmi/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Makefile for linux/drivers/platform/wmi +# ACPI WMI core +# + +wmi-y := core.o marshalling.o string.o +obj-$(CONFIG_ACPI_WMI) += wmi.o + +# Unit tests +obj-y += tests/ diff --git a/drivers/platform/wmi/core.c b/drivers/platform/wmi/core.c new file mode 100644 index 000000000..529825dcf --- /dev/null +++ b/drivers/platform/wmi/core.c @@ -0,0 +1,1612 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ACPI-WMI mapping driver + * + * Copyright (C) 2007-2008 Carlos Corbacho + * + * GUID parsing code from ldm.c is: + * Copyright (C) 2001,2002 Richard Russon + * Copyright (c) 2001-2007 Anton Altaparmakov + * Copyright (C) 2001,2002 Jakob Kemi + * + * WMI bus infrastructure by Andrew Lutomirski and Darren Hart: + * Copyright (C) 2015 Andrew Lutomirski + * Copyright (C) 2017 VMware, Inc. All Rights Reserved. + */ + +#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 "internal.h" + +MODULE_AUTHOR("Carlos Corbacho"); +MODULE_DESCRIPTION("ACPI-WMI Mapping Driver"); +MODULE_LICENSE("GPL"); + +struct guid_block { + guid_t guid; + union { + char object_id[2]; + struct { + unsigned char notify_id; + unsigned char reserved; + }; + }; + u8 instance_count; + u8 flags; +} __packed; +static_assert(sizeof(typeof_member(struct guid_block, guid)) == 16); +static_assert(sizeof(struct guid_block) == 20); +static_assert(__alignof__(struct guid_block) == 1); + +enum { /* wmi_block flags */ + WMI_READ_TAKES_NO_ARGS, + WMI_GUID_DUPLICATED, + WMI_NO_EVENT_DATA, +}; + +struct wmi_block { + struct wmi_device dev; + struct guid_block gblock; + struct acpi_device *acpi_device; + struct rw_semaphore notify_lock; /* Protects notify callback add/remove */ + wmi_notify_handler handler; + void *handler_data; + bool driver_ready; + unsigned long flags; +}; + +struct wmi_guid_count_context { + const guid_t *guid; + int count; +}; + +static DEFINE_IDA(wmi_ida); + +/* + * If the GUID data block is marked as expensive, we must enable and + * explicitily disable data collection. + */ +#define ACPI_WMI_EXPENSIVE BIT(0) +#define ACPI_WMI_METHOD BIT(1) /* GUID is a method */ +#define ACPI_WMI_STRING BIT(2) /* GUID takes & returns a string */ +#define ACPI_WMI_EVENT BIT(3) /* GUID is an event */ + +static const struct acpi_device_id wmi_device_ids[] = { + {"PNP0C14", 0}, + {"pnp0c14", 0}, + { } +}; +MODULE_DEVICE_TABLE(acpi, wmi_device_ids); + +#define dev_to_wblock(__dev) container_of_const(__dev, struct wmi_block, dev.dev) + +/* + * GUID parsing functions + */ + +static bool guid_parse_and_compare(const char *string, const guid_t *guid) +{ + guid_t guid_input; + + if (guid_parse(string, &guid_input)) + return false; + + return guid_equal(&guid_input, guid); +} + +static const void *find_guid_context(struct wmi_block *wblock, + struct wmi_driver *wdriver) +{ + const struct wmi_device_id *id; + + id = wdriver->id_table; + if (!id) + return NULL; + + while (*id->guid_string) { + if (guid_parse_and_compare(id->guid_string, &wblock->gblock.guid)) + return id->context; + id++; + } + return NULL; +} + +#define WMI_ACPI_METHOD_NAME_SIZE 5 + +static inline void get_acpi_method_name(const struct wmi_block *wblock, + const char method, + char buffer[static WMI_ACPI_METHOD_NAME_SIZE]) +{ + static_assert(ARRAY_SIZE(wblock->gblock.object_id) == 2); + static_assert(WMI_ACPI_METHOD_NAME_SIZE >= 5); + + buffer[0] = 'W'; + buffer[1] = method; + buffer[2] = wblock->gblock.object_id[0]; + buffer[3] = wblock->gblock.object_id[1]; + buffer[4] = '\0'; +} + +static int wmidev_match_guid(struct device *dev, const void *data) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + const guid_t *guid = data; + + /* Legacy GUID-based functions are restricted to only see + * a single WMI device for each GUID. + */ + if (test_bit(WMI_GUID_DUPLICATED, &wblock->flags)) + return 0; + + if (guid_equal(guid, &wblock->gblock.guid)) + return 1; + + return 0; +} + +static const struct bus_type wmi_bus_type; + +static const struct device_type wmi_type_event; + +static const struct device_type wmi_type_method; + +static int wmi_device_enable(struct wmi_device *wdev, bool enable) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + char method[WMI_ACPI_METHOD_NAME_SIZE]; + acpi_handle handle; + acpi_status status; + + if (wblock->dev.dev.type == &wmi_type_method) + return 0; + + if (wblock->dev.dev.type == &wmi_type_event) { + /* + * Windows always enables/disables WMI events, even when they are + * not marked as being expensive. We follow this behavior for + * compatibility reasons. + */ + snprintf(method, sizeof(method), "WE%02X", wblock->gblock.notify_id); + } else { + if (!(wblock->gblock.flags & ACPI_WMI_EXPENSIVE)) + return 0; + + get_acpi_method_name(wblock, 'C', method); + } + + /* + * Not all WMI devices marked as expensive actually implement the + * necessary ACPI method. Ignore this missing ACPI method to match + * the behaviour of the Windows driver. + */ + status = acpi_get_handle(wblock->acpi_device->handle, method, &handle); + if (ACPI_FAILURE(status)) + return 0; + + status = acpi_execute_simple_method(handle, NULL, enable); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +static struct wmi_device *wmi_find_device_by_guid(const char *guid_string) +{ + struct device *dev; + guid_t guid; + int ret; + + ret = guid_parse(guid_string, &guid); + if (ret < 0) + return ERR_PTR(ret); + + dev = bus_find_device(&wmi_bus_type, NULL, &guid, wmidev_match_guid); + if (!dev) + return ERR_PTR(-ENODEV); + + return to_wmi_device(dev); +} + +static void wmi_device_put(struct wmi_device *wdev) +{ + put_device(&wdev->dev); +} + +/* + * Exported WMI functions + */ + +/** + * wmi_instance_count - Get number of WMI object instances + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * + * Get the number of WMI object instances. + * + * Returns: Number of WMI object instances or negative error code. + */ +int wmi_instance_count(const char *guid_string) +{ + struct wmi_device *wdev; + int ret; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return PTR_ERR(wdev); + + ret = wmidev_instance_count(wdev); + wmi_device_put(wdev); + + return ret; +} +EXPORT_SYMBOL_GPL(wmi_instance_count); + +/** + * wmidev_instance_count - Get number of WMI object instances + * @wdev: A wmi bus device from a driver + * + * Get the number of WMI object instances. + * + * Returns: Number of WMI object instances. + */ +u8 wmidev_instance_count(struct wmi_device *wdev) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + + return wblock->gblock.instance_count; +} +EXPORT_SYMBOL_GPL(wmidev_instance_count); + +/** + * wmi_evaluate_method - Evaluate a WMI method (deprecated) + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * @method_id: Method ID to call + * @in: Mandatory buffer containing input for the method call + * @out: Empty buffer to return the method results + * + * Call an ACPI-WMI method, the caller must free @out. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmi_evaluate_method(const char *guid_string, u8 instance, u32 method_id, + const struct acpi_buffer *in, struct acpi_buffer *out) +{ + struct wmi_device *wdev; + acpi_status status; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return AE_ERROR; + + status = wmidev_evaluate_method(wdev, instance, method_id, in, out); + + wmi_device_put(wdev); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_evaluate_method); + +/** + * wmidev_evaluate_method - Evaluate a WMI method (deprecated) + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @method_id: Method ID to call + * @in: Mandatory buffer containing input for the method call + * @out: Empty buffer to return the method results + * + * Call an ACPI-WMI method, the caller must free @out. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmidev_evaluate_method(struct wmi_device *wdev, u8 instance, u32 method_id, + const struct acpi_buffer *in, struct acpi_buffer *out) +{ + struct guid_block *block; + struct wmi_block *wblock; + acpi_handle handle; + struct acpi_object_list input; + union acpi_object params[3]; + char method[WMI_ACPI_METHOD_NAME_SIZE]; + + wblock = container_of(wdev, struct wmi_block, dev); + block = &wblock->gblock; + handle = wblock->acpi_device->handle; + + if (!in) + return AE_BAD_DATA; + + if (!(block->flags & ACPI_WMI_METHOD)) + return AE_BAD_DATA; + + if (block->instance_count <= instance) + return AE_BAD_PARAMETER; + + input.count = 3; + input.pointer = params; + + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = instance; + params[1].type = ACPI_TYPE_INTEGER; + params[1].integer.value = method_id; + + if (wblock->gblock.flags & ACPI_WMI_STRING) { + params[2].type = ACPI_TYPE_STRING; + params[2].string.length = in->length; + params[2].string.pointer = in->pointer; + } else { + params[2].type = ACPI_TYPE_BUFFER; + params[2].buffer.length = in->length; + params[2].buffer.pointer = in->pointer; + } + + get_acpi_method_name(wblock, 'M', method); + + return acpi_evaluate_object(handle, method, &input, out); +} +EXPORT_SYMBOL_GPL(wmidev_evaluate_method); + +/** + * wmidev_invoke_method - Invoke a WMI method that returns values + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @method_id: Method ID to call + * @in: Mandatory WMI buffer containing input for the method call + * @out: Mandatory WMI buffer to return the method results + * @min_size: Minimum size of the method result data in bytes + * + * Invoke a WMI method that returns values, the caller must free the resulting + * data inside @out using kfree(). Said data is guaranteed to be aligned on a + * 8-byte boundary. Use wmidev_invoke_procedure() for WMI methods that + * return no values. + * + * Return: 0 on success or negative error code on failure. + */ +int wmidev_invoke_method(struct wmi_device *wdev, u8 instance, u32 method_id, + const struct wmi_buffer *in, struct wmi_buffer *out, size_t min_size) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + struct acpi_buffer aout = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer ain; + union acpi_object *obj; + acpi_status status; + int ret; + + if (wblock->gblock.flags & ACPI_WMI_STRING) { + ret = wmi_marshal_string(in, &ain); + if (ret < 0) + return ret; + } else { + if (in->length > U32_MAX) + return -E2BIG; + + ain.length = in->length; + ain.pointer = in->data; + } + + status = wmidev_evaluate_method(wdev, instance, method_id, &ain, &aout); + + if (wblock->gblock.flags & ACPI_WMI_STRING) + kfree(ain.pointer); + + if (ACPI_FAILURE(status)) + return -EIO; + + obj = aout.pointer; + if (!obj) { + if (min_size != 0) + return -ENOMSG; + + out->length = 0; + out->data = ZERO_SIZE_PTR; + + return 0; + } + + ret = wmi_unmarshal_acpi_object(obj, out, min_size); + kfree(obj); + + return ret; +} +EXPORT_SYMBOL_GPL(wmidev_invoke_method); + +/** + * wmidev_invoke_procedure - Invoke a WMI method that does not return values + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @method_id: Method ID to call + * @in: Mandatory WMI buffer containing input for the method call + * + * Invoke a WMI method that does not return any values. Use wmidev_invoke_method() + * for WMI methods that do return values. + * + * Return: 0 on success or negative error code on failure. + */ +int wmidev_invoke_procedure(struct wmi_device *wdev, u8 instance, u32 method_id, + const struct wmi_buffer *in) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + struct acpi_buffer ain; + acpi_status status; + int ret; + + if (wblock->gblock.flags & ACPI_WMI_STRING) { + ret = wmi_marshal_string(in, &ain); + if (ret < 0) + return ret; + } else { + if (in->length > U32_MAX) + return -E2BIG; + + ain.length = in->length; + ain.pointer = in->data; + } + + status = wmidev_evaluate_method(wdev, instance, method_id, &ain, NULL); + + if (wblock->gblock.flags & ACPI_WMI_STRING) + kfree(ain.pointer); + + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} +EXPORT_SYMBOL_GPL(wmidev_invoke_procedure); + +static acpi_status __query_block(struct wmi_block *wblock, u8 instance, + struct acpi_buffer *out) +{ + struct guid_block *block; + acpi_handle handle; + struct acpi_object_list input; + union acpi_object wq_params[1]; + char method[WMI_ACPI_METHOD_NAME_SIZE]; + + if (!out) + return AE_BAD_PARAMETER; + + block = &wblock->gblock; + handle = wblock->acpi_device->handle; + + if (block->instance_count <= instance) + return AE_BAD_PARAMETER; + + /* Check GUID is a data block */ + if (block->flags & (ACPI_WMI_EVENT | ACPI_WMI_METHOD)) + return AE_ERROR; + + input.count = 1; + input.pointer = wq_params; + wq_params[0].type = ACPI_TYPE_INTEGER; + wq_params[0].integer.value = instance; + + if (instance == 0 && test_bit(WMI_READ_TAKES_NO_ARGS, &wblock->flags)) + input.count = 0; + + get_acpi_method_name(wblock, 'Q', method); + + return acpi_evaluate_object(handle, method, &input, out); +} + +/** + * wmi_query_block - Return contents of a WMI block (deprecated) + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * @out: Empty buffer to return the contents of the data block to + * + * Query a ACPI-WMI block, the caller must free @out. + * + * Return: ACPI object containing the content of the WMI block. + */ +acpi_status wmi_query_block(const char *guid_string, u8 instance, + struct acpi_buffer *out) +{ + struct wmi_block *wblock; + struct wmi_device *wdev; + acpi_status status; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return AE_ERROR; + + if (wmi_device_enable(wdev, true) < 0) + dev_warn(&wdev->dev, "Failed to enable device\n"); + + wblock = container_of(wdev, struct wmi_block, dev); + status = __query_block(wblock, instance, out); + + if (wmi_device_enable(wdev, false) < 0) + dev_warn(&wdev->dev, "Failed to disable device\n"); + + wmi_device_put(wdev); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_query_block); + +/** + * wmidev_block_query - Return contents of a WMI block (deprectated) + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * + * Query an ACPI-WMI block, the caller must free the result. + * + * Return: ACPI object containing the content of the WMI block. + */ +union acpi_object *wmidev_block_query(struct wmi_device *wdev, u8 instance) +{ + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + + if (ACPI_FAILURE(__query_block(wblock, instance, &out))) + return NULL; + + return out.pointer; +} +EXPORT_SYMBOL_GPL(wmidev_block_query); + +/** + * wmidev_query_block - Return contents of a WMI data block + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @out: WMI buffer to fill + * @min_size: Minimum size of the result data in bytes + * + * Query a WMI data block, the caller must free the resulting data inside @out + * using kfree(). Said data is guaranteed to be aligned on a 8-byte boundary. + * + * Return: 0 on success or a negative error code on failure. + */ +int wmidev_query_block(struct wmi_device *wdev, u8 instance, struct wmi_buffer *out, + size_t min_size) +{ + union acpi_object *obj; + int ret; + + obj = wmidev_block_query(wdev, instance); + if (!obj) + return -EIO; + + ret = wmi_unmarshal_acpi_object(obj, out, min_size); + kfree(obj); + + return ret; +} +EXPORT_SYMBOL_GPL(wmidev_query_block); + +/** + * wmi_set_block - Write to a WMI block (deprecated) + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @instance: Instance index + * @in: Buffer containing new values for the data block + * + * Write the contents of the input buffer to an ACPI-WMI data block. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmi_set_block(const char *guid_string, u8 instance, const struct acpi_buffer *in) +{ + struct wmi_device *wdev; + acpi_status status; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return AE_ERROR; + + if (wmi_device_enable(wdev, true) < 0) + dev_warn(&wdev->dev, "Failed to enable device\n"); + + status = wmidev_block_set(wdev, instance, in); + + if (wmi_device_enable(wdev, false) < 0) + dev_warn(&wdev->dev, "Failed to disable device\n"); + + wmi_device_put(wdev); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_set_block); + +/** + * wmidev_block_set - Write to a WMI block (deprecated) + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @in: Buffer containing new values for the data block + * + * Write contents of the input buffer to an ACPI-WMI data block. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmidev_block_set(struct wmi_device *wdev, u8 instance, const struct acpi_buffer *in) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + acpi_handle handle = wblock->acpi_device->handle; + struct guid_block *block = &wblock->gblock; + char method[WMI_ACPI_METHOD_NAME_SIZE]; + struct acpi_object_list input; + union acpi_object params[2]; + + if (!in) + return AE_BAD_DATA; + + if (block->instance_count <= instance) + return AE_BAD_PARAMETER; + + /* Check GUID is a data block */ + if (block->flags & (ACPI_WMI_EVENT | ACPI_WMI_METHOD)) + return AE_ERROR; + + input.count = 2; + input.pointer = params; + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = instance; + + if (wblock->gblock.flags & ACPI_WMI_STRING) { + params[1].type = ACPI_TYPE_STRING; + params[1].string.length = in->length; + params[1].string.pointer = in->pointer; + } else { + params[1].type = ACPI_TYPE_BUFFER; + params[1].buffer.length = in->length; + params[1].buffer.pointer = in->pointer; + } + + get_acpi_method_name(wblock, 'S', method); + + return acpi_evaluate_object(handle, method, &input, NULL); +} +EXPORT_SYMBOL_GPL(wmidev_block_set); + +/** + * wmidev_set_block - Write to a WMI data block + * @wdev: A wmi bus device from a driver + * @instance: Instance index + * @in: WMI buffer containing new values for the data block + * + * Write the content of @in into a WMI data block. + * + * Return: 0 on success or negative error code on failure. + */ +int wmidev_set_block(struct wmi_device *wdev, u8 instance, const struct wmi_buffer *in) +{ + struct wmi_block *wblock = container_of(wdev, struct wmi_block, dev); + struct acpi_buffer buffer; + acpi_status status; + int ret; + + if (wblock->gblock.flags & ACPI_WMI_STRING) { + ret = wmi_marshal_string(in, &buffer); + if (ret < 0) + return ret; + } else { + if (in->length > U32_MAX) + return -E2BIG; + + buffer.length = in->length; + buffer.pointer = in->data; + } + + status = wmidev_block_set(wdev, instance, &buffer); + if (wblock->gblock.flags & ACPI_WMI_STRING) + kfree(buffer.pointer); + + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} +EXPORT_SYMBOL_GPL(wmidev_set_block); + +/** + * wmi_install_notify_handler - Register handler for WMI events (deprecated) + * @guid: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * @handler: Function to handle notifications + * @data: Data to be returned to handler when event is fired + * + * Register a handler for events sent to the ACPI-WMI mapper device. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmi_install_notify_handler(const char *guid, + wmi_notify_handler handler, + void *data) +{ + struct wmi_block *wblock; + struct wmi_device *wdev; + acpi_status status; + + wdev = wmi_find_device_by_guid(guid); + if (IS_ERR(wdev)) + return AE_ERROR; + + wblock = container_of(wdev, struct wmi_block, dev); + + down_write(&wblock->notify_lock); + if (wblock->handler) { + status = AE_ALREADY_ACQUIRED; + } else { + wblock->handler = handler; + wblock->handler_data = data; + + if (wmi_device_enable(wdev, true) < 0) + dev_warn(&wblock->dev.dev, "Failed to enable device\n"); + + status = AE_OK; + } + up_write(&wblock->notify_lock); + + wmi_device_put(wdev); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_install_notify_handler); + +/** + * wmi_remove_notify_handler - Unregister handler for WMI events (deprecated) + * @guid: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * + * Unregister handler for events sent to the ACPI-WMI mapper device. + * + * Return: acpi_status signaling success or error. + */ +acpi_status wmi_remove_notify_handler(const char *guid) +{ + struct wmi_block *wblock; + struct wmi_device *wdev; + acpi_status status; + + wdev = wmi_find_device_by_guid(guid); + if (IS_ERR(wdev)) + return AE_ERROR; + + wblock = container_of(wdev, struct wmi_block, dev); + + down_write(&wblock->notify_lock); + if (!wblock->handler) { + status = AE_NULL_ENTRY; + } else { + if (wmi_device_enable(wdev, false) < 0) + dev_warn(&wblock->dev.dev, "Failed to disable device\n"); + + wblock->handler = NULL; + wblock->handler_data = NULL; + + status = AE_OK; + } + up_write(&wblock->notify_lock); + + wmi_device_put(wdev); + + return status; +} +EXPORT_SYMBOL_GPL(wmi_remove_notify_handler); + +/** + * wmi_has_guid - Check if a GUID is available + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * + * Check if a given GUID is defined by _WDG. + * + * Return: True if GUID is available, false otherwise. + */ +bool wmi_has_guid(const char *guid_string) +{ + struct wmi_device *wdev; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return false; + + wmi_device_put(wdev); + + return true; +} +EXPORT_SYMBOL_GPL(wmi_has_guid); + +/** + * wmi_get_acpi_device_uid() - Get _UID name of ACPI device that defines GUID (deprecated) + * @guid_string: 36 char string of the form fa50ff2b-f2e8-45de-83fa-65417f2f49ba + * + * Find the _UID of ACPI device associated with this WMI GUID. + * + * Return: The ACPI _UID field value or NULL if the WMI GUID was not found. + */ +char *wmi_get_acpi_device_uid(const char *guid_string) +{ + struct wmi_block *wblock; + struct wmi_device *wdev; + char *uid; + + wdev = wmi_find_device_by_guid(guid_string); + if (IS_ERR(wdev)) + return NULL; + + wblock = container_of(wdev, struct wmi_block, dev); + uid = acpi_device_uid(wblock->acpi_device); + + wmi_device_put(wdev); + + return uid; +} +EXPORT_SYMBOL_GPL(wmi_get_acpi_device_uid); + +/* + * sysfs interface + */ +static ssize_t modalias_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "wmi:%pUL\n", &wblock->gblock.guid); +} + +static const DEVICE_ATTR_RO(modalias); + +static ssize_t guid_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "%pUL\n", &wblock->gblock.guid); +} + +static const DEVICE_ATTR_RO(guid); + +static ssize_t instance_count_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "%d\n", (int)wblock->gblock.instance_count); +} + +static const DEVICE_ATTR_RO(instance_count); + +static ssize_t expensive_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "%d\n", + (wblock->gblock.flags & ACPI_WMI_EXPENSIVE) != 0); +} + +static const DEVICE_ATTR_RO(expensive); + +static const struct attribute * const wmi_attrs[] = { + &dev_attr_modalias.attr, + &dev_attr_guid.attr, + &dev_attr_instance_count.attr, + &dev_attr_expensive.attr, + NULL +}; +ATTRIBUTE_GROUPS(wmi); + +static ssize_t notify_id_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "%02X\n", (unsigned int)wblock->gblock.notify_id); +} + +static const DEVICE_ATTR_RO(notify_id); + +static const struct attribute * const wmi_event_attrs[] = { + &dev_attr_notify_id.attr, + NULL +}; +ATTRIBUTE_GROUPS(wmi_event); + +static ssize_t object_id_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + return sysfs_emit(buf, "%c%c\n", wblock->gblock.object_id[0], + wblock->gblock.object_id[1]); +} + +static const DEVICE_ATTR_RO(object_id); + +static ssize_t setable_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct wmi_device *wdev = to_wmi_device(dev); + + return sysfs_emit(buf, "%d\n", (int)wdev->setable); +} + +static const DEVICE_ATTR_RO(setable); + +static const struct attribute * const wmi_data_attrs[] = { + &dev_attr_object_id.attr, + &dev_attr_setable.attr, + NULL +}; +ATTRIBUTE_GROUPS(wmi_data); + +static const struct attribute * const wmi_method_attrs[] = { + &dev_attr_object_id.attr, + NULL +}; +ATTRIBUTE_GROUPS(wmi_method); + +static int wmi_dev_uevent(const struct device *dev, struct kobj_uevent_env *env) +{ + const struct wmi_block *wblock = dev_to_wblock(dev); + + if (add_uevent_var(env, "MODALIAS=wmi:%pUL", &wblock->gblock.guid)) + return -ENOMEM; + + if (add_uevent_var(env, "WMI_GUID=%pUL", &wblock->gblock.guid)) + return -ENOMEM; + + return 0; +} + +static void wmi_dev_release(struct device *dev) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + + kfree(wblock); +} + +static int wmi_dev_match(struct device *dev, const struct device_driver *driver) +{ + const struct wmi_driver *wmi_driver = to_wmi_driver(driver); + struct wmi_block *wblock = dev_to_wblock(dev); + const struct wmi_device_id *id = wmi_driver->id_table; + int ret; + + /* When driver_override is set, only bind to the matching driver */ + ret = device_match_driver_override(dev, driver); + if (ret >= 0) + return ret; + + if (id == NULL) + return 0; + + while (*id->guid_string) { + if (guid_parse_and_compare(id->guid_string, &wblock->gblock.guid)) + return 1; + + id++; + } + + return 0; +} + +static void wmi_dev_disable(void *data) +{ + struct device *dev = data; + + if (wmi_device_enable(to_wmi_device(dev), false) < 0) + dev_warn(dev, "Failed to disable device\n"); +} + +static int wmi_dev_probe(struct device *dev) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + struct wmi_driver *wdriver = to_wmi_driver(dev->driver); + int ret; + + /* Some older WMI drivers will break if instantiated multiple times, + * so they are blocked from probing WMI devices with a duplicated GUID. + * + * New WMI drivers should support being instantiated multiple times. + */ + if (test_bit(WMI_GUID_DUPLICATED, &wblock->flags) && !wdriver->no_singleton) { + dev_warn(dev, "Legacy driver %s cannot be instantiated multiple times\n", + dev->driver->name); + + return -ENODEV; + } + + if (wdriver->notify || wdriver->notify_new) { + if (test_bit(WMI_NO_EVENT_DATA, &wblock->flags) && wdriver->min_event_size) + return -ENODEV; + } + + if (wmi_device_enable(to_wmi_device(dev), true) < 0) + dev_warn(dev, "failed to enable device -- probing anyway\n"); + + /* + * We have to make sure that all devres-managed resources are released first because + * some might still want to access the underlying WMI device. + */ + ret = devm_add_action_or_reset(dev, wmi_dev_disable, dev); + if (ret < 0) + return ret; + + if (wdriver->probe) { + ret = wdriver->probe(to_wmi_device(dev), + find_guid_context(wblock, wdriver)); + if (ret) + return ret; + } + + down_write(&wblock->notify_lock); + wblock->driver_ready = true; + up_write(&wblock->notify_lock); + + return 0; +} + +static void wmi_dev_remove(struct device *dev) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + struct wmi_driver *wdriver = to_wmi_driver(dev->driver); + + down_write(&wblock->notify_lock); + wblock->driver_ready = false; + up_write(&wblock->notify_lock); + + if (wdriver->remove) + wdriver->remove(to_wmi_device(dev)); +} + +static void wmi_dev_shutdown(struct device *dev) +{ + struct wmi_driver *wdriver; + struct wmi_block *wblock; + + if (dev->driver) { + wdriver = to_wmi_driver(dev->driver); + wblock = dev_to_wblock(dev); + + /* + * Some machines return bogus WMI event data when disabling + * the WMI event. Because of this we must prevent the associated + * WMI driver from receiving new WMI events before disabling it. + */ + down_write(&wblock->notify_lock); + wblock->driver_ready = false; + up_write(&wblock->notify_lock); + + if (wdriver->shutdown) + wdriver->shutdown(to_wmi_device(dev)); + + /* + * We still need to disable the WMI device here since devres-managed resources + * like wmi_dev_disable() will not be release during shutdown. + */ + if (wmi_device_enable(to_wmi_device(dev), false) < 0) + dev_warn(dev, "Failed to disable device\n"); + } +} + +static const struct class wmi_bus_class = { + .name = "wmi_bus", +}; + +static const struct bus_type wmi_bus_type = { + .name = "wmi", + .dev_groups = wmi_groups, + .driver_override = true, + .match = wmi_dev_match, + .uevent = wmi_dev_uevent, + .probe = wmi_dev_probe, + .remove = wmi_dev_remove, + .shutdown = wmi_dev_shutdown, +}; + +static const struct device_type wmi_type_event = { + .name = "event", + .groups = wmi_event_groups, + .release = wmi_dev_release, +}; + +static const struct device_type wmi_type_method = { + .name = "method", + .groups = wmi_method_groups, + .release = wmi_dev_release, +}; + +static const struct device_type wmi_type_data = { + .name = "data", + .groups = wmi_data_groups, + .release = wmi_dev_release, +}; + +static int wmi_count_guids(struct device *dev, void *data) +{ + struct wmi_guid_count_context *context = data; + struct wmi_block *wblock = dev_to_wblock(dev); + + if (guid_equal(&wblock->gblock.guid, context->guid)) + context->count++; + + return 0; +} + +static int guid_count(const guid_t *guid) +{ + struct wmi_guid_count_context context = { + .guid = guid, + .count = 0, + }; + int ret; + + ret = bus_for_each_dev(&wmi_bus_type, NULL, &context, wmi_count_guids); + if (ret < 0) + return ret; + + return context.count; +} + +static int wmi_dev_set_name(struct wmi_block *wblock, int count) +{ + if (IS_ENABLED(CONFIG_ACPI_WMI_LEGACY_DEVICE_NAMES)) { + if (count) + return dev_set_name(&wblock->dev.dev, "%pUL-%d", &wblock->gblock.guid, + count); + else + return dev_set_name(&wblock->dev.dev, "%pUL", &wblock->gblock.guid); + } + + return dev_set_name(&wblock->dev.dev, "%pUL-%d", &wblock->gblock.guid, wblock->dev.dev.id); +} + +static int wmi_create_device(struct device *wmi_bus_dev, + struct wmi_block *wblock, + struct acpi_device *device) +{ + char method[WMI_ACPI_METHOD_NAME_SIZE]; + struct acpi_device_info *info; + acpi_handle method_handle; + acpi_status status; + int count, ret; + + if (wblock->gblock.flags & ACPI_WMI_EVENT) { + wblock->dev.dev.type = &wmi_type_event; + goto out_init; + } + + if (wblock->gblock.flags & ACPI_WMI_METHOD) { + get_acpi_method_name(wblock, 'M', method); + if (!acpi_has_method(device->handle, method)) { + dev_warn(wmi_bus_dev, + FW_BUG "%s method block execution control method not found\n", + method); + + return -ENXIO; + } + + wblock->dev.dev.type = &wmi_type_method; + goto out_init; + } + + /* + * Data Block Query Control Method (WQxx by convention) is + * required per the WMI documentation. If it is not present, + * we ignore this data block. + */ + get_acpi_method_name(wblock, 'Q', method); + status = acpi_get_handle(device->handle, method, &method_handle); + if (ACPI_FAILURE(status)) { + dev_warn(wmi_bus_dev, + FW_BUG "%s data block query control method not found\n", + method); + + return -ENXIO; + } + + status = acpi_get_object_info(method_handle, &info); + if (ACPI_FAILURE(status)) + return -EIO; + + wblock->dev.dev.type = &wmi_type_data; + + /* + * The Microsoft documentation specifically states: + * + * Data blocks registered with only a single instance + * can ignore the parameter. + * + * ACPICA will get mad at us if we call the method with the wrong number + * of arguments, so check what our method expects. (On some Dell + * laptops, WQxx may not be a method at all.) + */ + if (info->type != ACPI_TYPE_METHOD || info->param_count == 0) + set_bit(WMI_READ_TAKES_NO_ARGS, &wblock->flags); + + kfree(info); + + get_acpi_method_name(wblock, 'S', method); + if (acpi_has_method(device->handle, method)) + wblock->dev.setable = true; + + out_init: + init_rwsem(&wblock->notify_lock); + wblock->driver_ready = false; + wblock->dev.dev.bus = &wmi_bus_type; + wblock->dev.dev.parent = wmi_bus_dev; + + count = guid_count(&wblock->gblock.guid); + if (count < 0) + return count; + + if (count) + set_bit(WMI_GUID_DUPLICATED, &wblock->flags); + + ret = ida_alloc(&wmi_ida, GFP_KERNEL); + if (ret < 0) + return ret; + + wblock->dev.dev.id = ret; + ret = wmi_dev_set_name(wblock, count); + if (ret < 0) { + ida_free(&wmi_ida, wblock->dev.dev.id); + return ret; + } + + device_initialize(&wblock->dev.dev); + + return 0; +} + +static int wmi_add_device(struct platform_device *pdev, struct wmi_device *wdev) +{ + struct device_link *link; + + /* + * Many aggregate WMI drivers do not use -EPROBE_DEFER when they + * are unable to find a WMI device during probe, instead they require + * all WMI devices associated with an platform device to become available + * at once. This device link thus prevents WMI drivers from probing until + * the associated platform device has finished probing (and has registered + * all discovered WMI devices). + */ + + link = device_link_add(&wdev->dev, &pdev->dev, DL_FLAG_AUTOREMOVE_SUPPLIER); + if (!link) + return -EINVAL; + + return device_add(&wdev->dev); +} + +/* + * Parse the _WDG method for the GUID data blocks + */ +static int parse_wdg(struct device *wmi_bus_dev, struct platform_device *pdev) +{ + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + const struct guid_block *gblock; + bool event_data_available; + struct wmi_block *wblock; + union acpi_object *obj; + acpi_status status; + u32 i, total; + int retval; + + status = acpi_evaluate_object(device->handle, "_WDG", NULL, &out); + if (ACPI_FAILURE(status)) + return -ENXIO; + + obj = out.pointer; + if (!obj) + return -ENXIO; + + if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return -ENXIO; + } + + event_data_available = acpi_has_method(device->handle, "_WED"); + gblock = (const struct guid_block *)obj->buffer.pointer; + total = obj->buffer.length / sizeof(struct guid_block); + + for (i = 0; i < total; i++) { + if (!gblock[i].instance_count) { + dev_info(wmi_bus_dev, FW_INFO "%pUL has zero instances\n", &gblock[i].guid); + continue; + } + + wblock = kzalloc_obj(*wblock); + if (!wblock) + continue; + + wblock->acpi_device = device; + wblock->gblock = gblock[i]; + if (gblock[i].flags & ACPI_WMI_EVENT && !event_data_available) + set_bit(WMI_NO_EVENT_DATA, &wblock->flags); + + retval = wmi_create_device(wmi_bus_dev, wblock, device); + if (retval) { + kfree(wblock); + continue; + } + + retval = wmi_add_device(pdev, &wblock->dev); + if (retval) { + dev_err(wmi_bus_dev, "failed to register %pUL\n", + &wblock->gblock.guid); + + ida_free(&wmi_ida, wblock->dev.dev.id); + put_device(&wblock->dev.dev); + } + } + + kfree(obj); + + return 0; +} + +static int wmi_get_notify_data(struct wmi_block *wblock, union acpi_object **obj) +{ + struct acpi_buffer data = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object param = { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = wblock->gblock.notify_id, + } + }; + struct acpi_object_list input = { + .count = 1, + .pointer = ¶m, + }; + acpi_status status; + + status = acpi_evaluate_object(wblock->acpi_device->handle, "_WED", &input, &data); + if (ACPI_FAILURE(status)) { + dev_warn(&wblock->dev.dev, "Failed to get event data\n"); + return -EIO; + } + + *obj = data.pointer; + + return 0; +} + +static void wmi_notify_driver(struct wmi_block *wblock, union acpi_object *obj) +{ + struct wmi_driver *driver = to_wmi_driver(wblock->dev.dev.driver); + struct wmi_buffer dummy = { + .length = 0, + .data = ZERO_SIZE_PTR, + }; + struct wmi_buffer buffer; + int ret; + + if (!obj && driver->min_event_size) { + dev_warn(&wblock->dev.dev, "Event contains no event data\n"); + return; + } + + if (driver->notify) + driver->notify(&wblock->dev, obj); + + if (driver->notify_new) { + if (!obj) { + driver->notify_new(&wblock->dev, &dummy); + return; + } + + ret = wmi_unmarshal_acpi_object(obj, &buffer, driver->min_event_size); + if (ret < 0) { + dev_warn(&wblock->dev.dev, "Failed to unmarshal event data: %d\n", ret); + return; + } + + driver->notify_new(&wblock->dev, &buffer); + kfree(buffer.data); + } +} + +static int wmi_notify_device(struct device *dev, void *data) +{ + struct wmi_block *wblock = dev_to_wblock(dev); + union acpi_object *obj = NULL; + u32 *event = data; + int ret; + + if (!(wblock->gblock.flags & ACPI_WMI_EVENT && wblock->gblock.notify_id == *event)) + return 0; + + /* The ACPI WMI specification says that _WED should be + * evaluated every time an notification is received, even + * if no consumers are present. + * + * Some firmware implementations actually depend on this + * by using a queue for events which will fill up if the + * WMI driver core stops evaluating _WED due to missing + * WMI event consumers. + */ + if (!test_bit(WMI_NO_EVENT_DATA, &wblock->flags)) { + ret = wmi_get_notify_data(wblock, &obj); + if (ret < 0) + return -EIO; + } + + down_read(&wblock->notify_lock); + + if (wblock->dev.dev.driver && wblock->driver_ready) + wmi_notify_driver(wblock, obj); + + if (wblock->handler) + wblock->handler(obj, wblock->handler_data); + + up_read(&wblock->notify_lock); + + kfree(obj); + + acpi_bus_generate_netlink_event("wmi", acpi_dev_name(wblock->acpi_device), *event, 0); + + return -EBUSY; +} + +static void acpi_wmi_notify_handler(acpi_handle handle, u32 event, void *context) +{ + struct device *wmi_bus_dev = context; + + device_for_each_child(wmi_bus_dev, &event, wmi_notify_device); +} + +static int wmi_remove_device(struct device *dev, void *data) +{ + int id = dev->id; + + device_unregister(dev); + ida_free(&wmi_ida, id); + + return 0; +} + +static void acpi_wmi_remove(struct platform_device *device) +{ + struct device *wmi_bus_device = dev_get_drvdata(&device->dev); + + device_for_each_child_reverse(wmi_bus_device, NULL, wmi_remove_device); +} + +static void acpi_wmi_remove_notify_handler(void *data) +{ + struct acpi_device *acpi_device = data; + + acpi_remove_notify_handler(acpi_device->handle, ACPI_ALL_NOTIFY, acpi_wmi_notify_handler); +} + +static void acpi_wmi_remove_bus_device(void *data) +{ + struct device *wmi_bus_dev = data; + + device_unregister(wmi_bus_dev); +} + +static int acpi_wmi_probe(struct platform_device *device) +{ + struct acpi_device *acpi_device; + struct device *wmi_bus_dev; + acpi_status status; + int error; + + acpi_device = ACPI_COMPANION(&device->dev); + if (!acpi_device) { + dev_err(&device->dev, "ACPI companion is missing\n"); + return -ENODEV; + } + + wmi_bus_dev = device_create(&wmi_bus_class, &device->dev, MKDEV(0, 0), NULL, "wmi_bus-%s", + dev_name(&device->dev)); + if (IS_ERR(wmi_bus_dev)) + return PTR_ERR(wmi_bus_dev); + + error = devm_add_action_or_reset(&device->dev, acpi_wmi_remove_bus_device, wmi_bus_dev); + if (error < 0) + return error; + + dev_set_drvdata(&device->dev, wmi_bus_dev); + + status = acpi_install_notify_handler(acpi_device->handle, ACPI_ALL_NOTIFY, + acpi_wmi_notify_handler, wmi_bus_dev); + if (ACPI_FAILURE(status)) { + dev_err(&device->dev, "Error installing notify handler\n"); + return -ENODEV; + } + error = devm_add_action_or_reset(&device->dev, acpi_wmi_remove_notify_handler, + acpi_device); + if (error < 0) + return error; + + error = parse_wdg(wmi_bus_dev, device); + if (error) { + dev_err(&device->dev, "Failed to parse _WDG method\n"); + return error; + } + + return 0; +} + +int __must_check __wmi_driver_register(struct wmi_driver *driver, + struct module *owner) +{ + driver->driver.owner = owner; + driver->driver.bus = &wmi_bus_type; + + return driver_register(&driver->driver); +} +EXPORT_SYMBOL(__wmi_driver_register); + +/** + * wmi_driver_unregister() - Unregister a WMI driver + * @driver: WMI driver to unregister + * + * Unregisters a WMI driver from the WMI bus. + */ +void wmi_driver_unregister(struct wmi_driver *driver) +{ + driver_unregister(&driver->driver); +} +EXPORT_SYMBOL(wmi_driver_unregister); + +static struct platform_driver acpi_wmi_driver = { + .driver = { + .name = "acpi-wmi", + .acpi_match_table = wmi_device_ids, + }, + .probe = acpi_wmi_probe, + .remove = acpi_wmi_remove, +}; + +static int __init acpi_wmi_init(void) +{ + int error; + + if (acpi_disabled) + return -ENODEV; + + error = class_register(&wmi_bus_class); + if (error) + return error; + + error = bus_register(&wmi_bus_type); + if (error) + goto err_unreg_class; + + error = platform_driver_register(&acpi_wmi_driver); + if (error) { + pr_err("Error loading mapper\n"); + goto err_unreg_bus; + } + + return 0; + +err_unreg_bus: + bus_unregister(&wmi_bus_type); + +err_unreg_class: + class_unregister(&wmi_bus_class); + + return error; +} + +static void __exit acpi_wmi_exit(void) +{ + platform_driver_unregister(&acpi_wmi_driver); + bus_unregister(&wmi_bus_type); + class_unregister(&wmi_bus_class); +} + +subsys_initcall_sync(acpi_wmi_init); +module_exit(acpi_wmi_exit); diff --git a/drivers/platform/wmi/internal.h b/drivers/platform/wmi/internal.h new file mode 100644 index 000000000..c02908694 --- /dev/null +++ b/drivers/platform/wmi/internal.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Internal interfaces used by the WMI core. + * + * Copyright (C) 2025 Armin Wolf + */ + +#ifndef _WMI_INTERNAL_H_ +#define _WMI_INTERNAL_H_ + +union acpi_object; +struct wmi_buffer; + +int wmi_unmarshal_acpi_object(const union acpi_object *obj, struct wmi_buffer *buffer, + size_t min_size); +int wmi_marshal_string(const struct wmi_buffer *buffer, struct acpi_buffer *out); + +#endif /* _WMI_INTERNAL_H_ */ diff --git a/drivers/platform/wmi/marshalling.c b/drivers/platform/wmi/marshalling.c new file mode 100644 index 000000000..870918325 --- /dev/null +++ b/drivers/platform/wmi/marshalling.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ACPI-WMI buffer marshalling. + * + * Copyright (C) 2025 Armin Wolf + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "internal.h" + +static int wmi_adjust_buffer_length(size_t *length, const union acpi_object *obj) +{ + size_t alignment, size; + + switch (obj->type) { + case ACPI_TYPE_INTEGER: + /* + * Integers are threated as 32 bit even if the ACPI DSDT + * declares 64 bit integer width. + */ + alignment = 4; + size = sizeof(u32); + break; + case ACPI_TYPE_STRING: + /* + * Strings begin with a single little-endian 16-bit field containing + * the string length in bytes and are encoded as UTF-16LE with a terminating + * nul character. + */ + if (obj->string.length + 1 > U16_MAX / 2) + return -EOVERFLOW; + + alignment = 2; + size = struct_size_t(struct wmi_string, chars, obj->string.length + 1); + break; + case ACPI_TYPE_BUFFER: + /* + * Buffers are copied as-is. + */ + alignment = 1; + size = obj->buffer.length; + break; + default: + return -EPROTO; + } + + *length = size_add(ALIGN(*length, alignment), size); + + return 0; +} + +static int wmi_obj_get_buffer_length(const union acpi_object *obj, size_t *length) +{ + size_t total = 0; + int ret; + + if (obj->type == ACPI_TYPE_PACKAGE) { + for (int i = 0; i < obj->package.count; i++) { + ret = wmi_adjust_buffer_length(&total, &obj->package.elements[i]); + if (ret < 0) + return ret; + } + } else { + ret = wmi_adjust_buffer_length(&total, obj); + if (ret < 0) + return ret; + } + + *length = total; + + return 0; +} + +static int wmi_obj_transform_simple(const union acpi_object *obj, u8 *buffer, size_t *consumed) +{ + struct wmi_string *string; + size_t length; + __le32 value; + u8 *aligned; + + switch (obj->type) { + case ACPI_TYPE_INTEGER: + aligned = PTR_ALIGN(buffer, 4); + length = sizeof(value); + + value = cpu_to_le32(obj->integer.value); + memcpy(aligned, &value, length); + break; + case ACPI_TYPE_STRING: + aligned = PTR_ALIGN(buffer, 2); + string = (struct wmi_string *)aligned; + length = struct_size(string, chars, obj->string.length + 1); + + /* We do not have to worry about unaligned accesses here as the WMI + * string will already be aligned on a two-byte boundary. + */ + string->length = cpu_to_le16((obj->string.length + 1) * 2); + for (int i = 0; i < obj->string.length; i++) + string->chars[i] = cpu_to_le16(obj->string.pointer[i]); + + /* + * The Windows WMI-ACPI driver always emits a terminating nul character, + * so we emulate this behavior here as well. + */ + string->chars[obj->string.length] = '\0'; + break; + case ACPI_TYPE_BUFFER: + aligned = buffer; + length = obj->buffer.length; + + memcpy(aligned, obj->buffer.pointer, length); + break; + default: + return -EPROTO; + } + + *consumed = (aligned - buffer) + length; + + return 0; +} + +static int wmi_obj_transform(const union acpi_object *obj, u8 *buffer) +{ + size_t consumed; + int ret; + + if (obj->type == ACPI_TYPE_PACKAGE) { + for (int i = 0; i < obj->package.count; i++) { + ret = wmi_obj_transform_simple(&obj->package.elements[i], buffer, + &consumed); + if (ret < 0) + return ret; + + buffer += consumed; + } + } else { + ret = wmi_obj_transform_simple(obj, buffer, &consumed); + if (ret < 0) + return ret; + } + + return 0; +} + +int wmi_unmarshal_acpi_object(const union acpi_object *obj, struct wmi_buffer *buffer, + size_t min_size) +{ + size_t length, alloc_length; + u8 *data; + int ret; + + ret = wmi_obj_get_buffer_length(obj, &length); + if (ret < 0) + return ret; + + if (length < min_size) + return -ENODATA; + + if (ARCH_KMALLOC_MINALIGN < 8) { + /* + * kmalloc() guarantees that the alignment of the resulting memory allocation is at + * least the largest power-of-two divisor of the allocation size. The WMI buffer + * data needs to be aligned on a 8 byte boundary to properly support 64-bit WMI + * integers, so we have to round the allocation size to the next multiple of 8. + */ + alloc_length = round_up(length, 8); + } else { + alloc_length = length; + } + + data = kzalloc(alloc_length, GFP_KERNEL); + if (!data) + return -ENOMEM; + + ret = wmi_obj_transform(obj, data); + if (ret < 0) { + kfree(data); + return ret; + } + + buffer->length = length; + buffer->data = data; + + return 0; +} +EXPORT_SYMBOL_IF_KUNIT(wmi_unmarshal_acpi_object); + +int wmi_marshal_string(const struct wmi_buffer *buffer, struct acpi_buffer *out) +{ + const struct wmi_string *string; + u16 length, value; + size_t chars; + char *str; + + if (buffer->length < sizeof(*string)) + return -ENODATA; + + string = buffer->data; + length = get_unaligned_le16(&string->length); + if (buffer->length < sizeof(*string) + length) + return -ENODATA; + + /* Each character needs to be 16 bits long */ + if (length % 2) + return -EINVAL; + + chars = length / 2; + str = kmalloc(chars + 1, GFP_KERNEL); + if (!str) + return -ENOMEM; + + for (int i = 0; i < chars; i++) { + value = get_unaligned_le16(&string->chars[i]); + + /* ACPI only accepts ASCII strings */ + if (value > 0x7F) { + kfree(str); + return -EINVAL; + } + + str[i] = value & 0xFF; + + /* + * ACPI strings should only contain a single nul character at the end. + * Because of this we must not copy any padding from the WMI string. + */ + if (!value) { + /* ACPICA wants the length of the string without the nul character */ + out->length = i; + out->pointer = str; + return 0; + } + } + + str[chars] = '\0'; + + out->length = chars; + out->pointer = str; + + return 0; +} +EXPORT_SYMBOL_IF_KUNIT(wmi_marshal_string); diff --git a/drivers/platform/wmi/string.c b/drivers/platform/wmi/string.c new file mode 100644 index 000000000..0fc43218a --- /dev/null +++ b/drivers/platform/wmi/string.c @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * WMI string utility functions. + * + * Copyright (C) 2025 Armin Wolf + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static_assert(sizeof(__le16) == sizeof(wchar_t)); + +/** + * wmi_string_to_utf8s - Convert a WMI string into a UTF8 string. + * @str: WMI string representation + * @dst: Buffer to fill with UTF8 characters + * @length: Length of the destination buffer + * + * Convert as WMI string into a standard UTF8 string. The conversion will stop + * once a NUL character is detected or when the buffer is full. Any invalid UTF16 + * characters will be ignored. The resulting UTF8 string will always be NUL-terminated + * when this function returns successfully. + * + * Return: Length of the resulting UTF8 string or negative errno code on failure. + */ +ssize_t wmi_string_to_utf8s(const struct wmi_string *str, u8 *dst, size_t length) +{ + /* Contains the maximum number of UTF16 code points to read */ + int inlen = le16_to_cpu(str->length) / 2; + int ret; + + if (length < 1) + return -EINVAL; + + /* We must leave room for the NUL character at the end of the destination buffer */ + ret = utf16s_to_utf8s((__force const wchar_t *)str->chars, inlen, UTF16_LITTLE_ENDIAN, dst, + length - 1); + if (ret < 0) + return ret; + + dst[ret] = '\0'; + + return ret; +} +EXPORT_SYMBOL_GPL(wmi_string_to_utf8s); + +/** + * wmi_string_from_utf8s - Convert a UTF8 string into a WMI string. + * @str: WMI string representation + * @max_chars: Maximum number of UTF16 code points to store inside the WMI string + * @src: UTF8 string to convert + * @src_length: Length of the source string without any trailing NUL-characters + * + * Convert a UTF8 string into a WMI string. The conversion will stop when the WMI string is + * full. The resulting WMI string will always be NUL-terminated and have its length field set + * to and appropriate value when this function returns successfully. + * + * Return: Number of UTF16 code points inside the WMI string or negative errno code on failure. + */ +ssize_t wmi_string_from_utf8s(struct wmi_string *str, size_t max_chars, const u8 *src, + size_t src_length) +{ + size_t str_length; + int ret; + + if (max_chars < 1) + return -EINVAL; + + /* We must leave room for the NUL character at the end of the WMI string */ + ret = utf8s_to_utf16s(src, src_length, UTF16_LITTLE_ENDIAN, (__force wchar_t *)str->chars, + max_chars - 1); + if (ret < 0) + return ret; + + str_length = (ret + 1) * sizeof(u16); + if (str_length > U16_MAX) + return -EOVERFLOW; + + str->length = cpu_to_le16(str_length); + str->chars[ret] = '\0'; + + return ret; +} +EXPORT_SYMBOL_GPL(wmi_string_from_utf8s); diff --git a/drivers/platform/wmi/tests/Kconfig b/drivers/platform/wmi/tests/Kconfig new file mode 100644 index 000000000..f7f0f3c54 --- /dev/null +++ b/drivers/platform/wmi/tests/Kconfig @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# ACPI WMI KUnit tests +# + +config ACPI_WMI_MARSHALLING_KUNIT_TEST + tristate "KUnit Test for ACPI-WMI marshalling" if !KUNIT_ALL_TESTS + depends on KUNIT + default KUNIT_ALL_TESTS + help + This builds unit tests for the ACPI-WMI marshalling code. + + For more information on KUnit and unit tests in general, please refer + to the KUnit documentation in Documentation/dev-tools/kunit/. + + If unsure, say N. + +config ACPI_WMI_STRING_KUNIT_TEST + tristate "KUnit Test for ACPI-WMI string conversion" if !KUNIT_ALL_TESTS + depends on KUNIT + default KUNIT_ALL_TESTS + help + This builds unit tests for the ACPI-WMI string conversion code. + For more information on KUnit and unit tests in general, please refer + to the KUnit documentation in Documentation/dev-tools/kunit/. + + If unsure, say N. diff --git a/drivers/platform/wmi/tests/Makefile b/drivers/platform/wmi/tests/Makefile new file mode 100644 index 000000000..62c438e26 --- /dev/null +++ b/drivers/platform/wmi/tests/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Makefile for linux/drivers/platform/x86/wmi/tests +# ACPI WMI KUnit tests +# + +wmi_marshalling_kunit-y := marshalling_kunit.o +obj-$(CONFIG_ACPI_WMI_MARSHALLING_KUNIT_TEST) += wmi_marshalling_kunit.o + +wmi_string_kunit-y := string_kunit.o +obj-$(CONFIG_ACPI_WMI_STRING_KUNIT_TEST) += wmi_string_kunit.o diff --git a/drivers/platform/wmi/tests/marshalling_kunit.c b/drivers/platform/wmi/tests/marshalling_kunit.c new file mode 100644 index 000000000..471963076 --- /dev/null +++ b/drivers/platform/wmi/tests/marshalling_kunit.c @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * KUnit test for the ACPI-WMI marshalling code. + * + * Copyright (C) 2025 Armin Wolf + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../internal.h" + +struct wmi_acpi_param { + const char *name; + const union acpi_object obj; + const struct wmi_buffer buffer; +}; + +struct wmi_string_param { + const char *name; + const char *string; + const struct wmi_buffer buffer; +}; + +struct wmi_invalid_acpi_param { + const char *name; + const union acpi_object obj; +}; + +struct wmi_invalid_string_param { + const char *name; + const struct wmi_buffer buffer; +}; + +/* 0xdeadbeef */ +static u8 expected_single_integer[] = { + 0xef, 0xbe, 0xad, 0xde, +}; + +/* "TEST" */ +static u8 expected_single_string[] = { + 0x0a, 0x00, 0x54, 0x00, 0x45, 0x00, 0x53, 0x00, 0x54, 0x00, 0x00, 0x00, +}; + +static u8 test_buffer[] = { + 0xab, 0xcd, +}; + +static u8 expected_single_buffer[] = { + 0xab, 0xcd, +}; + +static union acpi_object simple_package_elements[] = { + { + .buffer = { + .type = ACPI_TYPE_BUFFER, + .length = sizeof(test_buffer), + .pointer = test_buffer, + }, + }, + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = 0x01020304, + }, + }, +}; + +static u8 expected_simple_package[] = { + 0xab, 0xcd, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, +}; + +static u8 test_small_buffer[] = { + 0xde, +}; + +static union acpi_object complex_package_elements[] = { + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = 0xdeadbeef, + }, + }, + { + .buffer = { + .type = ACPI_TYPE_BUFFER, + .length = sizeof(test_small_buffer), + .pointer = test_small_buffer, + }, + }, + { + .string = { + .type = ACPI_TYPE_STRING, + .length = sizeof("TEST") - 1, + .pointer = "TEST", + }, + }, + { + .buffer = { + .type = ACPI_TYPE_BUFFER, + .length = sizeof(test_small_buffer), + .pointer = test_small_buffer, + }, + }, + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = 0x01020304, + }, + } +}; + +static u8 expected_complex_package[] = { + 0xef, 0xbe, 0xad, 0xde, + 0xde, + 0x00, + 0x0a, 0x00, 0x54, 0x00, 0x45, 0x00, 0x53, 0x00, 0x54, 0x00, 0x00, 0x00, + 0xde, + 0x00, + 0x04, 0x03, 0x02, 0x01, +}; + +static const struct wmi_acpi_param wmi_acpi_params_array[] = { + { + .name = "single_integer", + .obj = { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = 0xdeadbeef, + }, + }, + .buffer = { + .data = expected_single_integer, + .length = sizeof(expected_single_integer), + }, + }, + { + .name = "single_string", + .obj = { + .string = { + .type = ACPI_TYPE_STRING, + .length = sizeof("TEST") - 1, + .pointer = "TEST", + }, + }, + .buffer = { + .data = expected_single_string, + .length = sizeof(expected_single_string), + }, + }, + { + .name = "single_buffer", + .obj = { + .buffer = { + .type = ACPI_TYPE_BUFFER, + .length = sizeof(test_buffer), + .pointer = test_buffer, + }, + }, + .buffer = { + .data = expected_single_buffer, + .length = sizeof(expected_single_buffer), + }, + }, + { + .name = "simple_package", + .obj = { + .package = { + .type = ACPI_TYPE_PACKAGE, + .count = ARRAY_SIZE(simple_package_elements), + .elements = simple_package_elements, + }, + }, + .buffer = { + .data = expected_simple_package, + .length = sizeof(expected_simple_package), + }, + }, + { + .name = "complex_package", + .obj = { + .package = { + .type = ACPI_TYPE_PACKAGE, + .count = ARRAY_SIZE(complex_package_elements), + .elements = complex_package_elements, + }, + }, + .buffer = { + .data = expected_complex_package, + .length = sizeof(expected_complex_package), + }, + }, +}; + +static void wmi_acpi_param_get_desc(const struct wmi_acpi_param *param, char *desc) +{ + strscpy(desc, param->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(wmi_unmarshal_acpi_object, wmi_acpi_params_array, wmi_acpi_param_get_desc); + +/* "WMI\0" */ +static u8 padded_wmi_string[] = { + 0x0a, 0x00, + 0x57, 0x00, + 0x4D, 0x00, + 0x49, 0x00, + 0x00, 0x00, + 0x00, 0x00, +}; + +static const struct wmi_string_param wmi_string_params_array[] = { + { + .name = "test", + .string = "TEST", + .buffer = { + .length = sizeof(expected_single_string), + .data = expected_single_string, + }, + }, + { + .name = "padded", + .string = "WMI", + .buffer = { + .length = sizeof(padded_wmi_string), + .data = padded_wmi_string, + }, + }, +}; + +static void wmi_string_param_get_desc(const struct wmi_string_param *param, char *desc) +{ + strscpy(desc, param->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(wmi_marshal_string, wmi_string_params_array, wmi_string_param_get_desc); + +static union acpi_object nested_package_elements[] = { + { + .package = { + .type = ACPI_TYPE_PACKAGE, + .count = ARRAY_SIZE(simple_package_elements), + .elements = simple_package_elements, + }, + } +}; + +static const struct wmi_invalid_acpi_param wmi_invalid_acpi_params_array[] = { + { + .name = "nested_package", + .obj = { + .package = { + .type = ACPI_TYPE_PACKAGE, + .count = ARRAY_SIZE(nested_package_elements), + .elements = nested_package_elements, + }, + }, + }, + { + .name = "reference", + .obj = { + .reference = { + .type = ACPI_TYPE_LOCAL_REFERENCE, + .actual_type = ACPI_TYPE_ANY, + .handle = NULL, + }, + }, + }, + { + .name = "processor", + .obj = { + .processor = { + .type = ACPI_TYPE_PROCESSOR, + .proc_id = 0, + .pblk_address = 0, + .pblk_length = 0, + }, + }, + }, + { + .name = "power_resource", + .obj = { + .power_resource = { + .type = ACPI_TYPE_POWER, + .system_level = 0, + .resource_order = 0, + }, + }, + }, +}; + +static void wmi_invalid_acpi_param_get_desc(const struct wmi_invalid_acpi_param *param, char *desc) +{ + strscpy(desc, param->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(wmi_unmarshal_acpi_object_failure, wmi_invalid_acpi_params_array, + wmi_invalid_acpi_param_get_desc); + +static u8 oversized_wmi_string[] = { + 0x04, 0x00, 0x00, 0x00, +}; + +/* + * The error is that 3 bytes can not hold UTF-16 characters + * without cutting of the last one. + */ +static u8 undersized_wmi_string[] = { + 0x03, 0x00, 0x00, 0x00, 0x00, +}; + +static u8 non_ascii_wmi_string[] = { + 0x04, 0x00, 0xC4, 0x00, 0x00, 0x00, +}; + +static const struct wmi_invalid_string_param wmi_invalid_string_params_array[] = { + { + .name = "empty_buffer", + .buffer = { + .length = 0, + .data = ZERO_SIZE_PTR, + }, + + }, + { + .name = "oversized", + .buffer = { + .length = sizeof(oversized_wmi_string), + .data = oversized_wmi_string, + }, + }, + { + .name = "undersized", + .buffer = { + .length = sizeof(undersized_wmi_string), + .data = undersized_wmi_string, + }, + }, + { + .name = "non_ascii", + .buffer = { + .length = sizeof(non_ascii_wmi_string), + .data = non_ascii_wmi_string, + }, + }, +}; + +static void wmi_invalid_string_param_get_desc(const struct wmi_invalid_string_param *param, + char *desc) +{ + strscpy(desc, param->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(wmi_marshal_string_failure, wmi_invalid_string_params_array, + wmi_invalid_string_param_get_desc); + +KUNIT_DEFINE_ACTION_WRAPPER(kfree_wrapper, kfree, const void *); + +static void wmi_unmarshal_acpi_object_test(struct kunit *test) +{ + const struct wmi_acpi_param *param = test->param_value; + struct wmi_buffer result; + int ret; + + ret = wmi_unmarshal_acpi_object(¶m->obj, &result, param->buffer.length); + if (ret < 0) + KUNIT_FAIL_AND_ABORT(test, "Unmarshalling of ACPI object failed\n"); + + kunit_add_action(test, kfree_wrapper, result.data); + + KUNIT_EXPECT_TRUE(test, IS_ALIGNED((uintptr_t)result.data, 8)); + KUNIT_EXPECT_EQ(test, result.length, param->buffer.length); + KUNIT_EXPECT_MEMEQ(test, result.data, param->buffer.data, result.length); +} + +static void wmi_unmarshal_acpi_object_failure_test(struct kunit *test) +{ + const struct wmi_invalid_acpi_param *param = test->param_value; + struct wmi_buffer result; + int ret; + + ret = wmi_unmarshal_acpi_object(¶m->obj, &result, 0); + if (ret < 0) + return; + + kfree(result.data); + KUNIT_FAIL(test, "Invalid ACPI object was not rejected\n"); +} + +static void wmi_marshal_string_test(struct kunit *test) +{ + const struct wmi_string_param *param = test->param_value; + struct acpi_buffer result; + int ret; + + ret = wmi_marshal_string(¶m->buffer, &result); + if (ret < 0) + KUNIT_FAIL_AND_ABORT(test, "Marshalling of WMI string failed\n"); + + kunit_add_action(test, kfree_wrapper, result.pointer); + + KUNIT_EXPECT_EQ(test, result.length, strlen(param->string)); + KUNIT_EXPECT_STREQ(test, result.pointer, param->string); +} + +static void wmi_marshal_string_failure_test(struct kunit *test) +{ + const struct wmi_invalid_string_param *param = test->param_value; + struct acpi_buffer result; + int ret; + + ret = wmi_marshal_string(¶m->buffer, &result); + if (ret < 0) + return; + + kfree(result.pointer); + KUNIT_FAIL(test, "Invalid string was not rejected\n"); +} + +static void wmi_unmarshal_acpi_object_undersized_test(struct kunit *test) +{ + const union acpi_object obj = { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = 0xdeadbeef, + }, + }; + struct wmi_buffer result; + int ret; + + ret = wmi_unmarshal_acpi_object(&obj, &result, sizeof(expected_single_integer) + 1); + if (ret < 0) + return; + + kfree(result.data); + KUNIT_FAIL(test, "Undersized unmarshalling result was not rejected\n"); +} + +static struct kunit_case wmi_marshalling_test_cases[] = { + KUNIT_CASE_PARAM(wmi_unmarshal_acpi_object_test, + wmi_unmarshal_acpi_object_gen_params), + KUNIT_CASE_PARAM(wmi_marshal_string_test, + wmi_marshal_string_gen_params), + KUNIT_CASE_PARAM(wmi_unmarshal_acpi_object_failure_test, + wmi_unmarshal_acpi_object_failure_gen_params), + KUNIT_CASE_PARAM(wmi_marshal_string_failure_test, + wmi_marshal_string_failure_gen_params), + KUNIT_CASE(wmi_unmarshal_acpi_object_undersized_test), + {} +}; + +static struct kunit_suite wmi_marshalling_test_suite = { + .name = "wmi_marshalling", + .test_cases = wmi_marshalling_test_cases, +}; + +kunit_test_suite(wmi_marshalling_test_suite); + +MODULE_AUTHOR("Armin Wolf "); +MODULE_DESCRIPTION("KUnit test for the ACPI-WMI marshalling code"); +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/wmi/tests/string_kunit.c b/drivers/platform/wmi/tests/string_kunit.c new file mode 100644 index 000000000..117f32ee2 --- /dev/null +++ b/drivers/platform/wmi/tests/string_kunit.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * KUnit test for the ACPI-WMI string conversion code. + * + * Copyright (C) 2025 Armin Wolf + */ + +#include +#include +#include +#include + +#include +#include + +#include + +struct wmi_string_param { + const char *name; + const struct wmi_string *wmi_string; + /* + * Remember that using sizeof() on a struct wmi_string will + * always return a size of two bytes due to the flexible + * array member! + */ + size_t wmi_string_length; + const u8 *utf8_string; + size_t utf8_string_length; +}; + +#define TEST_WMI_STRING_LENGTH 12 + +static const struct wmi_string test_wmi_string = { + .length = cpu_to_le16(10), + .chars = { + cpu_to_le16(u'T'), + cpu_to_le16(u'E'), + cpu_to_le16(u'S'), + cpu_to_le16(u'T'), + cpu_to_le16(u'\0'), + }, +}; + +static const u8 test_utf8_string[] = "TEST"; + +#define SPECIAL_WMI_STRING_LENGTH 14 + +static const struct wmi_string special_wmi_string = { + .length = cpu_to_le16(12), + .chars = { + cpu_to_le16(u'Ä'), + cpu_to_le16(u'Ö'), + cpu_to_le16(u'Ü'), + cpu_to_le16(u'ß'), + cpu_to_le16(u'€'), + cpu_to_le16(u'\0'), + }, +}; + +static const u8 special_utf8_string[] = "ÄÖÜ߀"; + +#define MULTI_POINT_WMI_STRING_LENGTH 12 + +static const struct wmi_string multi_point_wmi_string = { + .length = cpu_to_le16(10), + .chars = { + cpu_to_le16(u'K'), + /* 🐧 */ + cpu_to_le16(0xD83D), + cpu_to_le16(0xDC27), + cpu_to_le16(u'!'), + cpu_to_le16(u'\0'), + }, +}; + +static const u8 multi_point_utf8_string[] = "K🐧!"; + +#define PADDED_TEST_WMI_STRING_LENGTH 14 + +static const struct wmi_string padded_test_wmi_string = { + .length = cpu_to_le16(12), + .chars = { + cpu_to_le16(u'T'), + cpu_to_le16(u'E'), + cpu_to_le16(u'S'), + cpu_to_le16(u'T'), + cpu_to_le16(u'\0'), + cpu_to_le16(u'\0'), + }, +}; + +static const u8 padded_test_utf8_string[] = "TEST\0"; + +#define OVERSIZED_TEST_WMI_STRING_LENGTH 14 + +static const struct wmi_string oversized_test_wmi_string = { + .length = cpu_to_le16(8), + .chars = { + cpu_to_le16(u'T'), + cpu_to_le16(u'E'), + cpu_to_le16(u'S'), + cpu_to_le16(u'T'), + cpu_to_le16(u'!'), + cpu_to_le16(u'\0'), + }, +}; + +static const u8 oversized_test_utf8_string[] = "TEST!"; + +#define INVALID_TEST_WMI_STRING_LENGTH 14 + +static const struct wmi_string invalid_test_wmi_string = { + .length = cpu_to_le16(12), + .chars = { + cpu_to_le16(u'T'), + /* 🐧, with low surrogate missing */ + cpu_to_le16(0xD83D), + cpu_to_le16(u'E'), + cpu_to_le16(u'S'), + cpu_to_le16(u'T'), + cpu_to_le16(u'\0'), + }, +}; + +/* We have to split the string here to end the hex escape sequence */ +static const u8 invalid_test_utf8_string[] = "T" "\xF0\x9F" "EST"; + +static const struct wmi_string_param wmi_string_params_array[] = { + { + .name = "ascii_string", + .wmi_string = &test_wmi_string, + .wmi_string_length = TEST_WMI_STRING_LENGTH, + .utf8_string = test_utf8_string, + .utf8_string_length = sizeof(test_utf8_string), + }, + { + .name = "special_string", + .wmi_string = &special_wmi_string, + .wmi_string_length = SPECIAL_WMI_STRING_LENGTH, + .utf8_string = special_utf8_string, + .utf8_string_length = sizeof(special_utf8_string), + }, + { + .name = "multi_point_string", + .wmi_string = &multi_point_wmi_string, + .wmi_string_length = MULTI_POINT_WMI_STRING_LENGTH, + .utf8_string = multi_point_utf8_string, + .utf8_string_length = sizeof(multi_point_utf8_string), + }, +}; + +static void wmi_string_param_get_desc(const struct wmi_string_param *param, char *desc) +{ + strscpy(desc, param->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(wmi_string, wmi_string_params_array, wmi_string_param_get_desc); + +static void wmi_string_to_utf8s_test(struct kunit *test) +{ + const struct wmi_string_param *param = test->param_value; + ssize_t ret; + u8 *result; + + result = kunit_kzalloc(test, param->utf8_string_length, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, result); + + ret = wmi_string_to_utf8s(param->wmi_string, result, param->utf8_string_length); + + KUNIT_EXPECT_EQ(test, ret, param->utf8_string_length - 1); + KUNIT_EXPECT_MEMEQ(test, result, param->utf8_string, param->utf8_string_length); +} + +static void wmi_string_from_utf8s_test(struct kunit *test) +{ + const struct wmi_string_param *param = test->param_value; + struct wmi_string *result; + size_t max_chars; + ssize_t ret; + + max_chars = (param->wmi_string_length - sizeof(*result)) / 2; + result = kunit_kzalloc(test, param->wmi_string_length, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, result); + + ret = wmi_string_from_utf8s(result, max_chars, param->utf8_string, + param->utf8_string_length); + + KUNIT_EXPECT_EQ(test, ret, max_chars - 1); + KUNIT_EXPECT_MEMEQ(test, result, param->wmi_string, param->wmi_string_length); +} + +static void wmi_string_to_utf8s_padded_test(struct kunit *test) +{ + u8 result[sizeof(padded_test_utf8_string)]; + ssize_t ret; + + ret = wmi_string_to_utf8s(&padded_test_wmi_string, result, sizeof(result)); + + KUNIT_EXPECT_EQ(test, ret, sizeof(test_utf8_string) - 1); + KUNIT_EXPECT_MEMEQ(test, result, test_utf8_string, sizeof(test_utf8_string)); +} + +static void wmi_string_from_utf8s_padded_test(struct kunit *test) +{ + struct wmi_string *result; + size_t max_chars; + ssize_t ret; + + max_chars = (PADDED_TEST_WMI_STRING_LENGTH - sizeof(*result)) / 2; + result = kunit_kzalloc(test, PADDED_TEST_WMI_STRING_LENGTH, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, result); + + ret = wmi_string_from_utf8s(result, max_chars, padded_test_utf8_string, + sizeof(padded_test_utf8_string)); + + KUNIT_EXPECT_EQ(test, ret, sizeof(test_utf8_string) - 1); + KUNIT_EXPECT_MEMEQ(test, result, &test_wmi_string, sizeof(test_wmi_string)); +} + +static void wmi_string_to_utf8s_oversized_test(struct kunit *test) +{ + u8 result[sizeof(oversized_test_utf8_string)]; + ssize_t ret; + + ret = wmi_string_to_utf8s(&oversized_test_wmi_string, result, sizeof(result)); + + KUNIT_EXPECT_EQ(test, ret, sizeof(test_utf8_string) - 1); + KUNIT_EXPECT_MEMEQ(test, result, test_utf8_string, sizeof(test_utf8_string)); +} + +static void wmi_string_from_utf8s_oversized_test(struct kunit *test) +{ + struct wmi_string *result; + size_t max_chars; + ssize_t ret; + + max_chars = (TEST_WMI_STRING_LENGTH - sizeof(*result)) / 2; + result = kunit_kzalloc(test, TEST_WMI_STRING_LENGTH, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, result); + + ret = wmi_string_from_utf8s(result, max_chars, oversized_test_utf8_string, + sizeof(oversized_test_utf8_string)); + + KUNIT_EXPECT_EQ(test, ret, sizeof(test_utf8_string) - 1); + KUNIT_EXPECT_MEMEQ(test, result, &test_wmi_string, sizeof(test_wmi_string)); +} + +static void wmi_string_to_utf8s_invalid_test(struct kunit *test) +{ + u8 result[sizeof(invalid_test_utf8_string)]; + ssize_t ret; + + ret = wmi_string_to_utf8s(&invalid_test_wmi_string, result, sizeof(result)); + + KUNIT_EXPECT_EQ(test, ret, sizeof(test_utf8_string) - 1); + KUNIT_EXPECT_MEMEQ(test, result, test_utf8_string, sizeof(test_utf8_string)); +} + +static void wmi_string_from_utf8s_invalid_test(struct kunit *test) +{ + struct wmi_string *result; + size_t max_chars; + ssize_t ret; + + max_chars = (INVALID_TEST_WMI_STRING_LENGTH - sizeof(*result)) / 2; + result = kunit_kzalloc(test, INVALID_TEST_WMI_STRING_LENGTH, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, result); + + ret = wmi_string_from_utf8s(result, max_chars, invalid_test_utf8_string, + sizeof(invalid_test_utf8_string)); + + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +static struct kunit_case wmi_string_test_cases[] = { + KUNIT_CASE_PARAM(wmi_string_to_utf8s_test, wmi_string_gen_params), + KUNIT_CASE_PARAM(wmi_string_from_utf8s_test, wmi_string_gen_params), + KUNIT_CASE(wmi_string_to_utf8s_padded_test), + KUNIT_CASE(wmi_string_from_utf8s_padded_test), + KUNIT_CASE(wmi_string_to_utf8s_oversized_test), + KUNIT_CASE(wmi_string_from_utf8s_oversized_test), + KUNIT_CASE(wmi_string_to_utf8s_invalid_test), + KUNIT_CASE(wmi_string_from_utf8s_invalid_test), + {} +}; + +static struct kunit_suite wmi_string_test_suite = { + .name = "wmi_string", + .test_cases = wmi_string_test_cases, +}; + +kunit_test_suite(wmi_string_test_suite); + +MODULE_AUTHOR("Armin Wolf "); +MODULE_DESCRIPTION("KUnit test for the ACPI-WMI string conversion code"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig new file mode 100644 index 000000000..b54b5212b --- /dev/null +++ b/drivers/platform/x86/Kconfig @@ -0,0 +1,1078 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# X86 Platform Specific Drivers +# + +menuconfig X86_PLATFORM_DEVICES + bool "X86 Platform Specific Device Drivers" + default y + depends on X86 + help + Say Y here to get to see options for device drivers for various + x86 platforms, including vendor-specific laptop extension drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if X86_PLATFORM_DEVICES + +config WMI_BMOF + tristate "WMI embedded Binary MOF driver" + depends on ACPI_WMI + default ACPI_WMI + help + Say Y here if you want to be able to read a firmware-embedded + WMI Binary MOF (Managed Object Format) data. Using this requires + userspace tools and may be rather tedious. + + To compile this driver as a module, choose M here: the module will + be called wmi-bmof. + +config HUAWEI_WMI + tristate "Huawei WMI laptop extras driver" + depends on ACPI_BATTERY + depends on ACPI_EC + depends on ACPI_WMI + depends on INPUT + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + help + This driver provides support for Huawei WMI hotkeys, battery charge + control, fn-lock, mic-mute LED, and other extra features. + + To compile this driver as a module, choose M here: the module + will be called huawei-wmi. + +source "drivers/platform/x86/uniwill/Kconfig" + +config UV_SYSFS + tristate "Sysfs structure for UV systems" + depends on X86_UV + depends on SYSFS + help + This driver supports a sysfs tree describing information about + UV systems at /sys/firmware/sgi_uv/. + + To compile this driver as a module, choose M here: the module will + be called uv_sysfs. + +config MXM_WMI + tristate "WMI support for MXM Laptop Graphics" + depends on ACPI_WMI + help + MXM is a standard for laptop graphics cards, the WMI interface + is required for switchable nvidia graphics machines + +config NVIDIA_WMI_EC_BACKLIGHT + tristate "EC Backlight Driver for Hybrid Graphics Notebook Systems" + depends on ACPI_VIDEO + depends on ACPI_WMI + depends on BACKLIGHT_CLASS_DEVICE + help + This driver provides a sysfs backlight interface for notebook systems + which are equipped with NVIDIA hybrid graphics and drive LCD backlight + levels through the Embedded Controller (EC). + + Say Y or M here if you want to control the backlight on a notebook + system with an EC-driven backlight. + + If you choose to compile this driver as a module the module will be + called nvidia-wmi-ec-backlight. + +config XIAOMI_WMI + tristate "Xiaomi WMI key driver" + depends on ACPI_WMI + depends on INPUT + help + Say Y here if you want to support WMI-based keys on Xiaomi notebooks. + + To compile this driver as a module, choose M here: the module will + be called xiaomi-wmi. + +config REDMI_WMI + tristate "Redmibook WMI key driver" + depends on ACPI_WMI + depends on INPUT + select INPUT_SPARSEKMAP + help + Say Y here if you want support for WMI-based hotkey events on + Xiaomi Redmibook devices. + + To compile this driver as a module, choose M here: the module will + be called redmi-wmi. + +config GIGABYTE_WMI + tristate "Gigabyte WMI temperature driver" + depends on ACPI_WMI + depends on HWMON + help + Say Y here if you want to support WMI-based temperature reporting on + Gigabyte mainboards. + + To compile this driver as a module, choose M here: the module will + be called gigabyte-wmi. + +config BITLAND_MIFS_WMI + tristate "Bitland MIFS (MiInterface) WMI driver" + depends on ACPI_WMI + depends on HWMON + depends on INPUT + depends on LEDS_CLASS + depends on POWER_SUPPLY + select ACPI_PLATFORM_PROFILE + select INPUT_SPARSEKMAP + help + This is a driver for Bitland MiInterface based laptops. + + It provides the access to the temperature, fan speed, gpu + control, keyboard backlight brightness and platform profile + via hwmon and sysfs. + + To compile this driver as a module, choose M here: the module will + be called bitland-mifs-wmi. + +config ACERHDF + tristate "Acer Aspire One temperature and fan driver" + depends on ACPI_EC && THERMAL + select THERMAL_GOV_BANG_BANG + help + This is a driver for Acer Aspire One netbooks. It allows to access + the temperature sensor and to control the fan. + + After loading this driver the BIOS is still in control of the fan. + To let the kernel handle the fan, do: + echo -n enabled > /sys/class/thermal/thermal_zoneN/mode + where N=0,1,2... depending on the number of thermal nodes and the + detection order of your particular system. The "type" parameter + in the same node directory will tell you if it is "acerhdf". + + For more information about this driver see + + + If you have an Acer Aspire One netbook, say Y or M + here. + +config ACER_WIRELESS + tristate "Acer Wireless Radio Control Driver" + depends on ACPI + depends on INPUT + help + The Acer Wireless Radio Control handles the airplane mode hotkey + present on new Acer laptops. + + Say Y or M here if you have an Acer notebook with an airplane mode + hotkey. + + If you choose to compile this driver as a module the module will be + called acer-wireless. + +config ACER_WMI + tristate "Acer WMI Laptop Extras" + depends on BACKLIGHT_CLASS_DEVICE + depends on SERIO_I8042 + depends on INPUT + depends on RFKILL || RFKILL = n + depends on ACPI_EC + depends on ACPI_WMI + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on HWMON + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + select ACPI_PLATFORM_PROFILE + help + This is a driver for newer Acer (and Wistron) laptops. It adds + wireless radio and bluetooth control, and on some laptops, + exposes the mail LED and LCD backlight. + + If you have an ACPI-WMI compatible Acer/ Wistron laptop, say Y or M + here. + +source "drivers/platform/x86/amd/Kconfig" + +config ADV_SWBUTTON + tristate "Advantech ACPI Software Button Driver" + depends on ACPI && INPUT + help + Say Y here to enable support for Advantech software defined + button feature. More information can be found at + + + To compile this driver as a module, choose M here. The module will + be called adv_swbutton. + +config APPLE_GMUX + tristate "Apple Gmux Driver" + depends on ACPI && PCI + depends on PNP + depends on BACKLIGHT_CLASS_DEVICE + help + This driver provides support for the gmux device found on many + Apple laptops, which controls the display mux for the hybrid + graphics as well as the backlight. Currently only backlight + control is supported by the driver. + +config ASUS_LAPTOP + tristate "Asus Laptop Extras" + depends on ACPI + select LEDS_CLASS + select NEW_LEDS + depends on BACKLIGHT_CLASS_DEVICE + depends on INPUT + depends on RFKILL || RFKILL = n + depends on ACPI_VIDEO || ACPI_VIDEO = n + select INPUT_SPARSEKMAP + help + This is a driver for Asus laptops, Lenovo SL and the Pegatron + Lucid tablet. It may also support some MEDION, JVC or VICTOR + laptops. It makes all the extra buttons generate standard + ACPI events and input events, and on the Lucid the built-in + accelerometer appears as an input device. It also adds + support for video output switching, LCD backlight control, + Bluetooth and Wlan control, and most importantly, allows you + to blink those fancy LEDs. + + For more information see . + + If you have an ACPI-compatible ASUS laptop, say Y or M here. + +config ASUS_WIRELESS + tristate "Asus Wireless Radio Control Driver" + depends on ACPI + depends on INPUT + select NEW_LEDS + select LEDS_CLASS + help + The Asus Wireless Radio Control handles the airplane mode hotkey + present on some Asus laptops. + + Say Y or M here if you have an ASUS notebook with an airplane mode + hotkey. + + If you choose to compile this driver as a module the module will be + called asus-wireless. + +config ASUS_ARMOURY + tristate "ASUS Armoury driver" + depends on ASUS_WMI + select FW_ATTR_CLASS + help + Say Y here if you have a WMI aware Asus machine and would like to use the + firmware_attributes API to control various settings typically exposed in + the ASUS Armoury Crate application available on Windows. + + To compile this driver as a module, choose M here: the module will + be called asus-armoury. + +config ASUS_WMI + tristate "ASUS WMI Driver" + depends on ACPI_WMI + depends on ACPI_BATTERY + depends on INPUT + depends on HWMON + depends on BACKLIGHT_CLASS_DEVICE + depends on RFKILL || RFKILL = n + depends on HOTPLUG_PCI + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on SERIO_I8042 || SERIO_I8042 = n + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + select ACPI_PLATFORM_PROFILE + help + Say Y here if you have a WMI aware Asus laptop (like Eee PCs or new + Asus Notebooks). + + To compile this driver as a module, choose M here: the module will + be called asus-wmi. + +config ASUS_WMI_DEPRECATED_ATTRS + bool "BIOS option support in WMI platform (DEPRECATED)" + depends on ASUS_WMI + default y + help + Say Y to expose the configurable BIOS options through the asus-wmi + driver. + + This can be used with or without the asus-armoury driver which + has the same attributes, but more, and better features. + +config ASUS_NB_WMI + tristate "Asus Notebook WMI Driver" + depends on ASUS_WMI + help + This is a driver for newer Asus notebooks. It adds extra features + like wireless radio and bluetooth control, leds, hotkeys, backlight... + + For more information, see + + + If you have an ACPI-WMI compatible Asus Notebook, say Y or M + here. + +config ASUS_TF103C_DOCK + tristate "Asus TF103C 2-in-1 keyboard dock" + depends on ACPI + depends on I2C + depends on INPUT + depends on HID + depends on GPIOLIB + help + This is a driver for the keyboard, touchpad and USB port of the + keyboard dock for the Asus TF103C 2-in-1 tablet. + + This keyboard dock has its own I2C attached embedded controller + and the keyboard and touchpad are also connected over I2C, + instead of using the usual USB connection. This means that the + keyboard dock requires this special driver to function. + + If you have an Asus TF103C tablet say Y or M here, for a generic x86 + distro config say M here. + +config AYANEO_EC + tristate "Ayaneo EC platform control" + depends on DMI + depends on ACPI_EC + depends on ACPI_BATTERY + depends on HWMON + help + Enables support for the platform EC of Ayaneo devices. This + includes fan control, fan speed, charge limit, magic + module detection, and controller power control. + + If you have an Ayaneo device, say Y or M here. + +config MERAKI_MX100 + tristate "Cisco Meraki MX100 Platform Driver" + depends on GPIOLIB + depends on LPC_ICH + depends on GPIO_ICH + depends on LEDS_CLASS + select LEDS_GPIO + help + This driver provides support for the front button and LEDs on + the Cisco Meraki MX100 (Tinkerbell) 1U appliance. + + To compile this driver as a module, choose M here: the module + will be called meraki-mx100. + +config EEEPC_LAPTOP + tristate "Eee PC Hotkey Driver" + depends on ACPI_EC + depends on INPUT + depends on RFKILL || RFKILL = n + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on HOTPLUG_PCI + depends on BACKLIGHT_CLASS_DEVICE + select HWMON + select LEDS_CLASS + select NEW_LEDS + select INPUT_SPARSEKMAP + help + This driver supports the Fn-Fx keys on Eee PC laptops. + + It also gives access to some extra laptop functionalities like + Bluetooth, backlight and allows powering on/off some other + devices. + + If you have an Eee PC laptop, say Y or M here. If this driver + doesn't work on your Eee PC, try eeepc-wmi instead. + +config EEEPC_WMI + tristate "Eee PC WMI Driver" + depends on ASUS_WMI + help + This is a driver for newer Eee PC laptops. It adds extra features + like wireless radio and bluetooth control, leds, hotkeys, backlight... + + For more information, see + + + If you have an ACPI-WMI compatible Eee PC laptop (>= 1000), say Y or M + here. + +source "drivers/platform/x86/dell/Kconfig" + +config AMILO_RFKILL + tristate "Fujitsu-Siemens Amilo rfkill support" + depends on RFKILL + depends on SERIO_I8042 + help + This is a driver for enabling wifi on some Fujitsu-Siemens Amilo + laptops. + +config FUJITSU_LAPTOP + tristate "Fujitsu Laptop Extras" + depends on ACPI + depends on INPUT + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_BATTERY + depends on ACPI_VIDEO || ACPI_VIDEO = n + select INPUT_SPARSEKMAP + select NEW_LEDS + select LEDS_CLASS + help + This is a driver for laptops built by Fujitsu: + + * P2xxx/P5xxx/S6xxx/S7xxx series Lifebooks + * Possibly other Fujitsu laptop models + * Tested with S6410 and S7020 + + It adds support for LCD brightness control and some hotkeys. + + If you have a Fujitsu laptop, say Y or M here. + +config FUJITSU_TABLET + tristate "Fujitsu Tablet Extras" + depends on ACPI + depends on INPUT + help + This is a driver for tablets built by Fujitsu: + + * Lifebook P1510/P1610/P1620/Txxxx + * Stylistic ST5xxx + * Possibly other Fujitsu tablet models + + It adds support for the panel buttons, docking station detection, + tablet/notebook mode detection for convertible and + orientation detection for docked slates. + + If you have a Fujitsu convertible or slate, say Y or M here. + +config GPD_POCKET_FAN + tristate "GPD Pocket Fan Controller support" + depends on ACPI + depends on THERMAL + help + Driver for the GPD Pocket vendor specific FAN02501 ACPI device + which controls the fan speed on the GPD Pocket. + + Without this driver the fan on the Pocket will stay off independent + of the CPU temperature. Say Y or M if the kernel may be used on a + GPD pocket. + +source "drivers/platform/x86/hp/Kconfig" + +config WIRELESS_HOTKEY + tristate "Wireless hotkey button" + depends on ACPI + depends on INPUT + help + This driver provides supports for the wireless buttons found on some AMD, + HP, & Xiaomi laptops. + On such systems the driver should load automatically (via ACPI alias). + + To compile this driver as a module, choose M here: the module will + be called wireless-hotkey. + +config IBM_RTL + tristate "Device driver to enable PRTL support" + depends on PCI + help + Enable support for IBM Premium Real Time Mode (PRTM). + This module will allow you the enter and exit PRTM in the BIOS via + sysfs on platforms that support this feature. System in PRTM will + not receive CPU-generated SMIs for recoverable errors. Use of this + feature without proper support may void your hardware warranty. + + If the proper BIOS support is found the driver will load and create + /sys/devices/system/ibm_rtl/. The "state" variable will indicate + whether or not the BIOS is in PRTM. + state = 0 (BIOS SMIs on) + state = 1 (BIOS SMIs off) + +config SENSORS_HDAPS + tristate "Thinkpad Hard Drive Active Protection System (hdaps)" + depends on INPUT + help + This driver provides support for the IBM Hard Drive Active Protection + System (hdaps), which provides an accelerometer and other misc. data. + ThinkPads starting with the R50, T41, and X40 are supported. The + accelerometer data is readable via sysfs. + + This driver also provides an absolute input class device, allowing + the laptop to act as a pinball machine-esque joystick. + + If your ThinkPad is not recognized by the driver, please update to latest + BIOS. This is especially the case for some R52 ThinkPads. + + Say Y here if you have an applicable laptop and want to experience + the awesome power of hdaps. + +source "drivers/platform/x86/intel/Kconfig" +source "drivers/platform/x86/lenovo/Kconfig" + +config ACPI_QUICKSTART + tristate "ACPI Quickstart button driver" + depends on ACPI + depends on INPUT + select INPUT_SPARSEKMAP + help + This driver adds support for ACPI quickstart button (PNP0C32) devices. + The button emits a manufacturer-specific key value when pressed, so + userspace has to map this value to a standard key code. + + To compile this driver as a module, choose M here: the module will be + called quickstart. + +config MEEGOPAD_ANX7428 + tristate "MeeGoPad ANX7428 Type-C Switch" + depends on ACPI && GPIOLIB && I2C + help + Some MeeGoPad top-set boxes have an ANX7428 Type-C Switch for + USB3.1 Gen 1 and DisplayPort over Type-C alternate mode support. + + This driver takes care of powering on the ANX7428 on supported + MeeGoPad top-set boxes. After this the ANX7428 takes care of Type-C + connector orientation and PD alternate mode switching autonomously. + +config MSI_EC + tristate "MSI EC Extras" + depends on ACPI_EC + depends on ACPI_BATTERY + help + This driver allows various MSI laptops' functionalities to be + controlled from userspace, including battery charge threshold. + +config MSI_LAPTOP + tristate "MSI Laptop Extras" + depends on ACPI_EC + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on RFKILL + depends on INPUT && SERIO_I8042 + select INPUT_SPARSEKMAP + help + This is a driver for laptops built by MSI (MICRO-STAR + INTERNATIONAL): + + MSI MegaBook S270 (MS-1013) + Cytron/TCM/Medion/Tchibo MD96100/SAM2000 + + It adds support for Bluetooth, WLAN and LCD brightness control. + + More information about this driver is available at + . + + If you have an MSI S270 laptop, say Y or M here. + +config MSI_WMI + tristate "MSI WMI extras" + depends on ACPI_WMI + depends on INPUT + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_VIDEO || ACPI_VIDEO = n + select INPUT_SPARSEKMAP + help + Say Y here if you want to support WMI-based hotkeys on MSI laptops. + + To compile this driver as a module, choose M here: the module will + be called msi-wmi. + +config MSI_WMI_PLATFORM + tristate "MSI WMI Platform features" + depends on ACPI_WMI + depends on DMI + depends on HWMON + help + Say Y here if you want to have support for WMI-based platform features + like fan sensor access on MSI machines. + + To compile this driver as a module, choose M here: the module will + be called msi-wmi-platform. + +config XO15_EBOOK + tristate "OLPC XO-1.5 ebook switch" + depends on OLPC || COMPILE_TEST + depends on ACPI && INPUT + help + Support for the ebook switch on the OLPC XO-1.5 laptop. + + This switch is triggered as the screen is rotated and folded down to + convert the device into ebook form. + +config XO1_RFKILL + tristate "OLPC XO-1 software RF kill switch" + depends on OLPC || COMPILE_TEST + depends on RFKILL + help + Support for enabling/disabling the WLAN interface on the OLPC XO-1 + laptop. + +config PCENGINES_APU2 + tristate "PC Engines APUv2/3 front button and LEDs driver" + depends on INPUT && INPUT_KEYBOARD && GPIOLIB + depends on LEDS_CLASS + select GPIO_AMD_FCH + select KEYBOARD_GPIO_POLLED + select LEDS_GPIO + help + This driver provides support for the front button and LEDs on + PC Engines APUv2/APUv3 board. + + To compile this driver as a module, choose M here: the module + will be called pcengines-apuv2. + +config PORTWELL_EC + tristate "Portwell Embedded Controller driver" + depends on X86 && HAS_IOPORT && WATCHDOG && GPIOLIB + select WATCHDOG_CORE + help + This driver provides support for the GPIO pins and watchdog timer + embedded in Portwell's EC. + + Theoretically, this driver should work on multiple Portwell platforms, + but it has only been tested on the Portwell NANO-6064 board. + If you encounter any issues on other boards, please report them. + + To compile this driver as a module, choose M here: the module + will be called portwell-ec. + +config BARCO_P50_GPIO + tristate "Barco P50 GPIO driver for identify LED/button" + depends on GPIOLIB + help + This driver provides access to the GPIOs for the identify button + and led present on Barco P50 board. + + To compile this driver as a module, choose M here: the module + will be called barco-p50-gpio. + +config SAMSUNG_GALAXYBOOK + tristate "Samsung Galaxy Book driver" + depends on ACPI + depends on ACPI_BATTERY + depends on INPUT + depends on LEDS_CLASS + depends on SERIO_I8042 + select ACPI_PLATFORM_PROFILE + select FW_ATTR_CLASS + help + This is a driver for Samsung Galaxy Book series notebooks. It adds + support for the keyboard backlight control, performance mode control, + function keys, and various firmware attributes. + + For more information about this driver, see + . + +config SAMSUNG_LAPTOP + tristate "Samsung Laptop driver" + depends on RFKILL || RFKILL = n + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on ACPI_BATTERY + depends on BACKLIGHT_CLASS_DEVICE + select LEDS_CLASS + select NEW_LEDS + help + This module implements a driver for a wide range of different + Samsung laptops. It offers control over the different + function keys, wireless LED, LCD backlight level. + + It may also provide some sysfs files described in + + + To compile this driver as a module, choose M here: the module + will be called samsung-laptop. + +config SAMSUNG_Q10 + tristate "Samsung Q10 Extras" + depends on ACPI_EC + select BACKLIGHT_CLASS_DEVICE + help + This driver provides support for backlight control on Samsung Q10 + and related laptops, including Dell Latitude X200. + +config ACPI_TOSHIBA + tristate "Toshiba Laptop Extras" + depends on ACPI_EC + depends on ACPI_BATTERY + depends on ACPI_WMI + select LEDS_CLASS + select NEW_LEDS + depends on BACKLIGHT_CLASS_DEVICE + depends on INPUT + depends on SERIO_I8042 || SERIO_I8042 = n + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on HWMON || HWMON = n + depends on RFKILL || RFKILL = n + depends on IIO + select INPUT_SPARSEKMAP + help + This driver adds support for access to certain system settings + on "legacy free" Toshiba laptops. These laptops can be recognized by + their lack of a BIOS setup menu and APM support. + + On these machines, all system configuration is handled through the + ACPI. This driver is required for access to controls not covered + by the general ACPI drivers, such as LCD brightness, video output, + etc. + + This driver differs from the non-ACPI Toshiba laptop driver (located + under "Processor type and features") in several aspects. + Configuration is accessed by reading and writing text files in the + /proc tree instead of by program interface to /dev. Furthermore, no + power management functions are exposed, as those are handled by the + general ACPI drivers. + + More information about this driver is available at + . + + If you have a legacy free Toshiba laptop (such as the Libretto L1 + series), say Y. + +config TOSHIBA_BT_RFKILL + tristate "Toshiba Bluetooth RFKill switch support" + depends on ACPI + depends on RFKILL || RFKILL = n + help + This driver adds support for Bluetooth events for the RFKill + switch on modern Toshiba laptops with full ACPI support and + an RFKill switch. + + This driver handles RFKill events for the TOS6205 Bluetooth, + and re-enables it when the switch is set back to the 'on' + position. + + If you have a modern Toshiba laptop with a Bluetooth and an + RFKill switch (such as the Portege R500), say Y. + +config TOSHIBA_HAPS + tristate "Toshiba HDD Active Protection Sensor" + depends on ACPI + help + This driver adds support for the built-in accelerometer + found on recent Toshiba laptops equipped with HID TOS620A + device. + + This driver receives ACPI notify events 0x80 when the sensor + detects a sudden move or a harsh vibration, as well as an + ACPI notify event 0x81 whenever the movement or vibration has + been stabilized. + + Also provides sysfs entries to get/set the desired protection + level and resetting the HDD protection interface. + + If you have a recent Toshiba laptop with a built-in accelerometer + device, say Y. + +config TOSHIBA_WMI + tristate "Toshiba WMI Hotkeys Driver (EXPERIMENTAL)" + depends on ACPI_WMI + depends on INPUT + select INPUT_SPARSEKMAP + help + This driver adds hotkey monitoring support to some Toshiba models + that manage the hotkeys via WMI events. + + WARNING: This driver is incomplete as it lacks a proper keymap and the + *notify function only prints the ACPI event type value. Be warned that + you will need to provide some information if you have a Toshiba model + with WMI event hotkeys and want to help with the development of this + driver. + + If you have a WMI-based hotkeys Toshiba laptop, say Y or M here. + +config ACPI_CMPC + tristate "CMPC Laptop Extras" + depends on ACPI && INPUT + depends on RFKILL || RFKILL=n + select BACKLIGHT_CLASS_DEVICE + help + Support for Intel Classmate PC ACPI devices, including some + keys as input device, backlight device, tablet and accelerometer + devices. + +config COMPAL_LAPTOP + tristate "Compal (and others) Laptop Extras" + depends on ACPI_EC + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on RFKILL + depends on HWMON + depends on POWER_SUPPLY + help + This is a driver for laptops built by Compal, and some models by + other brands (e.g. Dell, Toshiba). + + It adds support for rfkill, Bluetooth, WLAN, LCD brightness, hwmon + and battery charging level control. + +config LG_LAPTOP + tristate "LG Laptop Extras" + depends on ACPI + depends on ACPI_BATTERY + depends on ACPI_WMI + depends on INPUT + select INPUT_SPARSEKMAP + select NEW_LEDS + select LEDS_CLASS + help + This driver adds support for hotkeys as well as control of keyboard + backlight, battery maximum charge level and various other ACPI + features. + + If you have an LG Gram laptop, say Y or M here. + +config PANASONIC_LAPTOP + tristate "Panasonic Laptop Extras" + depends on INPUT && ACPI + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_VIDEO=n || ACPI_VIDEO + depends on SERIO_I8042 || SERIO_I8042 = n + select INPUT_SPARSEKMAP + help + This driver adds support for access to backlight control and hotkeys + on Panasonic Let's Note laptops. + + If you have a Panasonic Let's note laptop (such as the R1(N variant), + R2, R3, R5, T2, W2 and Y2 series), say Y. + +config SONY_LAPTOP + tristate "Sony Laptop Extras" + depends on ACPI_EC + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on BACKLIGHT_CLASS_DEVICE + depends on INPUT + depends on RFKILL + help + This mini-driver drives the SNC and SPIC devices present in the ACPI + BIOS of the Sony Vaio laptops. + + It gives access to some extra laptop functionalities like Bluetooth, + screen brightness control, Fn keys and allows powering on/off some + devices. + + Read for more information. + +config SONYPI_COMPAT + bool "Sonypi compatibility" + depends on SONY_LAPTOP + help + Build the sonypi driver compatibility code into the sony-laptop driver. + +config SYSTEM76_ACPI + tristate "System76 ACPI Driver" + depends on ACPI_EC + depends on ACPI_BATTERY + depends on HWMON + depends on INPUT + select NEW_LEDS + select LEDS_CLASS + select LEDS_TRIGGERS + help + This is a driver for System76 laptops running open firmware. It adds + support for Fn-Fx key combinations, keyboard backlight, and airplane mode + LEDs. + + If you have a System76 laptop running open firmware, say Y or M here. + +config TOPSTAR_LAPTOP + tristate "Topstar Laptop Extras" + depends on ACPI + depends on INPUT + select INPUT_SPARSEKMAP + select LEDS_CLASS + select NEW_LEDS + help + This driver adds support for hotkeys found on Topstar laptops. + + If you have a Topstar laptop, say Y or M here. + +config SERIAL_MULTI_INSTANTIATE + tristate "Serial bus multi instantiate pseudo device driver" + depends on ACPI + depends on (I2C && !SPI) || (!I2C && SPI) || (I2C && SPI) + help + Some ACPI-based systems list multiple devices in a single ACPI + firmware-node. This driver will instantiate separate clients + for each device in the firmware-node. + + To compile this driver as a module, choose M here: the module + will be called serial-multi-instantiate. + +config TOUCHSCREEN_DMI + bool "DMI based touchscreen configuration info" + depends on ACPI && DMI && I2C=y && TOUCHSCREEN_SILEAD + select EFI_EMBEDDED_FIRMWARE if EFI + help + Certain ACPI based tablets with e.g. Silead or Chipone touchscreens + do not have enough data in ACPI tables for the touchscreen driver to + handle the touchscreen properly, as OEMs expect the data to be baked + into the tablet model specific version of the driver shipped with the + the OS-image for the device. This option supplies the missing info. + Enable this for x86 tablets with Silead or Chipone touchscreens. + +config INSPUR_PLATFORM_PROFILE + tristate "Inspur WMI platform profile driver" + depends on ACPI_WMI + select ACPI_PLATFORM_PROFILE + help + This will allow users to determine and control the platform modes + between low-power, balanced and performance modes. + + To compile this driver as a module, choose M here: the module + will be called inspur-platform-profile. + +config DASHARO_ACPI + tristate "Dasharo ACPI Platform Driver" + depends on ACPI + depends on HWMON + help + This driver provides HWMON support for devices running Dasharo + firmware. + + If you have a device with Dasharo firmware, choose Y or M here. + +source "drivers/platform/x86/x86-android-tablets/Kconfig" + +config FW_ATTR_CLASS + tristate + +config INTEL_IMR + bool "Intel Isolated Memory Region support" + depends on X86_INTEL_QUARK && IOSF_MBI + help + This option provides a means to manipulate Isolated Memory Regions. + IMRs are a set of registers that define read and write access masks + to prohibit certain system agents from accessing memory with 1 KiB + granularity. + + IMRs make it possible to control read/write access to an address + by hardware agents inside the SoC. Read and write masks can be + defined for: + - eSRAM flush + - Dirty CPU snoop (write only) + - RMU access + - PCI Virtual Channel 0/Virtual Channel 1 + - SMM mode + - Non SMM mode + + Quark contains a set of eight IMR registers and makes use of those + registers during its bootup process. + + If you are running on a Galileo/Quark say Y here. + +config INTEL_IPS + tristate "Intel Intelligent Power Sharing" + depends on ACPI && PCI + help + Intel Calpella platforms support dynamic power sharing between the + CPU and GPU, maximizing performance in a given TDP. This driver, + along with the CPU frequency and i915 drivers, provides that + functionality. If in doubt, say Y here; it will only load on + supported platforms. + +config INTEL_SCU_IPC + bool + +config INTEL_SCU + bool + select INTEL_SCU_IPC + +config INTEL_SCU_PCI + bool "Intel SCU PCI driver" + depends on PCI + select INTEL_SCU + help + This driver is used to bridge the communications between kernel + and SCU on some embedded Intel x86 platforms. It also creates + devices that are connected to the SoC through the SCU. + Platforms supported: + Medfield + Clovertrail + Merrifield + Broxton + Apollo Lake + +config INTEL_SCU_PLATFORM + tristate "Intel SCU platform driver" + depends on ACPI + select INTEL_SCU + help + This driver is used to bridge the communications between kernel + and SCU (sometimes called PMC as well). The driver currently + supports Intel Elkhart Lake and compatible platforms. + +config INTEL_SCU_WDT + bool + default INTEL_SCU_PCI + depends on INTEL_MID_WATCHDOG + help + This is a specific platform code to instantiate watchdog device + on ACPI-based Intel MID platforms. + +config INTEL_SCU_IPC_UTIL + tristate "Intel SCU IPC utility driver" + depends on INTEL_SCU + help + The IPC Util driver provides an interface with the SCU enabling + low level access for debug work and updating the firmware. Say + N unless you will be doing this on an Intel MID platform. + +source "drivers/platform/x86/siemens/Kconfig" + +config SILICOM_PLATFORM + tristate "Silicom Edge Networking device support" + depends on HWMON + depends on GPIOLIB + depends on LEDS_CLASS_MULTICOLOR + help + This option enables support for the LEDs/GPIO/etc downstream of the + embedded controller on Silicom "Cordoba" hardware and derivatives. + + This platform driver provides support for various functions via + the Linux LED framework, GPIO framework, Hardware Monitoring (HWMON) + and device attributes. + + If you have a Silicom network appliance, say Y or M here. + +config WINMATE_FM07_KEYS + tristate "Winmate FM07/FM07P front-panel keys driver" + depends on INPUT + help + Winmate FM07 and FM07P in-vehicle computers have a row of five + buttons below the display. This module adds an input device + that delivers key events when these buttons are pressed. + +config SEL3350_PLATFORM + tristate "SEL-3350 LEDs and power supplies" + depends on ACPI + depends on GPIOLIB + depends on PINCTRL_BROXTON + select POWER_SUPPLY + select NEW_LEDS + select LEDS_CLASS + select LEDS_GPIO + help + Support for LEDs and power supplies on SEL-3350 computers. + + To compile this driver as a module, choose M here: the module + will be called sel3350-platform. + +config OXP_EC + tristate "OneXPlayer EC platform control" + depends on ACPI_EC + depends on ACPI_BATTERY + depends on HWMON + depends on X86 + help + Enables support for the platform EC of OneXPlayer and AOKZOE + handheld devices. This includes fan speed, fan controls, and + disabling the default TDP behavior of the device. + +source "drivers/platform/x86/tuxedo/Kconfig" + +endif # X86_PLATFORM_DEVICES + +config P2SB + bool + depends on PCI && X86 + help + The Primary to Sideband (P2SB) bridge is an interface to some + PCI devices connected through it. In particular, SPI NOR controller + in Intel Apollo Lake SoC is one of such devices. + + The main purpose of this library is to unhide P2SB device in case + firmware kept it hidden on some platforms in order to access devices + behind it. diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile new file mode 100644 index 000000000..872ac3842 --- /dev/null +++ b/drivers/platform/x86/Makefile @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86 +# x86 Platform-Specific Drivers +# + +# Windows Management Interface +obj-$(CONFIG_WMI_BMOF) += wmi-bmof.o + +# WMI drivers +obj-$(CONFIG_HUAWEI_WMI) += huawei-wmi.o +obj-$(CONFIG_MXM_WMI) += mxm-wmi.o +obj-$(CONFIG_NVIDIA_WMI_EC_BACKLIGHT) += nvidia-wmi-ec-backlight.o +obj-$(CONFIG_XIAOMI_WMI) += xiaomi-wmi.o +obj-$(CONFIG_REDMI_WMI) += redmi-wmi.o +obj-$(CONFIG_GIGABYTE_WMI) += gigabyte-wmi.o +obj-$(CONFIG_BITLAND_MIFS_WMI) += bitland-mifs-wmi.o + +# Acer +obj-$(CONFIG_ACERHDF) += acerhdf.o +obj-$(CONFIG_ACER_WIRELESS) += acer-wireless.o +obj-$(CONFIG_ACER_WMI) += acer-wmi.o + +# AMD +obj-y += amd/ + +# Advantech +obj-$(CONFIG_ADV_SWBUTTON) += adv_swbutton.o + +# Apple +obj-$(CONFIG_APPLE_GMUX) += apple-gmux.o + +# ASUS +obj-$(CONFIG_ASUS_LAPTOP) += asus-laptop.o +obj-$(CONFIG_ASUS_WIRELESS) += asus-wireless.o +obj-$(CONFIG_ASUS_ARMOURY) += asus-armoury.o +obj-$(CONFIG_ASUS_WMI) += asus-wmi.o +obj-$(CONFIG_ASUS_NB_WMI) += asus-nb-wmi.o +obj-$(CONFIG_ASUS_TF103C_DOCK) += asus-tf103c-dock.o +obj-$(CONFIG_EEEPC_LAPTOP) += eeepc-laptop.o +obj-$(CONFIG_EEEPC_WMI) += eeepc-wmi.o + +# Ayaneo +obj-$(CONFIG_AYANEO_EC) += ayaneo-ec.o + +# Cisco/Meraki +obj-$(CONFIG_MERAKI_MX100) += meraki-mx100.o + +# Dell +obj-$(CONFIG_X86_PLATFORM_DRIVERS_DELL) += dell/ + +# Fujitsu +obj-$(CONFIG_AMILO_RFKILL) += amilo-rfkill.o +obj-$(CONFIG_FUJITSU_LAPTOP) += fujitsu-laptop.o +obj-$(CONFIG_FUJITSU_TABLET) += fujitsu-tablet.o + +# GPD +obj-$(CONFIG_GPD_POCKET_FAN) += gpd-pocket-fan.o + +# Hewlett Packard +obj-$(CONFIG_X86_PLATFORM_DRIVERS_HP) += hp/ + +# Hewlett Packard Enterprise +obj-$(CONFIG_UV_SYSFS) += uv_sysfs.o + +obj-$(CONFIG_FW_ATTR_CLASS) += firmware_attributes_class.o + +# IBM Thinkpad (before 2005) +obj-$(CONFIG_IBM_RTL) += ibm_rtl.o +obj-$(CONFIG_SENSORS_HDAPS) += hdaps.o + +# Lenovo +obj-y += lenovo/ + +# Intel +obj-y += intel/ + +# Microsoft +obj-$(CONFIG_ACPI_QUICKSTART) += quickstart.o + +# MeeGoPad +obj-$(CONFIG_MEEGOPAD_ANX7428) += meegopad_anx7428.o + +# MSI +obj-$(CONFIG_MSI_EC) += msi-ec.o +obj-$(CONFIG_MSI_LAPTOP) += msi-laptop.o +obj-$(CONFIG_MSI_WMI) += msi-wmi.o +obj-$(CONFIG_MSI_WMI_PLATFORM) += msi-wmi-platform.o + +# OLPC +obj-$(CONFIG_XO15_EBOOK) += xo15-ebook.o +obj-$(CONFIG_XO1_RFKILL) += xo1-rfkill.o + +# PC Engines +obj-$(CONFIG_PCENGINES_APU2) += pcengines-apuv2.o + +# Portwell +obj-$(CONFIG_PORTWELL_EC) += portwell-ec.o + +# Barco +obj-$(CONFIG_BARCO_P50_GPIO) += barco-p50-gpio.o + +# Samsung +obj-$(CONFIG_SAMSUNG_GALAXYBOOK) += samsung-galaxybook.o +obj-$(CONFIG_SAMSUNG_LAPTOP) += samsung-laptop.o +obj-$(CONFIG_SAMSUNG_Q10) += samsung-q10.o + +# Toshiba +obj-$(CONFIG_TOSHIBA_BT_RFKILL) += toshiba_bluetooth.o +obj-$(CONFIG_TOSHIBA_HAPS) += toshiba_haps.o +obj-$(CONFIG_TOSHIBA_WMI) += toshiba-wmi.o + +# toshiba_acpi must link after wmi to ensure that wmi devices are found +# before toshiba_acpi initializes +obj-$(CONFIG_ACPI_TOSHIBA) += toshiba_acpi.o + +# Uniwill +obj-y += uniwill/ + +# Inspur +obj-$(CONFIG_INSPUR_PLATFORM_PROFILE) += inspur_platform_profile.o + +# Dasharo +obj-$(CONFIG_DASHARO_ACPI) += dasharo-acpi.o + +# Laptop drivers +obj-$(CONFIG_ACPI_CMPC) += classmate-laptop.o +obj-$(CONFIG_COMPAL_LAPTOP) += compal-laptop.o +obj-$(CONFIG_LG_LAPTOP) += lg-laptop.o +obj-$(CONFIG_PANASONIC_LAPTOP) += panasonic-laptop.o +obj-$(CONFIG_SONY_LAPTOP) += sony-laptop.o +obj-$(CONFIG_SYSTEM76_ACPI) += system76_acpi.o +obj-$(CONFIG_TOPSTAR_LAPTOP) += topstar-laptop.o + +# Platform drivers +obj-$(CONFIG_SERIAL_MULTI_INSTANTIATE) += serial-multi-instantiate.o +obj-$(CONFIG_TOUCHSCREEN_DMI) += touchscreen_dmi.o +obj-$(CONFIG_WIRELESS_HOTKEY) += wireless-hotkey.o +obj-$(CONFIG_X86_ANDROID_TABLETS) += x86-android-tablets/ + +# Intel uncore drivers +obj-$(CONFIG_INTEL_IPS) += intel_ips.o + +# Intel miscellaneous drivers +intel_p2sb-y := p2sb.o +obj-$(CONFIG_P2SB) += intel_p2sb.o + +# Intel PMIC / PMC / P-Unit devices +obj-$(CONFIG_INTEL_SCU_IPC) += intel_scu_ipc.o +obj-$(CONFIG_INTEL_SCU_PCI) += intel_scu_pcidrv.o +obj-$(CONFIG_INTEL_SCU_PLATFORM) += intel_scu_pltdrv.o +obj-$(CONFIG_INTEL_SCU_WDT) += intel_scu_wdt.o +obj-$(CONFIG_INTEL_SCU_IPC_UTIL) += intel_scu_ipcutil.o +obj-$(CONFIG_X86_INTEL_LPSS) += pmc_atom.o + +# Siemens Simatic Industrial PCs +obj-$(CONFIG_SIEMENS_SIMATIC_IPC) += siemens/ + +# Silicom +obj-$(CONFIG_SILICOM_PLATFORM) += silicom-platform.o + +# TUXEDO +obj-y += tuxedo/ + +# Winmate +obj-$(CONFIG_WINMATE_FM07_KEYS) += winmate-fm07-keys.o + +# SEL +obj-$(CONFIG_SEL3350_PLATFORM) += sel3350-platform.o + +# OneXPlayer +obj-$(CONFIG_OXP_EC) += oxpec.o diff --git a/drivers/platform/x86/acer-wireless.c b/drivers/platform/x86/acer-wireless.c new file mode 100644 index 000000000..fae8e5ad0 --- /dev/null +++ b/drivers/platform/x86/acer-wireless.c @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Acer Wireless Radio Control Driver + * + * Copyright (C) 2017 Endless Mobile, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include + +static const struct acpi_device_id acer_wireless_acpi_ids[] = { + {"10251229", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, acer_wireless_acpi_ids); + +static void acer_wireless_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct input_dev *idev = dev_get_drvdata(dev); + + dev_dbg(dev, "event=%#x\n", event); + if (event != 0x80) { + dev_notice(dev, "Unknown SMKB event: %#x\n", event); + return; + } + input_report_key(idev, KEY_RFKILL, 1); + input_sync(idev); + input_report_key(idev, KEY_RFKILL, 0); + input_sync(idev); +} + +static int acer_wireless_probe(struct platform_device *pdev) +{ + struct acpi_device *adev; + struct input_dev *idev; + int ret; + + adev = ACPI_COMPANION(&pdev->dev); + if (!adev) + return -ENODEV; + + idev = devm_input_allocate_device(&pdev->dev); + if (!idev) + return -ENOMEM; + + platform_set_drvdata(pdev, idev); + idev->name = "Acer Wireless Radio Control"; + idev->phys = "acer-wireless/input0"; + idev->id.bustype = BUS_HOST; + idev->id.vendor = PCI_VENDOR_ID_AI; + idev->id.product = 0x1229; + set_bit(EV_KEY, idev->evbit); + set_bit(KEY_RFKILL, idev->keybit); + + ret = input_register_device(idev); + if (ret) + return ret; + + return acpi_dev_install_notify_handler(adev, ACPI_DEVICE_NOTIFY, + acer_wireless_notify, + &pdev->dev); +} + +static void acer_wireless_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, + acer_wireless_notify); +} + +static struct platform_driver acer_wireless_driver = { + .probe = acer_wireless_probe, + .remove = acer_wireless_remove, + .driver = { + .name = "Acer Wireless Radio Control Driver", + .acpi_match_table = acer_wireless_acpi_ids, + }, +}; +module_platform_driver(acer_wireless_driver); + +MODULE_DESCRIPTION("Acer Wireless Radio Control Driver"); +MODULE_AUTHOR("Chris Chiu "); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/acer-wmi.c b/drivers/platform/x86/acer-wmi.c new file mode 100644 index 000000000..e0eaaefb1 --- /dev/null +++ b/drivers/platform/x86/acer-wmi.c @@ -0,0 +1,3299 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Acer WMI Laptop Extras + * + * Copyright (C) 2007-2009 Carlos Corbacho + * + * Based on acer_acpi: + * Copyright (C) 2005-2007 E.M. Smith + * Copyright (C) 2007-2008 Carlos Corbacho + */ + +#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 +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Carlos Corbacho"); +MODULE_DESCRIPTION("Acer Laptop WMI Extras Driver"); +MODULE_LICENSE("GPL"); + +/* + * Magic Number + * Meaning is unknown - this number is required for writing to ACPI for AMW0 + * (it's also used in acerhk when directly accessing the BIOS) + */ +#define ACER_AMW0_WRITE 0x9610 + +/* + * Bit masks for the AMW0 interface + */ +#define ACER_AMW0_WIRELESS_MASK 0x35 +#define ACER_AMW0_BLUETOOTH_MASK 0x34 +#define ACER_AMW0_MAILLED_MASK 0x31 + +/* + * Method IDs for WMID interface + */ +#define ACER_WMID_GET_WIRELESS_METHODID 1 +#define ACER_WMID_GET_BLUETOOTH_METHODID 2 +#define ACER_WMID_GET_BRIGHTNESS_METHODID 3 +#define ACER_WMID_SET_WIRELESS_METHODID 4 +#define ACER_WMID_SET_BLUETOOTH_METHODID 5 +#define ACER_WMID_SET_BRIGHTNESS_METHODID 6 +#define ACER_WMID_GET_THREEG_METHODID 10 +#define ACER_WMID_SET_THREEG_METHODID 11 + +#define ACER_WMID_SET_GAMING_LED_METHODID 2 +#define ACER_WMID_GET_GAMING_LED_METHODID 4 +#define ACER_WMID_GET_GAMING_SYS_INFO_METHODID 5 +#define ACER_WMID_SET_GAMING_FAN_BEHAVIOR_METHODID 14 +#define ACER_WMID_GET_GAMING_FAN_BEHAVIOR_METHODID 15 +#define ACER_WMID_SET_GAMING_FAN_SPEED_METHODID 16 +#define ACER_WMID_GET_GAMING_FAN_SPEED_METHODID 17 +#define ACER_WMID_SET_GAMING_MISC_SETTING_METHODID 22 +#define ACER_WMID_GET_GAMING_MISC_SETTING_METHODID 23 + +#define ACER_GAMING_FAN_BEHAVIOR_CPU BIT(0) +#define ACER_GAMING_FAN_BEHAVIOR_GPU BIT(3) + +#define ACER_GAMING_FAN_BEHAVIOR_STATUS_MASK GENMASK_ULL(7, 0) +#define ACER_GAMING_FAN_BEHAVIOR_ID_MASK GENMASK_ULL(15, 0) +#define ACER_GAMING_FAN_BEHAVIOR_SET_CPU_MODE_MASK GENMASK(17, 16) +#define ACER_GAMING_FAN_BEHAVIOR_SET_GPU_MODE_MASK GENMASK(23, 22) +#define ACER_GAMING_FAN_BEHAVIOR_GET_CPU_MODE_MASK GENMASK(9, 8) +#define ACER_GAMING_FAN_BEHAVIOR_GET_GPU_MODE_MASK GENMASK(15, 14) + +#define ACER_GAMING_FAN_SPEED_STATUS_MASK GENMASK_ULL(7, 0) +#define ACER_GAMING_FAN_SPEED_ID_MASK GENMASK_ULL(7, 0) +#define ACER_GAMING_FAN_SPEED_VALUE_MASK GENMASK_ULL(15, 8) + +#define ACER_GAMING_MISC_SETTING_STATUS_MASK GENMASK_ULL(7, 0) +#define ACER_GAMING_MISC_SETTING_INDEX_MASK GENMASK_ULL(7, 0) +#define ACER_GAMING_MISC_SETTING_VALUE_MASK GENMASK_ULL(15, 8) + +#define ACER_PREDATOR_V4_RETURN_STATUS_BIT_MASK GENMASK_ULL(7, 0) +#define ACER_PREDATOR_V4_SENSOR_INDEX_BIT_MASK GENMASK_ULL(15, 8) +#define ACER_PREDATOR_V4_SENSOR_READING_BIT_MASK GENMASK_ULL(23, 8) +#define ACER_PREDATOR_V4_SUPPORTED_SENSORS_BIT_MASK GENMASK_ULL(39, 24) + +/* + * Acer ACPI method GUIDs + */ +#define AMW0_GUID1 "67C3371D-95A3-4C37-BB61-DD47B491DAAB" +#define AMW0_GUID2 "431F16ED-0C2B-444C-B267-27DEB140CF9C" +#define WMID_GUID1 "6AF4F258-B401-42FD-BE91-3D4AC2D7C0D3" +#define WMID_GUID2 "95764E09-FB56-4E83-B31A-37761F60994A" +#define WMID_GUID3 "61EF69EA-865C-4BC3-A502-A0DEBA0CB531" +#define WMID_GUID4 "7A4DDFE7-5B5D-40B4-8595-4408E0CC7F56" + +/* + * Acer ACPI event GUIDs + */ +#define ACERWMID_EVENT_GUID "676AA15E-6A47-4D9F-A2CC-1E6D18D14026" + +MODULE_ALIAS("wmi:67C3371D-95A3-4C37-BB61-DD47B491DAAB"); +MODULE_ALIAS("wmi:6AF4F258-B401-42FD-BE91-3D4AC2D7C0D3"); +MODULE_ALIAS("wmi:676AA15E-6A47-4D9F-A2CC-1E6D18D14026"); + +enum acer_wmi_event_ids { + WMID_HOTKEY_EVENT = 0x1, + WMID_BACKLIGHT_EVENT = 0x4, + WMID_ACCEL_OR_KBD_DOCK_EVENT = 0x5, + WMID_GAMING_TURBO_KEY_EVENT = 0x7, + WMID_AC_EVENT = 0x8, +}; + +enum acer_wmi_predator_v4_sys_info_command { + ACER_WMID_CMD_GET_PREDATOR_V4_SUPPORTED_SENSORS = 0x0000, + ACER_WMID_CMD_GET_PREDATOR_V4_SENSOR_READING = 0x0001, + ACER_WMID_CMD_GET_PREDATOR_V4_BAT_STATUS = 0x0002, +}; + +enum acer_wmi_predator_v4_sensor_id { + ACER_WMID_SENSOR_CPU_TEMPERATURE = 0x01, + ACER_WMID_SENSOR_CPU_FAN_SPEED = 0x02, + ACER_WMID_SENSOR_EXTERNAL_TEMPERATURE_2 = 0x03, + ACER_WMID_SENSOR_GPU_FAN_SPEED = 0x06, + ACER_WMID_SENSOR_GPU_TEMPERATURE = 0x0A, +}; + +enum acer_wmi_gaming_fan_id { + ACER_WMID_CPU_FAN = 0x01, + ACER_WMID_GPU_FAN = 0x04, +}; + +enum acer_wmi_gaming_fan_mode { + ACER_WMID_FAN_MODE_AUTO = 0x01, + ACER_WMID_FAN_MODE_TURBO = 0x02, + ACER_WMID_FAN_MODE_CUSTOM = 0x03, +}; + +enum acer_wmi_predator_v4_oc { + ACER_WMID_OC_NORMAL = 0x0000, + ACER_WMID_OC_TURBO = 0x0002, +}; + +enum acer_wmi_gaming_misc_setting { + ACER_WMID_MISC_SETTING_OC_1 = 0x0005, + ACER_WMID_MISC_SETTING_OC_2 = 0x0007, + /* Unreliable on some models */ + ACER_WMID_MISC_SETTING_SUPPORTED_PROFILES = 0x000A, + ACER_WMID_MISC_SETTING_PLATFORM_PROFILE = 0x000B, +}; + +static const struct key_entry acer_wmi_keymap[] __initconst = { + {KE_KEY, 0x01, {KEY_WLAN} }, /* WiFi */ + {KE_KEY, 0x03, {KEY_WLAN} }, /* WiFi */ + {KE_KEY, 0x04, {KEY_WLAN} }, /* WiFi */ + {KE_KEY, 0x12, {KEY_BLUETOOTH} }, /* BT */ + {KE_KEY, 0x21, {KEY_PROG1} }, /* Backup */ + {KE_KEY, 0x22, {KEY_PROG2} }, /* Arcade */ + {KE_KEY, 0x23, {KEY_PROG3} }, /* P_Key */ + {KE_KEY, 0x24, {KEY_PROG4} }, /* Social networking_Key */ + {KE_KEY, 0x27, {KEY_HELP} }, + {KE_KEY, 0x29, {KEY_PROG3} }, /* P_Key for TM8372 */ + {KE_IGNORE, 0x41, {KEY_MUTE} }, + {KE_IGNORE, 0x42, {KEY_PREVIOUSSONG} }, + {KE_IGNORE, 0x4d, {KEY_PREVIOUSSONG} }, + {KE_IGNORE, 0x43, {KEY_NEXTSONG} }, + {KE_IGNORE, 0x4e, {KEY_NEXTSONG} }, + {KE_IGNORE, 0x44, {KEY_PLAYPAUSE} }, + {KE_IGNORE, 0x4f, {KEY_PLAYPAUSE} }, + {KE_IGNORE, 0x45, {KEY_STOP} }, + {KE_IGNORE, 0x50, {KEY_STOP} }, + {KE_IGNORE, 0x48, {KEY_VOLUMEUP} }, + {KE_IGNORE, 0x49, {KEY_VOLUMEDOWN} }, + {KE_IGNORE, 0x4a, {KEY_VOLUMEDOWN} }, + /* + * 0x61 is KEY_SWITCHVIDEOMODE. Usually this is a duplicate input event + * with the "Video Bus" input device events. But sometimes it is not + * a dup. Map it to KEY_UNKNOWN instead of using KE_IGNORE so that + * udev/hwdb can override it on systems where it is not a dup. + */ + {KE_KEY, 0x61, {KEY_UNKNOWN} }, + {KE_IGNORE, 0x62, {KEY_BRIGHTNESSUP} }, + {KE_IGNORE, 0x63, {KEY_BRIGHTNESSDOWN} }, + {KE_KEY, 0x64, {KEY_SWITCHVIDEOMODE} }, /* Display Switch */ + {KE_IGNORE, 0x81, {KEY_SLEEP} }, + {KE_KEY, 0x82, {KEY_TOUCHPAD_TOGGLE} }, /* Touch Pad Toggle */ + {KE_IGNORE, 0x84, {KEY_KBDILLUMTOGGLE} }, /* Automatic Keyboard background light toggle */ + {KE_KEY, KEY_TOUCHPAD_ON, {KEY_TOUCHPAD_ON} }, + {KE_KEY, KEY_TOUCHPAD_OFF, {KEY_TOUCHPAD_OFF} }, + {KE_IGNORE, 0x83, {KEY_TOUCHPAD_TOGGLE} }, + {KE_KEY, 0x85, {KEY_TOUCHPAD_TOGGLE} }, + {KE_KEY, 0x86, {KEY_WLAN} }, + {KE_KEY, 0x87, {KEY_POWER} }, + {KE_END, 0} +}; + +static struct input_dev *acer_wmi_input_dev; +static struct input_dev *acer_wmi_accel_dev; + +struct event_return_value { + u8 function; + u8 key_num; + u16 device_state; + u16 reserved1; + u8 kbd_dock_state; + u8 reserved2; +} __packed; + +/* + * GUID3 Get Device Status device flags + */ +#define ACER_WMID3_GDS_WIRELESS (1<<0) /* WiFi */ +#define ACER_WMID3_GDS_THREEG (1<<6) /* 3G */ +#define ACER_WMID3_GDS_WIMAX (1<<7) /* WiMAX */ +#define ACER_WMID3_GDS_BLUETOOTH (1<<11) /* BT */ +#define ACER_WMID3_GDS_RFBTN (1<<14) /* RF Button */ + +#define ACER_WMID3_GDS_TOUCHPAD (1<<1) /* Touchpad */ + +/* Hotkey Customized Setting and Acer Application Status. + * Set Device Default Value and Report Acer Application Status. + * When Acer Application starts, it will run this method to inform + * BIOS/EC that Acer Application is on. + * App Status + * Bit[0]: Launch Manager Status + * Bit[1]: ePM Status + * Bit[2]: Device Control Status + * Bit[3]: Acer Power Button Utility Status + * Bit[4]: RF Button Status + * Bit[5]: ODD PM Status + * Bit[6]: Device Default Value Control + * Bit[7]: Hall Sensor Application Status + */ +struct func_input_params { + u8 function_num; /* Function Number */ + u16 commun_devices; /* Communication type devices default status */ + u16 devices; /* Other type devices default status */ + u8 app_status; /* Acer Device Status. LM, ePM, RF Button... */ + u8 app_mask; /* Bit mask to app_status */ + u8 reserved; +} __packed; + +struct func_return_value { + u8 error_code; /* Error Code */ + u8 ec_return_value; /* EC Return Value */ + u16 reserved; +} __packed; + +struct wmid3_gds_set_input_param { /* Set Device Status input parameter */ + u8 function_num; /* Function Number */ + u8 hotkey_number; /* Hotkey Number */ + u16 devices; /* Set Device */ + u8 volume_value; /* Volume Value */ +} __packed; + +struct wmid3_gds_get_input_param { /* Get Device Status input parameter */ + u8 function_num; /* Function Number */ + u8 hotkey_number; /* Hotkey Number */ + u16 devices; /* Get Device */ +} __packed; + +struct wmid3_gds_return_value { /* Get Device Status return value*/ + u8 error_code; /* Error Code */ + u8 ec_return_value; /* EC Return Value */ + u16 devices; /* Current Device Status */ + u32 reserved; +} __packed; + +struct hotkey_function_type_aa { + u8 type; + u8 length; + u16 handle; + u16 commun_func_bitmap; + u16 application_func_bitmap; + u16 media_func_bitmap; + u16 display_func_bitmap; + u16 others_func_bitmap; + u8 commun_fn_key_number; +} __packed; + +/* + * Interface capability flags + */ +#define ACER_CAP_MAILLED BIT(0) +#define ACER_CAP_WIRELESS BIT(1) +#define ACER_CAP_BLUETOOTH BIT(2) +#define ACER_CAP_BRIGHTNESS BIT(3) +#define ACER_CAP_THREEG BIT(4) +#define ACER_CAP_SET_FUNCTION_MODE BIT(5) +#define ACER_CAP_KBD_DOCK BIT(6) +#define ACER_CAP_TURBO_OC BIT(7) +#define ACER_CAP_TURBO_LED BIT(8) +#define ACER_CAP_TURBO_FAN BIT(9) +#define ACER_CAP_PLATFORM_PROFILE BIT(10) +#define ACER_CAP_HWMON BIT(11) +#define ACER_CAP_PWM BIT(12) + +/* + * Interface type flags + */ +enum interface_flags { + ACER_AMW0, + ACER_AMW0_V2, + ACER_WMID, + ACER_WMID_v2, +}; + +static int max_brightness = 0xF; + +static int mailled = -1; +static int brightness = -1; +static int threeg = -1; +static int force_series; +static int force_caps = -1; +static bool ec_raw_mode; +static bool has_type_aa; +static u16 commun_func_bitmap; +static u8 commun_fn_key_number; +static bool cycle_gaming_thermal_profile = true; +static bool predator_v4; +static u64 supported_sensors; + +module_param(mailled, int, 0444); +module_param(brightness, int, 0444); +module_param(threeg, int, 0444); +module_param(force_series, int, 0444); +module_param(force_caps, int, 0444); +module_param(ec_raw_mode, bool, 0444); +module_param(cycle_gaming_thermal_profile, bool, 0644); +module_param(predator_v4, bool, 0444); +MODULE_PARM_DESC(mailled, "Set initial state of Mail LED"); +MODULE_PARM_DESC(brightness, "Set initial LCD backlight brightness"); +MODULE_PARM_DESC(threeg, "Set initial state of 3G hardware"); +MODULE_PARM_DESC(force_series, "Force a different laptop series"); +MODULE_PARM_DESC(force_caps, "Force the capability bitmask to this value"); +MODULE_PARM_DESC(ec_raw_mode, "Enable EC raw mode"); +MODULE_PARM_DESC(cycle_gaming_thermal_profile, + "Set thermal mode key in cycle mode. Disabling it sets the mode key in turbo toggle mode"); +MODULE_PARM_DESC(predator_v4, + "Enable features for predator laptops that use predator sense v4"); + +struct acer_data { + int mailled; + int threeg; + int brightness; +}; + +struct acer_debug { + struct dentry *root; + u32 wmid_devices; +}; + +static struct rfkill *wireless_rfkill; +static struct rfkill *bluetooth_rfkill; +static struct rfkill *threeg_rfkill; +static bool rfkill_inited; + +/* Each low-level interface must define at least some of the following */ +struct wmi_interface { + /* The WMI device type */ + u32 type; + + /* The capabilities this interface provides */ + u32 capability; + + /* Private data for the current interface */ + struct acer_data data; + + /* debugfs entries associated with this interface */ + struct acer_debug debug; +}; + +/* The static interface pointer, points to the currently detected interface */ +static struct wmi_interface *interface; + +/* + * Embedded Controller quirks + * Some laptops require us to directly access the EC to either enable or query + * features that are not available through WMI. + */ + +struct quirk_entry { + u8 wireless; + u8 mailled; + s8 brightness; + u8 bluetooth; + u8 turbo; + u8 cpu_fans; + u8 gpu_fans; + u8 predator_v4; + u8 pwm; +}; + +static struct quirk_entry *quirks; + +static void __init set_quirks(void) +{ + if (quirks->mailled) + interface->capability |= ACER_CAP_MAILLED; + + if (quirks->brightness) + interface->capability |= ACER_CAP_BRIGHTNESS; + + if (quirks->turbo) + interface->capability |= ACER_CAP_TURBO_OC | ACER_CAP_TURBO_LED + | ACER_CAP_TURBO_FAN; + + if (quirks->predator_v4) + interface->capability |= ACER_CAP_PLATFORM_PROFILE | + ACER_CAP_HWMON; + + if (quirks->pwm) + interface->capability |= ACER_CAP_PWM; +} + +static int __init dmi_matched(const struct dmi_system_id *dmi) +{ + quirks = dmi->driver_data; + return 1; +} + +static int __init set_force_caps(const struct dmi_system_id *dmi) +{ + if (force_caps == -1) { + force_caps = (uintptr_t)dmi->driver_data; + pr_info("Found %s, set force_caps to 0x%x\n", dmi->ident, force_caps); + } + return 1; +} + +static struct quirk_entry quirk_unknown = { +}; + +static struct quirk_entry quirk_acer_aspire_1520 = { + .brightness = -1, +}; + +static struct quirk_entry quirk_acer_travelmate_2490 = { + .mailled = 1, +}; + +static struct quirk_entry quirk_acer_nitro_an515_58 = { + .predator_v4 = 1, + .pwm = 1, +}; + +static struct quirk_entry quirk_acer_predator_ph315_53 = { + .turbo = 1, + .cpu_fans = 1, + .gpu_fans = 1, +}; + +static struct quirk_entry quirk_acer_predator_ph16_72 = { + .turbo = 1, + .cpu_fans = 1, + .gpu_fans = 1, + .predator_v4 = 1, + .pwm = 1, +}; + +static struct quirk_entry quirk_acer_predator_pt14_51 = { + .turbo = 1, + .cpu_fans = 1, + .gpu_fans = 1, + .predator_v4 = 1, + .pwm = 1, +}; + +static struct quirk_entry quirk_acer_predator_v4 = { + .predator_v4 = 1, +}; + +/* This AMW0 laptop has no bluetooth */ +static struct quirk_entry quirk_medion_md_98300 = { + .wireless = 1, +}; + +static struct quirk_entry quirk_fujitsu_amilo_li_1718 = { + .wireless = 2, +}; + +static struct quirk_entry quirk_lenovo_ideapad_s205 = { + .wireless = 3, +}; + +/* The Aspire One has a dummy ACPI-WMI interface - disable it */ +static const struct dmi_system_id acer_blacklist[] __initconst = { + { + .ident = "Acer Aspire One (SSD)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "AOA110"), + }, + }, + { + .ident = "Acer Aspire One (HDD)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "AOA150"), + }, + }, + {} +}; + +static const struct dmi_system_id amw0_whitelist[] __initconst = { + { + .ident = "Acer", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + }, + }, + { + .ident = "Gateway", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Gateway"), + }, + }, + { + .ident = "Packard Bell", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Packard Bell"), + }, + }, + {} +}; + +/* + * This quirk table is only for Acer/Gateway/Packard Bell family + * that those machines are supported by acer-wmi driver. + */ +static const struct dmi_system_id acer_quirks[] __initconst = { + { + .callback = dmi_matched, + .ident = "Acer Aspire 1360", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 1360"), + }, + .driver_data = &quirk_acer_aspire_1520, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 1520", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 1520"), + }, + .driver_data = &quirk_acer_aspire_1520, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 3100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 3100"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 3610", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 3610"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5100"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5610", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5610"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5630", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5630"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5650", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5650"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 5680", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 5680"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Aspire 9110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire 9110"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer TravelMate 2490", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "TravelMate 2490"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer TravelMate 4200", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "TravelMate 4200"), + }, + .driver_data = &quirk_acer_travelmate_2490, + }, + { + .callback = dmi_matched, + .ident = "Acer Nitro AN515-58", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Nitro AN515-58"), + }, + .driver_data = &quirk_acer_nitro_an515_58, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PH315-53", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PH315-53"), + }, + .driver_data = &quirk_acer_predator_ph315_53, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PHN16-71", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PHN16-71"), + }, + .driver_data = &quirk_acer_predator_v4, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PH16-71", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PH16-71"), + }, + .driver_data = &quirk_acer_predator_v4, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PH16-72", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PH16-72"), + }, + .driver_data = &quirk_acer_predator_ph16_72, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator Helios Neo 16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PHN16-72"), + }, + .driver_data = &quirk_acer_predator_ph16_72, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PH18-71", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PH18-71"), + }, + .driver_data = &quirk_acer_predator_v4, + }, + { + .callback = dmi_matched, + .ident = "Acer Predator PT14-51", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Predator PT14-51"), + }, + .driver_data = &quirk_acer_predator_pt14_51, + }, + { + .callback = set_force_caps, + .ident = "Acer Aspire Switch 10E SW3-016", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire SW3-016"), + }, + .driver_data = (void *)ACER_CAP_KBD_DOCK, + }, + { + .callback = set_force_caps, + .ident = "Acer Aspire Switch 10 SW5-012", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Aspire SW5-012"), + }, + .driver_data = (void *)ACER_CAP_KBD_DOCK, + }, + { + .callback = set_force_caps, + .ident = "Acer Aspire Switch V 10 SW5-017", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "SW5-017"), + }, + .driver_data = (void *)ACER_CAP_KBD_DOCK, + }, + { + .callback = set_force_caps, + .ident = "Acer One 10 (S1003)", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "One S1003"), + }, + .driver_data = (void *)ACER_CAP_KBD_DOCK, + }, + {} +}; + +/* + * This quirk list is for those non-acer machines that have AMW0_GUID1 + * but supported by acer-wmi in past days. Keeping this quirk list here + * is only for backward compatible. Please do not add new machine to + * here anymore. Those non-acer machines should be supported by + * appropriate wmi drivers. + */ +static const struct dmi_system_id non_acer_quirks[] __initconst = { + { + .callback = dmi_matched, + .ident = "Fujitsu Siemens Amilo Li 1718", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "AMILO Li 1718"), + }, + .driver_data = &quirk_fujitsu_amilo_li_1718, + }, + { + .callback = dmi_matched, + .ident = "Medion MD 98300", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MEDION"), + DMI_MATCH(DMI_PRODUCT_NAME, "WAM2030"), + }, + .driver_data = &quirk_medion_md_98300, + }, + { + .callback = dmi_matched, + .ident = "Lenovo Ideapad S205", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "10382LG"), + }, + .driver_data = &quirk_lenovo_ideapad_s205, + }, + { + .callback = dmi_matched, + .ident = "Lenovo Ideapad S205 (Brazos)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "Brazos"), + }, + .driver_data = &quirk_lenovo_ideapad_s205, + }, + { + .callback = dmi_matched, + .ident = "Lenovo 3000 N200", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "0687A31"), + }, + .driver_data = &quirk_fujitsu_amilo_li_1718, + }, + { + .callback = dmi_matched, + .ident = "Lenovo Ideapad S205-10382JG", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "10382JG"), + }, + .driver_data = &quirk_lenovo_ideapad_s205, + }, + { + .callback = dmi_matched, + .ident = "Lenovo Ideapad S205-1038DPG", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "1038DPG"), + }, + .driver_data = &quirk_lenovo_ideapad_s205, + }, + {} +}; + +static struct device *platform_profile_device; +static bool platform_profile_support; + +/* + * The profile used before turbo mode. This variable is needed for + * returning from turbo mode when the mode key is in toggle mode. + */ +static int last_non_turbo_profile = INT_MIN; + +enum acer_predator_v4_thermal_profile { + ACER_PREDATOR_V4_THERMAL_PROFILE_QUIET = 0x00, + ACER_PREDATOR_V4_THERMAL_PROFILE_BALANCED = 0x01, + ACER_PREDATOR_V4_THERMAL_PROFILE_PERFORMANCE = 0x04, + ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO = 0x05, + ACER_PREDATOR_V4_THERMAL_PROFILE_ECO = 0x06, +}; + +/* Find which quirks are needed for a particular vendor/ model pair */ +static void __init find_quirks(void) +{ + if (predator_v4) { + quirks = &quirk_acer_predator_v4; + } else if (!force_series) { + dmi_check_system(acer_quirks); + dmi_check_system(non_acer_quirks); + } else if (force_series == 2490) { + quirks = &quirk_acer_travelmate_2490; + } + + if (quirks == NULL) + quirks = &quirk_unknown; +} + +/* + * General interface convenience methods + */ + +static bool has_cap(u32 cap) +{ + return interface->capability & cap; +} + +/* + * AMW0 (V1) interface + */ +struct wmab_args { + u32 eax; + u32 ebx; + u32 ecx; + u32 edx; +}; + +struct wmab_ret { + u32 eax; + u32 ebx; + u32 ecx; + u32 edx; + u32 eex; +}; + +static acpi_status wmab_execute(struct wmab_args *regbuf, +struct acpi_buffer *result) +{ + struct acpi_buffer input; + acpi_status status; + input.length = sizeof(struct wmab_args); + input.pointer = (u8 *)regbuf; + + status = wmi_evaluate_method(AMW0_GUID1, 0, 1, &input, result); + + return status; +} + +static acpi_status AMW0_get_u32(u32 *value, u32 cap) +{ + int err; + u8 result; + + switch (cap) { + case ACER_CAP_MAILLED: + switch (quirks->mailled) { + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 7) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_WIRELESS: + switch (quirks->wireless) { + case 1: + err = ec_read(0x7B, &result); + if (err) + return AE_ERROR; + *value = result & 0x1; + return AE_OK; + case 2: + err = ec_read(0x71, &result); + if (err) + return AE_ERROR; + *value = result & 0x1; + return AE_OK; + case 3: + err = ec_read(0x78, &result); + if (err) + return AE_ERROR; + *value = result & 0x1; + return AE_OK; + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 2) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_BLUETOOTH: + switch (quirks->bluetooth) { + default: + err = ec_read(0xA, &result); + if (err) + return AE_ERROR; + *value = (result >> 4) & 0x1; + return AE_OK; + } + break; + case ACER_CAP_BRIGHTNESS: + switch (quirks->brightness) { + default: + err = ec_read(0x83, &result); + if (err) + return AE_ERROR; + *value = result; + return AE_OK; + } + break; + default: + return AE_ERROR; + } + return AE_OK; +} + +static acpi_status AMW0_set_u32(u32 value, u32 cap) +{ + struct wmab_args args; + + args.eax = ACER_AMW0_WRITE; + args.ebx = value ? (1<<8) : 0; + args.ecx = args.edx = 0; + + switch (cap) { + case ACER_CAP_MAILLED: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_MAILLED_MASK; + break; + case ACER_CAP_WIRELESS: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_WIRELESS_MASK; + break; + case ACER_CAP_BLUETOOTH: + if (value > 1) + return AE_BAD_PARAMETER; + args.ebx |= ACER_AMW0_BLUETOOTH_MASK; + break; + case ACER_CAP_BRIGHTNESS: + if (value > max_brightness) + return AE_BAD_PARAMETER; + switch (quirks->brightness) { + default: + return ec_write(0x83, value); + } + default: + return AE_ERROR; + } + + /* Actually do the set */ + return wmab_execute(&args, NULL); +} + +static acpi_status __init AMW0_find_mailled(void) +{ + struct wmab_args args; + struct wmab_ret ret; + acpi_status status = AE_OK; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + args.eax = 0x86; + args.ebx = args.ecx = args.edx = 0; + + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + kfree(out.pointer); + return AE_ERROR; + } + + if (ret.eex & 0x1) + interface->capability |= ACER_CAP_MAILLED; + + kfree(out.pointer); + + return AE_OK; +} + +static const struct acpi_device_id norfkill_ids[] __initconst = { + { "VPC2004", 0}, + { "IBM0068", 0}, + { "LEN0068", 0}, + { "SNY5001", 0}, /* sony-laptop in charge */ + { "HPQ6601", 0}, + { "", 0}, +}; + +static int __init AMW0_set_cap_acpi_check_device(void) +{ + const struct acpi_device_id *id; + + for (id = norfkill_ids; id->id[0]; id++) + if (acpi_dev_found(id->id)) + return true; + + return false; +} + +static acpi_status __init AMW0_set_capabilities(void) +{ + struct wmab_args args; + struct wmab_ret ret; + acpi_status status; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + /* + * On laptops with this strange GUID (non Acer), normal probing doesn't + * work. + */ + if (wmi_has_guid(AMW0_GUID2)) { + if ((quirks != &quirk_unknown) || + !AMW0_set_cap_acpi_check_device()) + interface->capability |= ACER_CAP_WIRELESS; + return AE_OK; + } + + args.eax = ACER_AMW0_WRITE; + args.ecx = args.edx = 0; + + args.ebx = 0xa2 << 8; + args.ebx |= ACER_AMW0_WIRELESS_MASK; + + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER && + obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + status = AE_ERROR; + goto out; + } + + if (ret.eax & 0x1) + interface->capability |= ACER_CAP_WIRELESS; + + args.ebx = 2 << 8; + args.ebx |= ACER_AMW0_BLUETOOTH_MASK; + + /* + * It's ok to use existing buffer for next wmab_execute call. + * But we need to kfree(out.pointer) if next wmab_execute fail. + */ + status = wmab_execute(&args, &out); + if (ACPI_FAILURE(status)) + goto out; + + obj = (union acpi_object *) out.pointer; + if (obj && obj->type == ACPI_TYPE_BUFFER + && obj->buffer.length == sizeof(struct wmab_ret)) { + ret = *((struct wmab_ret *) obj->buffer.pointer); + } else { + status = AE_ERROR; + goto out; + } + + if (ret.eax & 0x1) + interface->capability |= ACER_CAP_BLUETOOTH; + + /* + * This appears to be safe to enable, since all Wistron based laptops + * appear to use the same EC register for brightness, even if they + * differ for wireless, etc + */ + if (quirks->brightness >= 0) + interface->capability |= ACER_CAP_BRIGHTNESS; + + status = AE_OK; +out: + kfree(out.pointer); + return status; +} + +static struct wmi_interface AMW0_interface = { + .type = ACER_AMW0, +}; + +static struct wmi_interface AMW0_V2_interface = { + .type = ACER_AMW0_V2, +}; + +/* + * New interface (The WMID interface) + */ +static acpi_status +WMI_execute_u32(u32 method_id, u32 in, u32 *out) +{ + struct acpi_buffer input = { (acpi_size) sizeof(u32), (void *)(&in) }; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + u32 tmp = 0; + acpi_status status; + + status = wmi_evaluate_method(WMID_GUID1, 0, method_id, &input, &result); + + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) result.pointer; + if (obj) { + if (obj->type == ACPI_TYPE_BUFFER && + (obj->buffer.length == sizeof(u32) || + obj->buffer.length == sizeof(u64))) { + tmp = *((u32 *) obj->buffer.pointer); + } else if (obj->type == ACPI_TYPE_INTEGER) { + tmp = (u32) obj->integer.value; + } + } + + if (out) + *out = tmp; + + kfree(result.pointer); + + return status; +} + +static acpi_status WMID_get_u32(u32 *value, u32 cap) +{ + acpi_status status; + u8 tmp; + u32 result, method_id = 0; + + switch (cap) { + case ACER_CAP_WIRELESS: + method_id = ACER_WMID_GET_WIRELESS_METHODID; + break; + case ACER_CAP_BLUETOOTH: + method_id = ACER_WMID_GET_BLUETOOTH_METHODID; + break; + case ACER_CAP_BRIGHTNESS: + method_id = ACER_WMID_GET_BRIGHTNESS_METHODID; + break; + case ACER_CAP_THREEG: + method_id = ACER_WMID_GET_THREEG_METHODID; + break; + case ACER_CAP_MAILLED: + if (quirks->mailled == 1) { + ec_read(0x9f, &tmp); + *value = tmp & 0x1; + return 0; + } + fallthrough; + default: + return AE_ERROR; + } + status = WMI_execute_u32(method_id, 0, &result); + + if (ACPI_SUCCESS(status)) + *value = (u8)result; + + return status; +} + +static acpi_status WMID_set_u32(u32 value, u32 cap) +{ + u32 method_id = 0; + char param; + + switch (cap) { + case ACER_CAP_BRIGHTNESS: + if (value > max_brightness) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_BRIGHTNESS_METHODID; + break; + case ACER_CAP_WIRELESS: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_WIRELESS_METHODID; + break; + case ACER_CAP_BLUETOOTH: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_BLUETOOTH_METHODID; + break; + case ACER_CAP_THREEG: + if (value > 1) + return AE_BAD_PARAMETER; + method_id = ACER_WMID_SET_THREEG_METHODID; + break; + case ACER_CAP_MAILLED: + if (value > 1) + return AE_BAD_PARAMETER; + if (quirks->mailled == 1) { + param = value ? 0x92 : 0x93; + i8042_lock_chip(); + i8042_command(¶m, 0x1059); + i8042_unlock_chip(); + return 0; + } + break; + default: + return AE_ERROR; + } + return WMI_execute_u32(method_id, (u32)value, NULL); +} + +static acpi_status wmid3_get_device_status(u32 *value, u16 device) +{ + struct wmid3_gds_return_value return_value; + acpi_status status; + union acpi_object *obj; + struct wmid3_gds_get_input_param params = { + .function_num = 0x1, + .hotkey_number = commun_fn_key_number, + .devices = device, + }; + struct acpi_buffer input = { + sizeof(struct wmid3_gds_get_input_param), + ¶ms + }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + + status = wmi_evaluate_method(WMID_GUID3, 0, 0x2, &input, &output); + if (ACPI_FAILURE(status)) + return status; + + obj = output.pointer; + + if (!obj) + return AE_ERROR; + else if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return AE_ERROR; + } + if (obj->buffer.length != 8) { + pr_warn("Unknown buffer length %d\n", obj->buffer.length); + kfree(obj); + return AE_ERROR; + } + + return_value = *((struct wmid3_gds_return_value *)obj->buffer.pointer); + kfree(obj); + + if (return_value.error_code || return_value.ec_return_value) + pr_warn("Get 0x%x Device Status failed: 0x%x - 0x%x\n", + device, + return_value.error_code, + return_value.ec_return_value); + else + *value = !!(return_value.devices & device); + + return status; +} + +static acpi_status wmid_v2_get_u32(u32 *value, u32 cap) +{ + u16 device; + + switch (cap) { + case ACER_CAP_WIRELESS: + device = ACER_WMID3_GDS_WIRELESS; + break; + case ACER_CAP_BLUETOOTH: + device = ACER_WMID3_GDS_BLUETOOTH; + break; + case ACER_CAP_THREEG: + device = ACER_WMID3_GDS_THREEG; + break; + default: + return AE_ERROR; + } + return wmid3_get_device_status(value, device); +} + +static acpi_status wmid3_set_device_status(u32 value, u16 device) +{ + struct wmid3_gds_return_value return_value; + acpi_status status; + union acpi_object *obj; + u16 devices; + struct wmid3_gds_get_input_param get_params = { + .function_num = 0x1, + .hotkey_number = commun_fn_key_number, + .devices = commun_func_bitmap, + }; + struct acpi_buffer get_input = { + sizeof(struct wmid3_gds_get_input_param), + &get_params + }; + struct wmid3_gds_set_input_param set_params = { + .function_num = 0x2, + .hotkey_number = commun_fn_key_number, + .devices = commun_func_bitmap, + }; + struct acpi_buffer set_input = { + sizeof(struct wmid3_gds_set_input_param), + &set_params + }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer output2 = { ACPI_ALLOCATE_BUFFER, NULL }; + + status = wmi_evaluate_method(WMID_GUID3, 0, 0x2, &get_input, &output); + if (ACPI_FAILURE(status)) + return status; + + obj = output.pointer; + + if (!obj) + return AE_ERROR; + else if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return AE_ERROR; + } + if (obj->buffer.length != 8) { + pr_warn("Unknown buffer length %d\n", obj->buffer.length); + kfree(obj); + return AE_ERROR; + } + + return_value = *((struct wmid3_gds_return_value *)obj->buffer.pointer); + kfree(obj); + + if (return_value.error_code || return_value.ec_return_value) { + pr_warn("Get Current Device Status failed: 0x%x - 0x%x\n", + return_value.error_code, + return_value.ec_return_value); + return status; + } + + devices = return_value.devices; + set_params.devices = (value) ? (devices | device) : (devices & ~device); + + status = wmi_evaluate_method(WMID_GUID3, 0, 0x1, &set_input, &output2); + if (ACPI_FAILURE(status)) + return status; + + obj = output2.pointer; + + if (!obj) + return AE_ERROR; + else if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return AE_ERROR; + } + if (obj->buffer.length != 4) { + pr_warn("Unknown buffer length %d\n", obj->buffer.length); + kfree(obj); + return AE_ERROR; + } + + return_value = *((struct wmid3_gds_return_value *)obj->buffer.pointer); + kfree(obj); + + if (return_value.error_code || return_value.ec_return_value) + pr_warn("Set Device Status failed: 0x%x - 0x%x\n", + return_value.error_code, + return_value.ec_return_value); + + return status; +} + +static acpi_status wmid_v2_set_u32(u32 value, u32 cap) +{ + u16 device; + + switch (cap) { + case ACER_CAP_WIRELESS: + device = ACER_WMID3_GDS_WIRELESS; + break; + case ACER_CAP_BLUETOOTH: + device = ACER_WMID3_GDS_BLUETOOTH; + break; + case ACER_CAP_THREEG: + device = ACER_WMID3_GDS_THREEG; + break; + default: + return AE_ERROR; + } + return wmid3_set_device_status(value, device); +} + +static void __init type_aa_dmi_decode(const struct dmi_header *header, void *d) +{ + struct hotkey_function_type_aa *type_aa; + + /* We are looking for OEM-specific Type AAh */ + if (header->type != 0xAA) + return; + + has_type_aa = true; + type_aa = (struct hotkey_function_type_aa *) header; + + pr_info("Function bitmap for Communication Button: 0x%x\n", + type_aa->commun_func_bitmap); + commun_func_bitmap = type_aa->commun_func_bitmap; + + if (type_aa->commun_func_bitmap & ACER_WMID3_GDS_WIRELESS) + interface->capability |= ACER_CAP_WIRELESS; + if (type_aa->commun_func_bitmap & ACER_WMID3_GDS_THREEG) + interface->capability |= ACER_CAP_THREEG; + if (type_aa->commun_func_bitmap & ACER_WMID3_GDS_BLUETOOTH) + interface->capability |= ACER_CAP_BLUETOOTH; + if (type_aa->commun_func_bitmap & ACER_WMID3_GDS_RFBTN) + commun_func_bitmap &= ~ACER_WMID3_GDS_RFBTN; + + commun_fn_key_number = type_aa->commun_fn_key_number; +} + +static acpi_status __init WMID_set_capabilities(void) +{ + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obj; + acpi_status status; + u32 devices; + + status = wmi_query_block(WMID_GUID2, 0, &out); + if (ACPI_FAILURE(status)) + return status; + + obj = (union acpi_object *) out.pointer; + if (obj) { + if (obj->type == ACPI_TYPE_BUFFER && + (obj->buffer.length == sizeof(u32) || + obj->buffer.length == sizeof(u64))) { + devices = *((u32 *) obj->buffer.pointer); + } else if (obj->type == ACPI_TYPE_INTEGER) { + devices = (u32) obj->integer.value; + } else { + kfree(out.pointer); + return AE_ERROR; + } + } else { + kfree(out.pointer); + return AE_ERROR; + } + + pr_info("Function bitmap for Communication Device: 0x%x\n", devices); + if (devices & 0x07) + interface->capability |= ACER_CAP_WIRELESS; + if (devices & 0x40) + interface->capability |= ACER_CAP_THREEG; + if (devices & 0x10) + interface->capability |= ACER_CAP_BLUETOOTH; + + if (!(devices & 0x20)) + max_brightness = 0x9; + + kfree(out.pointer); + return status; +} + +static struct wmi_interface wmid_interface = { + .type = ACER_WMID, +}; + +static struct wmi_interface wmid_v2_interface = { + .type = ACER_WMID_v2, +}; + +/* + * WMID Gaming interface + */ + +static acpi_status +WMI_gaming_execute_u64(u32 method_id, u64 in, u64 *out) +{ + struct acpi_buffer input = { (acpi_size) sizeof(u64), (void *)(&in) }; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + u64 tmp = 0; + acpi_status status; + + status = wmi_evaluate_method(WMID_GUID4, 0, method_id, &input, &result); + + if (ACPI_FAILURE(status)) + return status; + obj = (union acpi_object *) result.pointer; + + if (obj) { + if (obj->type == ACPI_TYPE_BUFFER) { + if (obj->buffer.length == sizeof(u32)) + tmp = *((u32 *) obj->buffer.pointer); + else if (obj->buffer.length == sizeof(u64)) + tmp = *((u64 *) obj->buffer.pointer); + } else if (obj->type == ACPI_TYPE_INTEGER) { + tmp = (u64) obj->integer.value; + } + } + + if (out) + *out = tmp; + + kfree(result.pointer); + + return status; +} + +static int WMI_gaming_execute_u32_u64(u32 method_id, u32 in, u64 *out) +{ + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer input = { + .length = sizeof(in), + .pointer = &in, + }; + union acpi_object *obj; + acpi_status status; + int ret = 0; + + status = wmi_evaluate_method(WMID_GUID4, 0, method_id, &input, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = result.pointer; + if (obj && out) { + switch (obj->type) { + case ACPI_TYPE_INTEGER: + *out = obj->integer.value; + break; + case ACPI_TYPE_BUFFER: + if (obj->buffer.length < sizeof(*out)) + ret = -ENOMSG; + else + *out = get_unaligned_le64(obj->buffer.pointer); + + break; + default: + ret = -ENOMSG; + break; + } + } + + kfree(obj); + + return ret; +} + +static acpi_status WMID_gaming_set_u64(u64 value, u32 cap) +{ + u32 method_id = 0; + + if (!(interface->capability & cap)) + return AE_BAD_PARAMETER; + + switch (cap) { + case ACER_CAP_TURBO_LED: + method_id = ACER_WMID_SET_GAMING_LED_METHODID; + break; + default: + return AE_BAD_PARAMETER; + } + + return WMI_gaming_execute_u64(method_id, value, NULL); +} + +static acpi_status WMID_gaming_get_u64(u64 *value, u32 cap) +{ + acpi_status status; + u64 result; + u64 input; + u32 method_id; + + if (!(interface->capability & cap)) + return AE_BAD_PARAMETER; + + switch (cap) { + case ACER_CAP_TURBO_LED: + method_id = ACER_WMID_GET_GAMING_LED_METHODID; + input = 0x1; + break; + default: + return AE_BAD_PARAMETER; + } + status = WMI_gaming_execute_u64(method_id, input, &result); + if (ACPI_SUCCESS(status)) + *value = (u64) result; + + return status; +} + +static int WMID_gaming_get_sys_info(u32 command, u64 *out) +{ + acpi_status status; + u64 result; + + status = WMI_gaming_execute_u64(ACER_WMID_GET_GAMING_SYS_INFO_METHODID, command, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + /* The return status must be zero for the operation to have succeeded */ + if (FIELD_GET(ACER_PREDATOR_V4_RETURN_STATUS_BIT_MASK, result)) + return -EIO; + + *out = result; + + return 0; +} + +static int WMID_gaming_set_fan_behavior(u16 fan_bitmap, enum acer_wmi_gaming_fan_mode mode) +{ + acpi_status status; + u64 input = 0; + u64 result; + + input |= FIELD_PREP(ACER_GAMING_FAN_BEHAVIOR_ID_MASK, fan_bitmap); + + if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_CPU) + input |= FIELD_PREP(ACER_GAMING_FAN_BEHAVIOR_SET_CPU_MODE_MASK, mode); + + if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_GPU) + input |= FIELD_PREP(ACER_GAMING_FAN_BEHAVIOR_SET_GPU_MODE_MASK, mode); + + status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_FAN_BEHAVIOR_METHODID, input, + &result); + if (ACPI_FAILURE(status)) + return -EIO; + + /* The return status must be zero for the operation to have succeeded */ + if (FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_STATUS_MASK, result)) + return -EIO; + + return 0; +} + +static int WMID_gaming_get_fan_behavior(u16 fan_bitmap, enum acer_wmi_gaming_fan_mode *mode) +{ + acpi_status status; + u32 input = 0; + u64 result; + int value; + + input |= FIELD_PREP(ACER_GAMING_FAN_BEHAVIOR_ID_MASK, fan_bitmap); + status = WMI_gaming_execute_u32_u64(ACER_WMID_GET_GAMING_FAN_BEHAVIOR_METHODID, input, + &result); + if (ACPI_FAILURE(status)) + return -EIO; + + /* The return status must be zero for the operation to have succeeded */ + if (FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_STATUS_MASK, result)) + return -EIO; + + /* Theoretically multiple fans can be specified, but this is currently unused */ + if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_CPU) + value = FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_GET_CPU_MODE_MASK, result); + else if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_GPU) + value = FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_GET_GPU_MODE_MASK, result); + else + return -EINVAL; + + if (value < ACER_WMID_FAN_MODE_AUTO || value > ACER_WMID_FAN_MODE_CUSTOM) + return -ENXIO; + + *mode = value; + + return 0; +} + +static void WMID_gaming_set_fan_mode(enum acer_wmi_gaming_fan_mode mode) +{ + u16 fan_bitmap = 0; + + if (quirks->cpu_fans > 0) + fan_bitmap |= ACER_GAMING_FAN_BEHAVIOR_CPU; + + if (quirks->gpu_fans > 0) + fan_bitmap |= ACER_GAMING_FAN_BEHAVIOR_GPU; + + WMID_gaming_set_fan_behavior(fan_bitmap, mode); +} + +static int WMID_gaming_set_gaming_fan_speed(u8 fan, u8 speed) +{ + acpi_status status; + u64 input = 0; + u64 result; + + if (speed > 100) + return -EINVAL; + + input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_ID_MASK, fan); + input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_VALUE_MASK, speed); + + status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_FAN_SPEED_METHODID, input, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + switch (FIELD_GET(ACER_GAMING_FAN_SPEED_STATUS_MASK, result)) { + case 0x00: + return 0; + case 0x01: + return -ENODEV; + case 0x02: + return -EINVAL; + default: + return -ENXIO; + } +} + +static int WMID_gaming_get_gaming_fan_speed(u8 fan, u8 *speed) +{ + acpi_status status; + u32 input = 0; + u64 result; + + input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_ID_MASK, fan); + + status = WMI_gaming_execute_u32_u64(ACER_WMID_GET_GAMING_FAN_SPEED_METHODID, input, + &result); + if (ACPI_FAILURE(status)) + return -EIO; + + if (FIELD_GET(ACER_GAMING_FAN_SPEED_STATUS_MASK, result)) + return -ENODEV; + + *speed = FIELD_GET(ACER_GAMING_FAN_SPEED_VALUE_MASK, result); + + return 0; +} + +static int WMID_gaming_set_misc_setting(enum acer_wmi_gaming_misc_setting setting, u8 value) +{ + acpi_status status; + u64 input = 0; + u64 result; + + input |= FIELD_PREP(ACER_GAMING_MISC_SETTING_INDEX_MASK, setting); + input |= FIELD_PREP(ACER_GAMING_MISC_SETTING_VALUE_MASK, value); + + status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_MISC_SETTING_METHODID, input, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + /* The return status must be zero for the operation to have succeeded */ + if (FIELD_GET(ACER_GAMING_MISC_SETTING_STATUS_MASK, result)) + return -EIO; + + return 0; +} + +static int WMID_gaming_get_misc_setting(enum acer_wmi_gaming_misc_setting setting, u8 *value) +{ + u64 input = 0; + u64 result; + int ret; + + input |= FIELD_PREP(ACER_GAMING_MISC_SETTING_INDEX_MASK, setting); + + ret = WMI_gaming_execute_u32_u64(ACER_WMID_GET_GAMING_MISC_SETTING_METHODID, input, + &result); + if (ret < 0) + return ret; + + /* The return status must be zero for the operation to have succeeded */ + if (FIELD_GET(ACER_GAMING_MISC_SETTING_STATUS_MASK, result)) + return -EIO; + + *value = FIELD_GET(ACER_GAMING_MISC_SETTING_VALUE_MASK, result); + + return 0; +} + +/* + * Generic Device (interface-independent) + */ + +static acpi_status get_u32(u32 *value, u32 cap) +{ + acpi_status status = AE_ERROR; + + switch (interface->type) { + case ACER_AMW0: + status = AMW0_get_u32(value, cap); + break; + case ACER_AMW0_V2: + if (cap == ACER_CAP_MAILLED) { + status = AMW0_get_u32(value, cap); + break; + } + fallthrough; + case ACER_WMID: + status = WMID_get_u32(value, cap); + break; + case ACER_WMID_v2: + if (cap & (ACER_CAP_WIRELESS | + ACER_CAP_BLUETOOTH | + ACER_CAP_THREEG)) + status = wmid_v2_get_u32(value, cap); + else if (wmi_has_guid(WMID_GUID2)) + status = WMID_get_u32(value, cap); + break; + } + + return status; +} + +static acpi_status set_u32(u32 value, u32 cap) +{ + acpi_status status; + + if (interface->capability & cap) { + switch (interface->type) { + case ACER_AMW0: + return AMW0_set_u32(value, cap); + case ACER_AMW0_V2: + if (cap == ACER_CAP_MAILLED) + return AMW0_set_u32(value, cap); + + /* + * On some models, some WMID methods don't toggle + * properly. For those cases, we want to run the AMW0 + * method afterwards to be certain we've really toggled + * the device state. + */ + if (cap == ACER_CAP_WIRELESS || + cap == ACER_CAP_BLUETOOTH) { + status = WMID_set_u32(value, cap); + if (ACPI_FAILURE(status)) + return status; + + return AMW0_set_u32(value, cap); + } + fallthrough; + case ACER_WMID: + return WMID_set_u32(value, cap); + case ACER_WMID_v2: + if (cap & (ACER_CAP_WIRELESS | + ACER_CAP_BLUETOOTH | + ACER_CAP_THREEG)) + return wmid_v2_set_u32(value, cap); + else if (wmi_has_guid(WMID_GUID2)) + return WMID_set_u32(value, cap); + fallthrough; + default: + return AE_BAD_PARAMETER; + } + } + return AE_BAD_PARAMETER; +} + +static void __init acer_commandline_init(void) +{ + /* + * These will all fail silently if the value given is invalid, or the + * capability isn't available on the given interface + */ + if (mailled >= 0) + set_u32(mailled, ACER_CAP_MAILLED); + if (!has_type_aa && threeg >= 0) + set_u32(threeg, ACER_CAP_THREEG); + if (brightness >= 0) + set_u32(brightness, ACER_CAP_BRIGHTNESS); +} + +/* + * LED device (Mail LED only, no other LEDs known yet) + */ +static void mail_led_set(struct led_classdev *led_cdev, +enum led_brightness value) +{ + set_u32(value, ACER_CAP_MAILLED); +} + +static struct led_classdev mail_led = { + .name = "acer-wmi::mail", + .brightness_set = mail_led_set, +}; + +static int acer_led_init(struct device *dev) +{ + return led_classdev_register(dev, &mail_led); +} + +static void acer_led_exit(void) +{ + set_u32(LED_OFF, ACER_CAP_MAILLED); + led_classdev_unregister(&mail_led); +} + +/* + * Backlight device + */ +static struct backlight_device *acer_backlight_device; + +static int read_brightness(struct backlight_device *bd) +{ + u32 value; + get_u32(&value, ACER_CAP_BRIGHTNESS); + return value; +} + +static int update_bl_status(struct backlight_device *bd) +{ + int intensity = backlight_get_brightness(bd); + + set_u32(intensity, ACER_CAP_BRIGHTNESS); + + return 0; +} + +static const struct backlight_ops acer_bl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +static int acer_backlight_init(struct device *dev) +{ + struct backlight_properties props; + struct backlight_device *bd; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = max_brightness; + bd = backlight_device_register("acer-wmi", dev, NULL, &acer_bl_ops, + &props); + if (IS_ERR(bd)) { + pr_err("Could not register Acer backlight device\n"); + acer_backlight_device = NULL; + return PTR_ERR(bd); + } + + acer_backlight_device = bd; + + bd->props.power = BACKLIGHT_POWER_ON; + bd->props.brightness = read_brightness(bd); + backlight_update_status(bd); + return 0; +} + +static void acer_backlight_exit(void) +{ + backlight_device_unregister(acer_backlight_device); +} + +/* + * Accelerometer device + */ +static acpi_handle gsensor_handle; + +static int acer_gsensor_init(void) +{ + acpi_status status; + struct acpi_buffer output; + union acpi_object out_obj; + + output.length = sizeof(out_obj); + output.pointer = &out_obj; + status = acpi_evaluate_object(gsensor_handle, "_INI", NULL, &output); + if (ACPI_FAILURE(status)) + return -1; + + return 0; +} + +static int acer_gsensor_open(struct input_dev *input) +{ + return acer_gsensor_init(); +} + +static int acer_gsensor_event(void) +{ + acpi_status status; + struct acpi_buffer output; + union acpi_object out_obj[5]; + + if (!acer_wmi_accel_dev) + return -1; + + output.length = sizeof(out_obj); + output.pointer = out_obj; + + status = acpi_evaluate_object(gsensor_handle, "RDVL", NULL, &output); + if (ACPI_FAILURE(status)) + return -1; + + if (out_obj->package.count != 4) + return -1; + + input_report_abs(acer_wmi_accel_dev, ABS_X, + (s16)out_obj->package.elements[0].integer.value); + input_report_abs(acer_wmi_accel_dev, ABS_Y, + (s16)out_obj->package.elements[1].integer.value); + input_report_abs(acer_wmi_accel_dev, ABS_Z, + (s16)out_obj->package.elements[2].integer.value); + input_sync(acer_wmi_accel_dev); + return 0; +} + +/* + * Predator series turbo button + */ +static int acer_toggle_turbo(void) +{ + u64 turbo_led_state; + + /* Get current state from turbo button */ + if (ACPI_FAILURE(WMID_gaming_get_u64(&turbo_led_state, ACER_CAP_TURBO_LED))) + return -1; + + if (turbo_led_state) { + /* Turn off turbo led */ + WMID_gaming_set_u64(0x1, ACER_CAP_TURBO_LED); + + /* Set FAN mode to auto */ + if (has_cap(ACER_CAP_TURBO_FAN)) + WMID_gaming_set_fan_mode(ACER_WMID_FAN_MODE_AUTO); + + /* Set OC to normal */ + if (has_cap(ACER_CAP_TURBO_OC)) { + WMID_gaming_set_misc_setting(ACER_WMID_MISC_SETTING_OC_1, + ACER_WMID_OC_NORMAL); + WMID_gaming_set_misc_setting(ACER_WMID_MISC_SETTING_OC_2, + ACER_WMID_OC_NORMAL); + } + } else { + /* Turn on turbo led */ + WMID_gaming_set_u64(0x10001, ACER_CAP_TURBO_LED); + + /* Set FAN mode to turbo */ + if (has_cap(ACER_CAP_TURBO_FAN)) + WMID_gaming_set_fan_mode(ACER_WMID_FAN_MODE_TURBO); + + /* Set OC to turbo mode */ + if (has_cap(ACER_CAP_TURBO_OC)) { + WMID_gaming_set_misc_setting(ACER_WMID_MISC_SETTING_OC_1, + ACER_WMID_OC_TURBO); + WMID_gaming_set_misc_setting(ACER_WMID_MISC_SETTING_OC_2, + ACER_WMID_OC_TURBO); + } + } + return turbo_led_state; +} + +static int +acer_predator_v4_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + u8 tp; + int err; + + err = WMID_gaming_get_misc_setting(ACER_WMID_MISC_SETTING_PLATFORM_PROFILE, &tp); + if (err) + return err; + + switch (tp) { + case ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case ACER_PREDATOR_V4_THERMAL_PROFILE_PERFORMANCE: + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + break; + case ACER_PREDATOR_V4_THERMAL_PROFILE_BALANCED: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case ACER_PREDATOR_V4_THERMAL_PROFILE_QUIET: + *profile = PLATFORM_PROFILE_QUIET; + break; + case ACER_PREDATOR_V4_THERMAL_PROFILE_ECO: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +acer_predator_v4_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + int err, tp; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO; + break; + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_BALANCED; + break; + case PLATFORM_PROFILE_QUIET: + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_QUIET; + break; + case PLATFORM_PROFILE_LOW_POWER: + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_ECO; + break; + default: + return -EOPNOTSUPP; + } + + err = WMID_gaming_set_misc_setting(ACER_WMID_MISC_SETTING_PLATFORM_PROFILE, tp); + if (err) + return err; + + if (tp != ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO) + last_non_turbo_profile = tp; + + return 0; +} + +static int +acer_predator_v4_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_QUIET, choices); + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + + /* Set default non-turbo profile */ + last_non_turbo_profile = ACER_PREDATOR_V4_THERMAL_PROFILE_BALANCED; + + return 0; +} + +static const struct platform_profile_ops acer_predator_v4_platform_profile_ops = { + .probe = acer_predator_v4_platform_profile_probe, + .profile_get = acer_predator_v4_platform_profile_get, + .profile_set = acer_predator_v4_platform_profile_set, +}; + +static int acer_platform_profile_setup(struct platform_device *device) +{ + if (quirks->predator_v4) { + platform_profile_device = devm_platform_profile_register( + &device->dev, "acer-wmi", NULL, &acer_predator_v4_platform_profile_ops); + if (IS_ERR(platform_profile_device)) + return PTR_ERR(platform_profile_device); + + platform_profile_support = true; + } + return 0; +} + +static int acer_thermal_profile_change(void) +{ + /* + * This mode key will either cycle through each mode or toggle the + * most performant profile. + */ + if (quirks->predator_v4) { + u8 current_tp; + int err, tp; + + if (cycle_gaming_thermal_profile) { + platform_profile_cycle(); + } else { + err = WMID_gaming_get_misc_setting( + ACER_WMID_MISC_SETTING_PLATFORM_PROFILE, ¤t_tp); + if (err) + return err; + + if (current_tp == ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO) + tp = last_non_turbo_profile; + else + tp = ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO; + + err = WMID_gaming_set_misc_setting( + ACER_WMID_MISC_SETTING_PLATFORM_PROFILE, tp); + if (err) + return err; + + /* Store last profile for toggle */ + if (current_tp != ACER_PREDATOR_V4_THERMAL_PROFILE_TURBO) + last_non_turbo_profile = current_tp; + + platform_profile_notify(platform_profile_device); + } + } + + return 0; +} + +/* + * Switch series keyboard dock status + */ +static int acer_kbd_dock_state_to_sw_tablet_mode(u8 kbd_dock_state) +{ + switch (kbd_dock_state) { + case 0x01: /* Docked, traditional clamshell laptop mode */ + return 0; + case 0x04: /* Stand-alone tablet */ + case 0x40: /* Docked, tent mode, keyboard not usable */ + return 1; + default: + pr_warn("Unknown kbd_dock_state 0x%02x\n", kbd_dock_state); + } + + return 0; +} + +static void acer_kbd_dock_get_initial_state(void) +{ + u8 *output, input[8] = { 0x05, 0x00, }; + struct acpi_buffer input_buf = { sizeof(input), input }; + struct acpi_buffer output_buf = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int sw_tablet_mode; + + status = wmi_evaluate_method(WMID_GUID3, 0, 0x2, &input_buf, &output_buf); + if (ACPI_FAILURE(status)) { + pr_err("Error getting keyboard-dock initial status: %s\n", + acpi_format_exception(status)); + return; + } + + obj = output_buf.pointer; + if (!obj || obj->type != ACPI_TYPE_BUFFER || obj->buffer.length != 8) { + pr_err("Unexpected output format getting keyboard-dock initial status\n"); + goto out_free_obj; + } + + output = obj->buffer.pointer; + if (output[0] != 0x00 || (output[3] != 0x05 && output[3] != 0x45)) { + pr_err("Unexpected output [0]=0x%02x [3]=0x%02x getting keyboard-dock initial status\n", + output[0], output[3]); + goto out_free_obj; + } + + sw_tablet_mode = acer_kbd_dock_state_to_sw_tablet_mode(output[4]); + input_report_switch(acer_wmi_input_dev, SW_TABLET_MODE, sw_tablet_mode); + +out_free_obj: + kfree(obj); +} + +static void acer_kbd_dock_event(const struct event_return_value *event) +{ + int sw_tablet_mode; + + if (!has_cap(ACER_CAP_KBD_DOCK)) + return; + + sw_tablet_mode = acer_kbd_dock_state_to_sw_tablet_mode(event->kbd_dock_state); + input_report_switch(acer_wmi_input_dev, SW_TABLET_MODE, sw_tablet_mode); + input_sync(acer_wmi_input_dev); +} + +/* + * Rfkill devices + */ +static void acer_rfkill_update(struct work_struct *ignored); +static DECLARE_DELAYED_WORK(acer_rfkill_work, acer_rfkill_update); +static void acer_rfkill_update(struct work_struct *ignored) +{ + u32 state; + acpi_status status; + + if (has_cap(ACER_CAP_WIRELESS)) { + status = get_u32(&state, ACER_CAP_WIRELESS); + if (ACPI_SUCCESS(status)) { + if (quirks->wireless == 3) + rfkill_set_hw_state(wireless_rfkill, !state); + else + rfkill_set_sw_state(wireless_rfkill, !state); + } + } + + if (has_cap(ACER_CAP_BLUETOOTH)) { + status = get_u32(&state, ACER_CAP_BLUETOOTH); + if (ACPI_SUCCESS(status)) + rfkill_set_sw_state(bluetooth_rfkill, !state); + } + + if (has_cap(ACER_CAP_THREEG) && wmi_has_guid(WMID_GUID3)) { + status = get_u32(&state, ACER_WMID3_GDS_THREEG); + if (ACPI_SUCCESS(status)) + rfkill_set_sw_state(threeg_rfkill, !state); + } + + schedule_delayed_work(&acer_rfkill_work, round_jiffies_relative(HZ)); +} + +static int acer_rfkill_set(void *data, bool blocked) +{ + acpi_status status; + u32 cap = (unsigned long)data; + + if (rfkill_inited) { + status = set_u32(!blocked, cap); + if (ACPI_FAILURE(status)) + return -ENODEV; + } + + return 0; +} + +static const struct rfkill_ops acer_rfkill_ops = { + .set_block = acer_rfkill_set, +}; + +static struct rfkill *acer_rfkill_register(struct device *dev, + enum rfkill_type type, + char *name, u32 cap) +{ + int err; + struct rfkill *rfkill_dev; + u32 state; + acpi_status status; + + rfkill_dev = rfkill_alloc(name, dev, type, + &acer_rfkill_ops, + (void *)(unsigned long)cap); + if (!rfkill_dev) + return ERR_PTR(-ENOMEM); + + status = get_u32(&state, cap); + + err = rfkill_register(rfkill_dev); + if (err) { + rfkill_destroy(rfkill_dev); + return ERR_PTR(err); + } + + if (ACPI_SUCCESS(status)) + rfkill_set_sw_state(rfkill_dev, !state); + + return rfkill_dev; +} + +static int acer_rfkill_init(struct device *dev) +{ + int err; + + if (has_cap(ACER_CAP_WIRELESS)) { + wireless_rfkill = acer_rfkill_register(dev, RFKILL_TYPE_WLAN, + "acer-wireless", ACER_CAP_WIRELESS); + if (IS_ERR(wireless_rfkill)) { + err = PTR_ERR(wireless_rfkill); + goto error_wireless; + } + } + + if (has_cap(ACER_CAP_BLUETOOTH)) { + bluetooth_rfkill = acer_rfkill_register(dev, + RFKILL_TYPE_BLUETOOTH, "acer-bluetooth", + ACER_CAP_BLUETOOTH); + if (IS_ERR(bluetooth_rfkill)) { + err = PTR_ERR(bluetooth_rfkill); + goto error_bluetooth; + } + } + + if (has_cap(ACER_CAP_THREEG)) { + threeg_rfkill = acer_rfkill_register(dev, + RFKILL_TYPE_WWAN, "acer-threeg", + ACER_CAP_THREEG); + if (IS_ERR(threeg_rfkill)) { + err = PTR_ERR(threeg_rfkill); + goto error_threeg; + } + } + + rfkill_inited = true; + + if ((ec_raw_mode || !wmi_has_guid(ACERWMID_EVENT_GUID)) && + has_cap(ACER_CAP_WIRELESS | ACER_CAP_BLUETOOTH | ACER_CAP_THREEG)) + schedule_delayed_work(&acer_rfkill_work, + round_jiffies_relative(HZ)); + + return 0; + +error_threeg: + if (has_cap(ACER_CAP_BLUETOOTH)) { + rfkill_unregister(bluetooth_rfkill); + rfkill_destroy(bluetooth_rfkill); + } +error_bluetooth: + if (has_cap(ACER_CAP_WIRELESS)) { + rfkill_unregister(wireless_rfkill); + rfkill_destroy(wireless_rfkill); + } +error_wireless: + return err; +} + +static void acer_rfkill_exit(void) +{ + if ((ec_raw_mode || !wmi_has_guid(ACERWMID_EVENT_GUID)) && + has_cap(ACER_CAP_WIRELESS | ACER_CAP_BLUETOOTH | ACER_CAP_THREEG)) + cancel_delayed_work_sync(&acer_rfkill_work); + + if (has_cap(ACER_CAP_WIRELESS)) { + rfkill_unregister(wireless_rfkill); + rfkill_destroy(wireless_rfkill); + } + + if (has_cap(ACER_CAP_BLUETOOTH)) { + rfkill_unregister(bluetooth_rfkill); + rfkill_destroy(bluetooth_rfkill); + } + + if (has_cap(ACER_CAP_THREEG)) { + rfkill_unregister(threeg_rfkill); + rfkill_destroy(threeg_rfkill); + } +} + +static void acer_wmi_notify(union acpi_object *obj, void *context) +{ + struct event_return_value return_value; + u16 device_state; + const struct key_entry *key; + u32 scancode; + + if (!obj) + return; + if (obj->type != ACPI_TYPE_BUFFER) { + pr_warn("Unknown response received %d\n", obj->type); + return; + } + if (obj->buffer.length != 8) { + pr_warn("Unknown buffer length %d\n", obj->buffer.length); + return; + } + + return_value = *((struct event_return_value *)obj->buffer.pointer); + + switch (return_value.function) { + case WMID_HOTKEY_EVENT: + device_state = return_value.device_state; + pr_debug("device state: 0x%x\n", device_state); + + key = sparse_keymap_entry_from_scancode(acer_wmi_input_dev, + return_value.key_num); + if (!key) { + pr_warn("Unknown key number - 0x%x\n", + return_value.key_num); + } else { + scancode = return_value.key_num; + switch (key->keycode) { + case KEY_WLAN: + case KEY_BLUETOOTH: + if (has_cap(ACER_CAP_WIRELESS)) + rfkill_set_sw_state(wireless_rfkill, + !(device_state & ACER_WMID3_GDS_WIRELESS)); + if (has_cap(ACER_CAP_THREEG)) + rfkill_set_sw_state(threeg_rfkill, + !(device_state & ACER_WMID3_GDS_THREEG)); + if (has_cap(ACER_CAP_BLUETOOTH)) + rfkill_set_sw_state(bluetooth_rfkill, + !(device_state & ACER_WMID3_GDS_BLUETOOTH)); + break; + case KEY_TOUCHPAD_TOGGLE: + scancode = (device_state & ACER_WMID3_GDS_TOUCHPAD) ? + KEY_TOUCHPAD_ON : KEY_TOUCHPAD_OFF; + } + sparse_keymap_report_event(acer_wmi_input_dev, scancode, 1, true); + } + break; + case WMID_BACKLIGHT_EVENT: + /* Already handled by acpi-video */ + break; + case WMID_ACCEL_OR_KBD_DOCK_EVENT: + acer_gsensor_event(); + acer_kbd_dock_event(&return_value); + break; + case WMID_GAMING_TURBO_KEY_EVENT: + if (return_value.key_num == 0x4) + acer_toggle_turbo(); + if (return_value.key_num == 0x5 && has_cap(ACER_CAP_PLATFORM_PROFILE)) + acer_thermal_profile_change(); + break; + case WMID_AC_EVENT: + /* We ignore AC events here */ + break; + default: + pr_warn("Unknown function number - %d - %d\n", + return_value.function, return_value.key_num); + break; + } +} + +static acpi_status __init +wmid3_set_function_mode(struct func_input_params *params, + struct func_return_value *return_value) +{ + acpi_status status; + union acpi_object *obj; + + struct acpi_buffer input = { sizeof(struct func_input_params), params }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + + status = wmi_evaluate_method(WMID_GUID3, 0, 0x1, &input, &output); + if (ACPI_FAILURE(status)) + return status; + + obj = output.pointer; + + if (!obj) + return AE_ERROR; + else if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return AE_ERROR; + } + if (obj->buffer.length != 4) { + pr_warn("Unknown buffer length %d\n", obj->buffer.length); + kfree(obj); + return AE_ERROR; + } + + *return_value = *((struct func_return_value *)obj->buffer.pointer); + kfree(obj); + + return status; +} + +static int __init acer_wmi_enable_ec_raw(void) +{ + struct func_return_value return_value; + acpi_status status; + struct func_input_params params = { + .function_num = 0x1, + .commun_devices = 0xFFFF, + .devices = 0xFFFF, + .app_status = 0x00, /* Launch Manager Deactive */ + .app_mask = 0x01, + }; + + status = wmid3_set_function_mode(¶ms, &return_value); + + if (return_value.error_code || return_value.ec_return_value) + pr_warn("Enabling EC raw mode failed: 0x%x - 0x%x\n", + return_value.error_code, + return_value.ec_return_value); + else + pr_info("Enabled EC raw mode\n"); + + return status; +} + +static int __init acer_wmi_enable_lm(void) +{ + struct func_return_value return_value; + acpi_status status; + struct func_input_params params = { + .function_num = 0x1, + .commun_devices = 0xFFFF, + .devices = 0xFFFF, + .app_status = 0x01, /* Launch Manager Active */ + .app_mask = 0x01, + }; + + status = wmid3_set_function_mode(¶ms, &return_value); + + if (return_value.error_code || return_value.ec_return_value) + pr_warn("Enabling Launch Manager failed: 0x%x - 0x%x\n", + return_value.error_code, + return_value.ec_return_value); + + return status; +} + +static int __init acer_wmi_enable_rf_button(void) +{ + struct func_return_value return_value; + acpi_status status; + struct func_input_params params = { + .function_num = 0x1, + .commun_devices = 0xFFFF, + .devices = 0xFFFF, + .app_status = 0x10, /* RF Button Active */ + .app_mask = 0x10, + }; + + status = wmid3_set_function_mode(¶ms, &return_value); + + if (return_value.error_code || return_value.ec_return_value) + pr_warn("Enabling RF Button failed: 0x%x - 0x%x\n", + return_value.error_code, + return_value.ec_return_value); + + return status; +} + +static int __init acer_wmi_accel_setup(void) +{ + struct acpi_device *adev; + int err; + + adev = acpi_dev_get_first_match_dev("BST0001", NULL, -1); + if (!adev) + return -ENODEV; + + gsensor_handle = acpi_device_handle(adev); + acpi_dev_put(adev); + + acer_wmi_accel_dev = input_allocate_device(); + if (!acer_wmi_accel_dev) + return -ENOMEM; + + acer_wmi_accel_dev->open = acer_gsensor_open; + + acer_wmi_accel_dev->name = "Acer BMA150 accelerometer"; + acer_wmi_accel_dev->phys = "wmi/input1"; + acer_wmi_accel_dev->id.bustype = BUS_HOST; + acer_wmi_accel_dev->evbit[0] = BIT_MASK(EV_ABS); + input_set_abs_params(acer_wmi_accel_dev, ABS_X, -16384, 16384, 0, 0); + input_set_abs_params(acer_wmi_accel_dev, ABS_Y, -16384, 16384, 0, 0); + input_set_abs_params(acer_wmi_accel_dev, ABS_Z, -16384, 16384, 0, 0); + + err = input_register_device(acer_wmi_accel_dev); + if (err) + goto err_free_dev; + + return 0; + +err_free_dev: + input_free_device(acer_wmi_accel_dev); + return err; +} + +static int __init acer_wmi_input_setup(void) +{ + acpi_status status; + int err; + + acer_wmi_input_dev = input_allocate_device(); + if (!acer_wmi_input_dev) + return -ENOMEM; + + acer_wmi_input_dev->name = "Acer WMI hotkeys"; + acer_wmi_input_dev->phys = "wmi/input0"; + acer_wmi_input_dev->id.bustype = BUS_HOST; + + err = sparse_keymap_setup(acer_wmi_input_dev, acer_wmi_keymap, NULL); + if (err) + goto err_free_dev; + + if (has_cap(ACER_CAP_KBD_DOCK)) + input_set_capability(acer_wmi_input_dev, EV_SW, SW_TABLET_MODE); + + status = wmi_install_notify_handler(ACERWMID_EVENT_GUID, + acer_wmi_notify, NULL); + if (ACPI_FAILURE(status)) { + err = -EIO; + goto err_free_dev; + } + + if (has_cap(ACER_CAP_KBD_DOCK)) + acer_kbd_dock_get_initial_state(); + + err = input_register_device(acer_wmi_input_dev); + if (err) + goto err_uninstall_notifier; + + return 0; + +err_uninstall_notifier: + wmi_remove_notify_handler(ACERWMID_EVENT_GUID); +err_free_dev: + input_free_device(acer_wmi_input_dev); + return err; +} + +static void acer_wmi_input_destroy(void) +{ + wmi_remove_notify_handler(ACERWMID_EVENT_GUID); + input_unregister_device(acer_wmi_input_dev); +} + +/* + * debugfs functions + */ +static u32 get_wmid_devices(void) +{ + struct acpi_buffer out = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obj; + acpi_status status; + u32 devices = 0; + + status = wmi_query_block(WMID_GUID2, 0, &out); + if (ACPI_FAILURE(status)) + return 0; + + obj = (union acpi_object *) out.pointer; + if (obj) { + if (obj->type == ACPI_TYPE_BUFFER && + (obj->buffer.length == sizeof(u32) || + obj->buffer.length == sizeof(u64))) { + devices = *((u32 *) obj->buffer.pointer); + } else if (obj->type == ACPI_TYPE_INTEGER) { + devices = (u32) obj->integer.value; + } + } + + kfree(out.pointer); + return devices; +} + +static int acer_wmi_hwmon_init(void); + +/* + * Platform device + */ +static int acer_platform_probe(struct platform_device *device) +{ + int err; + + if (has_cap(ACER_CAP_MAILLED)) { + err = acer_led_init(&device->dev); + if (err) + goto error_mailled; + } + + if (has_cap(ACER_CAP_BRIGHTNESS)) { + err = acer_backlight_init(&device->dev); + if (err) + goto error_brightness; + } + + err = acer_rfkill_init(&device->dev); + if (err) + goto error_rfkill; + + if (has_cap(ACER_CAP_PLATFORM_PROFILE)) { + err = acer_platform_profile_setup(device); + if (err) + goto error_platform_profile; + } + + if (has_cap(ACER_CAP_HWMON)) { + err = acer_wmi_hwmon_init(); + if (err) + goto error_hwmon; + } + + return 0; + +error_hwmon: +error_platform_profile: + acer_rfkill_exit(); +error_rfkill: + if (has_cap(ACER_CAP_BRIGHTNESS)) + acer_backlight_exit(); +error_brightness: + if (has_cap(ACER_CAP_MAILLED)) + acer_led_exit(); +error_mailled: + return err; +} + +static void acer_platform_remove(struct platform_device *device) +{ + if (has_cap(ACER_CAP_MAILLED)) + acer_led_exit(); + if (has_cap(ACER_CAP_BRIGHTNESS)) + acer_backlight_exit(); + + acer_rfkill_exit(); +} + +#ifdef CONFIG_PM_SLEEP +static int acer_suspend(struct device *dev) +{ + u32 value; + struct acer_data *data = &interface->data; + + if (!data) + return -ENOMEM; + + if (has_cap(ACER_CAP_MAILLED)) { + get_u32(&value, ACER_CAP_MAILLED); + set_u32(LED_OFF, ACER_CAP_MAILLED); + data->mailled = value; + } + + if (has_cap(ACER_CAP_BRIGHTNESS)) { + get_u32(&value, ACER_CAP_BRIGHTNESS); + data->brightness = value; + } + + return 0; +} + +static int acer_resume(struct device *dev) +{ + struct acer_data *data = &interface->data; + + if (!data) + return -ENOMEM; + + if (has_cap(ACER_CAP_MAILLED)) + set_u32(data->mailled, ACER_CAP_MAILLED); + + if (has_cap(ACER_CAP_BRIGHTNESS)) + set_u32(data->brightness, ACER_CAP_BRIGHTNESS); + + if (acer_wmi_accel_dev) + acer_gsensor_init(); + + return 0; +} +#else +#define acer_suspend NULL +#define acer_resume NULL +#endif + +static SIMPLE_DEV_PM_OPS(acer_pm, acer_suspend, acer_resume); + +static void acer_platform_shutdown(struct platform_device *device) +{ + struct acer_data *data = &interface->data; + + if (!data) + return; + + if (has_cap(ACER_CAP_MAILLED)) + set_u32(LED_OFF, ACER_CAP_MAILLED); +} + +static struct platform_driver acer_platform_driver = { + .driver = { + .name = "acer-wmi", + .pm = &acer_pm, + }, + .probe = acer_platform_probe, + .remove = acer_platform_remove, + .shutdown = acer_platform_shutdown, +}; + +static struct platform_device *acer_platform_device; + +static void remove_debugfs(void) +{ + debugfs_remove_recursive(interface->debug.root); +} + +static void __init create_debugfs(void) +{ + interface->debug.root = debugfs_create_dir("acer-wmi", NULL); + + debugfs_create_u32("devices", S_IRUGO, interface->debug.root, + &interface->debug.wmid_devices); +} + +static const enum acer_wmi_predator_v4_sensor_id acer_wmi_temp_channel_to_sensor_id[] = { + [0] = ACER_WMID_SENSOR_CPU_TEMPERATURE, + [1] = ACER_WMID_SENSOR_GPU_TEMPERATURE, + [2] = ACER_WMID_SENSOR_EXTERNAL_TEMPERATURE_2, +}; + +static const enum acer_wmi_predator_v4_sensor_id acer_wmi_fan_channel_to_sensor_id[] = { + [0] = ACER_WMID_SENSOR_CPU_FAN_SPEED, + [1] = ACER_WMID_SENSOR_GPU_FAN_SPEED, +}; + +static const enum acer_wmi_gaming_fan_id acer_wmi_fan_channel_to_fan_id[] = { + [0] = ACER_WMID_CPU_FAN, + [1] = ACER_WMID_GPU_FAN, +}; + +static const u16 acer_wmi_fan_channel_to_fan_bitmap[] = { + [0] = ACER_GAMING_FAN_BEHAVIOR_CPU, + [1] = ACER_GAMING_FAN_BEHAVIOR_GPU, +}; + +static umode_t acer_wmi_hwmon_is_visible(const void *data, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + enum acer_wmi_predator_v4_sensor_id sensor_id; + const u64 *supported_sensors = data; + + switch (type) { + case hwmon_temp: + sensor_id = acer_wmi_temp_channel_to_sensor_id[channel]; + break; + case hwmon_pwm: + if (!has_cap(ACER_CAP_PWM)) + return 0; + + fallthrough; + case hwmon_fan: + sensor_id = acer_wmi_fan_channel_to_sensor_id[channel]; + break; + default: + return 0; + } + + if (*supported_sensors & BIT(sensor_id - 1)) { + if (type == hwmon_pwm) + return 0644; + + return 0444; + } + + return 0; +} + +static int acer_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + u64 command = ACER_WMID_CMD_GET_PREDATOR_V4_SENSOR_READING; + enum acer_wmi_gaming_fan_mode mode; + u16 fan_bitmap; + u8 fan, speed; + u64 result; + int ret; + + switch (type) { + case hwmon_temp: + command |= FIELD_PREP(ACER_PREDATOR_V4_SENSOR_INDEX_BIT_MASK, + acer_wmi_temp_channel_to_sensor_id[channel]); + + ret = WMID_gaming_get_sys_info(command, &result); + if (ret < 0) + return ret; + + result = FIELD_GET(ACER_PREDATOR_V4_SENSOR_READING_BIT_MASK, result); + *val = result * MILLIDEGREE_PER_DEGREE; + return 0; + case hwmon_fan: + command |= FIELD_PREP(ACER_PREDATOR_V4_SENSOR_INDEX_BIT_MASK, + acer_wmi_fan_channel_to_sensor_id[channel]); + + ret = WMID_gaming_get_sys_info(command, &result); + if (ret < 0) + return ret; + + *val = FIELD_GET(ACER_PREDATOR_V4_SENSOR_READING_BIT_MASK, result); + return 0; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + fan = acer_wmi_fan_channel_to_fan_id[channel]; + ret = WMID_gaming_get_gaming_fan_speed(fan, &speed); + if (ret < 0) + return ret; + + *val = fixp_linear_interpolate(0, 0, 100, U8_MAX, speed); + return 0; + case hwmon_pwm_enable: + fan_bitmap = acer_wmi_fan_channel_to_fan_bitmap[channel]; + ret = WMID_gaming_get_fan_behavior(fan_bitmap, &mode); + if (ret < 0) + return ret; + + switch (mode) { + case ACER_WMID_FAN_MODE_AUTO: + *val = 2; + return 0; + case ACER_WMID_FAN_MODE_TURBO: + *val = 0; + return 0; + case ACER_WMID_FAN_MODE_CUSTOM: + *val = 1; + return 0; + default: + return -ENXIO; + } + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } +} + +static int acer_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + enum acer_wmi_gaming_fan_mode mode; + u16 fan_bitmap; + u8 fan, speed; + + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + fan = acer_wmi_fan_channel_to_fan_id[channel]; + speed = fixp_linear_interpolate(0, 0, U8_MAX, 100, + clamp_val(val, 0, U8_MAX)); + + return WMID_gaming_set_gaming_fan_speed(fan, speed); + case hwmon_pwm_enable: + fan_bitmap = acer_wmi_fan_channel_to_fan_bitmap[channel]; + + switch (val) { + case 0: + mode = ACER_WMID_FAN_MODE_TURBO; + break; + case 1: + mode = ACER_WMID_FAN_MODE_CUSTOM; + break; + case 2: + mode = ACER_WMID_FAN_MODE_AUTO; + break; + default: + return -EINVAL; + } + + return WMID_gaming_set_fan_behavior(fan_bitmap, mode); + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_channel_info *const acer_wmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT + ), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT, + HWMON_F_INPUT + ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE + ), + NULL +}; + +static const struct hwmon_ops acer_wmi_hwmon_ops = { + .read = acer_wmi_hwmon_read, + .write = acer_wmi_hwmon_write, + .is_visible = acer_wmi_hwmon_is_visible, +}; + +static const struct hwmon_chip_info acer_wmi_hwmon_chip_info = { + .ops = &acer_wmi_hwmon_ops, + .info = acer_wmi_hwmon_info, +}; + +static int acer_wmi_hwmon_init(void) +{ + struct device *dev = &acer_platform_device->dev; + struct device *hwmon; + u64 result; + int ret; + + ret = WMID_gaming_get_sys_info(ACER_WMID_CMD_GET_PREDATOR_V4_SUPPORTED_SENSORS, &result); + if (ret < 0) + return ret; + + /* Return early if no sensors are available */ + supported_sensors = FIELD_GET(ACER_PREDATOR_V4_SUPPORTED_SENSORS_BIT_MASK, result); + if (!supported_sensors) + return 0; + + hwmon = devm_hwmon_device_register_with_info(dev, "acer", + &supported_sensors, + &acer_wmi_hwmon_chip_info, + NULL); + + if (IS_ERR(hwmon)) { + dev_err(dev, "Could not register acer hwmon device\n"); + return PTR_ERR(hwmon); + } + + return 0; +} + +static int __init acer_wmi_init(void) +{ + int err; + + pr_info("Acer Laptop ACPI-WMI Extras\n"); + + if (dmi_check_system(acer_blacklist)) { + pr_info("Blacklisted hardware detected - not loading\n"); + return -ENODEV; + } + + find_quirks(); + + /* + * The AMW0_GUID1 wmi is not only found on Acer family but also other + * machines like Lenovo, Fujitsu and Medion. In the past days, + * acer-wmi driver handled those non-Acer machines by quirks list. + * But actually acer-wmi driver was loaded on any machines that have + * AMW0_GUID1. This behavior is strange because those machines should + * be supported by appropriate wmi drivers. e.g. fujitsu-laptop, + * ideapad-laptop. So, here checks the machine that has AMW0_GUID1 + * should be in Acer/Gateway/Packard Bell white list, or it's already + * in the past quirk list. + */ + if (wmi_has_guid(AMW0_GUID1) && + !dmi_check_system(amw0_whitelist) && + quirks == &quirk_unknown) { + pr_debug("Unsupported machine has AMW0_GUID1, unable to load\n"); + return -ENODEV; + } + + /* + * Detect which ACPI-WMI interface we're using. + */ + if (wmi_has_guid(AMW0_GUID1) && wmi_has_guid(WMID_GUID1)) + interface = &AMW0_V2_interface; + + if (!wmi_has_guid(AMW0_GUID1) && wmi_has_guid(WMID_GUID1)) + interface = &wmid_interface; + + if (wmi_has_guid(WMID_GUID3)) + interface = &wmid_v2_interface; + + if (interface) + dmi_walk(type_aa_dmi_decode, NULL); + + if (wmi_has_guid(WMID_GUID2) && interface) { + if (!has_type_aa && ACPI_FAILURE(WMID_set_capabilities())) { + pr_err("Unable to detect available WMID devices\n"); + return -ENODEV; + } + /* WMID always provides brightness methods */ + interface->capability |= ACER_CAP_BRIGHTNESS; + } else if (!wmi_has_guid(WMID_GUID2) && interface && !has_type_aa && force_caps == -1) { + pr_err("No WMID device detection method found\n"); + return -ENODEV; + } + + if (wmi_has_guid(AMW0_GUID1) && !wmi_has_guid(WMID_GUID1)) { + interface = &AMW0_interface; + + if (ACPI_FAILURE(AMW0_set_capabilities())) { + pr_err("Unable to detect available AMW0 devices\n"); + return -ENODEV; + } + } + + if (wmi_has_guid(AMW0_GUID1)) + AMW0_find_mailled(); + + if (!interface) { + pr_err("No or unsupported WMI interface, unable to load\n"); + return -ENODEV; + } + + set_quirks(); + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) + interface->capability &= ~ACER_CAP_BRIGHTNESS; + + if (wmi_has_guid(WMID_GUID3)) + interface->capability |= ACER_CAP_SET_FUNCTION_MODE; + + if (force_caps != -1) + interface->capability = force_caps; + + if (wmi_has_guid(WMID_GUID3) && + (interface->capability & ACER_CAP_SET_FUNCTION_MODE)) { + if (ACPI_FAILURE(acer_wmi_enable_rf_button())) + pr_warn("Cannot enable RF Button Driver\n"); + + if (ec_raw_mode) { + if (ACPI_FAILURE(acer_wmi_enable_ec_raw())) { + pr_err("Cannot enable EC raw mode\n"); + return -ENODEV; + } + } else if (ACPI_FAILURE(acer_wmi_enable_lm())) { + pr_err("Cannot enable Launch Manager mode\n"); + return -ENODEV; + } + } else if (ec_raw_mode) { + pr_info("No WMID EC raw mode enable method\n"); + } + + if (wmi_has_guid(ACERWMID_EVENT_GUID)) { + err = acer_wmi_input_setup(); + if (err) + return err; + err = acer_wmi_accel_setup(); + if (err && err != -ENODEV) + pr_warn("Cannot enable accelerometer\n"); + } + + err = platform_driver_register(&acer_platform_driver); + if (err) { + pr_err("Unable to register platform driver\n"); + goto error_platform_register; + } + + acer_platform_device = platform_device_alloc("acer-wmi", PLATFORM_DEVID_NONE); + if (!acer_platform_device) { + err = -ENOMEM; + goto error_device_alloc; + } + + err = platform_device_add(acer_platform_device); + if (err) + goto error_device_add; + + if (wmi_has_guid(WMID_GUID2)) { + interface->debug.wmid_devices = get_wmid_devices(); + create_debugfs(); + } + + /* Override any initial settings with values from the commandline */ + acer_commandline_init(); + + return 0; + +error_device_add: + platform_device_put(acer_platform_device); +error_device_alloc: + platform_driver_unregister(&acer_platform_driver); +error_platform_register: + if (wmi_has_guid(ACERWMID_EVENT_GUID)) + acer_wmi_input_destroy(); + if (acer_wmi_accel_dev) + input_unregister_device(acer_wmi_accel_dev); + + return err; +} + +static void __exit acer_wmi_exit(void) +{ + if (wmi_has_guid(ACERWMID_EVENT_GUID)) + acer_wmi_input_destroy(); + + if (acer_wmi_accel_dev) + input_unregister_device(acer_wmi_accel_dev); + + remove_debugfs(); + platform_device_unregister(acer_platform_device); + platform_driver_unregister(&acer_platform_driver); + + pr_info("Acer Laptop WMI Extras unloaded\n"); +} + +module_init(acer_wmi_init); +module_exit(acer_wmi_exit); diff --git a/drivers/platform/x86/acerhdf.c b/drivers/platform/x86/acerhdf.c new file mode 100644 index 000000000..5ce5ad3ef --- /dev/null +++ b/drivers/platform/x86/acerhdf.c @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * acerhdf - A driver which monitors the temperature + * of the aspire one netbook, turns on/off the fan + * as soon as the upper/lower threshold is reached. + * + * (C) 2009 - Peter Kaestle peter (a) piie.net + * https://piie.net + * 2009 Borislav Petkov bp (a) alien8.de + * + * Inspired by and many thanks to: + * o acerfand - Rachel Greenham + * o acer_ec.pl - Michael Kurz michi.kurz (at) googlemail.com + * - Petr Tomasek tomasek (#) etf,cuni,cz + * - Carlos Corbacho cathectic (at) gmail.com + * o lkml - Matthew Garrett + * - Borislav Petkov + * - Andreas Mohr + */ + +#define pr_fmt(fmt) "acerhdf: " fmt + +#include +#include +#include +#include +#include +#include + +/* + * The driver is started with "kernel mode off" by default. That means, the BIOS + * is still in control of the fan. In this mode the driver allows to read the + * temperature of the cpu and a userspace tool may take over control of the fan. + * If the driver is switched to "kernel mode" (e.g. via module parameter) the + * driver is in full control of the fan. If you want the module to be started in + * kernel mode by default, define the following: + */ +#undef START_IN_KERNEL_MODE + +#define DRV_VER "0.7.0" + +/* + * According to the Atom N270 datasheet, + * (http://download.intel.com/design/processor/datashts/320032.pdf) the + * CPU's optimal operating limits denoted in junction temperature as + * measured by the on-die thermal monitor are within 0 <= Tj <= 90. So, + * assume 89°C is critical temperature. + */ +#define ACERHDF_DEFAULT_TEMP_FANON 60000 +#define ACERHDF_DEFAULT_TEMP_FANOFF 53000 +#define ACERHDF_TEMP_CRIT 89000 +#define ACERHDF_FAN_OFF 0 +#define ACERHDF_FAN_AUTO 1 + +/* + * No matter what value the user puts into the fanon variable, turn on the fan + * at 80 degree Celsius to prevent hardware damage + */ +#define ACERHDF_MAX_FANON 80000 + +/* + * Maximum interval between two temperature checks is 15 seconds, as the die + * can get hot really fast under heavy load (plus we shouldn't forget about + * possible impact of _external_ aggressive sources such as heaters, sun etc.) + */ +#define ACERHDF_MAX_INTERVAL 15 + +#ifdef START_IN_KERNEL_MODE +static int kernelmode = 1; +#else +static int kernelmode; +#endif + +static unsigned int interval = 10; +static unsigned int fanon = ACERHDF_DEFAULT_TEMP_FANON; +static unsigned int fanoff = ACERHDF_DEFAULT_TEMP_FANOFF; +static unsigned int verbose; +static unsigned int list_supported; +static unsigned int fanstate = ACERHDF_FAN_AUTO; +static char force_bios[16]; +static char force_product[16]; +static struct thermal_zone_device *thz_dev; +static struct thermal_cooling_device *cl_dev; +static struct platform_device *acerhdf_dev; + +module_param(kernelmode, uint, 0); +MODULE_PARM_DESC(kernelmode, "Kernel mode fan control on / off"); +module_param(fanon, uint, 0600); +MODULE_PARM_DESC(fanon, "Turn the fan on above this temperature"); +module_param(fanoff, uint, 0600); +MODULE_PARM_DESC(fanoff, "Turn the fan off below this temperature"); +module_param(verbose, uint, 0600); +MODULE_PARM_DESC(verbose, "Enable verbose dmesg output"); +module_param(list_supported, uint, 0600); +MODULE_PARM_DESC(list_supported, "List supported models and BIOS versions"); +module_param_string(force_bios, force_bios, 16, 0); +MODULE_PARM_DESC(force_bios, "Pretend system has this known supported BIOS version"); +module_param_string(force_product, force_product, 16, 0); +MODULE_PARM_DESC(force_product, "Pretend system is this known supported model"); + +/* + * cmd_off: to switch the fan completely off and check if the fan is off + * cmd_auto: to set the BIOS in control of the fan. The BIOS regulates then + * the fan speed depending on the temperature + */ +struct fancmd { + u8 cmd_off; + u8 cmd_auto; +}; + +struct manualcmd { + u8 mreg; + u8 moff; +}; + +/* default register and command to disable fan in manual mode */ +static const struct manualcmd mcmd = { + .mreg = 0x94, + .moff = 0xff, +}; + +/* BIOS settings - only used during probe */ +struct bios_settings { + const char *vendor; + const char *product; + const char *version; + u8 fanreg; + u8 tempreg; + struct fancmd cmd; + int mcmd_enable; +}; + +/* This could be a daughter struct in the above, but not worth the redirect */ +struct ctrl_settings { + u8 fanreg; + u8 tempreg; + struct fancmd cmd; + int mcmd_enable; +}; + +static struct thermal_trip trips[] = { + [0] = { .temperature = ACERHDF_DEFAULT_TEMP_FANON, + .hysteresis = ACERHDF_DEFAULT_TEMP_FANON - ACERHDF_DEFAULT_TEMP_FANOFF, + .type = THERMAL_TRIP_ACTIVE }, + + [1] = { .temperature = ACERHDF_TEMP_CRIT, + .type = THERMAL_TRIP_CRITICAL } +}; + +static struct ctrl_settings ctrl_cfg __read_mostly; + +/* Register addresses and values for different BIOS versions */ +static const struct bios_settings bios_tbl[] __initconst = { + /* AOA110 */ + {"Acer", "AOA110", "v0.3109", 0x55, 0x58, {0x1f, 0x00}, 0}, + {"Acer", "AOA110", "v0.3114", 0x55, 0x58, {0x1f, 0x00}, 0}, + {"Acer", "AOA110", "v0.3301", 0x55, 0x58, {0xaf, 0x00}, 0}, + {"Acer", "AOA110", "v0.3304", 0x55, 0x58, {0xaf, 0x00}, 0}, + {"Acer", "AOA110", "v0.3305", 0x55, 0x58, {0xaf, 0x00}, 0}, + {"Acer", "AOA110", "v0.3307", 0x55, 0x58, {0xaf, 0x00}, 0}, + {"Acer", "AOA110", "v0.3308", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Acer", "AOA110", "v0.3309", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Acer", "AOA110", "v0.3310", 0x55, 0x58, {0x21, 0x00}, 0}, + /* AOA150 */ + {"Acer", "AOA150", "v0.3114", 0x55, 0x58, {0x1f, 0x00}, 0}, + {"Acer", "AOA150", "v0.3301", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3304", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3305", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3307", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3308", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3309", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AOA150", "v0.3310", 0x55, 0x58, {0x20, 0x00}, 0}, + /* LT1005u */ + {"Acer", "LT-10Q", "v0.3310", 0x55, 0x58, {0x20, 0x00}, 0}, + /* Acer 1410 */ + {"Acer", "Aspire 1410", "v0.3108", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v0.3113", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v0.3115", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v0.3117", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v0.3119", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v0.3120", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v1.3204", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v1.3303", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v1.3308", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v1.3310", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1410", "v1.3314", 0x55, 0x58, {0x9e, 0x00}, 0}, + /* Acer 1810xx */ + {"Acer", "Aspire 1810TZ", "v0.3108", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3108", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v0.3113", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3113", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v0.3115", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3115", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v0.3117", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3117", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v0.3119", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3119", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v0.3120", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v0.3120", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v1.3204", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v1.3204", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v1.3303", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v1.3303", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v1.3308", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v1.3308", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v1.3310", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v1.3310", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810TZ", "v1.3314", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1810T", "v1.3314", 0x55, 0x58, {0x9e, 0x00}, 0}, + /* Acer 5755G */ + {"Acer", "Aspire 5755G", "V1.20", 0xab, 0xb4, {0x00, 0x08}, 0}, + {"Acer", "Aspire 5755G", "V1.21", 0xab, 0xb3, {0x00, 0x08}, 0}, + /* Acer 521 */ + {"Acer", "AO521", "V1.11", 0x55, 0x58, {0x1f, 0x00}, 0}, + /* Acer 531 */ + {"Acer", "AO531h", "v0.3104", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AO531h", "v0.3201", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Acer", "AO531h", "v0.3304", 0x55, 0x58, {0x20, 0x00}, 0}, + /* Acer 751 */ + {"Acer", "AO751h", "V0.3206", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Acer", "AO751h", "V0.3212", 0x55, 0x58, {0x21, 0x00}, 0}, + /* Acer 753 */ + {"Acer", "Aspire One 753", "V1.24", 0x93, 0xac, {0x14, 0x04}, 1}, + /* Acer 1825 */ + {"Acer", "Aspire 1825PTZ", "V1.3118", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Acer", "Aspire 1825PTZ", "V1.3127", 0x55, 0x58, {0x9e, 0x00}, 0}, + /* Acer Extensa 5420 */ + {"Acer", "Extensa 5420", "V1.17", 0x93, 0xac, {0x14, 0x04}, 1}, + /* Acer Aspire 5315 */ + {"Acer", "Aspire 5315", "V1.19", 0x93, 0xac, {0x14, 0x04}, 1}, + /* Acer Aspire 5739 */ + {"Acer", "Aspire 5739G", "V1.3311", 0x55, 0x58, {0x20, 0x00}, 0}, + /* Acer TravelMate 7730 */ + {"Acer", "TravelMate 7730G", "v0.3509", 0x55, 0x58, {0xaf, 0x00}, 0}, + /* Acer Aspire 7551 */ + {"Acer", "Aspire 7551", "V1.18", 0x93, 0xa8, {0x14, 0x04}, 1}, + /* Acer TravelMate TM8573T */ + {"Acer", "TM8573T", "V1.13", 0x93, 0xa8, {0x14, 0x04}, 1}, + /* Gateway */ + {"Gateway", "AOA110", "v0.3103", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Gateway", "AOA150", "v0.3103", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Gateway", "LT31", "v1.3103", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Gateway", "LT31", "v1.3201", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Gateway", "LT31", "v1.3302", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Gateway", "LT31", "v1.3303t", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Gateway", "LT31", "v1.3307", 0x55, 0x58, {0x9e, 0x00}, 0}, + /* Packard Bell */ + {"Packard Bell", "DOA150", "v0.3104", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Packard Bell", "DOA150", "v0.3105", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Packard Bell", "AOA110", "v0.3105", 0x55, 0x58, {0x21, 0x00}, 0}, + {"Packard Bell", "AOA150", "v0.3105", 0x55, 0x58, {0x20, 0x00}, 0}, + {"Packard Bell", "ENBFT", "V1.3118", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "ENBFT", "V1.3127", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v1.3303", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3120", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3108", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3113", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3115", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3117", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v0.3119", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMU", "v1.3204", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMA", "v1.3201", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMA", "v1.3302", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTMA", "v1.3303t", 0x55, 0x58, {0x9e, 0x00}, 0}, + {"Packard Bell", "DOTVR46", "v1.3308", 0x55, 0x58, {0x9e, 0x00}, 0}, + /* pewpew-terminator */ + {"", "", "", 0, 0, {0, 0}, 0} +}; + +/* + * this struct is used to instruct thermal layer to use bang_bang instead of + * default governor for acerhdf + */ +static const struct thermal_zone_params acerhdf_zone_params = { + .governor_name = "bang_bang", +}; + +static int acerhdf_get_temp(int *temp) +{ + u8 read_temp; + + if (ec_read(ctrl_cfg.tempreg, &read_temp)) + return -EINVAL; + + *temp = read_temp * 1000; + + return 0; +} + +static int acerhdf_get_fanstate(int *state) +{ + u8 fan; + + if (ec_read(ctrl_cfg.fanreg, &fan)) + return -EINVAL; + + if (fan != ctrl_cfg.cmd.cmd_off) + *state = ACERHDF_FAN_AUTO; + else + *state = ACERHDF_FAN_OFF; + + return 0; +} + +static void acerhdf_change_fanstate(int state) +{ + unsigned char cmd; + + if (verbose) + pr_notice("fan %s\n", state == ACERHDF_FAN_OFF ? "OFF" : "ON"); + + if ((state != ACERHDF_FAN_OFF) && (state != ACERHDF_FAN_AUTO)) { + pr_err("invalid fan state %d requested, setting to auto!\n", + state); + state = ACERHDF_FAN_AUTO; + } + + cmd = (state == ACERHDF_FAN_OFF) ? ctrl_cfg.cmd.cmd_off + : ctrl_cfg.cmd.cmd_auto; + fanstate = state; + + ec_write(ctrl_cfg.fanreg, cmd); + + if (ctrl_cfg.mcmd_enable && state == ACERHDF_FAN_OFF) { + if (verbose) + pr_notice("turning off fan manually\n"); + ec_write(mcmd.mreg, mcmd.moff); + } +} + +static void acerhdf_check_param(struct thermal_zone_device *thermal) +{ + if (fanon > ACERHDF_MAX_FANON) { + pr_err("fanon temperature too high, set to %d\n", + ACERHDF_MAX_FANON); + fanon = ACERHDF_MAX_FANON; + } + + if (fanon < fanoff) { + pr_err("fanoff temperature (%d) is above fanon temperature (%d), clamping to %d\n", + fanoff, fanon, fanon); + fanoff = fanon; + } + + trips[0].temperature = fanon; + trips[0].hysteresis = fanon - fanoff; + + if (kernelmode) { + if (interval > ACERHDF_MAX_INTERVAL) { + pr_err("interval too high, set to %d\n", + ACERHDF_MAX_INTERVAL); + interval = ACERHDF_MAX_INTERVAL; + } + + if (verbose) + pr_notice("interval changed to: %d\n", interval); + } +} + +/* + * This is the thermal zone callback which does the delayed polling of the fan + * state. We do check /sysfs-originating settings here in acerhdf_check_param() + * as late as the polling interval is since we can't do that in the respective + * accessors of the module parameters. + */ +static int acerhdf_get_ec_temp(struct thermal_zone_device *thermal, int *t) +{ + int temp, err = 0; + + err = acerhdf_get_temp(&temp); + if (err) + return err; + + if (verbose) + pr_notice("temp %d\n", temp); + + *t = temp; + return 0; +} + +static bool acerhdf_should_bind(struct thermal_zone_device *thermal, + const struct thermal_trip *trip, + struct thermal_cooling_device *cdev, + struct cooling_spec *c) +{ + /* if the cooling device is the one from acerhdf bind it */ + return cdev == cl_dev && trip->type == THERMAL_TRIP_ACTIVE; +} + +static inline void acerhdf_revert_to_bios_mode(void) +{ + acerhdf_change_fanstate(ACERHDF_FAN_AUTO); + kernelmode = 0; + + pr_notice("kernel mode fan control OFF\n"); +} +static inline void acerhdf_enable_kernelmode(void) +{ + kernelmode = 1; + + pr_notice("kernel mode fan control ON\n"); +} + +/* + * set operation mode; + * enabled: the thermal layer of the kernel takes care about + * the temperature and the fan. + * disabled: the BIOS takes control of the fan. + */ +static int acerhdf_change_mode(struct thermal_zone_device *thermal, + enum thermal_device_mode mode) +{ + if (mode == THERMAL_DEVICE_DISABLED && kernelmode) + acerhdf_revert_to_bios_mode(); + else if (mode == THERMAL_DEVICE_ENABLED && !kernelmode) + acerhdf_enable_kernelmode(); + + return 0; +} + +static int acerhdf_get_crit_temp(struct thermal_zone_device *thermal, + int *temperature) +{ + *temperature = ACERHDF_TEMP_CRIT; + return 0; +} + +/* bind callback functions to thermalzone */ +static const struct thermal_zone_device_ops acerhdf_dev_ops = { + .should_bind = acerhdf_should_bind, + .get_temp = acerhdf_get_ec_temp, + .change_mode = acerhdf_change_mode, + .get_crit_temp = acerhdf_get_crit_temp, +}; + +/* + * cooling device callback functions + * get maximal fan cooling state + */ +static int acerhdf_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + *state = 1; + + return 0; +} + +static int acerhdf_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + int err = 0, tmp; + + err = acerhdf_get_fanstate(&tmp); + if (err) + return err; + + *state = (tmp == ACERHDF_FAN_AUTO) ? 1 : 0; + return 0; +} + +/* change current fan state - is overwritten when running in kernel mode */ +static int acerhdf_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + int cur_temp, cur_state, err = 0; + + if (!kernelmode) + return 0; + + err = acerhdf_get_temp(&cur_temp); + if (err) { + pr_err("error reading temperature, hand off control to BIOS\n"); + goto err_out; + } + + err = acerhdf_get_fanstate(&cur_state); + if (err) { + pr_err("error reading fan state, hand off control to BIOS\n"); + goto err_out; + } + + if (state == 0) { + if (cur_state == ACERHDF_FAN_AUTO) + acerhdf_change_fanstate(ACERHDF_FAN_OFF); + } else { + if (cur_state == ACERHDF_FAN_OFF) + acerhdf_change_fanstate(ACERHDF_FAN_AUTO); + } + return 0; + +err_out: + acerhdf_revert_to_bios_mode(); + return -EINVAL; +} + +/* bind fan callbacks to fan device */ +static const struct thermal_cooling_device_ops acerhdf_cooling_ops = { + .get_max_state = acerhdf_get_max_state, + .get_cur_state = acerhdf_get_cur_state, + .set_cur_state = acerhdf_set_cur_state, +}; + +/* suspend / resume functionality */ +static int acerhdf_suspend(struct device *dev) +{ + if (kernelmode) + acerhdf_change_fanstate(ACERHDF_FAN_AUTO); + + if (verbose) + pr_notice("going suspend\n"); + + return 0; +} + +static int acerhdf_probe(struct platform_device *device) +{ + return 0; +} + +static const struct dev_pm_ops acerhdf_pm_ops = { + .suspend = acerhdf_suspend, + .freeze = acerhdf_suspend, +}; + +static struct platform_driver acerhdf_driver = { + .driver = { + .name = "acerhdf", + .pm = &acerhdf_pm_ops, + }, + .probe = acerhdf_probe, +}; + +/* check hardware */ +static int __init acerhdf_check_hardware(void) +{ + char const *vendor, *version, *product; + const struct bios_settings *bt = NULL; + int found = 0; + + /* get BIOS data */ + vendor = dmi_get_system_info(DMI_SYS_VENDOR); + version = dmi_get_system_info(DMI_BIOS_VERSION); + product = dmi_get_system_info(DMI_PRODUCT_NAME); + + if (!vendor || !version || !product) { + pr_err("error getting hardware information\n"); + return -EINVAL; + } + + pr_info("Acer Aspire One Fan driver, v.%s\n", DRV_VER); + + if (list_supported) { + pr_info("List of supported Manufacturer/Model/BIOS:\n"); + pr_info("---------------------------------------------------\n"); + for (bt = bios_tbl; bt->vendor[0]; bt++) { + pr_info("%-13s | %-17s | %-10s\n", bt->vendor, + bt->product, bt->version); + } + pr_info("---------------------------------------------------\n"); + return -ECANCELED; + } + + if (force_bios[0]) { + version = force_bios; + pr_info("forcing BIOS version: %s\n", version); + kernelmode = 0; + } + + if (force_product[0]) { + product = force_product; + pr_info("forcing BIOS product: %s\n", product); + kernelmode = 0; + } + + if (verbose) + pr_info("BIOS info: %s %s, product: %s\n", + vendor, version, product); + + /* search BIOS version and vendor in BIOS settings table */ + for (bt = bios_tbl; bt->vendor[0]; bt++) { + /* + * check if actual hardware BIOS vendor, product and version + * IDs start with the strings of BIOS table entry + */ + if (strstarts(vendor, bt->vendor) && + strstarts(product, bt->product) && + strstarts(version, bt->version)) { + found = 1; + break; + } + } + + if (!found) { + pr_err("unknown (unsupported) BIOS version %s/%s/%s, please report, aborting!\n", + vendor, product, version); + return -EINVAL; + } + + /* Copy control settings from BIOS table before we free it. */ + ctrl_cfg.fanreg = bt->fanreg; + ctrl_cfg.tempreg = bt->tempreg; + memcpy(&ctrl_cfg.cmd, &bt->cmd, sizeof(struct fancmd)); + ctrl_cfg.mcmd_enable = bt->mcmd_enable; + + /* + * if started with kernel mode off, prevent the kernel from switching + * off the fan + */ + if (!kernelmode) { + pr_notice("Fan control off, to enable do:\n"); + pr_notice("echo -n \"enabled\" > /sys/class/thermal/thermal_zoneN/mode # N=0,1,2...\n"); + } + + return 0; +} + +static int __init acerhdf_register_platform(void) +{ + int err = 0; + + err = platform_driver_register(&acerhdf_driver); + if (err) + return err; + + acerhdf_dev = platform_device_alloc("acerhdf", PLATFORM_DEVID_NONE); + if (!acerhdf_dev) { + err = -ENOMEM; + goto err_device_alloc; + } + err = platform_device_add(acerhdf_dev); + if (err) + goto err_device_add; + + return 0; + +err_device_add: + platform_device_put(acerhdf_dev); +err_device_alloc: + platform_driver_unregister(&acerhdf_driver); + return err; +} + +static void acerhdf_unregister_platform(void) +{ + platform_device_unregister(acerhdf_dev); + platform_driver_unregister(&acerhdf_driver); +} + +static int __init acerhdf_register_thermal(void) +{ + int ret; + + cl_dev = thermal_cooling_device_register("acerhdf-fan", NULL, + &acerhdf_cooling_ops); + + if (IS_ERR(cl_dev)) + return -EINVAL; + + thz_dev = thermal_zone_device_register_with_trips("acerhdf", trips, ARRAY_SIZE(trips), + NULL, &acerhdf_dev_ops, + &acerhdf_zone_params, 0, + (kernelmode) ? interval*1000 : 0); + if (IS_ERR(thz_dev)) + return -EINVAL; + + if (kernelmode) + ret = thermal_zone_device_enable(thz_dev); + else + ret = thermal_zone_device_disable(thz_dev); + if (ret) + return ret; + + return 0; +} + +static void acerhdf_unregister_thermal(void) +{ + if (cl_dev) { + thermal_cooling_device_unregister(cl_dev); + cl_dev = NULL; + } + + if (thz_dev) { + thermal_zone_device_unregister(thz_dev); + thz_dev = NULL; + } +} + +static int __init acerhdf_init(void) +{ + int err = 0; + + err = acerhdf_check_hardware(); + if (err) + goto out_err; + + err = acerhdf_register_platform(); + if (err) + goto out_err; + + err = acerhdf_register_thermal(); + if (err) + goto err_unreg; + + return 0; + +err_unreg: + acerhdf_unregister_thermal(); + acerhdf_unregister_platform(); + +out_err: + return err; +} + +static void __exit acerhdf_exit(void) +{ + acerhdf_change_fanstate(ACERHDF_FAN_AUTO); + acerhdf_unregister_thermal(); + acerhdf_unregister_platform(); +} + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Peter Kaestle"); +MODULE_DESCRIPTION("Aspire One temperature and fan driver"); +MODULE_ALIAS("dmi:*:*Acer*:pnAOA*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAO751h*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*1410*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*1810*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*5755G:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*1825PTZ:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAO521*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAO531*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*5739G:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*One*753:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*5315:"); +MODULE_ALIAS("dmi:*:*Acer*:TravelMate*7730G:"); +MODULE_ALIAS("dmi:*:*Acer*:pnAspire*7551:"); +MODULE_ALIAS("dmi:*:*Acer*:TM8573T:"); +MODULE_ALIAS("dmi:*:*Gateway*:pnAOA*:"); +MODULE_ALIAS("dmi:*:*Gateway*:pnLT31*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnAOA*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnDOA*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnDOTMU*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnENBFT*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnDOTMA*:"); +MODULE_ALIAS("dmi:*:*Packard*Bell*:pnDOTVR46*:"); +MODULE_ALIAS("dmi:*:*Acer*:pnExtensa*5420*:"); + +module_init(acerhdf_init); +module_exit(acerhdf_exit); + +static int interval_set_uint(const char *val, const struct kernel_param *kp) +{ + int ret; + + ret = param_set_uint(val, kp); + if (ret) + return ret; + + acerhdf_check_param(thz_dev); + + return 0; +} + +static const struct kernel_param_ops interval_ops = { + .set = interval_set_uint, + .get = param_get_uint, +}; + +module_param_cb(interval, &interval_ops, &interval, 0000); +MODULE_PARM_DESC(interval, "Polling interval of temperature check"); diff --git a/drivers/platform/x86/adv_swbutton.c b/drivers/platform/x86/adv_swbutton.c new file mode 100644 index 000000000..8f7a26e6d --- /dev/null +++ b/drivers/platform/x86/adv_swbutton.c @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * adv_swbutton.c - Software Button Interface Driver. + * + * (C) Copyright 2020 Advantech Corporation, Inc + * + */ +#include +#include +#include +#include +#include + +#define ACPI_BUTTON_HID_SWBTN "AHC0310" + +#define ACPI_BUTTON_NOTIFY_SWBTN_RELEASE 0x86 +#define ACPI_BUTTON_NOTIFY_SWBTN_PRESSED 0x85 + +struct adv_swbutton { + struct input_dev *input; + char phys[32]; +}; + +/*------------------------------------------------------------------------- + * Driver Interface + *-------------------------------------------------------------------------- + */ +static void adv_swbutton_notify(acpi_handle handle, u32 event, void *context) +{ + struct platform_device *device = context; + struct adv_swbutton *button = dev_get_drvdata(&device->dev); + + switch (event) { + case ACPI_BUTTON_NOTIFY_SWBTN_RELEASE: + input_report_key(button->input, KEY_PROG1, 0); + input_sync(button->input); + break; + case ACPI_BUTTON_NOTIFY_SWBTN_PRESSED: + input_report_key(button->input, KEY_PROG1, 1); + input_sync(button->input); + break; + default: + dev_dbg(&device->dev, "Unsupported event [0x%x]\n", event); + } +} + +static int adv_swbutton_probe(struct platform_device *device) +{ + struct adv_swbutton *button; + struct input_dev *input; + acpi_handle handle; + acpi_status status; + int error; + + handle = ACPI_HANDLE(&device->dev); + if (!handle) + return -ENODEV; + + button = devm_kzalloc(&device->dev, sizeof(*button), GFP_KERNEL); + if (!button) + return -ENOMEM; + + dev_set_drvdata(&device->dev, button); + + input = devm_input_allocate_device(&device->dev); + if (!input) + return -ENOMEM; + + button->input = input; + snprintf(button->phys, sizeof(button->phys), "%s/button/input0", ACPI_BUTTON_HID_SWBTN); + + input->name = "Advantech Software Button"; + input->phys = button->phys; + input->id.bustype = BUS_HOST; + input->dev.parent = &device->dev; + set_bit(EV_REP, input->evbit); + input_set_capability(input, EV_KEY, KEY_PROG1); + + error = input_register_device(input); + if (error) + return error; + + device_init_wakeup(&device->dev, true); + + status = acpi_install_notify_handler(handle, + ACPI_DEVICE_NOTIFY, + adv_swbutton_notify, + device); + if (ACPI_FAILURE(status)) { + dev_err(&device->dev, "Error installing notify handler\n"); + return -EIO; + } + + return 0; +} + +static void adv_swbutton_remove(struct platform_device *device) +{ + acpi_handle handle = ACPI_HANDLE(&device->dev); + + acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, + adv_swbutton_notify); +} + +static const struct acpi_device_id button_device_ids[] = { + {ACPI_BUTTON_HID_SWBTN, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, button_device_ids); + +static struct platform_driver adv_swbutton_driver = { + .driver = { + .name = "adv_swbutton", + .acpi_match_table = button_device_ids, + }, + .probe = adv_swbutton_probe, + .remove = adv_swbutton_remove, +}; +module_platform_driver(adv_swbutton_driver); + +MODULE_AUTHOR("Andrea Ho"); +MODULE_DESCRIPTION("Advantech ACPI SW Button Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig new file mode 100644 index 000000000..b813f9265 --- /dev/null +++ b/drivers/platform/x86/amd/Kconfig @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# AMD x86 Platform Specific Drivers +# + +source "drivers/platform/x86/amd/hsmp/Kconfig" +source "drivers/platform/x86/amd/pmf/Kconfig" +source "drivers/platform/x86/amd/pmc/Kconfig" +source "drivers/platform/x86/amd/hfi/Kconfig" + +config AMD_3D_VCACHE + tristate "AMD 3D V-Cache Performance Optimizer Driver" + depends on X86_64 && ACPI + help + The driver provides a sysfs interface, enabling the setting of a bias + that alters CPU core reordering. This bias prefers cores with higher + frequencies or larger L3 caches on processors supporting AMD 3D V-Cache + technology. + + If you choose to compile this driver as a module the module will be + called amd_3d_vcache. + +config AMD_WBRF + bool "AMD Wifi RF Band mitigations (WBRF)" + depends on ACPI + help + WBRF(Wifi Band RFI mitigation) mechanism allows Wifi drivers + to notify the frequencies they are using so that other hardware + can be reconfigured to avoid harmonic conflicts. + + AMD provides an ACPI based mechanism to support WBRF on platform with + appropriate underlying support. + + This mechanism will only be activated on platforms that advertise a + need for it. + +config AMD_ISP_PLATFORM + tristate "AMD ISP4 platform driver" + depends on I2C && X86_64 && ACPI + help + Platform driver for AMD platforms containing image signal processor + gen 4. Provides camera sensor module board information to allow + sensor and V4L drivers to work properly. + + This driver can also be built as a module. If so, the module + will be called amd_isp4. diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile new file mode 100644 index 000000000..f6ff0c837 --- /dev/null +++ b/drivers/platform/x86/amd/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for drivers/platform/x86/amd +# AMD x86 Platform-Specific Drivers +# + +obj-$(CONFIG_AMD_3D_VCACHE) += amd_3d_vcache.o +amd_3d_vcache-y := x3d_vcache.o +obj-$(CONFIG_AMD_PMC) += pmc/ +obj-$(CONFIG_AMD_HSMP) += hsmp/ +obj-$(CONFIG_AMD_PMF) += pmf/ +obj-$(CONFIG_AMD_WBRF) += wbrf.o +obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o +obj-$(CONFIG_AMD_HFI) += hfi/ diff --git a/drivers/platform/x86/amd/amd_isp4.c b/drivers/platform/x86/amd/amd_isp4.c new file mode 100644 index 000000000..0d4948995 --- /dev/null +++ b/drivers/platform/x86/amd/amd_isp4.c @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * AMD ISP platform driver for sensor i2-client instantiation + * + * Copyright 2025 Advanced Micro Devices, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AMDISP_OV05C10_I2C_ADDR 0x10 +#define AMDISP_OV05C10_HID "OMNI5C10" +#define AMDISP_OV05C10_REMOTE_EP_NAME "ov05c10_isp_4_1_1" +#define AMD_ISP_PLAT_DRV_NAME "amd-isp4" + +static const struct software_node isp4_mipi1_endpoint_node; +static const struct software_node ov05c10_endpoint_node; + +/* + * AMD ISP platform info definition to initialize sensor + * specific platform configuration to prepare the amdisp + * platform. + */ +struct amdisp_platform_info { + struct i2c_board_info board_info; + const struct software_node **swnodes; +}; + +/* + * AMD ISP platform definition to configure the device properties + * missing in the ACPI table. + */ +struct amdisp_platform { + const struct amdisp_platform_info *pinfo; + struct i2c_board_info board_info; + struct notifier_block i2c_nb; + struct i2c_client *i2c_dev; + struct mutex lock; /* protects i2c client creation */ +}; + +/* Root AMD CAMERA SWNODE */ + +/* Root amd camera node definition */ +static const struct software_node amd_camera_node = { + .name = "amd_camera", +}; + +/* ISP4 SWNODE */ + +/* ISP4 OV05C10 camera node definition */ +static const struct software_node isp4_node = { + .name = "isp4", + .parent = &amd_camera_node, +}; + +/* + * ISP4 Ports node definition. No properties defined for + * ports node. + */ +static const struct software_node isp4_ports = { + .name = "ports", + .parent = &isp4_node, +}; + +/* + * ISP4 Port node definition. No properties defined for + * port node. + */ +static const struct software_node isp4_port_node = { + .name = "port@0", + .parent = &isp4_ports, +}; + +/* + * ISP4 MIPI1 remote endpoint points to OV05C10 endpoint + * node. + */ +static const struct software_node_ref_args isp4_refs[] = { + SOFTWARE_NODE_REFERENCE(&ov05c10_endpoint_node), +}; + +/* ISP4 MIPI1 endpoint node properties table */ +static const struct property_entry isp4_mipi1_endpoint_props[] = { + PROPERTY_ENTRY_REF_ARRAY("remote-endpoint", isp4_refs), + { } +}; + +/* ISP4 MIPI1 endpoint node definition */ +static const struct software_node isp4_mipi1_endpoint_node = { + .name = "endpoint", + .parent = &isp4_port_node, + .properties = isp4_mipi1_endpoint_props, +}; + +/* I2C1 SWNODE */ + +/* I2C1 camera node property table */ +static const struct property_entry i2c1_camera_props[] = { + PROPERTY_ENTRY_U32("clock-frequency", 1 * HZ_PER_MHZ), + { } +}; + +/* I2C1 camera node definition */ +static const struct software_node i2c1_node = { + .name = "i2c1", + .parent = &amd_camera_node, + .properties = i2c1_camera_props, +}; + +/* I2C1 camera node property table */ +static const struct property_entry ov05c10_camera_props[] = { + PROPERTY_ENTRY_U32("clock-frequency", 24 * HZ_PER_MHZ), + { } +}; + +/* OV05C10 camera node definition */ +static const struct software_node ov05c10_camera_node = { + .name = AMDISP_OV05C10_HID, + .parent = &i2c1_node, + .properties = ov05c10_camera_props, +}; + +/* + * OV05C10 Ports node definition. No properties defined for + * ports node for OV05C10. + */ +static const struct software_node ov05c10_ports = { + .name = "ports", + .parent = &ov05c10_camera_node, +}; + +/* + * OV05C10 Port node definition. + */ +static const struct software_node ov05c10_port_node = { + .name = "port@0", + .parent = &ov05c10_ports, +}; + +/* + * OV05C10 remote endpoint points to ISP4 MIPI1 endpoint + * node. + */ +static const struct software_node_ref_args ov05c10_refs[] = { + SOFTWARE_NODE_REFERENCE(&isp4_mipi1_endpoint_node), +}; + +/* OV05C10 supports one single link frequency */ +static const u64 ov05c10_link_freqs[] = { + 900 * HZ_PER_MHZ, +}; + +/* OV05C10 supports only 2-lane configuration */ +static const u32 ov05c10_data_lanes[] = { + 1, + 2, +}; + +/* OV05C10 endpoint node properties table */ +static const struct property_entry ov05c10_endpoint_props[] = { + PROPERTY_ENTRY_U32("bus-type", 4), + PROPERTY_ENTRY_U32_ARRAY_LEN("data-lanes", ov05c10_data_lanes, + ARRAY_SIZE(ov05c10_data_lanes)), + PROPERTY_ENTRY_U64_ARRAY_LEN("link-frequencies", ov05c10_link_freqs, + ARRAY_SIZE(ov05c10_link_freqs)), + PROPERTY_ENTRY_REF_ARRAY("remote-endpoint", ov05c10_refs), + { } +}; + +/* OV05C10 endpoint node definition */ +static const struct software_node ov05c10_endpoint_node = { + .name = "endpoint", + .parent = &ov05c10_port_node, + .properties = ov05c10_endpoint_props, +}; + +/* + * AMD Camera swnode graph uses 10 nodes and also its relationship is + * fixed to align with the structure that v4l2 and i2c frameworks expects + * for successful parsing of fwnodes and its properties with standard names. + * + * It is only the node property_entries that will vary for each platform + * supporting different sensor modules. + * + * AMD ISP4 SWNODE GRAPH Structure + * + * amd_camera { + * isp4 { + * ports { + * port@0 { + * isp4_mipi1_ep: endpoint { + * remote-endpoint = &OMNI5C10_ep; + * }; + * }; + * }; + * }; + * + * i2c1 { + * clock-frequency = 1 MHz; + * OMNI5C10 { + * clock-frequency = 24MHz; + * ports { + * port@0 { + * OMNI5C10_ep: endpoint { + * bus-type = 4; + * data-lanes = <1 2>; + * link-frequencies = 900MHz; + * remote-endpoint = &isp4_mipi1; + * }; + * }; + * }; + * }; + * }; + * }; + * + */ +static const struct software_node *amd_isp4_nodes[] = { + &amd_camera_node, + &isp4_node, + &isp4_ports, + &isp4_port_node, + &isp4_mipi1_endpoint_node, + &i2c1_node, + &ov05c10_camera_node, + &ov05c10_ports, + &ov05c10_port_node, + &ov05c10_endpoint_node, + NULL +}; + +/* OV05C10 specific AMD ISP platform configuration */ +static const struct amdisp_platform_info ov05c10_platform_config = { + .board_info = { + .dev_name = "ov05c10", + I2C_BOARD_INFO("ov05c10", AMDISP_OV05C10_I2C_ADDR), + }, + .swnodes = amd_isp4_nodes, +}; + +static const struct acpi_device_id amdisp_sensor_ids[] = { + { AMDISP_OV05C10_HID, (kernel_ulong_t)&ov05c10_platform_config }, + { } +}; +MODULE_DEVICE_TABLE(acpi, amdisp_sensor_ids); + +static inline bool is_isp_i2c_adapter(struct i2c_adapter *adap) +{ + return !strcmp(adap->name, AMDISP_I2C_ADAP_NAME); +} + +static void instantiate_isp_i2c_client(struct amdisp_platform *isp4_platform, + struct i2c_adapter *adap) +{ + struct i2c_board_info *info = &isp4_platform->board_info; + struct i2c_client *i2c_dev; + + guard(mutex)(&isp4_platform->lock); + + if (isp4_platform->i2c_dev) + return; + + i2c_dev = i2c_new_client_device(adap, info); + if (IS_ERR(i2c_dev)) { + dev_err(&adap->dev, "error %pe registering isp i2c_client\n", i2c_dev); + return; + } + isp4_platform->i2c_dev = i2c_dev; +} + +static int isp_i2c_bus_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct amdisp_platform *isp4_platform = + container_of(nb, struct amdisp_platform, i2c_nb); + struct device *dev = data; + struct i2c_client *client; + struct i2c_adapter *adap; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + adap = i2c_verify_adapter(dev); + if (!adap) + break; + if (is_isp_i2c_adapter(adap)) + instantiate_isp_i2c_client(isp4_platform, adap); + break; + case BUS_NOTIFY_REMOVED_DEVICE: + client = i2c_verify_client(dev); + if (!client) + break; + + scoped_guard(mutex, &isp4_platform->lock) { + if (isp4_platform->i2c_dev == client) { + dev_dbg(&client->adapter->dev, "amdisp i2c_client removed\n"); + isp4_platform->i2c_dev = NULL; + } + } + break; + default: + break; + } + + return NOTIFY_DONE; +} + +static struct amdisp_platform *prepare_amdisp_platform(struct device *dev, + const struct amdisp_platform_info *src) +{ + struct amdisp_platform *isp4_platform; + int ret; + + isp4_platform = devm_kzalloc(dev, sizeof(*isp4_platform), GFP_KERNEL); + if (!isp4_platform) + return ERR_PTR(-ENOMEM); + + ret = devm_mutex_init(dev, &isp4_platform->lock); + if (ret) + return ERR_PTR(ret); + + isp4_platform->board_info.dev_name = src->board_info.dev_name; + strscpy(isp4_platform->board_info.type, src->board_info.type); + isp4_platform->board_info.addr = src->board_info.addr; + isp4_platform->pinfo = src; + + ret = software_node_register_node_group(src->swnodes); + if (ret) + return ERR_PTR(ret); + + /* initialize ov05c10_camera_node */ + isp4_platform->board_info.swnode = src->swnodes[6]; + + return isp4_platform; +} + +static int try_to_instantiate_i2c_client(struct device *dev, void *data) +{ + struct i2c_adapter *adap = i2c_verify_adapter(dev); + struct amdisp_platform *isp4_platform = data; + + if (!isp4_platform || !adap) + return 0; + if (!adap->owner) + return 0; + + if (is_isp_i2c_adapter(adap)) + instantiate_isp_i2c_client(isp4_platform, adap); + + return 0; +} + +static int amd_isp_probe(struct platform_device *pdev) +{ + const struct amdisp_platform_info *pinfo; + struct amdisp_platform *isp4_platform; + struct acpi_device *adev; + int ret; + + pinfo = device_get_match_data(&pdev->dev); + if (!pinfo) + return dev_err_probe(&pdev->dev, -EINVAL, + "failed to get valid ACPI data\n"); + + isp4_platform = prepare_amdisp_platform(&pdev->dev, pinfo); + if (IS_ERR(isp4_platform)) + return dev_err_probe(&pdev->dev, PTR_ERR(isp4_platform), + "failed to prepare AMD ISP platform fwnode\n"); + + isp4_platform->i2c_nb.notifier_call = isp_i2c_bus_notify; + ret = bus_register_notifier(&i2c_bus_type, &isp4_platform->i2c_nb); + if (ret) + goto error_unregister_sw_node; + + adev = ACPI_COMPANION(&pdev->dev); + /* initialize root amd_camera_node */ + adev->driver_data = (void *)pinfo->swnodes[0]; + + /* check if adapter is already registered and create i2c client instance */ + i2c_for_each_dev(isp4_platform, try_to_instantiate_i2c_client); + + platform_set_drvdata(pdev, isp4_platform); + return 0; + +error_unregister_sw_node: + software_node_unregister_node_group(isp4_platform->pinfo->swnodes); + return ret; +} + +static void amd_isp_remove(struct platform_device *pdev) +{ + struct amdisp_platform *isp4_platform = platform_get_drvdata(pdev); + + bus_unregister_notifier(&i2c_bus_type, &isp4_platform->i2c_nb); + i2c_unregister_device(isp4_platform->i2c_dev); + software_node_unregister_node_group(isp4_platform->pinfo->swnodes); +} + +static struct platform_driver amd_isp_platform_driver = { + .driver = { + .name = AMD_ISP_PLAT_DRV_NAME, + .acpi_match_table = amdisp_sensor_ids, + }, + .probe = amd_isp_probe, + .remove = amd_isp_remove, +}; + +module_platform_driver(amd_isp_platform_driver); + +MODULE_AUTHOR("Benjamin Chan "); +MODULE_AUTHOR("Pratap Nirujogi "); +MODULE_DESCRIPTION("AMD ISP4 Platform Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/amd/hfi/Kconfig b/drivers/platform/x86/amd/hfi/Kconfig new file mode 100644 index 000000000..fecef6848 --- /dev/null +++ b/drivers/platform/x86/amd/hfi/Kconfig @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# AMD Hardware Feedback Interface Driver +# + +config AMD_HFI + bool "AMD Hetero Core Hardware Feedback Driver" + depends on ACPI + depends on CPU_SUP_AMD + depends on SCHED_MC_PRIO + help + Select this option to enable the AMD Heterogeneous Core Hardware + Feedback Interface. If selected, hardware provides runtime thread + classification guidance to the operating system on the performance and + energy efficiency capabilities of each heterogeneous CPU core. These + capabilities may vary due to the inherent differences in the core types + and can also change as a result of variations in the operating + conditions of the system such as power and thermal limits. diff --git a/drivers/platform/x86/amd/hfi/Makefile b/drivers/platform/x86/amd/hfi/Makefile new file mode 100644 index 000000000..672c6ac10 --- /dev/null +++ b/drivers/platform/x86/amd/hfi/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# AMD Hardware Feedback Interface Driver +# + +obj-$(CONFIG_AMD_HFI) += amd_hfi.o +amd_hfi-objs := hfi.o diff --git a/drivers/platform/x86/amd/hfi/hfi.c b/drivers/platform/x86/amd/hfi/hfi.c new file mode 100644 index 000000000..e0ebcb0c4 --- /dev/null +++ b/drivers/platform/x86/amd/hfi/hfi.c @@ -0,0 +1,548 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD Hardware Feedback Interface Driver + * + * Copyright (C) 2025 Advanced Micro Devices, Inc. All Rights Reserved. + * + * Authors: Perry Yuan + * Mario Limonciello + */ + +#define pr_fmt(fmt) "amd-hfi: " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#define AMD_HFI_DRIVER "amd_hfi" +#define AMD_HFI_MAILBOX_COUNT 1 +#define AMD_HETERO_RANKING_TABLE_MIN_VER 2 +#define AMD_HETERO_RANKING_TABLE_MAX_VER 3 + +#define AMD_HETERO_CPUID_27 0x80000027 + +static struct platform_device *device; + +/** + * struct amd_shmem_info - Shared memory table for AMD HFI + * + * @header: The PCCT table header including signature, length flags and command. + * @version_number: Version number of the table + * @n_logical_processors: Number of logical processors + * @n_capabilities: Number of ranking dimensions (performance, efficiency, etc) + * @table_update_context: Command being sent over the subspace + * @n_bitmaps: Number of 32-bit bitmaps to enumerate all the APIC IDs + * This is based on the maximum APIC ID enumerated in the system + * @reserved: 24 bit spare + * @table_data: Bit Map(s) of enabled logical processors + * Followed by the ranking data for each logical processor + */ +struct amd_shmem_info { + struct acpi_pcct_ext_pcc_shared_memory header; + u32 version_number :8, + n_logical_processors :8, + n_capabilities :8, + table_update_context :8; + u32 n_bitmaps :8, + reserved :24; + u32 table_data[]; +}; + +struct amd_hfi_data { + const char *name; + struct device *dev; + + /* PCCT table related */ + struct pcc_mbox_chan *pcc_chan; + void __iomem *pcc_comm_addr; + struct acpi_subtable_header *pcct_entry; + struct amd_shmem_info *shmem; + + struct dentry *dbgfs_dir; +}; + +/** + * struct amd_hfi_classes - HFI class capabilities per CPU + * @perf: Performance capability + * @eff: Power efficiency capability + * + * Capabilities of a logical processor in the ranking table. These capabilities + * are unitless and specific to each HFI class. + */ +struct amd_hfi_classes { + u32 perf; + u32 eff; +}; + +/** + * struct amd_hfi_cpuinfo - HFI workload class info per CPU + * @cpu: CPU index + * @apic_id: APIC id of the current CPU + * @class_index: workload class ID index + * @nr_class: max number of workload class supported + * @ipcc_scores: ipcc scores for each class + * @amd_hfi_classes: current CPU workload class ranking data + * + * Parameters of a logical processor linked with hardware feedback class. + */ +struct amd_hfi_cpuinfo { + int cpu; + u32 apic_id; + s16 class_index; + u8 nr_class; + int *ipcc_scores; + struct amd_hfi_classes *amd_hfi_classes; +}; + +static DEFINE_PER_CPU(struct amd_hfi_cpuinfo, amd_hfi_cpuinfo) = {.class_index = -1}; + +static DEFINE_MUTEX(hfi_cpuinfo_lock); + +static void amd_hfi_sched_itmt_work(struct work_struct *work) +{ + sched_set_itmt_support(); +} +static DECLARE_WORK(sched_amd_hfi_itmt_work, amd_hfi_sched_itmt_work); + +static int find_cpu_index_by_apicid(unsigned int target_apicid) +{ + int cpu_index; + + for_each_possible_cpu(cpu_index) { + struct cpuinfo_x86 *info = &cpu_data(cpu_index); + + if (info->topo.apicid == target_apicid) { + pr_debug("match APIC id %u for CPU index: %d\n", + info->topo.apicid, cpu_index); + return cpu_index; + } + } + + return -ENODEV; +} + +static int amd_hfi_fill_metadata(struct amd_hfi_data *amd_hfi_data) +{ + struct acpi_pcct_ext_pcc_slave *pcct_ext = + (struct acpi_pcct_ext_pcc_slave *)amd_hfi_data->pcct_entry; + void __iomem *pcc_comm_addr; + u32 apic_start = 0; + + pcc_comm_addr = acpi_os_ioremap(amd_hfi_data->pcc_chan->shmem_base_addr, + amd_hfi_data->pcc_chan->shmem_size); + if (!pcc_comm_addr) { + dev_err(amd_hfi_data->dev, "failed to ioremap PCC common region mem\n"); + return -ENOMEM; + } + + memcpy_fromio(amd_hfi_data->shmem, pcc_comm_addr, pcct_ext->length); + iounmap(pcc_comm_addr); + + if (amd_hfi_data->shmem->header.signature != PCC_SIGNATURE) { + dev_err(amd_hfi_data->dev, "invalid signature in shared memory\n"); + return -EINVAL; + } + if (amd_hfi_data->shmem->version_number < AMD_HETERO_RANKING_TABLE_MIN_VER || + amd_hfi_data->shmem->version_number > AMD_HETERO_RANKING_TABLE_MAX_VER) { + dev_err(amd_hfi_data->dev, "invalid version %d\n", + amd_hfi_data->shmem->version_number); + return -EINVAL; + } + + for (unsigned int i = 0; i < amd_hfi_data->shmem->n_bitmaps; i++) { + u32 bitmap = amd_hfi_data->shmem->table_data[i]; + + for (unsigned int j = 0; j < BITS_PER_TYPE(u32); j++) { + u32 apic_id = i * BITS_PER_TYPE(u32) + j; + struct amd_hfi_cpuinfo *info; + int cpu_index, apic_index; + + if (!(bitmap & BIT(j))) + continue; + + cpu_index = find_cpu_index_by_apicid(apic_id); + if (cpu_index < 0) { + dev_warn(amd_hfi_data->dev, "APIC ID %u not found\n", apic_id); + continue; + } + + info = per_cpu_ptr(&amd_hfi_cpuinfo, cpu_index); + info->apic_id = apic_id; + + /* Fill the ranking data for each logical processor */ + info = per_cpu_ptr(&amd_hfi_cpuinfo, cpu_index); + apic_index = apic_start * info->nr_class * 2; + for (unsigned int k = 0; k < info->nr_class; k++) { + u32 *table = amd_hfi_data->shmem->table_data + + amd_hfi_data->shmem->n_bitmaps + + i * info->nr_class; + + info->amd_hfi_classes[k].eff = table[apic_index + 2 * k]; + info->amd_hfi_classes[k].perf = table[apic_index + 2 * k + 1]; + } + apic_start++; + } + } + + return 0; +} + +static int amd_hfi_alloc_class_data(struct platform_device *pdev) +{ + struct amd_hfi_cpuinfo *hfi_cpuinfo; + struct device *dev = &pdev->dev; + u32 nr_class_id; + int idx; + + nr_class_id = cpuid_eax(AMD_HETERO_CPUID_27); + if (nr_class_id > 255) { + dev_err(dev, "number of supported classes too large: %d\n", + nr_class_id); + return -EINVAL; + } + + for_each_possible_cpu(idx) { + struct amd_hfi_classes *classes; + int *ipcc_scores; + + classes = devm_kcalloc(dev, + nr_class_id, + sizeof(struct amd_hfi_classes), + GFP_KERNEL); + if (!classes) + return -ENOMEM; + ipcc_scores = devm_kcalloc(dev, nr_class_id, sizeof(int), GFP_KERNEL); + if (!ipcc_scores) + return -ENOMEM; + hfi_cpuinfo = per_cpu_ptr(&amd_hfi_cpuinfo, idx); + hfi_cpuinfo->amd_hfi_classes = classes; + hfi_cpuinfo->ipcc_scores = ipcc_scores; + hfi_cpuinfo->nr_class = nr_class_id; + } + + return 0; +} + +static void amd_hfi_remove(struct platform_device *pdev) +{ + struct amd_hfi_data *dev = platform_get_drvdata(pdev); + + debugfs_remove_recursive(dev->dbgfs_dir); +} + +static int amd_set_hfi_ipcc_score(struct amd_hfi_cpuinfo *hfi_cpuinfo, int cpu) +{ + for (int i = 0; i < hfi_cpuinfo->nr_class; i++) + WRITE_ONCE(hfi_cpuinfo->ipcc_scores[i], + hfi_cpuinfo->amd_hfi_classes[i].perf); + + sched_set_itmt_core_prio(hfi_cpuinfo->ipcc_scores[0], cpu); + + return 0; +} + +static int amd_hfi_set_state(unsigned int cpu, bool state) +{ + int ret; + + ret = wrmsrq_on_cpu(cpu, MSR_AMD_WORKLOAD_CLASS_CONFIG, state ? 1 : 0); + if (ret) + return ret; + + return wrmsrq_on_cpu(cpu, MSR_AMD_WORKLOAD_HRST, 0x1); +} + +/** + * amd_hfi_online() - Enable workload classification on @cpu + * @cpu: CPU in which the workload classification will be enabled + * + * Return: 0 on success, negative error code on failure. + */ +static int amd_hfi_online(unsigned int cpu) +{ + struct amd_hfi_cpuinfo *hfi_info = per_cpu_ptr(&amd_hfi_cpuinfo, cpu); + struct amd_hfi_classes *hfi_classes; + int ret; + + if (WARN_ON_ONCE(!hfi_info)) + return -EINVAL; + + /* + * Check if @cpu as an associated, initialized and ranking data must + * be filled. + */ + hfi_classes = hfi_info->amd_hfi_classes; + if (!hfi_classes) + return -EINVAL; + + guard(mutex)(&hfi_cpuinfo_lock); + + ret = amd_hfi_set_state(cpu, true); + if (ret) + pr_err("WCT enable failed for CPU %u\n", cpu); + + return ret; +} + +/** + * amd_hfi_offline() - Disable workload classification on @cpu + * @cpu: CPU in which the workload classification will be disabled + * + * Remove @cpu from those covered by its HFI instance. + * + * Return: 0 on success, negative error code on failure + */ +static int amd_hfi_offline(unsigned int cpu) +{ + struct amd_hfi_cpuinfo *hfi_info = &per_cpu(amd_hfi_cpuinfo, cpu); + int ret; + + if (WARN_ON_ONCE(!hfi_info)) + return -EINVAL; + + guard(mutex)(&hfi_cpuinfo_lock); + + ret = amd_hfi_set_state(cpu, false); + if (ret) + pr_err("WCT disable failed for CPU %u\n", cpu); + + return ret; +} + +static int update_hfi_ipcc_scores(void) +{ + int cpu; + int ret; + + for_each_possible_cpu(cpu) { + struct amd_hfi_cpuinfo *hfi_cpuinfo = per_cpu_ptr(&amd_hfi_cpuinfo, cpu); + + ret = amd_set_hfi_ipcc_score(hfi_cpuinfo, cpu); + if (ret) + return ret; + } + + return 0; +} + +static int amd_hfi_metadata_parser(struct platform_device *pdev, + struct amd_hfi_data *amd_hfi_data) +{ + struct acpi_pcct_ext_pcc_slave *pcct_ext; + struct acpi_subtable_header *pcct_entry; + struct mbox_chan *pcc_mbox_channels; + struct acpi_table_header *pcct_tbl; + struct pcc_mbox_chan *pcc_chan; + acpi_status status; + int ret; + + pcc_mbox_channels = devm_kcalloc(&pdev->dev, AMD_HFI_MAILBOX_COUNT, + sizeof(*pcc_mbox_channels), GFP_KERNEL); + if (!pcc_mbox_channels) + return -ENOMEM; + + pcc_chan = devm_kcalloc(&pdev->dev, AMD_HFI_MAILBOX_COUNT, + sizeof(*pcc_chan), GFP_KERNEL); + if (!pcc_chan) + return -ENOMEM; + + status = acpi_get_table(ACPI_SIG_PCCT, 0, &pcct_tbl); + if (ACPI_FAILURE(status) || !pcct_tbl) + return -ENODEV; + + /* get pointer to the first PCC subspace entry */ + pcct_entry = (struct acpi_subtable_header *) ( + (unsigned long)pcct_tbl + sizeof(struct acpi_table_pcct)); + + pcc_chan->mchan = &pcc_mbox_channels[0]; + + amd_hfi_data->pcc_chan = pcc_chan; + amd_hfi_data->pcct_entry = pcct_entry; + pcct_ext = (struct acpi_pcct_ext_pcc_slave *)pcct_entry; + + if (pcct_ext->length <= 0) { + ret = -EINVAL; + goto out; + } + + amd_hfi_data->shmem = devm_kzalloc(amd_hfi_data->dev, pcct_ext->length, GFP_KERNEL); + if (!amd_hfi_data->shmem) { + ret = -ENOMEM; + goto out; + } + + pcc_chan->shmem_base_addr = pcct_ext->base_address; + pcc_chan->shmem_size = pcct_ext->length; + + /* parse the shared memory info from the PCCT table */ + ret = amd_hfi_fill_metadata(amd_hfi_data); + +out: + /* Don't leak any ACPI memory */ + acpi_put_table(pcct_tbl); + + return ret; +} + +static int class_capabilities_show(struct seq_file *s, void *unused) +{ + u32 cpu, idx; + + seq_puts(s, "CPU #\tWLC\tPerf\tEff\n"); + for_each_possible_cpu(cpu) { + struct amd_hfi_cpuinfo *hfi_cpuinfo = per_cpu_ptr(&amd_hfi_cpuinfo, cpu); + + seq_printf(s, "%d", cpu); + for (idx = 0; idx < hfi_cpuinfo->nr_class; idx++) { + seq_printf(s, "\t%u\t%u\t%u\n", idx, + hfi_cpuinfo->amd_hfi_classes[idx].perf, + hfi_cpuinfo->amd_hfi_classes[idx].eff); + } + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(class_capabilities); + +static int amd_hfi_pm_resume(struct device *dev) +{ + int ret, cpu; + + for_each_online_cpu(cpu) { + ret = amd_hfi_set_state(cpu, true); + if (ret < 0) { + dev_err(dev, "failed to enable workload class config: %d\n", ret); + return ret; + } + } + + return 0; +} + +static int amd_hfi_pm_suspend(struct device *dev) +{ + int ret, cpu; + + for_each_online_cpu(cpu) { + ret = amd_hfi_set_state(cpu, false); + if (ret < 0) { + dev_err(dev, "failed to disable workload class config: %d\n", ret); + return ret; + } + } + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(amd_hfi_pm_ops, amd_hfi_pm_suspend, amd_hfi_pm_resume); + +static const struct acpi_device_id amd_hfi_platform_match[] = { + {"AMDI0104", 0}, + { } +}; +MODULE_DEVICE_TABLE(acpi, amd_hfi_platform_match); + +static int amd_hfi_probe(struct platform_device *pdev) +{ + struct amd_hfi_data *amd_hfi_data; + int ret; + + if (!acpi_match_device(amd_hfi_platform_match, &pdev->dev)) + return -ENODEV; + + amd_hfi_data = devm_kzalloc(&pdev->dev, sizeof(*amd_hfi_data), GFP_KERNEL); + if (!amd_hfi_data) + return -ENOMEM; + + amd_hfi_data->dev = &pdev->dev; + platform_set_drvdata(pdev, amd_hfi_data); + + ret = amd_hfi_alloc_class_data(pdev); + if (ret) + return ret; + + ret = amd_hfi_metadata_parser(pdev, amd_hfi_data); + if (ret) + return ret; + + ret = update_hfi_ipcc_scores(); + if (ret) + return ret; + + /* + * Tasks will already be running at the time this happens. This is + * OK because rankings will be adjusted by the callbacks. + */ + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "x86/amd_hfi:online", + amd_hfi_online, amd_hfi_offline); + if (ret < 0) + return ret; + + schedule_work(&sched_amd_hfi_itmt_work); + + amd_hfi_data->dbgfs_dir = debugfs_create_dir("amd_hfi", arch_debugfs_dir); + debugfs_create_file("class_capabilities", 0644, amd_hfi_data->dbgfs_dir, pdev, + &class_capabilities_fops); + + return 0; +} + +static struct platform_driver amd_hfi_driver = { + .driver = { + .name = AMD_HFI_DRIVER, + .pm = &amd_hfi_pm_ops, + .acpi_match_table = ACPI_PTR(amd_hfi_platform_match), + }, + .probe = amd_hfi_probe, + .remove = amd_hfi_remove, +}; + +static int __init amd_hfi_init(void) +{ + int ret; + + if (acpi_disabled || + !cpu_feature_enabled(X86_FEATURE_AMD_HTR_CORES) || + !cpu_feature_enabled(X86_FEATURE_AMD_WORKLOAD_CLASS)) + return -ENODEV; + + device = platform_device_register_simple(AMD_HFI_DRIVER, -1, NULL, 0); + if (IS_ERR(device)) { + pr_err("unable to register HFI platform device\n"); + return PTR_ERR(device); + } + + ret = platform_driver_register(&amd_hfi_driver); + if (ret) + pr_err("failed to register HFI driver\n"); + + return ret; +} + +static __exit void amd_hfi_exit(void) +{ + platform_driver_unregister(&amd_hfi_driver); + platform_device_unregister(device); +} +module_init(amd_hfi_init); +module_exit(amd_hfi_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("AMD Hardware Feedback Interface Driver"); diff --git a/drivers/platform/x86/amd/hsmp/Kconfig b/drivers/platform/x86/amd/hsmp/Kconfig new file mode 100644 index 000000000..291112079 --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/Kconfig @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# AMD HSMP Driver +# + +config AMD_HSMP + tristate + +menu "AMD HSMP Driver" + depends on AMD_NODE || COMPILE_TEST + +config AMD_HSMP_ACPI + tristate "AMD HSMP ACPI device driver" + depends on ACPI + depends on HWMON || !HWMON + select AMD_HSMP + help + Host System Management Port (HSMP) interface is a mailbox interface + between the x86 core and the System Management Unit (SMU) firmware. + The driver provides a way for user space tools to monitor and manage + system management functionality on EPYC and MI300A server CPUs + from AMD. + + This option supports ACPI based probing. + You may enable this, if your platform BIOS provides an ACPI object + as described in amd_hsmp.rst document. + + If you choose to compile this driver as a module the module will be + called hsmp_acpi. + +config AMD_HSMP_PLAT + tristate "AMD HSMP platform device driver" + depends on HWMON || !HWMON + select AMD_HSMP + help + Host System Management Port (HSMP) interface is a mailbox interface + between the x86 core and the System Management Unit (SMU) firmware. + The driver provides a way for user space tools to monitor and manage + system management functionality on EPYC and MI300A server CPUs + from AMD. + + This option supports platform device based probing. + You may enable this, if your platform BIOS does not provide + HSMP ACPI object. + + If you choose to compile this driver as a module the module will be + called amd_hsmp. + +endmenu diff --git a/drivers/platform/x86/amd/hsmp/Makefile b/drivers/platform/x86/amd/hsmp/Makefile new file mode 100644 index 000000000..ce8342e71 --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/Makefile @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for drivers/platform/x86/amd/hsmp +# AMD HSMP Driver +# + +obj-$(CONFIG_AMD_HSMP) += hsmp_common.o +hsmp_common-y := hsmp.o +hsmp_common-$(CONFIG_HWMON) += hwmon.o +obj-$(CONFIG_AMD_HSMP_PLAT) += amd_hsmp.o +amd_hsmp-y := plat.o +obj-$(CONFIG_AMD_HSMP_ACPI) += hsmp_acpi.o +hsmp_acpi-y := acpi.o diff --git a/drivers/platform/x86/amd/hsmp/acpi.c b/drivers/platform/x86/amd/hsmp/acpi.c new file mode 100644 index 000000000..97ed71593 --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/acpi.c @@ -0,0 +1,647 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD HSMP Platform Driver + * Copyright (c) 2024, AMD. + * All Rights Reserved. + * + * This file provides an ACPI based driver implementation for HSMP interface. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "hsmp.h" + +#define DRIVER_NAME "hsmp_acpi" + +/* These are the strings specified in ACPI table */ +#define MSG_IDOFF_STR "MsgIdOffset" +#define MSG_ARGOFF_STR "MsgArgOffset" +#define MSG_RESPOFF_STR "MsgRspOffset" + +static struct hsmp_plat_device *hsmp_pdev; + +struct hsmp_sys_attr { + struct device_attribute dattr; + u32 msg_id; +}; + +static int amd_hsmp_acpi_rdwr(struct hsmp_socket *sock, u32 offset, + u32 *value, bool write) +{ + if (write) + iowrite32(*value, sock->virt_base_addr + offset); + else + *value = ioread32(sock->virt_base_addr + offset); + + return 0; +} + +/* This is the UUID used for HSMP */ +static const guid_t acpi_hsmp_uuid = GUID_INIT(0xb74d619d, 0x5707, 0x48bd, + 0xa6, 0x9f, 0x4e, 0xa2, + 0x87, 0x1f, 0xc2, 0xf6); + +static inline bool is_acpi_hsmp_uuid(union acpi_object *obj) +{ + if (obj->type == ACPI_TYPE_BUFFER && obj->buffer.length == UUID_SIZE) + return guid_equal((guid_t *)obj->buffer.pointer, &acpi_hsmp_uuid); + + return false; +} + +static inline int hsmp_get_uid(struct device *dev, u16 *sock_ind) +{ + char *uid; + + /* + * UID (ID00, ID01..IDXX) is used for differentiating sockets, + * read it and strip the "ID" part of it and convert the remaining + * bytes to integer. + */ + uid = acpi_device_uid(ACPI_COMPANION(dev)); + + return kstrtou16(uid + 2, 10, sock_ind); +} + +static acpi_status hsmp_resource(struct acpi_resource *res, void *data) +{ + struct hsmp_socket *sock = data; + struct resource r; + + switch (res->type) { + case ACPI_RESOURCE_TYPE_FIXED_MEMORY32: + if (!acpi_dev_resource_memory(res, &r)) + return AE_ERROR; + if (!r.start || r.end < r.start || !(r.flags & IORESOURCE_MEM_WRITEABLE)) + return AE_ERROR; + sock->mbinfo.base_addr = r.start; + sock->mbinfo.size = resource_size(&r); + break; + case ACPI_RESOURCE_TYPE_END_TAG: + break; + default: + return AE_ERROR; + } + + return AE_OK; +} + +static int hsmp_read_acpi_dsd(struct hsmp_socket *sock) +{ + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *guid, *mailbox_package; + union acpi_object *dsd; + acpi_status status; + int ret = 0; + int j; + + status = acpi_evaluate_object_typed(ACPI_HANDLE(sock->dev), "_DSD", NULL, + &buf, ACPI_TYPE_PACKAGE); + if (ACPI_FAILURE(status)) { + dev_err(sock->dev, "Failed to read mailbox reg offsets from DSD table, err: %s\n", + acpi_format_exception(status)); + return -ENODEV; + } + + dsd = buf.pointer; + + /* HSMP _DSD property should contain 2 objects. + * 1. guid which is an acpi object of type ACPI_TYPE_BUFFER + * 2. mailbox which is an acpi object of type ACPI_TYPE_PACKAGE + * This mailbox object contains 3 more acpi objects of type + * ACPI_TYPE_PACKAGE for holding msgid, msgresp, msgarg offsets + * these packages inturn contain 2 acpi objects of type + * ACPI_TYPE_STRING and ACPI_TYPE_INTEGER + */ + if (!dsd || dsd->type != ACPI_TYPE_PACKAGE || dsd->package.count != 2) { + ret = -EINVAL; + goto free_buf; + } + + guid = &dsd->package.elements[0]; + mailbox_package = &dsd->package.elements[1]; + if (!is_acpi_hsmp_uuid(guid) || mailbox_package->type != ACPI_TYPE_PACKAGE) { + dev_err(sock->dev, "Invalid hsmp _DSD table data\n"); + ret = -EINVAL; + goto free_buf; + } + + for (j = 0; j < mailbox_package->package.count; j++) { + union acpi_object *msgobj, *msgstr, *msgint; + + msgobj = &mailbox_package->package.elements[j]; + msgstr = &msgobj->package.elements[0]; + msgint = &msgobj->package.elements[1]; + + /* package should have 1 string and 1 integer object */ + if (msgobj->type != ACPI_TYPE_PACKAGE || + msgstr->type != ACPI_TYPE_STRING || + msgint->type != ACPI_TYPE_INTEGER) { + ret = -EINVAL; + goto free_buf; + } + + if (!strncmp(msgstr->string.pointer, MSG_IDOFF_STR, + msgstr->string.length)) { + sock->mbinfo.msg_id_off = msgint->integer.value; + } else if (!strncmp(msgstr->string.pointer, MSG_RESPOFF_STR, + msgstr->string.length)) { + sock->mbinfo.msg_resp_off = msgint->integer.value; + } else if (!strncmp(msgstr->string.pointer, MSG_ARGOFF_STR, + msgstr->string.length)) { + sock->mbinfo.msg_arg_off = msgint->integer.value; + } else { + ret = -ENOENT; + goto free_buf; + } + } + + if (!sock->mbinfo.msg_id_off || !sock->mbinfo.msg_resp_off || + !sock->mbinfo.msg_arg_off) + ret = -EINVAL; + +free_buf: + ACPI_FREE(buf.pointer); + return ret; +} + +static int hsmp_read_acpi_crs(struct hsmp_socket *sock) +{ + acpi_status status; + + status = acpi_walk_resources(ACPI_HANDLE(sock->dev), METHOD_NAME__CRS, + hsmp_resource, sock); + if (ACPI_FAILURE(status)) { + dev_err(sock->dev, "Failed to look up MP1 base address from CRS method, err: %s\n", + acpi_format_exception(status)); + return -EINVAL; + } + if (!sock->mbinfo.base_addr || !sock->mbinfo.size) + return -EINVAL; + + /* The mapped region should be un-cached */ + sock->virt_base_addr = devm_ioremap_uc(sock->dev, sock->mbinfo.base_addr, + sock->mbinfo.size); + if (!sock->virt_base_addr) { + dev_err(sock->dev, "Failed to ioremap MP1 base address\n"); + return -ENOMEM; + } + + return 0; +} + +/* Parse the ACPI table to read the data */ +static int hsmp_parse_acpi_table(struct device *dev, u16 sock_ind) +{ + struct hsmp_socket *sock = &hsmp_pdev->sock[sock_ind]; + int ret; + + sock->sock_ind = sock_ind; + sock->dev = dev; + sock->amd_hsmp_rdwr = amd_hsmp_acpi_rdwr; + + sema_init(&sock->hsmp_sem, 1); + + dev_set_drvdata(dev, sock); + + /* Read MP1 base address from CRS method */ + ret = hsmp_read_acpi_crs(sock); + if (ret) + return ret; + + /* Read mailbox offsets from DSD table */ + return hsmp_read_acpi_dsd(sock); +} + +static ssize_t hsmp_metric_tbl_acpi_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct hsmp_socket *sock = dev_get_drvdata(dev); + + return hsmp_metric_tbl_read(sock, buf, count); +} + +static umode_t hsmp_is_sock_attr_visible(struct kobject *kobj, + const struct bin_attribute *battr, int id) +{ + if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) + return battr->attr.mode; + + return 0; +} + +static umode_t hsmp_is_sock_dev_attr_visible(struct kobject *kobj, + struct attribute *attr, int id) +{ + return attr->mode; +} + +#define to_hsmp_sys_attr(_attr) container_of(_attr, struct hsmp_sys_attr, dattr) + +static ssize_t hsmp_msg_resp32_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", data); +} + +#define DDR_MAX_BW_MASK GENMASK(31, 20) +#define DDR_UTIL_BW_MASK GENMASK(19, 8) +#define DDR_UTIL_BW_PERC_MASK GENMASK(7, 0) +#define FW_VER_MAJOR_MASK GENMASK(23, 16) +#define FW_VER_MINOR_MASK GENMASK(15, 8) +#define FW_VER_DEBUG_MASK GENMASK(7, 0) +#define FMAX_MASK GENMASK(31, 16) +#define FMIN_MASK GENMASK(15, 0) +#define FREQ_LIMIT_MASK GENMASK(31, 16) +#define FREQ_SRC_IND_MASK GENMASK(15, 0) + +static ssize_t hsmp_ddr_max_bw_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(DDR_MAX_BW_MASK, data)); +} + +static ssize_t hsmp_ddr_util_bw_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(DDR_UTIL_BW_MASK, data)); +} + +static ssize_t hsmp_ddr_util_bw_perc_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(DDR_UTIL_BW_PERC_MASK, data)); +} + +static ssize_t hsmp_msg_fw_ver_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu.%lu.%lu\n", + FIELD_GET(FW_VER_MAJOR_MASK, data), + FIELD_GET(FW_VER_MINOR_MASK, data), + FIELD_GET(FW_VER_DEBUG_MASK, data)); +} + +static ssize_t hsmp_fclk_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data[2]; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, data, 2); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", data[0]); +} + +static ssize_t hsmp_mclk_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data[2]; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, data, 2); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", data[1]); +} + +static ssize_t hsmp_clk_fmax_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(FMAX_MASK, data)); +} + +static ssize_t hsmp_clk_fmin_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(FMIN_MASK, data)); +} + +static ssize_t hsmp_freq_limit_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + return sysfs_emit(buf, "%lu\n", FIELD_GET(FREQ_LIMIT_MASK, data)); +} + +static const char * const freqlimit_srcnames[] = { + "cHTC-Active", + "PROCHOT", + "TDC limit", + "PPT Limit", + "OPN Max", + "Reliability Limit", + "APML Agent", + "HSMP Agent", +}; + +static ssize_t hsmp_freq_limit_source_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct hsmp_sys_attr *hattr = to_hsmp_sys_attr(attr); + struct hsmp_socket *sock = dev_get_drvdata(dev); + unsigned int index; + int len = 0; + u16 src_ind; + u32 data; + int ret; + + ret = hsmp_msg_get_nargs(sock->sock_ind, hattr->msg_id, &data, 1); + if (ret) + return ret; + + src_ind = FIELD_GET(FREQ_SRC_IND_MASK, data); + for (index = 0; index < ARRAY_SIZE(freqlimit_srcnames); index++) { + if (!src_ind) + break; + if (src_ind & 1) + len += sysfs_emit_at(buf, len, "%s\n", freqlimit_srcnames[index]); + src_ind >>= 1; + } + return len; +} + +static int init_acpi(struct device *dev) +{ + u16 sock_ind; + int ret; + + ret = hsmp_get_uid(dev, &sock_ind); + if (ret) + return ret; + if (sock_ind >= hsmp_pdev->num_sockets) + return -EINVAL; + + ret = hsmp_parse_acpi_table(dev, sock_ind); + if (ret) { + dev_err(dev, "Failed to parse ACPI table\n"); + return ret; + } + + /* Test the hsmp interface */ + ret = hsmp_test(sock_ind, 0xDEADBEEF); + if (ret) { + dev_err(dev, "HSMP test message failed on Fam:%x model:%x\n", + boot_cpu_data.x86, boot_cpu_data.x86_model); + dev_err(dev, "Is HSMP disabled in BIOS ?\n"); + return ret; + } + + ret = hsmp_cache_proto_ver(sock_ind); + if (ret) { + dev_err(dev, "Failed to read HSMP protocol version\n"); + return ret; + } + + if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) { + ret = hsmp_get_tbl_dram_base(sock_ind); + if (ret) + dev_info(dev, "Failed to init metric table\n"); + } + + ret = hsmp_create_sensor(dev, sock_ind); + if (ret) + dev_info(dev, "Failed to register HSMP sensors with hwmon\n"); + + dev_set_drvdata(dev, &hsmp_pdev->sock[sock_ind]); + + return 0; +} + +static const struct bin_attribute hsmp_metric_tbl_attr = { + .attr = { .name = HSMP_METRICS_TABLE_NAME, .mode = 0444}, + .read = hsmp_metric_tbl_acpi_read, + .size = sizeof(struct hsmp_metric_table), +}; + +static const struct bin_attribute *hsmp_attr_list[] = { + &hsmp_metric_tbl_attr, + NULL +}; + +#define HSMP_DEV_ATTR(_name, _msg_id, _show, _mode) \ +static struct hsmp_sys_attr hattr_##_name = { \ + .dattr = __ATTR(_name, _mode, _show, NULL), \ + .msg_id = _msg_id, \ +} + +HSMP_DEV_ATTR(c0_residency_input, HSMP_GET_C0_PERCENT, hsmp_msg_resp32_show, 0444); +HSMP_DEV_ATTR(prochot_status, HSMP_GET_PROC_HOT, hsmp_msg_resp32_show, 0444); +HSMP_DEV_ATTR(smu_fw_version, HSMP_GET_SMU_VER, hsmp_msg_fw_ver_show, 0444); +HSMP_DEV_ATTR(protocol_version, HSMP_GET_PROTO_VER, hsmp_msg_resp32_show, 0444); +HSMP_DEV_ATTR(cclk_freq_limit_input, HSMP_GET_CCLK_THROTTLE_LIMIT, hsmp_msg_resp32_show, 0444); +HSMP_DEV_ATTR(ddr_max_bw, HSMP_GET_DDR_BANDWIDTH, hsmp_ddr_max_bw_show, 0444); +HSMP_DEV_ATTR(ddr_utilised_bw_input, HSMP_GET_DDR_BANDWIDTH, hsmp_ddr_util_bw_show, 0444); +HSMP_DEV_ATTR(ddr_utilised_bw_perc_input, HSMP_GET_DDR_BANDWIDTH, hsmp_ddr_util_bw_perc_show, 0444); +HSMP_DEV_ATTR(fclk_input, HSMP_GET_FCLK_MCLK, hsmp_fclk_show, 0444); +HSMP_DEV_ATTR(mclk_input, HSMP_GET_FCLK_MCLK, hsmp_mclk_show, 0444); +HSMP_DEV_ATTR(clk_fmax, HSMP_GET_SOCKET_FMAX_FMIN, hsmp_clk_fmax_show, 0444); +HSMP_DEV_ATTR(clk_fmin, HSMP_GET_SOCKET_FMAX_FMIN, hsmp_clk_fmin_show, 0444); +HSMP_DEV_ATTR(pwr_current_active_freq_limit, HSMP_GET_SOCKET_FREQ_LIMIT, + hsmp_freq_limit_show, 0444); +HSMP_DEV_ATTR(pwr_current_active_freq_limit_source, HSMP_GET_SOCKET_FREQ_LIMIT, + hsmp_freq_limit_source_show, 0444); + +static struct attribute *hsmp_dev_attr_list[] = { + &hattr_c0_residency_input.dattr.attr, + &hattr_prochot_status.dattr.attr, + &hattr_smu_fw_version.dattr.attr, + &hattr_protocol_version.dattr.attr, + &hattr_cclk_freq_limit_input.dattr.attr, + &hattr_ddr_max_bw.dattr.attr, + &hattr_ddr_utilised_bw_input.dattr.attr, + &hattr_ddr_utilised_bw_perc_input.dattr.attr, + &hattr_fclk_input.dattr.attr, + &hattr_mclk_input.dattr.attr, + &hattr_clk_fmax.dattr.attr, + &hattr_clk_fmin.dattr.attr, + &hattr_pwr_current_active_freq_limit.dattr.attr, + &hattr_pwr_current_active_freq_limit_source.dattr.attr, + NULL +}; + +static const struct attribute_group hsmp_attr_grp = { + .bin_attrs = hsmp_attr_list, + .attrs = hsmp_dev_attr_list, + .is_bin_visible = hsmp_is_sock_attr_visible, + .is_visible = hsmp_is_sock_dev_attr_visible, +}; + +static const struct attribute_group *hsmp_groups[] = { + &hsmp_attr_grp, + NULL +}; + +static const struct acpi_device_id amd_hsmp_acpi_ids[] = { + {ACPI_HSMP_DEVICE_HID, 0}, + {} +}; +MODULE_DEVICE_TABLE(acpi, amd_hsmp_acpi_ids); + +static int hsmp_acpi_probe(struct platform_device *pdev) +{ + int ret; + + hsmp_pdev = get_hsmp_pdev(); + if (!hsmp_pdev) + return -ENOMEM; + + if (!hsmp_pdev->is_probed) { + hsmp_pdev->num_sockets = topology_max_packages(); + if (!hsmp_pdev->num_sockets) { + dev_err(&pdev->dev, "No CPU sockets detected\n"); + return -ENODEV; + } + + hsmp_pdev->sock = devm_kcalloc(&pdev->dev, hsmp_pdev->num_sockets, + sizeof(*hsmp_pdev->sock), + GFP_KERNEL); + if (!hsmp_pdev->sock) + return -ENOMEM; + } + + ret = init_acpi(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "Failed to initialize HSMP interface.\n"); + return ret; + } + + if (!hsmp_pdev->is_probed) { + ret = hsmp_misc_register(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "Failed to register misc device\n"); + return ret; + } + hsmp_pdev->is_probed = true; + dev_dbg(&pdev->dev, "AMD HSMP ACPI is probed successfully\n"); + } + + return 0; +} + +static void hsmp_acpi_remove(struct platform_device *pdev) +{ + /* + * We register only one misc_device even on multi-socket system. + * So, deregister should happen only once. + */ + if (hsmp_pdev->is_probed) { + hsmp_misc_deregister(); + hsmp_pdev->is_probed = false; + } +} + +static struct platform_driver amd_hsmp_driver = { + .probe = hsmp_acpi_probe, + .remove = hsmp_acpi_remove, + .driver = { + .name = DRIVER_NAME, + .acpi_match_table = amd_hsmp_acpi_ids, + .dev_groups = hsmp_groups, + }, +}; + +module_platform_driver(amd_hsmp_driver); + +MODULE_IMPORT_NS("AMD_HSMP"); +MODULE_DESCRIPTION("AMD HSMP Platform Interface Driver"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/amd/hsmp/hsmp.c b/drivers/platform/x86/amd/hsmp/hsmp.c new file mode 100644 index 000000000..6a26937fc --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/hsmp.c @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD HSMP Platform Driver + * Copyright (c) 2022, AMD. + * All Rights Reserved. + * + * This file provides a device implementation for HSMP interface + */ + +#include + +#include +#include +#include +#include +#include + +#include "hsmp.h" + +/* HSMP Status / Error codes */ +#define HSMP_STATUS_NOT_READY 0x00 +#define HSMP_STATUS_OK 0x01 +#define HSMP_ERR_INVALID_MSG 0xFE +#define HSMP_ERR_INVALID_INPUT 0xFF +#define HSMP_ERR_PREREQ_NOT_SATISFIED 0xFD +#define HSMP_ERR_SMU_BUSY 0xFC + +/* Timeout in millsec */ +#define HSMP_MSG_TIMEOUT 100 +#define HSMP_SHORT_SLEEP 1 + +#define HSMP_WR true +#define HSMP_RD false + +/* + * When same message numbers are used for both GET and SET operation, + * bit:31 indicates whether its SET or GET operation. + */ +#define CHECK_GET_BIT BIT(31) + +static struct hsmp_plat_device hsmp_pdev; + +/* + * Send a message to the HSMP port via PCI-e config space registers + * or by writing to MMIO space. + * + * The caller is expected to zero out any unused arguments. + * If a response is expected, the number of response words should be greater than 0. + * + * Returns 0 for success and populates the requested number of arguments. + * Returns a negative error code for failure. + */ +static int __hsmp_send_message(struct hsmp_socket *sock, struct hsmp_message *msg) +{ + struct hsmp_mbaddr_info *mbinfo; + unsigned long timeout, short_sleep; + u32 mbox_status; + u32 index; + int ret; + + mbinfo = &sock->mbinfo; + + /* Clear the status register */ + mbox_status = HSMP_STATUS_NOT_READY; + ret = sock->amd_hsmp_rdwr(sock, mbinfo->msg_resp_off, &mbox_status, HSMP_WR); + if (ret) { + dev_err(sock->dev, "Error %d clearing mailbox status register\n", ret); + return ret; + } + + index = 0; + /* Write any message arguments */ + while (index < msg->num_args) { + ret = sock->amd_hsmp_rdwr(sock, mbinfo->msg_arg_off + (index << 2), + &msg->args[index], HSMP_WR); + if (ret) { + dev_err(sock->dev, "Error %d writing message argument %d\n", ret, index); + return ret; + } + index++; + } + + /* Write the message ID which starts the operation */ + ret = sock->amd_hsmp_rdwr(sock, mbinfo->msg_id_off, &msg->msg_id, HSMP_WR); + if (ret) { + dev_err(sock->dev, "Error %d writing message ID %u\n", ret, msg->msg_id); + return ret; + } + + /* + * Depending on when the trigger write completes relative to the SMU + * firmware 1 ms cycle, the operation may take from tens of us to 1 ms + * to complete. Some operations may take more. Therefore we will try + * a few short duration sleeps and switch to long sleeps if we don't + * succeed quickly. + */ + short_sleep = jiffies + msecs_to_jiffies(HSMP_SHORT_SLEEP); + timeout = jiffies + msecs_to_jiffies(HSMP_MSG_TIMEOUT); + + while (true) { + ret = sock->amd_hsmp_rdwr(sock, mbinfo->msg_resp_off, &mbox_status, HSMP_RD); + if (ret) { + dev_err(sock->dev, "Error %d reading mailbox status\n", ret); + return ret; + } + + if (mbox_status != HSMP_STATUS_NOT_READY) + break; + + if (!time_before(jiffies, timeout)) + break; + + if (time_before(jiffies, short_sleep)) + usleep_range(50, 100); + else + usleep_range(1000, 2000); + } + + if (unlikely(mbox_status == HSMP_STATUS_NOT_READY)) { + dev_err(sock->dev, "Message ID 0x%X failure : SMU timeout (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -ETIMEDOUT; + } else if (unlikely(mbox_status == HSMP_ERR_INVALID_MSG)) { + dev_err(sock->dev, "Message ID 0x%X failure : Invalid message (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -ENOMSG; + } else if (unlikely(mbox_status == HSMP_ERR_INVALID_INPUT)) { + dev_err(sock->dev, "Message ID 0x%X failure : Invalid arguments (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -EINVAL; + } else if (unlikely(mbox_status == HSMP_ERR_PREREQ_NOT_SATISFIED)) { + dev_err(sock->dev, "Message ID 0x%X failure : Prerequisite not satisfied (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -EREMOTEIO; + } else if (unlikely(mbox_status == HSMP_ERR_SMU_BUSY)) { + dev_err(sock->dev, "Message ID 0x%X failure : SMU BUSY (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -EBUSY; + } else if (unlikely(mbox_status != HSMP_STATUS_OK)) { + dev_err(sock->dev, "Message ID 0x%X unknown failure (status = 0x%X)\n", + msg->msg_id, mbox_status); + return -EIO; + } + + /* + * SMU has responded OK. Read response data. + * SMU reads the input arguments from eight 32 bit registers starting + * from SMN_HSMP_MSG_DATA and writes the response data to the same + * SMN_HSMP_MSG_DATA address. + * We copy the response data if any, back to the args[]. + */ + index = 0; + while (index < msg->response_sz) { + ret = sock->amd_hsmp_rdwr(sock, mbinfo->msg_arg_off + (index << 2), + &msg->args[index], HSMP_RD); + if (ret) { + dev_err(sock->dev, "Error %d reading response %u for message ID:%u\n", + ret, index, msg->msg_id); + break; + } + index++; + } + + return ret; +} + +static int validate_message(struct hsmp_message *msg) +{ + /* msg_id against valid range of message IDs */ + if (msg->msg_id < HSMP_TEST || msg->msg_id >= HSMP_MSG_ID_MAX) + return -ENOMSG; + + /* msg_id is a reserved message ID */ + if (hsmp_msg_desc_table[msg->msg_id].type == HSMP_RSVD) + return -ENOMSG; + + /* + * num_args passed by user should match the num_args specified in + * message description table. + */ + if (msg->num_args != hsmp_msg_desc_table[msg->msg_id].num_args) + return -EINVAL; + + /* + * Some older HSMP SET messages are updated to add GET in the same message. + * In these messages, GET returns the current value and SET also returns + * the successfully set value. To support this GET and SET in same message + * while maintaining backward compatibility for the HSMP users, + * hsmp_msg_desc_table[] indicates only maximum allowed response_sz. + */ + if (hsmp_msg_desc_table[msg->msg_id].type == HSMP_SET_GET) { + if (msg->response_sz > hsmp_msg_desc_table[msg->msg_id].response_sz) + return -EINVAL; + } else { + /* only HSMP_SET or HSMP_GET messages go through this strict check */ + if (msg->response_sz != hsmp_msg_desc_table[msg->msg_id].response_sz) + return -EINVAL; + } + return 0; +} + +int hsmp_send_message(struct hsmp_message *msg) +{ + struct hsmp_socket *sock; + unsigned int sock_ind; + int ret; + + if (!msg) + return -EINVAL; + ret = validate_message(msg); + if (ret) + return ret; + + if (!hsmp_pdev.sock || msg->sock_ind >= hsmp_pdev.num_sockets) + return -ENODEV; + + /* + * Sanitize sock_ind after the bounds check. A mispredicted branch can + * still let the CPU speculatively use msg->sock_ind as an index into + * hsmp_pdev.sock[] (Spectre v1, CVE-2017-5753), including for callers + * other than hsmp_ioctl_msg() that pass a user-derived socket index. + */ + sock_ind = array_index_nospec(msg->sock_ind, hsmp_pdev.num_sockets); + sock = &hsmp_pdev.sock[sock_ind]; + + ret = down_interruptible(&sock->hsmp_sem); + if (ret < 0) + return ret; + + ret = __hsmp_send_message(sock, msg); + + up(&sock->hsmp_sem); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(hsmp_send_message, "AMD_HSMP"); + +int hsmp_msg_get_nargs(u16 sock_ind, u32 msg_id, u32 *data, u8 num_args) +{ + struct hsmp_message msg = {}; + unsigned int i; + int ret; + + if (!data) + return -EINVAL; + msg.msg_id = msg_id; + msg.sock_ind = sock_ind; + msg.response_sz = num_args; + + ret = hsmp_send_message(&msg); + if (ret) + return ret; + + for (i = 0; i < num_args; i++) + data[i] = msg.args[i]; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(hsmp_msg_get_nargs, "AMD_HSMP"); + +int hsmp_test(u16 sock_ind, u32 value) +{ + struct hsmp_message msg = { 0 }; + int ret; + + /* + * Test the hsmp port by performing TEST command. The test message + * takes one argument and returns the value of that argument + 1. + */ + msg.msg_id = HSMP_TEST; + msg.num_args = 1; + msg.response_sz = 1; + msg.args[0] = value; + msg.sock_ind = sock_ind; + + ret = hsmp_send_message(&msg); + if (ret) + return ret; + + /* Check the response value */ + if (msg.args[0] != (value + 1)) { + dev_err(hsmp_pdev.sock[sock_ind].dev, + "Socket %d test message failed, Expected 0x%08X, received 0x%08X\n", + sock_ind, (value + 1), msg.args[0]); + return -EBADE; + } + + return ret; +} +EXPORT_SYMBOL_NS_GPL(hsmp_test, "AMD_HSMP"); + +static bool is_get_msg(struct hsmp_message *msg) +{ + if (hsmp_msg_desc_table[msg->msg_id].type == HSMP_GET) + return true; + + if (hsmp_msg_desc_table[msg->msg_id].type == HSMP_SET_GET && + (msg->args[0] & CHECK_GET_BIT)) + return true; + + return false; +} + +long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) +{ + int __user *arguser = (int __user *)arg; + struct hsmp_message msg = { 0 }; + int ret; + + if (copy_struct_from_user(&msg, sizeof(msg), arguser, sizeof(struct hsmp_message))) + return -EFAULT; + + /* + * Check msg_id is within the range of supported msg ids + * i.e within the array bounds of hsmp_msg_desc_table + */ + if (msg.msg_id < HSMP_TEST || msg.msg_id >= HSMP_MSG_ID_MAX) + return -ENOMSG; + + /* + * Sanitize the user-controlled msg_id against speculative + * execution. The bounds check above retires the out-of-range + * case with -ENOMSG, but a mispredicted branch can still let the + * CPU speculatively use msg_id as an index into + * hsmp_msg_desc_table[] (here and in validate_message() / + * is_get_msg() called downstream via hsmp_send_message()), and + * pull arbitrary kernel memory into the cache (Spectre v1, + * CVE-2017-5753). Clamp once into msg.msg_id so every downstream + * dereference sees the sanitized value. + */ + msg.msg_id = array_index_nospec(msg.msg_id, HSMP_MSG_ID_MAX); + + switch (fp->f_mode & (FMODE_WRITE | FMODE_READ)) { + case FMODE_WRITE: + /* + * Device is opened in O_WRONLY mode + * Execute only set/configure commands + */ + if (is_get_msg(&msg)) + return -EPERM; + break; + case FMODE_READ: + /* + * Device is opened in O_RDONLY mode + * Execute only get/monitor commands + */ + if (!is_get_msg(&msg)) + return -EPERM; + break; + case FMODE_READ | FMODE_WRITE: + /* + * Device is opened in O_RDWR mode + * Execute both get/monitor and set/configure commands + */ + break; + default: + return -EPERM; + } + + ret = hsmp_send_message(&msg); + if (ret) + return ret; + + if (hsmp_msg_desc_table[msg.msg_id].response_sz > 0) { + /* Copy results back to user for get/monitor commands */ + if (copy_to_user(arguser, &msg, sizeof(struct hsmp_message))) + return -EFAULT; + } + + return 0; +} + +ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size) +{ + struct hsmp_message msg = { 0 }; + int ret; + + if (!sock || !buf) + return -EINVAL; + + if (!sock->metric_tbl_addr) { + dev_err(sock->dev, "Metrics table address not available\n"); + return -ENOMEM; + } + + /* Do not support lseek(), also don't allow more than the size of metric table */ + if (size != sizeof(struct hsmp_metric_table)) { + dev_err(sock->dev, "Wrong buffer size\n"); + return -EINVAL; + } + + msg.msg_id = HSMP_GET_METRIC_TABLE; + msg.sock_ind = sock->sock_ind; + + ret = hsmp_send_message(&msg); + if (ret) + return ret; + memcpy_fromio(buf, sock->metric_tbl_addr, size); + + return size; +} +EXPORT_SYMBOL_NS_GPL(hsmp_metric_tbl_read, "AMD_HSMP"); + +int hsmp_get_tbl_dram_base(u16 sock_ind) +{ + struct hsmp_socket *sock = &hsmp_pdev.sock[sock_ind]; + struct hsmp_message msg = { 0 }; + phys_addr_t dram_addr; + int ret; + + msg.sock_ind = sock_ind; + msg.response_sz = hsmp_msg_desc_table[HSMP_GET_METRIC_TABLE_DRAM_ADDR].response_sz; + msg.msg_id = HSMP_GET_METRIC_TABLE_DRAM_ADDR; + + ret = hsmp_send_message(&msg); + if (ret) + return ret; + + /* + * calculate the metric table DRAM address from lower and upper 32 bits + * sent from SMU and ioremap it to virtual address. + */ + dram_addr = msg.args[0] | ((u64)(msg.args[1]) << 32); + if (!dram_addr) { + dev_err(sock->dev, "Invalid DRAM address for metric table\n"); + return -ENOMEM; + } + sock->metric_tbl_addr = devm_ioremap(sock->dev, dram_addr, + sizeof(struct hsmp_metric_table)); + if (!sock->metric_tbl_addr) { + dev_err(sock->dev, "Failed to ioremap metric table addr\n"); + return -ENOMEM; + } + return 0; +} +EXPORT_SYMBOL_NS_GPL(hsmp_get_tbl_dram_base, "AMD_HSMP"); + +int hsmp_cache_proto_ver(u16 sock_ind) +{ + struct hsmp_message msg = { 0 }; + int ret; + + msg.msg_id = HSMP_GET_PROTO_VER; + msg.sock_ind = sock_ind; + msg.response_sz = hsmp_msg_desc_table[HSMP_GET_PROTO_VER].response_sz; + + ret = hsmp_send_message(&msg); + if (!ret) + hsmp_pdev.proto_ver = msg.args[0]; + + return ret; +} +EXPORT_SYMBOL_NS_GPL(hsmp_cache_proto_ver, "AMD_HSMP"); + +static const struct file_operations hsmp_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = hsmp_ioctl, + .compat_ioctl = hsmp_ioctl, +}; + +int hsmp_misc_register(struct device *dev) +{ + hsmp_pdev.mdev.name = HSMP_CDEV_NAME; + hsmp_pdev.mdev.minor = MISC_DYNAMIC_MINOR; + hsmp_pdev.mdev.fops = &hsmp_fops; + hsmp_pdev.mdev.parent = dev; + hsmp_pdev.mdev.nodename = HSMP_DEVNODE_NAME; + hsmp_pdev.mdev.mode = 0644; + + return misc_register(&hsmp_pdev.mdev); +} +EXPORT_SYMBOL_NS_GPL(hsmp_misc_register, "AMD_HSMP"); + +void hsmp_misc_deregister(void) +{ + misc_deregister(&hsmp_pdev.mdev); +} +EXPORT_SYMBOL_NS_GPL(hsmp_misc_deregister, "AMD_HSMP"); + +struct hsmp_plat_device *get_hsmp_pdev(void) +{ + return &hsmp_pdev; +} +EXPORT_SYMBOL_NS_GPL(get_hsmp_pdev, "AMD_HSMP"); + +MODULE_DESCRIPTION("AMD HSMP Common driver"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/amd/hsmp/hsmp.h b/drivers/platform/x86/amd/hsmp/hsmp.h new file mode 100644 index 000000000..0509a442e --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/hsmp.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * AMD HSMP Platform Driver + * Copyright (c) 2024, AMD. + * All Rights Reserved. + * + * Header file for HSMP driver + */ + +#ifndef HSMP_H +#define HSMP_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HSMP_METRICS_TABLE_NAME "metrics_bin" + +#define HSMP_ATTR_GRP_NAME_SIZE 10 + +#define HSMP_CDEV_NAME "hsmp_cdev" +#define HSMP_DEVNODE_NAME "hsmp" +#define ACPI_HSMP_DEVICE_HID "AMDI0097" + +#define DRIVER_VERSION "2.5" + +struct hsmp_mbaddr_info { + u32 base_addr; + u32 msg_id_off; + u32 msg_resp_off; + u32 msg_arg_off; + u32 size; +}; + +struct hsmp_socket { + struct bin_attribute hsmp_attr; + struct hsmp_mbaddr_info mbinfo; + void __iomem *metric_tbl_addr; + void __iomem *virt_base_addr; + struct semaphore hsmp_sem; + char name[HSMP_ATTR_GRP_NAME_SIZE]; + struct device *dev; + u16 sock_ind; + int (*amd_hsmp_rdwr)(struct hsmp_socket *sock, u32 off, u32 *val, bool rw); +}; + +struct hsmp_plat_device { + struct miscdevice mdev; + struct hsmp_socket *sock; + u32 proto_ver; + u16 num_sockets; + bool is_probed; +}; + +int hsmp_cache_proto_ver(u16 sock_ind); +int hsmp_test(u16 sock_ind, u32 value); +long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg); +void hsmp_misc_deregister(void); +int hsmp_misc_register(struct device *dev); +int hsmp_get_tbl_dram_base(u16 sock_ind); +ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size); +struct hsmp_plat_device *get_hsmp_pdev(void); +#if IS_ENABLED(CONFIG_HWMON) +int hsmp_create_sensor(struct device *dev, u16 sock_ind); +#else +static inline int hsmp_create_sensor(struct device *dev, u16 sock_ind) { return 0; } +#endif +int hsmp_msg_get_nargs(u16 sock_ind, u32 msg_id, u32 *data, u8 num_args); +#endif /* HSMP_H */ diff --git a/drivers/platform/x86/amd/hsmp/hwmon.c b/drivers/platform/x86/amd/hsmp/hwmon.c new file mode 100644 index 000000000..0cc9a7424 --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/hwmon.c @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD HSMP hwmon support + * Copyright (c) 2025, AMD. + * All Rights Reserved. + * + * This file provides hwmon implementation for HSMP interface. + */ + +#include + +#include +#include +#include +#include +#include + +#include "hsmp.h" + +#define HSMP_HWMON_NAME "amd_hsmp_hwmon" + +static int hsmp_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + u16 sock_ind = (uintptr_t)dev_get_drvdata(dev); + struct hsmp_message msg = {}; + + if (type != hwmon_power) + return -EOPNOTSUPP; + + if (attr != hwmon_power_cap) + return -EOPNOTSUPP; + + msg.num_args = 1; + msg.args[0] = val / MICROWATT_PER_MILLIWATT; + msg.msg_id = HSMP_SET_SOCKET_POWER_LIMIT; + msg.sock_ind = sock_ind; + return hsmp_send_message(&msg); +} + +static int hsmp_hwmon_read(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + u16 sock_ind = (uintptr_t)dev_get_drvdata(dev); + struct hsmp_message msg = {}; + int ret; + + if (type != hwmon_power) + return -EOPNOTSUPP; + + msg.sock_ind = sock_ind; + msg.response_sz = 1; + + switch (attr) { + case hwmon_power_input: + msg.msg_id = HSMP_GET_SOCKET_POWER; + break; + case hwmon_power_cap: + msg.msg_id = HSMP_GET_SOCKET_POWER_LIMIT; + break; + case hwmon_power_cap_max: + msg.msg_id = HSMP_GET_SOCKET_POWER_LIMIT_MAX; + break; + default: + return -EOPNOTSUPP; + } + + ret = hsmp_send_message(&msg); + if (!ret) + *val = msg.args[0] * MICROWATT_PER_MILLIWATT; + + return ret; +} + +static umode_t hsmp_hwmon_is_visble(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + if (type != hwmon_power) + return 0; + + switch (attr) { + case hwmon_power_input: + return 0444; + case hwmon_power_cap: + return 0644; + case hwmon_power_cap_max: + return 0444; + default: + return 0; + } +} + +static const struct hwmon_ops hsmp_hwmon_ops = { + .read = hsmp_hwmon_read, + .is_visible = hsmp_hwmon_is_visble, + .write = hsmp_hwmon_write, +}; + +static const struct hwmon_channel_info * const hsmp_info[] = { + HWMON_CHANNEL_INFO(power, HWMON_P_INPUT | HWMON_P_CAP | HWMON_P_CAP_MAX), + NULL +}; + +static const struct hwmon_chip_info hsmp_chip_info = { + .ops = &hsmp_hwmon_ops, + .info = hsmp_info, +}; + +int hsmp_create_sensor(struct device *dev, u16 sock_ind) +{ + struct device *hwmon_dev; + + hwmon_dev = devm_hwmon_device_register_with_info(dev, HSMP_HWMON_NAME, + (void *)(uintptr_t)sock_ind, + &hsmp_chip_info, + NULL); + return PTR_ERR_OR_ZERO(hwmon_dev); +} +EXPORT_SYMBOL_NS(hsmp_create_sensor, "AMD_HSMP"); diff --git a/drivers/platform/x86/amd/hsmp/plat.c b/drivers/platform/x86/amd/hsmp/plat.c new file mode 100644 index 000000000..e07f68575 --- /dev/null +++ b/drivers/platform/x86/amd/hsmp/plat.c @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD HSMP Platform Driver + * Copyright (c) 2024, AMD. + * All Rights Reserved. + * + * This file provides platform device implementations. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "hsmp.h" + +#define DRIVER_NAME "amd_hsmp" + +/* + * To access specific HSMP mailbox register, s/w writes the SMN address of HSMP mailbox + * register into the SMN_INDEX register, and reads/writes the SMN_DATA reg. + * Below are required SMN address for HSMP Mailbox register offsets in SMU address space + */ +#define SMN_HSMP_BASE 0x3B00000 +#define SMN_HSMP_MSG_ID 0x0010534 +#define SMN_HSMP_MSG_ID_F1A_M0H 0x0010934 +#define SMN_HSMP_MSG_RESP 0x0010980 +#define SMN_HSMP_MSG_DATA 0x00109E0 + +static struct hsmp_plat_device *hsmp_pdev; + +static int amd_hsmp_pci_rdwr(struct hsmp_socket *sock, u32 offset, + u32 *value, bool write) +{ + return amd_smn_hsmp_rdwr(sock->sock_ind, sock->mbinfo.base_addr + offset, value, write); +} + +static ssize_t hsmp_metric_tbl_plat_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, char *buf, + loff_t off, size_t count) +{ + struct hsmp_socket *sock; + u16 sock_ind; + + sock_ind = (uintptr_t)bin_attr->private; + if (sock_ind >= hsmp_pdev->num_sockets) + return -EINVAL; + + sock = &hsmp_pdev->sock[sock_ind]; + + return hsmp_metric_tbl_read(sock, buf, count); +} + +static umode_t hsmp_is_sock_attr_visible(struct kobject *kobj, + const struct bin_attribute *battr, int id) +{ + u16 sock_ind; + + sock_ind = (uintptr_t)battr->private; + + if (id == 0 && sock_ind >= hsmp_pdev->num_sockets) + return SYSFS_GROUP_INVISIBLE; + + if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) + return battr->attr.mode; + + return 0; +} + +/* + * AMD supports maximum of 8 sockets in a system. + * Static array of 8 + 1(for NULL) elements is created below + * to create sysfs groups for sockets. + * is_bin_visible function is used to show / hide the necessary groups. + * + * Validate the maximum number against MAX_AMD_NUM_NODES. If this changes, + * then the attributes and groups below must be adjusted. + */ +static_assert(MAX_AMD_NUM_NODES == 8); + +#define HSMP_BIN_ATTR(index, _list) \ +static const struct bin_attribute attr##index = { \ + .attr = { .name = HSMP_METRICS_TABLE_NAME, .mode = 0444}, \ + .private = (void *)index, \ + .read = hsmp_metric_tbl_plat_read, \ + .size = sizeof(struct hsmp_metric_table), \ +}; \ +static const struct bin_attribute _list[] = { \ + &attr##index, \ + NULL \ +} + +HSMP_BIN_ATTR(0, *sock0_attr_list); +HSMP_BIN_ATTR(1, *sock1_attr_list); +HSMP_BIN_ATTR(2, *sock2_attr_list); +HSMP_BIN_ATTR(3, *sock3_attr_list); +HSMP_BIN_ATTR(4, *sock4_attr_list); +HSMP_BIN_ATTR(5, *sock5_attr_list); +HSMP_BIN_ATTR(6, *sock6_attr_list); +HSMP_BIN_ATTR(7, *sock7_attr_list); + +#define HSMP_BIN_ATTR_GRP(index, _list, _name) \ +static const struct attribute_group sock##index##_attr_grp = { \ + .bin_attrs = _list, \ + .is_bin_visible = hsmp_is_sock_attr_visible, \ + .name = #_name, \ +} + +HSMP_BIN_ATTR_GRP(0, sock0_attr_list, socket0); +HSMP_BIN_ATTR_GRP(1, sock1_attr_list, socket1); +HSMP_BIN_ATTR_GRP(2, sock2_attr_list, socket2); +HSMP_BIN_ATTR_GRP(3, sock3_attr_list, socket3); +HSMP_BIN_ATTR_GRP(4, sock4_attr_list, socket4); +HSMP_BIN_ATTR_GRP(5, sock5_attr_list, socket5); +HSMP_BIN_ATTR_GRP(6, sock6_attr_list, socket6); +HSMP_BIN_ATTR_GRP(7, sock7_attr_list, socket7); + +static const struct attribute_group *hsmp_groups[] = { + &sock0_attr_grp, + &sock1_attr_grp, + &sock2_attr_grp, + &sock3_attr_grp, + &sock4_attr_grp, + &sock5_attr_grp, + &sock6_attr_grp, + &sock7_attr_grp, + NULL +}; + +static inline bool is_f1a_m0h(void) +{ + if (boot_cpu_data.x86 == 0x1A && boot_cpu_data.x86_model <= 0x0F) + return true; + + return false; +} + +static int init_platform_device(struct device *dev) +{ + struct hsmp_socket *sock; + int ret, i; + + for (i = 0; i < hsmp_pdev->num_sockets; i++) { + sock = &hsmp_pdev->sock[i]; + sock->sock_ind = i; + sock->dev = dev; + sock->mbinfo.base_addr = SMN_HSMP_BASE; + sock->amd_hsmp_rdwr = amd_hsmp_pci_rdwr; + + /* + * This is a transitional change from non-ACPI to ACPI, only + * family 0x1A, model 0x00 platform is supported for both ACPI and non-ACPI. + */ + if (is_f1a_m0h()) + sock->mbinfo.msg_id_off = SMN_HSMP_MSG_ID_F1A_M0H; + else + sock->mbinfo.msg_id_off = SMN_HSMP_MSG_ID; + + sock->mbinfo.msg_resp_off = SMN_HSMP_MSG_RESP; + sock->mbinfo.msg_arg_off = SMN_HSMP_MSG_DATA; + sema_init(&sock->hsmp_sem, 1); + + /* Test the hsmp interface on each socket */ + ret = hsmp_test(i, 0xDEADBEEF); + if (ret) { + dev_err(dev, "HSMP test message failed on Fam:%x model:%x\n", + boot_cpu_data.x86, boot_cpu_data.x86_model); + dev_err(dev, "Is HSMP disabled in BIOS ?\n"); + return ret; + } + + ret = hsmp_cache_proto_ver(i); + if (ret) { + dev_err(dev, "Failed to read HSMP protocol version\n"); + return ret; + } + + if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) { + ret = hsmp_get_tbl_dram_base(i); + if (ret) + dev_info(dev, "Failed to init metric table\n"); + } + + /* Register with hwmon interface for reporting power */ + ret = hsmp_create_sensor(dev, i); + if (ret) + dev_info(dev, "Failed to register HSMP sensors with hwmon\n"); + } + + return 0; +} + +static int hsmp_pltdrv_probe(struct platform_device *pdev) +{ + int ret; + + hsmp_pdev->sock = devm_kcalloc(&pdev->dev, hsmp_pdev->num_sockets, + sizeof(*hsmp_pdev->sock), + GFP_KERNEL); + if (!hsmp_pdev->sock) + return -ENOMEM; + + ret = init_platform_device(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "Failed to init HSMP mailbox\n"); + return ret; + } + + ret = hsmp_misc_register(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "Failed to register misc device\n"); + return ret; + } + + dev_dbg(&pdev->dev, "AMD HSMP is probed successfully\n"); + return 0; +} + +static void hsmp_pltdrv_remove(struct platform_device *pdev) +{ + hsmp_misc_deregister(); +} + +static struct platform_driver amd_hsmp_driver = { + .probe = hsmp_pltdrv_probe, + .remove = hsmp_pltdrv_remove, + .driver = { + .name = DRIVER_NAME, + .dev_groups = hsmp_groups, + }, +}; + +static struct platform_device *amd_hsmp_platdev; + +static int hsmp_plat_dev_register(void) +{ + int ret; + + amd_hsmp_platdev = platform_device_alloc(DRIVER_NAME, PLATFORM_DEVID_NONE); + if (!amd_hsmp_platdev) + return -ENOMEM; + + ret = platform_device_add(amd_hsmp_platdev); + if (ret) + platform_device_put(amd_hsmp_platdev); + + return ret; +} + +/* + * This check is only needed for backward compatibility of previous platforms. + * All new platforms are expected to support ACPI based probing. + */ +static bool legacy_hsmp_support(void) +{ + if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD) + return false; + + switch (boot_cpu_data.x86) { + case 0x19: + switch (boot_cpu_data.x86_model) { + case 0x00 ... 0x1F: + case 0x30 ... 0x3F: + case 0x90 ... 0x9F: + case 0xA0 ... 0xAF: + return true; + default: + return false; + } + case 0x1A: + switch (boot_cpu_data.x86_model) { + case 0x00 ... 0x0F: + return true; + default: + return false; + } + default: + return false; + } + + return false; +} + +static int __init hsmp_plt_init(void) +{ + int ret = -ENODEV; + + if (acpi_dev_present(ACPI_HSMP_DEVICE_HID, NULL, -1)) { + if (IS_ENABLED(CONFIG_AMD_HSMP_ACPI)) + pr_debug("HSMP is supported through ACPI on this platform, please use hsmp_acpi.ko\n"); + else + pr_info("HSMP is supported through ACPI on this platform, please enable AMD_HSMP_ACPI config\n"); + return -ENODEV; + } + + if (!legacy_hsmp_support()) { + pr_info("HSMP interface is either disabled or not supported on family:%x model:%x\n", + boot_cpu_data.x86, boot_cpu_data.x86_model); + return ret; + } + + hsmp_pdev = get_hsmp_pdev(); + if (!hsmp_pdev) + return -ENOMEM; + + /* + * amd_num_nodes() returns number of SMN/DF interfaces present in the system + * if we have N SMN/DF interfaces that ideally means N sockets + */ + hsmp_pdev->num_sockets = amd_num_nodes(); + if (hsmp_pdev->num_sockets == 0 || hsmp_pdev->num_sockets > MAX_AMD_NUM_NODES) { + pr_err("Wrong number of sockets\n"); + return ret; + } + + ret = platform_driver_register(&amd_hsmp_driver); + if (ret) + return ret; + + ret = hsmp_plat_dev_register(); + if (ret) + platform_driver_unregister(&amd_hsmp_driver); + + return ret; +} + +static void __exit hsmp_plt_exit(void) +{ + platform_device_unregister(amd_hsmp_platdev); + platform_driver_unregister(&amd_hsmp_driver); +} + +device_initcall(hsmp_plt_init); +module_exit(hsmp_plt_exit); + +MODULE_IMPORT_NS("AMD_HSMP"); +MODULE_DESCRIPTION("AMD HSMP Platform Interface Driver"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/amd/pmc/Kconfig b/drivers/platform/x86/amd/pmc/Kconfig new file mode 100644 index 000000000..eeffdafd6 --- /dev/null +++ b/drivers/platform/x86/amd/pmc/Kconfig @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# AMD PMC Driver +# + +config AMD_PMC + tristate "AMD SoC PMC driver" + depends on ACPI && PCI && RTC_CLASS && AMD_NODE + depends on SUSPEND + select SERIO + help + The driver provides support for AMD Power Management Controller + primarily responsible for S2Idle transactions that are driven from + a platform firmware running on SMU. This driver also provides a debug + mechanism to investigate the S2Idle transactions and failures. + + Say Y or M here if you have a notebook powered by AMD RYZEN CPU/APU. + + If you choose to compile this driver as a module the module will be + called amd-pmc. + +config AMD_MP2_STB + bool "AMD SoC MP2 STB function" + depends on AMD_PMC + default AMD_PMC + help + AMD MP2 STB function provides a data buffer used to log debug + information about the system execution during S2Idle suspend/resume. + A data buffer known as the STB (Smart Trace Buffer) is a circular + buffer which is a low-level log for the SoC which is used to debug + any hangs/stalls during S2Idle suspend/resume. + + Creates debugfs to get STB, a userspace daemon can access STB log of + last S2Idle suspend/resume which can help to debug if hangs/stalls + during S2Idle suspend/resume. diff --git a/drivers/platform/x86/amd/pmc/Makefile b/drivers/platform/x86/amd/pmc/Makefile new file mode 100644 index 000000000..bb6905c4c --- /dev/null +++ b/drivers/platform/x86/amd/pmc/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/amd/pmc +# AMD Power Management Controller Driver +# + +obj-$(CONFIG_AMD_PMC) += amd-pmc.o +amd-pmc-y := pmc.o pmc-quirks.o mp1_stb.o +amd-pmc-$(CONFIG_AMD_MP2_STB) += mp2_stb.o diff --git a/drivers/platform/x86/amd/pmc/mp1_stb.c b/drivers/platform/x86/amd/pmc/mp1_stb.c new file mode 100644 index 000000000..753d630f3 --- /dev/null +++ b/drivers/platform/x86/amd/pmc/mp1_stb.c @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD MP1 Smart Trace Buffer (STB) Layer + * + * Copyright (c) 2024, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Authors: Shyam Sundar S K + * Sanket Goswami + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include + +#include "pmc.h" + +/* STB Spill to DRAM Parameters */ +#define S2D_TELEMETRY_DRAMBYTES_MAX 0x1000000 +#define S2D_TELEMETRY_BYTES_MAX 0x100000U +#define S2D_RSVD_RAM_SPACE 0x100000 + +/* STB Registers */ +#define AMD_STB_PMI_0 0x03E30600 +#define AMD_PMC_STB_DUMMY_PC 0xC6000007 + +/* STB Spill to DRAM Message Definition */ +#define STB_FORCE_FLUSH_DATA 0xCF +#define FIFO_SIZE 4096 + +/* STB S2D(Spill to DRAM) has different message port offset */ +#define AMD_S2D_REGISTER_MESSAGE 0xA20 +#define AMD_S2D_REGISTER_RESPONSE 0xA80 +#define AMD_S2D_REGISTER_ARGUMENT 0xA88 + +/* STB S2D (Spill to DRAM) message port offset for 44h model */ +#define AMD_GNR_REGISTER_MESSAGE 0x524 +#define AMD_GNR_REGISTER_RESPONSE 0x570 +#define AMD_GNR_REGISTER_ARGUMENT 0xA40 + +static bool enable_stb; +module_param(enable_stb, bool, 0644); +MODULE_PARM_DESC(enable_stb, "Enable the STB debug mechanism"); + +static bool dump_custom_stb; +module_param(dump_custom_stb, bool, 0644); +MODULE_PARM_DESC(dump_custom_stb, "Enable to dump full STB buffer"); + +enum s2d_arg { + S2D_TELEMETRY_SIZE = 0x01, + S2D_PHYS_ADDR_LOW, + S2D_PHYS_ADDR_HIGH, + S2D_NUM_SAMPLES, + S2D_DRAM_SIZE, +}; + +struct amd_stb_v2_data { + size_t size; + u8 data[] __counted_by(size); +}; + +int amd_stb_write(struct amd_pmc_dev *dev, u32 data) +{ + int err; + + err = amd_smn_write(0, AMD_STB_PMI_0, data); + if (err) { + dev_err(dev->dev, "failed to write data in stb: 0x%X\n", AMD_STB_PMI_0); + return pcibios_err_to_errno(err); + } + + return 0; +} + +int amd_stb_read(struct amd_pmc_dev *dev, u32 *buf) +{ + int i, err; + + for (i = 0; i < FIFO_SIZE; i++) { + err = amd_smn_read(0, AMD_STB_PMI_0, buf++); + if (err) { + dev_err(dev->dev, "error reading data from stb: 0x%X\n", AMD_STB_PMI_0); + return pcibios_err_to_errno(err); + } + } + + return 0; +} + +static int amd_stb_debugfs_open(struct inode *inode, struct file *filp) +{ + struct amd_pmc_dev *dev = filp->f_inode->i_private; + u32 size = FIFO_SIZE * sizeof(u32); + u32 *buf; + int rc; + + buf = kzalloc(size, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + rc = amd_stb_read(dev, buf); + if (rc) { + kfree(buf); + return rc; + } + + filp->private_data = buf; + return rc; +} + +static ssize_t amd_stb_debugfs_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) +{ + if (!filp->private_data) + return -EINVAL; + + return simple_read_from_buffer(buf, size, pos, filp->private_data, + FIFO_SIZE * sizeof(u32)); +} + +static int amd_stb_debugfs_release(struct inode *inode, struct file *filp) +{ + kfree(filp->private_data); + return 0; +} + +static const struct file_operations amd_stb_debugfs_fops = { + .owner = THIS_MODULE, + .open = amd_stb_debugfs_open, + .read = amd_stb_debugfs_read, + .release = amd_stb_debugfs_release, +}; + +/* Enhanced STB Firmware Reporting Mechanism */ +static int amd_stb_handle_efr(struct file *filp) +{ + struct amd_pmc_dev *dev = filp->f_inode->i_private; + struct amd_stb_v2_data *stb_data_arr; + u32 fsize; + + fsize = dev->dram_size - S2D_RSVD_RAM_SPACE; + stb_data_arr = kmalloc_flex(*stb_data_arr, data, fsize); + if (!stb_data_arr) + return -ENOMEM; + + stb_data_arr->size = fsize; + memcpy_fromio(stb_data_arr->data, dev->stb_virt_addr, fsize); + filp->private_data = stb_data_arr; + + return 0; +} + +static int amd_stb_debugfs_open_v2(struct inode *inode, struct file *filp) +{ + struct amd_pmc_dev *dev = filp->f_inode->i_private; + u32 fsize, num_samples, val, stb_rdptr_offset = 0; + struct amd_stb_v2_data *stb_data_arr; + int ret; + + /* Write dummy postcode while reading the STB buffer */ + ret = amd_stb_write(dev, AMD_PMC_STB_DUMMY_PC); + if (ret) + dev_err(dev->dev, "error writing to STB: %d\n", ret); + + /* Spill to DRAM num_samples uses separate SMU message port */ + dev->msg_port = MSG_PORT_S2D; + + ret = amd_pmc_send_cmd(dev, 0, &val, STB_FORCE_FLUSH_DATA, 1); + if (ret) + dev_dbg_once(dev->dev, "S2D force flush not supported: %d\n", ret); + + /* + * We have a custom stb size and the PMFW is supposed to give + * the enhanced dram size. Note that we land here only for the + * platforms that support enhanced dram size reporting. + */ + if (dump_custom_stb) + return amd_stb_handle_efr(filp); + + /* Get the num_samples to calculate the last push location */ + ret = amd_pmc_send_cmd(dev, S2D_NUM_SAMPLES, &num_samples, dev->stb_arg.s2d_msg_id, true); + /* Clear msg_port for other SMU operation */ + dev->msg_port = MSG_PORT_PMC; + if (ret) { + dev_err(dev->dev, "error: S2D_NUM_SAMPLES not supported : %d\n", ret); + return ret; + } + + fsize = min(num_samples, S2D_TELEMETRY_BYTES_MAX); + stb_data_arr = kmalloc_flex(*stb_data_arr, data, fsize); + if (!stb_data_arr) + return -ENOMEM; + + stb_data_arr->size = fsize; + + /* + * Start capturing data from the last push location. + * This is for general cases, where the stb limits + * are meant for standard usage. + */ + if (num_samples > S2D_TELEMETRY_BYTES_MAX) { + /* First read oldest data starting 1 behind last write till end of ringbuffer */ + stb_rdptr_offset = num_samples % S2D_TELEMETRY_BYTES_MAX; + fsize = S2D_TELEMETRY_BYTES_MAX - stb_rdptr_offset; + + memcpy_fromio(stb_data_arr->data, dev->stb_virt_addr + stb_rdptr_offset, fsize); + /* Second copy the newer samples from offset 0 - last write */ + memcpy_fromio(stb_data_arr->data + fsize, dev->stb_virt_addr, stb_rdptr_offset); + } else { + memcpy_fromio(stb_data_arr->data, dev->stb_virt_addr, fsize); + } + + filp->private_data = stb_data_arr; + + return 0; +} + +static ssize_t amd_stb_debugfs_read_v2(struct file *filp, char __user *buf, size_t size, + loff_t *pos) +{ + struct amd_stb_v2_data *data = filp->private_data; + + return simple_read_from_buffer(buf, size, pos, data->data, data->size); +} + +static int amd_stb_debugfs_release_v2(struct inode *inode, struct file *filp) +{ + kfree(filp->private_data); + return 0; +} + +static const struct file_operations amd_stb_debugfs_fops_v2 = { + .owner = THIS_MODULE, + .open = amd_stb_debugfs_open_v2, + .read = amd_stb_debugfs_read_v2, + .release = amd_stb_debugfs_release_v2, +}; + +static void amd_stb_update_args(struct amd_pmc_dev *dev) +{ + if (cpu_feature_enabled(X86_FEATURE_ZEN5)) + switch (boot_cpu_data.x86_model) { + case 0x44: + dev->stb_arg.msg = AMD_GNR_REGISTER_MESSAGE; + dev->stb_arg.arg = AMD_GNR_REGISTER_ARGUMENT; + dev->stb_arg.resp = AMD_GNR_REGISTER_RESPONSE; + return; + default: + break; + } + + dev->stb_arg.msg = AMD_S2D_REGISTER_MESSAGE; + dev->stb_arg.arg = AMD_S2D_REGISTER_ARGUMENT; + dev->stb_arg.resp = AMD_S2D_REGISTER_RESPONSE; +} + +static bool amd_is_stb_supported(struct amd_pmc_dev *dev) +{ + switch (dev->cpu_id) { + case AMD_CPU_ID_YC: + case AMD_CPU_ID_CB: + if (boot_cpu_data.x86_model == 0x44) + dev->stb_arg.s2d_msg_id = 0x9B; + else + dev->stb_arg.s2d_msg_id = 0xBE; + break; + case AMD_CPU_ID_PS: + dev->stb_arg.s2d_msg_id = 0x85; + break; + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + if (boot_cpu_data.x86_model == 0x70) + dev->stb_arg.s2d_msg_id = 0xF1; + else + dev->stb_arg.s2d_msg_id = 0xDE; + break; + default: + return false; + } + + amd_stb_update_args(dev); + return true; +} + +int amd_stb_s2d_init(struct amd_pmc_dev *dev) +{ + u32 phys_addr_low, phys_addr_hi; + u64 stb_phys_addr; + u32 size = 0; + int ret; + + if (!enable_stb) + return 0; + + if (amd_is_stb_supported(dev)) { + debugfs_create_file("stb_read", 0644, dev->dbgfs_dir, dev, + &amd_stb_debugfs_fops_v2); + } else { + debugfs_create_file("stb_read", 0644, dev->dbgfs_dir, dev, + &amd_stb_debugfs_fops); + return 0; + } + + /* Spill to DRAM feature uses separate SMU message port */ + dev->msg_port = MSG_PORT_S2D; + + amd_pmc_send_cmd(dev, S2D_TELEMETRY_SIZE, &size, dev->stb_arg.s2d_msg_id, true); + if (size != S2D_TELEMETRY_BYTES_MAX) + return -EIO; + + /* Get DRAM size */ + ret = amd_pmc_send_cmd(dev, S2D_DRAM_SIZE, &dev->dram_size, dev->stb_arg.s2d_msg_id, true); + if (ret || !dev->dram_size) + dev->dram_size = S2D_TELEMETRY_DRAMBYTES_MAX; + + /* Get STB DRAM address */ + amd_pmc_send_cmd(dev, S2D_PHYS_ADDR_LOW, &phys_addr_low, dev->stb_arg.s2d_msg_id, true); + amd_pmc_send_cmd(dev, S2D_PHYS_ADDR_HIGH, &phys_addr_hi, dev->stb_arg.s2d_msg_id, true); + + stb_phys_addr = ((u64)phys_addr_hi << 32 | phys_addr_low); + + /* Clear msg_port for other SMU operation */ + dev->msg_port = MSG_PORT_PMC; + + dev->stb_virt_addr = devm_ioremap(dev->dev, stb_phys_addr, dev->dram_size); + if (!dev->stb_virt_addr) + return -ENOMEM; + + return 0; +} diff --git a/drivers/platform/x86/amd/pmc/mp2_stb.c b/drivers/platform/x86/amd/pmc/mp2_stb.c new file mode 100644 index 000000000..9775ddc1b --- /dev/null +++ b/drivers/platform/x86/amd/pmc/mp2_stb.c @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD MP2 STB layer + * + * Copyright (c) 2024, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Basavaraj Natikar + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pmc.h" + +#define VALID_MSG 0xA +#define VALID_RESPONSE 2 + +#define AMD_C2P_MSG0 0x10500 +#define AMD_C2P_MSG1 0x10504 +#define AMD_P2C_MSG0 0x10680 +#define AMD_P2C_MSG1 0x10684 + +#define MP2_RESP_SLEEP_US 500 +#define MP2_RESP_TIMEOUT_US (1600 * USEC_PER_MSEC) + +#define MP2_STB_DATA_LEN_2KB 1 +#define MP2_STB_DATA_LEN_16KB 4 + +#define MP2_MMIO_BAR 2 + +struct mp2_cmd_base { + union { + u32 ul; + struct { + u32 cmd_id : 4; + u32 intr_disable : 1; + u32 is_dma_used : 1; + u32 rsvd : 26; + } field; + }; +}; + +struct mp2_cmd_response { + union { + u32 resp; + struct { + u32 cmd_id : 4; + u32 status : 4; + u32 response : 4; + u32 rsvd2 : 20; + } field; + }; +}; + +struct mp2_stb_data_valid { + union { + u32 data_valid; + struct { + u32 valid : 16; + u32 length : 16; + } val; + }; +}; + +static int amd_mp2_wait_response(struct amd_mp2_dev *mp2, u8 cmd_id, u32 command_sts) +{ + struct mp2_cmd_response cmd_resp; + + if (!readl_poll_timeout(mp2->mmio + AMD_P2C_MSG0, cmd_resp.resp, + (cmd_resp.field.response == 0x0 && + cmd_resp.field.status == command_sts && + cmd_resp.field.cmd_id == cmd_id), MP2_RESP_SLEEP_US, + MP2_RESP_TIMEOUT_US)) + return cmd_resp.field.status; + + return -ETIMEDOUT; +} + +static void amd_mp2_stb_send_cmd(struct amd_mp2_dev *mp2, u8 cmd_id, bool is_dma_used) +{ + struct mp2_cmd_base cmd_base; + + cmd_base.ul = 0; + cmd_base.field.cmd_id = cmd_id; + cmd_base.field.intr_disable = 1; + cmd_base.field.is_dma_used = is_dma_used; + + writeq(mp2->dma_addr, mp2->mmio + AMD_C2P_MSG1); + writel(cmd_base.ul, mp2->mmio + AMD_C2P_MSG0); +} + +static int amd_mp2_stb_region(struct amd_mp2_dev *mp2) +{ + struct device *dev = &mp2->pdev->dev; + unsigned int len = mp2->stb_len; + + if (!mp2->stbdata) { + mp2->vslbase = dmam_alloc_coherent(dev, len, &mp2->dma_addr, GFP_KERNEL); + if (!mp2->vslbase) + return -ENOMEM; + + mp2->stbdata = devm_kzalloc(dev, len, GFP_KERNEL); + if (!mp2->stbdata) + return -ENOMEM; + } + + return 0; +} + +static int amd_mp2_process_cmd(struct amd_mp2_dev *mp2, struct file *filp) +{ + struct device *dev = &mp2->pdev->dev; + struct mp2_stb_data_valid stb_dv; + int status; + + stb_dv.data_valid = readl(mp2->mmio + AMD_P2C_MSG1); + + if (stb_dv.val.valid != VALID_MSG) { + dev_dbg(dev, "Invalid STB data\n"); + return -EBADMSG; + } + + if (stb_dv.val.length != MP2_STB_DATA_LEN_2KB && + stb_dv.val.length != MP2_STB_DATA_LEN_16KB) { + dev_dbg(dev, "Unsupported length\n"); + return -EMSGSIZE; + } + + mp2->stb_len = BIT(stb_dv.val.length) * SZ_1K; + + status = amd_mp2_stb_region(mp2); + if (status) { + dev_err(dev, "Failed to init STB region, status %d\n", status); + return status; + } + + amd_mp2_stb_send_cmd(mp2, VALID_MSG, true); + status = amd_mp2_wait_response(mp2, VALID_MSG, VALID_RESPONSE); + if (status == VALID_RESPONSE) { + memcpy_fromio(mp2->stbdata, mp2->vslbase, mp2->stb_len); + filp->private_data = mp2->stbdata; + mp2->is_stb_data = true; + } else { + dev_err(dev, "Failed to start STB dump, status %d\n", status); + return -EOPNOTSUPP; + } + + return 0; +} + +static int amd_mp2_stb_debugfs_open(struct inode *inode, struct file *filp) +{ + struct amd_pmc_dev *dev = filp->f_inode->i_private; + struct amd_mp2_dev *mp2 = dev->mp2; + + if (mp2) { + if (!mp2->is_stb_data) + return amd_mp2_process_cmd(mp2, filp); + + filp->private_data = mp2->stbdata; + + return 0; + } + + return -ENODEV; +} + +static ssize_t amd_mp2_stb_debugfs_read(struct file *filp, char __user *buf, size_t size, + loff_t *pos) +{ + struct amd_pmc_dev *dev = filp->f_inode->i_private; + struct amd_mp2_dev *mp2 = dev->mp2; + + if (!mp2) + return -ENODEV; + + if (!filp->private_data) + return -EINVAL; + + return simple_read_from_buffer(buf, size, pos, filp->private_data, mp2->stb_len); +} + +static const struct file_operations amd_mp2_stb_debugfs_fops = { + .owner = THIS_MODULE, + .open = amd_mp2_stb_debugfs_open, + .read = amd_mp2_stb_debugfs_read, +}; + +static void amd_mp2_dbgfs_register(struct amd_pmc_dev *dev) +{ + if (!dev->dbgfs_dir) + return; + + debugfs_create_file("stb_read_previous_boot", 0644, dev->dbgfs_dir, dev, + &amd_mp2_stb_debugfs_fops); +} + +void amd_mp2_stb_deinit(struct amd_pmc_dev *dev) +{ + struct amd_mp2_dev *mp2 = dev->mp2; + struct pci_dev *pdev; + + if (mp2 && mp2->pdev) { + pdev = mp2->pdev; + + if (mp2->mmio) + pci_clear_master(pdev); + + pci_dev_put(pdev); + + if (mp2->devres_gid) + devres_release_group(&pdev->dev, mp2->devres_gid); + + dev->mp2 = NULL; + } +} + +void amd_mp2_stb_init(struct amd_pmc_dev *dev) +{ + struct amd_mp2_dev *mp2 = NULL; + struct pci_dev *pdev; + int rc; + + mp2 = devm_kzalloc(dev->dev, sizeof(*mp2), GFP_KERNEL); + if (!mp2) + return; + + pdev = pci_get_device(PCI_VENDOR_ID_AMD, PCI_DEVICE_ID_AMD_MP2_STB, NULL); + if (!pdev) + return; + + dev->mp2 = mp2; + mp2->pdev = pdev; + + mp2->devres_gid = devres_open_group(&pdev->dev, NULL, GFP_KERNEL); + if (!mp2->devres_gid) { + dev_err(&pdev->dev, "devres_open_group failed\n"); + goto mp2_error; + } + + rc = pcim_enable_device(pdev); + if (rc) { + dev_err(&pdev->dev, "pcim_enable_device failed\n"); + goto mp2_error; + } + + rc = pcim_iomap_regions(pdev, BIT(MP2_MMIO_BAR), "mp2 stb"); + if (rc) { + dev_err(&pdev->dev, "pcim_iomap_regions failed\n"); + goto mp2_error; + } + + mp2->mmio = pcim_iomap_table(pdev)[MP2_MMIO_BAR]; + if (!mp2->mmio) { + dev_err(&pdev->dev, "pcim_iomap_table failed\n"); + goto mp2_error; + } + + pci_set_master(pdev); + + rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); + if (rc) { + dev_err(&pdev->dev, "failed to set DMA mask\n"); + goto mp2_error; + } + + amd_mp2_dbgfs_register(dev); + + return; + +mp2_error: + amd_mp2_stb_deinit(dev); +} diff --git a/drivers/platform/x86/amd/pmc/pmc-quirks.c b/drivers/platform/x86/amd/pmc/pmc-quirks.c new file mode 100644 index 000000000..74ddf1d82 --- /dev/null +++ b/drivers/platform/x86/amd/pmc/pmc-quirks.c @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD SoC Power Management Controller Driver Quirks + * + * Copyright (c) 2023, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Mario Limonciello + */ + +#include +#include +#include +#include + +#include "pmc.h" + +struct quirk_entry { + u32 s2idle_bug_mmio; + bool spurious_8042; + bool need_suspend_delay; +}; + +static struct quirk_entry quirk_s2idle_bug = { + .s2idle_bug_mmio = FCH_PM_BASE + FCH_PM_SCRATCH, +}; + +static struct quirk_entry quirk_spurious_8042 = { + .spurious_8042 = true, +}; + +static struct quirk_entry quirk_s2idle_spurious_8042 = { + .s2idle_bug_mmio = FCH_PM_BASE + FCH_PM_SCRATCH, + .spurious_8042 = true, +}; + +static struct quirk_entry quirk_s2idle_need_suspend_delay = { + .need_suspend_delay = true, +}; + +static const struct dmi_system_id fwbug_list[] = { + { + .ident = "L14 Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20X5"), + } + }, + { + .ident = "T14s Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20XF"), + } + }, + { + .ident = "X13 Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20XH"), + } + }, + { + .ident = "T14 Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20XK"), + } + }, + { + .ident = "T14 Gen1 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20UD"), + } + }, + { + .ident = "T14 Gen1 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20UE"), + } + }, + { + .ident = "T14s Gen1 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20UH"), + } + }, + { + .ident = "T14s Gen1 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20UJ"), + } + }, + { + .ident = "P14s Gen1 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "20Y1"), + } + }, + { + .ident = "P14s Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "21A0"), + } + }, + { + .ident = "P14s Gen2 AMD", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "21A1"), + } + }, + { + .ident = "ROG Xbox Ally RC73YA", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_BOARD_NAME, "RC73YA"), + } + }, + /* https://bugzilla.kernel.org/show_bug.cgi?id=218024 */ + { + .ident = "V14 G4 AMN", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82YT"), + } + }, + { + .ident = "V14 G4 AMN", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83GE"), + } + }, + { + .ident = "V15 G4 AMN", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82YU"), + } + }, + { + .ident = "V15 G4 AMN", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83CQ"), + } + }, + { + .ident = "IdeaPad 1 14AMN7", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82VF"), + } + }, + { + .ident = "IdeaPad 1 15AMN7", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82VG"), + } + }, + { + .ident = "IdeaPad 1 15AMN7", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82X5"), + } + }, + { + .ident = "IdeaPad Slim 3 14AMN8", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82XN"), + } + }, + { + .ident = "IdeaPad Slim 3 15AMN8", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82XQ"), + } + }, + /* https://bugzilla.kernel.org/show_bug.cgi?id=221383 */ + { + .ident = "Zen3-based IdeaPad Slim and similar", + .driver_data = &quirk_s2idle_need_suspend_delay, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + /* + * Note: there are also some Zen2-based 82X* devices that + * need different quirks, they're already handled above + */ + DMI_MATCH(DMI_PRODUCT_NAME, "82X"), + } + }, + { + .ident = "Zen3+-based IdeaPad Slim and similar", + .driver_data = &quirk_s2idle_need_suspend_delay, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83K"), + } + }, + { + .ident = "IdeaPad Slim 3 15ARP10 (83MM)", + .driver_data = &quirk_s2idle_need_suspend_delay, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83MM"), + } + }, + /* https://bugzilla.kernel.org/show_bug.cgi?id=221273 */ + { + .ident = "Thinkpad L14 Gen3", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "21C6"), + } + }, + /* https://gitlab.freedesktop.org/drm/amd/-/issues/4434 */ + { + .ident = "Lenovo Yoga 6 13ALC6", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82ND"), + } + }, + /* https://gitlab.freedesktop.org/drm/amd/-/issues/4618 */ + { + .ident = "Lenovo Legion Go 2", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83N0"), + } + }, + { + .ident = "Lenovo Legion Go 2", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83N1"), + } + }, + /* https://gitlab.freedesktop.org/drm/amd/-/issues/2684 */ + { + .ident = "HP Laptop 15s-eq2xxx", + .driver_data = &quirk_s2idle_spurious_8042, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Laptop 15s-eq2xxx"), + } + }, + /* https://community.frame.work/t/tracking-framework-amd-ryzen-7040-series-lid-wakeup-behavior-feedback/39128 */ + { + .ident = "Framework Laptop 13 (Phoenix)", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_MATCH(DMI_PRODUCT_NAME, "Laptop 13 (AMD Ryzen 7040Series)"), + DMI_MATCH(DMI_BIOS_VERSION, "03.03"), + } + }, + { + .ident = "Framework Laptop 13 (Phoenix)", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Framework"), + DMI_MATCH(DMI_PRODUCT_NAME, "Laptop 13 (AMD Ryzen 7040Series)"), + DMI_MATCH(DMI_BIOS_VERSION, "03.05"), + } + }, + { + .ident = "MECHREVO Wujie 14X (GX4HRXL)", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "WUJIE14-GX4HRXL"), + } + }, + { + .ident = "MECHREVO Yilong15Pro Series GM5HG7A", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MECHREVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "Yilong15Pro Series GM5HG7A"), + } + }, + /* https://bugzilla.kernel.org/show_bug.cgi?id=220116 */ + { + .ident = "PCSpecialist Lafite Pro V 14M", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PCSpecialist"), + DMI_MATCH(DMI_PRODUCT_NAME, "Lafite Pro V 14M"), + } + }, + { + .ident = "TUXEDO Stellaris Slim 15 AMD Gen6", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GMxHGxx"), + } + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 AMD Gen10", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "XxHP4NAx"), + } + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 AMD Gen10", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "XxKK4NAx_XxSP4NAx"), + } + }, + { + .ident = "MECHREVO Wujie 15X Pro", + .driver_data = &quirk_spurious_8042, + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "WUJIE Series-X5SP4NAG"), + } + }, + {} +}; + +/* + * Laptops that run a SMI handler during the D3->D0 transition that occurs + * specifically when exiting suspend to idle which can cause + * large delays during resume when the IOMMU translation layer is enabled (the default + * behavior) for NVME devices: + * + * To avoid this firmware problem, skip the SMI handler on these machines before the + * D0 transition occurs. + */ +static void amd_pmc_skip_nvme_smi_handler(u32 s2idle_bug_mmio) +{ + void __iomem *addr; + u8 val; + + if (!request_mem_region_muxed(s2idle_bug_mmio, 1, "amd_pmc_pm80")) + return; + + addr = ioremap(s2idle_bug_mmio, 1); + if (!addr) + goto cleanup_resource; + + val = ioread8(addr); + iowrite8(val & ~BIT(0), addr); + + iounmap(addr); +cleanup_resource: + release_mem_region(s2idle_bug_mmio, 1); +} + +void amd_pmc_process_restore_quirks(struct amd_pmc_dev *dev) +{ + if (dev->quirks && dev->quirks->s2idle_bug_mmio) + amd_pmc_skip_nvme_smi_handler(dev->quirks->s2idle_bug_mmio); +} + +bool amd_pmc_quirk_need_suspend_delay(struct amd_pmc_dev *dev) +{ + return dev->quirks && dev->quirks->need_suspend_delay; +} + +void amd_pmc_quirks_init(struct amd_pmc_dev *dev) +{ + const struct dmi_system_id *dmi_id; + + /* + * IRQ1 may cause an interrupt during resume even without a keyboard + * press. + * + * Affects Renoir, Cezanne and Barcelo SoCs + * + * A solution is available in PMFW 64.66.0, but it must be activated by + * SBIOS. If SBIOS is known to have the fix a quirk can be added for + * a given system to avoid workaround. + */ + if (dev->cpu_id == AMD_CPU_ID_CZN) + dev->disable_8042_wakeup = true; + + dmi_id = dmi_first_match(fwbug_list); + if (!dmi_id) + return; + dev->quirks = dmi_id->driver_data; + if (dev->quirks->s2idle_bug_mmio) + pr_info("Using s2idle quirk to avoid %s platform firmware bug\n", + dmi_id->ident); + dev->disable_8042_wakeup = dev->quirks->spurious_8042; +} diff --git a/drivers/platform/x86/amd/pmc/pmc.c b/drivers/platform/x86/amd/pmc/pmc.c new file mode 100644 index 000000000..a37083cdb --- /dev/null +++ b/drivers/platform/x86/amd/pmc/pmc.c @@ -0,0 +1,996 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD SoC Power Management Controller Driver + * + * Copyright (c) 2020, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#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 "pmc.h" + +static const struct amd_pmc_bit_map soc15_ip_blk_v3[] = { + {"VDDCR", BIT(0)}, + {"VDDCR_LP", BIT(1)}, + {"LSOCV", BIT(2)}, + {"DISPLAY", BIT(3)}, + {"VCN", BIT(4)}, + {"JPEG", BIT(5)}, + {"UMSCH", BIT(6)}, + {"VPE", BIT(7)}, + {"MPM", BIT(8)}, + {"NPU", BIT(9)}, + {"USB_HC0", BIT(10)}, + {"eUSB_HC0", BIT(11)}, + {"RT0_ADP_HC1", BIT(12)}, + {"RT1_ADP_HC1", BIT(13)}, + {"RT2_ADP_HC2", BIT(14)}, + {"USB4_RT0", BIT(15)}, + {"USB4_RT1", BIT(16)}, + {"USB4-RT2", BIT(17)}, + {"LAPIC", BIT(18)}, +}; + +static const struct amd_pmc_bit_map soc15_ip_blk_v2[] = { + {"DISPLAY", BIT(0)}, + {"CPU", BIT(1)}, + {"GFX", BIT(2)}, + {"VDD", BIT(3)}, + {"VDD_CCX", BIT(4)}, + {"ACP", BIT(5)}, + {"VCN_0", BIT(6)}, + {"VCN_1", BIT(7)}, + {"ISP", BIT(8)}, + {"NBIO", BIT(9)}, + {"DF", BIT(10)}, + {"USB3_0", BIT(11)}, + {"USB3_1", BIT(12)}, + {"LAPIC", BIT(13)}, + {"USB3_2", BIT(14)}, + {"USB4_RT0", BIT(15)}, + {"USB4_RT1", BIT(16)}, + {"USB4_0", BIT(17)}, + {"USB4_1", BIT(18)}, + {"MPM", BIT(19)}, + {"JPEG_0", BIT(20)}, + {"JPEG_1", BIT(21)}, + {"IPU", BIT(22)}, + {"UMSCH", BIT(23)}, + {"VPE", BIT(24)}, +}; + +static const struct amd_pmc_bit_map soc15_ip_blk[] = { + {"DISPLAY", BIT(0)}, + {"CPU", BIT(1)}, + {"GFX", BIT(2)}, + {"VDD", BIT(3)}, + {"ACP", BIT(4)}, + {"VCN", BIT(5)}, + {"ISP", BIT(6)}, + {"NBIO", BIT(7)}, + {"DF", BIT(8)}, + {"USB3_0", BIT(9)}, + {"USB3_1", BIT(10)}, + {"LAPIC", BIT(11)}, + {"USB3_2", BIT(12)}, + {"USB3_3", BIT(13)}, + {"USB3_4", BIT(14)}, + {"USB4_0", BIT(15)}, + {"USB4_1", BIT(16)}, + {"MPM", BIT(17)}, + {"JPEG", BIT(18)}, + {"IPU", BIT(19)}, + {"UMSCH", BIT(20)}, + {"VPE", BIT(21)}, +}; + +/* CPU info structures for different SoC variants */ +static const struct amd_pmc_cpu_info amd_pco_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MESSAGE, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = 12, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_PCO, +}; + +static const struct amd_pmc_cpu_info amd_czn_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MESSAGE, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = 12, + .scratch_reg = AMD_PMC_SCRATCH_REG_CZN, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_vg_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MESSAGE, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = 12, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_yc_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MESSAGE, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = 12, + .scratch_reg = AMD_PMC_SCRATCH_REG_YC, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_ps_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MESSAGE, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = 21, + .scratch_reg = AMD_PMC_SCRATCH_REG_YC, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_1ah_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MSG_1AH_20H, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = ARRAY_SIZE(soc15_ip_blk), + .scratch_reg = AMD_PMC_SCRATCH_REG_1AH, + .ips_ptr = soc15_ip_blk, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_1ah_m70_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MSG_1AH_20H, + .smu_arg = AMD_PMC_REGISTER_ARGUMENT, + .smu_rsp = AMD_PMC_REGISTER_RESPONSE, + .num_ips = ARRAY_SIZE(soc15_ip_blk_v2), + .scratch_reg = AMD_PMC_SCRATCH_REG_1AH, + .ips_ptr = soc15_ip_blk_v2, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct amd_pmc_cpu_info amd_1ah_m80_cpu_info = { + .smu_msg = AMD_PMC_REGISTER_MSG_1AH_80H, + .smu_arg = AMD_PMC_REGISTER_ARG_1AH_80H, + .smu_rsp = AMD_PMC_REGISTER_RSP_1AH_80H, + .num_ips = ARRAY_SIZE(soc15_ip_blk_v3), + .scratch_reg = AMD_PMC_SCRATCH_REG_1AH, + .ips_ptr = soc15_ip_blk_v3, + .os_hint = MSG_OS_HINT_RN, +}; + +static const struct pci_device_id pmc_pci_ids[] = { + { PCI_DEVICE_DATA(AMD, CPU_ID_PCO, &amd_pco_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_CZN, &amd_czn_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_VG, &amd_vg_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_YC, &amd_yc_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_CB, &amd_yc_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_PS, &amd_ps_cpu_info) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_SP, NULL) }, + { PCI_DEVICE_DATA(AMD, CPU_ID_SHP, NULL) }, + { PCI_DEVICE_DATA(AMD, 1AH_M20H_ROOT, NULL) }, + { PCI_DEVICE_DATA(AMD, 1AH_M60H_ROOT, NULL) }, + { PCI_DEVICE_DATA(AMD, 1AH_M80H_ROOT, &amd_1ah_m80_cpu_info) }, + { } +}; + +static bool disable_workarounds; +module_param(disable_workarounds, bool, 0644); +MODULE_PARM_DESC(disable_workarounds, "Disable workarounds for platform bugs"); + +static int delay_suspend = -1; +module_param(delay_suspend, int, 0644); +MODULE_PARM_DESC(delay_suspend, + "Delays s2idle by 2.5 seconds to work around buggy ECs, often causing keyboard issues after suspend. 0: don't delay, 1: do delay, -1 (default): let amd_pmc decide. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static struct amd_pmc_dev pmc; + +static inline u32 amd_pmc_reg_read(struct amd_pmc_dev *dev, int reg_offset) +{ + return ioread32(dev->regbase + reg_offset); +} + +static inline void amd_pmc_reg_write(struct amd_pmc_dev *dev, int reg_offset, u32 val) +{ + iowrite32(val, dev->regbase + reg_offset); +} + +static int amd_pmc_set_cpu_info(struct amd_pmc_dev *dev, struct pci_dev *rdev) +{ + const struct pci_device_id *id; + + id = pci_match_id(pmc_pci_ids, rdev); + if (!id) + return -ENODEV; + + dev->cpu_id = rdev->device; + + if (id->driver_data) { + dev->cpu_info = (const struct amd_pmc_cpu_info *)id->driver_data; + return 0; + } + + /* Special case: 1Ah M20H/M60H needs x86_model detection */ + switch (dev->cpu_id) { + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + if (boot_cpu_data.x86_model == 0x70) + dev->cpu_info = &amd_1ah_m70_cpu_info; + else + dev->cpu_info = &amd_1ah_cpu_info; + break; + case AMD_CPU_ID_SP: + case AMD_CPU_ID_SHP: + dev_warn_once(dev->dev, "S0i3 is not supported on this hardware\n"); + return -ENODEV; + default: + dev_err(dev->dev, "Unknown CPU ID: 0x%x\n", dev->cpu_id); + return -ENODEV; + } + + return 0; +} + +static int amd_pmc_setup_smu_logging(struct amd_pmc_dev *dev) +{ + if (dev->cpu_id == AMD_CPU_ID_PCO) { + dev_warn_once(dev->dev, "SMU debugging info not supported on this platform\n"); + return -EINVAL; + } + + /* Get Active devices list from SMU */ + if (!dev->active_ips) + amd_pmc_send_cmd(dev, 0, &dev->active_ips, SMU_MSG_GET_SUP_CONSTRAINTS, true); + + /* Get dram address */ + if (!dev->smu_virt_addr) { + u32 phys_addr_low, phys_addr_hi; + u64 smu_phys_addr; + + amd_pmc_send_cmd(dev, 0, &phys_addr_low, SMU_MSG_LOG_GETDRAM_ADDR_LO, true); + amd_pmc_send_cmd(dev, 0, &phys_addr_hi, SMU_MSG_LOG_GETDRAM_ADDR_HI, true); + smu_phys_addr = ((u64)phys_addr_hi << 32 | phys_addr_low); + + dev->smu_virt_addr = devm_ioremap(dev->dev, smu_phys_addr, + sizeof(struct smu_metrics)); + if (!dev->smu_virt_addr) + return -ENOMEM; + } + + memset_io(dev->smu_virt_addr, 0, sizeof(struct smu_metrics)); + + /* Start the logging */ + amd_pmc_send_cmd(dev, 0, NULL, SMU_MSG_LOG_RESET, false); + amd_pmc_send_cmd(dev, 0, NULL, SMU_MSG_LOG_START, false); + + return 0; +} + +static int get_metrics_table(struct amd_pmc_dev *pdev, struct smu_metrics *table) +{ + int rc; + + if (!pdev->smu_virt_addr) { + rc = amd_pmc_setup_smu_logging(pdev); + if (rc) + return rc; + } + + if (pdev->cpu_id == AMD_CPU_ID_PCO) + return -ENODEV; + memcpy_fromio(table, pdev->smu_virt_addr, sizeof(struct smu_metrics)); + return 0; +} + +static void amd_pmc_validate_deepest(struct amd_pmc_dev *pdev) +{ + struct smu_metrics table; + + if (get_metrics_table(pdev, &table)) + return; + + if (!table.s0i3_last_entry_status) + dev_warn(pdev->dev, "Last suspend didn't reach deepest state\n"); + pm_report_hw_sleep_time(table.s0i3_last_entry_status ? + table.timein_s0i3_lastcapture : 0); +} + +static int amd_pmc_get_smu_version(struct amd_pmc_dev *dev) +{ + int rc; + u32 val; + + if (dev->cpu_id == AMD_CPU_ID_PCO) + return -ENODEV; + + rc = amd_pmc_send_cmd(dev, 0, &val, SMU_MSG_GETSMUVERSION, true); + if (rc) + return rc; + + dev->smu_program = (val >> 24) & GENMASK(7, 0); + dev->major = (val >> 16) & GENMASK(7, 0); + dev->minor = (val >> 8) & GENMASK(7, 0); + dev->rev = (val >> 0) & GENMASK(7, 0); + + dev_dbg(dev->dev, "SMU program %u version is %u.%u.%u\n", + dev->smu_program, dev->major, dev->minor, dev->rev); + + return 0; +} + +static ssize_t smu_fw_version_show(struct device *d, struct device_attribute *attr, + char *buf) +{ + struct amd_pmc_dev *dev = dev_get_drvdata(d); + int rc; + + if (!dev->major) { + rc = amd_pmc_get_smu_version(dev); + if (rc) + return rc; + } + return sysfs_emit(buf, "%u.%u.%u\n", dev->major, dev->minor, dev->rev); +} + +static ssize_t smu_program_show(struct device *d, struct device_attribute *attr, + char *buf) +{ + struct amd_pmc_dev *dev = dev_get_drvdata(d); + int rc; + + if (!dev->major) { + rc = amd_pmc_get_smu_version(dev); + if (rc) + return rc; + } + return sysfs_emit(buf, "%u\n", dev->smu_program); +} + +static DEVICE_ATTR_RO(smu_fw_version); +static DEVICE_ATTR_RO(smu_program); + +static umode_t pmc_attr_is_visible(struct kobject *kobj, struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct amd_pmc_dev *pdev = dev_get_drvdata(dev); + + if (pdev->cpu_id == AMD_CPU_ID_PCO) + return 0; + return 0444; +} + +static struct attribute *pmc_attrs[] = { + &dev_attr_smu_fw_version.attr, + &dev_attr_smu_program.attr, + NULL, +}; + +static struct attribute_group pmc_attr_group = { + .attrs = pmc_attrs, + .is_visible = pmc_attr_is_visible, +}; + +static const struct attribute_group *pmc_groups[] = { + &pmc_attr_group, + NULL, +}; + +static int smu_fw_info_show(struct seq_file *s, void *unused) +{ + struct amd_pmc_dev *dev = s->private; + struct smu_metrics table; + int idx; + + if (get_metrics_table(dev, &table)) + return -EINVAL; + + seq_puts(s, "\n=== SMU Statistics ===\n"); + seq_printf(s, "Table Version: %d\n", table.table_version); + seq_printf(s, "Hint Count: %d\n", table.hint_count); + seq_printf(s, "Last S0i3 Status: %s\n", table.s0i3_last_entry_status ? "Success" : + "Unknown/Fail"); + seq_printf(s, "Time (in us) to S0i3: %lld\n", table.timeentering_s0i3_lastcapture); + seq_printf(s, "Time (in us) in S0i3: %lld\n", table.timein_s0i3_lastcapture); + seq_printf(s, "Time (in us) to resume from S0i3: %lld\n", + table.timeto_resume_to_os_lastcapture); + + seq_puts(s, "\n=== Active time (in us) ===\n"); + for (idx = 0 ; idx < dev->cpu_info->num_ips ; idx++) { + if (dev->cpu_info->ips_ptr[idx].bit_mask & dev->active_ips) + seq_printf(s, "%-8s : %lld\n", dev->cpu_info->ips_ptr[idx].name, + table.timecondition_notmet_lastcapture[idx]); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(smu_fw_info); + +static int s0ix_stats_show(struct seq_file *s, void *unused) +{ + struct amd_pmc_dev *dev = s->private; + u64 entry_time, exit_time, residency; + + /* Use FCH registers to get the S0ix stats */ + if (!dev->fch_virt_addr) { + u32 base_addr_lo = FCH_BASE_PHY_ADDR_LOW; + u32 base_addr_hi = FCH_BASE_PHY_ADDR_HIGH; + u64 fch_phys_addr = ((u64)base_addr_hi << 32 | base_addr_lo); + + dev->fch_virt_addr = devm_ioremap(dev->dev, fch_phys_addr, FCH_SSC_MAPPING_SIZE); + if (!dev->fch_virt_addr) + return -ENOMEM; + } + + entry_time = ioread32(dev->fch_virt_addr + FCH_S0I3_ENTRY_TIME_H_OFFSET); + entry_time = entry_time << 32 | ioread32(dev->fch_virt_addr + FCH_S0I3_ENTRY_TIME_L_OFFSET); + + exit_time = ioread32(dev->fch_virt_addr + FCH_S0I3_EXIT_TIME_H_OFFSET); + exit_time = exit_time << 32 | ioread32(dev->fch_virt_addr + FCH_S0I3_EXIT_TIME_L_OFFSET); + + /* It's in 48MHz. We need to convert it */ + residency = exit_time - entry_time; + do_div(residency, 48); + + seq_puts(s, "=== S0ix statistics ===\n"); + seq_printf(s, "S0ix Entry Time: %lld\n", entry_time); + seq_printf(s, "S0ix Exit Time: %lld\n", exit_time); + seq_printf(s, "Residency Time: %lld\n", residency); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(s0ix_stats); + +static int amd_pmc_idlemask_read(struct amd_pmc_dev *pdev, struct device *dev, + struct seq_file *s) +{ + u32 val; + int rc; + + /* we haven't yet read SMU version */ + if (pdev->cpu_id == AMD_CPU_ID_CZN) { + if (!pdev->major) { + rc = amd_pmc_get_smu_version(pdev); + if (rc) + return rc; + } + if (!(pdev->major > 56 || (pdev->major >= 55 && pdev->minor >= 37))) + return -EINVAL; + } + + if (!pdev->cpu_info->scratch_reg) + return -EINVAL; + + val = amd_pmc_reg_read(pdev, pdev->cpu_info->scratch_reg); + + if (dev) + pm_pr_dbg("SMU idlemask s0i3: 0x%x\n", val); + + if (s) + seq_printf(s, "SMU idlemask : 0x%x\n", val); + + return 0; +} + +static int amd_pmc_idlemask_show(struct seq_file *s, void *unused) +{ + return amd_pmc_idlemask_read(s->private, NULL, s); +} +DEFINE_SHOW_ATTRIBUTE(amd_pmc_idlemask); + +static void amd_pmc_dbgfs_unregister(struct amd_pmc_dev *dev) +{ + debugfs_remove_recursive(dev->dbgfs_dir); +} + +static void amd_pmc_dbgfs_register(struct amd_pmc_dev *dev) +{ + dev->dbgfs_dir = debugfs_create_dir("amd_pmc", NULL); + debugfs_create_file("smu_fw_info", 0644, dev->dbgfs_dir, dev, + &smu_fw_info_fops); + debugfs_create_file("s0ix_stats", 0644, dev->dbgfs_dir, dev, + &s0ix_stats_fops); + debugfs_create_file("amd_pmc_idlemask", 0644, dev->dbgfs_dir, dev, + &amd_pmc_idlemask_fops); +} + +static char *amd_pmc_get_msg_port(struct amd_pmc_dev *dev) +{ + switch (dev->msg_port) { + case MSG_PORT_PMC: + return "PMC"; + case MSG_PORT_S2D: + return "S2D"; + default: + return "Invalid message port"; + } +} + +static void amd_pmc_dump_registers(struct amd_pmc_dev *dev) +{ + u32 value, message, argument, response; + + if (dev->msg_port == MSG_PORT_S2D) { + message = dev->stb_arg.msg; + argument = dev->stb_arg.arg; + response = dev->stb_arg.resp; + } else { + message = dev->cpu_info->smu_msg; + argument = dev->cpu_info->smu_arg; + response = dev->cpu_info->smu_rsp; + } + + value = amd_pmc_reg_read(dev, response); + dev_dbg(dev->dev, "AMD_%s_REGISTER_RESPONSE:%x\n", amd_pmc_get_msg_port(dev), value); + + value = amd_pmc_reg_read(dev, argument); + dev_dbg(dev->dev, "AMD_%s_REGISTER_ARGUMENT:%x\n", amd_pmc_get_msg_port(dev), value); + + value = amd_pmc_reg_read(dev, message); + dev_dbg(dev->dev, "AMD_%s_REGISTER_MESSAGE:%x\n", amd_pmc_get_msg_port(dev), value); +} + +int amd_pmc_send_cmd(struct amd_pmc_dev *dev, u32 arg, u32 *data, u8 msg, bool ret) +{ + int rc; + u32 val, message, argument, response; + + guard(mutex)(&dev->lock); + + if (dev->msg_port == MSG_PORT_S2D) { + message = dev->stb_arg.msg; + argument = dev->stb_arg.arg; + response = dev->stb_arg.resp; + } else { + message = dev->cpu_info->smu_msg; + argument = dev->cpu_info->smu_arg; + response = dev->cpu_info->smu_rsp; + } + + /* Wait until we get a valid response */ + rc = readx_poll_timeout(ioread32, dev->regbase + response, + val, val != 0, PMC_MSG_DELAY_MIN_US, + PMC_MSG_DELAY_MIN_US * RESPONSE_REGISTER_LOOP_MAX); + if (rc) { + dev_err(dev->dev, "failed to talk to SMU\n"); + return rc; + } + + /* Write zero to response register */ + amd_pmc_reg_write(dev, response, 0); + + /* Write argument into response register */ + amd_pmc_reg_write(dev, argument, arg); + + /* Write message ID to message ID register */ + amd_pmc_reg_write(dev, message, msg); + + /* Wait until we get a valid response */ + rc = readx_poll_timeout(ioread32, dev->regbase + response, + val, val != 0, PMC_MSG_DELAY_MIN_US, + PMC_MSG_DELAY_MIN_US * RESPONSE_REGISTER_LOOP_MAX); + if (rc) { + dev_err(dev->dev, "SMU response timed out\n"); + return rc; + } + + switch (val) { + case AMD_PMC_RESULT_OK: + if (ret) { + /* PMFW may take longer time to return back the data */ + usleep_range(DELAY_MIN_US, 10 * DELAY_MAX_US); + *data = amd_pmc_reg_read(dev, argument); + } + break; + case AMD_PMC_RESULT_CMD_REJECT_BUSY: + dev_err(dev->dev, "SMU not ready. err: 0x%x\n", val); + rc = -EBUSY; + break; + case AMD_PMC_RESULT_CMD_UNKNOWN: + dev_err(dev->dev, "SMU cmd unknown. err: 0x%x\n", val); + rc = -EINVAL; + break; + case AMD_PMC_RESULT_CMD_REJECT_PREREQ: + case AMD_PMC_RESULT_FAILED: + default: + dev_err(dev->dev, "SMU cmd failed. err: 0x%x\n", val); + rc = -EIO; + break; + } + + amd_pmc_dump_registers(dev); + return rc; +} + +static int amd_pmc_wa_irq1(struct amd_pmc_dev *pdev) +{ + struct device *d; + + d = bus_find_device_by_name(&serio_bus, NULL, "serio0"); + if (!d) + return 0; + if (device_may_wakeup(d)) { + dev_info_once(d, "Disabling IRQ1 wakeup source to avoid platform firmware bug\n"); + disable_irq_wake(1); + device_set_wakeup_enable(d, false); + } + put_device(d); + + return 0; +} + +static int amd_pmc_verify_czn_rtc(struct amd_pmc_dev *pdev, u32 *arg) +{ + struct rtc_device *rtc_device; + time64_t then, now, duration; + struct rtc_wkalrm alarm; + struct rtc_time tm; + int rc; + + /* we haven't yet read SMU version */ + if (!pdev->major) { + rc = amd_pmc_get_smu_version(pdev); + if (rc) + return rc; + } + + if (pdev->major < 64 || (pdev->major == 64 && pdev->minor < 53)) + return 0; + + rtc_device = rtc_class_open("rtc0"); + if (!rtc_device) + return 0; + rc = rtc_read_next_alarm(rtc_device, &alarm); + if (rc) { + if (rc == -ENOENT) + dev_dbg(pdev->dev, "no alarm pending\n"); + return rc == -ENOENT ? 0 : rc; + } + if (!alarm.enabled) { + dev_dbg(pdev->dev, "alarm not enabled\n"); + return 0; + } + rc = rtc_read_time(rtc_device, &tm); + if (rc) + return rc; + then = rtc_tm_to_time64(&alarm.time); + now = rtc_tm_to_time64(&tm); + duration = then-now; + + /* in the past */ + if (then < now) + return 0; + + /* will be stored in upper 16 bits of s0i3 hint argument, + * so timer wakeup from s0i3 is limited to ~18 hours or less + */ + if (duration <= 4 || duration > U16_MAX) + return -EINVAL; + + *arg |= (duration << 16); + rc = rtc_alarm_irq_enable(rtc_device, 0); + pm_pr_dbg("wakeup timer programmed for %lld seconds\n", duration); + + return rc; +} + +static bool amd_pmc_intermediate_wakeup_need_delay(struct amd_pmc_dev *pdev) +{ + /* + * Starting a new HW sleep cycle right after waking from one + * can cause electrical problems triggering the over voltage protection. + * That is avoided by delaying the next suspend a bit, see also + * https://lore.kernel.org/all/20250414162446.3853194-1-superm1@kernel.org/ + */ + struct smu_metrics table; + + return get_metrics_table(pdev, &table) == 0 && table.s0i3_last_entry_status; +} + +static bool amd_pmc_want_suspend_delay(struct amd_pmc_dev *pdev) +{ + /* + * intermediate_wakeup implies that the machine didn't get to deepest sleep + * state before - otherwise this function isn't called in amd_pmc_s2idle_check() + * because amd_pmc_intermediate_wakeup_need_delay() returns true first. + * On some IdeaPads that happens when charging, because the EC seems + * to send lots of messages then that wake the machine. + * + * But even in that case, the sleep here is necessary (on those IdeaPads), + * otherwise they wake up completely (resume) after a few seconds. + * So this variable is only used to avoid spamming dmesg on each + * intermediate wakeup. + */ + bool intermediate_wakeup = !pdev->is_first_check_after_suspend; + + /* + * Some Lenovo Laptops (like different IdeaPad 3 Slims) need some + * me-time before sleeping or they get uncooperative after waking + * up and don't send events for keyboard and lid switch anymore. + * + * Unfortunately this doesn't entirely fix the problem: It can still + * happen when resuming with a timer (wakealarm), but at least the + * more common usecases (wakeup by opening lid or pressing a key) + * work fine with this workaround. + * + * See https://bugzilla.kernel.org/show_bug.cgi?id=221383 + */ + if (amd_pmc_quirk_need_suspend_delay(pdev)) { + /* + * delay_suspend=1 force-enables this, otherwise it can be + * disabled with disable_workarounds or delay_suspend=0 + */ + if (delay_suspend == 1 || (delay_suspend == -1 && !disable_workarounds)) { + if (!intermediate_wakeup) + dev_info(pdev->dev, "Delaying suspend by 2.5s to avoid platform bug\n"); + return true; + } + if (!intermediate_wakeup) + dev_info(pdev->dev, "Not delaying suspend because of module parameter, even though your device is assumed to need it!\n"); + } else if (delay_suspend == 1) { + if (!intermediate_wakeup) + dev_info(pdev->dev, "Delaying suspend by 2.5s because delay_suspend=1. If this solves problems on your machine, please report this whole line to: platform-driver-x86@vger.kernel.org so it can be automatically detected as affected in the future. System Vendor: \"%s\" Product Name: \"%s\" Product Family: \"%s\" Board Vendor: \"%s\" Board Name: \"%s\"\n", + dmi_get_system_info(DMI_SYS_VENDOR) ?: "(Unknown)", + dmi_get_system_info(DMI_PRODUCT_NAME) ?: "(Unknown)", + dmi_get_system_info(DMI_PRODUCT_FAMILY) ?: "(Unknown)", + dmi_get_system_info(DMI_BOARD_VENDOR) ?: "(Unknown)", + dmi_get_system_info(DMI_BOARD_NAME) ?: "(Unknown)"); + return true; + } + return false; +} + +static void amd_pmc_s2idle_prepare(void) +{ + struct amd_pmc_dev *pdev = &pmc; + int rc; + u32 arg = 1; + + /* Reset this variable because this is a fresh suspend */ + pdev->is_first_check_after_suspend = true; + + /* Reset and Start SMU logging - to monitor the s0i3 stats */ + amd_pmc_setup_smu_logging(pdev); + + /* Activate CZN specific platform bug workarounds */ + if (pdev->cpu_id == AMD_CPU_ID_CZN && !disable_workarounds) { + rc = amd_pmc_verify_czn_rtc(pdev, &arg); + if (rc) { + dev_err(pdev->dev, "failed to set RTC: %d\n", rc); + return; + } + } + + rc = amd_pmc_send_cmd(pdev, arg, NULL, pdev->cpu_info->os_hint, false); + if (rc) { + dev_err(pdev->dev, "suspend failed: %d\n", rc); + return; + } + + rc = amd_stb_write(pdev, AMD_PMC_STB_S2IDLE_PREPARE); + if (rc) + dev_err(pdev->dev, "error writing to STB: %d\n", rc); +} + +static void amd_pmc_s2idle_check(void) +{ + struct amd_pmc_dev *pdev = &pmc; + int rc; + + if (amd_pmc_intermediate_wakeup_need_delay(pdev) || + amd_pmc_want_suspend_delay(pdev)) + msleep(2500); + + /* Dump the IdleMask before we add to the STB */ + amd_pmc_idlemask_read(pdev, pdev->dev, NULL); + + rc = amd_stb_write(pdev, AMD_PMC_STB_S2IDLE_CHECK); + if (rc) + dev_err(pdev->dev, "error writing to STB: %d\n", rc); + + /* remember that first check after suspend is done (until next prepare) */ + pdev->is_first_check_after_suspend = false; +} + +static int amd_pmc_dump_data(struct amd_pmc_dev *pdev) +{ + if (pdev->cpu_id == AMD_CPU_ID_PCO) + return -ENODEV; + + return amd_pmc_send_cmd(pdev, 0, NULL, SMU_MSG_LOG_DUMP_DATA, false); +} + +static void amd_pmc_s2idle_restore(void) +{ + struct amd_pmc_dev *pdev = &pmc; + int rc; + + rc = amd_pmc_send_cmd(pdev, 0, NULL, pdev->cpu_info->os_hint, false); + if (rc) + dev_err(pdev->dev, "resume failed: %d\n", rc); + + /* Let SMU know that we are looking for stats */ + amd_pmc_dump_data(pdev); + + rc = amd_stb_write(pdev, AMD_PMC_STB_S2IDLE_RESTORE); + if (rc) + dev_err(pdev->dev, "error writing to STB: %d\n", rc); + + /* Notify on failed entry */ + amd_pmc_validate_deepest(pdev); + + amd_pmc_process_restore_quirks(pdev); +} + +static struct acpi_s2idle_dev_ops amd_pmc_s2idle_dev_ops = { + .prepare = amd_pmc_s2idle_prepare, + .check = amd_pmc_s2idle_check, + .restore = amd_pmc_s2idle_restore, +}; + +static int amd_pmc_suspend_handler(struct device *dev) +{ + struct amd_pmc_dev *pdev = dev_get_drvdata(dev); + int rc; + + /* + * Must be called only from the same set of dev_pm_ops handlers + * as i8042_pm_suspend() is called: currently just from .suspend. + */ + if (pdev->disable_8042_wakeup && !disable_workarounds) { + rc = amd_pmc_wa_irq1(pdev); + if (rc) { + dev_err(pdev->dev, "failed to adjust keyboard wakeup: %d\n", rc); + return rc; + } + } + + return 0; +} + +static const struct dev_pm_ops amd_pmc_pm = { + .suspend = amd_pmc_suspend_handler, +}; + +static int amd_pmc_probe(struct platform_device *pdev) +{ + struct amd_pmc_dev *dev = &pmc; + struct pci_dev *rdev; + u32 base_addr_lo, base_addr_hi; + u64 base_addr; + int err; + u32 val; + + dev->dev = &pdev->dev; + rdev = pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(0, 0)); + if (!rdev) { + err = -ENODEV; + goto err_pci_dev_put; + } + + err = amd_pmc_set_cpu_info(dev, rdev); + if (err) + goto err_pci_dev_put; + + dev->rdev = rdev; + err = amd_smn_read(0, AMD_PMC_BASE_ADDR_LO, &val); + if (err) { + dev_err(dev->dev, "error reading 0x%x\n", AMD_PMC_BASE_ADDR_LO); + err = pcibios_err_to_errno(err); + goto err_pci_dev_put; + } + + base_addr_lo = val & AMD_PMC_BASE_ADDR_HI_MASK; + err = amd_smn_read(0, AMD_PMC_BASE_ADDR_HI, &val); + if (err) { + dev_err(dev->dev, "error reading 0x%x\n", AMD_PMC_BASE_ADDR_HI); + err = pcibios_err_to_errno(err); + goto err_pci_dev_put; + } + + base_addr_hi = val & AMD_PMC_BASE_ADDR_LO_MASK; + base_addr = ((u64)base_addr_hi << 32 | base_addr_lo); + + dev->regbase = devm_ioremap(dev->dev, base_addr + AMD_PMC_BASE_ADDR_OFFSET, + AMD_PMC_MAPPING_SIZE); + if (!dev->regbase) { + err = -ENOMEM; + goto err_pci_dev_put; + } + + err = devm_mutex_init(dev->dev, &dev->lock); + if (err) + goto err_pci_dev_put; + + platform_set_drvdata(pdev, dev); + if (IS_ENABLED(CONFIG_SUSPEND)) { + err = acpi_register_lps0_dev(&amd_pmc_s2idle_dev_ops); + if (err) + dev_warn(dev->dev, "failed to register LPS0 sleep handler, expect increased power consumption\n"); + if (!disable_workarounds) + amd_pmc_quirks_init(dev); + } + + amd_pmc_dbgfs_register(dev); + err = amd_stb_s2d_init(dev); + if (err) + goto err_pci_dev_put; + + if (IS_ENABLED(CONFIG_AMD_MP2_STB)) + amd_mp2_stb_init(dev); + pm_report_max_hw_sleep(U64_MAX); + return 0; + +err_pci_dev_put: + pci_dev_put(rdev); + return err; +} + +static void amd_pmc_remove(struct platform_device *pdev) +{ + struct amd_pmc_dev *dev = platform_get_drvdata(pdev); + + if (IS_ENABLED(CONFIG_SUSPEND)) + acpi_unregister_lps0_dev(&amd_pmc_s2idle_dev_ops); + amd_pmc_dbgfs_unregister(dev); + pci_dev_put(dev->rdev); + if (IS_ENABLED(CONFIG_AMD_MP2_STB)) + amd_mp2_stb_deinit(dev); +} + +static const struct acpi_device_id amd_pmc_acpi_ids[] = { + {"AMDI0005", 0}, + {"AMDI0006", 0}, + {"AMDI0007", 0}, + {"AMDI0008", 0}, + {"AMDI0009", 0}, + {"AMDI000A", 0}, + {"AMDI000B", 0}, + {"AMDI000C", 0}, + {"AMD0004", 0}, + {"AMD0005", 0}, + { } +}; +MODULE_DEVICE_TABLE(acpi, amd_pmc_acpi_ids); + +static struct platform_driver amd_pmc_driver = { + .driver = { + .name = "amd_pmc", + .acpi_match_table = amd_pmc_acpi_ids, + .dev_groups = pmc_groups, + .pm = pm_sleep_ptr(&amd_pmc_pm), + }, + .probe = amd_pmc_probe, + .remove = amd_pmc_remove, +}; +module_platform_driver(amd_pmc_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("AMD PMC Driver"); diff --git a/drivers/platform/x86/amd/pmc/pmc.h b/drivers/platform/x86/amd/pmc/pmc.h new file mode 100644 index 000000000..6973a639d --- /dev/null +++ b/drivers/platform/x86/amd/pmc/pmc.h @@ -0,0 +1,204 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * AMD SoC Power Management Controller Driver + * + * Copyright (c) 2023, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Mario Limonciello + */ + +#ifndef PMC_H +#define PMC_H + +#include +#include + +/* SMU communication registers */ +#define AMD_PMC_REGISTER_RESPONSE 0x980 +#define AMD_PMC_REGISTER_ARGUMENT 0x9BC +#define AMD_PMC_REGISTER_MESSAGE 0x538 + +/* SMU communication registers for 1Ah 20h SoC */ +#define AMD_PMC_REGISTER_MSG_1AH_20H 0x938 + +/* SMU communication registers for 1Ah 80h SoC */ +#define AMD_PMC_REGISTER_MSG_1AH_80H 0xA10 +#define AMD_PMC_REGISTER_ARG_1AH_80H 0xA18 +#define AMD_PMC_REGISTER_RSP_1AH_80H 0xA14 + +/* PMC Scratch Registers */ +#define AMD_PMC_SCRATCH_REG_CZN 0x94 +#define AMD_PMC_SCRATCH_REG_YC 0xD14 +#define AMD_PMC_SCRATCH_REG_1AH 0xF14 + +/* STB Registers */ +#define AMD_PMC_STB_S2IDLE_PREPARE 0xC6000001 +#define AMD_PMC_STB_S2IDLE_RESTORE 0xC6000002 +#define AMD_PMC_STB_S2IDLE_CHECK 0xC6000003 + +/* Base address of SMU for mapping physical address to virtual address */ +#define AMD_PMC_MAPPING_SIZE 0x01000 +#define AMD_PMC_BASE_ADDR_OFFSET 0x10000 +#define AMD_PMC_BASE_ADDR_LO 0x13B102E8 +#define AMD_PMC_BASE_ADDR_HI 0x13B102EC +#define AMD_PMC_BASE_ADDR_LO_MASK GENMASK(15, 0) +#define AMD_PMC_BASE_ADDR_HI_MASK GENMASK(31, 20) + +/* SMU Response Codes */ +#define AMD_PMC_RESULT_OK 0x01 +#define AMD_PMC_RESULT_CMD_REJECT_BUSY 0xFC +#define AMD_PMC_RESULT_CMD_REJECT_PREREQ 0xFD +#define AMD_PMC_RESULT_CMD_UNKNOWN 0xFE +#define AMD_PMC_RESULT_FAILED 0xFF + +/* FCH SSC Registers */ +#define FCH_S0I3_ENTRY_TIME_L_OFFSET 0x30 +#define FCH_S0I3_ENTRY_TIME_H_OFFSET 0x34 +#define FCH_S0I3_EXIT_TIME_L_OFFSET 0x38 +#define FCH_S0I3_EXIT_TIME_H_OFFSET 0x3C +#define FCH_SSC_MAPPING_SIZE 0x800 +#define FCH_BASE_PHY_ADDR_LOW 0xFED81100 +#define FCH_BASE_PHY_ADDR_HIGH 0x00000000 + +/* SMU Message Definations */ +#define SMU_MSG_GETSMUVERSION 0x02 +#define SMU_MSG_LOG_GETDRAM_ADDR_HI 0x04 +#define SMU_MSG_LOG_GETDRAM_ADDR_LO 0x05 +#define SMU_MSG_LOG_START 0x06 +#define SMU_MSG_LOG_RESET 0x07 +#define SMU_MSG_LOG_DUMP_DATA 0x08 +#define SMU_MSG_GET_SUP_CONSTRAINTS 0x09 + +#define PMC_MSG_DELAY_MIN_US 50 +#define RESPONSE_REGISTER_LOOP_MAX 20000 + +#define DELAY_MIN_US 2000 +#define DELAY_MAX_US 3000 + +enum s2d_msg_port { + MSG_PORT_PMC, + MSG_PORT_S2D, +}; + +struct amd_mp2_dev { + void __iomem *mmio; + void __iomem *vslbase; + void *stbdata; + void *devres_gid; + struct pci_dev *pdev; + dma_addr_t dma_addr; + int stb_len; + bool is_stb_data; +}; + +struct stb_arg { + u32 s2d_msg_id; + u32 msg; + u32 arg; + u32 resp; +}; + +struct amd_pmc_bit_map { + const char *name; + u32 bit_mask; +}; + +/* SoC-specific information */ +struct amd_pmc_cpu_info { + u32 smu_msg; + u32 smu_arg; + u32 smu_rsp; + u32 num_ips; + u32 scratch_reg; + const struct amd_pmc_bit_map *ips_ptr; + u8 os_hint; +}; + +struct amd_pmc_dev { + void __iomem *regbase; + void __iomem *smu_virt_addr; + void __iomem *stb_virt_addr; + void __iomem *fch_virt_addr; + u32 base_addr; + u32 cpu_id; + u32 dram_size; + u32 active_ips; +/* SMU version information */ + u8 smu_program; + u8 major; + u8 minor; + u8 rev; + u8 msg_port; + struct device *dev; + struct pci_dev *rdev; + struct mutex lock; /* generic mutex lock */ + struct dentry *dbgfs_dir; + struct quirk_entry *quirks; + bool disable_8042_wakeup; + bool is_first_check_after_suspend; + struct amd_mp2_dev *mp2; + struct stb_arg stb_arg; + const struct amd_pmc_cpu_info *cpu_info; +}; + +struct smu_metrics { + u32 table_version; + u32 hint_count; + u32 s0i3_last_entry_status; + u32 timein_s0i2; + u64 timeentering_s0i3_lastcapture; + u64 timeentering_s0i3_totaltime; + u64 timeto_resume_to_os_lastcapture; + u64 timeto_resume_to_os_totaltime; + u64 timein_s0i3_lastcapture; + u64 timein_s0i3_totaltime; + u64 timein_swdrips_lastcapture; + u64 timein_swdrips_totaltime; + u64 timecondition_notmet_lastcapture[32]; + u64 timecondition_notmet_totaltime[32]; +} __packed; + +enum amd_pmc_def { + MSG_TEST = 0x01, + MSG_OS_HINT_PCO, + MSG_OS_HINT_RN, +}; + +void amd_pmc_process_restore_quirks(struct amd_pmc_dev *dev); +bool amd_pmc_quirk_need_suspend_delay(struct amd_pmc_dev *dev); +void amd_pmc_quirks_init(struct amd_pmc_dev *dev); +void amd_mp2_stb_init(struct amd_pmc_dev *dev); +void amd_mp2_stb_deinit(struct amd_pmc_dev *dev); + +/* List of supported CPU/device IDs */ +#define PCI_DEVICE_ID_AMD_CPU_ID_PCO 0x15D0 +#define PCI_DEVICE_ID_AMD_CPU_ID_CZN 0x1630 +#define PCI_DEVICE_ID_AMD_CPU_ID_VG 0x1645 +#define PCI_DEVICE_ID_AMD_CPU_ID_YC 0x14B5 +#define PCI_DEVICE_ID_AMD_CPU_ID_CB 0x14D8 +#define PCI_DEVICE_ID_AMD_CPU_ID_PS 0x14E8 +#define PCI_DEVICE_ID_AMD_CPU_ID_SP 0x14A4 +#define PCI_DEVICE_ID_AMD_CPU_ID_SHP 0x153A + +/* Backward compatibility aliases */ +#define AMD_CPU_ID_PCO PCI_DEVICE_ID_AMD_CPU_ID_PCO +#define AMD_CPU_ID_CZN PCI_DEVICE_ID_AMD_CPU_ID_CZN +#define AMD_CPU_ID_VG PCI_DEVICE_ID_AMD_CPU_ID_VG +#define AMD_CPU_ID_YC PCI_DEVICE_ID_AMD_CPU_ID_YC +#define AMD_CPU_ID_CB PCI_DEVICE_ID_AMD_CPU_ID_CB +#define AMD_CPU_ID_PS PCI_DEVICE_ID_AMD_CPU_ID_PS +#define AMD_CPU_ID_SP PCI_DEVICE_ID_AMD_CPU_ID_SP +#define AMD_CPU_ID_SHP PCI_DEVICE_ID_AMD_CPU_ID_SHP + +#define PCI_DEVICE_ID_AMD_1AH_M20H_ROOT 0x1507 +#define PCI_DEVICE_ID_AMD_1AH_M60H_ROOT 0x1122 +#define PCI_DEVICE_ID_AMD_1AH_M80H_ROOT 0x115b +#define PCI_DEVICE_ID_AMD_MP2_STB 0x172c + +int amd_stb_s2d_init(struct amd_pmc_dev *dev); +int amd_stb_read(struct amd_pmc_dev *dev, u32 *buf); +int amd_stb_write(struct amd_pmc_dev *dev, u32 data); +int amd_pmc_send_cmd(struct amd_pmc_dev *dev, u32 arg, u32 *data, u8 msg, bool ret); + +#endif /* PMC_H */ diff --git a/drivers/platform/x86/amd/pmf/Kconfig b/drivers/platform/x86/amd/pmf/Kconfig new file mode 100644 index 000000000..25b8f7ae3 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/Kconfig @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# AMD PMF Driver +# + +config AMD_PMF + tristate "AMD Platform Management Framework" + depends on ACPI && PCI + depends on POWER_SUPPLY + depends on AMD_NODE + select ACPI_PLATFORM_PROFILE + depends on TEE && AMDTEE + depends on AMD_SFH_HID + depends on HAS_IOMEM + help + This driver provides support for the AMD Platform Management Framework. + The goal is to enhance end user experience by making AMD PCs smarter, + quiter, power efficient by adapting to user behavior and environment. + + To compile this driver as a module, choose M here: the module will + be called amd_pmf. + +config AMD_PMF_DEBUG + bool "PMF debug information" + depends on AMD_PMF + help + Enabling this option would give more debug information on the OEM fed + power setting values for each of the PMF feature. PMF driver gets this + information after evaluating a ACPI method and the information is stored + in the PMF config store. + + Say Y here to enable more debug logs and Say N here if you are not sure. diff --git a/drivers/platform/x86/amd/pmf/Makefile b/drivers/platform/x86/amd/pmf/Makefile new file mode 100644 index 000000000..5978464e0 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/amd/pmf +# AMD Platform Management Framework +# + +obj-$(CONFIG_AMD_PMF) += amd-pmf.o +amd-pmf-y := core.o acpi.o sps.o \ + auto-mode.o cnqf.o \ + tee-if.o spc.o diff --git a/drivers/platform/x86/amd/pmf/acpi.c b/drivers/platform/x86/amd/pmf/acpi.c new file mode 100644 index 000000000..3d94b03cf --- /dev/null +++ b/drivers/platform/x86/amd/pmf/acpi.c @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include +#include +#include +#include +#include "pmf.h" + +#define APMF_CQL_NOTIFICATION 2 +#define APMF_AMT_NOTIFICATION 3 + +static union acpi_object *apmf_if_call(struct amd_pmf_dev *pdev, int fn, struct acpi_buffer *param) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_handle ahandle = ACPI_HANDLE(pdev->dev); + struct acpi_object_list apmf_if_arg_list; + union acpi_object apmf_if_args[2]; + acpi_status status; + + apmf_if_arg_list.count = 2; + apmf_if_arg_list.pointer = &apmf_if_args[0]; + + apmf_if_args[0].type = ACPI_TYPE_INTEGER; + apmf_if_args[0].integer.value = fn; + + if (param) { + apmf_if_args[1].type = ACPI_TYPE_BUFFER; + apmf_if_args[1].buffer.length = param->length; + apmf_if_args[1].buffer.pointer = param->pointer; + } else { + apmf_if_args[1].type = ACPI_TYPE_INTEGER; + apmf_if_args[1].integer.value = 0; + } + + status = acpi_evaluate_object(ahandle, "APMF", &apmf_if_arg_list, &buffer); + if (ACPI_FAILURE(status)) { + dev_err(pdev->dev, "APMF method:%d call failed\n", fn); + kfree(buffer.pointer); + return NULL; + } + + return buffer.pointer; +} + +static int apmf_if_call_store_buffer(struct amd_pmf_dev *pdev, int fn, void *dest, size_t out_sz) +{ + union acpi_object *info; + size_t size; + int err = 0; + + info = apmf_if_call(pdev, fn, NULL); + if (!info) + return -EIO; + + if (info->type != ACPI_TYPE_BUFFER) { + dev_err(pdev->dev, "object is not a buffer\n"); + err = -EINVAL; + goto out; + } + + if (info->buffer.length < 2) { + dev_err(pdev->dev, "buffer too small\n"); + err = -EINVAL; + goto out; + } + + size = *(u16 *)info->buffer.pointer; + if (info->buffer.length < size) { + dev_err(pdev->dev, "buffer smaller then headersize %u < %zu\n", + info->buffer.length, size); + err = -EINVAL; + goto out; + } + + if (size < out_sz) { + dev_err(pdev->dev, "buffer too small %zu\n", size); + err = -EINVAL; + goto out; + } + + memcpy(dest, info->buffer.pointer, out_sz); + +out: + kfree(info); + return err; +} + +static union acpi_object *apts_if_call(struct amd_pmf_dev *pdev, u32 state_index) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_handle ahandle = ACPI_HANDLE(pdev->dev); + struct acpi_object_list apts_if_arg_list; + union acpi_object apts_if_args[3]; + acpi_status status; + + apts_if_arg_list.count = 3; + apts_if_arg_list.pointer = &apts_if_args[0]; + + apts_if_args[0].type = ACPI_TYPE_INTEGER; + apts_if_args[0].integer.value = 1; + apts_if_args[1].type = ACPI_TYPE_INTEGER; + apts_if_args[1].integer.value = state_index; + apts_if_args[2].type = ACPI_TYPE_INTEGER; + apts_if_args[2].integer.value = 0; + + status = acpi_evaluate_object(ahandle, "APTS", &apts_if_arg_list, &buffer); + if (ACPI_FAILURE(status)) { + dev_err(pdev->dev, "APTS state_idx:%u call failed\n", state_index); + kfree(buffer.pointer); + return NULL; + } + + return buffer.pointer; +} + +static int apts_if_call_store_buffer(struct amd_pmf_dev *pdev, + u32 index, void *data, size_t out_sz) +{ + union acpi_object *info; + size_t size; + int err = 0; + + info = apts_if_call(pdev, index); + if (!info) + return -EIO; + + if (info->type != ACPI_TYPE_BUFFER) { + dev_err(pdev->dev, "object is not a buffer\n"); + err = -EINVAL; + goto out; + } + + size = *(u16 *)info->buffer.pointer; + if (info->buffer.length < size) { + dev_err(pdev->dev, "buffer smaller than header size %u < %zu\n", + info->buffer.length, size); + err = -EINVAL; + goto out; + } + + if (size < out_sz) { + dev_err(pdev->dev, "buffer too small %zu\n", size); + err = -EINVAL; + goto out; + } + + memcpy(data, info->buffer.pointer, out_sz); +out: + kfree(info); + return err; +} + +int is_apmf_func_supported(struct amd_pmf_dev *pdev, unsigned long index) +{ + /* If bit-n is set, that indicates function n+1 is supported */ + return !!(pdev->supported_func & BIT(index - 1)); +} + +int is_apmf_bios_input_notifications_supported(struct amd_pmf_dev *pdev) +{ + return !!(pdev->notifications & CUSTOM_BIOS_INPUT_BITS); +} + +int apts_get_static_slider_granular_v2(struct amd_pmf_dev *pdev, + struct amd_pmf_apts_granular_output *data, u32 apts_idx) +{ + if (!is_apmf_func_supported(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) + return -EINVAL; + + return apts_if_call_store_buffer(pdev, apts_idx, data, sizeof(*data)); +} + +int apmf_get_static_slider_granular_v2(struct amd_pmf_dev *pdev, + struct apmf_static_slider_granular_output_v2 *data) +{ + if (!is_apmf_func_supported(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) + return -EINVAL; + + return apmf_if_call_store_buffer(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR, + data, sizeof(*data)); +} + +int apmf_get_static_slider_granular(struct amd_pmf_dev *pdev, + struct apmf_static_slider_granular_output *data) +{ + if (!is_apmf_func_supported(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) + return -EINVAL; + + return apmf_if_call_store_buffer(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR, + data, sizeof(*data)); +} + +int apmf_os_power_slider_update(struct amd_pmf_dev *pdev, u8 event) +{ + struct os_power_slider args; + struct acpi_buffer params; + union acpi_object *info; + + args.size = sizeof(args); + args.slider_event = event; + + params.length = sizeof(args); + params.pointer = (void *)&args; + + info = apmf_if_call(pdev, APMF_FUNC_OS_POWER_SLIDER_UPDATE, ¶ms); + if (!info) + return -EIO; + + kfree(info); + return 0; +} + +static void apmf_sbios_heartbeat_notify(struct work_struct *work) +{ + struct amd_pmf_dev *dev = container_of(work, struct amd_pmf_dev, heart_beat.work); + union acpi_object *info; + + dev_dbg(dev->dev, "Sending heartbeat to SBIOS\n"); + info = apmf_if_call(dev, APMF_FUNC_SBIOS_HEARTBEAT, NULL); + if (!info) + return; + + schedule_delayed_work(&dev->heart_beat, secs_to_jiffies(dev->hb_interval)); + kfree(info); +} + +int amd_pmf_notify_sbios_heartbeat_event_v2(struct amd_pmf_dev *dev, u8 flag) +{ + struct sbios_hb_event_v2 args = { }; + struct acpi_buffer params; + union acpi_object *info; + + args.size = sizeof(args); + + switch (flag) { + case ON_LOAD: + args.load = 1; + break; + case ON_UNLOAD: + args.unload = 1; + break; + case ON_SUSPEND: + args.suspend = 1; + break; + case ON_RESUME: + args.resume = 1; + break; + default: + dev_dbg(dev->dev, "Failed to send v2 heartbeat event, flag:0x%x\n", flag); + return -EINVAL; + } + + params.length = sizeof(args); + params.pointer = &args; + + info = apmf_if_call(dev, APMF_FUNC_SBIOS_HEARTBEAT_V2, ¶ms); + if (!info) + return -EIO; + + kfree(info); + return 0; +} + +int apmf_update_fan_idx(struct amd_pmf_dev *pdev, bool manual, u32 idx) +{ + union acpi_object *info; + struct apmf_fan_idx args; + struct acpi_buffer params; + + args.size = sizeof(args); + args.fan_ctl_mode = manual; + args.fan_ctl_idx = idx; + + params.length = sizeof(args); + params.pointer = (void *)&args; + + info = apmf_if_call(pdev, APMF_FUNC_SET_FAN_IDX, ¶ms); + if (!info) + return -EIO; + + kfree(info); + return 0; +} + +static int apmf_notify_smart_pc_update(struct amd_pmf_dev *pdev, u32 val, u32 preq, u32 index) +{ + struct amd_pmf_notify_smart_pc_update args; + struct acpi_buffer params; + union acpi_object *info; + + args.size = sizeof(args); + args.pending_req = preq; + args.custom_bios[index] = val; + + params.length = sizeof(args); + params.pointer = &args; + + info = apmf_if_call(pdev, APMF_FUNC_NOTIFY_SMART_PC_UPDATES, ¶ms); + if (!info) + return -EIO; + + kfree(info); + dev_dbg(pdev->dev, "Notify smart pc update, val: %u\n", val); + + return 0; +} + +int apmf_get_auto_mode_def(struct amd_pmf_dev *pdev, struct apmf_auto_mode *data) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_AUTO_MODE, data, sizeof(*data)); +} + +int apmf_get_sbios_requests_v2(struct amd_pmf_dev *pdev, struct apmf_sbios_req_v2 *req) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_SBIOS_REQUESTS, req, sizeof(*req)); +} + +int apmf_get_sbios_requests_v1(struct amd_pmf_dev *pdev, struct apmf_sbios_req_v1 *req) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_SBIOS_REQUESTS, req, sizeof(*req)); +} + +int apmf_get_sbios_requests(struct amd_pmf_dev *pdev, struct apmf_sbios_req *req) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_SBIOS_REQUESTS, + req, sizeof(*req)); +} + +/* Store custom BIOS inputs data in ring buffer */ +static void amd_pmf_custom_bios_inputs_rb(struct amd_pmf_dev *pmf_dev) +{ + struct pmf_cbi_ring_buffer *rb = &pmf_dev->cbi_buf; + int i; + + guard(mutex)(&pmf_dev->cbi_mutex); + + switch (pmf_dev->cpu_id) { + case AMD_CPU_ID_PS: + for (i = 0; i < ARRAY_SIZE(custom_bios_inputs_v1); i++) + rb->data[rb->head].val[i] = pmf_dev->req1.custom_policy[i]; + rb->data[rb->head].preq = pmf_dev->req1.pending_req; + break; + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + for (i = 0; i < ARRAY_SIZE(custom_bios_inputs); i++) + rb->data[rb->head].val[i] = pmf_dev->req.custom_policy[i]; + rb->data[rb->head].preq = pmf_dev->req.pending_req; + break; + default: + return; + } + + if (CIRC_SPACE(rb->head, rb->tail, CUSTOM_BIOS_INPUT_RING_ENTRIES) == 0) { + /* Rare case: ensures the newest BIOS input value is kept */ + dev_warn(pmf_dev->dev, "Overwriting BIOS input value, data may be lost\n"); + rb->tail = (rb->tail + 1) & (CUSTOM_BIOS_INPUT_RING_ENTRIES - 1); + } + + rb->head = (rb->head + 1) & (CUSTOM_BIOS_INPUT_RING_ENTRIES - 1); +} + +static void amd_pmf_handle_early_preq(struct amd_pmf_dev *pdev) +{ + if (!pdev->cb_flag) + return; + + amd_pmf_invoke_cmd_enact(pdev); + pdev->cb_flag = false; +} + +static void apmf_event_handler_v2(acpi_handle handle, u32 event, void *data) +{ + struct amd_pmf_dev *pmf_dev = data; + int ret; + + guard(mutex)(&pmf_dev->cb_mutex); + + ret = apmf_get_sbios_requests_v2(pmf_dev, &pmf_dev->req); + if (ret) { + dev_err(pmf_dev->dev, "Failed to get v2 SBIOS requests: %d\n", ret); + return; + } + + dev_dbg(pmf_dev->dev, "Pending request (preq): 0x%x\n", pmf_dev->req.pending_req); + + amd_pmf_handle_early_preq(pmf_dev); + + amd_pmf_custom_bios_inputs_rb(pmf_dev); +} + +static void apmf_event_handler_v1(acpi_handle handle, u32 event, void *data) +{ + struct amd_pmf_dev *pmf_dev = data; + int ret; + + guard(mutex)(&pmf_dev->cb_mutex); + + ret = apmf_get_sbios_requests_v1(pmf_dev, &pmf_dev->req1); + if (ret) { + dev_err(pmf_dev->dev, "Failed to get v1 SBIOS requests: %d\n", ret); + return; + } + + dev_dbg(pmf_dev->dev, "Pending request (preq1): 0x%x\n", pmf_dev->req1.pending_req); + + amd_pmf_handle_early_preq(pmf_dev); + + amd_pmf_custom_bios_inputs_rb(pmf_dev); +} + +static void apmf_event_handler(acpi_handle handle, u32 event, void *data) +{ + struct amd_pmf_dev *pmf_dev = data; + struct apmf_sbios_req req; + int ret; + + guard(mutex)(&pmf_dev->update_mutex); + ret = apmf_get_sbios_requests(pmf_dev, &req); + if (ret) { + dev_err(pmf_dev->dev, "Failed to get SBIOS requests:%d\n", ret); + return; + } + + if (req.pending_req & BIT(APMF_AMT_NOTIFICATION)) { + dev_dbg(pmf_dev->dev, "AMT is supported and notifications %s\n", + req.amt_event ? "Enabled" : "Disabled"); + pmf_dev->amt_enabled = !!req.amt_event; + + if (pmf_dev->amt_enabled) + amd_pmf_handle_amt(pmf_dev); + else + amd_pmf_reset_amt(pmf_dev); + } + + if (req.pending_req & BIT(APMF_CQL_NOTIFICATION)) { + dev_dbg(pmf_dev->dev, "CQL is supported and notifications %s\n", + req.cql_event ? "Enabled" : "Disabled"); + + /* update the target mode information */ + if (pmf_dev->amt_enabled) + amd_pmf_update_2_cql(pmf_dev, req.cql_event); + } +} + +static int apmf_if_verify_interface(struct amd_pmf_dev *pdev) +{ + struct apmf_verify_interface output; + int err; + + err = apmf_if_call_store_buffer(pdev, APMF_FUNC_VERIFY_INTERFACE, &output, sizeof(output)); + if (err) + return err; + + /* only set if not already set by a quirk */ + if (!pdev->supported_func) + pdev->supported_func = output.supported_functions; + + dev_dbg(pdev->dev, "supported functions:0x%x notifications:0x%x version:%u\n", + output.supported_functions, output.notification_mask, output.version); + + pdev->pmf_if_version = output.version; + + pdev->notifications = output.notification_mask; + return 0; +} + +static int apmf_get_system_params(struct amd_pmf_dev *dev) +{ + struct apmf_system_params params; + int err; + + if (!is_apmf_func_supported(dev, APMF_FUNC_GET_SYS_PARAMS)) + return -EINVAL; + + err = apmf_if_call_store_buffer(dev, APMF_FUNC_GET_SYS_PARAMS, ¶ms, sizeof(params)); + if (err) + return err; + + dev_dbg(dev->dev, "system params mask:0x%x flags:0x%x cmd_code:0x%x heartbeat:%d\n", + params.valid_mask, + params.flags, + params.command_code, + params.heartbeat_int); + params.flags = params.flags & params.valid_mask; + dev->hb_interval = params.heartbeat_int; + + return 0; +} + +int apmf_get_dyn_slider_def_ac(struct amd_pmf_dev *pdev, struct apmf_dyn_slider_output *data) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_DYN_SLIDER_AC, data, sizeof(*data)); +} + +int apmf_get_dyn_slider_def_dc(struct amd_pmf_dev *pdev, struct apmf_dyn_slider_output *data) +{ + return apmf_if_call_store_buffer(pdev, APMF_FUNC_DYN_SLIDER_DC, data, sizeof(*data)); +} + +static apmf_event_handler_t apmf_event_handlers[] = { + [PMF_IF_V1] = apmf_event_handler_v1, + [PMF_IF_V2] = apmf_event_handler_v2, +}; + +int apmf_install_handler(struct amd_pmf_dev *pmf_dev) +{ + acpi_handle ahandle = ACPI_HANDLE(pmf_dev->dev); + acpi_status status; + + /* Install the APMF Notify handler */ + if (is_apmf_func_supported(pmf_dev, APMF_FUNC_AUTO_MODE) && + is_apmf_func_supported(pmf_dev, APMF_FUNC_SBIOS_REQUESTS)) { + status = acpi_install_notify_handler(ahandle, ACPI_ALL_NOTIFY, + apmf_event_handler, pmf_dev); + if (ACPI_FAILURE(status)) { + dev_err(pmf_dev->dev, "failed to install notify handler\n"); + return -ENODEV; + } + + /* Call the handler once manually to catch up with possibly missed notifies. */ + apmf_event_handler(ahandle, 0, pmf_dev); + } + + if (!pmf_dev->smart_pc_enabled) + return -EINVAL; + + switch (pmf_dev->pmf_if_version) { + case PMF_IF_V1: + if (!is_apmf_bios_input_notifications_supported(pmf_dev)) + break; + fallthrough; + case PMF_IF_V2: + status = acpi_install_notify_handler(ahandle, ACPI_ALL_NOTIFY, + apmf_event_handlers[pmf_dev->pmf_if_version], pmf_dev); + if (ACPI_FAILURE(status)) { + dev_err(pmf_dev->dev, + "failed to install notify handler v%d for custom BIOS inputs\n", + pmf_dev->pmf_if_version); + return -ENODEV; + } + break; + default: + break; + } + + return 0; +} + +int apmf_check_smart_pc(struct amd_pmf_dev *pmf_dev) +{ + struct platform_device *pdev = to_platform_device(pmf_dev->dev); + + pmf_dev->res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!pmf_dev->res) { + dev_dbg(pmf_dev->dev, "Failed to get I/O memory resource\n"); + return -EINVAL; + } + + pmf_dev->policy_addr = pmf_dev->res->start; + /* + * We cannot use resource_size() here because it adds an extra byte to round off the size. + * In the case of PMF ResourceTemplate(), this rounding is already handled within the _CRS. + * Using resource_size() would increase the resource size by 1, causing a mismatch with the + * length field and leading to issues. Therefore, simply use end-start of the ACPI resource + * to obtain the actual length. + */ + pmf_dev->policy_sz = pmf_dev->res->end - pmf_dev->res->start; + + if (!pmf_dev->policy_addr || pmf_dev->policy_sz > POLICY_BUF_MAX_SZ || + pmf_dev->policy_sz == 0) { + dev_err(pmf_dev->dev, "Incorrect policy params, possibly a SBIOS bug\n"); + return -EINVAL; + } + + return 0; +} + +int amd_pmf_smartpc_apply_bios_output(struct amd_pmf_dev *dev, u32 val, u32 preq, u32 idx) +{ + if (!is_apmf_func_supported(dev, APMF_FUNC_NOTIFY_SMART_PC_UPDATES)) + return -EINVAL; + + return apmf_notify_smart_pc_update(dev, val, preq, idx); +} + +void apmf_acpi_deinit(struct amd_pmf_dev *pmf_dev) +{ + acpi_handle ahandle = ACPI_HANDLE(pmf_dev->dev); + + if (pmf_dev->hb_interval && pmf_dev->pmf_if_version == PMF_IF_V1) + cancel_delayed_work_sync(&pmf_dev->heart_beat); + + if (is_apmf_func_supported(pmf_dev, APMF_FUNC_AUTO_MODE) && + is_apmf_func_supported(pmf_dev, APMF_FUNC_SBIOS_REQUESTS)) + acpi_remove_notify_handler(ahandle, ACPI_ALL_NOTIFY, apmf_event_handler); + + if (!pmf_dev->smart_pc_enabled) + return; + + switch (pmf_dev->pmf_if_version) { + case PMF_IF_V1: + if (!is_apmf_bios_input_notifications_supported(pmf_dev)) + break; + fallthrough; + case PMF_IF_V2: + acpi_remove_notify_handler(ahandle, ACPI_ALL_NOTIFY, + apmf_event_handlers[pmf_dev->pmf_if_version]); + break; + default: + break; + } +} + +int apmf_acpi_init(struct amd_pmf_dev *pmf_dev) +{ + int ret; + + ret = apmf_if_verify_interface(pmf_dev); + if (ret) { + dev_err(pmf_dev->dev, "APMF verify interface failed :%d\n", ret); + goto out; + } + + ret = apmf_get_system_params(pmf_dev); + if (ret) { + dev_dbg(pmf_dev->dev, "APMF apmf_get_system_params failed :%d\n", ret); + goto out; + } + + if (pmf_dev->hb_interval && pmf_dev->pmf_if_version == PMF_IF_V1) { + /* send heartbeats only if the interval is not zero */ + INIT_DELAYED_WORK(&pmf_dev->heart_beat, apmf_sbios_heartbeat_notify); + schedule_delayed_work(&pmf_dev->heart_beat, 0); + } + +out: + return ret; +} diff --git a/drivers/platform/x86/amd/pmf/auto-mode.c b/drivers/platform/x86/amd/pmf/auto-mode.c new file mode 100644 index 000000000..faf15a8f7 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/auto-mode.c @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include +#include +#include "pmf.h" + +static struct auto_mode_mode_config config_store; +static const char *state_as_str(unsigned int state); + +#ifdef CONFIG_AMD_PMF_DEBUG +static void amd_pmf_dump_auto_mode_defaults(struct auto_mode_mode_config *data) +{ + struct auto_mode_mode_settings *its_mode; + + pr_debug("Auto Mode Data - BEGIN\n"); + + /* time constant */ + pr_debug("balanced_to_perf: %u ms\n", + data->transition[AUTO_TRANSITION_TO_PERFORMANCE].time_constant); + pr_debug("perf_to_balanced: %u ms\n", + data->transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].time_constant); + pr_debug("quiet_to_balanced: %u ms\n", + data->transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].time_constant); + pr_debug("balanced_to_quiet: %u ms\n", + data->transition[AUTO_TRANSITION_TO_QUIET].time_constant); + + /* power floor */ + pr_debug("pfloor_perf: %u mW\n", data->mode_set[AUTO_PERFORMANCE].power_floor); + pr_debug("pfloor_balanced: %u mW\n", data->mode_set[AUTO_BALANCE].power_floor); + pr_debug("pfloor_quiet: %u mW\n", data->mode_set[AUTO_QUIET].power_floor); + + /* Power delta for mode change */ + pr_debug("pd_balanced_to_perf: %u mW\n", + data->transition[AUTO_TRANSITION_TO_PERFORMANCE].power_delta); + pr_debug("pd_perf_to_balanced: %u mW\n", + data->transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].power_delta); + pr_debug("pd_quiet_to_balanced: %u mW\n", + data->transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].power_delta); + pr_debug("pd_balanced_to_quiet: %u mW\n", + data->transition[AUTO_TRANSITION_TO_QUIET].power_delta); + + /* skin temperature limits */ + its_mode = &data->mode_set[AUTO_PERFORMANCE_ON_LAP]; + pr_debug("stt_apu_perf_on_lap: %u C\n", + its_mode->power_control.stt_skin_temp[STT_TEMP_APU]); + pr_debug("stt_hs2_perf_on_lap: %u C\n", + its_mode->power_control.stt_skin_temp[STT_TEMP_HS2]); + pr_debug("stt_min_limit_perf_on_lap: %u mW\n", its_mode->power_control.stt_min); + + its_mode = &data->mode_set[AUTO_PERFORMANCE]; + pr_debug("stt_apu_perf: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_APU]); + pr_debug("stt_hs2_perf: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_HS2]); + pr_debug("stt_min_limit_perf: %u mW\n", its_mode->power_control.stt_min); + + its_mode = &data->mode_set[AUTO_BALANCE]; + pr_debug("stt_apu_balanced: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_APU]); + pr_debug("stt_hs2_balanced: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_HS2]); + pr_debug("stt_min_limit_balanced: %u mW\n", its_mode->power_control.stt_min); + + its_mode = &data->mode_set[AUTO_QUIET]; + pr_debug("stt_apu_quiet: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_APU]); + pr_debug("stt_hs2_quiet: %u C\n", its_mode->power_control.stt_skin_temp[STT_TEMP_HS2]); + pr_debug("stt_min_limit_quiet: %u mW\n", its_mode->power_control.stt_min); + + /* SPL based power limits */ + its_mode = &data->mode_set[AUTO_PERFORMANCE_ON_LAP]; + pr_debug("fppt_perf_on_lap: %u mW\n", its_mode->power_control.fppt); + pr_debug("sppt_perf_on_lap: %u mW\n", its_mode->power_control.sppt); + pr_debug("spl_perf_on_lap: %u mW\n", its_mode->power_control.spl); + pr_debug("sppt_apu_only_perf_on_lap: %u mW\n", its_mode->power_control.sppt_apu_only); + + its_mode = &data->mode_set[AUTO_PERFORMANCE]; + pr_debug("fppt_perf: %u mW\n", its_mode->power_control.fppt); + pr_debug("sppt_perf: %u mW\n", its_mode->power_control.sppt); + pr_debug("spl_perf: %u mW\n", its_mode->power_control.spl); + pr_debug("sppt_apu_only_perf: %u mW\n", its_mode->power_control.sppt_apu_only); + + its_mode = &data->mode_set[AUTO_BALANCE]; + pr_debug("fppt_balanced: %u mW\n", its_mode->power_control.fppt); + pr_debug("sppt_balanced: %u mW\n", its_mode->power_control.sppt); + pr_debug("spl_balanced: %u mW\n", its_mode->power_control.spl); + pr_debug("sppt_apu_only_balanced: %u mW\n", its_mode->power_control.sppt_apu_only); + + its_mode = &data->mode_set[AUTO_QUIET]; + pr_debug("fppt_quiet: %u mW\n", its_mode->power_control.fppt); + pr_debug("sppt_quiet: %u mW\n", its_mode->power_control.sppt); + pr_debug("spl_quiet: %u mW\n", its_mode->power_control.spl); + pr_debug("sppt_apu_only_quiet: %u mW\n", its_mode->power_control.sppt_apu_only); + + /* Fan ID */ + pr_debug("fan_id_perf: %lu\n", + data->mode_set[AUTO_PERFORMANCE].fan_control.fan_id); + pr_debug("fan_id_balanced: %lu\n", + data->mode_set[AUTO_BALANCE].fan_control.fan_id); + pr_debug("fan_id_quiet: %lu\n", + data->mode_set[AUTO_QUIET].fan_control.fan_id); + + pr_debug("Auto Mode Data - END\n"); +} +#else +static void amd_pmf_dump_auto_mode_defaults(struct auto_mode_mode_config *data) {} +#endif + +static void amd_pmf_set_automode(struct amd_pmf_dev *dev, int idx, + struct auto_mode_mode_config *table) +{ + struct power_table_control *pwr_ctrl = &config_store.mode_set[idx].power_control; + + amd_pmf_send_cmd(dev, SET_SPL, SET_CMD, pwr_ctrl->spl, NULL); + amd_pmf_send_cmd(dev, SET_FPPT, SET_CMD, pwr_ctrl->fppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT, SET_CMD, pwr_ctrl->sppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT_APU_ONLY, SET_CMD, pwr_ctrl->sppt_apu_only, NULL); + amd_pmf_send_cmd(dev, SET_STT_MIN_LIMIT, SET_CMD, pwr_ctrl->stt_min, NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_APU, SET_CMD, + fixp_q88_fromint(pwr_ctrl->stt_skin_temp[STT_TEMP_APU]), NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_HS2, SET_CMD, + fixp_q88_fromint(pwr_ctrl->stt_skin_temp[STT_TEMP_HS2]), NULL); + + if (is_apmf_func_supported(dev, APMF_FUNC_SET_FAN_IDX)) + apmf_update_fan_idx(dev, config_store.mode_set[idx].fan_control.manual, + config_store.mode_set[idx].fan_control.fan_id); +} + +static int amd_pmf_get_moving_avg(struct amd_pmf_dev *pdev, int socket_power) +{ + int i, total = 0; + + if (pdev->socket_power_history_idx == -1) { + for (i = 0; i < AVG_SAMPLE_SIZE; i++) + pdev->socket_power_history[i] = socket_power; + } + + pdev->socket_power_history_idx = (pdev->socket_power_history_idx + 1) % AVG_SAMPLE_SIZE; + pdev->socket_power_history[pdev->socket_power_history_idx] = socket_power; + + for (i = 0; i < AVG_SAMPLE_SIZE; i++) + total += pdev->socket_power_history[i]; + + return total / AVG_SAMPLE_SIZE; +} + +void amd_pmf_trans_automode(struct amd_pmf_dev *dev, int socket_power, ktime_t time_elapsed_ms) +{ + int avg_power = 0; + bool update = false; + int i, j; + + /* Get the average moving average computed by auto mode algorithm */ + avg_power = amd_pmf_get_moving_avg(dev, socket_power); + + for (i = 0; i < AUTO_TRANSITION_MAX; i++) { + if ((config_store.transition[i].shifting_up && avg_power >= + config_store.transition[i].power_threshold) || + (!config_store.transition[i].shifting_up && avg_power <= + config_store.transition[i].power_threshold)) { + if (config_store.transition[i].timer < + config_store.transition[i].time_constant) + config_store.transition[i].timer += time_elapsed_ms; + } else { + config_store.transition[i].timer = 0; + } + + if (config_store.transition[i].timer >= + config_store.transition[i].time_constant && + !config_store.transition[i].applied) { + config_store.transition[i].applied = true; + update = true; + } else if (config_store.transition[i].timer <= + config_store.transition[i].time_constant && + config_store.transition[i].applied) { + config_store.transition[i].applied = false; + update = true; + } + +#ifdef CONFIG_AMD_PMF_DEBUG + dev_dbg(dev->dev, "[AUTO MODE] average_power : %d mW mode: %s\n", avg_power, + state_as_str(config_store.current_mode)); + + dev_dbg(dev->dev, "[AUTO MODE] time: %lld ms timer: %u ms tc: %u ms\n", + time_elapsed_ms, config_store.transition[i].timer, + config_store.transition[i].time_constant); + + dev_dbg(dev->dev, "[AUTO MODE] shiftup: %u pt: %u mW pf: %u mW pd: %u mW\n", + config_store.transition[i].shifting_up, + config_store.transition[i].power_threshold, + config_store.mode_set[i].power_floor, + config_store.transition[i].power_delta); +#endif + } + + dev_dbg(dev->dev, "[AUTO_MODE] avg power: %u mW mode: %s\n", avg_power, + state_as_str(config_store.current_mode)); + +#ifdef CONFIG_AMD_PMF_DEBUG + dev_dbg(dev->dev, "[AUTO MODE] priority1: %u priority2: %u priority3: %u priority4: %u\n", + config_store.transition[0].applied, + config_store.transition[1].applied, + config_store.transition[2].applied, + config_store.transition[3].applied); +#endif + + if (update) { + for (j = 0; j < AUTO_TRANSITION_MAX; j++) { + /* Apply the mode with highest priority indentified */ + if (config_store.transition[j].applied) { + if (config_store.current_mode != + config_store.transition[j].target_mode) { + config_store.current_mode = + config_store.transition[j].target_mode; + dev_dbg(dev->dev, "[AUTO_MODE] moving to mode:%s\n", + state_as_str(config_store.current_mode)); + amd_pmf_set_automode(dev, config_store.current_mode, NULL); + } + break; + } + } + } +} + +void amd_pmf_update_2_cql(struct amd_pmf_dev *dev, bool is_cql_event) +{ + int mode = config_store.current_mode; + + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].target_mode = + is_cql_event ? AUTO_PERFORMANCE_ON_LAP : AUTO_PERFORMANCE; + + if ((mode == AUTO_PERFORMANCE || mode == AUTO_PERFORMANCE_ON_LAP) && + mode != config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].target_mode) { + mode = config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].target_mode; + amd_pmf_set_automode(dev, mode, NULL); + } + dev_dbg(dev->dev, "updated CQL thermals\n"); +} + +static void amd_pmf_get_power_threshold(void) +{ + config_store.transition[AUTO_TRANSITION_TO_QUIET].power_threshold = + config_store.mode_set[AUTO_BALANCE].power_floor - + config_store.transition[AUTO_TRANSITION_TO_QUIET].power_delta; + + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].power_threshold = + config_store.mode_set[AUTO_BALANCE].power_floor - + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].power_delta; + + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].power_threshold = + config_store.mode_set[AUTO_QUIET].power_floor - + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].power_delta; + + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].power_threshold = + config_store.mode_set[AUTO_PERFORMANCE].power_floor - + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].power_delta; + +#ifdef CONFIG_AMD_PMF_DEBUG + pr_debug("[AUTO MODE TO_QUIET] pt: %u mW pf: %u mW pd: %u mW\n", + config_store.transition[AUTO_TRANSITION_TO_QUIET].power_threshold, + config_store.mode_set[AUTO_BALANCE].power_floor, + config_store.transition[AUTO_TRANSITION_TO_QUIET].power_delta); + + pr_debug("[AUTO MODE TO_PERFORMANCE] pt: %u mW pf: %u mW pd: %u mW\n", + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].power_threshold, + config_store.mode_set[AUTO_BALANCE].power_floor, + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].power_delta); + + pr_debug("[AUTO MODE QUIET_TO_BALANCE] pt: %u mW pf: %u mW pd: %u mW\n", + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE] + .power_threshold, + config_store.mode_set[AUTO_QUIET].power_floor, + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].power_delta); + + pr_debug("[AUTO MODE PERFORMANCE_TO_BALANCE] pt: %u mW pf: %u mW pd: %u mW\n", + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE] + .power_threshold, + config_store.mode_set[AUTO_PERFORMANCE].power_floor, + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].power_delta); +#endif +} + +static const char *state_as_str(unsigned int state) +{ + switch (state) { + case AUTO_QUIET: + return "QUIET"; + case AUTO_BALANCE: + return "BALANCED"; + case AUTO_PERFORMANCE_ON_LAP: + return "ON_LAP"; + case AUTO_PERFORMANCE: + return "PERFORMANCE"; + default: + return "Unknown Auto Mode State"; + } +} + +static void amd_pmf_load_defaults_auto_mode(struct amd_pmf_dev *dev) +{ + struct apmf_auto_mode output; + struct power_table_control *pwr_ctrl; + int i; + + apmf_get_auto_mode_def(dev, &output); + /* time constant */ + config_store.transition[AUTO_TRANSITION_TO_QUIET].time_constant = + output.balanced_to_quiet; + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].time_constant = + output.balanced_to_perf; + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].time_constant = + output.quiet_to_balanced; + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].time_constant = + output.perf_to_balanced; + + /* power floor */ + config_store.mode_set[AUTO_QUIET].power_floor = output.pfloor_quiet; + config_store.mode_set[AUTO_BALANCE].power_floor = output.pfloor_balanced; + config_store.mode_set[AUTO_PERFORMANCE].power_floor = output.pfloor_perf; + config_store.mode_set[AUTO_PERFORMANCE_ON_LAP].power_floor = output.pfloor_perf; + + /* Power delta for mode change */ + config_store.transition[AUTO_TRANSITION_TO_QUIET].power_delta = + output.pd_balanced_to_quiet; + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].power_delta = + output.pd_balanced_to_perf; + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].power_delta = + output.pd_quiet_to_balanced; + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].power_delta = + output.pd_perf_to_balanced; + + /* Power threshold */ + amd_pmf_get_power_threshold(); + + /* skin temperature limits */ + pwr_ctrl = &config_store.mode_set[AUTO_QUIET].power_control; + pwr_ctrl->spl = output.spl_quiet; + pwr_ctrl->sppt = output.sppt_quiet; + pwr_ctrl->fppt = output.fppt_quiet; + pwr_ctrl->sppt_apu_only = output.sppt_apu_only_quiet; + pwr_ctrl->stt_min = output.stt_min_limit_quiet; + pwr_ctrl->stt_skin_temp[STT_TEMP_APU] = output.stt_apu_quiet; + pwr_ctrl->stt_skin_temp[STT_TEMP_HS2] = output.stt_hs2_quiet; + + pwr_ctrl = &config_store.mode_set[AUTO_BALANCE].power_control; + pwr_ctrl->spl = output.spl_balanced; + pwr_ctrl->sppt = output.sppt_balanced; + pwr_ctrl->fppt = output.fppt_balanced; + pwr_ctrl->sppt_apu_only = output.sppt_apu_only_balanced; + pwr_ctrl->stt_min = output.stt_min_limit_balanced; + pwr_ctrl->stt_skin_temp[STT_TEMP_APU] = output.stt_apu_balanced; + pwr_ctrl->stt_skin_temp[STT_TEMP_HS2] = output.stt_hs2_balanced; + + pwr_ctrl = &config_store.mode_set[AUTO_PERFORMANCE].power_control; + pwr_ctrl->spl = output.spl_perf; + pwr_ctrl->sppt = output.sppt_perf; + pwr_ctrl->fppt = output.fppt_perf; + pwr_ctrl->sppt_apu_only = output.sppt_apu_only_perf; + pwr_ctrl->stt_min = output.stt_min_limit_perf; + pwr_ctrl->stt_skin_temp[STT_TEMP_APU] = output.stt_apu_perf; + pwr_ctrl->stt_skin_temp[STT_TEMP_HS2] = output.stt_hs2_perf; + + pwr_ctrl = &config_store.mode_set[AUTO_PERFORMANCE_ON_LAP].power_control; + pwr_ctrl->spl = output.spl_perf_on_lap; + pwr_ctrl->sppt = output.sppt_perf_on_lap; + pwr_ctrl->fppt = output.fppt_perf_on_lap; + pwr_ctrl->sppt_apu_only = output.sppt_apu_only_perf_on_lap; + pwr_ctrl->stt_min = output.stt_min_limit_perf_on_lap; + pwr_ctrl->stt_skin_temp[STT_TEMP_APU] = output.stt_apu_perf_on_lap; + pwr_ctrl->stt_skin_temp[STT_TEMP_HS2] = output.stt_hs2_perf_on_lap; + + /* Fan ID */ + config_store.mode_set[AUTO_QUIET].fan_control.fan_id = output.fan_id_quiet; + config_store.mode_set[AUTO_BALANCE].fan_control.fan_id = output.fan_id_balanced; + config_store.mode_set[AUTO_PERFORMANCE].fan_control.fan_id = output.fan_id_perf; + config_store.mode_set[AUTO_PERFORMANCE_ON_LAP].fan_control.fan_id = + output.fan_id_perf; + + config_store.transition[AUTO_TRANSITION_TO_QUIET].target_mode = AUTO_QUIET; + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].target_mode = + AUTO_PERFORMANCE; + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].target_mode = + AUTO_BALANCE; + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].target_mode = + AUTO_BALANCE; + + config_store.transition[AUTO_TRANSITION_TO_QUIET].shifting_up = false; + config_store.transition[AUTO_TRANSITION_TO_PERFORMANCE].shifting_up = true; + config_store.transition[AUTO_TRANSITION_FROM_QUIET_TO_BALANCE].shifting_up = true; + config_store.transition[AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE].shifting_up = + false; + + for (i = 0 ; i < AUTO_MODE_MAX ; i++) { + if (config_store.mode_set[i].fan_control.fan_id == FAN_INDEX_AUTO) + config_store.mode_set[i].fan_control.manual = false; + else + config_store.mode_set[i].fan_control.manual = true; + } + + /* set to initial default values */ + config_store.current_mode = AUTO_BALANCE; + dev->socket_power_history_idx = -1; + + amd_pmf_dump_auto_mode_defaults(&config_store); +} + +int amd_pmf_reset_amt(struct amd_pmf_dev *dev) +{ + /* + * OEM BIOS implementation guide says that if the auto mode is enabled + * the platform_profile registration shall be done by the OEM driver. + * There could be cases where both static slider and auto mode BIOS + * functions are enabled, in that case enable static slider updates + * only if it advertised as supported. + */ + + if (is_apmf_func_supported(dev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) { + dev_dbg(dev->dev, "resetting AMT thermals\n"); + amd_pmf_set_sps_power_limits(dev); + } + return 0; +} + +void amd_pmf_handle_amt(struct amd_pmf_dev *dev) +{ + amd_pmf_set_automode(dev, config_store.current_mode, NULL); +} + +void amd_pmf_deinit_auto_mode(struct amd_pmf_dev *dev) +{ + cancel_delayed_work_sync(&dev->work_buffer); +} + +void amd_pmf_init_auto_mode(struct amd_pmf_dev *dev) +{ + amd_pmf_load_defaults_auto_mode(dev); + amd_pmf_init_metrics_table(dev); +} diff --git a/drivers/platform/x86/amd/pmf/cnqf.c b/drivers/platform/x86/amd/pmf/cnqf.c new file mode 100644 index 000000000..5469fefb6 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/cnqf.c @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include +#include +#include "pmf.h" + +static struct cnqf_config config_store; + +#ifdef CONFIG_AMD_PMF_DEBUG +static const char *state_as_str_cnqf(unsigned int state) +{ + switch (state) { + case APMF_CNQF_TURBO: + return "turbo"; + case APMF_CNQF_PERFORMANCE: + return "performance"; + case APMF_CNQF_BALANCE: + return "balance"; + case APMF_CNQF_QUIET: + return "quiet"; + default: + return "Unknown CnQF State"; + } +} + +static void amd_pmf_cnqf_dump_defaults(struct apmf_dyn_slider_output *data, int idx) +{ + int i; + + pr_debug("Dynamic Slider %s Defaults - BEGIN\n", idx ? "DC" : "AC"); + pr_debug("size: %u\n", data->size); + pr_debug("flags: 0x%x\n", data->flags); + + /* Time constants */ + pr_debug("t_perf_to_turbo: %u ms\n", data->t_perf_to_turbo); + pr_debug("t_balanced_to_perf: %u ms\n", data->t_balanced_to_perf); + pr_debug("t_quiet_to_balanced: %u ms\n", data->t_quiet_to_balanced); + pr_debug("t_balanced_to_quiet: %u ms\n", data->t_balanced_to_quiet); + pr_debug("t_perf_to_balanced: %u ms\n", data->t_perf_to_balanced); + pr_debug("t_turbo_to_perf: %u ms\n", data->t_turbo_to_perf); + + for (i = 0 ; i < CNQF_MODE_MAX ; i++) { + pr_debug("pfloor_%s: %u mW\n", state_as_str_cnqf(i), data->ps[i].pfloor); + pr_debug("fppt_%s: %u mW\n", state_as_str_cnqf(i), data->ps[i].fppt); + pr_debug("sppt_%s: %u mW\n", state_as_str_cnqf(i), data->ps[i].sppt); + pr_debug("sppt_apuonly_%s: %u mW\n", + state_as_str_cnqf(i), data->ps[i].sppt_apu_only); + pr_debug("spl_%s: %u mW\n", state_as_str_cnqf(i), data->ps[i].spl); + pr_debug("stt_minlimit_%s: %u mW\n", + state_as_str_cnqf(i), data->ps[i].stt_min_limit); + pr_debug("stt_skintemp_apu_%s: %u C\n", state_as_str_cnqf(i), + data->ps[i].stt_skintemp[STT_TEMP_APU]); + pr_debug("stt_skintemp_hs2_%s: %u C\n", state_as_str_cnqf(i), + data->ps[i].stt_skintemp[STT_TEMP_HS2]); + pr_debug("fan_id_%s: %u\n", state_as_str_cnqf(i), data->ps[i].fan_id); + } + + pr_debug("Dynamic Slider %s Defaults - END\n", idx ? "DC" : "AC"); +} +#else +static void amd_pmf_cnqf_dump_defaults(struct apmf_dyn_slider_output *data, int idx) {} +#endif + +static int amd_pmf_set_cnqf(struct amd_pmf_dev *dev, int src, int idx, + struct cnqf_config *table) +{ + struct power_table_control *pc; + + pc = &config_store.mode_set[src][idx].power_control; + + amd_pmf_send_cmd(dev, SET_SPL, SET_CMD, pc->spl, NULL); + amd_pmf_send_cmd(dev, SET_FPPT, SET_CMD, pc->fppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT, SET_CMD, pc->sppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT_APU_ONLY, SET_CMD, pc->sppt_apu_only, NULL); + amd_pmf_send_cmd(dev, SET_STT_MIN_LIMIT, SET_CMD, pc->stt_min, NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_APU, SET_CMD, + fixp_q88_fromint(pc->stt_skin_temp[STT_TEMP_APU]), NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_HS2, SET_CMD, + fixp_q88_fromint(pc->stt_skin_temp[STT_TEMP_HS2]), NULL); + + if (is_apmf_func_supported(dev, APMF_FUNC_SET_FAN_IDX)) + apmf_update_fan_idx(dev, + config_store.mode_set[src][idx].fan_control.manual, + config_store.mode_set[src][idx].fan_control.fan_id); + + return 0; +} + +static void amd_pmf_update_power_threshold(int src) +{ + struct cnqf_mode_settings *ts; + struct cnqf_tran_params *tp; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_TO_QUIET]; + ts = &config_store.mode_set[src][CNQF_MODE_BALANCE]; + tp->power_threshold = ts->power_floor; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_TO_TURBO]; + ts = &config_store.mode_set[src][CNQF_MODE_PERFORMANCE]; + tp->power_threshold = ts->power_floor; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_FROM_BALANCE_TO_PERFORMANCE]; + ts = &config_store.mode_set[src][CNQF_MODE_BALANCE]; + tp->power_threshold = ts->power_floor; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_FROM_PERFORMANCE_TO_BALANCE]; + ts = &config_store.mode_set[src][CNQF_MODE_PERFORMANCE]; + tp->power_threshold = ts->power_floor; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_FROM_QUIET_TO_BALANCE]; + ts = &config_store.mode_set[src][CNQF_MODE_QUIET]; + tp->power_threshold = ts->power_floor; + + tp = &config_store.trans_param[src][CNQF_TRANSITION_FROM_TURBO_TO_PERFORMANCE]; + ts = &config_store.mode_set[src][CNQF_MODE_TURBO]; + tp->power_threshold = ts->power_floor; +} + +static const char *state_as_str(unsigned int state) +{ + switch (state) { + case CNQF_MODE_QUIET: + return "QUIET"; + case CNQF_MODE_BALANCE: + return "BALANCED"; + case CNQF_MODE_TURBO: + return "TURBO"; + case CNQF_MODE_PERFORMANCE: + return "PERFORMANCE"; + default: + return "Unknown CnQF mode"; + } +} + +static int amd_pmf_cnqf_get_power_source(struct amd_pmf_dev *dev) +{ + if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_AC) && + is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_DC)) + return amd_pmf_get_power_source(); + else if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_DC)) + return POWER_SOURCE_DC; + else + return POWER_SOURCE_AC; +} + +int amd_pmf_trans_cnqf(struct amd_pmf_dev *dev, int socket_power, ktime_t time_lapsed_ms) +{ + struct cnqf_tran_params *tp; + int src, i, j; + u32 avg_power = 0; + + src = amd_pmf_cnqf_get_power_source(dev); + + if (is_pprof_balanced(dev)) { + amd_pmf_set_cnqf(dev, src, config_store.current_mode, NULL); + } else { + /* + * Return from here if the platform_profile is not balanced + * so that preference is given to user mode selection, rather + * than enforcing CnQF to run all the time (if enabled) + */ + return -EINVAL; + } + + for (i = 0; i < CNQF_TRANSITION_MAX; i++) { + config_store.trans_param[src][i].timer += time_lapsed_ms; + config_store.trans_param[src][i].total_power += socket_power; + config_store.trans_param[src][i].count++; + + tp = &config_store.trans_param[src][i]; + +#ifdef CONFIG_AMD_PMF_DEBUG + dev_dbg(dev->dev, "avg_power: %u mW total_power: %u mW count: %u timer: %u ms\n", + avg_power, config_store.trans_param[src][i].total_power, + config_store.trans_param[src][i].count, + config_store.trans_param[src][i].timer); +#endif + if (tp->timer >= tp->time_constant && tp->count) { + avg_power = tp->total_power / tp->count; + + /* Reset the indices */ + tp->timer = 0; + tp->total_power = 0; + tp->count = 0; + + if ((tp->shifting_up && avg_power >= tp->power_threshold) || + (!tp->shifting_up && avg_power <= tp->power_threshold)) { + tp->priority = true; + } else { + tp->priority = false; + } + } + } + + dev_dbg(dev->dev, "[CNQF] Avg power: %u mW socket power: %u mW mode:%s\n", + avg_power, socket_power, state_as_str(config_store.current_mode)); + +#ifdef CONFIG_AMD_PMF_DEBUG + dev_dbg(dev->dev, "[CNQF] priority1: %u priority2: %u priority3: %u\n", + config_store.trans_param[src][0].priority, + config_store.trans_param[src][1].priority, + config_store.trans_param[src][2].priority); + + dev_dbg(dev->dev, "[CNQF] priority4: %u priority5: %u priority6: %u\n", + config_store.trans_param[src][3].priority, + config_store.trans_param[src][4].priority, + config_store.trans_param[src][5].priority); +#endif + + for (j = 0; j < CNQF_TRANSITION_MAX; j++) { + /* apply the highest priority */ + if (config_store.trans_param[src][j].priority) { + if (config_store.current_mode != + config_store.trans_param[src][j].target_mode) { + config_store.current_mode = + config_store.trans_param[src][j].target_mode; + dev_dbg(dev->dev, "Moving to Mode :%s\n", + state_as_str(config_store.current_mode)); + amd_pmf_set_cnqf(dev, src, + config_store.current_mode, NULL); + } + break; + } + } + return 0; +} + +static void amd_pmf_update_trans_data(int idx, struct apmf_dyn_slider_output *out) +{ + struct cnqf_tran_params *tp; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_TO_QUIET]; + tp->time_constant = out->t_balanced_to_quiet; + tp->target_mode = CNQF_MODE_QUIET; + tp->shifting_up = false; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_FROM_BALANCE_TO_PERFORMANCE]; + tp->time_constant = out->t_balanced_to_perf; + tp->target_mode = CNQF_MODE_PERFORMANCE; + tp->shifting_up = true; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_FROM_QUIET_TO_BALANCE]; + tp->time_constant = out->t_quiet_to_balanced; + tp->target_mode = CNQF_MODE_BALANCE; + tp->shifting_up = true; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_FROM_PERFORMANCE_TO_BALANCE]; + tp->time_constant = out->t_perf_to_balanced; + tp->target_mode = CNQF_MODE_BALANCE; + tp->shifting_up = false; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_FROM_TURBO_TO_PERFORMANCE]; + tp->time_constant = out->t_turbo_to_perf; + tp->target_mode = CNQF_MODE_PERFORMANCE; + tp->shifting_up = false; + + tp = &config_store.trans_param[idx][CNQF_TRANSITION_TO_TURBO]; + tp->time_constant = out->t_perf_to_turbo; + tp->target_mode = CNQF_MODE_TURBO; + tp->shifting_up = true; +} + +static void amd_pmf_update_mode_set(int idx, struct apmf_dyn_slider_output *out) +{ + struct cnqf_mode_settings *ms; + + /* Quiet Mode */ + ms = &config_store.mode_set[idx][CNQF_MODE_QUIET]; + ms->power_floor = out->ps[APMF_CNQF_QUIET].pfloor; + ms->power_control.fppt = out->ps[APMF_CNQF_QUIET].fppt; + ms->power_control.sppt = out->ps[APMF_CNQF_QUIET].sppt; + ms->power_control.sppt_apu_only = out->ps[APMF_CNQF_QUIET].sppt_apu_only; + ms->power_control.spl = out->ps[APMF_CNQF_QUIET].spl; + ms->power_control.stt_min = out->ps[APMF_CNQF_QUIET].stt_min_limit; + ms->power_control.stt_skin_temp[STT_TEMP_APU] = + out->ps[APMF_CNQF_QUIET].stt_skintemp[STT_TEMP_APU]; + ms->power_control.stt_skin_temp[STT_TEMP_HS2] = + out->ps[APMF_CNQF_QUIET].stt_skintemp[STT_TEMP_HS2]; + ms->fan_control.fan_id = out->ps[APMF_CNQF_QUIET].fan_id; + + /* Balance Mode */ + ms = &config_store.mode_set[idx][CNQF_MODE_BALANCE]; + ms->power_floor = out->ps[APMF_CNQF_BALANCE].pfloor; + ms->power_control.fppt = out->ps[APMF_CNQF_BALANCE].fppt; + ms->power_control.sppt = out->ps[APMF_CNQF_BALANCE].sppt; + ms->power_control.sppt_apu_only = out->ps[APMF_CNQF_BALANCE].sppt_apu_only; + ms->power_control.spl = out->ps[APMF_CNQF_BALANCE].spl; + ms->power_control.stt_min = out->ps[APMF_CNQF_BALANCE].stt_min_limit; + ms->power_control.stt_skin_temp[STT_TEMP_APU] = + out->ps[APMF_CNQF_BALANCE].stt_skintemp[STT_TEMP_APU]; + ms->power_control.stt_skin_temp[STT_TEMP_HS2] = + out->ps[APMF_CNQF_BALANCE].stt_skintemp[STT_TEMP_HS2]; + ms->fan_control.fan_id = out->ps[APMF_CNQF_BALANCE].fan_id; + + /* Performance Mode */ + ms = &config_store.mode_set[idx][CNQF_MODE_PERFORMANCE]; + ms->power_floor = out->ps[APMF_CNQF_PERFORMANCE].pfloor; + ms->power_control.fppt = out->ps[APMF_CNQF_PERFORMANCE].fppt; + ms->power_control.sppt = out->ps[APMF_CNQF_PERFORMANCE].sppt; + ms->power_control.sppt_apu_only = out->ps[APMF_CNQF_PERFORMANCE].sppt_apu_only; + ms->power_control.spl = out->ps[APMF_CNQF_PERFORMANCE].spl; + ms->power_control.stt_min = out->ps[APMF_CNQF_PERFORMANCE].stt_min_limit; + ms->power_control.stt_skin_temp[STT_TEMP_APU] = + out->ps[APMF_CNQF_PERFORMANCE].stt_skintemp[STT_TEMP_APU]; + ms->power_control.stt_skin_temp[STT_TEMP_HS2] = + out->ps[APMF_CNQF_PERFORMANCE].stt_skintemp[STT_TEMP_HS2]; + ms->fan_control.fan_id = out->ps[APMF_CNQF_PERFORMANCE].fan_id; + + /* Turbo Mode */ + ms = &config_store.mode_set[idx][CNQF_MODE_TURBO]; + ms->power_floor = out->ps[APMF_CNQF_TURBO].pfloor; + ms->power_control.fppt = out->ps[APMF_CNQF_TURBO].fppt; + ms->power_control.sppt = out->ps[APMF_CNQF_TURBO].sppt; + ms->power_control.sppt_apu_only = out->ps[APMF_CNQF_TURBO].sppt_apu_only; + ms->power_control.spl = out->ps[APMF_CNQF_TURBO].spl; + ms->power_control.stt_min = out->ps[APMF_CNQF_TURBO].stt_min_limit; + ms->power_control.stt_skin_temp[STT_TEMP_APU] = + out->ps[APMF_CNQF_TURBO].stt_skintemp[STT_TEMP_APU]; + ms->power_control.stt_skin_temp[STT_TEMP_HS2] = + out->ps[APMF_CNQF_TURBO].stt_skintemp[STT_TEMP_HS2]; + ms->fan_control.fan_id = out->ps[APMF_CNQF_TURBO].fan_id; +} + +static int amd_pmf_check_flags(struct amd_pmf_dev *dev) +{ + struct apmf_dyn_slider_output out = {}; + + if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_AC)) + apmf_get_dyn_slider_def_ac(dev, &out); + else if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_DC)) + apmf_get_dyn_slider_def_dc(dev, &out); + + return out.flags; +} + +static int amd_pmf_load_defaults_cnqf(struct amd_pmf_dev *dev) +{ + struct apmf_dyn_slider_output out; + int i, j, ret; + + for (i = 0; i < POWER_SOURCE_MAX; i++) { + if (!is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_AC + i)) + continue; + + if (i == POWER_SOURCE_AC) + ret = apmf_get_dyn_slider_def_ac(dev, &out); + else + ret = apmf_get_dyn_slider_def_dc(dev, &out); + if (ret) { + dev_err(dev->dev, "APMF apmf_get_dyn_slider_def_dc failed :%d\n", ret); + return ret; + } + + amd_pmf_cnqf_dump_defaults(&out, i); + amd_pmf_update_mode_set(i, &out); + amd_pmf_update_trans_data(i, &out); + amd_pmf_update_power_threshold(i); + + for (j = 0; j < CNQF_MODE_MAX; j++) { + if (config_store.mode_set[i][j].fan_control.fan_id == FAN_INDEX_AUTO) + config_store.mode_set[i][j].fan_control.manual = false; + else + config_store.mode_set[i][j].fan_control.manual = true; + } + } + + /* set to initial default values */ + config_store.current_mode = CNQF_MODE_BALANCE; + + return 0; +} + +static ssize_t cnqf_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + int result, src; + bool input; + + result = kstrtobool(buf, &input); + if (result) + return result; + + src = amd_pmf_cnqf_get_power_source(pdev); + pdev->cnqf_enabled = input; + + if (pdev->cnqf_enabled && is_pprof_balanced(pdev)) { + amd_pmf_set_cnqf(pdev, src, config_store.current_mode, NULL); + } else { + if (is_apmf_func_supported(pdev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) + amd_pmf_set_sps_power_limits(pdev); + } + + dev_dbg(pdev->dev, "Received CnQF %s\n", str_on_off(input)); + return count; +} + +static ssize_t cnqf_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%s\n", str_on_off(pdev->cnqf_enabled)); +} + +static DEVICE_ATTR_RW(cnqf_enable); + +static umode_t cnqf_feature_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + + return pdev->cnqf_supported ? attr->mode : 0; +} + +static struct attribute *cnqf_feature_attrs[] = { + &dev_attr_cnqf_enable.attr, + NULL +}; + +const struct attribute_group cnqf_feature_attribute_group = { + .is_visible = cnqf_feature_is_visible, + .attrs = cnqf_feature_attrs, +}; + +void amd_pmf_deinit_cnqf(struct amd_pmf_dev *dev) +{ + cancel_delayed_work_sync(&dev->work_buffer); +} + +int amd_pmf_init_cnqf(struct amd_pmf_dev *dev) +{ + int ret, src; + + /* + * Note the caller of this function has already checked that both + * APMF_FUNC_DYN_SLIDER_AC and APMF_FUNC_DYN_SLIDER_DC are supported. + */ + + ret = amd_pmf_load_defaults_cnqf(dev); + if (ret < 0) + return ret; + + amd_pmf_init_metrics_table(dev); + + dev->cnqf_supported = true; + dev->cnqf_enabled = amd_pmf_check_flags(dev); + + /* update the thermal for CnQF */ + if (dev->cnqf_enabled && is_pprof_balanced(dev)) { + src = amd_pmf_cnqf_get_power_source(dev); + amd_pmf_set_cnqf(dev, src, config_store.current_mode, NULL); + } + + return 0; +} diff --git a/drivers/platform/x86/amd/pmf/core.c b/drivers/platform/x86/amd/pmf/core.c new file mode 100644 index 000000000..b9e5a2cf3 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/core.c @@ -0,0 +1,672 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD Platform Management Framework Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pmf.h" + +/* PMF-SMU communication registers */ +#define AMD_PMF_REGISTER_MESSAGE 0xA18 +#define AMD_PMF_REGISTER_RESPONSE 0xA78 +#define AMD_PMF_REGISTER_ARGUMENT 0xA58 + +/* Base address of SMU for mapping physical address to virtual address */ +#define AMD_PMF_MAPPING_SIZE 0x01000 +#define AMD_PMF_BASE_ADDR_OFFSET 0x10000 +#define AMD_PMF_BASE_ADDR_LO 0x13B102E8 +#define AMD_PMF_BASE_ADDR_HI 0x13B102EC +#define AMD_PMF_BASE_ADDR_LO_MASK GENMASK(15, 0) +#define AMD_PMF_BASE_ADDR_HI_MASK GENMASK(31, 20) + +/* SMU Response Codes */ +#define AMD_PMF_RESULT_OK 0x01 +#define AMD_PMF_RESULT_CMD_REJECT_BUSY 0xFC +#define AMD_PMF_RESULT_CMD_REJECT_PREREQ 0xFD +#define AMD_PMF_RESULT_CMD_UNKNOWN 0xFE +#define AMD_PMF_RESULT_FAILED 0xFF + +#define PMF_MSG_DELAY_MIN_US 50 +#define RESPONSE_REGISTER_LOOP_MAX 20000 + +#define DELAY_MIN_US 2000 +#define DELAY_MAX_US 3000 + +/* override Metrics Table sample size time (in ms) */ +static int metrics_table_loop_ms = 1000; +module_param(metrics_table_loop_ms, int, 0644); +MODULE_PARM_DESC(metrics_table_loop_ms, "Metrics Table sample size time (default = 1000ms)"); + +/* Force load on supported older platforms */ +static bool force_load; +module_param(force_load, bool, 0444); +MODULE_PARM_DESC(force_load, "Force load this driver on supported older platforms (experimental)"); + +static bool smart_pc_support = true; +module_param(smart_pc_support, bool, 0444); +MODULE_PARM_DESC(smart_pc_support, "Smart PC Support (default = true)"); + +static struct device *pmf_device; + +static int amd_pmf_pwr_src_notify_call(struct notifier_block *nb, unsigned long event, void *data) +{ + struct amd_pmf_dev *pmf = container_of(nb, struct amd_pmf_dev, pwr_src_notifier); + + if (event != PSY_EVENT_PROP_CHANGED) + return NOTIFY_OK; + + if (is_apmf_func_supported(pmf, APMF_FUNC_AUTO_MODE) || + is_apmf_func_supported(pmf, APMF_FUNC_DYN_SLIDER_DC) || + is_apmf_func_supported(pmf, APMF_FUNC_DYN_SLIDER_AC)) { + if ((pmf->amt_enabled || pmf->cnqf_enabled) && is_pprof_balanced(pmf)) + return NOTIFY_DONE; + } + + if (is_apmf_func_supported(pmf, APMF_FUNC_STATIC_SLIDER_GRANULAR)) + amd_pmf_set_sps_power_limits(pmf); + + if (is_apmf_func_supported(pmf, APMF_FUNC_OS_POWER_SLIDER_UPDATE)) + amd_pmf_power_slider_update_event(pmf); + + return NOTIFY_OK; +} + +static int current_power_limits_show(struct seq_file *seq, void *unused) +{ + struct amd_pmf_dev *dev = seq->private; + struct amd_pmf_static_slider_granular table; + int mode, src = 0; + + mode = amd_pmf_get_pprof_modes(dev); + if (mode < 0) + return mode; + + src = amd_pmf_get_power_source(); + amd_pmf_update_slider(dev, SLIDER_OP_GET, mode, &table); + seq_printf(seq, "spl:%u fppt:%u sppt:%u sppt_apu_only:%u stt_min:%u stt[APU]:%u stt[HS2]: %u\n", + table.prop[src][mode].spl, + table.prop[src][mode].fppt, + table.prop[src][mode].sppt, + table.prop[src][mode].sppt_apu_only, + table.prop[src][mode].stt_min, + table.prop[src][mode].stt_skin_temp[STT_TEMP_APU], + table.prop[src][mode].stt_skin_temp[STT_TEMP_HS2]); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(current_power_limits); + +static void amd_pmf_dbgfs_unregister(struct amd_pmf_dev *dev) +{ + debugfs_remove_recursive(dev->dbgfs_dir); +} + +static void amd_pmf_dbgfs_register(struct amd_pmf_dev *dev) +{ + dev->dbgfs_dir = debugfs_create_dir("amd_pmf", NULL); + if (dev->pmf_if_version == PMF_IF_V1) + debugfs_create_file("current_power_limits", 0644, dev->dbgfs_dir, dev, + ¤t_power_limits_fops); +} + +int amd_pmf_get_power_source(void) +{ + if (power_supply_is_system_supplied() > 0) + return POWER_SOURCE_AC; + else + return POWER_SOURCE_DC; +} + +static void amd_pmf_get_metrics(struct work_struct *work) +{ + struct amd_pmf_dev *dev = container_of(work, struct amd_pmf_dev, work_buffer.work); + ktime_t time_elapsed_ms; + int socket_power; + + guard(mutex)(&dev->update_mutex); + + /* Transfer table contents */ + memset(dev->buf, 0, sizeof(dev->m_table)); + amd_pmf_send_cmd(dev, SET_TRANSFER_TABLE, SET_CMD, METRICS_TABLE_ID, NULL); + memcpy(&dev->m_table, dev->buf, sizeof(dev->m_table)); + + time_elapsed_ms = ktime_to_ms(ktime_get()) - dev->start_time; + /* Calculate the avg SoC power consumption */ + socket_power = dev->m_table.apu_power + dev->m_table.dgpu_power; + + if (dev->amt_enabled) { + /* Apply the Auto Mode transition */ + amd_pmf_trans_automode(dev, socket_power, time_elapsed_ms); + } + + if (dev->cnqf_enabled) { + /* Apply the CnQF transition */ + amd_pmf_trans_cnqf(dev, socket_power, time_elapsed_ms); + } + + dev->start_time = ktime_to_ms(ktime_get()); + schedule_delayed_work(&dev->work_buffer, msecs_to_jiffies(metrics_table_loop_ms)); +} + +static inline u32 amd_pmf_reg_read(struct amd_pmf_dev *dev, int reg_offset) +{ + return ioread32(dev->regbase + reg_offset); +} + +static inline void amd_pmf_reg_write(struct amd_pmf_dev *dev, int reg_offset, u32 val) +{ + iowrite32(val, dev->regbase + reg_offset); +} + +static void __maybe_unused amd_pmf_dump_registers(struct amd_pmf_dev *dev) +{ + u32 value; + + value = amd_pmf_reg_read(dev, AMD_PMF_REGISTER_RESPONSE); + dev_dbg(dev->dev, "AMD_PMF_REGISTER_RESPONSE:%x\n", value); + + value = amd_pmf_reg_read(dev, AMD_PMF_REGISTER_ARGUMENT); + dev_dbg(dev->dev, "AMD_PMF_REGISTER_ARGUMENT:%d\n", value); + + value = amd_pmf_reg_read(dev, AMD_PMF_REGISTER_MESSAGE); + dev_dbg(dev->dev, "AMD_PMF_REGISTER_MESSAGE:%x\n", value); +} + +/** + * fixp_q88_fromint: Convert integer to Q8.8 + * @val: input value + * + * Converts an integer into binary fixed point format where 8 bits + * are used for integer and 8 bits are used for the decimal. + * + * Return: unsigned integer converted to Q8.8 format + */ +u32 fixp_q88_fromint(u32 val) +{ + return val << 8; +} + +int amd_pmf_send_cmd(struct amd_pmf_dev *dev, u8 message, bool get, u32 arg, u32 *data) +{ + int rc; + u32 val; + + guard(mutex)(&dev->lock); + + /* Wait until we get a valid response */ + rc = readx_poll_timeout(ioread32, dev->regbase + AMD_PMF_REGISTER_RESPONSE, + val, val != 0, PMF_MSG_DELAY_MIN_US, + PMF_MSG_DELAY_MIN_US * RESPONSE_REGISTER_LOOP_MAX); + if (rc) { + dev_err(dev->dev, "failed to talk to SMU\n"); + return rc; + } + + /* Write zero to response register */ + amd_pmf_reg_write(dev, AMD_PMF_REGISTER_RESPONSE, 0); + + /* Write argument into argument register */ + amd_pmf_reg_write(dev, AMD_PMF_REGISTER_ARGUMENT, arg); + + /* Write message ID to message ID register */ + amd_pmf_reg_write(dev, AMD_PMF_REGISTER_MESSAGE, message); + + /* Wait until we get a valid response */ + rc = readx_poll_timeout(ioread32, dev->regbase + AMD_PMF_REGISTER_RESPONSE, + val, val != 0, PMF_MSG_DELAY_MIN_US, + PMF_MSG_DELAY_MIN_US * RESPONSE_REGISTER_LOOP_MAX); + if (rc) { + dev_err(dev->dev, "SMU response timed out\n"); + return rc; + } + + switch (val) { + case AMD_PMF_RESULT_OK: + if (get) { + /* PMFW may take longer time to return back the data */ + usleep_range(DELAY_MIN_US, 10 * DELAY_MAX_US); + *data = amd_pmf_reg_read(dev, AMD_PMF_REGISTER_ARGUMENT); + } + break; + case AMD_PMF_RESULT_CMD_REJECT_BUSY: + dev_err(dev->dev, "SMU not ready. err: 0x%x\n", val); + rc = -EBUSY; + break; + case AMD_PMF_RESULT_CMD_UNKNOWN: + dev_err(dev->dev, "SMU cmd unknown. err: 0x%x\n", val); + rc = -EINVAL; + break; + case AMD_PMF_RESULT_CMD_REJECT_PREREQ: + case AMD_PMF_RESULT_FAILED: + default: + dev_err(dev->dev, "SMU cmd failed. err: 0x%x\n", val); + rc = -EIO; + break; + } + + amd_pmf_dump_registers(dev); + return rc; +} + +static const struct pci_device_id pmf_pci_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_AMD, AMD_CPU_ID_RMB) }, + { PCI_DEVICE(PCI_VENDOR_ID_AMD, AMD_CPU_ID_PS) }, + { PCI_DEVICE(PCI_VENDOR_ID_AMD, PCI_DEVICE_ID_AMD_1AH_M20H_ROOT) }, + { PCI_DEVICE(PCI_VENDOR_ID_AMD, PCI_DEVICE_ID_AMD_1AH_M60H_ROOT) }, + { } +}; + +int amd_pmf_set_dram_addr(struct amd_pmf_dev *dev, bool alloc_buffer) +{ + u64 phys_addr; + u32 hi, low; + + /* Get Metrics Table Address */ + if (alloc_buffer) { + switch (dev->cpu_id) { + case AMD_CPU_ID_PS: + case AMD_CPU_ID_RMB: + dev->mtable_size = sizeof(dev->m_table); + break; + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + dev->mtable_size = sizeof(dev->m_table_v2); + break; + default: + dev_err(dev->dev, "Invalid CPU id: 0x%x", dev->cpu_id); + } + + dev->buf = devm_kzalloc(dev->dev, dev->mtable_size, GFP_KERNEL); + if (!dev->buf) + return -ENOMEM; + } + + phys_addr = virt_to_phys(dev->buf); + hi = phys_addr >> 32; + low = phys_addr & GENMASK(31, 0); + + amd_pmf_send_cmd(dev, SET_DRAM_ADDR_HIGH, SET_CMD, hi, NULL); + amd_pmf_send_cmd(dev, SET_DRAM_ADDR_LOW, SET_CMD, low, NULL); + + return 0; +} + +int amd_pmf_init_metrics_table(struct amd_pmf_dev *dev) +{ + int ret; + + INIT_DELAYED_WORK(&dev->work_buffer, amd_pmf_get_metrics); + + ret = amd_pmf_set_dram_addr(dev, true); + if (ret) + return ret; + + /* + * Start collecting the metrics data after a small delay + * or else, we might end up getting stale values from PMFW. + */ + schedule_delayed_work(&dev->work_buffer, msecs_to_jiffies(metrics_table_loop_ms * 3)); + + return 0; +} + +static int is_npu_metrics_supported(struct amd_pmf_dev *pdev) +{ + switch (pdev->cpu_id) { + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int amd_pmf_get_smu_metrics(struct amd_pmf_dev *dev, struct amd_pmf_npu_metrics *data) +{ + int ret, i; + + guard(mutex)(&dev->metrics_mutex); + + ret = is_npu_metrics_supported(dev); + if (ret) + return ret; + + ret = amd_pmf_set_dram_addr(dev, true); + if (ret) + return ret; + + memset(dev->buf, 0, dev->mtable_size); + + /* Send SMU command to get NPU metrics */ + ret = amd_pmf_send_cmd(dev, SET_TRANSFER_TABLE, SET_CMD, METRICS_TABLE_ID, NULL); + if (ret) { + dev_err(dev->dev, "SMU command failed to get NPU metrics: %d\n", ret); + return ret; + } + + memcpy(&dev->m_table_v2, dev->buf, dev->mtable_size); + + data->npuclk_freq = dev->m_table_v2.npuclk_freq; + for (i = 0; i < ARRAY_SIZE(data->npu_busy); i++) + data->npu_busy[i] = dev->m_table_v2.npu_busy[i]; + data->npu_power = dev->m_table_v2.npu_power; + data->mpnpuclk_freq = dev->m_table_v2.mpnpuclk_freq; + data->npu_reads = dev->m_table_v2.npu_reads; + data->npu_writes = dev->m_table_v2.npu_writes; + + return 0; +} + +int amd_pmf_get_npu_data(struct amd_pmf_npu_metrics *info) +{ + struct amd_pmf_dev *pdev; + + if (!info) + return -EINVAL; + + if (!pmf_device) + return -ENODEV; + + pdev = dev_get_drvdata(pmf_device); + if (!pdev) + return -ENODEV; + + return amd_pmf_get_smu_metrics(pdev, info); +} +EXPORT_SYMBOL_NS_GPL(amd_pmf_get_npu_data, "AMD_PMF"); + +static int amd_pmf_reinit_ta(struct amd_pmf_dev *pdev) +{ + bool status; + int ret, i; + + for (i = 0; i < ARRAY_SIZE(amd_pmf_ta_uuid); i++) { + ret = amd_pmf_tee_init(pdev, &amd_pmf_ta_uuid[i]); + if (ret) { + dev_err(pdev->dev, "TEE init failed for UUID[%d] ret: %d\n", i, ret); + return ret; + } + + ret = amd_pmf_start_policy_engine(pdev); + dev_dbg(pdev->dev, "start policy engine ret: %d (UUID idx: %d)\n", ret, i); + status = ret == TA_PMF_TYPE_SUCCESS; + if (status) + break; + amd_pmf_tee_deinit(pdev); + } + + return 0; +} + +static int amd_pmf_restore_handler(struct device *dev) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + int ret; + + if (pdev->buf) { + ret = amd_pmf_set_dram_addr(pdev, false); + if (ret) + return ret; + } + + if (pdev->smart_pc_enabled) + amd_pmf_reinit_ta(pdev); + + return 0; +} + +static int amd_pmf_freeze_handler(struct device *dev) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + + if (!pdev->smart_pc_enabled) + return 0; + + cancel_delayed_work_sync(&pdev->pb_work); + /* Clear all TEE resources */ + amd_pmf_tee_deinit(pdev); + pdev->session_id = 0; + + return 0; +} + +static int amd_pmf_suspend_handler(struct device *dev) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + + if (pdev->smart_pc_enabled) + cancel_delayed_work_sync(&pdev->pb_work); + + if (is_apmf_func_supported(pdev, APMF_FUNC_SBIOS_HEARTBEAT_V2)) + amd_pmf_notify_sbios_heartbeat_event_v2(pdev, ON_SUSPEND); + + return 0; +} + +static int amd_pmf_resume_handler(struct device *dev) +{ + struct amd_pmf_dev *pdev = dev_get_drvdata(dev); + int ret; + + if (pdev->buf) { + ret = amd_pmf_set_dram_addr(pdev, false); + if (ret) + return ret; + } + + if (is_apmf_func_supported(pdev, APMF_FUNC_SBIOS_HEARTBEAT_V2)) + amd_pmf_notify_sbios_heartbeat_event_v2(pdev, ON_RESUME); + + if (pdev->smart_pc_enabled) + schedule_delayed_work(&pdev->pb_work, msecs_to_jiffies(2000)); + + return 0; +} + +static const struct dev_pm_ops amd_pmf_pm = { + .suspend = amd_pmf_suspend_handler, + .resume = amd_pmf_resume_handler, + .freeze = amd_pmf_freeze_handler, + .restore = amd_pmf_restore_handler, +}; + +static void amd_pmf_init_features(struct amd_pmf_dev *dev) +{ + int ret; + + /* Enable Static Slider */ + if (is_apmf_func_supported(dev, APMF_FUNC_STATIC_SLIDER_GRANULAR) || + is_apmf_func_supported(dev, APMF_FUNC_OS_POWER_SLIDER_UPDATE)) { + amd_pmf_init_sps(dev); + dev->pwr_src_notifier.notifier_call = amd_pmf_pwr_src_notify_call; + power_supply_reg_notifier(&dev->pwr_src_notifier); + dev_dbg(dev->dev, "SPS enabled and Platform Profiles registered\n"); + } + + if (smart_pc_support) { + amd_pmf_init_smart_pc(dev); + if (dev->smart_pc_enabled) { + dev_dbg(dev->dev, "Smart PC Solution Enabled\n"); + /* If Smart PC is enabled, no need to check for other features */ + return; + } + } else { + dev->smart_pc_enabled = false; + } + + if (is_apmf_func_supported(dev, APMF_FUNC_AUTO_MODE)) { + amd_pmf_init_auto_mode(dev); + dev_dbg(dev->dev, "Auto Mode Init done\n"); + } else if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_AC) || + is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_DC)) { + ret = amd_pmf_init_cnqf(dev); + if (ret) + dev_warn(dev->dev, "CnQF Init failed\n"); + } +} + +static void amd_pmf_deinit_features(struct amd_pmf_dev *dev) +{ + if (is_apmf_func_supported(dev, APMF_FUNC_STATIC_SLIDER_GRANULAR) || + is_apmf_func_supported(dev, APMF_FUNC_OS_POWER_SLIDER_UPDATE)) { + power_supply_unreg_notifier(&dev->pwr_src_notifier); + } + + if (dev->smart_pc_enabled) { + amd_pmf_deinit_smart_pc(dev); + } else if (is_apmf_func_supported(dev, APMF_FUNC_AUTO_MODE)) { + amd_pmf_deinit_auto_mode(dev); + } else if (is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_AC) || + is_apmf_func_supported(dev, APMF_FUNC_DYN_SLIDER_DC)) { + amd_pmf_deinit_cnqf(dev); + } +} + +static const struct acpi_device_id amd_pmf_acpi_ids[] = { + {"AMDI0100", 0x100}, + {"AMDI0102", 0}, + {"AMDI0103", 0}, + {"AMDI0105", 0}, + {"AMDI0107", 0}, + {"AMDI0108", 0}, + { } +}; +MODULE_DEVICE_TABLE(acpi, amd_pmf_acpi_ids); + +static int amd_pmf_probe(struct platform_device *pdev) +{ + const struct acpi_device_id *id; + struct amd_pmf_dev *dev; + struct pci_dev *rdev; + u32 base_addr_lo; + u32 base_addr_hi; + u64 base_addr; + u32 val; + int err; + + id = acpi_match_device(amd_pmf_acpi_ids, &pdev->dev); + if (!id) + return -ENODEV; + + if (id->driver_data == 0x100 && !force_load) + return -ENODEV; + + dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + dev->dev = &pdev->dev; + + rdev = pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(0, 0)); + if (!rdev || !pci_match_id(pmf_pci_ids, rdev)) { + pci_dev_put(rdev); + return -ENODEV; + } + + dev->cpu_id = rdev->device; + + err = amd_smn_read(0, AMD_PMF_BASE_ADDR_LO, &val); + if (err) { + pci_dev_put(rdev); + return dev_err_probe(dev->dev, pcibios_err_to_errno(err), + "error in reading from 0x%x\n", AMD_PMF_BASE_ADDR_LO); + } + + base_addr_lo = val & AMD_PMF_BASE_ADDR_HI_MASK; + + err = amd_smn_read(0, AMD_PMF_BASE_ADDR_HI, &val); + if (err) { + pci_dev_put(rdev); + return dev_err_probe(dev->dev, pcibios_err_to_errno(err), + "error in reading from 0x%x\n", AMD_PMF_BASE_ADDR_HI); + } + + base_addr_hi = val & AMD_PMF_BASE_ADDR_LO_MASK; + pci_dev_put(rdev); + base_addr = ((u64)base_addr_hi << 32 | base_addr_lo); + + dev->regbase = devm_ioremap(dev->dev, base_addr + AMD_PMF_BASE_ADDR_OFFSET, + AMD_PMF_MAPPING_SIZE); + if (!dev->regbase) + return -ENOMEM; + + err = devm_mutex_init(dev->dev, &dev->lock); + if (err) + return err; + + err = devm_mutex_init(dev->dev, &dev->update_mutex); + if (err) + return err; + + err = devm_mutex_init(dev->dev, &dev->cb_mutex); + if (err) + return err; + + err = devm_mutex_init(dev->dev, &dev->cbi_mutex); + if (err) + return err; + + err = devm_mutex_init(dev->dev, &dev->metrics_mutex); + if (err) + return err; + + apmf_acpi_init(dev); + platform_set_drvdata(pdev, dev); + amd_pmf_dbgfs_register(dev); + amd_pmf_init_features(dev); + apmf_install_handler(dev); + if (is_apmf_func_supported(dev, APMF_FUNC_SBIOS_HEARTBEAT_V2)) + amd_pmf_notify_sbios_heartbeat_event_v2(dev, ON_LOAD); + + pmf_device = dev->dev; + + dev_info(dev->dev, "registered PMF device successfully\n"); + + return 0; +} + +static void amd_pmf_remove(struct platform_device *pdev) +{ + struct amd_pmf_dev *dev = platform_get_drvdata(pdev); + + amd_pmf_deinit_features(dev); + if (is_apmf_func_supported(dev, APMF_FUNC_SBIOS_HEARTBEAT_V2)) + amd_pmf_notify_sbios_heartbeat_event_v2(dev, ON_UNLOAD); + apmf_acpi_deinit(dev); + amd_pmf_dbgfs_unregister(dev); +} + +static const struct attribute_group *amd_pmf_driver_groups[] = { + &cnqf_feature_attribute_group, + NULL, +}; + +static struct platform_driver amd_pmf_driver = { + .driver = { + .name = "amd-pmf", + .acpi_match_table = amd_pmf_acpi_ids, + .dev_groups = amd_pmf_driver_groups, + .pm = pm_sleep_ptr(&amd_pmf_pm), + }, + .probe = amd_pmf_probe, + .remove = amd_pmf_remove, +}; +module_platform_driver(amd_pmf_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("AMD Platform Management Framework Driver"); +MODULE_SOFTDEP("pre: amdtee"); diff --git a/drivers/platform/x86/amd/pmf/pmf.h b/drivers/platform/x86/amd/pmf/pmf.h new file mode 100644 index 000000000..69fef7448 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/pmf.h @@ -0,0 +1,931 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * AMD Platform Management Framework Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#ifndef PMF_H +#define PMF_H + +#include +#include +#include +#include +#include +#include +#include + +#define POLICY_BUF_MAX_SZ 0x4b000 +#define POLICY_SIGN_COOKIE 0x31535024 +#define POLICY_COOKIE_OFFSET 0x10 + +/* List of supported CPU ids */ +#define AMD_CPU_ID_RMB 0x14b5 +#define AMD_CPU_ID_PS 0x14e8 +#define PCI_DEVICE_ID_AMD_1AH_M20H_ROOT 0x1507 +#define PCI_DEVICE_ID_AMD_1AH_M60H_ROOT 0x1122 + +struct cookie_header { + u32 sign; + u32 length; +} __packed; + +/* APMF Functions */ +#define APMF_FUNC_VERIFY_INTERFACE 0 +#define APMF_FUNC_GET_SYS_PARAMS 1 +#define APMF_FUNC_SBIOS_REQUESTS 2 +#define APMF_FUNC_SBIOS_HEARTBEAT 4 +#define APMF_FUNC_AUTO_MODE 5 +#define APMF_FUNC_SET_FAN_IDX 7 +#define APMF_FUNC_OS_POWER_SLIDER_UPDATE 8 +#define APMF_FUNC_STATIC_SLIDER_GRANULAR 9 +#define APMF_FUNC_DYN_SLIDER_AC 11 +#define APMF_FUNC_DYN_SLIDER_DC 12 +#define APMF_FUNC_NOTIFY_SMART_PC_UPDATES 14 +#define APMF_FUNC_SBIOS_HEARTBEAT_V2 16 + +/* Message Definitions */ +#define SET_SPL 0x03 /* SPL: Sustained Power Limit */ +#define SET_SPPT 0x05 /* SPPT: Slow Package Power Tracking */ +#define SET_FPPT 0x07 /* FPPT: Fast Package Power Tracking */ +#define GET_SPL 0x0B +#define GET_SPPT 0x0D +#define GET_FPPT 0x0F +#define SET_DRAM_ADDR_HIGH 0x14 +#define SET_DRAM_ADDR_LOW 0x15 +#define SET_TRANSFER_TABLE 0x16 +#define SET_STT_MIN_LIMIT 0x18 /* STT: Skin Temperature Tracking */ +#define SET_STT_LIMIT_APU 0x19 +#define SET_STT_LIMIT_HS2 0x1A +#define SET_SPPT_APU_ONLY 0x1D +#define GET_SPPT_APU_ONLY 0x1E +#define GET_STT_MIN_LIMIT 0x1F +#define GET_STT_LIMIT_APU 0x20 +#define GET_STT_LIMIT_HS2 0x21 +#define SET_P3T 0x23 /* P3T: Peak Package Power Limit */ +#define SET_PMF_PPT 0x25 +#define SET_PMF_PPT_APU_ONLY 0x26 + +/* OS slider update notification */ +#define DC_BEST_PERF 0 +#define DC_BETTER_PERF 1 +#define DC_BATTERY_SAVER 3 +#define AC_BEST_PERF 4 +#define AC_BETTER_PERF 5 +#define AC_BETTER_BATTERY 6 + +/* Fan Index for Auto Mode */ +#define FAN_INDEX_AUTO 0xFFFFFFFF + +#define ARG_NONE 0 +#define AVG_SAMPLE_SIZE 3 + +/* Policy Actions */ +#define PMF_POLICY_SPL 2 +#define PMF_POLICY_SPPT 3 +#define PMF_POLICY_FPPT 4 +#define PMF_POLICY_SPPT_APU_ONLY 5 +#define PMF_POLICY_STT_MIN 6 +#define PMF_POLICY_STT_SKINTEMP_APU 7 +#define PMF_POLICY_STT_SKINTEMP_HS2 8 +#define PMF_POLICY_SYSTEM_STATE 9 +#define PMF_POLICY_BIOS_OUTPUT_1 10 +#define PMF_POLICY_BIOS_OUTPUT_2 11 +#define PMF_POLICY_P3T 38 +#define PMF_POLICY_PMF_PPT 54 +#define PMF_POLICY_PMF_PPT_APU_ONLY 55 +#define PMF_POLICY_BIOS_OUTPUT_3 57 +#define PMF_POLICY_BIOS_OUTPUT_4 58 +#define PMF_POLICY_BIOS_OUTPUT_5 59 +#define PMF_POLICY_BIOS_OUTPUT_6 60 +#define PMF_POLICY_BIOS_OUTPUT_7 61 +#define PMF_POLICY_BIOS_OUTPUT_8 62 +#define PMF_POLICY_BIOS_OUTPUT_9 63 +#define PMF_POLICY_BIOS_OUTPUT_10 64 + +/* TA macros */ +#define PMF_TA_IF_VERSION_MAJOR 1 +#define TA_PMF_ACTION_MAX 32 +#define TA_PMF_UNDO_MAX 8 +#define TA_OUTPUT_RESERVED_MEM 922 +#define MAX_OPERATION_PARAMS 4 + +#define TA_ERROR_CRYPTO_INVALID_PARAM 0x20002 +#define TA_ERROR_CRYPTO_BIN_TOO_LARGE 0x2000d + +#define PMF_IF_V1 1 +#define PMF_IF_V2 2 + +#define APTS_MAX_STATES 16 +#define CUSTOM_BIOS_INPUT_BITS GENMASK(16, 7) +#define BIOS_INPUTS_MAX 10 +#define CUSTOM_BIOS_INPUT_RING_ENTRIES 64 /* Must be power of two for CIRC_* macros */ + +/* amd_pmf_send_cmd() set/get */ +#define SET_CMD false +#define GET_CMD true + +#define METRICS_TABLE_ID 7 + +typedef void (*apmf_event_handler_t)(acpi_handle handle, u32 event, void *data); + +static const uuid_t amd_pmf_ta_uuid[] __used = { UUID_INIT(0xd9b39bf2, 0x66bd, 0x4154, 0xaf, 0xb8, + 0x8a, 0xcc, 0x2b, 0x2b, 0x60, 0xd6), + UUID_INIT(0x6fd93b77, 0x3fb8, 0x524d, 0xb1, 0x2d, + 0xc5, 0x29, 0xb1, 0x3d, 0x85, 0x43), + }; + +/* APTS PMF BIOS Interface */ +struct amd_pmf_apts_output { + u16 table_version; + u32 fan_table_idx; + u32 pmf_ppt; + u32 ppt_pmf_apu_only; + u32 stt_min_limit; + u8 stt_skin_temp_limit_apu; + u8 stt_skin_temp_limit_hs2; +} __packed; + +struct amd_pmf_apts_granular_output { + u16 size; + struct amd_pmf_apts_output val; +} __packed; + +struct amd_pmf_apts_granular { + u16 size; + struct amd_pmf_apts_output val[APTS_MAX_STATES]; +}; + +struct sbios_hb_event_v2 { + u16 size; + u8 load; + u8 unload; + u8 suspend; + u8 resume; +} __packed; + +enum sbios_hb_v2 { + ON_LOAD, + ON_UNLOAD, + ON_SUSPEND, + ON_RESUME, +}; + +/* AMD PMF BIOS interfaces */ +struct apmf_verify_interface { + u16 size; + u16 version; + u32 notification_mask; + u32 supported_functions; +} __packed; + +struct apmf_system_params { + u16 size; + u32 valid_mask; + u32 flags; + u8 command_code; + u32 heartbeat_int; +} __packed; + +struct apmf_sbios_req { + u16 size; + u32 pending_req; + u8 rsd; + u8 cql_event; + u8 amt_event; + u32 fppt; + u32 sppt; + u32 fppt_apu_only; + u32 spl; + u32 stt_min_limit; + u8 skin_temp_apu; + u8 skin_temp_hs2; +} __packed; + +/* As per APMF spec 1.3 */ +struct apmf_sbios_req_v1 { + u16 size; + u32 pending_req; + u8 rsvd; + u8 cql_event; + u8 amt_event; + u32 fppt; + u32 sppt; + u32 sppt_apu_only; + u32 spl; + u32 stt_min_limit; + u8 skin_temp_apu; + u8 skin_temp_hs2; + u8 enable_cnqf; + u32 custom_policy[BIOS_INPUTS_MAX]; +} __packed; + +struct apmf_sbios_req_v2 { + u16 size; + u32 pending_req; + u8 rsd; + u32 ppt_pmf; + u32 ppt_pmf_apu_only; + u32 stt_min_limit; + u8 skin_temp_apu; + u8 skin_temp_hs2; + u32 custom_policy[BIOS_INPUTS_MAX]; +} __packed; + +struct apmf_fan_idx { + u16 size; + u8 fan_ctl_mode; + u32 fan_ctl_idx; +} __packed; + +struct smu_pmf_metrics_v2 { + u16 core_frequency[16]; /* MHz */ + u16 core_power[16]; /* mW */ + u16 core_temp[16]; /* centi-C */ + u16 gfx_temp; /* centi-C */ + u16 soc_temp; /* centi-C */ + u16 stapm_opn_limit; /* mW */ + u16 stapm_cur_limit; /* mW */ + u16 infra_cpu_maxfreq; /* MHz */ + u16 infra_gfx_maxfreq; /* MHz */ + u16 skin_temp; /* centi-C */ + u16 gfxclk_freq; /* MHz */ + u16 fclk_freq; /* MHz */ + u16 gfx_activity; /* GFX busy % [0-100] */ + u16 socclk_freq; /* MHz */ + u16 vclk_freq; /* MHz */ + u16 vcn_activity; /* VCN busy % [0-100] */ + u16 vpeclk_freq; /* MHz */ + u16 npuclk_freq; /* MHz */ + u16 npu_busy[8]; /* NPU busy % [0-100] */ + u16 dram_reads; /* MB/sec */ + u16 dram_writes; /* MB/sec */ + u16 core_c0residency[16]; /* C0 residency % [0-100] */ + u16 npu_power; /* mW */ + u32 apu_power; /* mW */ + u32 gfx_power; /* mW */ + u32 dgpu_power; /* mW */ + u32 socket_power; /* mW */ + u32 all_core_power; /* mW */ + u32 filter_alpha_value; /* time constant [us] */ + u32 metrics_counter; + u16 memclk_freq; /* MHz */ + u16 mpnpuclk_freq; /* MHz */ + u16 npu_reads; /* MB/sec */ + u16 npu_writes; /* MB/sec */ + u32 throttle_residency_prochot; + u32 throttle_residency_spl; + u32 throttle_residency_fppt; + u32 throttle_residency_sppt; + u32 throttle_residency_thm_core; + u32 throttle_residency_thm_gfx; + u32 throttle_residency_thm_soc; + u16 psys; + u16 spare1; + u32 spare[6]; +} __packed; + +struct smu_pmf_metrics { + u16 gfxclk_freq; /* in MHz */ + u16 socclk_freq; /* in MHz */ + u16 vclk_freq; /* in MHz */ + u16 dclk_freq; /* in MHz */ + u16 memclk_freq; /* in MHz */ + u16 spare; + u16 gfx_activity; /* in Centi */ + u16 uvd_activity; /* in Centi */ + u16 voltage[2]; /* in mV */ + u16 currents[2]; /* in mA */ + u16 power[2];/* in mW */ + u16 core_freq[8]; /* in MHz */ + u16 core_power[8]; /* in mW */ + u16 core_temp[8]; /* in centi-Celsius */ + u16 l3_freq; /* in MHz */ + u16 l3_temp; /* in centi-Celsius */ + u16 gfx_temp; /* in centi-Celsius */ + u16 soc_temp; /* in centi-Celsius */ + u16 throttler_status; + u16 current_socketpower; /* in mW */ + u16 stapm_orig_limit; /* in W */ + u16 stapm_cur_limit; /* in W */ + u32 apu_power; /* in mW */ + u32 dgpu_power; /* in mW */ + u16 vdd_tdc_val; /* in mA */ + u16 soc_tdc_val; /* in mA */ + u16 vdd_edc_val; /* in mA */ + u16 soc_edcv_al; /* in mA */ + u16 infra_cpu_maxfreq; /* in MHz */ + u16 infra_gfx_maxfreq; /* in MHz */ + u16 skin_temp; /* in centi-Celsius */ + u16 device_state; + u16 curtemp; /* in centi-Celsius */ + u16 filter_alpha_value; + u16 avg_gfx_clkfrequency; + u16 avg_fclk_frequency; + u16 avg_gfx_activity; + u16 avg_socclk_frequency; + u16 avg_vclk_frequency; + u16 avg_vcn_activity; + u16 avg_dram_reads; + u16 avg_dram_writes; + u16 avg_socket_power; + u16 avg_core_power[2]; + u16 avg_core_c0residency[16]; + u16 spare1; + u32 metrics_counter; +} __packed; + +enum amd_stt_skin_temp { + STT_TEMP_APU, + STT_TEMP_HS2, + STT_TEMP_COUNT, +}; + +enum amd_slider_op { + SLIDER_OP_GET, + SLIDER_OP_SET, +}; + +enum power_source { + POWER_SOURCE_AC, + POWER_SOURCE_DC, + POWER_SOURCE_MAX, +}; + +enum power_modes { + POWER_MODE_PERFORMANCE, + POWER_MODE_BALANCED_POWER, + POWER_MODE_POWER_SAVER, + POWER_MODE_MAX, +}; + +enum power_modes_v2 { + POWER_MODE_BEST_PERFORMANCE, + POWER_MODE_BALANCED, + POWER_MODE_BEST_POWER_EFFICIENCY, + POWER_MODE_ENERGY_SAVE, + POWER_MODE_V2_MAX, +}; + +struct pmf_bios_inputs_prev { + u32 custom_bios_inputs[BIOS_INPUTS_MAX]; +}; + +/** + * struct pmf_bios_input_entry - Snapshot of custom BIOS input event + * @val: Array of custom BIOS input values + * @preq: Pending request value associated with this event + */ +struct pmf_bios_input_entry { + u32 val[BIOS_INPUTS_MAX]; + u32 preq; +}; + +struct pmf_cbi_ring_buffer { + struct pmf_bios_input_entry data[CUSTOM_BIOS_INPUT_RING_ENTRIES]; + int head; + int tail; +}; + +struct amd_pmf_dev { + void __iomem *regbase; + void __iomem *smu_virt_addr; + void *buf; + u32 base_addr; + u32 cpu_id; + struct device *dev; + struct mutex lock; /* protects the PMF interface */ + u32 supported_func; + enum platform_profile_option current_profile; + struct device *ppdev; /* platform profile class device */ + struct dentry *dbgfs_dir; + int hb_interval; /* SBIOS heartbeat interval */ + struct delayed_work heart_beat; + struct smu_pmf_metrics m_table; + struct smu_pmf_metrics_v2 m_table_v2; + struct delayed_work work_buffer; + ktime_t start_time; + int socket_power_history[AVG_SAMPLE_SIZE]; + int socket_power_history_idx; + bool amt_enabled; + struct mutex update_mutex; /* protects race between ACPI handler and metrics thread */ + bool cnqf_enabled; + bool cnqf_supported; + struct notifier_block pwr_src_notifier; + /* Smart PC solution builder */ + struct dentry *esbin; + unsigned char *policy_buf; + resource_size_t policy_sz; + struct tee_context *tee_ctx; + struct tee_shm *fw_shm_pool; + u32 session_id; + void *shbuf; + struct delayed_work pb_work; + struct pmf_action_table *prev_data; + resource_size_t policy_addr; + void __iomem *policy_base; + bool smart_pc_enabled; + u16 pmf_if_version; + struct input_dev *pmf_idev; + size_t mtable_size; + struct resource *res; + struct apmf_sbios_req_v2 req; /* To get custom bios pending request */ + struct mutex cb_mutex; + u32 notifications; + struct apmf_sbios_req_v1 req1; + struct pmf_bios_inputs_prev cb_prev; /* To preserve custom BIOS inputs */ + bool cb_flag; /* To handle first custom BIOS input */ + struct pmf_cbi_ring_buffer cbi_buf; + struct mutex cbi_mutex; /* Protects ring buffer access */ + struct mutex metrics_mutex; +}; + +struct apmf_sps_prop_granular_v2 { + u8 power_states[POWER_SOURCE_MAX][POWER_MODE_V2_MAX]; +} __packed; + +struct apmf_sps_prop_granular { + u32 fppt; + u32 sppt; + u32 sppt_apu_only; + u32 spl; + u32 stt_min; + u8 stt_skin_temp[STT_TEMP_COUNT]; + u32 fan_id; +} __packed; + +/* Static Slider */ +struct apmf_static_slider_granular_output { + u16 size; + struct apmf_sps_prop_granular prop[POWER_SOURCE_MAX * POWER_MODE_MAX]; +} __packed; + +struct amd_pmf_static_slider_granular { + u16 size; + struct apmf_sps_prop_granular prop[POWER_SOURCE_MAX][POWER_MODE_MAX]; +}; + +struct apmf_static_slider_granular_output_v2 { + u16 size; + struct apmf_sps_prop_granular_v2 sps_idx; +} __packed; + +struct amd_pmf_static_slider_granular_v2 { + u16 size; + struct apmf_sps_prop_granular_v2 sps_idx; +}; + +struct os_power_slider { + u16 size; + u8 slider_event; +} __packed; + +struct amd_pmf_notify_smart_pc_update { + u16 size; + u32 pending_req; + u32 custom_bios[BIOS_INPUTS_MAX]; +} __packed; + +struct fan_table_control { + bool manual; + unsigned long fan_id; +}; + +struct power_table_control { + u32 spl; + u32 sppt; + u32 fppt; + u32 sppt_apu_only; + u32 stt_min; + u32 stt_skin_temp[STT_TEMP_COUNT]; + u32 reserved[16]; +}; + +/* Auto Mode Layer */ +enum auto_mode_transition_priority { + AUTO_TRANSITION_TO_PERFORMANCE, /* Any other mode to Performance Mode */ + AUTO_TRANSITION_FROM_QUIET_TO_BALANCE, /* Quiet Mode to Balance Mode */ + AUTO_TRANSITION_TO_QUIET, /* Any other mode to Quiet Mode */ + AUTO_TRANSITION_FROM_PERFORMANCE_TO_BALANCE, /* Performance Mode to Balance Mode */ + AUTO_TRANSITION_MAX, +}; + +enum auto_mode_mode { + AUTO_QUIET, + AUTO_BALANCE, + AUTO_PERFORMANCE_ON_LAP, + AUTO_PERFORMANCE, + AUTO_MODE_MAX, +}; + +struct auto_mode_trans_params { + u32 time_constant; /* minimum time required to switch to next mode */ + u32 power_delta; /* delta power to shift mode */ + u32 power_threshold; + u32 timer; /* elapsed time. if timer > TimeThreshold, it will move to next mode */ + u32 applied; + enum auto_mode_mode target_mode; + u32 shifting_up; +}; + +struct auto_mode_mode_settings { + struct power_table_control power_control; + struct fan_table_control fan_control; + u32 power_floor; +}; + +struct auto_mode_mode_config { + struct auto_mode_trans_params transition[AUTO_TRANSITION_MAX]; + struct auto_mode_mode_settings mode_set[AUTO_MODE_MAX]; + enum auto_mode_mode current_mode; +}; + +struct apmf_auto_mode { + u16 size; + /* time constant */ + u32 balanced_to_perf; + u32 perf_to_balanced; + u32 quiet_to_balanced; + u32 balanced_to_quiet; + /* power floor */ + u32 pfloor_perf; + u32 pfloor_balanced; + u32 pfloor_quiet; + /* Power delta for mode change */ + u32 pd_balanced_to_perf; + u32 pd_perf_to_balanced; + u32 pd_quiet_to_balanced; + u32 pd_balanced_to_quiet; + /* skin temperature limits */ + u8 stt_apu_perf_on_lap; /* CQL ON */ + u8 stt_hs2_perf_on_lap; /* CQL ON */ + u8 stt_apu_perf; + u8 stt_hs2_perf; + u8 stt_apu_balanced; + u8 stt_hs2_balanced; + u8 stt_apu_quiet; + u8 stt_hs2_quiet; + u32 stt_min_limit_perf_on_lap; /* CQL ON */ + u32 stt_min_limit_perf; + u32 stt_min_limit_balanced; + u32 stt_min_limit_quiet; + /* SPL based */ + u32 fppt_perf_on_lap; /* CQL ON */ + u32 sppt_perf_on_lap; /* CQL ON */ + u32 spl_perf_on_lap; /* CQL ON */ + u32 sppt_apu_only_perf_on_lap; /* CQL ON */ + u32 fppt_perf; + u32 sppt_perf; + u32 spl_perf; + u32 sppt_apu_only_perf; + u32 fppt_balanced; + u32 sppt_balanced; + u32 spl_balanced; + u32 sppt_apu_only_balanced; + u32 fppt_quiet; + u32 sppt_quiet; + u32 spl_quiet; + u32 sppt_apu_only_quiet; + /* Fan ID */ + u32 fan_id_perf; + u32 fan_id_balanced; + u32 fan_id_quiet; +} __packed; + +/* CnQF Layer */ +enum cnqf_trans_priority { + CNQF_TRANSITION_TO_TURBO, /* Any other mode to Turbo Mode */ + CNQF_TRANSITION_FROM_BALANCE_TO_PERFORMANCE, /* quiet/balance to Performance Mode */ + CNQF_TRANSITION_FROM_QUIET_TO_BALANCE, /* Quiet Mode to Balance Mode */ + CNQF_TRANSITION_TO_QUIET, /* Any other mode to Quiet Mode */ + CNQF_TRANSITION_FROM_PERFORMANCE_TO_BALANCE, /* Performance/Turbo to Balance Mode */ + CNQF_TRANSITION_FROM_TURBO_TO_PERFORMANCE, /* Turbo mode to Performance Mode */ + CNQF_TRANSITION_MAX, +}; + +enum cnqf_mode { + CNQF_MODE_QUIET, + CNQF_MODE_BALANCE, + CNQF_MODE_PERFORMANCE, + CNQF_MODE_TURBO, + CNQF_MODE_MAX, +}; + +enum apmf_cnqf_pos { + APMF_CNQF_TURBO, + APMF_CNQF_PERFORMANCE, + APMF_CNQF_BALANCE, + APMF_CNQF_QUIET, + APMF_CNQF_MAX, +}; + +struct cnqf_mode_settings { + struct power_table_control power_control; + struct fan_table_control fan_control; + u32 power_floor; +}; + +struct cnqf_tran_params { + u32 time_constant; /* minimum time required to switch to next mode */ + u32 power_threshold; + u32 timer; /* elapsed time. if timer > timethreshold, it will move to next mode */ + u32 total_power; + u32 count; + bool priority; + bool shifting_up; + enum cnqf_mode target_mode; +}; + +struct cnqf_config { + struct cnqf_tran_params trans_param[POWER_SOURCE_MAX][CNQF_TRANSITION_MAX]; + struct cnqf_mode_settings mode_set[POWER_SOURCE_MAX][CNQF_MODE_MAX]; + struct power_table_control defaults; + enum cnqf_mode current_mode; + u32 power_src; + u32 avg_power; +}; + +struct apmf_cnqf_power_set { + u32 pfloor; + u32 fppt; + u32 sppt; + u32 sppt_apu_only; + u32 spl; + u32 stt_min_limit; + u8 stt_skintemp[STT_TEMP_COUNT]; + u32 fan_id; +} __packed; + +struct apmf_dyn_slider_output { + u16 size; + u16 flags; + u32 t_perf_to_turbo; + u32 t_balanced_to_perf; + u32 t_quiet_to_balanced; + u32 t_balanced_to_quiet; + u32 t_perf_to_balanced; + u32 t_turbo_to_perf; + struct apmf_cnqf_power_set ps[APMF_CNQF_MAX]; +} __packed; + +/* Smart PC - TA internals */ +enum system_state { + SYSTEM_STATE_S0i3, + SYSTEM_STATE_S4, + SYSTEM_STATE_SCREEN_LOCK, + SYSTEM_STATE_MAX, +}; + +enum ta_slider { + TA_BEST_BATTERY, + TA_BETTER_BATTERY, + TA_BETTER_PERFORMANCE, + TA_BEST_PERFORMANCE, + TA_MAX, +}; + +struct amd_pmf_pb_bitmap { + const char *name; + u32 bit_mask; +}; + +static const struct amd_pmf_pb_bitmap custom_bios_inputs[] __used = { + {"NOTIFY_CUSTOM_BIOS_INPUT1", BIT(5)}, + {"NOTIFY_CUSTOM_BIOS_INPUT2", BIT(6)}, + {"NOTIFY_CUSTOM_BIOS_INPUT3", BIT(7)}, + {"NOTIFY_CUSTOM_BIOS_INPUT4", BIT(8)}, + {"NOTIFY_CUSTOM_BIOS_INPUT5", BIT(9)}, + {"NOTIFY_CUSTOM_BIOS_INPUT6", BIT(10)}, + {"NOTIFY_CUSTOM_BIOS_INPUT7", BIT(11)}, + {"NOTIFY_CUSTOM_BIOS_INPUT8", BIT(12)}, + {"NOTIFY_CUSTOM_BIOS_INPUT9", BIT(13)}, + {"NOTIFY_CUSTOM_BIOS_INPUT10", BIT(14)}, +}; + +static const struct amd_pmf_pb_bitmap custom_bios_inputs_v1[] __used = { + {"NOTIFY_CUSTOM_BIOS_INPUT1", BIT(7)}, + {"NOTIFY_CUSTOM_BIOS_INPUT2", BIT(8)}, + {"NOTIFY_CUSTOM_BIOS_INPUT3", BIT(9)}, + {"NOTIFY_CUSTOM_BIOS_INPUT4", BIT(10)}, + {"NOTIFY_CUSTOM_BIOS_INPUT5", BIT(11)}, + {"NOTIFY_CUSTOM_BIOS_INPUT6", BIT(12)}, + {"NOTIFY_CUSTOM_BIOS_INPUT7", BIT(13)}, + {"NOTIFY_CUSTOM_BIOS_INPUT8", BIT(14)}, + {"NOTIFY_CUSTOM_BIOS_INPUT9", BIT(15)}, + {"NOTIFY_CUSTOM_BIOS_INPUT10", BIT(16)}, +}; + +enum platform_type { + PTYPE_UNKNOWN = 0, + LID_CLOSE, + CLAMSHELL, + FLAT, + TENT, + STAND, + TABLET, + BOOK, + PRESENTATION, + PULL_FWD, + PTYPE_INVALID = 0xf, +}; + +/* Command ids for TA communication */ +enum ta_pmf_command { + TA_PMF_COMMAND_POLICY_BUILDER_INITIALIZE, + TA_PMF_COMMAND_POLICY_BUILDER_ENACT_POLICIES, +}; + +enum ta_pmf_error_type { + TA_PMF_TYPE_SUCCESS, + TA_PMF_ERROR_TYPE_GENERIC, + TA_PMF_ERROR_TYPE_CRYPTO, + TA_PMF_ERROR_TYPE_CRYPTO_VALIDATE, + TA_PMF_ERROR_TYPE_CRYPTO_VERIFY_OEM, + TA_PMF_ERROR_TYPE_POLICY_BUILDER, + TA_PMF_ERROR_TYPE_PB_CONVERT, + TA_PMF_ERROR_TYPE_PB_SETUP, + TA_PMF_ERROR_TYPE_PB_ENACT, + TA_PMF_ERROR_TYPE_ASD_GET_DEVICE_INFO, + TA_PMF_ERROR_TYPE_ASD_GET_DEVICE_PCIE_INFO, + TA_PMF_ERROR_TYPE_SYS_DRV_FW_VALIDATION, + TA_PMF_ERROR_TYPE_MAX, +}; + +struct pmf_action_table { + enum system_state system_state; + u32 spl; /* in mW */ + u32 sppt; /* in mW */ + u32 sppt_apuonly; /* in mW */ + u32 fppt; /* in mW */ + u32 stt_minlimit; /* in mW */ + u32 stt_skintemp_apu; /* in C */ + u32 stt_skintemp_hs2; /* in C */ + u32 p3t_limit; /* in mW */ + u32 pmf_ppt; /* in mW */ + u32 pmf_ppt_apu_only; /* in mW */ +}; + +/* Input conditions */ +struct ta_pmf_condition_info { + u32 power_source; + u32 bat_percentage; + u32 power_slider; + u32 lid_state; + bool user_present; + u32 bios_input_1[2]; + u32 monitor_count; + u32 rsvd2[2]; + u32 bat_design; + u32 full_charge_capacity; + int drain_rate; + bool user_engaged; + u32 device_state; + u32 socket_power; + u32 skin_temperature; + u32 rsvd3[2]; + u32 platform_type; + u32 rsvd3_1[2]; + u32 ambient_light; + u32 length; + u32 avg_c0residency; + u32 max_c0residency; + u32 s0i3_entry; + u32 gfx_busy; + u32 rsvd4[7]; + bool camera_state; + u32 workload_type; + u32 display_type; + u32 display_state; + u32 rsvd5_1[17]; + u32 bios_input_2[8]; + u32 rsvd5[125]; +}; + +struct ta_pmf_load_policy_table { + u32 table_size; + u8 table[POLICY_BUF_MAX_SZ]; +}; + +/* TA initialization params */ +struct ta_pmf_init_table { + u32 frequency; /* SMU sampling frequency */ + bool validate; + bool sku_check; + bool metadata_macrocheck; + struct ta_pmf_load_policy_table policies_table; +}; + +/* Everything the TA needs to Enact Policies */ +struct ta_pmf_enact_table { + struct ta_pmf_condition_info ev_info; + u32 name; +}; + +struct ta_pmf_action { + u32 action_index; + u32 value; + u32 spl_arg; +}; + +/* Output actions from TA */ +struct ta_pmf_enact_result { + u32 actions_count; + struct ta_pmf_action actions_list[TA_PMF_ACTION_MAX]; + u32 undo_count; + struct ta_pmf_action undo_list[TA_PMF_UNDO_MAX]; +}; + +union ta_pmf_input { + struct ta_pmf_enact_table enact_table; + struct ta_pmf_init_table init_table; +}; + +union ta_pmf_output { + struct ta_pmf_enact_result policy_apply_table; + u32 rsvd[TA_OUTPUT_RESERVED_MEM]; +}; + +struct ta_pmf_shared_memory { + int command_id; + int resp_id; + u32 pmf_result; + u32 if_version; + union ta_pmf_output pmf_output; + union ta_pmf_input pmf_input; +}; + +/* Core Layer */ +int apmf_acpi_init(struct amd_pmf_dev *pmf_dev); +void apmf_acpi_deinit(struct amd_pmf_dev *pmf_dev); +int is_apmf_func_supported(struct amd_pmf_dev *pdev, unsigned long index); +int amd_pmf_send_cmd(struct amd_pmf_dev *dev, u8 message, bool get, u32 arg, u32 *data); +int amd_pmf_init_metrics_table(struct amd_pmf_dev *dev); +int amd_pmf_get_power_source(void); +int apmf_install_handler(struct amd_pmf_dev *pmf_dev); +int apmf_os_power_slider_update(struct amd_pmf_dev *dev, u8 flag); +int amd_pmf_set_dram_addr(struct amd_pmf_dev *dev, bool alloc_buffer); +int amd_pmf_notify_sbios_heartbeat_event_v2(struct amd_pmf_dev *dev, u8 flag); +u32 fixp_q88_fromint(u32 val); +int is_apmf_bios_input_notifications_supported(struct amd_pmf_dev *pdev); + +/* SPS Layer */ +int amd_pmf_get_pprof_modes(struct amd_pmf_dev *pmf); +void amd_pmf_update_slider(struct amd_pmf_dev *dev, bool op, int idx, + struct amd_pmf_static_slider_granular *table); +int amd_pmf_init_sps(struct amd_pmf_dev *dev); +int apmf_get_static_slider_granular(struct amd_pmf_dev *pdev, + struct apmf_static_slider_granular_output *output); +bool is_pprof_balanced(struct amd_pmf_dev *pmf); +int amd_pmf_power_slider_update_event(struct amd_pmf_dev *dev); +const char *amd_pmf_source_as_str(unsigned int state); + +const char *amd_pmf_source_as_str(unsigned int state); + +int apmf_update_fan_idx(struct amd_pmf_dev *pdev, bool manual, u32 idx); +int amd_pmf_set_sps_power_limits(struct amd_pmf_dev *pmf); +int apmf_get_static_slider_granular_v2(struct amd_pmf_dev *dev, + struct apmf_static_slider_granular_output_v2 *data); +int apts_get_static_slider_granular_v2(struct amd_pmf_dev *pdev, + struct amd_pmf_apts_granular_output *data, u32 apts_idx); + +/* Auto Mode Layer */ +int apmf_get_auto_mode_def(struct amd_pmf_dev *pdev, struct apmf_auto_mode *data); +void amd_pmf_init_auto_mode(struct amd_pmf_dev *dev); +void amd_pmf_deinit_auto_mode(struct amd_pmf_dev *dev); +void amd_pmf_trans_automode(struct amd_pmf_dev *dev, int socket_power, ktime_t time_elapsed_ms); +int apmf_get_sbios_requests(struct amd_pmf_dev *pdev, struct apmf_sbios_req *req); +int apmf_get_sbios_requests_v1(struct amd_pmf_dev *pdev, struct apmf_sbios_req_v1 *req); +int apmf_get_sbios_requests_v2(struct amd_pmf_dev *pdev, struct apmf_sbios_req_v2 *req); + +void amd_pmf_update_2_cql(struct amd_pmf_dev *dev, bool is_cql_event); +int amd_pmf_reset_amt(struct amd_pmf_dev *dev); +void amd_pmf_handle_amt(struct amd_pmf_dev *dev); + +/* CnQF Layer */ +int apmf_get_dyn_slider_def_ac(struct amd_pmf_dev *pdev, struct apmf_dyn_slider_output *data); +int apmf_get_dyn_slider_def_dc(struct amd_pmf_dev *pdev, struct apmf_dyn_slider_output *data); +int amd_pmf_init_cnqf(struct amd_pmf_dev *dev); +void amd_pmf_deinit_cnqf(struct amd_pmf_dev *dev); +int amd_pmf_trans_cnqf(struct amd_pmf_dev *dev, int socket_power, ktime_t time_lapsed_ms); +extern const struct attribute_group cnqf_feature_attribute_group; + +/* Smart PC builder Layer */ +int amd_pmf_init_smart_pc(struct amd_pmf_dev *dev); +void amd_pmf_deinit_smart_pc(struct amd_pmf_dev *dev); +int apmf_check_smart_pc(struct amd_pmf_dev *pmf_dev); +int amd_pmf_smartpc_apply_bios_output(struct amd_pmf_dev *dev, u32 val, u32 preq, u32 idx); + +/* Smart PC - TA interfaces */ +void amd_pmf_populate_ta_inputs(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in); +void amd_pmf_dump_ta_inputs(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in); +int amd_pmf_invoke_cmd_enact(struct amd_pmf_dev *dev); + +int amd_pmf_tee_init(struct amd_pmf_dev *dev, const uuid_t *uuid); +void amd_pmf_tee_deinit(struct amd_pmf_dev *dev); +int amd_pmf_start_policy_engine(struct amd_pmf_dev *dev); + +#endif /* PMF_H */ diff --git a/drivers/platform/x86/amd/pmf/spc.c b/drivers/platform/x86/amd/pmf/spc.c new file mode 100644 index 000000000..f48678a23 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/spc.c @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework Driver - Smart PC Capabilities + * + * Copyright (c) 2023, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Authors: Shyam Sundar S K + * Patil Rajesh Reddy + */ + +#include +#include +#include +#include +#include +#include "pmf.h" + +#ifdef CONFIG_AMD_PMF_DEBUG +static const char *platform_type_as_str(u16 platform_type) +{ + switch (platform_type) { + case CLAMSHELL: + return "CLAMSHELL"; + case FLAT: + return "FLAT"; + case TENT: + return "TENT"; + case STAND: + return "STAND"; + case TABLET: + return "TABLET"; + case BOOK: + return "BOOK"; + case PRESENTATION: + return "PRESENTATION"; + case PULL_FWD: + return "PULL_FWD"; + default: + return "UNKNOWN"; + } +} + +static const char *laptop_placement_as_str(u16 device_state) +{ + switch (device_state) { + case ON_TABLE: + return "ON_TABLE"; + case ON_LAP_MOTION: + return "ON_LAP_MOTION"; + case IN_BAG: + return "IN_BAG"; + case OUT_OF_BAG: + return "OUT_OF_BAG"; + default: + return "UNKNOWN"; + } +} + +static const char *ta_slider_as_str(unsigned int state) +{ + switch (state) { + case TA_BEST_PERFORMANCE: + return "PERFORMANCE"; + case TA_BETTER_PERFORMANCE: + return "BALANCED"; + case TA_BEST_BATTERY: + return "POWER_SAVER"; + default: + return "Unknown TA Slider State"; + } +} + +static u32 amd_pmf_get_ta_custom_bios_inputs(struct ta_pmf_enact_table *in, int index) +{ + switch (index) { + case 0 ... 1: + return in->ev_info.bios_input_1[index]; + case 2 ... 9: + return in->ev_info.bios_input_2[index - 2]; + default: + return 0; + } +} + +void amd_pmf_dump_ta_inputs(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + int i; + + dev_dbg(dev->dev, "==== TA inputs START ====\n"); + dev_dbg(dev->dev, "Slider State: %s\n", ta_slider_as_str(in->ev_info.power_slider)); + dev_dbg(dev->dev, "Power Source: %s\n", amd_pmf_source_as_str(in->ev_info.power_source)); + dev_dbg(dev->dev, "Battery Percentage: %u\n", in->ev_info.bat_percentage); + dev_dbg(dev->dev, "Designed Battery Capacity: %u\n", in->ev_info.bat_design); + dev_dbg(dev->dev, "Fully Charged Capacity: %u\n", in->ev_info.full_charge_capacity); + dev_dbg(dev->dev, "Drain Rate: %d\n", in->ev_info.drain_rate); + dev_dbg(dev->dev, "Socket Power: %u\n", in->ev_info.socket_power); + dev_dbg(dev->dev, "Skin Temperature: %u\n", in->ev_info.skin_temperature); + dev_dbg(dev->dev, "Avg C0 Residency: %u\n", in->ev_info.avg_c0residency); + dev_dbg(dev->dev, "Max C0 Residency: %u\n", in->ev_info.max_c0residency); + dev_dbg(dev->dev, "GFX Busy: %u\n", in->ev_info.gfx_busy); + dev_dbg(dev->dev, "LID State: %s\n", in->ev_info.lid_state ? "close" : "open"); + dev_dbg(dev->dev, "User Presence: %s\n", in->ev_info.user_present ? "Present" : "Away"); + dev_dbg(dev->dev, "Ambient Light: %d\n", in->ev_info.ambient_light); + dev_dbg(dev->dev, "Platform type: %s\n", platform_type_as_str(in->ev_info.platform_type)); + dev_dbg(dev->dev, "Laptop placement: %s\n", + laptop_placement_as_str(in->ev_info.device_state)); + for (i = 0; i < ARRAY_SIZE(custom_bios_inputs); i++) + dev_dbg(dev->dev, "Custom BIOS input%d: %u\n", i + 1, + amd_pmf_get_ta_custom_bios_inputs(in, i)); + dev_dbg(dev->dev, "==== TA inputs END ====\n"); +} +#else +void amd_pmf_dump_ta_inputs(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) {} +#endif + +/* + * This helper function sets the appropriate BIOS input value in the TA enact + * table based on the provided index. We need this approach because the custom + * BIOS input array is not continuous, due to the existing TA structure layout. + */ +static void amd_pmf_set_ta_custom_bios_input(struct ta_pmf_enact_table *in, int index, u32 value) +{ + switch (index) { + case 0 ... 1: + in->ev_info.bios_input_1[index] = value; + break; + case 2 ... 9: + in->ev_info.bios_input_2[index - 2] = value; + break; + default: + return; + } +} + +static void amd_pmf_update_bios_inputs(struct amd_pmf_dev *pdev, struct pmf_bios_input_entry *data, + const struct amd_pmf_pb_bitmap *inputs, + struct ta_pmf_enact_table *in) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(custom_bios_inputs); i++) { + if (!(data->preq & inputs[i].bit_mask)) + continue; + amd_pmf_set_ta_custom_bios_input(in, i, data->val[i]); + pdev->cb_prev.custom_bios_inputs[i] = data->val[i]; + dev_dbg(pdev->dev, "Custom BIOS Input[%d]: %u\n", i, data->val[i]); + } +} + +static void amd_pmf_get_custom_bios_inputs(struct amd_pmf_dev *pdev, + struct ta_pmf_enact_table *in) +{ + struct pmf_cbi_ring_buffer *rb = &pdev->cbi_buf; + unsigned int i; + + guard(mutex)(&pdev->cbi_mutex); + + for (i = 0; i < ARRAY_SIZE(custom_bios_inputs); i++) + amd_pmf_set_ta_custom_bios_input(in, i, pdev->cb_prev.custom_bios_inputs[i]); + + if (CIRC_CNT(rb->head, rb->tail, CUSTOM_BIOS_INPUT_RING_ENTRIES) == 0) + return; + + /* If no active custom BIOS input pending request, do not consume further work */ + if (!rb->data[rb->tail].preq) + goto out_rbadvance; + + if (!pdev->smart_pc_enabled) + return; + + switch (pdev->pmf_if_version) { + case PMF_IF_V1: + if (!is_apmf_bios_input_notifications_supported(pdev)) + return; + amd_pmf_update_bios_inputs(pdev, &rb->data[rb->tail], custom_bios_inputs_v1, in); + break; + case PMF_IF_V2: + amd_pmf_update_bios_inputs(pdev, &rb->data[rb->tail], custom_bios_inputs, in); + break; + default: + break; + } + +out_rbadvance: + rb->tail = (rb->tail + 1) & (CUSTOM_BIOS_INPUT_RING_ENTRIES - 1); +} + +static void amd_pmf_get_c0_residency(u16 *core_res, size_t size, struct ta_pmf_enact_table *in) +{ + u16 max, avg = 0; + int i; + + /* Get the avg and max C0 residency of all the cores */ + max = *core_res; + for (i = 0; i < size; i++) { + avg += core_res[i]; + if (core_res[i] > max) + max = core_res[i]; + } + avg = DIV_ROUND_CLOSEST(avg, size); + in->ev_info.avg_c0residency = avg; + in->ev_info.max_c0residency = max; +} + +static void amd_pmf_get_smu_info(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + /* Get the updated metrics table data */ + memset(dev->buf, 0, dev->mtable_size); + amd_pmf_send_cmd(dev, SET_TRANSFER_TABLE, SET_CMD, METRICS_TABLE_ID, NULL); + + switch (dev->cpu_id) { + case AMD_CPU_ID_PS: + memcpy(&dev->m_table, dev->buf, dev->mtable_size); + in->ev_info.socket_power = dev->m_table.apu_power + dev->m_table.dgpu_power; + in->ev_info.skin_temperature = dev->m_table.skin_temp; + in->ev_info.gfx_busy = dev->m_table.avg_gfx_activity; + amd_pmf_get_c0_residency(dev->m_table.avg_core_c0residency, + ARRAY_SIZE(dev->m_table.avg_core_c0residency), in); + break; + case PCI_DEVICE_ID_AMD_1AH_M20H_ROOT: + case PCI_DEVICE_ID_AMD_1AH_M60H_ROOT: + memcpy(&dev->m_table_v2, dev->buf, dev->mtable_size); + in->ev_info.socket_power = dev->m_table_v2.apu_power + dev->m_table_v2.dgpu_power; + in->ev_info.skin_temperature = dev->m_table_v2.skin_temp; + in->ev_info.gfx_busy = dev->m_table_v2.gfx_activity; + amd_pmf_get_c0_residency(dev->m_table_v2.core_c0residency, + ARRAY_SIZE(dev->m_table_v2.core_c0residency), in); + break; + default: + dev_err(dev->dev, "Unsupported CPU id: 0x%x", dev->cpu_id); + } +} + +static const char * const pmf_battery_supply_name[] = { + "BATT", + "BAT0", +}; + +static int amd_pmf_get_battery_prop(enum power_supply_property prop) +{ + union power_supply_propval value; + struct power_supply *psy; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(pmf_battery_supply_name); i++) { + psy = power_supply_get_by_name(pmf_battery_supply_name[i]); + if (!psy) + continue; + + ret = power_supply_get_property(psy, prop, &value); + if (ret) { + power_supply_put(psy); + return ret; + } + } + + return value.intval; +} + +static int amd_pmf_get_battery_info(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + int val; + + val = amd_pmf_get_battery_prop(POWER_SUPPLY_PROP_PRESENT); + if (val < 0) + return val; + if (val != 1) + return -ENODEV; + + in->ev_info.bat_percentage = amd_pmf_get_battery_prop(POWER_SUPPLY_PROP_CAPACITY); + /* all values in mWh metrics */ + in->ev_info.bat_design = amd_pmf_get_battery_prop(POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN) / + MILLIWATT_PER_WATT; + in->ev_info.full_charge_capacity = amd_pmf_get_battery_prop(POWER_SUPPLY_PROP_ENERGY_FULL) / + MILLIWATT_PER_WATT; + in->ev_info.drain_rate = amd_pmf_get_battery_prop(POWER_SUPPLY_PROP_POWER_NOW) / + MILLIWATT_PER_WATT; + + return 0; +} + +static int amd_pmf_get_slider_info(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + int val; + + switch (dev->current_profile) { + case PLATFORM_PROFILE_PERFORMANCE: + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + val = TA_BEST_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + val = TA_BETTER_PERFORMANCE; + break; + case PLATFORM_PROFILE_LOW_POWER: + case PLATFORM_PROFILE_QUIET: + val = TA_BEST_BATTERY; + break; + default: + dev_err(dev->dev, "Unknown Platform Profile.\n"); + return -EOPNOTSUPP; + } + in->ev_info.power_slider = val; + + return 0; +} + +static void amd_pmf_get_sensor_info(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + struct amd_sfh_info sfh_info; + + /* Get the latest information from SFH */ + in->ev_info.user_present = false; + + /* Get ALS data */ + if (!amd_get_sfh_info(&sfh_info, MT_ALS)) + in->ev_info.ambient_light = sfh_info.ambient_light; + else + dev_dbg(dev->dev, "ALS is not enabled/detected\n"); + + /* get HPD data */ + if (!amd_get_sfh_info(&sfh_info, MT_HPD)) { + if (sfh_info.user_present == SFH_USER_PRESENT) + in->ev_info.user_present = true; + } else { + dev_dbg(dev->dev, "HPD is not enabled/detected\n"); + } + + /* Get SRA (Secondary Accelerometer) data */ + if (!amd_get_sfh_info(&sfh_info, MT_SRA)) { + in->ev_info.platform_type = sfh_info.platform_type; + in->ev_info.device_state = sfh_info.laptop_placement; + } else { + dev_dbg(dev->dev, "SRA is not enabled/detected\n"); + } +} + +void amd_pmf_populate_ta_inputs(struct amd_pmf_dev *dev, struct ta_pmf_enact_table *in) +{ + /* TA side lid open is 1 and close is 0, hence the ! here */ + in->ev_info.lid_state = !acpi_lid_open(); + in->ev_info.power_source = amd_pmf_get_power_source(); + amd_pmf_get_smu_info(dev, in); + amd_pmf_get_battery_info(dev, in); + amd_pmf_get_slider_info(dev, in); + amd_pmf_get_sensor_info(dev, in); + amd_pmf_get_custom_bios_inputs(dev, in); +} diff --git a/drivers/platform/x86/amd/pmf/sps.c b/drivers/platform/x86/amd/pmf/sps.c new file mode 100644 index 000000000..0b70a5153 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/sps.c @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework (PMF) Driver + * + * Copyright (c) 2022, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include "pmf.h" + +static struct amd_pmf_static_slider_granular_v2 config_store_v2; +static struct amd_pmf_static_slider_granular config_store; +static struct amd_pmf_apts_granular apts_config_store; + +#ifdef CONFIG_AMD_PMF_DEBUG +static const char *slider_v2_as_str(unsigned int state) +{ + switch (state) { + case POWER_MODE_BEST_PERFORMANCE: + return "Best Performance"; + case POWER_MODE_BALANCED: + return "Balanced"; + case POWER_MODE_BEST_POWER_EFFICIENCY: + return "Best Power Efficiency"; + case POWER_MODE_ENERGY_SAVE: + return "Energy Save"; + default: + return "Unknown Power Mode"; + } +} + +static const char *slider_as_str(unsigned int state) +{ + switch (state) { + case POWER_MODE_PERFORMANCE: + return "PERFORMANCE"; + case POWER_MODE_BALANCED_POWER: + return "BALANCED_POWER"; + case POWER_MODE_POWER_SAVER: + return "POWER_SAVER"; + default: + return "Unknown Slider State"; + } +} + +const char *amd_pmf_source_as_str(unsigned int state) +{ + switch (state) { + case POWER_SOURCE_AC: + return "AC"; + case POWER_SOURCE_DC: + return "DC"; + default: + return "Unknown Power State"; + } +} + +static void amd_pmf_dump_sps_defaults(struct amd_pmf_static_slider_granular *data) +{ + int i, j; + + pr_debug("Static Slider Data - BEGIN\n"); + + for (i = 0; i < POWER_SOURCE_MAX; i++) { + for (j = 0; j < POWER_MODE_MAX; j++) { + pr_debug("--- Source:%s Mode:%s ---\n", amd_pmf_source_as_str(i), + slider_as_str(j)); + pr_debug("SPL: %u mW\n", data->prop[i][j].spl); + pr_debug("SPPT: %u mW\n", data->prop[i][j].sppt); + pr_debug("SPPT_ApuOnly: %u mW\n", data->prop[i][j].sppt_apu_only); + pr_debug("FPPT: %u mW\n", data->prop[i][j].fppt); + pr_debug("STTMinLimit: %u mW\n", data->prop[i][j].stt_min); + pr_debug("STT_SkinTempLimit_APU: %u C\n", + data->prop[i][j].stt_skin_temp[STT_TEMP_APU]); + pr_debug("STT_SkinTempLimit_HS2: %u C\n", + data->prop[i][j].stt_skin_temp[STT_TEMP_HS2]); + } + } + + pr_debug("Static Slider Data - END\n"); +} + +static void amd_pmf_dump_sps_defaults_v2(struct amd_pmf_static_slider_granular_v2 *data) +{ + unsigned int i, j; + + pr_debug("Static Slider APTS state index data - BEGIN"); + pr_debug("size: %u\n", data->size); + + for (i = 0; i < POWER_SOURCE_MAX; i++) + for (j = 0; j < POWER_MODE_V2_MAX; j++) + pr_debug("%s %s: %u\n", amd_pmf_source_as_str(i), slider_v2_as_str(j), + data->sps_idx.power_states[i][j]); + + pr_debug("Static Slider APTS state index data - END\n"); +} + +static void amd_pmf_dump_apts_sps_defaults(struct amd_pmf_apts_granular *info) +{ + int i; + + pr_debug("Static Slider APTS index default values data - BEGIN"); + + for (i = 0; i < APTS_MAX_STATES; i++) { + pr_debug("Table Version[%d] = %u\n", i, info->val[i].table_version); + pr_debug("Fan Index[%d] = %u\n", i, info->val[i].fan_table_idx); + pr_debug("PPT[%d] = %u\n", i, info->val[i].pmf_ppt); + pr_debug("PPT APU[%d] = %u\n", i, info->val[i].ppt_pmf_apu_only); + pr_debug("STT Min[%d] = %u\n", i, info->val[i].stt_min_limit); + pr_debug("STT APU[%d] = %u\n", i, info->val[i].stt_skin_temp_limit_apu); + pr_debug("STT HS2[%d] = %u\n", i, info->val[i].stt_skin_temp_limit_hs2); + } + + pr_debug("Static Slider APTS index default values data - END"); +} +#else +static void amd_pmf_dump_sps_defaults(struct amd_pmf_static_slider_granular *data) {} +static void amd_pmf_dump_sps_defaults_v2(struct amd_pmf_static_slider_granular_v2 *data) {} +static void amd_pmf_dump_apts_sps_defaults(struct amd_pmf_apts_granular *info) {} +#endif + +static void amd_pmf_load_apts_defaults_sps_v2(struct amd_pmf_dev *pdev) +{ + struct amd_pmf_apts_granular_output output; + struct amd_pmf_apts_output *ps; + int i; + + memset(&apts_config_store, 0, sizeof(apts_config_store)); + + ps = apts_config_store.val; + + for (i = 0; i < APTS_MAX_STATES; i++) { + apts_get_static_slider_granular_v2(pdev, &output, i); + ps[i].table_version = output.val.table_version; + ps[i].fan_table_idx = output.val.fan_table_idx; + ps[i].pmf_ppt = output.val.pmf_ppt; + ps[i].ppt_pmf_apu_only = output.val.ppt_pmf_apu_only; + ps[i].stt_min_limit = output.val.stt_min_limit; + ps[i].stt_skin_temp_limit_apu = output.val.stt_skin_temp_limit_apu; + ps[i].stt_skin_temp_limit_hs2 = output.val.stt_skin_temp_limit_hs2; + } + + amd_pmf_dump_apts_sps_defaults(&apts_config_store); +} + +static void amd_pmf_load_defaults_sps_v2(struct amd_pmf_dev *dev) +{ + struct apmf_static_slider_granular_output_v2 output; + unsigned int i, j; + + memset(&config_store_v2, 0, sizeof(config_store_v2)); + apmf_get_static_slider_granular_v2(dev, &output); + + config_store_v2.size = output.size; + + for (i = 0; i < POWER_SOURCE_MAX; i++) + for (j = 0; j < POWER_MODE_V2_MAX; j++) + config_store_v2.sps_idx.power_states[i][j] = + output.sps_idx.power_states[i][j]; + + amd_pmf_dump_sps_defaults_v2(&config_store_v2); +} + +static void amd_pmf_load_defaults_sps(struct amd_pmf_dev *dev) +{ + struct apmf_static_slider_granular_output output; + int i, j, idx = 0; + + memset(&config_store, 0, sizeof(config_store)); + apmf_get_static_slider_granular(dev, &output); + + for (i = 0; i < POWER_SOURCE_MAX; i++) { + for (j = 0; j < POWER_MODE_MAX; j++) { + config_store.prop[i][j].spl = output.prop[idx].spl; + config_store.prop[i][j].sppt = output.prop[idx].sppt; + config_store.prop[i][j].sppt_apu_only = + output.prop[idx].sppt_apu_only; + config_store.prop[i][j].fppt = output.prop[idx].fppt; + config_store.prop[i][j].stt_min = output.prop[idx].stt_min; + config_store.prop[i][j].stt_skin_temp[STT_TEMP_APU] = + output.prop[idx].stt_skin_temp[STT_TEMP_APU]; + config_store.prop[i][j].stt_skin_temp[STT_TEMP_HS2] = + output.prop[idx].stt_skin_temp[STT_TEMP_HS2]; + config_store.prop[i][j].fan_id = output.prop[idx].fan_id; + idx++; + } + } + amd_pmf_dump_sps_defaults(&config_store); +} + +static void amd_pmf_update_slider_v2(struct amd_pmf_dev *dev, int idx) +{ + amd_pmf_send_cmd(dev, SET_PMF_PPT, SET_CMD, apts_config_store.val[idx].pmf_ppt, NULL); + amd_pmf_send_cmd(dev, SET_PMF_PPT_APU_ONLY, SET_CMD, + apts_config_store.val[idx].ppt_pmf_apu_only, NULL); + amd_pmf_send_cmd(dev, SET_STT_MIN_LIMIT, SET_CMD, + apts_config_store.val[idx].stt_min_limit, NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_APU, SET_CMD, + fixp_q88_fromint(apts_config_store.val[idx].stt_skin_temp_limit_apu), + NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_HS2, SET_CMD, + fixp_q88_fromint(apts_config_store.val[idx].stt_skin_temp_limit_hs2), + NULL); +} + +void amd_pmf_update_slider(struct amd_pmf_dev *dev, bool op, int idx, + struct amd_pmf_static_slider_granular *table) +{ + int src = amd_pmf_get_power_source(); + + if (op == SLIDER_OP_SET) { + amd_pmf_send_cmd(dev, SET_SPL, SET_CMD, config_store.prop[src][idx].spl, NULL); + amd_pmf_send_cmd(dev, SET_FPPT, SET_CMD, config_store.prop[src][idx].fppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT, SET_CMD, config_store.prop[src][idx].sppt, NULL); + amd_pmf_send_cmd(dev, SET_SPPT_APU_ONLY, SET_CMD, + config_store.prop[src][idx].sppt_apu_only, NULL); + amd_pmf_send_cmd(dev, SET_STT_MIN_LIMIT, SET_CMD, + config_store.prop[src][idx].stt_min, NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_APU, SET_CMD, + fixp_q88_fromint(config_store.prop[src][idx].stt_skin_temp[STT_TEMP_APU]), + NULL); + amd_pmf_send_cmd(dev, SET_STT_LIMIT_HS2, SET_CMD, + fixp_q88_fromint(config_store.prop[src][idx].stt_skin_temp[STT_TEMP_HS2]), + NULL); + } else if (op == SLIDER_OP_GET) { + amd_pmf_send_cmd(dev, GET_SPL, GET_CMD, ARG_NONE, &table->prop[src][idx].spl); + amd_pmf_send_cmd(dev, GET_FPPT, GET_CMD, ARG_NONE, &table->prop[src][idx].fppt); + amd_pmf_send_cmd(dev, GET_SPPT, GET_CMD, ARG_NONE, &table->prop[src][idx].sppt); + amd_pmf_send_cmd(dev, GET_SPPT_APU_ONLY, GET_CMD, ARG_NONE, + &table->prop[src][idx].sppt_apu_only); + amd_pmf_send_cmd(dev, GET_STT_MIN_LIMIT, GET_CMD, ARG_NONE, + &table->prop[src][idx].stt_min); + amd_pmf_send_cmd(dev, GET_STT_LIMIT_APU, GET_CMD, ARG_NONE, + (u32 *)&table->prop[src][idx].stt_skin_temp[STT_TEMP_APU]); + amd_pmf_send_cmd(dev, GET_STT_LIMIT_HS2, GET_CMD, ARG_NONE, + (u32 *)&table->prop[src][idx].stt_skin_temp[STT_TEMP_HS2]); + } +} + +static int amd_pmf_update_sps_power_limits_v2(struct amd_pmf_dev *pdev, int pwr_mode) +{ + int src, index; + + src = amd_pmf_get_power_source(); + + switch (pwr_mode) { + case POWER_MODE_PERFORMANCE: + index = config_store_v2.sps_idx.power_states[src][POWER_MODE_BEST_PERFORMANCE]; + amd_pmf_update_slider_v2(pdev, index); + break; + case POWER_MODE_BALANCED_POWER: + index = config_store_v2.sps_idx.power_states[src][POWER_MODE_BALANCED]; + amd_pmf_update_slider_v2(pdev, index); + break; + case POWER_MODE_POWER_SAVER: + index = config_store_v2.sps_idx.power_states[src][POWER_MODE_BEST_POWER_EFFICIENCY]; + amd_pmf_update_slider_v2(pdev, index); + break; + default: + return -EINVAL; + } + + return 0; +} + +int amd_pmf_set_sps_power_limits(struct amd_pmf_dev *pmf) +{ + int mode; + + mode = amd_pmf_get_pprof_modes(pmf); + if (mode < 0) + return mode; + + if (pmf->pmf_if_version == PMF_IF_V2) + return amd_pmf_update_sps_power_limits_v2(pmf, mode); + + amd_pmf_update_slider(pmf, SLIDER_OP_SET, mode, NULL); + + return 0; +} + +bool is_pprof_balanced(struct amd_pmf_dev *pmf) +{ + return pmf->current_profile == PLATFORM_PROFILE_BALANCED; +} + +static int amd_pmf_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct amd_pmf_dev *pmf = dev_get_drvdata(dev); + + *profile = pmf->current_profile; + return 0; +} + +int amd_pmf_get_pprof_modes(struct amd_pmf_dev *pmf) +{ + int mode; + + switch (pmf->current_profile) { + case PLATFORM_PROFILE_PERFORMANCE: + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + mode = POWER_MODE_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + mode = POWER_MODE_BALANCED_POWER; + break; + case PLATFORM_PROFILE_LOW_POWER: + case PLATFORM_PROFILE_QUIET: + mode = POWER_MODE_POWER_SAVER; + break; + default: + dev_err(pmf->dev, "Unknown Platform Profile.\n"); + return -EOPNOTSUPP; + } + + return mode; +} + +int amd_pmf_power_slider_update_event(struct amd_pmf_dev *dev) +{ + u8 flag = 0; + int mode; + int src; + + mode = amd_pmf_get_pprof_modes(dev); + if (mode < 0) + return mode; + + src = amd_pmf_get_power_source(); + + if (src == POWER_SOURCE_AC) { + switch (mode) { + case POWER_MODE_PERFORMANCE: + flag |= BIT(AC_BEST_PERF); + break; + case POWER_MODE_BALANCED_POWER: + flag |= BIT(AC_BETTER_PERF); + break; + case POWER_MODE_POWER_SAVER: + flag |= BIT(AC_BETTER_BATTERY); + break; + default: + dev_err(dev->dev, "unsupported platform profile\n"); + return -EOPNOTSUPP; + } + + } else if (src == POWER_SOURCE_DC) { + switch (mode) { + case POWER_MODE_PERFORMANCE: + flag |= BIT(DC_BEST_PERF); + break; + case POWER_MODE_BALANCED_POWER: + flag |= BIT(DC_BETTER_PERF); + break; + case POWER_MODE_POWER_SAVER: + flag |= BIT(DC_BATTERY_SAVER); + break; + default: + dev_err(dev->dev, "unsupported platform profile\n"); + return -EOPNOTSUPP; + } + } + + apmf_os_power_slider_update(dev, flag); + + return 0; +} + +static int amd_pmf_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct amd_pmf_dev *pmf = dev_get_drvdata(dev); + int ret = 0; + + pmf->current_profile = profile; + + /* Notify EC about the slider position change */ + if (is_apmf_func_supported(pmf, APMF_FUNC_OS_POWER_SLIDER_UPDATE)) { + ret = amd_pmf_power_slider_update_event(pmf); + if (ret) + return ret; + } + + if (is_apmf_func_supported(pmf, APMF_FUNC_STATIC_SLIDER_GRANULAR)) { + ret = amd_pmf_set_sps_power_limits(pmf); + if (ret) + return ret; + } + + return 0; +} + +static int amd_pmf_hidden_choices(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_QUIET, choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); + + return 0; +} + +static int amd_pmf_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); + + return 0; +} + +static const struct platform_profile_ops amd_pmf_profile_ops = { + .probe = amd_pmf_profile_probe, + .hidden_choices = amd_pmf_hidden_choices, + .profile_get = amd_pmf_profile_get, + .profile_set = amd_pmf_profile_set, +}; + +int amd_pmf_init_sps(struct amd_pmf_dev *dev) +{ + dev->current_profile = PLATFORM_PROFILE_BALANCED; + + if (is_apmf_func_supported(dev, APMF_FUNC_STATIC_SLIDER_GRANULAR)) { + if (dev->pmf_if_version == PMF_IF_V2) { + amd_pmf_load_defaults_sps_v2(dev); + amd_pmf_load_apts_defaults_sps_v2(dev); + } else { + amd_pmf_load_defaults_sps(dev); + } + + /* update SPS balanced power mode thermals */ + amd_pmf_set_sps_power_limits(dev); + } + + /* Create platform_profile structure and register */ + dev->ppdev = devm_platform_profile_register(dev->dev, "amd-pmf", dev, + &amd_pmf_profile_ops); + if (IS_ERR(dev->ppdev)) + dev_err(dev->dev, "Failed to register SPS support, this is most likely an SBIOS bug: %ld\n", + PTR_ERR(dev->ppdev)); + + return PTR_ERR_OR_ZERO(dev->ppdev); +} diff --git a/drivers/platform/x86/amd/pmf/tee-if.c b/drivers/platform/x86/amd/pmf/tee-if.c new file mode 100644 index 000000000..7ccd93f50 --- /dev/null +++ b/drivers/platform/x86/amd/pmf/tee-if.c @@ -0,0 +1,625 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * AMD Platform Management Framework Driver - TEE Interface + * + * Copyright (c) 2023, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Author: Shyam Sundar S K + */ + +#include +#include +#include +#include "pmf.h" + +#define MAX_TEE_PARAM 4 + +/* Policy binary actions sampling frequency (in ms) */ +static int pb_actions_ms = MSEC_PER_SEC; +/* Sideload policy binaries to debug policy failures */ +static bool pb_side_load; + +#ifdef CONFIG_AMD_PMF_DEBUG +module_param(pb_actions_ms, int, 0644); +MODULE_PARM_DESC(pb_actions_ms, "Policy binary actions sampling frequency (default = 1000ms)"); +module_param(pb_side_load, bool, 0444); +MODULE_PARM_DESC(pb_side_load, "Sideload policy binaries debug policy failures"); +#endif + +static const char *amd_pmf_uevent_as_str(unsigned int state) +{ + switch (state) { + case SYSTEM_STATE_S0i3: + return "S0i3"; + case SYSTEM_STATE_S4: + return "S4"; + case SYSTEM_STATE_SCREEN_LOCK: + return "SCREEN_LOCK"; + default: + return "Unknown Smart PC event"; + } +} + +static void amd_pmf_prepare_args(struct amd_pmf_dev *dev, int cmd, + struct tee_ioctl_invoke_arg *arg, + struct tee_param *param) +{ + memset(arg, 0, sizeof(*arg)); + memset(param, 0, MAX_TEE_PARAM * sizeof(*param)); + + arg->func = cmd; + arg->session = dev->session_id; + arg->num_params = MAX_TEE_PARAM; + + /* Fill invoke cmd params */ + param[0].u.memref.size = sizeof(struct ta_pmf_shared_memory); + param[0].attr = TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_INOUT; + param[0].u.memref.shm = dev->fw_shm_pool; + param[0].u.memref.shm_offs = 0; +} + +static void amd_pmf_update_uevents(struct amd_pmf_dev *dev, u16 event) +{ + input_report_key(dev->pmf_idev, event, 1); /* key press */ + input_sync(dev->pmf_idev); + input_report_key(dev->pmf_idev, event, 0); /* key release */ + input_sync(dev->pmf_idev); +} + +static int amd_pmf_get_bios_output_idx(u32 action_idx) +{ + switch (action_idx) { + case PMF_POLICY_BIOS_OUTPUT_1: + return 0; + case PMF_POLICY_BIOS_OUTPUT_2: + return 1; + case PMF_POLICY_BIOS_OUTPUT_3: + return 2; + case PMF_POLICY_BIOS_OUTPUT_4: + return 3; + case PMF_POLICY_BIOS_OUTPUT_5: + return 4; + case PMF_POLICY_BIOS_OUTPUT_6: + return 5; + case PMF_POLICY_BIOS_OUTPUT_7: + return 6; + case PMF_POLICY_BIOS_OUTPUT_8: + return 7; + case PMF_POLICY_BIOS_OUTPUT_9: + return 8; + case PMF_POLICY_BIOS_OUTPUT_10: + return 9; + default: + return -EINVAL; + } +} + +static void amd_pmf_update_bios_output(struct amd_pmf_dev *pdev, struct ta_pmf_action *action) +{ + u32 bios_idx; + + bios_idx = amd_pmf_get_bios_output_idx(action->action_index); + + amd_pmf_smartpc_apply_bios_output(pdev, action->value, BIT(bios_idx), bios_idx); +} + +static void amd_pmf_apply_policies(struct amd_pmf_dev *dev, struct ta_pmf_enact_result *out) +{ + struct ta_pmf_action *action; + u32 val; + int idx; + + for (idx = 0; idx < out->actions_count; idx++) { + action = &out->actions_list[idx]; + val = action->value; + switch (action->action_index) { + case PMF_POLICY_SPL: + if (dev->prev_data->spl != val) { + amd_pmf_send_cmd(dev, SET_SPL, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update SPL: %u\n", val); + dev->prev_data->spl = val; + } + break; + + case PMF_POLICY_SPPT: + if (dev->prev_data->sppt != val) { + amd_pmf_send_cmd(dev, SET_SPPT, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update SPPT: %u\n", val); + dev->prev_data->sppt = val; + } + break; + + case PMF_POLICY_FPPT: + if (dev->prev_data->fppt != val) { + amd_pmf_send_cmd(dev, SET_FPPT, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update FPPT: %u\n", val); + dev->prev_data->fppt = val; + } + break; + + case PMF_POLICY_SPPT_APU_ONLY: + if (dev->prev_data->sppt_apuonly != val) { + amd_pmf_send_cmd(dev, SET_SPPT_APU_ONLY, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update SPPT_APU_ONLY: %u\n", val); + dev->prev_data->sppt_apuonly = val; + } + break; + + case PMF_POLICY_STT_MIN: + if (dev->prev_data->stt_minlimit != val) { + amd_pmf_send_cmd(dev, SET_STT_MIN_LIMIT, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update STT_MIN: %u\n", val); + dev->prev_data->stt_minlimit = val; + } + break; + + case PMF_POLICY_STT_SKINTEMP_APU: + if (dev->prev_data->stt_skintemp_apu != val) { + amd_pmf_send_cmd(dev, SET_STT_LIMIT_APU, SET_CMD, + fixp_q88_fromint(val), NULL); + dev_dbg(dev->dev, "update STT_SKINTEMP_APU: %u\n", val); + dev->prev_data->stt_skintemp_apu = val; + } + break; + + case PMF_POLICY_STT_SKINTEMP_HS2: + if (dev->prev_data->stt_skintemp_hs2 != val) { + amd_pmf_send_cmd(dev, SET_STT_LIMIT_HS2, SET_CMD, + fixp_q88_fromint(val), NULL); + dev_dbg(dev->dev, "update STT_SKINTEMP_HS2: %u\n", val); + dev->prev_data->stt_skintemp_hs2 = val; + } + break; + + case PMF_POLICY_P3T: + if (dev->prev_data->p3t_limit != val) { + amd_pmf_send_cmd(dev, SET_P3T, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update P3T: %u\n", val); + dev->prev_data->p3t_limit = val; + } + break; + + case PMF_POLICY_PMF_PPT: + if (dev->prev_data->pmf_ppt != val) { + amd_pmf_send_cmd(dev, SET_PMF_PPT, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update PMF PPT: %u\n", val); + dev->prev_data->pmf_ppt = val; + } + break; + + case PMF_POLICY_PMF_PPT_APU_ONLY: + if (dev->prev_data->pmf_ppt_apu_only != val) { + amd_pmf_send_cmd(dev, SET_PMF_PPT_APU_ONLY, SET_CMD, val, NULL); + dev_dbg(dev->dev, "update PMF PPT APU ONLY: %u\n", val); + dev->prev_data->pmf_ppt_apu_only = val; + } + break; + + case PMF_POLICY_SYSTEM_STATE: + switch (val) { + case 0: + amd_pmf_update_uevents(dev, KEY_SLEEP); + break; + case 1: + amd_pmf_update_uevents(dev, KEY_SUSPEND); + break; + case 2: + amd_pmf_update_uevents(dev, KEY_SCREENLOCK); + break; + default: + dev_err(dev->dev, "Invalid PMF policy system state: %d\n", val); + } + + dev_dbg(dev->dev, "update SYSTEM_STATE: %s\n", + amd_pmf_uevent_as_str(val)); + break; + + case PMF_POLICY_BIOS_OUTPUT_1: + case PMF_POLICY_BIOS_OUTPUT_2: + case PMF_POLICY_BIOS_OUTPUT_3: + case PMF_POLICY_BIOS_OUTPUT_4: + case PMF_POLICY_BIOS_OUTPUT_5: + case PMF_POLICY_BIOS_OUTPUT_6: + case PMF_POLICY_BIOS_OUTPUT_7: + case PMF_POLICY_BIOS_OUTPUT_8: + case PMF_POLICY_BIOS_OUTPUT_9: + case PMF_POLICY_BIOS_OUTPUT_10: + amd_pmf_update_bios_output(dev, action); + break; + } + } +} + +int amd_pmf_invoke_cmd_enact(struct amd_pmf_dev *dev) +{ + struct ta_pmf_shared_memory *ta_sm = NULL; + struct ta_pmf_enact_result *out = NULL; + struct ta_pmf_enact_table *in = NULL; + struct tee_param param[MAX_TEE_PARAM]; + struct tee_ioctl_invoke_arg arg; + int ret = 0; + + if (!dev->tee_ctx) + return -ENODEV; + + memset(dev->shbuf, 0, dev->policy_sz); + ta_sm = dev->shbuf; + out = &ta_sm->pmf_output.policy_apply_table; + in = &ta_sm->pmf_input.enact_table; + + memset(ta_sm, 0, sizeof(*ta_sm)); + ta_sm->command_id = TA_PMF_COMMAND_POLICY_BUILDER_ENACT_POLICIES; + ta_sm->if_version = PMF_TA_IF_VERSION_MAJOR; + + amd_pmf_populate_ta_inputs(dev, in); + amd_pmf_prepare_args(dev, TA_PMF_COMMAND_POLICY_BUILDER_ENACT_POLICIES, &arg, param); + + ret = tee_client_invoke_func(dev->tee_ctx, &arg, param); + if (ret < 0 || arg.ret != 0) { + dev_err(dev->dev, "TEE enact cmd failed. err: %x, ret:%d\n", arg.ret, ret); + return ret; + } + + if (ta_sm->pmf_result == TA_PMF_TYPE_SUCCESS && out->actions_count) { + amd_pmf_dump_ta_inputs(dev, in); + dev_dbg(dev->dev, "action count:%u result:%x\n", out->actions_count, + ta_sm->pmf_result); + amd_pmf_apply_policies(dev, out); + } + + return 0; +} + +static int amd_pmf_invoke_cmd_init(struct amd_pmf_dev *dev) +{ + struct ta_pmf_shared_memory *ta_sm = NULL; + struct tee_param param[MAX_TEE_PARAM]; + struct ta_pmf_init_table *in = NULL; + struct tee_ioctl_invoke_arg arg; + int ret = 0; + + if (!dev->tee_ctx) { + dev_err(dev->dev, "Failed to get TEE context\n"); + return -ENODEV; + } + + dev_dbg(dev->dev, "Policy Binary size: %llu bytes\n", (unsigned long long)dev->policy_sz); + memset(dev->shbuf, 0, dev->policy_sz); + ta_sm = dev->shbuf; + in = &ta_sm->pmf_input.init_table; + + ta_sm->command_id = TA_PMF_COMMAND_POLICY_BUILDER_INITIALIZE; + ta_sm->if_version = PMF_TA_IF_VERSION_MAJOR; + + in->metadata_macrocheck = false; + in->sku_check = false; + in->validate = true; + in->frequency = pb_actions_ms; + in->policies_table.table_size = dev->policy_sz; + + memcpy(in->policies_table.table, dev->policy_buf, dev->policy_sz); + amd_pmf_prepare_args(dev, TA_PMF_COMMAND_POLICY_BUILDER_INITIALIZE, &arg, param); + + ret = tee_client_invoke_func(dev->tee_ctx, &arg, param); + if (ret < 0 || arg.ret != 0) { + dev_err(dev->dev, "Failed to invoke TEE init cmd. err: %x, ret:%d\n", arg.ret, ret); + return ret; + } + + return ta_sm->pmf_result; +} + +static void amd_pmf_invoke_cmd(struct work_struct *work) +{ + struct amd_pmf_dev *dev = container_of(work, struct amd_pmf_dev, pb_work.work); + + amd_pmf_invoke_cmd_enact(dev); + schedule_delayed_work(&dev->pb_work, msecs_to_jiffies(pb_actions_ms)); +} + +int amd_pmf_start_policy_engine(struct amd_pmf_dev *dev) +{ + struct cookie_header *header; + int res; + + if (dev->policy_sz < POLICY_COOKIE_OFFSET + sizeof(*header)) + return -EINVAL; + + header = (struct cookie_header *)(dev->policy_buf + POLICY_COOKIE_OFFSET); + + if (header->sign != POLICY_SIGN_COOKIE || !header->length) { + dev_dbg(dev->dev, "cookie doesn't match\n"); + return -EINVAL; + } + + if (dev->policy_sz < header->length + 512) + return -EINVAL; + + /* Update the actual length */ + dev->policy_sz = header->length + 512; + res = amd_pmf_invoke_cmd_init(dev); + if (res == TA_PMF_TYPE_SUCCESS) { + /* Now its safe to announce that smart pc is enabled */ + dev->smart_pc_enabled = true; + /* + * Start collecting the data from TA FW after a small delay + * or else, we might end up getting stale values. + */ + schedule_delayed_work(&dev->pb_work, msecs_to_jiffies(pb_actions_ms * 3)); + } else { + dev_dbg(dev->dev, "ta invoke cmd init failed err: %x\n", res); + dev->smart_pc_enabled = false; + return res; + } + + return 0; +} + +static inline bool amd_pmf_pb_valid(struct amd_pmf_dev *dev) +{ + return memchr_inv(dev->policy_buf, 0xff, dev->policy_sz); +} + +#ifdef CONFIG_AMD_PMF_DEBUG +static void amd_pmf_hex_dump_pb(struct amd_pmf_dev *dev) +{ + print_hex_dump_debug("(pb): ", DUMP_PREFIX_OFFSET, 16, 1, dev->policy_buf, + dev->policy_sz, false); +} + +static ssize_t amd_pmf_get_pb_data(struct file *filp, const char __user *buf, + size_t length, loff_t *pos) +{ + struct amd_pmf_dev *dev = filp->private_data; + unsigned char *new_policy_buf; + int ret; + + /* Policy binary size cannot exceed POLICY_BUF_MAX_SZ */ + if (length > POLICY_BUF_MAX_SZ || length == 0) + return -EINVAL; + + /* re-alloc to the new buffer length of the policy binary */ + new_policy_buf = devm_kzalloc(dev->dev, length, GFP_KERNEL); + if (!new_policy_buf) + return -ENOMEM; + + if (copy_from_user(new_policy_buf, buf, length)) { + devm_kfree(dev->dev, new_policy_buf); + return -EFAULT; + } + + devm_kfree(dev->dev, dev->policy_buf); + dev->policy_buf = new_policy_buf; + dev->policy_sz = length; + + if (!amd_pmf_pb_valid(dev)) + return -EINVAL; + + amd_pmf_hex_dump_pb(dev); + ret = amd_pmf_start_policy_engine(dev); + if (ret < 0) + return ret; + + return length; +} + +static const struct file_operations pb_fops = { + .write = amd_pmf_get_pb_data, + .open = simple_open, +}; + +static void amd_pmf_open_pb(struct amd_pmf_dev *dev, struct dentry *debugfs_root) +{ + dev->esbin = debugfs_create_dir("pb", debugfs_root); + debugfs_create_file("update_policy", 0644, dev->esbin, dev, &pb_fops); +} + +static void amd_pmf_remove_pb(struct amd_pmf_dev *dev) +{ + debugfs_remove_recursive(dev->esbin); +} +#else +static void amd_pmf_open_pb(struct amd_pmf_dev *dev, struct dentry *debugfs_root) {} +static void amd_pmf_remove_pb(struct amd_pmf_dev *dev) {} +static void amd_pmf_hex_dump_pb(struct amd_pmf_dev *dev) {} +#endif + +static int amd_pmf_amdtee_ta_match(struct tee_ioctl_version_data *ver, const void *data) +{ + return ver->impl_id == TEE_IMPL_ID_AMDTEE; +} + +static int amd_pmf_ta_open_session(struct tee_context *ctx, u32 *id, const uuid_t *uuid) +{ + struct tee_ioctl_open_session_arg sess_arg = {}; + int rc; + + export_uuid(sess_arg.uuid, uuid); + sess_arg.clnt_login = TEE_IOCTL_LOGIN_PUBLIC; + sess_arg.num_params = 0; + + rc = tee_client_open_session(ctx, &sess_arg, NULL); + if (rc < 0 || sess_arg.ret != 0) { + pr_err("Failed to open TEE session err:%#x, rc:%d\n", sess_arg.ret, rc); + return rc ?: -EINVAL; + } + + *id = sess_arg.session; + + return 0; +} + +static int amd_pmf_register_input_device(struct amd_pmf_dev *dev) +{ + int err; + + dev->pmf_idev = devm_input_allocate_device(dev->dev); + if (!dev->pmf_idev) + return -ENOMEM; + + dev->pmf_idev->name = "PMF-TA output events"; + dev->pmf_idev->phys = "amd-pmf/input0"; + + input_set_capability(dev->pmf_idev, EV_KEY, KEY_SLEEP); + input_set_capability(dev->pmf_idev, EV_KEY, KEY_SCREENLOCK); + input_set_capability(dev->pmf_idev, EV_KEY, KEY_SUSPEND); + + err = input_register_device(dev->pmf_idev); + if (err) { + dev_err(dev->dev, "Failed to register input device: %d\n", err); + return err; + } + + return 0; +} + +int amd_pmf_tee_init(struct amd_pmf_dev *dev, const uuid_t *uuid) +{ + u32 size; + int ret; + + dev->tee_ctx = tee_client_open_context(NULL, amd_pmf_amdtee_ta_match, NULL, NULL); + if (IS_ERR(dev->tee_ctx)) { + dev_err(dev->dev, "Failed to open TEE context\n"); + ret = PTR_ERR(dev->tee_ctx); + dev->tee_ctx = NULL; + return ret; + } + + ret = amd_pmf_ta_open_session(dev->tee_ctx, &dev->session_id, uuid); + if (ret) { + dev_err(dev->dev, "Failed to open TA session (%d)\n", ret); + ret = -EINVAL; + goto out_ctx; + } + + size = sizeof(struct ta_pmf_shared_memory) + dev->policy_sz; + dev->fw_shm_pool = tee_shm_alloc_kernel_buf(dev->tee_ctx, size); + if (IS_ERR(dev->fw_shm_pool)) { + dev_err(dev->dev, "Failed to alloc TEE shared memory\n"); + ret = PTR_ERR(dev->fw_shm_pool); + goto out_sess; + } + + dev->shbuf = tee_shm_get_va(dev->fw_shm_pool, 0); + if (IS_ERR(dev->shbuf)) { + dev_err(dev->dev, "Failed to get TEE virtual address\n"); + ret = PTR_ERR(dev->shbuf); + goto out_shm; + } + dev_dbg(dev->dev, "TEE init done\n"); + + return 0; + +out_shm: + tee_shm_free(dev->fw_shm_pool); +out_sess: + tee_client_close_session(dev->tee_ctx, dev->session_id); +out_ctx: + tee_client_close_context(dev->tee_ctx); + + return ret; +} + +void amd_pmf_tee_deinit(struct amd_pmf_dev *dev) +{ + if (!dev->tee_ctx) + return; + tee_shm_free(dev->fw_shm_pool); + tee_client_close_session(dev->tee_ctx, dev->session_id); + tee_client_close_context(dev->tee_ctx); + dev->tee_ctx = NULL; +} + +int amd_pmf_init_smart_pc(struct amd_pmf_dev *dev) +{ + bool status; + int ret, i; + + ret = apmf_check_smart_pc(dev); + if (ret) { + /* + * Lets not return from here if Smart PC bit is not advertised in + * the BIOS. This way, there will be some amount of power savings + * to the user with static slider (if enabled). + */ + dev_info(dev->dev, "PMF Smart PC not advertised in BIOS!:%d\n", ret); + return -ENODEV; + } + + INIT_DELAYED_WORK(&dev->pb_work, amd_pmf_invoke_cmd); + + ret = amd_pmf_set_dram_addr(dev, true); + if (ret) + return ret; + + dev->policy_base = devm_ioremap_resource(dev->dev, dev->res); + if (IS_ERR(dev->policy_base)) + return PTR_ERR(dev->policy_base); + + dev->policy_buf = devm_kzalloc(dev->dev, dev->policy_sz, GFP_KERNEL); + if (!dev->policy_buf) + return -ENOMEM; + + memcpy_fromio(dev->policy_buf, dev->policy_base, dev->policy_sz); + + if (!amd_pmf_pb_valid(dev)) { + dev_info(dev->dev, "No Smart PC policy present\n"); + return -EINVAL; + } + + amd_pmf_hex_dump_pb(dev); + + dev->prev_data = devm_kzalloc(dev->dev, sizeof(*dev->prev_data), GFP_KERNEL); + if (!dev->prev_data) + return -ENOMEM; + + for (i = 0; i < ARRAY_SIZE(amd_pmf_ta_uuid); i++) { + ret = amd_pmf_tee_init(dev, &amd_pmf_ta_uuid[i]); + if (ret) + return ret; + + ret = amd_pmf_start_policy_engine(dev); + dev_dbg(dev->dev, "start policy engine ret: %d\n", ret); + status = ret == TA_PMF_TYPE_SUCCESS; + if (status) { + dev->cb_flag = true; + dev->cbi_buf.head = 0; + dev->cbi_buf.tail = 0; + break; + } + amd_pmf_tee_deinit(dev); + } + + if (!status && !pb_side_load) { + ret = -EINVAL; + goto err; + } + + if (pb_side_load) + amd_pmf_open_pb(dev, dev->dbgfs_dir); + + ret = amd_pmf_register_input_device(dev); + if (ret) + goto err; + + return 0; + +err: + amd_pmf_deinit_smart_pc(dev); + + return ret; +} + +void amd_pmf_deinit_smart_pc(struct amd_pmf_dev *dev) +{ + if (dev->pmf_idev) + input_unregister_device(dev->pmf_idev); + + if (pb_side_load && dev->esbin) + amd_pmf_remove_pb(dev); + + cancel_delayed_work_sync(&dev->pb_work); + amd_pmf_tee_deinit(dev); +} diff --git a/drivers/platform/x86/amd/wbrf.c b/drivers/platform/x86/amd/wbrf.c new file mode 100644 index 000000000..328135718 --- /dev/null +++ b/drivers/platform/x86/amd/wbrf.c @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Wifi Frequency Band Manage Interface + * Copyright (C) 2023 Advanced Micro Devices + */ + +#include +#include + +/* + * Functions bit vector for WBRF method + * + * Bit 0: WBRF supported. + * Bit 1: Function 1 (Add / Remove frequency) is supported. + * Bit 2: Function 2 (Get frequency list) is supported. + */ +#define WBRF_ENABLED 0x0 +#define WBRF_RECORD 0x1 +#define WBRF_RETRIEVE 0x2 + +#define WBRF_REVISION 0x1 + +/* + * The data structure used for WBRF_RETRIEVE is not naturally aligned. + * And unfortunately the design has been settled down. + */ +struct amd_wbrf_ranges_out { + u32 num_of_ranges; + struct freq_band_range band_list[MAX_NUM_OF_WBRF_RANGES]; +} __packed; + +static const guid_t wifi_acpi_dsm_guid = + GUID_INIT(0x7b7656cf, 0xdc3d, 0x4c1c, + 0x83, 0xe9, 0x66, 0xe7, 0x21, 0xde, 0x30, 0x70); + +/* + * Used to notify consumer (amdgpu driver currently) about + * the wifi frequency is change. + */ +static BLOCKING_NOTIFIER_HEAD(wbrf_chain_head); + +static int wbrf_record(struct acpi_device *adev, uint8_t action, struct wbrf_ranges_in_out *in) +{ + union acpi_object argv4; + u32 num_of_ranges = 0; + u32 num_of_elements; + u32 arg_idx = 0; + int ret; + u32 i; + + if (!in) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(in->band_list); i++) { + if (in->band_list[i].start && in->band_list[i].end) + num_of_ranges++; + } + + /* + * The num_of_ranges value in the "in" object supplied by + * the caller is required to be equal to the number of + * entries in the band_list array in there. + */ + if (num_of_ranges != in->num_of_ranges) + return -EINVAL; + + /* + * Every input frequency band comes with two end points(start/end) + * and each is accounted as an element. Meanwhile the range count + * and action type are accounted as an element each. + * So, the total element count = 2 * num_of_ranges + 1 + 1. + */ + num_of_elements = 2 * num_of_ranges + 2; + + union acpi_object *tmp __free(kfree) = kzalloc_objs(*tmp, + num_of_elements); + if (!tmp) + return -ENOMEM; + + argv4.package.type = ACPI_TYPE_PACKAGE; + argv4.package.count = num_of_elements; + argv4.package.elements = tmp; + + /* save the number of ranges*/ + tmp[0].integer.type = ACPI_TYPE_INTEGER; + tmp[0].integer.value = num_of_ranges; + + /* save the action(WBRF_RECORD_ADD/REMOVE/RETRIEVE) */ + tmp[1].integer.type = ACPI_TYPE_INTEGER; + tmp[1].integer.value = action; + + arg_idx = 2; + for (i = 0; i < ARRAY_SIZE(in->band_list); i++) { + if (!in->band_list[i].start || !in->band_list[i].end) + continue; + + tmp[arg_idx].integer.type = ACPI_TYPE_INTEGER; + tmp[arg_idx++].integer.value = in->band_list[i].start; + tmp[arg_idx].integer.type = ACPI_TYPE_INTEGER; + tmp[arg_idx++].integer.value = in->band_list[i].end; + } + + union acpi_object *obj __free(kfree) = + acpi_evaluate_dsm(adev->handle, &wifi_acpi_dsm_guid, + WBRF_REVISION, WBRF_RECORD, &argv4); + + if (!obj) + return -EINVAL; + + if (obj->type != ACPI_TYPE_INTEGER) + return -EINVAL; + + ret = obj->integer.value; + if (ret) + return -EINVAL; + + return ret; +} + +/** + * acpi_amd_wbrf_add_remove - add or remove the frequency band the device is using + * + * @dev: device pointer + * @action: remove or add the frequency band into bios + * @in: input structure containing the frequency band the device is using + * + * Broadcast to other consumers the frequency band the device starts + * to use. Underneath the surface the information is cached into an + * internal buffer first. Then a notification is sent to all those + * registered consumers. So then they can retrieve that buffer to + * know the latest active frequency bands. Consumers that haven't + * yet been registered can retrieve the information from the cache + * when they register. + * + * Return: + * 0 for success add/remove wifi frequency band. + * Returns a negative error code for failure. + */ +int acpi_amd_wbrf_add_remove(struct device *dev, uint8_t action, struct wbrf_ranges_in_out *in) +{ + struct acpi_device *adev; + int ret; + + adev = ACPI_COMPANION(dev); + if (!adev) + return -ENODEV; + + ret = wbrf_record(adev, action, in); + if (ret) + return ret; + + blocking_notifier_call_chain(&wbrf_chain_head, WBRF_CHANGED, NULL); + + return 0; +} +EXPORT_SYMBOL_GPL(acpi_amd_wbrf_add_remove); + +/** + * acpi_amd_wbrf_supported_producer - determine if the WBRF can be enabled + * for the device as a producer + * + * @dev: device pointer + * + * Check if the platform equipped with necessary implementations to + * support WBRF for the device as a producer. + * + * Return: + * true if WBRF is supported, otherwise returns false + */ +bool acpi_amd_wbrf_supported_producer(struct device *dev) +{ + struct acpi_device *adev; + + adev = ACPI_COMPANION(dev); + if (!adev) + return false; + + return acpi_check_dsm(adev->handle, &wifi_acpi_dsm_guid, + WBRF_REVISION, BIT(WBRF_RECORD)); +} +EXPORT_SYMBOL_GPL(acpi_amd_wbrf_supported_producer); + +/** + * acpi_amd_wbrf_supported_consumer - determine if the WBRF can be enabled + * for the device as a consumer + * + * @dev: device pointer + * + * Determine if the platform equipped with necessary implementations to + * support WBRF for the device as a consumer. + * + * Return: + * true if WBRF is supported, otherwise returns false. + */ +bool acpi_amd_wbrf_supported_consumer(struct device *dev) +{ + struct acpi_device *adev; + + adev = ACPI_COMPANION(dev); + if (!adev) + return false; + + return acpi_check_dsm(adev->handle, &wifi_acpi_dsm_guid, + WBRF_REVISION, BIT(WBRF_RETRIEVE)); +} +EXPORT_SYMBOL_GPL(acpi_amd_wbrf_supported_consumer); + +/** + * amd_wbrf_retrieve_freq_band - retrieve current active frequency bands + * + * @dev: device pointer + * @out: output structure containing all the active frequency bands + * + * Retrieve the current active frequency bands which were broadcasted + * by other producers. The consumer who calls this API should take + * proper actions if any of the frequency band may cause RFI with its + * own frequency band used. + * + * Return: + * 0 for getting wifi freq band successfully. + * Returns a negative error code for failure. + */ +int amd_wbrf_retrieve_freq_band(struct device *dev, struct wbrf_ranges_in_out *out) +{ + struct amd_wbrf_ranges_out acpi_out = {0}; + struct acpi_device *adev; + union acpi_object *obj; + union acpi_object param; + int ret = 0; + + adev = ACPI_COMPANION(dev); + if (!adev) + return -ENODEV; + + param.type = ACPI_TYPE_STRING; + param.string.length = 0; + param.string.pointer = NULL; + + obj = acpi_evaluate_dsm(adev->handle, &wifi_acpi_dsm_guid, + WBRF_REVISION, WBRF_RETRIEVE, ¶m); + if (!obj) + return -EINVAL; + + /* + * The return buffer is with variable length and the format below: + * number_of_entries(1 DWORD): Number of entries + * start_freq of 1st entry(1 QWORD): Start frequency of the 1st entry + * end_freq of 1st entry(1 QWORD): End frequency of the 1st entry + * ... + * ... + * start_freq of the last entry(1 QWORD) + * end_freq of the last entry(1 QWORD) + * + * Thus the buffer length is determined by the number of entries. + * - For zero entry scenario, the buffer length will be 4 bytes. + * - For one entry scenario, the buffer length will be 20 bytes. + */ + if (obj->buffer.length > sizeof(acpi_out) || obj->buffer.length < 4) { + dev_err(dev, "Wrong sized WBRT information"); + ret = -EINVAL; + goto out; + } + memcpy(&acpi_out, obj->buffer.pointer, obj->buffer.length); + + out->num_of_ranges = acpi_out.num_of_ranges; + memcpy(out->band_list, acpi_out.band_list, sizeof(acpi_out.band_list)); + +out: + ACPI_FREE(obj); + return ret; +} +EXPORT_SYMBOL_GPL(amd_wbrf_retrieve_freq_band); + +/** + * amd_wbrf_register_notifier - register for notifications of frequency + * band update + * + * @nb: driver notifier block + * + * The consumer should register itself via this API so that it can get + * notified on the frequency band updates from other producers. + * + * Return: + * 0 for registering a consumer driver successfully. + * Returns a negative error code for failure. + */ +int amd_wbrf_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&wbrf_chain_head, nb); +} +EXPORT_SYMBOL_GPL(amd_wbrf_register_notifier); + +/** + * amd_wbrf_unregister_notifier - unregister for notifications of + * frequency band update + * + * @nb: driver notifier block + * + * The consumer should call this API when it is longer interested with + * the frequency band updates from other producers. Usually, this should + * be performed during driver cleanup. + * + * Return: + * 0 for unregistering a consumer driver. + * Returns a negative error code for failure. + */ +int amd_wbrf_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&wbrf_chain_head, nb); +} +EXPORT_SYMBOL_GPL(amd_wbrf_unregister_notifier); diff --git a/drivers/platform/x86/amd/x3d_vcache.c b/drivers/platform/x86/amd/x3d_vcache.c new file mode 100644 index 000000000..0f6d3c54d --- /dev/null +++ b/drivers/platform/x86/amd/x3d_vcache.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AMD 3D V-Cache Performance Optimizer Driver + * + * Copyright (c) 2024, Advanced Micro Devices, Inc. + * All Rights Reserved. + * + * Authors: Basavaraj Natikar + * Perry Yuan + * Mario Limonciello + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char *x3d_mode = "frequency"; +module_param(x3d_mode, charp, 0); +MODULE_PARM_DESC(x3d_mode, "Initial 3D-VCache mode; 'frequency' (default) or 'cache'"); + +#define DSM_REVISION_ID 0 +#define DSM_SET_X3D_MODE 1 + +static guid_t x3d_guid = GUID_INIT(0xdff8e55f, 0xbcfd, 0x46fb, 0xba, 0x0a, + 0xef, 0xd0, 0x45, 0x0f, 0x34, 0xee); + +enum amd_x3d_mode_type { + MODE_INDEX_FREQ, + MODE_INDEX_CACHE, +}; + +static const char * const amd_x3d_mode_strings[] = { + [MODE_INDEX_FREQ] = "frequency", + [MODE_INDEX_CACHE] = "cache", +}; + +struct amd_x3d_dev { + struct device *dev; + acpi_handle ahandle; + /* To protect x3d mode setting */ + struct mutex lock; + enum amd_x3d_mode_type curr_mode; +}; + +static int amd_x3d_get_mode(struct amd_x3d_dev *data) +{ + guard(mutex)(&data->lock); + + return data->curr_mode; +} + +static int amd_x3d_mode_switch(struct amd_x3d_dev *data, int new_state) +{ + union acpi_object *out, argv; + + guard(mutex)(&data->lock); + argv.type = ACPI_TYPE_INTEGER; + argv.integer.value = new_state; + + out = acpi_evaluate_dsm(data->ahandle, &x3d_guid, DSM_REVISION_ID, + DSM_SET_X3D_MODE, &argv); + if (!out) { + dev_err(data->dev, "failed to evaluate _DSM\n"); + return -EINVAL; + } + + data->curr_mode = new_state; + + kfree(out); + + return 0; +} + +static ssize_t amd_x3d_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct amd_x3d_dev *data = dev_get_drvdata(dev); + int ret; + + ret = sysfs_match_string(amd_x3d_mode_strings, buf); + if (ret < 0) + return ret; + + ret = amd_x3d_mode_switch(data, ret); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t amd_x3d_mode_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct amd_x3d_dev *data = dev_get_drvdata(dev); + int mode = amd_x3d_get_mode(data); + + return sysfs_emit(buf, "%s\n", amd_x3d_mode_strings[mode]); +} +static DEVICE_ATTR_RW(amd_x3d_mode); + +static struct attribute *amd_x3d_attrs[] = { + &dev_attr_amd_x3d_mode.attr, + NULL +}; +ATTRIBUTE_GROUPS(amd_x3d); + +static int amd_x3d_resume_handler(struct device *dev) +{ + struct amd_x3d_dev *data = dev_get_drvdata(dev); + int ret = amd_x3d_get_mode(data); + + return amd_x3d_mode_switch(data, ret); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(amd_x3d_pm, NULL, amd_x3d_resume_handler); + +static const struct acpi_device_id amd_x3d_acpi_ids[] = { + {"AMDI0101"}, + { }, +}; +MODULE_DEVICE_TABLE(acpi, amd_x3d_acpi_ids); + +static int amd_x3d_probe(struct platform_device *pdev) +{ + struct amd_x3d_dev *data; + acpi_handle handle; + int ret; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + if (!acpi_check_dsm(handle, &x3d_guid, DSM_REVISION_ID, BIT(DSM_SET_X3D_MODE))) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = &pdev->dev; + + ret = devm_mutex_init(data->dev, &data->lock); + if (ret) + return ret; + + data->ahandle = handle; + platform_set_drvdata(pdev, data); + + ret = match_string(amd_x3d_mode_strings, ARRAY_SIZE(amd_x3d_mode_strings), x3d_mode); + if (ret < 0) + return dev_err_probe(&pdev->dev, -EINVAL, "invalid mode %s\n", x3d_mode); + + return amd_x3d_mode_switch(data, ret); +} + +static struct platform_driver amd_3d_vcache_driver = { + .driver = { + .name = "amd_x3d_vcache", + .dev_groups = amd_x3d_groups, + .acpi_match_table = amd_x3d_acpi_ids, + .pm = pm_sleep_ptr(&amd_x3d_pm), + }, + .probe = amd_x3d_probe, +}; +module_platform_driver(amd_3d_vcache_driver); + +MODULE_DESCRIPTION("AMD 3D V-Cache Performance Optimizer Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/amilo-rfkill.c b/drivers/platform/x86/amilo-rfkill.c new file mode 100644 index 000000000..18397c527 --- /dev/null +++ b/drivers/platform/x86/amilo-rfkill.c @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Support for rfkill on some Fujitsu-Siemens Amilo laptops. + * Copyright 2011 Ben Hutchings. + * + * Based in part on the fsam7440 driver, which is: + * Copyright 2005 Alejandro Vidal Mata & Javier Vidal Mata. + * and on the fsaa1655g driver, which is: + * Copyright 2006 Martin Večeřa. + */ + +#include +#include +#include +#include +#include +#include +#include + +/* + * These values were obtained from disassembling and debugging the + * PM.exe program installed in the Fujitsu-Siemens AMILO A1655G + */ +#define A1655_WIFI_COMMAND 0x10C5 +#define A1655_WIFI_ON 0x25 +#define A1655_WIFI_OFF 0x45 + +static int amilo_a1655_rfkill_set_block(void *data, bool blocked) +{ + u8 param = blocked ? A1655_WIFI_OFF : A1655_WIFI_ON; + int rc; + + i8042_lock_chip(); + rc = i8042_command(¶m, A1655_WIFI_COMMAND); + i8042_unlock_chip(); + return rc; +} + +static const struct rfkill_ops amilo_a1655_rfkill_ops = { + .set_block = amilo_a1655_rfkill_set_block +}; + +/* + * These values were obtained from disassembling the PM.exe program + * installed in the Fujitsu-Siemens AMILO M 7440 + */ +#define M7440_PORT1 0x118f +#define M7440_PORT2 0x118e +#define M7440_RADIO_ON1 0x12 +#define M7440_RADIO_ON2 0x80 +#define M7440_RADIO_OFF1 0x10 +#define M7440_RADIO_OFF2 0x00 + +static int amilo_m7440_rfkill_set_block(void *data, bool blocked) +{ + u8 val1 = blocked ? M7440_RADIO_OFF1 : M7440_RADIO_ON1; + u8 val2 = blocked ? M7440_RADIO_OFF2 : M7440_RADIO_ON2; + + outb(val1, M7440_PORT1); + outb(val2, M7440_PORT2); + + /* Check whether the state has changed correctly */ + if (inb(M7440_PORT1) != val1 || inb(M7440_PORT2) != val2) + return -EIO; + + return 0; +} + +static const struct rfkill_ops amilo_m7440_rfkill_ops = { + .set_block = amilo_m7440_rfkill_set_block +}; + +static const struct dmi_system_id amilo_rfkill_id_table[] = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_BOARD_NAME, "AMILO A1655"), + }, + .driver_data = (void *)&amilo_a1655_rfkill_ops + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_BOARD_NAME, "AMILO L1310"), + }, + .driver_data = (void *)&amilo_a1655_rfkill_ops + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_BOARD_NAME, "AMILO M7440"), + }, + .driver_data = (void *)&amilo_m7440_rfkill_ops + }, + {} +}; + +static struct platform_device *amilo_rfkill_pdev; +static struct rfkill *amilo_rfkill_dev; + +static int amilo_rfkill_probe(struct platform_device *device) +{ + int rc; + const struct dmi_system_id *system_id = + dmi_first_match(amilo_rfkill_id_table); + + if (!system_id) + return -ENXIO; + + amilo_rfkill_dev = rfkill_alloc(KBUILD_MODNAME, &device->dev, + RFKILL_TYPE_WLAN, + system_id->driver_data, NULL); + if (!amilo_rfkill_dev) + return -ENOMEM; + + rc = rfkill_register(amilo_rfkill_dev); + if (rc) + goto fail; + + return 0; + +fail: + rfkill_destroy(amilo_rfkill_dev); + return rc; +} + +static void amilo_rfkill_remove(struct platform_device *device) +{ + rfkill_unregister(amilo_rfkill_dev); + rfkill_destroy(amilo_rfkill_dev); +} + +static struct platform_driver amilo_rfkill_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .probe = amilo_rfkill_probe, + .remove = amilo_rfkill_remove, +}; + +static int __init amilo_rfkill_init(void) +{ + int rc; + + if (dmi_first_match(amilo_rfkill_id_table) == NULL) + return -ENODEV; + + rc = platform_driver_register(&amilo_rfkill_driver); + if (rc) + return rc; + + amilo_rfkill_pdev = platform_device_register_simple(KBUILD_MODNAME, + PLATFORM_DEVID_NONE, + NULL, 0); + if (IS_ERR(amilo_rfkill_pdev)) { + rc = PTR_ERR(amilo_rfkill_pdev); + goto fail; + } + + return 0; + +fail: + platform_driver_unregister(&amilo_rfkill_driver); + return rc; +} + +static void __exit amilo_rfkill_exit(void) +{ + platform_device_unregister(amilo_rfkill_pdev); + platform_driver_unregister(&amilo_rfkill_driver); +} + +MODULE_AUTHOR("Ben Hutchings "); +MODULE_DESCRIPTION("Fujitsu-Siemens Amilo rfkill support"); +MODULE_LICENSE("GPL"); +MODULE_DEVICE_TABLE(dmi, amilo_rfkill_id_table); + +module_init(amilo_rfkill_init); +module_exit(amilo_rfkill_exit); diff --git a/drivers/platform/x86/apple-gmux.c b/drivers/platform/x86/apple-gmux.c new file mode 100644 index 000000000..9c728ac4e --- /dev/null +++ b/drivers/platform/x86/apple-gmux.c @@ -0,0 +1,1039 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Gmux driver for Apple laptops + * + * Copyright (C) Canonical Ltd. + * Copyright (C) 2010-2012 Andreas Heider + * Copyright (C) 2015 Lukas Wunner + * Copyright (C) 2023 Orlando Chamberlain + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * DOC: Overview + * + * gmux is a microcontroller built into the MacBook Pro to support dual GPUs: + * A `Lattice XP2`_ on pre-retinas, a `Renesas R4F2113`_ on pre-T2 retinas. + * + * On T2 Macbooks, the gmux is part of the T2 Coprocessor's SMC. The SMC has + * an I2C connection to a `NXP PCAL6524` GPIO expander, which enables/disables + * the voltage regulators of the discrete GPU, drives the display panel power, + * and has a GPIO to switch the eDP mux. The Intel CPU can interact with + * gmux through MMIO, similar to how the main SMC interface is controlled. + * + * (The MacPro6,1 2013 also has a gmux, however it is unclear why since it has + * dual GPUs but no built-in display.) + * + * gmux is connected to the LPC bus of the southbridge. Its I/O ports are + * accessed differently depending on the microcontroller: Driver functions + * to access a pre-retina gmux are infixed ``_pio_``, those for a pre-T2 + * retina gmux are infixed ``_index_``, and those on T2 Macs are infixed + * with ``_mmio_``. + * + * .. _Lattice XP2: + * http://www.latticesemi.com/en/Products/FPGAandCPLD/LatticeXP2.aspx + * .. _Renesas R4F2113: + * http://www.renesas.com/products/mpumcu/h8s/h8s2100/h8s2113/index.jsp + * .. _NXP PCAL6524: + * https://www.nxp.com/docs/en/data-sheet/PCAL6524.pdf + */ + +struct apple_gmux_config; + +struct apple_gmux_data { + u8 __iomem *iomem_base; + unsigned long iostart; + unsigned long iolen; + const struct apple_gmux_config *config; + struct mutex index_lock; + + struct backlight_device *bdev; + + /* switcheroo data */ + acpi_handle dhandle; + int gpe; + bool external_switchable; + enum vga_switcheroo_client_id switch_state_display; + enum vga_switcheroo_client_id switch_state_ddc; + enum vga_switcheroo_client_id switch_state_external; + enum vga_switcheroo_state power_state; + struct completion powerchange_done; + + /* debugfs data */ + u8 selected_port; + struct dentry *debug_dentry; +}; + +static struct apple_gmux_data *apple_gmux_data; + +struct apple_gmux_config { + u8 (*read8)(struct apple_gmux_data *gmux_data, int port); + void (*write8)(struct apple_gmux_data *gmux_data, int port, u8 val); + u32 (*read32)(struct apple_gmux_data *gmux_data, int port); + void (*write32)(struct apple_gmux_data *gmux_data, int port, u32 val); + const struct vga_switcheroo_handler *gmux_handler; + enum vga_switcheroo_handler_flags_t handler_flags; + unsigned long resource_type; + bool read_version_as_u32; + char *name; +}; + +#define GMUX_INTERRUPT_ENABLE 0xff +#define GMUX_INTERRUPT_DISABLE 0x00 + +#define GMUX_INTERRUPT_STATUS_ACTIVE 0 +#define GMUX_INTERRUPT_STATUS_DISPLAY (1 << 0) +#define GMUX_INTERRUPT_STATUS_POWER (1 << 2) +#define GMUX_INTERRUPT_STATUS_HOTPLUG (1 << 3) + +#define GMUX_BRIGHTNESS_MASK 0x00ffffff +#define GMUX_MAX_BRIGHTNESS GMUX_BRIGHTNESS_MASK + +# define MMIO_GMUX_MAX_BRIGHTNESS 0xffff + +static u8 gmux_pio_read8(struct apple_gmux_data *gmux_data, int port) +{ + return inb(gmux_data->iostart + port); +} + +static void gmux_pio_write8(struct apple_gmux_data *gmux_data, int port, + u8 val) +{ + outb(val, gmux_data->iostart + port); +} + +static u32 gmux_pio_read32(struct apple_gmux_data *gmux_data, int port) +{ + return inl(gmux_data->iostart + port); +} + +static void gmux_pio_write32(struct apple_gmux_data *gmux_data, int port, + u32 val) +{ + int i; + u8 tmpval; + + for (i = 0; i < 4; i++) { + tmpval = (val >> (i * 8)) & 0xff; + outb(tmpval, gmux_data->iostart + port + i); + } +} + +static int gmux_index_wait_ready(struct apple_gmux_data *gmux_data) +{ + int i = 200; + u8 gwr = inb(gmux_data->iostart + GMUX_PORT_WRITE); + + while (i && (gwr & 0x01)) { + inb(gmux_data->iostart + GMUX_PORT_READ); + gwr = inb(gmux_data->iostart + GMUX_PORT_WRITE); + udelay(100); + i--; + } + + return !!i; +} + +static int gmux_index_wait_complete(struct apple_gmux_data *gmux_data) +{ + int i = 200; + u8 gwr = inb(gmux_data->iostart + GMUX_PORT_WRITE); + + while (i && !(gwr & 0x01)) { + gwr = inb(gmux_data->iostart + GMUX_PORT_WRITE); + udelay(100); + i--; + } + + if (gwr & 0x01) + inb(gmux_data->iostart + GMUX_PORT_READ); + + return !!i; +} + +static u8 gmux_index_read8(struct apple_gmux_data *gmux_data, int port) +{ + u8 val; + + mutex_lock(&gmux_data->index_lock); + gmux_index_wait_ready(gmux_data); + outb((port & 0xff), gmux_data->iostart + GMUX_PORT_READ); + gmux_index_wait_complete(gmux_data); + val = inb(gmux_data->iostart + GMUX_PORT_VALUE); + mutex_unlock(&gmux_data->index_lock); + + return val; +} + +static void gmux_index_write8(struct apple_gmux_data *gmux_data, int port, + u8 val) +{ + mutex_lock(&gmux_data->index_lock); + outb(val, gmux_data->iostart + GMUX_PORT_VALUE); + gmux_index_wait_ready(gmux_data); + outb(port & 0xff, gmux_data->iostart + GMUX_PORT_WRITE); + gmux_index_wait_complete(gmux_data); + mutex_unlock(&gmux_data->index_lock); +} + +static u32 gmux_index_read32(struct apple_gmux_data *gmux_data, int port) +{ + u32 val; + + mutex_lock(&gmux_data->index_lock); + gmux_index_wait_ready(gmux_data); + outb((port & 0xff), gmux_data->iostart + GMUX_PORT_READ); + gmux_index_wait_complete(gmux_data); + val = inl(gmux_data->iostart + GMUX_PORT_VALUE); + mutex_unlock(&gmux_data->index_lock); + + return val; +} + +static void gmux_index_write32(struct apple_gmux_data *gmux_data, int port, + u32 val) +{ + int i; + u8 tmpval; + + mutex_lock(&gmux_data->index_lock); + + for (i = 0; i < 4; i++) { + tmpval = (val >> (i * 8)) & 0xff; + outb(tmpval, gmux_data->iostart + GMUX_PORT_VALUE + i); + } + + gmux_index_wait_ready(gmux_data); + outb(port & 0xff, gmux_data->iostart + GMUX_PORT_WRITE); + gmux_index_wait_complete(gmux_data); + mutex_unlock(&gmux_data->index_lock); +} + +static int gmux_mmio_wait(struct apple_gmux_data *gmux_data) +{ + int i = 200; + u8 gwr = ioread8(gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + + while (i && gwr) { + gwr = ioread8(gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + udelay(100); + i--; + } + + return !!i; +} + +static u8 gmux_mmio_read8(struct apple_gmux_data *gmux_data, int port) +{ + u8 val; + + mutex_lock(&gmux_data->index_lock); + gmux_mmio_wait(gmux_data); + iowrite8((port & 0xff), gmux_data->iomem_base + GMUX_MMIO_PORT_SELECT); + iowrite8(GMUX_MMIO_READ | sizeof(val), + gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + gmux_mmio_wait(gmux_data); + val = ioread8(gmux_data->iomem_base); + mutex_unlock(&gmux_data->index_lock); + + return val; +} + +static void gmux_mmio_write8(struct apple_gmux_data *gmux_data, int port, + u8 val) +{ + mutex_lock(&gmux_data->index_lock); + gmux_mmio_wait(gmux_data); + iowrite8(val, gmux_data->iomem_base); + + iowrite8(port & 0xff, gmux_data->iomem_base + GMUX_MMIO_PORT_SELECT); + iowrite8(GMUX_MMIO_WRITE | sizeof(val), + gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + + gmux_mmio_wait(gmux_data); + mutex_unlock(&gmux_data->index_lock); +} + +static u32 gmux_mmio_read32(struct apple_gmux_data *gmux_data, int port) +{ + u32 val; + + mutex_lock(&gmux_data->index_lock); + gmux_mmio_wait(gmux_data); + iowrite8((port & 0xff), gmux_data->iomem_base + GMUX_MMIO_PORT_SELECT); + iowrite8(GMUX_MMIO_READ | sizeof(val), + gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + gmux_mmio_wait(gmux_data); + val = ioread32be(gmux_data->iomem_base); + mutex_unlock(&gmux_data->index_lock); + + return val; +} + +static void gmux_mmio_write32(struct apple_gmux_data *gmux_data, int port, + u32 val) +{ + mutex_lock(&gmux_data->index_lock); + iowrite32be(val, gmux_data->iomem_base); + iowrite8(port & 0xff, gmux_data->iomem_base + GMUX_MMIO_PORT_SELECT); + iowrite8(GMUX_MMIO_WRITE | sizeof(val), + gmux_data->iomem_base + GMUX_MMIO_COMMAND_SEND); + gmux_mmio_wait(gmux_data); + mutex_unlock(&gmux_data->index_lock); +} + +static u8 gmux_read8(struct apple_gmux_data *gmux_data, int port) +{ + return gmux_data->config->read8(gmux_data, port); +} + +static void gmux_write8(struct apple_gmux_data *gmux_data, int port, u8 val) +{ + return gmux_data->config->write8(gmux_data, port, val); +} + +static u32 gmux_read32(struct apple_gmux_data *gmux_data, int port) +{ + return gmux_data->config->read32(gmux_data, port); +} + +static void gmux_write32(struct apple_gmux_data *gmux_data, int port, + u32 val) +{ + return gmux_data->config->write32(gmux_data, port, val); +} + +/** + * DOC: Backlight control + * + * On single GPU MacBooks, the PWM signal for the backlight is generated by + * the GPU. On dual GPU MacBook Pros by contrast, either GPU may be suspended + * to conserve energy. Hence the PWM signal needs to be generated by a separate + * backlight driver which is controlled by gmux. The earliest generation + * MBP5 2008/09 uses a `TI LP8543`_ backlight driver. Newer models + * use a `TI LP8545`_ or a TI LP8548. + * + * .. _TI LP8543: https://www.ti.com/lit/ds/symlink/lp8543.pdf + * .. _TI LP8545: https://www.ti.com/lit/ds/symlink/lp8545.pdf + */ + +static int gmux_get_brightness(struct backlight_device *bd) +{ + struct apple_gmux_data *gmux_data = bl_get_data(bd); + return gmux_read32(gmux_data, GMUX_PORT_BRIGHTNESS) & + GMUX_BRIGHTNESS_MASK; +} + +static int gmux_update_status(struct backlight_device *bd) +{ + struct apple_gmux_data *gmux_data = bl_get_data(bd); + u32 brightness = backlight_get_brightness(bd); + + gmux_write32(gmux_data, GMUX_PORT_BRIGHTNESS, brightness); + + return 0; +} + +static const struct backlight_ops gmux_bl_ops = { + .options = BL_CORE_SUSPENDRESUME, + .get_brightness = gmux_get_brightness, + .update_status = gmux_update_status, +}; + +/** + * DOC: Graphics mux + * + * On pre-retinas, the LVDS outputs of both GPUs feed into gmux which muxes + * either of them to the panel. One of the tricks gmux has up its sleeve is + * to lengthen the blanking interval of its output during a switch to + * synchronize it with the GPU switched to. This allows for a flicker-free + * switch that is imperceptible by the user (`US 8,687,007 B2`_). + * + * On retinas, muxing is no longer done by gmux itself, but by a separate + * chip which is controlled by gmux. The chip is triple sourced, it is + * either an `NXP CBTL06142`_, `TI HD3SS212`_ or `Pericom PI3VDP12412`_. + * The panel is driven with eDP instead of LVDS since the pixel clock + * required for retina resolution exceeds LVDS' limits. + * + * Pre-retinas are able to switch the panel's DDC pins separately. + * This is handled by a `TI SN74LV4066A`_ which is controlled by gmux. + * The inactive GPU can thus probe the panel's EDID without switching over + * the entire panel. Retinas lack this functionality as the chips used for + * eDP muxing are incapable of switching the AUX channel separately (see + * the linked data sheets, Pericom would be capable but this is unused). + * However the retina panel has the NO_AUX_HANDSHAKE_LINK_TRAINING bit set + * in its DPCD, allowing the inactive GPU to skip the AUX handshake and + * set up the output with link parameters pre-calibrated by the active GPU. + * + * The external DP port is only fully switchable on the first two unibody + * MacBook Pro generations, MBP5 2008/09 and MBP6 2010. This is done by an + * `NXP CBTL06141`_ which is controlled by gmux. It's the predecessor of the + * eDP mux on retinas, the difference being support for 2.7 versus 5.4 Gbit/s. + * + * The following MacBook Pro generations replaced the external DP port with a + * combined DP/Thunderbolt port and lost the ability to switch it between GPUs, + * connecting it either to the discrete GPU or the Thunderbolt controller. + * Oddly enough, while the full port is no longer switchable, AUX and HPD + * are still switchable by way of an `NXP CBTL03062`_ (on pre-retinas + * MBP8 2011 and MBP9 2012) or two `TI TS3DS10224`_ (on pre-t2 retinas) under + * the control of gmux. Since the integrated GPU is missing the main link, + * external displays appear to it as phantoms which fail to link-train. + * + * gmux receives the HPD signal of all display connectors and sends an + * interrupt on hotplug. On generations which cannot switch external ports, + * the discrete GPU can then be woken to drive the newly connected display. + * The ability to switch AUX on these generations could be used to improve + * reliability of hotplug detection by having the integrated GPU poll the + * ports while the discrete GPU is asleep, but currently we do not make use + * of this feature. + * + * Our switching policy for the external port is that on those generations + * which are able to switch it fully, the port is switched together with the + * panel when IGD / DIS commands are issued to vga_switcheroo. It is thus + * possible to drive e.g. a beamer on battery power with the integrated GPU. + * The user may manually switch to the discrete GPU if more performance is + * needed. + * + * On all newer generations, the external port can only be driven by the + * discrete GPU. If a display is plugged in while the panel is switched to + * the integrated GPU, *both* GPUs will be in use for maximum performance. + * To decrease power consumption, the user may manually switch to the + * discrete GPU, thereby suspending the integrated GPU. + * + * gmux' initial switch state on bootup is user configurable via the EFI + * variable ``gpu-power-prefs-fa4ce28d-b62f-4c99-9cc3-6815686e30f9`` (5th byte, + * 1 = IGD, 0 = DIS). Based on this setting, the EFI firmware tells gmux to + * switch the panel and the external DP connector and allocates a framebuffer + * for the selected GPU. + * + * .. _US 8,687,007 B2: https://pimg-fpiw.uspto.gov/fdd/07/870/086/0.pdf + * .. _NXP CBTL06141: https://www.nxp.com/documents/data_sheet/CBTL06141.pdf + * .. _NXP CBTL06142: https://www.nxp.com/documents/data_sheet/CBTL06141.pdf + * .. _TI HD3SS212: https://www.ti.com/lit/ds/symlink/hd3ss212.pdf + * .. _Pericom PI3VDP12412: https://www.pericom.com/assets/Datasheets/PI3VDP12412.pdf + * .. _TI SN74LV4066A: https://www.ti.com/lit/ds/symlink/sn74lv4066a.pdf + * .. _NXP CBTL03062: http://pdf.datasheetarchive.com/indexerfiles/Datasheets-SW16/DSASW00308511.pdf + * .. _TI TS3DS10224: https://www.ti.com/lit/ds/symlink/ts3ds10224.pdf + */ + +static void gmux_read_switch_state(struct apple_gmux_data *gmux_data) +{ + if (gmux_read8(gmux_data, GMUX_PORT_SWITCH_DDC) == 1) + gmux_data->switch_state_ddc = VGA_SWITCHEROO_IGD; + else + gmux_data->switch_state_ddc = VGA_SWITCHEROO_DIS; + + if (gmux_read8(gmux_data, GMUX_PORT_SWITCH_DISPLAY) & 1) + gmux_data->switch_state_display = VGA_SWITCHEROO_DIS; + else + gmux_data->switch_state_display = VGA_SWITCHEROO_IGD; + + if (gmux_read8(gmux_data, GMUX_PORT_SWITCH_EXTERNAL) == 2) + gmux_data->switch_state_external = VGA_SWITCHEROO_IGD; + else + gmux_data->switch_state_external = VGA_SWITCHEROO_DIS; +} + +static void gmux_write_switch_state(struct apple_gmux_data *gmux_data) +{ + if (gmux_data->switch_state_ddc == VGA_SWITCHEROO_IGD) + gmux_write8(gmux_data, GMUX_PORT_SWITCH_DDC, 1); + else + gmux_write8(gmux_data, GMUX_PORT_SWITCH_DDC, 2); + + if (gmux_data->switch_state_display == VGA_SWITCHEROO_IGD) + gmux_write8(gmux_data, GMUX_PORT_SWITCH_DISPLAY, 2); + else + gmux_write8(gmux_data, GMUX_PORT_SWITCH_DISPLAY, 3); + + if (gmux_data->switch_state_external == VGA_SWITCHEROO_IGD) + gmux_write8(gmux_data, GMUX_PORT_SWITCH_EXTERNAL, 2); + else + gmux_write8(gmux_data, GMUX_PORT_SWITCH_EXTERNAL, 3); +} + +static int gmux_switchto(enum vga_switcheroo_client_id id) +{ + apple_gmux_data->switch_state_ddc = id; + apple_gmux_data->switch_state_display = id; + if (apple_gmux_data->external_switchable) + apple_gmux_data->switch_state_external = id; + + gmux_write_switch_state(apple_gmux_data); + + return 0; +} + +static int gmux_switch_ddc(enum vga_switcheroo_client_id id) +{ + enum vga_switcheroo_client_id old_ddc_owner = + apple_gmux_data->switch_state_ddc; + + if (id == old_ddc_owner) + return id; + + pr_debug("Switching DDC from %d to %d\n", old_ddc_owner, id); + apple_gmux_data->switch_state_ddc = id; + + if (id == VGA_SWITCHEROO_IGD) + gmux_write8(apple_gmux_data, GMUX_PORT_SWITCH_DDC, 1); + else + gmux_write8(apple_gmux_data, GMUX_PORT_SWITCH_DDC, 2); + + return old_ddc_owner; +} + +/** + * DOC: Power control + * + * gmux is able to cut power to the discrete GPU. It automatically takes care + * of the correct sequence to tear down and bring up the power rails for + * core voltage, VRAM and PCIe. + */ + +static int gmux_set_discrete_state(struct apple_gmux_data *gmux_data, + enum vga_switcheroo_state state) +{ + reinit_completion(&gmux_data->powerchange_done); + + if (state == VGA_SWITCHEROO_ON) { + gmux_write8(gmux_data, GMUX_PORT_DISCRETE_POWER, 1); + gmux_write8(gmux_data, GMUX_PORT_DISCRETE_POWER, 3); + pr_debug("Discrete card powered up\n"); + } else { + gmux_write8(gmux_data, GMUX_PORT_DISCRETE_POWER, 1); + gmux_write8(gmux_data, GMUX_PORT_DISCRETE_POWER, 0); + pr_debug("Discrete card powered down\n"); + } + + gmux_data->power_state = state; + + if (gmux_data->gpe >= 0 && + !wait_for_completion_interruptible_timeout(&gmux_data->powerchange_done, + msecs_to_jiffies(200))) + pr_warn("Timeout waiting for gmux switch to complete\n"); + + return 0; +} + +static int gmux_set_power_state(enum vga_switcheroo_client_id id, + enum vga_switcheroo_state state) +{ + if (id == VGA_SWITCHEROO_IGD) + return 0; + + return gmux_set_discrete_state(apple_gmux_data, state); +} + +static enum vga_switcheroo_client_id gmux_get_client_id(struct pci_dev *pdev) +{ + /* + * Early Macbook Pros with switchable graphics use nvidia + * integrated graphics. Hardcode that the 9400M is integrated. + */ + if (pdev->vendor == PCI_VENDOR_ID_INTEL) + return VGA_SWITCHEROO_IGD; + else if (pdev->vendor == PCI_VENDOR_ID_NVIDIA && + pdev->device == 0x0863) + return VGA_SWITCHEROO_IGD; + else + return VGA_SWITCHEROO_DIS; +} + +static const struct vga_switcheroo_handler gmux_handler_no_ddc = { + .switchto = gmux_switchto, + .power_state = gmux_set_power_state, + .get_client_id = gmux_get_client_id, +}; + +static const struct vga_switcheroo_handler gmux_handler_ddc = { + .switchto = gmux_switchto, + .switch_ddc = gmux_switch_ddc, + .power_state = gmux_set_power_state, + .get_client_id = gmux_get_client_id, +}; + +static const struct apple_gmux_config apple_gmux_pio = { + .read8 = &gmux_pio_read8, + .write8 = &gmux_pio_write8, + .read32 = &gmux_pio_read32, + .write32 = &gmux_pio_write32, + .gmux_handler = &gmux_handler_ddc, + .handler_flags = VGA_SWITCHEROO_CAN_SWITCH_DDC, + .resource_type = IORESOURCE_IO, + .read_version_as_u32 = false, + .name = "classic" +}; + +static const struct apple_gmux_config apple_gmux_index = { + .read8 = &gmux_index_read8, + .write8 = &gmux_index_write8, + .read32 = &gmux_index_read32, + .write32 = &gmux_index_write32, + .gmux_handler = &gmux_handler_no_ddc, + .handler_flags = VGA_SWITCHEROO_NEEDS_EDP_CONFIG, + .resource_type = IORESOURCE_IO, + .read_version_as_u32 = true, + .name = "indexed" +}; + +static const struct apple_gmux_config apple_gmux_mmio = { + .read8 = &gmux_mmio_read8, + .write8 = &gmux_mmio_write8, + .read32 = &gmux_mmio_read32, + .write32 = &gmux_mmio_write32, + .gmux_handler = &gmux_handler_no_ddc, + .handler_flags = VGA_SWITCHEROO_NEEDS_EDP_CONFIG, + .resource_type = IORESOURCE_MEM, + .read_version_as_u32 = true, + .name = "T2" +}; + + +/** + * DOC: Interrupt + * + * gmux is also connected to a GPIO pin of the southbridge and thereby is able + * to trigger an ACPI GPE. ACPI name GMGP holds this GPIO pin's number. On the + * MBP5 2008/09 it's GPIO pin 22 of the Nvidia MCP79, on following generations + * it's GPIO pin 6 of the Intel PCH, on MMIO gmux's it's pin 21. + * + * The GPE merely signals that an interrupt occurred, the actual type of event + * is identified by reading a gmux register. + * + * In addition to the GMGP name, gmux's ACPI device also has two methods GMSP + * and GMLV. GMLV likely means "GMUX Level", and reads the value of the GPIO, + * while GMSP likely means "GMUX Set Polarity", and seems to write to the GPIO's + * value. On newer Macbooks (This was introduced with or sometime before the + * MacBookPro14,3), the ACPI GPE method differentiates between the OS type: On + * Darwin, only a notification is signaled, whereas on other OSes, the GPIO's + * value is read and then inverted. + * + * Because Linux masquerades as Darwin, it ends up in the notification-only code + * path. On MMIO gmux's, this seems to lead to us being unable to clear interrupts, + * unless we call GMSP(0). Without this, there is a flood of status=0 interrupts + * that can't be cleared. This issue seems to be unique to MMIO gmux's. + */ + +static inline void gmux_disable_interrupts(struct apple_gmux_data *gmux_data) +{ + gmux_write8(gmux_data, GMUX_PORT_INTERRUPT_ENABLE, + GMUX_INTERRUPT_DISABLE); +} + +static inline void gmux_enable_interrupts(struct apple_gmux_data *gmux_data) +{ + gmux_write8(gmux_data, GMUX_PORT_INTERRUPT_ENABLE, + GMUX_INTERRUPT_ENABLE); +} + +static inline u8 gmux_interrupt_get_status(struct apple_gmux_data *gmux_data) +{ + return gmux_read8(gmux_data, GMUX_PORT_INTERRUPT_STATUS); +} + +static void gmux_clear_interrupts(struct apple_gmux_data *gmux_data) +{ + u8 status; + + /* to clear interrupts write back current status */ + status = gmux_interrupt_get_status(gmux_data); + gmux_write8(gmux_data, GMUX_PORT_INTERRUPT_STATUS, status); + /* Prevent flood of status=0 interrupts */ + if (gmux_data->config == &apple_gmux_mmio) + acpi_execute_simple_method(gmux_data->dhandle, "GMSP", 0); +} + +static void gmux_notify_handler(acpi_handle device, u32 value, void *context) +{ + u8 status; + struct pnp_dev *pnp = (struct pnp_dev *)context; + struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp); + + status = gmux_interrupt_get_status(gmux_data); + gmux_disable_interrupts(gmux_data); + pr_debug("Notify handler called: status %d\n", status); + + gmux_clear_interrupts(gmux_data); + gmux_enable_interrupts(gmux_data); + + if (status & GMUX_INTERRUPT_STATUS_POWER) + complete(&gmux_data->powerchange_done); +} + +/** + * DOC: Debugfs Interface + * + * gmux ports can be accessed from userspace as a debugfs interface. For example: + * + * # echo 4 > /sys/kernel/debug/apple_gmux/selected_port + * # cat /sys/kernel/debug/apple_gmux/selected_port_data | xxd -p + * 00000005 + * + * Reads 4 bytes from port 4 (GMUX_PORT_VERSION_MAJOR). + * + * 1 and 4 byte writes are also allowed. + */ + +static ssize_t gmux_selected_port_data_write(struct file *file, + const char __user *userbuf, size_t count, loff_t *ppos) +{ + struct apple_gmux_data *gmux_data = file->private_data; + + if (*ppos) + return -EINVAL; + + if (count == 1) { + u8 data; + + if (copy_from_user(&data, userbuf, 1)) + return -EFAULT; + + gmux_write8(gmux_data, gmux_data->selected_port, data); + } else if (count == 4) { + u32 data; + + if (copy_from_user(&data, userbuf, 4)) + return -EFAULT; + + gmux_write32(gmux_data, gmux_data->selected_port, data); + } else + return -EINVAL; + + return count; +} + +static ssize_t gmux_selected_port_data_read(struct file *file, + char __user *userbuf, size_t count, loff_t *ppos) +{ + struct apple_gmux_data *gmux_data = file->private_data; + u32 data; + + data = gmux_read32(gmux_data, gmux_data->selected_port); + + return simple_read_from_buffer(userbuf, count, ppos, &data, sizeof(data)); +} + +static const struct file_operations gmux_port_data_ops = { + .open = simple_open, + .write = gmux_selected_port_data_write, + .read = gmux_selected_port_data_read +}; + +static void gmux_init_debugfs(struct apple_gmux_data *gmux_data) +{ + gmux_data->debug_dentry = debugfs_create_dir(KBUILD_MODNAME, NULL); + + debugfs_create_u8("selected_port", 0644, gmux_data->debug_dentry, + &gmux_data->selected_port); + debugfs_create_file("selected_port_data", 0644, gmux_data->debug_dentry, + gmux_data, &gmux_port_data_ops); +} + +static void gmux_fini_debugfs(struct apple_gmux_data *gmux_data) +{ + debugfs_remove_recursive(gmux_data->debug_dentry); +} + +static int gmux_suspend(struct device *dev) +{ + struct pnp_dev *pnp = to_pnp_dev(dev); + struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp); + + gmux_disable_interrupts(gmux_data); + return 0; +} + +static int gmux_resume(struct device *dev) +{ + struct pnp_dev *pnp = to_pnp_dev(dev); + struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp); + + gmux_enable_interrupts(gmux_data); + gmux_write_switch_state(gmux_data); + if (gmux_data->power_state == VGA_SWITCHEROO_OFF) + gmux_set_discrete_state(gmux_data, gmux_data->power_state); + return 0; +} + +static int is_thunderbolt(struct device *dev, void *data) +{ + return to_pci_dev(dev)->is_thunderbolt; +} + +static int gmux_probe(struct pnp_dev *pnp, const struct pnp_device_id *id) +{ + struct apple_gmux_data *gmux_data; + struct resource *res; + struct backlight_properties props; + struct backlight_device *bdev = NULL; + u8 ver_major, ver_minor, ver_release; + bool register_bdev = true; + int ret = -ENXIO; + acpi_status status; + unsigned long long gpe; + enum apple_gmux_type type; + u32 version; + + if (apple_gmux_data) + return -EBUSY; + + if (!apple_gmux_detect(pnp, &type)) { + pr_info("gmux device not present\n"); + return -ENODEV; + } + + gmux_data = kzalloc_obj(*gmux_data); + if (!gmux_data) + return -ENOMEM; + pnp_set_drvdata(pnp, gmux_data); + + switch (type) { + case APPLE_GMUX_TYPE_MMIO: + gmux_data->config = &apple_gmux_mmio; + mutex_init(&gmux_data->index_lock); + + res = pnp_get_resource(pnp, IORESOURCE_MEM, 0); + gmux_data->iostart = res->start; + /* Although the ACPI table only allocates 8 bytes, we need 16. */ + gmux_data->iolen = 16; + if (!request_mem_region(gmux_data->iostart, gmux_data->iolen, + "Apple gmux")) { + pr_err("gmux I/O already in use\n"); + goto err_free; + } + gmux_data->iomem_base = ioremap(gmux_data->iostart, gmux_data->iolen); + if (!gmux_data->iomem_base) { + pr_err("couldn't remap gmux mmio region"); + goto err_release; + } + goto get_version; + case APPLE_GMUX_TYPE_INDEXED: + gmux_data->config = &apple_gmux_index; + mutex_init(&gmux_data->index_lock); + break; + case APPLE_GMUX_TYPE_PIO: + gmux_data->config = &apple_gmux_pio; + break; + } + + res = pnp_get_resource(pnp, IORESOURCE_IO, 0); + gmux_data->iostart = res->start; + gmux_data->iolen = resource_size(res); + + if (!request_region(gmux_data->iostart, gmux_data->iolen, + "Apple gmux")) { + pr_err("gmux I/O already in use\n"); + goto err_free; + } + +get_version: + if (gmux_data->config->read_version_as_u32) { + version = gmux_read32(gmux_data, GMUX_PORT_VERSION_MAJOR); + ver_major = (version >> 24) & 0xff; + ver_minor = (version >> 16) & 0xff; + ver_release = (version >> 8) & 0xff; + } else { + ver_major = gmux_read8(gmux_data, GMUX_PORT_VERSION_MAJOR); + ver_minor = gmux_read8(gmux_data, GMUX_PORT_VERSION_MINOR); + ver_release = gmux_read8(gmux_data, GMUX_PORT_VERSION_RELEASE); + } + pr_info("Found gmux version %d.%d.%d [%s]\n", ver_major, ver_minor, + ver_release, gmux_data->config->name); + + memset(&props, 0, sizeof(props)); + props.type = BACKLIGHT_PLATFORM; + + /* + * All MMIO gmux's have 0xffff as max brightness, but some iMacs incorrectly + * report 0x03ff, despite the firmware being happy to set 0xffff as the brightness + * at boot. Force 0xffff for all MMIO gmux's so they all have the correct brightness + * range. + */ + if (type == APPLE_GMUX_TYPE_MMIO) + props.max_brightness = MMIO_GMUX_MAX_BRIGHTNESS; + else + props.max_brightness = gmux_read32(gmux_data, GMUX_PORT_MAX_BRIGHTNESS); + +#if IS_REACHABLE(CONFIG_ACPI_VIDEO) + register_bdev = acpi_video_get_backlight_type() == acpi_backlight_apple_gmux; +#endif + if (register_bdev) { + /* + * Currently it's assumed that the maximum brightness is less than + * 2^24 for compatibility with old gmux versions. Cap the max + * brightness at this value, but print a warning if the hardware + * reports something higher so that it can be fixed. + */ + if (WARN_ON(props.max_brightness > GMUX_MAX_BRIGHTNESS)) + props.max_brightness = GMUX_MAX_BRIGHTNESS; + + bdev = backlight_device_register("gmux_backlight", &pnp->dev, + gmux_data, &gmux_bl_ops, &props); + if (IS_ERR(bdev)) { + ret = PTR_ERR(bdev); + goto err_unmap; + } + + gmux_data->bdev = bdev; + bdev->props.brightness = gmux_get_brightness(bdev); + backlight_update_status(bdev); + } + + gmux_data->power_state = VGA_SWITCHEROO_ON; + + gmux_data->dhandle = ACPI_HANDLE(&pnp->dev); + if (!gmux_data->dhandle) { + pr_err("Cannot find acpi handle for pnp device %s\n", + dev_name(&pnp->dev)); + ret = -ENODEV; + goto err_notify; + } + + status = acpi_evaluate_integer(gmux_data->dhandle, "GMGP", NULL, &gpe); + if (ACPI_SUCCESS(status)) { + gmux_data->gpe = (int)gpe; + + status = acpi_install_notify_handler(gmux_data->dhandle, + ACPI_DEVICE_NOTIFY, + &gmux_notify_handler, pnp); + if (ACPI_FAILURE(status)) { + pr_err("Install notify handler failed: %s\n", + acpi_format_exception(status)); + ret = -ENODEV; + goto err_notify; + } + + status = acpi_enable_gpe(NULL, gmux_data->gpe); + if (ACPI_FAILURE(status)) { + pr_err("Cannot enable gpe: %s\n", + acpi_format_exception(status)); + goto err_enable_gpe; + } + } else { + pr_warn("No GPE found for gmux\n"); + gmux_data->gpe = -1; + } + + /* + * If Thunderbolt is present, the external DP port is not fully + * switchable. Force its AUX channel to the discrete GPU. + */ + gmux_data->external_switchable = + !bus_for_each_dev(&pci_bus_type, NULL, NULL, is_thunderbolt); + if (!gmux_data->external_switchable) + gmux_write8(gmux_data, GMUX_PORT_SWITCH_EXTERNAL, 3); + + apple_gmux_data = gmux_data; + init_completion(&gmux_data->powerchange_done); + gmux_enable_interrupts(gmux_data); + gmux_read_switch_state(gmux_data); + + /* + * Retina MacBook Pros cannot switch the panel's AUX separately + * and need eDP pre-calibration. They are distinguishable from + * pre-retinas by having an "indexed" or "T2" gmux. + * + * Pre-retina MacBook Pros can switch the panel's DDC separately. + */ + ret = vga_switcheroo_register_handler(gmux_data->config->gmux_handler, + gmux_data->config->handler_flags); + if (ret) { + pr_err("Failed to register vga_switcheroo handler\n"); + goto err_register_handler; + } + + gmux_init_debugfs(gmux_data); + return 0; + +err_register_handler: + gmux_disable_interrupts(gmux_data); + apple_gmux_data = NULL; + if (gmux_data->gpe >= 0) + acpi_disable_gpe(NULL, gmux_data->gpe); +err_enable_gpe: + if (gmux_data->gpe >= 0) + acpi_remove_notify_handler(gmux_data->dhandle, + ACPI_DEVICE_NOTIFY, + &gmux_notify_handler); +err_notify: + backlight_device_unregister(bdev); +err_unmap: + if (gmux_data->iomem_base) + iounmap(gmux_data->iomem_base); +err_release: + if (gmux_data->config->resource_type == IORESOURCE_MEM) + release_mem_region(gmux_data->iostart, gmux_data->iolen); + else + release_region(gmux_data->iostart, gmux_data->iolen); +err_free: + kfree(gmux_data); + return ret; +} + +static void gmux_remove(struct pnp_dev *pnp) +{ + struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp); + + gmux_fini_debugfs(gmux_data); + vga_switcheroo_unregister_handler(); + gmux_disable_interrupts(gmux_data); + if (gmux_data->gpe >= 0) { + acpi_disable_gpe(NULL, gmux_data->gpe); + acpi_remove_notify_handler(gmux_data->dhandle, + ACPI_DEVICE_NOTIFY, + &gmux_notify_handler); + } + + backlight_device_unregister(gmux_data->bdev); + + if (gmux_data->iomem_base) { + iounmap(gmux_data->iomem_base); + release_mem_region(gmux_data->iostart, gmux_data->iolen); + } else + release_region(gmux_data->iostart, gmux_data->iolen); + apple_gmux_data = NULL; + kfree(gmux_data); +} + +static const struct pnp_device_id gmux_device_ids[] = { + { .id = GMUX_ACPI_HID }, + { } +}; + +static const struct dev_pm_ops gmux_dev_pm_ops = { + .suspend = gmux_suspend, + .resume = gmux_resume, +}; + +static struct pnp_driver gmux_pnp_driver = { + .name = "apple-gmux", + .probe = gmux_probe, + .remove = gmux_remove, + .id_table = gmux_device_ids, + .driver = { + .pm = &gmux_dev_pm_ops, + }, +}; + +module_pnp_driver(gmux_pnp_driver); +MODULE_AUTHOR("Seth Forshee "); +MODULE_DESCRIPTION("Apple Gmux Driver"); +MODULE_LICENSE("GPL"); +MODULE_DEVICE_TABLE(pnp, gmux_device_ids); diff --git a/drivers/platform/x86/asus-armoury.c b/drivers/platform/x86/asus-armoury.c new file mode 100644 index 000000000..495dc1e31 --- /dev/null +++ b/drivers/platform/x86/asus-armoury.c @@ -0,0 +1,1167 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Asus Armoury (WMI) attributes driver. + * + * This driver uses the fw_attributes class to expose various WMI functions + * that are present in many gaming and some non-gaming ASUS laptops. + * + * These typically don't fit anywhere else in the sysfs such as under LED class, + * hwmon or others, and are set in Windows using the ASUS Armoury Crate tool. + * + * Copyright(C) 2024 Luke Jones + */ + +#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 "asus-armoury.h" +#include "firmware_attributes_class.h" + +#define ASUS_NB_WMI_EVENT_GUID "0B3CBB35-E3C2-45ED-91C2-4C5A6D195D1C" + +#define ASUS_MINI_LED_MODE_MASK GENMASK(1, 0) +/* Standard modes for devices with only on/off */ +#define ASUS_MINI_LED_OFF 0x00 +#define ASUS_MINI_LED_ON 0x01 +/* Like "on" but the effect is more vibrant or brighter */ +#define ASUS_MINI_LED_STRONG_MODE 0x02 +/* New modes for devices with 3 mini-led mode types */ +#define ASUS_MINI_LED_2024_WEAK 0x00 +#define ASUS_MINI_LED_2024_STRONG 0x01 +#define ASUS_MINI_LED_2024_OFF 0x02 + +/* Power tunable attribute name defines */ +#define ATTR_PPT_PL1_SPL "ppt_pl1_spl" +#define ATTR_PPT_PL2_SPPT "ppt_pl2_sppt" +#define ATTR_PPT_PL3_FPPT "ppt_pl3_fppt" +#define ATTR_PPT_APU_SPPT "ppt_apu_sppt" +#define ATTR_PPT_PLATFORM_SPPT "ppt_platform_sppt" +#define ATTR_NV_DYNAMIC_BOOST "nv_dynamic_boost" +#define ATTR_NV_TEMP_TARGET "nv_temp_target" +#define ATTR_NV_BASE_TGP "nv_base_tgp" +#define ATTR_NV_TGP "nv_tgp" + +#define ASUS_ROG_TUNABLE_DC 0 +#define ASUS_ROG_TUNABLE_AC 1 + +struct rog_tunables { + const struct power_limits *power_limits; + u32 ppt_pl1_spl; // cpu + u32 ppt_pl2_sppt; // cpu + u32 ppt_pl3_fppt; // cpu + u32 ppt_apu_sppt; // plat + u32 ppt_platform_sppt; // plat + + u32 nv_dynamic_boost; + u32 nv_temp_target; + u32 nv_tgp; +}; + +struct asus_armoury_priv { + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + + /* + * Mutex to protect eGPU activation/deactivation + * sequences and dGPU connection status: + * do not allow concurrent changes or changes + * before a reboot if dGPU got disabled. + */ + struct mutex egpu_mutex; + + /* Index 0 for DC, 1 for AC */ + struct rog_tunables *rog_tunables[2]; + + u32 mini_led_dev_id; + u32 gpu_mux_dev_id; +}; + +static struct asus_armoury_priv asus_armoury = { + .egpu_mutex = __MUTEX_INITIALIZER(asus_armoury.egpu_mutex), +}; + +struct fw_attrs_group { + bool pending_reboot; +}; + +static struct fw_attrs_group fw_attrs = { + .pending_reboot = false, +}; + +struct asus_attr_group { + const struct attribute_group *attr_group; + u32 wmi_devid; +}; + +static void asus_set_reboot_and_signal_event(void) +{ + fw_attrs.pending_reboot = true; + kobject_uevent(&asus_armoury.fw_attr_dev->kobj, KOBJ_CHANGE); +} + +static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%d\n", fw_attrs.pending_reboot); +} + +static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); + +static bool asus_bios_requires_reboot(struct kobj_attribute *attr) +{ + return !strcmp(attr->attr.name, "gpu_mux_mode") || + !strcmp(attr->attr.name, "panel_hd_mode"); +} + +/** + * armoury_has_devstate() - Check presence of the WMI function state. + * + * @dev_id: The WMI method ID to check for presence. + * + * Returns: true iif method is supported. + */ +static bool armoury_has_devstate(u32 dev_id) +{ + u32 retval; + int status; + + status = asus_wmi_evaluate_method(ASUS_WMI_METHODID_DSTS, dev_id, 0, &retval); + pr_debug("%s called (0x%08x), retval: 0x%08x\n", __func__, dev_id, retval); + + return status == 0 && (retval & ASUS_WMI_DSTS_PRESENCE_BIT); +} + +/** + * armoury_get_devstate() - Get the WMI function state. + * @attr: NULL or the kobj_attribute associated to called WMI function. + * @dev_id: The WMI method ID to call. + * @retval: + * * non-NULL pointer to where to store the value returned from WMI + * * with the function presence bit cleared. + * + * Intended usage is from sysfs attribute checking associated WMI function. + * + * Returns: + * * %-ENODEV - method ID is unsupported. + * * %0 - successful and retval is filled. + * * %other - error from WMI call. + */ +static int armoury_get_devstate(struct kobj_attribute *attr, u32 *retval, u32 dev_id) +{ + int err; + + err = asus_wmi_get_devstate_dsts(dev_id, retval); + if (err) { + if (attr) + pr_err("Failed to get %s: %d\n", attr->attr.name, err); + else + pr_err("Failed to get devstate for 0x%x: %d\n", dev_id, err); + + return err; + } + + /* + * asus_wmi_get_devstate_dsts will populate retval with WMI return, but + * the true value is expressed when ASUS_WMI_DSTS_PRESENCE_BIT is clear. + */ + *retval &= ~ASUS_WMI_DSTS_PRESENCE_BIT; + + return 0; +} + +/** + * armoury_set_devstate() - Set the WMI function state. + * @attr: The kobj_attribute associated to called WMI function. + * @dev_id: The WMI method ID to call. + * @value: The new value to be set. + * @retval: Where to store the value returned from WMI or NULL. + * + * Intended usage is from sysfs attribute setting associated WMI function. + * Before calling the presence of the function should be checked. + * + * Every WMI write MUST go through this function to enforce safety checks. + * + * Results !1 is usually considered a fail by ASUS, but some WMI methods + * (like eGPU or CPU cores) do use > 1 to return a status code or similar: + * in these cases caller is interested in the actual return value + * and should perform relevant checks. + * + * Returns: + * * %-EINVAL - attempt to set a dangerous or unsupported value. + * * %-EIO - WMI function returned an error. + * * %0 - successful and retval is filled. + * * %other - error from WMI call. + */ +static int armoury_set_devstate(struct kobj_attribute *attr, + u32 value, u32 *retval, u32 dev_id) +{ + u32 result; + int err; + + /* + * Prevent developers from bricking devices or issuing dangerous + * commands that can be difficult or impossible to recover from. + */ + switch (dev_id) { + case ASUS_WMI_DEVID_APU_MEM: + /* + * A hard reset might suffice to save the device, + * but there is no value in sending these commands. + */ + if (value == 0x100 || value == 0x101) { + pr_err("Refusing to set APU memory to unsafe value: 0x%x\n", value); + return -EINVAL; + } + break; + default: + /* No problems are known for this dev_id */ + break; + } + + err = asus_wmi_set_devstate(dev_id, value, retval ? retval : &result); + if (err) { + if (attr) + pr_err("Failed to set %s: %d\n", attr->attr.name, err); + else + pr_err("Failed to set devstate for 0x%x: %d\n", dev_id, err); + + return err; + } + + /* + * If retval == NULL caller is uninterested in return value: + * perform the most common result check here. + */ + if ((retval == NULL) && (result == 0)) { + pr_err("Failed to set %s: (result): 0x%x\n", attr->attr.name, result); + return -EIO; + } + + return 0; +} + +static int armoury_attr_enum_list(char *buf, size_t enum_values) +{ + size_t i; + int len = 0; + + for (i = 0; i < enum_values; i++) { + if (i == 0) + len += sysfs_emit_at(buf, len, "%zu", i); + else + len += sysfs_emit_at(buf, len, ";%zu", i); + } + len += sysfs_emit_at(buf, len, "\n"); + + return len; +} + +ssize_t armoury_attr_uint_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count, u32 min, u32 max, + u32 *store_value, u32 wmi_dev) +{ + u32 value; + int err; + + err = kstrtou32(buf, 10, &value); + if (err) + return err; + + if (value < min || value > max) + return -EINVAL; + + err = armoury_set_devstate(attr, value, NULL, wmi_dev); + if (err) + return err; + + if (store_value != NULL) + *store_value = value; + sysfs_notify(kobj, NULL, attr->attr.name); + + if (asus_bios_requires_reboot(attr)) + asus_set_reboot_and_signal_event(); + + return count; +} + +ssize_t armoury_attr_uint_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf, u32 wmi_dev) +{ + u32 result; + int err; + + err = armoury_get_devstate(attr, &result, wmi_dev); + if (err) + return err; + + return sysfs_emit(buf, "%u\n", result); +} + +static ssize_t enum_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "enumeration\n"); +} + +static ssize_t int_type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +/* Mini-LED mode **************************************************************/ + +/* Values map for mini-led modes on 2023 and earlier models. */ +static u32 mini_led_mode1_map[] = { + [0] = ASUS_MINI_LED_OFF, + [1] = ASUS_MINI_LED_ON, +}; + +/* Values map for mini-led modes on 2024 and later models. */ +static u32 mini_led_mode2_map[] = { + [0] = ASUS_MINI_LED_2024_OFF, + [1] = ASUS_MINI_LED_2024_WEAK, + [2] = ASUS_MINI_LED_2024_STRONG, +}; + +static ssize_t mini_led_mode_current_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + u32 *mini_led_mode_map; + size_t mini_led_mode_map_size; + u32 i, mode; + int err; + + switch (asus_armoury.mini_led_dev_id) { + case ASUS_WMI_DEVID_MINI_LED_MODE: + mini_led_mode_map = mini_led_mode1_map; + mini_led_mode_map_size = ARRAY_SIZE(mini_led_mode1_map); + break; + + case ASUS_WMI_DEVID_MINI_LED_MODE2: + mini_led_mode_map = mini_led_mode2_map; + mini_led_mode_map_size = ARRAY_SIZE(mini_led_mode2_map); + break; + + default: + pr_err("Unrecognized mini-LED device: %u\n", asus_armoury.mini_led_dev_id); + return -ENODEV; + } + + err = armoury_get_devstate(attr, &mode, asus_armoury.mini_led_dev_id); + if (err) + return err; + + mode = FIELD_GET(ASUS_MINI_LED_MODE_MASK, mode); + + for (i = 0; i < mini_led_mode_map_size; i++) + if (mode == mini_led_mode_map[i]) + return sysfs_emit(buf, "%u\n", i); + + pr_warn("Unrecognized mini-LED mode: %u", mode); + return -EINVAL; +} + +static ssize_t mini_led_mode_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + u32 *mini_led_mode_map; + size_t mini_led_mode_map_size; + char mapped_value[12]; + u32 mode; + int err; + + err = kstrtou32(buf, 10, &mode); + if (err) + return err; + + switch (asus_armoury.mini_led_dev_id) { + case ASUS_WMI_DEVID_MINI_LED_MODE: + mini_led_mode_map = mini_led_mode1_map; + mini_led_mode_map_size = ARRAY_SIZE(mini_led_mode1_map); + break; + + case ASUS_WMI_DEVID_MINI_LED_MODE2: + mini_led_mode_map = mini_led_mode2_map; + mini_led_mode_map_size = ARRAY_SIZE(mini_led_mode2_map); + break; + + default: + pr_err("Unrecognized mini-LED devid: %u\n", asus_armoury.mini_led_dev_id); + return -EINVAL; + } + + if (mode >= mini_led_mode_map_size) { + pr_warn("mini-LED mode unrecognized device: %u\n", mode); + return -ENODEV; + } + + /* + * armoury_attr_uint_store() parses and sends the value from the + * passed buffer; hand it the mapped firmware value so the device + * receives the translated mode instead of the raw index. + */ + snprintf(mapped_value, sizeof(mapped_value), "%u", mini_led_mode_map[mode]); + + return armoury_attr_uint_store(kobj, attr, mapped_value, count, 0, + mini_led_mode_map[mode], NULL, + asus_armoury.mini_led_dev_id); +} + +static ssize_t mini_led_mode_possible_values_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + switch (asus_armoury.mini_led_dev_id) { + case ASUS_WMI_DEVID_MINI_LED_MODE: + return armoury_attr_enum_list(buf, ARRAY_SIZE(mini_led_mode1_map)); + case ASUS_WMI_DEVID_MINI_LED_MODE2: + return armoury_attr_enum_list(buf, ARRAY_SIZE(mini_led_mode2_map)); + default: + return -ENODEV; + } +} +ASUS_ATTR_GROUP_ENUM(mini_led_mode, "mini_led_mode", "Set the mini-LED backlight mode"); + +static ssize_t gpu_mux_mode_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + bool optimus; + + err = kstrtobool(buf, &optimus); + if (err) + return err; + + if (armoury_has_devstate(ASUS_WMI_DEVID_DGPU)) { + err = armoury_get_devstate(NULL, &result, ASUS_WMI_DEVID_DGPU); + if (err) + return err; + if (result && !optimus) { + pr_warn("Cannot switch MUX to dGPU mode when dGPU is disabled: %02X\n", + result); + return -ENODEV; + } + } + + if (armoury_has_devstate(ASUS_WMI_DEVID_EGPU)) { + err = armoury_get_devstate(NULL, &result, ASUS_WMI_DEVID_EGPU); + if (err) + return err; + if (result && !optimus) { + pr_warn("Cannot switch MUX to dGPU mode when eGPU is enabled\n"); + return -EBUSY; + } + } + + err = armoury_set_devstate(attr, optimus ? 1 : 0, NULL, asus_armoury.gpu_mux_dev_id); + if (err) + return err; + + sysfs_notify(kobj, NULL, attr->attr.name); + asus_set_reboot_and_signal_event(); + + return count; +} +ASUS_WMI_SHOW_INT(gpu_mux_mode_current_value, asus_armoury.gpu_mux_dev_id); +ASUS_ATTR_GROUP_BOOL(gpu_mux_mode, "gpu_mux_mode", "Set the GPU display MUX mode"); + +static ssize_t dgpu_disable_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, const char *buf, + size_t count) +{ + int result, err; + bool disable; + + err = kstrtobool(buf, &disable); + if (err) + return err; + + if (asus_armoury.gpu_mux_dev_id) { + err = armoury_get_devstate(NULL, &result, asus_armoury.gpu_mux_dev_id); + if (err) + return err; + if (!result && disable) { + pr_warn("Cannot disable dGPU when the MUX is in dGPU mode\n"); + return -EBUSY; + } + } + + scoped_guard(mutex, &asus_armoury.egpu_mutex) { + err = armoury_set_devstate(attr, disable ? 1 : 0, NULL, ASUS_WMI_DEVID_DGPU); + if (err) + return err; + } + + sysfs_notify(kobj, NULL, attr->attr.name); + + return count; +} +ASUS_WMI_SHOW_INT(dgpu_disable_current_value, ASUS_WMI_DEVID_DGPU); +ASUS_ATTR_GROUP_BOOL(dgpu_disable, "dgpu_disable", "Disable the dGPU"); + +/* Values map for eGPU activation requests. */ +static u32 egpu_status_map[] = { + [0] = 0x00000000U, + [1] = 0x00000001U, + [2] = 0x00000101U, + [3] = 0x00000201U, +}; + +/* + * armoury_pci_rescan() - Performs a PCI rescan + * + * Bring up any GPU that has been hotplugged in the system. + */ +static void armoury_pci_rescan(void) +{ + struct pci_bus *b = NULL; + + pci_lock_rescan_remove(); + while ((b = pci_find_next_bus(b)) != NULL) + pci_rescan_bus(b); + pci_unlock_rescan_remove(); +} + +/* + * The ACPI call to enable the eGPU might also disable the internal dGPU, + * but this is not always the case and on certain models enabling the eGPU + * when the dGPU is either still active or has been disabled without rebooting + * will make both GPUs malfunction and the kernel will detect many + * PCI AER unrecoverable errors. + */ +static ssize_t egpu_enable_current_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int err; + u32 requested, enable, result; + + err = kstrtou32(buf, 10, &requested); + if (err) + return err; + + if (requested >= ARRAY_SIZE(egpu_status_map)) + return -EINVAL; + enable = egpu_status_map[requested]; + + scoped_guard(mutex, &asus_armoury.egpu_mutex) { + /* Ensure the eGPU is connected before attempting to activate it. */ + if (enable) { + err = armoury_get_devstate(NULL, &result, ASUS_WMI_DEVID_EGPU_CONNECTED); + if (err) { + pr_warn("Failed to get eGPU connection status: %d\n", err); + return err; + } + if (!result) { + pr_warn("Cannot activate eGPU while undetected\n"); + return -ENOENT; + } + } + + if (asus_armoury.gpu_mux_dev_id) { + err = armoury_get_devstate(NULL, &result, asus_armoury.gpu_mux_dev_id); + if (err) + return err; + + if (!result && enable) { + pr_warn("Cannot enable eGPU when the MUX is in dGPU mode\n"); + return -ENODEV; + } + } + + err = armoury_set_devstate(attr, enable, &result, ASUS_WMI_DEVID_EGPU); + if (err) { + pr_err("Failed to set %s: %d\n", attr->attr.name, err); + return err; + } + + /* + * ACPI returns value 0x01 on success and 0x02 on a partial activation: + * performing a pci rescan will bring up the device in pci-e 3.0 speed, + * after a reboot the device will work at full speed. + */ + switch (result) { + case 0x01: + /* + * When a GPU is in use it does not get disconnected even if + * the ACPI call returns a success. + */ + if (!enable) { + err = armoury_get_devstate(attr, &result, ASUS_WMI_DEVID_EGPU); + if (err) { + pr_warn("Failed to ensure eGPU is deactivated: %d\n", err); + return err; + } + + if (result != 0) + return -EBUSY; + } + + pr_debug("Success changing the eGPU status\n"); + break; + case 0x02: + pr_info("Success changing the eGPU status, a reboot is strongly advised\n"); + asus_set_reboot_and_signal_event(); + break; + default: + pr_err("Failed to change the eGPU status: wmi result is 0x%x\n", result); + return -EIO; + } + } + + /* + * Perform a PCI rescan: on every tested model this is necessary + * to make the eGPU visible on the bus without rebooting. + */ + armoury_pci_rescan(); + + sysfs_notify(kobj, NULL, attr->attr.name); + + return count; +} + +static ssize_t egpu_enable_current_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + int i, err; + u32 status; + + scoped_guard(mutex, &asus_armoury.egpu_mutex) { + err = armoury_get_devstate(attr, &status, ASUS_WMI_DEVID_EGPU); + if (err) + return err; + } + + for (i = 0; i < ARRAY_SIZE(egpu_status_map); i++) { + if (egpu_status_map[i] == status) + return sysfs_emit(buf, "%u\n", i); + } + + return -EIO; +} + +static ssize_t egpu_enable_possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return armoury_attr_enum_list(buf, ARRAY_SIZE(egpu_status_map)); +} +ASUS_ATTR_GROUP_ENUM(egpu_enable, "egpu_enable", "Enable the eGPU (also disables dGPU)"); + +/* Device memory available to APU */ + +/* + * Values map for APU reserved memory (index + 1 number of GB). + * Some looks out of order, but are actually correct. + */ +static u32 apu_mem_map[] = { + [0] = 0x000, /* called "AUTO" on the BIOS, is the minimum available */ + [1] = 0x102, + [2] = 0x103, + [3] = 0x104, + [4] = 0x105, + [5] = 0x107, + [6] = 0x108, + [7] = 0x109, + [8] = 0x106, +}; + +static ssize_t apu_mem_current_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + int err; + u32 mem; + + err = armoury_get_devstate(attr, &mem, ASUS_WMI_DEVID_APU_MEM); + if (err) + return err; + + /* After 0x000 is set, a read will return 0x100 */ + if (mem == 0x100) + return sysfs_emit(buf, "0\n"); + + for (unsigned int i = 0; i < ARRAY_SIZE(apu_mem_map); i++) { + if (apu_mem_map[i] == mem) + return sysfs_emit(buf, "%u\n", i); + } + + pr_warn("Unrecognised value for APU mem 0x%08x\n", mem); + return -EIO; +} + +static ssize_t apu_mem_current_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 requested, mem; + + result = kstrtou32(buf, 10, &requested); + if (result) + return result; + + if (requested >= ARRAY_SIZE(apu_mem_map)) + return -EINVAL; + mem = apu_mem_map[requested]; + + err = armoury_set_devstate(attr, mem, NULL, ASUS_WMI_DEVID_APU_MEM); + if (err) { + pr_warn("Failed to set apu_mem 0x%x: %d\n", mem, err); + return err; + } + + pr_info("APU memory changed to %uGB, reboot required\n", requested + 1); + sysfs_notify(kobj, NULL, attr->attr.name); + + asus_set_reboot_and_signal_event(); + + return count; +} + +static ssize_t apu_mem_possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return armoury_attr_enum_list(buf, ARRAY_SIZE(apu_mem_map)); +} +ASUS_ATTR_GROUP_ENUM(apu_mem, "apu_mem", "Set available system RAM (in GB) for the APU to use"); + +/* Define helper to access the current power mode tunable values */ +static inline struct rog_tunables *get_current_tunables(void) +{ + if (power_supply_is_system_supplied()) + return asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_AC]; + + return asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_DC]; +} + +/* Simple attribute creation */ +ASUS_ATTR_GROUP_ENUM_INT_RO(charge_mode, "charge_mode", ASUS_WMI_DEVID_CHARGE_MODE, "0;1;2\n", + "Show the current mode of charging"); +ASUS_ATTR_GROUP_BOOL_RW(boot_sound, "boot_sound", ASUS_WMI_DEVID_BOOT_SOUND, + "Set the boot POST sound"); +ASUS_ATTR_GROUP_BOOL_RW(mcu_powersave, "mcu_powersave", ASUS_WMI_DEVID_MCU_POWERSAVE, + "Set MCU powersaving mode"); +ASUS_ATTR_GROUP_BOOL_RW(panel_od, "panel_overdrive", ASUS_WMI_DEVID_PANEL_OD, + "Set the panel refresh overdrive"); +ASUS_ATTR_GROUP_BOOL_RW(panel_hd_mode, "panel_hd_mode", ASUS_WMI_DEVID_PANEL_HD, + "Set the panel HD mode to UHD<0> or FHD<1>"); +ASUS_ATTR_GROUP_BOOL_RW(screen_auto_brightness, "screen_auto_brightness", + ASUS_WMI_DEVID_SCREEN_AUTO_BRIGHTNESS, + "Set the panel brightness to Off<0> or On<1>"); +ASUS_ATTR_GROUP_BOOL_RO(egpu_connected, "egpu_connected", ASUS_WMI_DEVID_EGPU_CONNECTED, + "Show the eGPU connection status"); +ASUS_ATTR_GROUP_ROG_TUNABLE(ppt_pl1_spl, ATTR_PPT_PL1_SPL, ASUS_WMI_DEVID_PPT_PL1_SPL, + "Set the CPU slow package limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(ppt_pl2_sppt, ATTR_PPT_PL2_SPPT, ASUS_WMI_DEVID_PPT_PL2_SPPT, + "Set the CPU fast package limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(ppt_pl3_fppt, ATTR_PPT_PL3_FPPT, ASUS_WMI_DEVID_PPT_PL3_FPPT, + "Set the CPU fastest package limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(ppt_apu_sppt, ATTR_PPT_APU_SPPT, ASUS_WMI_DEVID_PPT_APU_SPPT, + "Set the APU package limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(ppt_platform_sppt, ATTR_PPT_PLATFORM_SPPT, ASUS_WMI_DEVID_PPT_PLAT_SPPT, + "Set the platform package limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(nv_dynamic_boost, ATTR_NV_DYNAMIC_BOOST, ASUS_WMI_DEVID_NV_DYN_BOOST, + "Set the Nvidia dynamic boost limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(nv_temp_target, ATTR_NV_TEMP_TARGET, ASUS_WMI_DEVID_NV_THERM_TARGET, + "Set the Nvidia max thermal limit"); +ASUS_ATTR_GROUP_ROG_TUNABLE(nv_tgp, "nv_tgp", ASUS_WMI_DEVID_DGPU_SET_TGP, + "Set the additional TGP on top of the base TGP"); +ASUS_ATTR_GROUP_INT_VALUE_ONLY_RO(nv_base_tgp, ATTR_NV_BASE_TGP, ASUS_WMI_DEVID_DGPU_BASE_TGP, + "Read the base TGP value"); + +/* If an attribute does not require any special case handling add it here */ +static const struct asus_attr_group armoury_attr_groups[] = { + { &egpu_connected_attr_group, ASUS_WMI_DEVID_EGPU_CONNECTED }, + { &egpu_enable_attr_group, ASUS_WMI_DEVID_EGPU }, + { &dgpu_disable_attr_group, ASUS_WMI_DEVID_DGPU }, + { &apu_mem_attr_group, ASUS_WMI_DEVID_APU_MEM }, + + { &ppt_pl1_spl_attr_group, ASUS_WMI_DEVID_PPT_PL1_SPL }, + { &ppt_pl2_sppt_attr_group, ASUS_WMI_DEVID_PPT_PL2_SPPT }, + { &ppt_pl3_fppt_attr_group, ASUS_WMI_DEVID_PPT_PL3_FPPT }, + { &ppt_apu_sppt_attr_group, ASUS_WMI_DEVID_PPT_APU_SPPT }, + { &ppt_platform_sppt_attr_group, ASUS_WMI_DEVID_PPT_PLAT_SPPT }, + { &nv_dynamic_boost_attr_group, ASUS_WMI_DEVID_NV_DYN_BOOST }, + { &nv_temp_target_attr_group, ASUS_WMI_DEVID_NV_THERM_TARGET }, + { &nv_base_tgp_attr_group, ASUS_WMI_DEVID_DGPU_BASE_TGP }, + { &nv_tgp_attr_group, ASUS_WMI_DEVID_DGPU_SET_TGP }, + + { &charge_mode_attr_group, ASUS_WMI_DEVID_CHARGE_MODE }, + { &boot_sound_attr_group, ASUS_WMI_DEVID_BOOT_SOUND }, + { &mcu_powersave_attr_group, ASUS_WMI_DEVID_MCU_POWERSAVE }, + { &panel_od_attr_group, ASUS_WMI_DEVID_PANEL_OD }, + { &panel_hd_mode_attr_group, ASUS_WMI_DEVID_PANEL_HD }, + { &screen_auto_brightness_attr_group, ASUS_WMI_DEVID_SCREEN_AUTO_BRIGHTNESS }, +}; + +/** + * is_power_tunable_attr - Determines if an attribute is a power-related tunable + * @name: The name of the attribute to check + * + * This function checks if the given attribute name is related to power tuning. + * + * Return: true if the attribute is a power-related tunable, false otherwise + */ +static bool is_power_tunable_attr(const char *name) +{ + static const char * const power_tunable_attrs[] = { + ATTR_PPT_PL1_SPL, ATTR_PPT_PL2_SPPT, + ATTR_PPT_PL3_FPPT, ATTR_PPT_APU_SPPT, + ATTR_PPT_PLATFORM_SPPT, ATTR_NV_DYNAMIC_BOOST, + ATTR_NV_TEMP_TARGET, ATTR_NV_BASE_TGP, + ATTR_NV_TGP + }; + + for (unsigned int i = 0; i < ARRAY_SIZE(power_tunable_attrs); i++) { + if (!strcmp(name, power_tunable_attrs[i])) + return true; + } + + return false; +} + +/** + * has_valid_limit - Checks if a power-related attribute has a valid limit value + * @name: The name of the attribute to check + * @limits: Pointer to the power_limits structure containing limit values + * + * This function checks if a power-related attribute has a valid limit value. + * It returns false if limits is NULL or if the corresponding limit value is zero. + * + * Return: true if the attribute has a valid limit value, false otherwise + */ +static bool has_valid_limit(const char *name, const struct power_limits *limits) +{ + u32 limit_value = 0; + + if (!limits) + return false; + + if (!strcmp(name, ATTR_PPT_PL1_SPL)) + limit_value = limits->ppt_pl1_spl_max; + else if (!strcmp(name, ATTR_PPT_PL2_SPPT)) + limit_value = limits->ppt_pl2_sppt_max; + else if (!strcmp(name, ATTR_PPT_PL3_FPPT)) + limit_value = limits->ppt_pl3_fppt_max; + else if (!strcmp(name, ATTR_PPT_APU_SPPT)) + limit_value = limits->ppt_apu_sppt_max; + else if (!strcmp(name, ATTR_PPT_PLATFORM_SPPT)) + limit_value = limits->ppt_platform_sppt_max; + else if (!strcmp(name, ATTR_NV_DYNAMIC_BOOST)) + limit_value = limits->nv_dynamic_boost_max; + else if (!strcmp(name, ATTR_NV_TEMP_TARGET)) + limit_value = limits->nv_temp_target_max; + else if (!strcmp(name, ATTR_NV_BASE_TGP) || + !strcmp(name, ATTR_NV_TGP)) + limit_value = limits->nv_tgp_max; + + return limit_value > 0; +} + +static int asus_fw_attr_add(void) +{ + const struct rog_tunables *const ac_rog_tunables = + asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_AC]; + const struct power_limits *limits; + bool should_create; + const char *name; + int err, i; + + asus_armoury.fw_attr_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(asus_armoury.fw_attr_dev)) { + err = PTR_ERR(asus_armoury.fw_attr_dev); + goto fail_class_get; + } + + asus_armoury.fw_attr_kset = kset_create_and_add("attributes", NULL, + &asus_armoury.fw_attr_dev->kobj); + if (!asus_armoury.fw_attr_kset) { + err = -ENOMEM; + goto err_destroy_classdev; + } + + err = sysfs_create_file(&asus_armoury.fw_attr_kset->kobj, &pending_reboot.attr); + if (err) { + pr_err("Failed to create sysfs level attributes\n"); + goto err_destroy_kset; + } + + asus_armoury.mini_led_dev_id = 0; + if (armoury_has_devstate(ASUS_WMI_DEVID_MINI_LED_MODE)) + asus_armoury.mini_led_dev_id = ASUS_WMI_DEVID_MINI_LED_MODE; + else if (armoury_has_devstate(ASUS_WMI_DEVID_MINI_LED_MODE2)) + asus_armoury.mini_led_dev_id = ASUS_WMI_DEVID_MINI_LED_MODE2; + + if (asus_armoury.mini_led_dev_id) { + err = sysfs_create_group(&asus_armoury.fw_attr_kset->kobj, + &mini_led_mode_attr_group); + if (err) { + pr_err("Failed to create sysfs-group for mini_led\n"); + goto err_remove_file; + } + } + + asus_armoury.gpu_mux_dev_id = 0; + if (armoury_has_devstate(ASUS_WMI_DEVID_GPU_MUX)) + asus_armoury.gpu_mux_dev_id = ASUS_WMI_DEVID_GPU_MUX; + else if (armoury_has_devstate(ASUS_WMI_DEVID_GPU_MUX_VIVO)) + asus_armoury.gpu_mux_dev_id = ASUS_WMI_DEVID_GPU_MUX_VIVO; + + if (asus_armoury.gpu_mux_dev_id) { + err = sysfs_create_group(&asus_armoury.fw_attr_kset->kobj, + &gpu_mux_mode_attr_group); + if (err) { + pr_err("Failed to create sysfs-group for gpu_mux\n"); + goto err_remove_mini_led_group; + } + } + + for (i = 0; i < ARRAY_SIZE(armoury_attr_groups); i++) { + if (!armoury_has_devstate(armoury_attr_groups[i].wmi_devid)) + continue; + + /* Always create by default, unless PPT is not present */ + should_create = true; + name = armoury_attr_groups[i].attr_group->name; + + /* Check if this is a power-related tunable requiring limits */ + if (ac_rog_tunables && ac_rog_tunables->power_limits && + is_power_tunable_attr(name)) { + limits = ac_rog_tunables->power_limits; + /* Check only AC: if not present then DC won't be either */ + should_create = has_valid_limit(name, limits); + if (!should_create) + pr_debug("Missing max value for tunable %s\n", name); + } + + if (should_create) { + err = sysfs_create_group(&asus_armoury.fw_attr_kset->kobj, + armoury_attr_groups[i].attr_group); + if (err) { + pr_err("Failed to create sysfs-group for %s\n", + armoury_attr_groups[i].attr_group->name); + goto err_remove_groups; + } + } + } + + return 0; + +err_remove_groups: + while (i--) { + if (armoury_has_devstate(armoury_attr_groups[i].wmi_devid)) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, + armoury_attr_groups[i].attr_group); + } + if (asus_armoury.gpu_mux_dev_id) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, &gpu_mux_mode_attr_group); +err_remove_mini_led_group: + if (asus_armoury.mini_led_dev_id) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, &mini_led_mode_attr_group); +err_remove_file: + sysfs_remove_file(&asus_armoury.fw_attr_kset->kobj, &pending_reboot.attr); +err_destroy_kset: + kset_unregister(asus_armoury.fw_attr_kset); +err_destroy_classdev: +fail_class_get: + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); + return err; +} + +/* Init / exit ****************************************************************/ + +/* Set up the min/max and defaults for ROG tunables */ +static void init_rog_tunables(void) +{ + const struct power_limits *ac_limits, *dc_limits; + struct rog_tunables *ac_rog_tunables = NULL, *dc_rog_tunables = NULL; + const struct power_data *power_data; + const struct dmi_system_id *dmi_id; + + /* Match the system against the power_limits table */ + dmi_id = dmi_first_match(power_limits); + if (!dmi_id) { + pr_warn("No matching power limits found for this system\n"); + return; + } + + /* Get the power data for this system */ + power_data = dmi_id->driver_data; + if (!power_data) { + pr_info("No power data available for this system\n"); + return; + } + + /* Initialize AC power tunables */ + ac_limits = power_data->ac_data; + if (ac_limits) { + ac_rog_tunables = kzalloc_obj(*asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_AC]); + if (!ac_rog_tunables) + goto err_nomem; + + asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_AC] = ac_rog_tunables; + ac_rog_tunables->power_limits = ac_limits; + + /* Set initial AC values */ + ac_rog_tunables->ppt_pl1_spl = + ac_limits->ppt_pl1_spl_def ? + ac_limits->ppt_pl1_spl_def : + ac_limits->ppt_pl1_spl_max; + + ac_rog_tunables->ppt_pl2_sppt = + ac_limits->ppt_pl2_sppt_def ? + ac_limits->ppt_pl2_sppt_def : + ac_limits->ppt_pl2_sppt_max; + + ac_rog_tunables->ppt_pl3_fppt = + ac_limits->ppt_pl3_fppt_def ? + ac_limits->ppt_pl3_fppt_def : + ac_limits->ppt_pl3_fppt_max; + + ac_rog_tunables->ppt_apu_sppt = + ac_limits->ppt_apu_sppt_def ? + ac_limits->ppt_apu_sppt_def : + ac_limits->ppt_apu_sppt_max; + + ac_rog_tunables->ppt_platform_sppt = + ac_limits->ppt_platform_sppt_def ? + ac_limits->ppt_platform_sppt_def : + ac_limits->ppt_platform_sppt_max; + + ac_rog_tunables->nv_dynamic_boost = + ac_limits->nv_dynamic_boost_max; + ac_rog_tunables->nv_temp_target = + ac_limits->nv_temp_target_max; + ac_rog_tunables->nv_tgp = ac_limits->nv_tgp_max; + + pr_debug("AC power limits initialized for %s\n", dmi_id->matches[0].substr); + } else { + pr_debug("No AC PPT limits defined\n"); + } + + /* Initialize DC power tunables */ + dc_limits = power_data->dc_data; + if (dc_limits) { + dc_rog_tunables = kzalloc_obj(*asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_DC]); + if (!dc_rog_tunables) { + kfree(ac_rog_tunables); + goto err_nomem; + } + + asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_DC] = dc_rog_tunables; + dc_rog_tunables->power_limits = dc_limits; + + /* Set initial DC values */ + dc_rog_tunables->ppt_pl1_spl = + dc_limits->ppt_pl1_spl_def ? + dc_limits->ppt_pl1_spl_def : + dc_limits->ppt_pl1_spl_max; + + dc_rog_tunables->ppt_pl2_sppt = + dc_limits->ppt_pl2_sppt_def ? + dc_limits->ppt_pl2_sppt_def : + dc_limits->ppt_pl2_sppt_max; + + dc_rog_tunables->ppt_pl3_fppt = + dc_limits->ppt_pl3_fppt_def ? + dc_limits->ppt_pl3_fppt_def : + dc_limits->ppt_pl3_fppt_max; + + dc_rog_tunables->ppt_apu_sppt = + dc_limits->ppt_apu_sppt_def ? + dc_limits->ppt_apu_sppt_def : + dc_limits->ppt_apu_sppt_max; + + dc_rog_tunables->ppt_platform_sppt = + dc_limits->ppt_platform_sppt_def ? + dc_limits->ppt_platform_sppt_def : + dc_limits->ppt_platform_sppt_max; + + dc_rog_tunables->nv_dynamic_boost = + dc_limits->nv_dynamic_boost_max; + dc_rog_tunables->nv_temp_target = + dc_limits->nv_temp_target_max; + dc_rog_tunables->nv_tgp = dc_limits->nv_tgp_max; + + pr_debug("DC power limits initialized for %s\n", dmi_id->matches[0].substr); + } else { + pr_debug("No DC PPT limits defined\n"); + } + + return; + +err_nomem: + pr_err("Failed to allocate memory for tunables\n"); +} + +static int __init asus_fw_init(void) +{ + char *wmi_uid; + + wmi_uid = wmi_get_acpi_device_uid(ASUS_WMI_MGMT_GUID); + if (!wmi_uid) + return -ENODEV; + + /* + * if equal to "ASUSWMI" then it's DCTS that can't be used for this + * driver, DSTS is required. + */ + if (!strcmp(wmi_uid, ASUS_ACPI_UID_ASUSWMI)) + return -ENODEV; + + init_rog_tunables(); + + /* Must always be last step to ensure data is available */ + return asus_fw_attr_add(); +} + +static void __exit asus_fw_exit(void) +{ + int i; + + for (i = ARRAY_SIZE(armoury_attr_groups) - 1; i >= 0; i--) { + if (armoury_has_devstate(armoury_attr_groups[i].wmi_devid)) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, + armoury_attr_groups[i].attr_group); + } + + if (asus_armoury.gpu_mux_dev_id) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, &gpu_mux_mode_attr_group); + + if (asus_armoury.mini_led_dev_id) + sysfs_remove_group(&asus_armoury.fw_attr_kset->kobj, &mini_led_mode_attr_group); + + sysfs_remove_file(&asus_armoury.fw_attr_kset->kobj, &pending_reboot.attr); + kset_unregister(asus_armoury.fw_attr_kset); + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); + + kfree(asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_AC]); + kfree(asus_armoury.rog_tunables[ASUS_ROG_TUNABLE_DC]); +} + +module_init(asus_fw_init); +module_exit(asus_fw_exit); + +MODULE_IMPORT_NS("ASUS_WMI"); +MODULE_AUTHOR("Luke Jones "); +MODULE_DESCRIPTION("ASUS BIOS Configuration Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("wmi:" ASUS_NB_WMI_EVENT_GUID); diff --git a/drivers/platform/x86/asus-armoury.h b/drivers/platform/x86/asus-armoury.h new file mode 100644 index 000000000..322bfa647 --- /dev/null +++ b/drivers/platform/x86/asus-armoury.h @@ -0,0 +1,2406 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * Definitions for kernel modules using asus-armoury driver + * + * Copyright (c) 2024 Luke Jones + */ + +#ifndef _ASUS_ARMOURY_H_ +#define _ASUS_ARMOURY_H_ + +#include +#include +#include +#include + +#define DRIVER_NAME "asus-armoury" + +/** + * armoury_attr_uint_store() - Send an uint to WMI method if within min/max. + * @kobj: Pointer to the driver object. + * @attr: Pointer to the attribute calling this function. + * @buf: The buffer to read from, this is parsed to `uint` type. + * @count: Required by sysfs attribute macros, pass in from the callee attr. + * @min: Minimum accepted value. Below this returns -EINVAL. + * @max: Maximum accepted value. Above this returns -EINVAL. + * @store_value: Pointer to where the parsed value should be stored. + * @wmi_dev: The WMI function ID to use. + * + * This function is intended to be generic so it can be called from any "_store" + * attribute which works only with integers. + * + * Integers to be sent to the WMI method is inclusive range checked and + * an error returned if out of range. + * + * If the value is valid and WMI is success then the sysfs attribute is notified + * and if asus_bios_requires_reboot() is true then reboot attribute + * is also notified. + * + * Returns: Either count, or an error. + */ +ssize_t armoury_attr_uint_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count, u32 min, u32 max, + u32 *store_value, u32 wmi_dev); + +/** + * armoury_attr_uint_show() - Receive an uint from a WMI method. + * @kobj: Pointer to the driver object. + * @attr: Pointer to the attribute calling this function. + * @buf: The buffer to write to, as an `uint` type. + * @wmi_dev: The WMI function ID to use. + * + * This function is intended to be generic so it can be called from any "_show" + * attribute which works only with integers. + * + * Returns: Either count, or an error. + */ +ssize_t armoury_attr_uint_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf, u32 wmi_dev); + +#define __ASUS_ATTR_RO(_func, _name) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _func##_##_name##_show, \ + } + +#define __ASUS_ATTR_RO_AS(_name, _show) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _show, \ + } + +#define __ASUS_ATTR_RW(_func, _name) \ + __ATTR(_name, 0644, _func##_##_name##_show, _func##_##_name##_store) + +#define __WMI_STORE_INT(_attr, _min, _max, _wmi) \ + static ssize_t _attr##_store(struct kobject *kobj, \ + struct kobj_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return armoury_attr_uint_store(kobj, attr, buf, count, _min, \ + _max, NULL, _wmi); \ + } + +#define ASUS_WMI_SHOW_INT(_attr, _wmi) \ + static ssize_t _attr##_show(struct kobject *kobj, \ + struct kobj_attribute *attr, char *buf) \ + { \ + return armoury_attr_uint_show(kobj, attr, buf, _wmi); \ + } + +/* Create functions and attributes for use in other macros or on their own */ + +/* Shows a formatted static variable */ +#define __ATTR_SHOW_FMT(_prop, _attrname, _fmt, _val) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + return sysfs_emit(buf, _fmt, _val); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __ASUS_ATTR_RO(_attrname, _prop) + +#define __ATTR_RO_INT_GROUP_ENUM(_attrname, _wmi, _fsname, _possible, _dispname)\ + ASUS_WMI_SHOW_INT(_attrname##_current_value, _wmi); \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __ASUS_ATTR_RO(_attrname, current_value); \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + __ATTR_SHOW_FMT(possible_values, _attrname, "%s\n", _possible); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, enum_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_possible_values.attr, \ + &attr_##_attrname##_type.attr, \ + NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +#define __ATTR_RW_INT_GROUP_ENUM(_attrname, _minv, _maxv, _wmi, _fsname,\ + _possible, _dispname) \ + __WMI_STORE_INT(_attrname##_current_value, _minv, _maxv, _wmi); \ + ASUS_WMI_SHOW_INT(_attrname##_current_value, _wmi); \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __ASUS_ATTR_RW(_attrname, current_value); \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + __ATTR_SHOW_FMT(possible_values, _attrname, "%s\n", _possible); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, enum_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_possible_values.attr, \ + &attr_##_attrname##_type.attr, \ + NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +/* Boolean style enumeration, base macro. Requires adding show/store */ +#define __ATTR_GROUP_ENUM(_attrname, _fsname, _possible, _dispname) \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + __ATTR_SHOW_FMT(possible_values, _attrname, "%s\n", _possible); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, enum_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_possible_values.attr, \ + &attr_##_attrname##_type.attr, \ + NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +#define ASUS_ATTR_GROUP_BOOL_RO(_attrname, _fsname, _wmi, _dispname) \ + __ATTR_RO_INT_GROUP_ENUM(_attrname, _wmi, _fsname, "0;1", _dispname) + + +#define ASUS_ATTR_GROUP_BOOL_RW(_attrname, _fsname, _wmi, _dispname) \ + __ATTR_RW_INT_GROUP_ENUM(_attrname, 0, 1, _wmi, _fsname, "0;1", _dispname) + +#define ASUS_ATTR_GROUP_ENUM_INT_RO(_attrname, _fsname, _wmi, _possible, _dispname) \ + __ATTR_RO_INT_GROUP_ENUM(_attrname, _wmi, _fsname, _possible, _dispname) + +/* + * Requires _current_value_show(), _current_value_show() + */ +#define ASUS_ATTR_GROUP_BOOL(_attrname, _fsname, _dispname) \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __ASUS_ATTR_RW(_attrname, current_value); \ + __ATTR_GROUP_ENUM(_attrname, _fsname, "0;1", _dispname) + +/* + * Requires _current_value_show(), _current_value_show() + * and _possible_values_show() + */ +#define ASUS_ATTR_GROUP_ENUM(_attrname, _fsname, _dispname) \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __ASUS_ATTR_RW(_attrname, current_value); \ + static struct kobj_attribute attr_##_attrname##_possible_values = \ + __ASUS_ATTR_RO(_attrname, possible_values); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, enum_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_possible_values.attr, \ + &attr_##_attrname##_type.attr, \ + NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +#define ASUS_ATTR_GROUP_INT_VALUE_ONLY_RO(_attrname, _fsname, _wmi, _dispname) \ + ASUS_WMI_SHOW_INT(_attrname##_current_value, _wmi); \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __ASUS_ATTR_RO(_attrname, current_value); \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, int_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_type.attr, NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +/* + * ROG PPT attributes need a little different in setup as they + * require rog_tunables members. + */ + +#define __ROG_TUNABLE_SHOW(_prop, _attrname, _val) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + struct rog_tunables *tunables = get_current_tunables(); \ + \ + if (!tunables || !tunables->power_limits) \ + return -ENODEV; \ + \ + return sysfs_emit(buf, "%d\n", tunables->power_limits->_val); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __ASUS_ATTR_RO(_attrname, _prop) + +#define __ROG_TUNABLE_SHOW_DEFAULT(_attrname) \ + static ssize_t _attrname##_default_value_show( \ + struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + struct rog_tunables *tunables = get_current_tunables(); \ + \ + if (!tunables || !tunables->power_limits) \ + return -ENODEV; \ + \ + return sysfs_emit( \ + buf, "%d\n", \ + tunables->power_limits->_attrname##_def ? \ + tunables->power_limits->_attrname##_def : \ + tunables->power_limits->_attrname##_max); \ + } \ + static struct kobj_attribute attr_##_attrname##_default_value = \ + __ASUS_ATTR_RO(_attrname, default_value) + +#define __ROG_TUNABLE_RW(_attr, _wmi) \ + static ssize_t _attr##_current_value_store( \ + struct kobject *kobj, struct kobj_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct rog_tunables *tunables = get_current_tunables(); \ + \ + if (!tunables || !tunables->power_limits) \ + return -ENODEV; \ + \ + if (tunables->power_limits->_attr##_min == \ + tunables->power_limits->_attr##_max) \ + return -EINVAL; \ + \ + return armoury_attr_uint_store(kobj, attr, buf, count, \ + tunables->power_limits->_attr##_min, \ + tunables->power_limits->_attr##_max, \ + &tunables->_attr, _wmi); \ + } \ + static ssize_t _attr##_current_value_show( \ + struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + struct rog_tunables *tunables = get_current_tunables(); \ + \ + if (!tunables) \ + return -ENODEV; \ + \ + return sysfs_emit(buf, "%u\n", tunables->_attr); \ + } \ + static struct kobj_attribute attr_##_attr##_current_value = \ + __ASUS_ATTR_RW(_attr, current_value) + +#define ASUS_ATTR_GROUP_ROG_TUNABLE(_attrname, _fsname, _wmi, _dispname) \ + __ROG_TUNABLE_RW(_attrname, _wmi); \ + __ROG_TUNABLE_SHOW_DEFAULT(_attrname); \ + __ROG_TUNABLE_SHOW(min_value, _attrname, _attrname##_min); \ + __ROG_TUNABLE_SHOW(max_value, _attrname, _attrname##_max); \ + __ATTR_SHOW_FMT(scalar_increment, _attrname, "%d\n", 1); \ + __ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __ASUS_ATTR_RO_AS(type, int_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_default_value.attr, \ + &attr_##_attrname##_min_value.attr, \ + &attr_##_attrname##_max_value.attr, \ + &attr_##_attrname##_scalar_increment.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_type.attr, \ + NULL \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +/* Default is always the maximum value unless *_def is specified */ +struct power_limits { + u8 ppt_pl1_spl_min; + u8 ppt_pl1_spl_def; + u8 ppt_pl1_spl_max; + u8 ppt_pl2_sppt_min; + u8 ppt_pl2_sppt_def; + u8 ppt_pl2_sppt_max; + u8 ppt_pl3_fppt_min; + u8 ppt_pl3_fppt_def; + u8 ppt_pl3_fppt_max; + u8 ppt_apu_sppt_min; + u8 ppt_apu_sppt_def; + u8 ppt_apu_sppt_max; + u8 ppt_platform_sppt_min; + u8 ppt_platform_sppt_def; + u8 ppt_platform_sppt_max; + /* Nvidia GPU specific, default is always max */ + u8 nv_dynamic_boost_def; // unused. exists for macro + u8 nv_dynamic_boost_min; + u8 nv_dynamic_boost_max; + u8 nv_temp_target_def; // unused. exists for macro + u8 nv_temp_target_min; + u8 nv_temp_target_max; + u8 nv_tgp_def; // unused. exists for macro + u8 nv_tgp_min; + u8 nv_tgp_max; +}; + +struct power_data { + const struct power_limits *ac_data; + const struct power_limits *dc_data; + bool requires_fan_curve; +}; + +/* + * For each available attribute there must be a min and a max. + * _def is not required and will be assumed to be default == max if missing. + */ +static const struct dmi_system_id power_limits[] = { + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA401EA"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 95, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 100, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 71, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 71, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 71, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA401UM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 31, + .ppt_pl2_sppt_max = 44, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA401UV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 75, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 31, + .ppt_pl2_sppt_max = 44, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA401W"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 75, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 30, + .ppt_pl2_sppt_min = 31, + .ppt_pl2_sppt_max = 44, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA507N"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA507UV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA507R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA507X"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA507Z"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 105, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA607NU"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 25, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA607P"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_def = 100, + .ppt_pl1_spl_max = 135, + .ppt_pl2_sppt_min = 30, + .ppt_pl2_sppt_def = 115, + .ppt_pl2_sppt_max = 135, + .ppt_pl3_fppt_min = 30, + .ppt_pl3_fppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_def = 60, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 25, + .ppt_pl3_fppt_max = 80, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA608UM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 90, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 65, + .ppt_pl3_fppt_max = 90, + .nv_dynamic_boost_min = 10, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 100, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA608WI"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 90, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 90, + .ppt_pl2_sppt_max = 90, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 90, + .ppt_pl3_fppt_max = 90, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 65, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA617NS"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 80, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 120, + }, + .dc_data = &(struct power_limits) { + .ppt_apu_sppt_min = 25, + .ppt_apu_sppt_max = 35, + .ppt_platform_sppt_min = 45, + .ppt_platform_sppt_max = 100, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA617NT"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 80, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 45, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 50, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA617XS"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 80, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 120, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_apu_sppt_min = 25, + .ppt_apu_sppt_max = 35, + .ppt_platform_sppt_min = 45, + .ppt_platform_sppt_max = 100, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FA617XT"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 80, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 145, + }, + .dc_data = &(struct power_limits) { + .ppt_apu_sppt_min = 25, + .ppt_apu_sppt_max = 35, + .ppt_platform_sppt_min = 45, + .ppt_platform_sppt_max = 100, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FX507VI"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 135, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FX507VV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 115, + .ppt_pl1_spl_max = 135, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FX507Z"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 15, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 60, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FX607VU"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 115, + .ppt_pl1_spl_max = 135, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "FX608JPR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 100, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 10, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 100, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 53, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA401Q"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 80, + }, + .dc_data = NULL, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA402NJ"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_def = 65, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_def = 10, + .nv_dynamic_boost_max = 15, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + // This model is full AMD. No Nvidia dGPU. + DMI_MATCH(DMI_BOARD_NAME, "GA402R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 80, + .ppt_platform_sppt_min = 30, + .ppt_platform_sppt_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_apu_sppt_min = 25, + .ppt_apu_sppt_def = 30, + .ppt_apu_sppt_max = 45, + .ppt_platform_sppt_min = 40, + .ppt_platform_sppt_max = 60, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA402X"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_def = 65, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403UI"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 65, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403UM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 0, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403UV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 65, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403WM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 0, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403WR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 0, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 80, + .nv_tgp_max = 95, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA403WW"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 0, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 80, + .nv_tgp_max = 95, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA503QM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_max = 80, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA503QR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_max = 80, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA503R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 65, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 60, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GA605W"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 31, + .ppt_pl2_sppt_max = 44, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU603Z"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 60, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 40, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 40, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + } + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU604V"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 65, + .ppt_pl1_spl_max = 120, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_max = 150, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 40, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 40, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605CP"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 45, + .ppt_pl1_spl_max = 75, + .ppt_pl2_sppt_min = 56, + .ppt_pl2_sppt_max = 95, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 15, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_def = 75, + .nv_tgp_max = 95, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 75, + .ppt_pl2_sppt_min = 32, + .ppt_pl2_sppt_max = 95, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605CR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 38, + .ppt_pl2_sppt_max = 110, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 80, + .nv_tgp_def = 90, + .nv_tgp_max = 105, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 38, + .ppt_pl2_sppt_max = 110, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605CW"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 45, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 56, + .ppt_pl2_sppt_max = 110, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 80, + .nv_tgp_def = 90, + .nv_tgp_max = 110, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 32, + .ppt_pl2_sppt_max = 110, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605CX"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 45, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 56, + .ppt_pl2_sppt_max = 110, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 7, + .nv_temp_target_max = 87, + .nv_tgp_min = 95, + .nv_tgp_def = 100, + .nv_tgp_max = 110, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 85, + .ppt_pl2_sppt_min = 32, + .ppt_pl2_sppt_max = 110, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605MU"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 38, + .ppt_pl2_sppt_max = 53, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GU605M"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 38, + .ppt_pl2_sppt_max = 53, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV301Q"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_max = 80, + }, + .dc_data = NULL, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV301R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 45, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 54, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV302XU"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 60, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV302XV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 60, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 35, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV601R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 35, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 100, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 80, + .ppt_pl3_fppt_max = 125, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 28, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 54, + .ppt_pl2_sppt_max = 60, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 80, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GV601V"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 100, + .ppt_pl1_spl_max = 110, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 40, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 40, + .ppt_pl2_sppt_max = 60, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GX650P"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 110, + .ppt_pl1_spl_max = 130, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 125, + .ppt_pl2_sppt_max = 130, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 125, + .ppt_pl3_fppt_max = 135, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_def = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_def = 35, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_def = 42, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GX650RX"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 70, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_def = 70, + .ppt_pl2_sppt_max = 100, + .ppt_pl3_fppt_min = 28, + .ppt_pl3_fppt_def = 110, + .ppt_pl3_fppt_max = 125, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 76, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 50, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 50, + .ppt_pl3_fppt_min = 28, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 76, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "GZ302EA"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 60, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 32, + .ppt_pl2_sppt_def = 75, + .ppt_pl2_sppt_max = 92, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_def = 86, + .ppt_pl3_fppt_max = 93, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 45, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 32, + .ppt_pl2_sppt_def = 52, + .ppt_pl2_sppt_max = 92, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_def = 71, + .ppt_pl3_fppt_max = 93, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G513I"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + /* Yes this laptop is very limited */ + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 80, + }, + .dc_data = NULL, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G513QM"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + /* Yes this laptop is very limited */ + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 100, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 190, + }, + .dc_data = NULL, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G513QY"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + /* Advantage Edition Laptop, no PL1 or PL2 limits */ + .ppt_apu_sppt_min = 15, + .ppt_apu_sppt_max = 100, + .ppt_platform_sppt_min = 70, + .ppt_platform_sppt_max = 190, + }, + .dc_data = NULL, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G513R"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 35, + .ppt_pl1_spl_max = 90, + .ppt_pl2_sppt_min = 54, + .ppt_pl2_sppt_max = 100, + .ppt_pl3_fppt_min = 54, + .ppt_pl3_fppt_max = 125, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 50, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 50, + .ppt_pl3_fppt_min = 28, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G614FP"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_max = 120, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_def = 140, + .ppt_pl2_sppt_max = 165, + .ppt_pl3_fppt_min = 65, + .ppt_pl3_fppt_def = 140, + .ppt_pl3_fppt_max = 165, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 15, + .nv_tgp_min = 50, + .nv_tgp_max = 100, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 75, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G614FR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_max = 120, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_def = 140, + .ppt_pl2_sppt_max = 162, + .ppt_pl3_fppt_min = 65, + .ppt_pl3_fppt_def = 140, + .ppt_pl3_fppt_max = 162, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_tgp_min = 65, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 75, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G614J"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 140, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G614PR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_max = 120, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_def = 140, + .ppt_pl2_sppt_max = 145, + .ppt_pl3_fppt_min = 65, + .ppt_pl3_fppt_def = 140, + .ppt_pl3_fppt_max = 145, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_tgp_min = 65, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 75, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G615LR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 140, + .ppt_pl1_spl_max = 175, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_tgp_min = 65, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G634J"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 140, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G713PV"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_def = 120, + .ppt_pl1_spl_max = 130, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_def = 125, + .ppt_pl2_sppt_max = 130, + .ppt_pl3_fppt_min = 65, + .ppt_pl3_fppt_def = 125, + .ppt_pl3_fppt_max = 130, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 75, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G733C"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 170, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 35, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G733P"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 30, + .ppt_pl1_spl_def = 100, + .ppt_pl1_spl_max = 130, + .ppt_pl2_sppt_min = 65, + .ppt_pl2_sppt_def = 125, + .ppt_pl2_sppt_max = 130, + .ppt_pl3_fppt_min = 65, + .ppt_pl3_fppt_def = 125, + .ppt_pl3_fppt_max = 130, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 65, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 65, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 75, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G733QS"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 80, + }, + .requires_fan_curve = false, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G814J"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 140, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 140, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G834J"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_max = 140, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G835LR"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 140, + .ppt_pl1_spl_max = 175, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 65, + .nv_tgp_max = 115, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "G835LW"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 28, + .ppt_pl1_spl_def = 140, + .ppt_pl1_spl_max = 175, + .ppt_pl2_sppt_min = 28, + .ppt_pl2_sppt_max = 175, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 25, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 80, + .nv_tgp_max = 150, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 55, + .ppt_pl2_sppt_min = 25, + .ppt_pl2_sppt_max = 70, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + .requires_fan_curve = true, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "H7606W"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 15, + .ppt_pl1_spl_max = 80, + .ppt_pl2_sppt_min = 35, + .ppt_pl2_sppt_max = 80, + .ppt_pl3_fppt_min = 35, + .ppt_pl3_fppt_max = 80, + .nv_dynamic_boost_min = 5, + .nv_dynamic_boost_max = 20, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + .nv_tgp_min = 55, + .nv_tgp_max = 85, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 25, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 31, + .ppt_pl2_sppt_max = 44, + .ppt_pl3_fppt_min = 45, + .ppt_pl3_fppt_max = 65, + .nv_temp_target_min = 75, + .nv_temp_target_max = 87, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "RC71"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_max = 30, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 43, + .ppt_pl3_fppt_min = 15, + .ppt_pl3_fppt_max = 53, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_def = 15, + .ppt_pl1_spl_max = 25, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_def = 20, + .ppt_pl2_sppt_max = 30, + .ppt_pl3_fppt_min = 15, + .ppt_pl3_fppt_def = 25, + .ppt_pl3_fppt_max = 35, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "RC72"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_max = 30, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_max = 43, + .ppt_pl3_fppt_min = 15, + .ppt_pl3_fppt_max = 53, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_def = 17, + .ppt_pl1_spl_max = 25, + .ppt_pl2_sppt_min = 15, + .ppt_pl2_sppt_def = 24, + .ppt_pl2_sppt_max = 30, + .ppt_pl3_fppt_min = 15, + .ppt_pl3_fppt_def = 30, + .ppt_pl3_fppt_max = 35, + }, + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "RC73XA"), + }, + .driver_data = &(struct power_data) { + .ac_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 14, + .ppt_pl2_sppt_max = 45, + .ppt_pl3_fppt_min = 19, + .ppt_pl3_fppt_max = 55, + }, + .dc_data = &(struct power_limits) { + .ppt_pl1_spl_min = 7, + .ppt_pl1_spl_def = 17, + .ppt_pl1_spl_max = 35, + .ppt_pl2_sppt_min = 13, + .ppt_pl2_sppt_def = 21, + .ppt_pl2_sppt_max = 45, + .ppt_pl3_fppt_min = 19, + .ppt_pl3_fppt_def = 26, + .ppt_pl3_fppt_max = 55, + }, + }, + }, + {} +}; + +#endif /* _ASUS_ARMOURY_H_ */ diff --git a/drivers/platform/x86/asus-laptop.c b/drivers/platform/x86/asus-laptop.c new file mode 100644 index 000000000..140ac8a10 --- /dev/null +++ b/drivers/platform/x86/asus-laptop.c @@ -0,0 +1,1980 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * asus-laptop.c - Asus Laptop Support + * + * Copyright (C) 2002-2005 Julien Lerouge, 2003-2006 Karol Kozimor + * Copyright (C) 2006-2007 Corentin Chary + * Copyright (C) 2011 Wind River Systems + * + * The development page for this driver is located at + * http://sourceforge.net/projects/acpi4asus/ + * + * Credits: + * Pontus Fuchs - Helper functions, cleanup + * Johann Wiesner - Small compile fixes + * John Belmonte - ACPI code for Toshiba laptop was a good starting point. + * Eric Burghard - LED display support for W1N + * Josh Green - Light Sens support + * Thomas Tuttle - His first patch for led support was very helpful + * Sam Lin - GPS support + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ASUS_LAPTOP_VERSION "0.42" + +#define ASUS_LAPTOP_NAME "Asus Laptop Support" +#define ASUS_LAPTOP_CLASS "hotkey" +#define ASUS_LAPTOP_DEVICE_NAME "Hotkey" +#define ASUS_LAPTOP_FILE KBUILD_MODNAME +#define ASUS_LAPTOP_PREFIX "\\_SB.ATKD." + +MODULE_AUTHOR("Julien Lerouge, Karol Kozimor, Corentin Chary"); +MODULE_DESCRIPTION(ASUS_LAPTOP_NAME); +MODULE_LICENSE("GPL"); + +/* + * WAPF defines the behavior of the Fn+Fx wlan key + * The significance of values is yet to be found, but + * most of the time: + * Bit | Bluetooth | WLAN + * 0 | Hardware | Hardware + * 1 | Hardware | Software + * 4 | Software | Software + */ +static uint wapf = 1; +module_param(wapf, uint, 0444); +MODULE_PARM_DESC(wapf, "WAPF value"); + +static char *wled_type = "unknown"; +static char *bled_type = "unknown"; + +module_param(wled_type, charp, 0444); +MODULE_PARM_DESC(wled_type, "Set the wled type on boot " + "(unknown, led or rfkill). " + "default is unknown"); + +module_param(bled_type, charp, 0444); +MODULE_PARM_DESC(bled_type, "Set the bled type on boot " + "(unknown, led or rfkill). " + "default is unknown"); + +static int wlan_status = 1; +static int bluetooth_status = 1; +static int wimax_status = -1; +static int wwan_status = -1; +static int als_status; + +module_param(wlan_status, int, 0444); +MODULE_PARM_DESC(wlan_status, "Set the wireless status on boot " + "(0 = disabled, 1 = enabled, -1 = don't do anything). " + "default is -1"); + +module_param(bluetooth_status, int, 0444); +MODULE_PARM_DESC(bluetooth_status, "Set the wireless status on boot " + "(0 = disabled, 1 = enabled, -1 = don't do anything). " + "default is -1"); + +module_param(wimax_status, int, 0444); +MODULE_PARM_DESC(wimax_status, "Set the wireless status on boot " + "(0 = disabled, 1 = enabled, -1 = don't do anything). " + "default is -1"); + +module_param(wwan_status, int, 0444); +MODULE_PARM_DESC(wwan_status, "Set the wireless status on boot " + "(0 = disabled, 1 = enabled, -1 = don't do anything). " + "default is -1"); + +module_param(als_status, int, 0444); +MODULE_PARM_DESC(als_status, "Set the ALS status on boot " + "(0 = disabled, 1 = enabled). " + "default is 0"); + +/* + * Some events we use, same for all Asus + */ +#define ATKD_BRNUP_MIN 0x10 +#define ATKD_BRNUP_MAX 0x1f +#define ATKD_BRNDOWN_MIN 0x20 +#define ATKD_BRNDOWN_MAX 0x2f +#define ATKD_BRNDOWN 0x20 +#define ATKD_BRNUP 0x2f +#define ATKD_LCD_ON 0x33 +#define ATKD_LCD_OFF 0x34 + +/* + * Known bits returned by \_SB.ATKD.HWRS + */ +#define WL_HWRS 0x80 +#define BT_HWRS 0x100 + +/* + * Flags for hotk status + * WL_ON and BT_ON are also used for wireless_status() + */ +#define WL_RSTS 0x01 /* internal Wifi */ +#define BT_RSTS 0x02 /* internal Bluetooth */ +#define WM_RSTS 0x08 /* internal wimax */ +#define WW_RSTS 0x20 /* internal wwan */ + +/* WLED and BLED type */ +#define TYPE_UNKNOWN 0 +#define TYPE_LED 1 +#define TYPE_RFKILL 2 + +/* LED */ +#define METHOD_MLED "MLED" +#define METHOD_TLED "TLED" +#define METHOD_RLED "RLED" /* W1JC */ +#define METHOD_PLED "PLED" /* A7J */ +#define METHOD_GLED "GLED" /* G1, G2 (probably) */ + +/* LEDD */ +#define METHOD_LEDD "SLCM" + +/* + * Bluetooth and WLAN + * WLED and BLED are not handled like other XLED, because in some dsdt + * they also control the WLAN/Bluetooth device. + */ +#define METHOD_WLAN "WLED" +#define METHOD_BLUETOOTH "BLED" + +/* WWAN and WIMAX */ +#define METHOD_WWAN "GSMC" +#define METHOD_WIMAX "WMXC" + +#define METHOD_WL_STATUS "RSTS" + +/* Brightness */ +#define METHOD_BRIGHTNESS_SET "SPLV" +#define METHOD_BRIGHTNESS_GET "GPLV" + +/* Display */ +#define METHOD_SWITCH_DISPLAY "SDSP" + +#define METHOD_ALS_CONTROL "ALSC" /* Z71A Z71V */ +#define METHOD_ALS_LEVEL "ALSL" /* Z71A Z71V */ + +/* GPS */ +/* R2H use different handle for GPS on/off */ +#define METHOD_GPS_ON "SDON" +#define METHOD_GPS_OFF "SDOF" +#define METHOD_GPS_STATUS "GPST" + +/* Keyboard light */ +#define METHOD_KBD_LIGHT_SET "SLKB" +#define METHOD_KBD_LIGHT_GET "GLKB" + +/* For Pegatron Lucid tablet */ +#define DEVICE_NAME_PEGA "Lucid" + +#define METHOD_PEGA_ENABLE "ENPR" +#define METHOD_PEGA_DISABLE "DAPR" +#define PEGA_WLAN 0x00 +#define PEGA_BLUETOOTH 0x01 +#define PEGA_WWAN 0x02 +#define PEGA_ALS 0x04 +#define PEGA_ALS_POWER 0x05 + +#define METHOD_PEGA_READ "RDLN" +#define PEGA_READ_ALS_H 0x02 +#define PEGA_READ_ALS_L 0x03 + +#define PEGA_ACCEL_NAME "pega_accel" +#define PEGA_ACCEL_DESC "Pegatron Lucid Tablet Accelerometer" +#define METHOD_XLRX "XLRX" +#define METHOD_XLRY "XLRY" +#define METHOD_XLRZ "XLRZ" +#define PEGA_ACC_CLAMP 512 /* 1G accel is reported as ~256, so clamp to 2G */ +#define PEGA_ACC_RETRIES 3 + +/* + * Define a specific led structure to keep the main structure clean + */ +struct asus_led { + int wk; + struct work_struct work; + struct led_classdev led; + struct asus_laptop *asus; + const char *method; +}; + +/* + * Same thing for rfkill + */ +struct asus_rfkill { + /* type of control. Maps to PEGA_* values or *_RSTS */ + int control_id; + struct rfkill *rfkill; + struct asus_laptop *asus; +}; + +/* + * This is the main structure, we can use it to store anything interesting + * about the hotk device + */ +struct asus_laptop { + char *name; /* laptop name */ + + struct acpi_table_header *dsdt_info; + struct platform_device *platform_device; + struct acpi_device *device; /* the device we are in */ + struct backlight_device *backlight_device; + + struct input_dev *inputdev; + struct key_entry *keymap; + struct input_dev *pega_accel_poll; + + struct asus_led wled; + struct asus_led bled; + struct asus_led mled; + struct asus_led tled; + struct asus_led rled; + struct asus_led pled; + struct asus_led gled; + struct asus_led kled; + struct workqueue_struct *led_workqueue; + + int wled_type; + int bled_type; + int wireless_status; + bool have_rsts; + bool is_pega_lucid; + bool pega_acc_live; + int pega_acc_x; + int pega_acc_y; + int pega_acc_z; + + struct asus_rfkill wlan; + struct asus_rfkill bluetooth; + struct asus_rfkill wwan; + struct asus_rfkill wimax; + struct asus_rfkill gps; + + acpi_handle handle; /* the handle of the hotk device */ + u32 ledd_status; /* status of the LED display */ + u8 light_level; /* light sensor level */ + u8 light_switch; /* light sensor switch value */ + u16 event_count[128]; /* count for each event TODO make this better */ +}; + +static const struct key_entry asus_keymap[] = { + /* Lenovo SL Specific keycodes */ + {KE_KEY, 0x02, { KEY_SCREENLOCK } }, + {KE_KEY, 0x05, { KEY_WLAN } }, + {KE_KEY, 0x08, { KEY_F13 } }, + {KE_KEY, 0x09, { KEY_PROG2 } }, /* Dock */ + {KE_KEY, 0x17, { KEY_ZOOM } }, + {KE_KEY, 0x1f, { KEY_BATTERY } }, + /* End of Lenovo SL Specific keycodes */ + {KE_KEY, ATKD_BRNDOWN, { KEY_BRIGHTNESSDOWN } }, + {KE_KEY, ATKD_BRNUP, { KEY_BRIGHTNESSUP } }, + {KE_KEY, 0x30, { KEY_VOLUMEUP } }, + {KE_KEY, 0x31, { KEY_VOLUMEDOWN } }, + {KE_KEY, 0x32, { KEY_MUTE } }, + {KE_KEY, 0x33, { KEY_DISPLAYTOGGLE } }, /* LCD on */ + {KE_KEY, 0x34, { KEY_DISPLAY_OFF } }, /* LCD off */ + {KE_KEY, 0x40, { KEY_PREVIOUSSONG } }, + {KE_KEY, 0x41, { KEY_NEXTSONG } }, + {KE_KEY, 0x43, { KEY_STOPCD } }, /* Stop/Eject */ + {KE_KEY, 0x45, { KEY_PLAYPAUSE } }, + {KE_KEY, 0x4c, { KEY_MEDIA } }, /* WMP Key */ + {KE_KEY, 0x50, { KEY_EMAIL } }, + {KE_KEY, 0x51, { KEY_WWW } }, + {KE_KEY, 0x55, { KEY_CALC } }, + {KE_IGNORE, 0x57, }, /* Battery mode */ + {KE_IGNORE, 0x58, }, /* AC mode */ + {KE_KEY, 0x5C, { KEY_SCREENLOCK } }, /* Screenlock */ + {KE_KEY, 0x5D, { KEY_WLAN } }, /* WLAN Toggle */ + {KE_KEY, 0x5E, { KEY_WLAN } }, /* WLAN Enable */ + {KE_KEY, 0x5F, { KEY_WLAN } }, /* WLAN Disable */ + {KE_KEY, 0x60, { KEY_TOUCHPAD_ON } }, + {KE_KEY, 0x61, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD only */ + {KE_KEY, 0x62, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT only */ + {KE_KEY, 0x63, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT */ + {KE_KEY, 0x64, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV */ + {KE_KEY, 0x65, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV */ + {KE_KEY, 0x66, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV */ + {KE_KEY, 0x67, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV */ + {KE_KEY, 0x6A, { KEY_TOUCHPAD_TOGGLE } }, /* Lock Touchpad Fn + F9 */ + {KE_KEY, 0x6B, { KEY_TOUCHPAD_TOGGLE } }, /* Lock Touchpad */ + {KE_KEY, 0x6C, { KEY_SLEEP } }, /* Suspend */ + {KE_KEY, 0x6D, { KEY_SLEEP } }, /* Hibernate */ + {KE_IGNORE, 0x6E, }, /* Low Battery notification */ + {KE_KEY, 0x7D, { KEY_BLUETOOTH } }, /* Bluetooth Enable */ + {KE_KEY, 0x7E, { KEY_BLUETOOTH } }, /* Bluetooth Disable */ + {KE_KEY, 0x82, { KEY_CAMERA } }, + {KE_KEY, 0x88, { KEY_RFKILL } }, /* Radio Toggle Key */ + {KE_KEY, 0x8A, { KEY_PROG1 } }, /* Color enhancement mode */ + {KE_KEY, 0x8C, { KEY_SWITCHVIDEOMODE } }, /* SDSP DVI only */ + {KE_KEY, 0x8D, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + DVI */ + {KE_KEY, 0x8E, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + DVI */ + {KE_KEY, 0x8F, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV + DVI */ + {KE_KEY, 0x90, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + DVI */ + {KE_KEY, 0x91, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV + DVI */ + {KE_KEY, 0x92, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV + DVI */ + {KE_KEY, 0x93, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV + DVI */ + {KE_KEY, 0x95, { KEY_MEDIA } }, + {KE_KEY, 0x99, { KEY_PHONE } }, + {KE_KEY, 0xA0, { KEY_SWITCHVIDEOMODE } }, /* SDSP HDMI only */ + {KE_KEY, 0xA1, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + HDMI */ + {KE_KEY, 0xA2, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + HDMI */ + {KE_KEY, 0xA3, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV + HDMI */ + {KE_KEY, 0xA4, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + HDMI */ + {KE_KEY, 0xA5, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV + HDMI */ + {KE_KEY, 0xA6, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV + HDMI */ + {KE_KEY, 0xA7, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV + HDMI */ + {KE_KEY, 0xB5, { KEY_CALC } }, + {KE_KEY, 0xC4, { KEY_KBDILLUMUP } }, + {KE_KEY, 0xC5, { KEY_KBDILLUMDOWN } }, + {KE_END, 0}, +}; + + +/* + * This function evaluates an ACPI method, given an int as parameter, the + * method is searched within the scope of the handle, can be NULL. The output + * of the method is written is output, which can also be NULL + * + * returns 0 if write is successful, -1 else. + */ +static int write_acpi_int_ret(acpi_handle handle, const char *method, int val, + struct acpi_buffer *output) +{ + struct acpi_object_list params; /* list of input parameters (an int) */ + union acpi_object in_obj; /* the only param we use */ + acpi_status status; + + if (!handle) + return -1; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = val; + + status = acpi_evaluate_object(handle, (char *)method, ¶ms, output); + if (status == AE_OK) + return 0; + else + return -1; +} + +static int write_acpi_int(acpi_handle handle, const char *method, int val) +{ + return write_acpi_int_ret(handle, method, val, NULL); +} + +static int acpi_check_handle(acpi_handle handle, const char *method, + acpi_handle *ret) +{ + acpi_status status; + + if (method == NULL) + return -ENODEV; + + if (ret) + status = acpi_get_handle(handle, (char *)method, + ret); + else { + acpi_handle dummy; + + status = acpi_get_handle(handle, (char *)method, + &dummy); + } + + if (status != AE_OK) { + if (ret) + pr_warn("Error finding %s\n", method); + return -ENODEV; + } + return 0; +} + +static bool asus_check_pega_lucid(struct asus_laptop *asus) +{ + return !strcmp(asus->name, DEVICE_NAME_PEGA) && + !acpi_check_handle(asus->handle, METHOD_PEGA_ENABLE, NULL) && + !acpi_check_handle(asus->handle, METHOD_PEGA_DISABLE, NULL) && + !acpi_check_handle(asus->handle, METHOD_PEGA_READ, NULL); +} + +static int asus_pega_lucid_set(struct asus_laptop *asus, int unit, bool enable) +{ + char *method = enable ? METHOD_PEGA_ENABLE : METHOD_PEGA_DISABLE; + return write_acpi_int(asus->handle, method, unit); +} + +static int pega_acc_axis(struct asus_laptop *asus, int curr, char *method) +{ + unsigned long long val = (unsigned long long)curr; + acpi_status status; + int i, delta; + + for (i = 0; i < PEGA_ACC_RETRIES; i++) { + status = acpi_evaluate_integer(asus->handle, method, NULL, &val); + if (ACPI_FAILURE(status)) + continue; + /* The output is noisy. From reading the ASL + * dissassembly, timeout errors are returned with 1's + * in the high word, and the lack of locking around + * thei hi/lo byte reads means that a transition + * between (for example) -1 and 0 could be read as + * 0xff00 or 0x00ff. */ + delta = abs(curr - (short)val); + if (delta < 128 && !(val & ~0xffff)) + break; + } + return clamp_val((short)val, -PEGA_ACC_CLAMP, PEGA_ACC_CLAMP); +} + +static void pega_accel_poll(struct input_dev *input) +{ + struct device *parent = input->dev.parent; + struct asus_laptop *asus = dev_get_drvdata(parent); + + /* In some cases, the very first call to poll causes a + * recursive fault under the polldev worker. This is + * apparently related to very early userspace access to the + * device, and perhaps a firmware bug. Fake the first report. */ + if (!asus->pega_acc_live) { + asus->pega_acc_live = true; + input_report_abs(input, ABS_X, 0); + input_report_abs(input, ABS_Y, 0); + input_report_abs(input, ABS_Z, 0); + input_sync(input); + return; + } + + asus->pega_acc_x = pega_acc_axis(asus, asus->pega_acc_x, METHOD_XLRX); + asus->pega_acc_y = pega_acc_axis(asus, asus->pega_acc_y, METHOD_XLRY); + asus->pega_acc_z = pega_acc_axis(asus, asus->pega_acc_z, METHOD_XLRZ); + + /* Note transform, convert to "right/up/out" in the native + * landscape orientation (i.e. the vector is the direction of + * "real up" in the device's cartiesian coordinates). */ + input_report_abs(input, ABS_X, -asus->pega_acc_x); + input_report_abs(input, ABS_Y, -asus->pega_acc_y); + input_report_abs(input, ABS_Z, asus->pega_acc_z); + input_sync(input); +} + +static void pega_accel_exit(struct asus_laptop *asus) +{ + if (asus->pega_accel_poll) { + input_unregister_device(asus->pega_accel_poll); + asus->pega_accel_poll = NULL; + } +} + +static int pega_accel_init(struct asus_laptop *asus) +{ + int err; + struct input_dev *input; + + if (!asus->is_pega_lucid) + return -ENODEV; + + if (acpi_check_handle(asus->handle, METHOD_XLRX, NULL) || + acpi_check_handle(asus->handle, METHOD_XLRY, NULL) || + acpi_check_handle(asus->handle, METHOD_XLRZ, NULL)) + return -ENODEV; + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input->name = PEGA_ACCEL_DESC; + input->phys = PEGA_ACCEL_NAME "/input0"; + input->dev.parent = &asus->platform_device->dev; + input->id.bustype = BUS_HOST; + + input_set_abs_params(input, ABS_X, + -PEGA_ACC_CLAMP, PEGA_ACC_CLAMP, 0, 0); + input_set_abs_params(input, ABS_Y, + -PEGA_ACC_CLAMP, PEGA_ACC_CLAMP, 0, 0); + input_set_abs_params(input, ABS_Z, + -PEGA_ACC_CLAMP, PEGA_ACC_CLAMP, 0, 0); + + err = input_setup_polling(input, pega_accel_poll); + if (err) + goto exit; + + input_set_poll_interval(input, 125); + input_set_min_poll_interval(input, 50); + input_set_max_poll_interval(input, 2000); + + err = input_register_device(input); + if (err) + goto exit; + + asus->pega_accel_poll = input; + return 0; + +exit: + input_free_device(input); + return err; +} + +/* Generic LED function */ +static int asus_led_set(struct asus_laptop *asus, const char *method, + int value) +{ + if (!strcmp(method, METHOD_MLED)) + value = !value; + else if (!strcmp(method, METHOD_GLED)) + value = !value + 1; + else + value = !!value; + + return write_acpi_int(asus->handle, method, value); +} + +/* + * LEDs + */ +/* /sys/class/led handlers */ +static void asus_led_cdev_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct asus_led *led = container_of(led_cdev, struct asus_led, led); + struct asus_laptop *asus = led->asus; + + led->wk = !!value; + queue_work(asus->led_workqueue, &led->work); +} + +static void asus_led_cdev_update(struct work_struct *work) +{ + struct asus_led *led = container_of(work, struct asus_led, work); + struct asus_laptop *asus = led->asus; + + asus_led_set(asus, led->method, led->wk); +} + +static enum led_brightness asus_led_cdev_get(struct led_classdev *led_cdev) +{ + return led_cdev->brightness; +} + +/* + * Keyboard backlight (also a LED) + */ +static int asus_kled_lvl(struct asus_laptop *asus) +{ + unsigned long long kblv; + struct acpi_object_list params; + union acpi_object in_obj; + acpi_status rv; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = 2; + + rv = acpi_evaluate_integer(asus->handle, METHOD_KBD_LIGHT_GET, + ¶ms, &kblv); + if (ACPI_FAILURE(rv)) { + pr_warn("Error reading kled level\n"); + return -ENODEV; + } + return kblv; +} + +static int asus_kled_set(struct asus_laptop *asus, int kblv) +{ + if (kblv > 0) + kblv = (1 << 7) | (kblv & 0x7F); + else + kblv = 0; + + if (write_acpi_int(asus->handle, METHOD_KBD_LIGHT_SET, kblv)) { + pr_warn("Keyboard LED display write failed\n"); + return -EINVAL; + } + return 0; +} + +static void asus_kled_cdev_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct asus_led *led = container_of(led_cdev, struct asus_led, led); + struct asus_laptop *asus = led->asus; + + led->wk = value; + queue_work(asus->led_workqueue, &led->work); +} + +static void asus_kled_cdev_update(struct work_struct *work) +{ + struct asus_led *led = container_of(work, struct asus_led, work); + struct asus_laptop *asus = led->asus; + + asus_kled_set(asus, led->wk); +} + +static enum led_brightness asus_kled_cdev_get(struct led_classdev *led_cdev) +{ + struct asus_led *led = container_of(led_cdev, struct asus_led, led); + struct asus_laptop *asus = led->asus; + + return asus_kled_lvl(asus); +} + +static void asus_led_exit(struct asus_laptop *asus) +{ + led_classdev_unregister(&asus->wled.led); + led_classdev_unregister(&asus->bled.led); + led_classdev_unregister(&asus->mled.led); + led_classdev_unregister(&asus->tled.led); + led_classdev_unregister(&asus->pled.led); + led_classdev_unregister(&asus->rled.led); + led_classdev_unregister(&asus->gled.led); + led_classdev_unregister(&asus->kled.led); + + if (asus->led_workqueue) { + destroy_workqueue(asus->led_workqueue); + asus->led_workqueue = NULL; + } +} + +/* Ugly macro, need to fix that later */ +static int asus_led_register(struct asus_laptop *asus, + struct asus_led *led, + const char *name, const char *method) +{ + struct led_classdev *led_cdev = &led->led; + + if (!method || acpi_check_handle(asus->handle, method, NULL)) + return 0; /* Led not present */ + + led->asus = asus; + led->method = method; + + INIT_WORK(&led->work, asus_led_cdev_update); + led_cdev->name = name; + led_cdev->brightness_set = asus_led_cdev_set; + led_cdev->brightness_get = asus_led_cdev_get; + led_cdev->max_brightness = 1; + return led_classdev_register(&asus->platform_device->dev, led_cdev); +} + +static int asus_led_init(struct asus_laptop *asus) +{ + int r = 0; + + /* + * The Pegatron Lucid has no physical leds, but all methods are + * available in the DSDT... + */ + if (asus->is_pega_lucid) + return 0; + + /* + * Functions that actually update the LED's are called from a + * workqueue. By doing this as separate work rather than when the LED + * subsystem asks, we avoid messing with the Asus ACPI stuff during a + * potentially bad time, such as a timer interrupt. + */ + asus->led_workqueue = create_singlethread_workqueue("led_workqueue"); + if (!asus->led_workqueue) + return -ENOMEM; + + if (asus->wled_type == TYPE_LED) + r = asus_led_register(asus, &asus->wled, "asus::wlan", + METHOD_WLAN); + if (r) + goto error; + if (asus->bled_type == TYPE_LED) + r = asus_led_register(asus, &asus->bled, "asus::bluetooth", + METHOD_BLUETOOTH); + if (r) + goto error; + r = asus_led_register(asus, &asus->mled, "asus::mail", METHOD_MLED); + if (r) + goto error; + r = asus_led_register(asus, &asus->tled, "asus::touchpad", METHOD_TLED); + if (r) + goto error; + r = asus_led_register(asus, &asus->rled, "asus::record", METHOD_RLED); + if (r) + goto error; + r = asus_led_register(asus, &asus->pled, "asus::phone", METHOD_PLED); + if (r) + goto error; + r = asus_led_register(asus, &asus->gled, "asus::gaming", METHOD_GLED); + if (r) + goto error; + if (!acpi_check_handle(asus->handle, METHOD_KBD_LIGHT_SET, NULL) && + !acpi_check_handle(asus->handle, METHOD_KBD_LIGHT_GET, NULL)) { + struct asus_led *led = &asus->kled; + struct led_classdev *cdev = &led->led; + + led->asus = asus; + + INIT_WORK(&led->work, asus_kled_cdev_update); + cdev->name = "asus::kbd_backlight"; + cdev->brightness_set = asus_kled_cdev_set; + cdev->brightness_get = asus_kled_cdev_get; + cdev->max_brightness = 3; + r = led_classdev_register(&asus->platform_device->dev, cdev); + } +error: + if (r) + asus_led_exit(asus); + return r; +} + +/* + * Backlight device + */ +static int asus_read_brightness(struct backlight_device *bd) +{ + struct asus_laptop *asus = bl_get_data(bd); + unsigned long long value; + acpi_status rv; + + rv = acpi_evaluate_integer(asus->handle, METHOD_BRIGHTNESS_GET, + NULL, &value); + if (ACPI_FAILURE(rv)) { + pr_warn("Error reading brightness\n"); + return 0; + } + + return value; +} + +static int asus_set_brightness(struct backlight_device *bd, int value) +{ + struct asus_laptop *asus = bl_get_data(bd); + + if (write_acpi_int(asus->handle, METHOD_BRIGHTNESS_SET, value)) { + pr_warn("Error changing brightness\n"); + return -EIO; + } + return 0; +} + +static int update_bl_status(struct backlight_device *bd) +{ + int value = bd->props.brightness; + + return asus_set_brightness(bd, value); +} + +static const struct backlight_ops asusbl_ops = { + .get_brightness = asus_read_brightness, + .update_status = update_bl_status, +}; + +static int asus_backlight_notify(struct asus_laptop *asus) +{ + struct backlight_device *bd = asus->backlight_device; + int old = bd->props.brightness; + + backlight_force_update(bd, BACKLIGHT_UPDATE_HOTKEY); + + return old; +} + +static int asus_backlight_init(struct asus_laptop *asus) +{ + struct backlight_device *bd; + struct backlight_properties props; + + if (acpi_check_handle(asus->handle, METHOD_BRIGHTNESS_GET, NULL) || + acpi_check_handle(asus->handle, METHOD_BRIGHTNESS_SET, NULL)) + return 0; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.max_brightness = 15; + props.type = BACKLIGHT_PLATFORM; + + bd = backlight_device_register(ASUS_LAPTOP_FILE, + &asus->platform_device->dev, asus, + &asusbl_ops, &props); + if (IS_ERR(bd)) { + pr_err("Could not register asus backlight device\n"); + asus->backlight_device = NULL; + return PTR_ERR(bd); + } + + asus->backlight_device = bd; + bd->props.brightness = asus_read_brightness(bd); + bd->props.power = BACKLIGHT_POWER_ON; + backlight_update_status(bd); + return 0; +} + +static void asus_backlight_exit(struct asus_laptop *asus) +{ + backlight_device_unregister(asus->backlight_device); + asus->backlight_device = NULL; +} + +/* + * Platform device handlers + */ + +/* + * We write our info in page, we begin at offset off and cannot write more + * than count bytes. We set eof to 1 if we handle those 2 values. We return the + * number of bytes written in page + */ +static ssize_t infos_show(struct device *dev, struct device_attribute *attr, + char *page) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int len = 0; + unsigned long long temp; + char buf[16]; /* enough for all info */ + acpi_status rv; + + /* + * We use the easy way, we don't care of off and count, + * so we don't set eof to 1 + */ + + len += sysfs_emit_at(page, len, ASUS_LAPTOP_NAME " " ASUS_LAPTOP_VERSION "\n"); + len += sysfs_emit_at(page, len, "Model reference : %s\n", asus->name); + /* + * The SFUN method probably allows the original driver to get the list + * of features supported by a given model. For now, 0x0100 or 0x0800 + * bit signifies that the laptop is equipped with a Wi-Fi MiniPCI card. + * The significance of others is yet to be found. + */ + rv = acpi_evaluate_integer(asus->handle, "SFUN", NULL, &temp); + if (ACPI_SUCCESS(rv)) + len += sysfs_emit_at(page, len, "SFUN value : %#x\n", + (uint) temp); + /* + * The HWRS method return informations about the hardware. + * 0x80 bit is for WLAN, 0x100 for Bluetooth. + * 0x40 for WWAN, 0x10 for WIMAX. + * The significance of others is yet to be found. + * We don't currently use this for device detection, and it + * takes several seconds to run on some systems. + */ + rv = acpi_evaluate_integer(asus->handle, "HWRS", NULL, &temp); + if (ACPI_SUCCESS(rv)) + len += sysfs_emit_at(page, len, "HWRS value : %#x\n", + (uint) temp); + /* + * Another value for userspace: the ASYM method returns 0x02 for + * battery low and 0x04 for battery critical, its readings tend to be + * more accurate than those provided by _BST. + * Note: since not all the laptops provide this method, errors are + * silently ignored. + */ + rv = acpi_evaluate_integer(asus->handle, "ASYM", NULL, &temp); + if (ACPI_SUCCESS(rv)) + len += sysfs_emit_at(page, len, "ASYM value : %#x\n", + (uint) temp); + if (asus->dsdt_info) { + snprintf(buf, 16, "%d", asus->dsdt_info->length); + len += sysfs_emit_at(page, len, "DSDT length : %s\n", buf); + snprintf(buf, 16, "%d", asus->dsdt_info->checksum); + len += sysfs_emit_at(page, len, "DSDT checksum : %s\n", buf); + snprintf(buf, 16, "%d", asus->dsdt_info->revision); + len += sysfs_emit_at(page, len, "DSDT revision : %s\n", buf); + snprintf(buf, 7, "%s", asus->dsdt_info->oem_id); + len += sysfs_emit_at(page, len, "OEM id : %s\n", buf); + snprintf(buf, 9, "%s", asus->dsdt_info->oem_table_id); + len += sysfs_emit_at(page, len, "OEM table id : %s\n", buf); + snprintf(buf, 16, "%x", asus->dsdt_info->oem_revision); + len += sysfs_emit_at(page, len, "OEM revision : 0x%s\n", buf); + snprintf(buf, 5, "%s", asus->dsdt_info->asl_compiler_id); + len += sysfs_emit_at(page, len, "ASL comp vendor id : %s\n", buf); + snprintf(buf, 16, "%x", asus->dsdt_info->asl_compiler_revision); + len += sysfs_emit_at(page, len, "ASL comp revision : 0x%s\n", buf); + } + + return len; +} +static DEVICE_ATTR_RO(infos); + +static ssize_t sysfs_acpi_set(struct asus_laptop *asus, + const char *buf, size_t count, + const char *method) +{ + int rv, value; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + + if (write_acpi_int(asus->handle, method, value)) + return -ENODEV; + return count; +} + +/* + * LEDD display + */ +static ssize_t ledd_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "0x%08x\n", asus->ledd_status); +} + +static ssize_t ledd_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int rv, value; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + + if (write_acpi_int(asus->handle, METHOD_LEDD, value)) { + pr_warn("LED display write failed\n"); + return -ENODEV; + } + + asus->ledd_status = (u32) value; + return count; +} +static DEVICE_ATTR_RW(ledd); + +/* + * Wireless + */ +static int asus_wireless_status(struct asus_laptop *asus, int mask) +{ + unsigned long long status; + acpi_status rv = AE_OK; + + if (!asus->have_rsts) + return (asus->wireless_status & mask) ? 1 : 0; + + rv = acpi_evaluate_integer(asus->handle, METHOD_WL_STATUS, + NULL, &status); + if (ACPI_FAILURE(rv)) { + pr_warn("Error reading Wireless status\n"); + return -EINVAL; + } + return !!(status & mask); +} + +/* + * WLAN + */ +static int asus_wlan_set(struct asus_laptop *asus, int status) +{ + if (write_acpi_int(asus->handle, METHOD_WLAN, !!status)) { + pr_warn("Error setting wlan status to %d\n", status); + return -EIO; + } + return 0; +} + +static ssize_t wlan_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus_wireless_status(asus, WL_RSTS)); +} + +static ssize_t wlan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_acpi_set(asus, buf, count, METHOD_WLAN); +} +static DEVICE_ATTR_RW(wlan); + +/*e + * Bluetooth + */ +static int asus_bluetooth_set(struct asus_laptop *asus, int status) +{ + if (write_acpi_int(asus->handle, METHOD_BLUETOOTH, !!status)) { + pr_warn("Error setting bluetooth status to %d\n", status); + return -EIO; + } + return 0; +} + +static ssize_t bluetooth_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus_wireless_status(asus, BT_RSTS)); +} + +static ssize_t bluetooth_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_acpi_set(asus, buf, count, METHOD_BLUETOOTH); +} +static DEVICE_ATTR_RW(bluetooth); + +/* + * Wimax + */ +static int asus_wimax_set(struct asus_laptop *asus, int status) +{ + if (write_acpi_int(asus->handle, METHOD_WIMAX, !!status)) { + pr_warn("Error setting wimax status to %d\n", status); + return -EIO; + } + return 0; +} + +static ssize_t wimax_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus_wireless_status(asus, WM_RSTS)); +} + +static ssize_t wimax_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_acpi_set(asus, buf, count, METHOD_WIMAX); +} +static DEVICE_ATTR_RW(wimax); + +/* + * Wwan + */ +static int asus_wwan_set(struct asus_laptop *asus, int status) +{ + if (write_acpi_int(asus->handle, METHOD_WWAN, !!status)) { + pr_warn("Error setting wwan status to %d\n", status); + return -EIO; + } + return 0; +} + +static ssize_t wwan_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus_wireless_status(asus, WW_RSTS)); +} + +static ssize_t wwan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_acpi_set(asus, buf, count, METHOD_WWAN); +} +static DEVICE_ATTR_RW(wwan); + +/* + * Display + */ +static void asus_set_display(struct asus_laptop *asus, int value) +{ + /* no sanity check needed for now */ + if (write_acpi_int(asus->handle, METHOD_SWITCH_DISPLAY, value)) + pr_warn("Error setting display\n"); + return; +} + +/* + * Experimental support for display switching. As of now: 1 should activate + * the LCD output, 2 should do for CRT, 4 for TV-Out and 8 for DVI. + * Any combination (bitwise) of these will suffice. I never actually tested 4 + * displays hooked up simultaneously, so be warned. See the acpi4asus README + * for more info. + */ +static ssize_t display_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int rv, value; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + + asus_set_display(asus, value); + return count; +} +static DEVICE_ATTR_WO(display); + +/* + * Light Sens + */ +static void asus_als_switch(struct asus_laptop *asus, int value) +{ + int ret; + + if (asus->is_pega_lucid) { + ret = asus_pega_lucid_set(asus, PEGA_ALS, value); + if (!ret) + ret = asus_pega_lucid_set(asus, PEGA_ALS_POWER, value); + } else { + ret = write_acpi_int(asus->handle, METHOD_ALS_CONTROL, value); + } + if (ret) + pr_warn("Error setting light sensor switch\n"); + + asus->light_switch = value; +} + +static ssize_t ls_switch_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus->light_switch); +} + +static ssize_t ls_switch_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int rv, value; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + + asus_als_switch(asus, value ? 1 : 0); + return count; +} +static DEVICE_ATTR_RW(ls_switch); + +static void asus_als_level(struct asus_laptop *asus, int value) +{ + if (write_acpi_int(asus->handle, METHOD_ALS_LEVEL, value)) + pr_warn("Error setting light sensor level\n"); + asus->light_level = value; +} + +static ssize_t ls_level_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus->light_level); +} + +static ssize_t ls_level_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int rv, value; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + + value = (0 < value) ? ((15 < value) ? 15 : value) : 0; + /* 0 <= value <= 15 */ + asus_als_level(asus, value); + + return count; +} +static DEVICE_ATTR_RW(ls_level); + +static int pega_int_read(struct asus_laptop *asus, int arg, int *result) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + int err = write_acpi_int_ret(asus->handle, METHOD_PEGA_READ, arg, + &buffer); + if (!err) { + union acpi_object *obj = buffer.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + *result = obj->integer.value; + else + err = -EIO; + } + return err; +} + +static ssize_t ls_value_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int err, hi, lo; + + err = pega_int_read(asus, PEGA_READ_ALS_H, &hi); + if (!err) + err = pega_int_read(asus, PEGA_READ_ALS_L, &lo); + if (!err) + return sysfs_emit(buf, "%d\n", 10 * hi + lo); + return err; +} +static DEVICE_ATTR_RO(ls_value); + +/* + * GPS + */ +static int asus_gps_status(struct asus_laptop *asus) +{ + unsigned long long status; + acpi_status rv; + + rv = acpi_evaluate_integer(asus->handle, METHOD_GPS_STATUS, + NULL, &status); + if (ACPI_FAILURE(rv)) { + pr_warn("Error reading GPS status\n"); + return -ENODEV; + } + return !!status; +} + +static int asus_gps_switch(struct asus_laptop *asus, int status) +{ + const char *meth = status ? METHOD_GPS_ON : METHOD_GPS_OFF; + + if (write_acpi_int(asus->handle, meth, 0x02)) + return -ENODEV; + return 0; +} + +static ssize_t gps_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus_gps_status(asus)); +} + +static ssize_t gps_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_laptop *asus = dev_get_drvdata(dev); + int rv, value; + int ret; + + rv = kstrtoint(buf, 0, &value); + if (rv < 0) + return rv; + ret = asus_gps_switch(asus, !!value); + if (ret) + return ret; + rfkill_set_sw_state(asus->gps.rfkill, !value); + return count; +} +static DEVICE_ATTR_RW(gps); + +/* + * rfkill + */ +static int asus_gps_rfkill_set(void *data, bool blocked) +{ + struct asus_laptop *asus = data; + + return asus_gps_switch(asus, !blocked); +} + +static const struct rfkill_ops asus_gps_rfkill_ops = { + .set_block = asus_gps_rfkill_set, +}; + +static int asus_rfkill_set(void *data, bool blocked) +{ + struct asus_rfkill *rfk = data; + struct asus_laptop *asus = rfk->asus; + + if (rfk->control_id == WL_RSTS) + return asus_wlan_set(asus, !blocked); + else if (rfk->control_id == BT_RSTS) + return asus_bluetooth_set(asus, !blocked); + else if (rfk->control_id == WM_RSTS) + return asus_wimax_set(asus, !blocked); + else if (rfk->control_id == WW_RSTS) + return asus_wwan_set(asus, !blocked); + + return -EINVAL; +} + +static const struct rfkill_ops asus_rfkill_ops = { + .set_block = asus_rfkill_set, +}; + +static void asus_rfkill_terminate(struct asus_rfkill *rfk) +{ + if (!rfk->rfkill) + return ; + + rfkill_unregister(rfk->rfkill); + rfkill_destroy(rfk->rfkill); + rfk->rfkill = NULL; +} + +static void asus_rfkill_exit(struct asus_laptop *asus) +{ + asus_rfkill_terminate(&asus->wwan); + asus_rfkill_terminate(&asus->bluetooth); + asus_rfkill_terminate(&asus->wlan); + asus_rfkill_terminate(&asus->gps); +} + +static int asus_rfkill_setup(struct asus_laptop *asus, struct asus_rfkill *rfk, + const char *name, int control_id, int type, + const struct rfkill_ops *ops) +{ + int result; + + rfk->control_id = control_id; + rfk->asus = asus; + rfk->rfkill = rfkill_alloc(name, &asus->platform_device->dev, + type, ops, rfk); + if (!rfk->rfkill) + return -EINVAL; + + result = rfkill_register(rfk->rfkill); + if (result) { + rfkill_destroy(rfk->rfkill); + rfk->rfkill = NULL; + } + + return result; +} + +static int asus_rfkill_init(struct asus_laptop *asus) +{ + int result = 0; + + if (asus->is_pega_lucid) + return -ENODEV; + + if (!acpi_check_handle(asus->handle, METHOD_GPS_ON, NULL) && + !acpi_check_handle(asus->handle, METHOD_GPS_OFF, NULL) && + !acpi_check_handle(asus->handle, METHOD_GPS_STATUS, NULL)) + result = asus_rfkill_setup(asus, &asus->gps, "asus-gps", + -1, RFKILL_TYPE_GPS, + &asus_gps_rfkill_ops); + if (result) + goto exit; + + + if (!acpi_check_handle(asus->handle, METHOD_WLAN, NULL) && + asus->wled_type == TYPE_RFKILL) + result = asus_rfkill_setup(asus, &asus->wlan, "asus-wlan", + WL_RSTS, RFKILL_TYPE_WLAN, + &asus_rfkill_ops); + if (result) + goto exit; + + if (!acpi_check_handle(asus->handle, METHOD_BLUETOOTH, NULL) && + asus->bled_type == TYPE_RFKILL) + result = asus_rfkill_setup(asus, &asus->bluetooth, + "asus-bluetooth", BT_RSTS, + RFKILL_TYPE_BLUETOOTH, + &asus_rfkill_ops); + if (result) + goto exit; + + if (!acpi_check_handle(asus->handle, METHOD_WWAN, NULL)) + result = asus_rfkill_setup(asus, &asus->wwan, "asus-wwan", + WW_RSTS, RFKILL_TYPE_WWAN, + &asus_rfkill_ops); + if (result) + goto exit; + + if (!acpi_check_handle(asus->handle, METHOD_WIMAX, NULL)) + result = asus_rfkill_setup(asus, &asus->wimax, "asus-wimax", + WM_RSTS, RFKILL_TYPE_WIMAX, + &asus_rfkill_ops); + if (result) + goto exit; + +exit: + if (result) + asus_rfkill_exit(asus); + + return result; +} + +static int pega_rfkill_set(void *data, bool blocked) +{ + struct asus_rfkill *rfk = data; + + int ret = asus_pega_lucid_set(rfk->asus, rfk->control_id, !blocked); + return ret; +} + +static const struct rfkill_ops pega_rfkill_ops = { + .set_block = pega_rfkill_set, +}; + +static int pega_rfkill_setup(struct asus_laptop *asus, struct asus_rfkill *rfk, + const char *name, int controlid, int rfkill_type) +{ + return asus_rfkill_setup(asus, rfk, name, controlid, rfkill_type, + &pega_rfkill_ops); +} + +static int pega_rfkill_init(struct asus_laptop *asus) +{ + int ret = 0; + + if(!asus->is_pega_lucid) + return -ENODEV; + + ret = pega_rfkill_setup(asus, &asus->wlan, "pega-wlan", + PEGA_WLAN, RFKILL_TYPE_WLAN); + if(ret) + goto exit; + + ret = pega_rfkill_setup(asus, &asus->bluetooth, "pega-bt", + PEGA_BLUETOOTH, RFKILL_TYPE_BLUETOOTH); + if(ret) + goto exit; + + ret = pega_rfkill_setup(asus, &asus->wwan, "pega-wwan", + PEGA_WWAN, RFKILL_TYPE_WWAN); + +exit: + if (ret) + asus_rfkill_exit(asus); + + return ret; +} + +/* + * Input device (i.e. hotkeys) + */ +static void asus_input_notify(struct asus_laptop *asus, int event) +{ + if (!asus->inputdev) + return ; + if (!sparse_keymap_report_event(asus->inputdev, event, 1, true)) + pr_info("Unknown key %x pressed\n", event); +} + +static int asus_input_init(struct asus_laptop *asus) +{ + struct input_dev *input; + int error; + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input->name = "Asus Laptop extra buttons"; + input->phys = ASUS_LAPTOP_FILE "/input0"; + input->id.bustype = BUS_HOST; + input->dev.parent = &asus->platform_device->dev; + + error = sparse_keymap_setup(input, asus_keymap, NULL); + if (error) { + pr_err("Unable to setup input device keymap\n"); + goto err_free_dev; + } + error = input_register_device(input); + if (error) { + pr_warn("Unable to register input device\n"); + goto err_free_dev; + } + + asus->inputdev = input; + return 0; + +err_free_dev: + input_free_device(input); + return error; +} + +static void asus_input_exit(struct asus_laptop *asus) +{ + if (asus->inputdev) + input_unregister_device(asus->inputdev); + asus->inputdev = NULL; +} + +/* + * ACPI driver + */ +static void asus_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct asus_laptop *asus = data; + u16 count; + + /* TODO Find a better way to handle events count. */ + count = asus->event_count[event % 128]++; + acpi_bus_generate_netlink_event(asus->device->pnp.device_class, + dev_name(&asus->device->dev), event, + count); + + if (event >= ATKD_BRNUP_MIN && event <= ATKD_BRNUP_MAX) + event = ATKD_BRNUP; + else if (event >= ATKD_BRNDOWN_MIN && + event <= ATKD_BRNDOWN_MAX) + event = ATKD_BRNDOWN; + + /* Brightness events are special */ + if (event == ATKD_BRNDOWN || event == ATKD_BRNUP) { + if (asus->backlight_device != NULL) { + /* Update the backlight device. */ + asus_backlight_notify(asus); + return ; + } + } + + /* Accelerometer "coarse orientation change" event */ + if (asus->pega_accel_poll && event == 0xEA) { + kobject_uevent(&asus->pega_accel_poll->dev.kobj, KOBJ_CHANGE); + return ; + } + + asus_input_notify(asus, event); +} + +static struct attribute *asus_attributes[] = { + &dev_attr_infos.attr, + &dev_attr_wlan.attr, + &dev_attr_bluetooth.attr, + &dev_attr_wimax.attr, + &dev_attr_wwan.attr, + &dev_attr_display.attr, + &dev_attr_ledd.attr, + &dev_attr_ls_value.attr, + &dev_attr_ls_level.attr, + &dev_attr_ls_switch.attr, + &dev_attr_gps.attr, + NULL +}; + +static umode_t asus_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, + int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct asus_laptop *asus = dev_get_drvdata(dev); + acpi_handle handle = asus->handle; + bool supported; + + if (asus->is_pega_lucid) { + /* no ls_level interface on the Lucid */ + if (attr == &dev_attr_ls_switch.attr) + supported = true; + else if (attr == &dev_attr_ls_level.attr) + supported = false; + else + goto normal; + + return supported ? attr->mode : 0; + } + +normal: + if (attr == &dev_attr_wlan.attr) { + supported = !acpi_check_handle(handle, METHOD_WLAN, NULL); + + } else if (attr == &dev_attr_bluetooth.attr) { + supported = !acpi_check_handle(handle, METHOD_BLUETOOTH, NULL); + + } else if (attr == &dev_attr_display.attr) { + supported = !acpi_check_handle(handle, METHOD_SWITCH_DISPLAY, NULL); + + } else if (attr == &dev_attr_wimax.attr) { + supported = + !acpi_check_handle(asus->handle, METHOD_WIMAX, NULL); + + } else if (attr == &dev_attr_wwan.attr) { + supported = !acpi_check_handle(asus->handle, METHOD_WWAN, NULL); + + } else if (attr == &dev_attr_ledd.attr) { + supported = !acpi_check_handle(handle, METHOD_LEDD, NULL); + + } else if (attr == &dev_attr_ls_switch.attr || + attr == &dev_attr_ls_level.attr) { + supported = !acpi_check_handle(handle, METHOD_ALS_CONTROL, NULL) && + !acpi_check_handle(handle, METHOD_ALS_LEVEL, NULL); + } else if (attr == &dev_attr_ls_value.attr) { + supported = asus->is_pega_lucid; + } else if (attr == &dev_attr_gps.attr) { + supported = !acpi_check_handle(handle, METHOD_GPS_ON, NULL) && + !acpi_check_handle(handle, METHOD_GPS_OFF, NULL) && + !acpi_check_handle(handle, METHOD_GPS_STATUS, NULL); + } else { + supported = true; + } + + return supported ? attr->mode : 0; +} + + +static const struct attribute_group asus_attr_group = { + .is_visible = asus_sysfs_is_visible, + .attrs = asus_attributes, +}; + +static int asus_platform_init(struct asus_laptop *asus) +{ + int result; + + asus->platform_device = platform_device_alloc(ASUS_LAPTOP_FILE, PLATFORM_DEVID_NONE); + if (!asus->platform_device) + return -ENOMEM; + platform_set_drvdata(asus->platform_device, asus); + + result = platform_device_add(asus->platform_device); + if (result) + goto fail_platform_device; + + result = sysfs_create_group(&asus->platform_device->dev.kobj, + &asus_attr_group); + if (result) + goto fail_sysfs; + + return 0; + +fail_sysfs: + platform_device_del(asus->platform_device); +fail_platform_device: + platform_device_put(asus->platform_device); + return result; +} + +static void asus_platform_exit(struct asus_laptop *asus) +{ + sysfs_remove_group(&asus->platform_device->dev.kobj, &asus_attr_group); + platform_device_unregister(asus->platform_device); +} + +static struct platform_driver platform_driver = { + .driver = { + .name = ASUS_LAPTOP_FILE, + }, +}; + +/* + * This function is used to initialize the context with right values. In this + * method, we can make all the detection we want, and modify the asus_laptop + * struct + */ +static int asus_laptop_get_info(struct asus_laptop *asus) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *model = NULL; + unsigned long long bsts_result; + char *string = NULL; + acpi_status status; + + /* + * Get DSDT headers early enough to allow for differentiating between + * models, but late enough to allow acpi_bus_register_driver() to fail + * before doing anything ACPI-specific. Should we encounter a machine, + * which needs special handling (i.e. its hotkey device has a different + * HID), this bit will be moved. + */ + status = acpi_get_table(ACPI_SIG_DSDT, 1, &asus->dsdt_info); + if (ACPI_FAILURE(status)) + pr_warn("Couldn't get the DSDT table header\n"); + + /* We have to write 0 on init this far for all ASUS models */ + if (write_acpi_int_ret(asus->handle, "INIT", 0, &buffer)) { + pr_err("Hotkey initialization failed\n"); + return -ENODEV; + } + + /* This needs to be called for some laptops to init properly */ + status = + acpi_evaluate_integer(asus->handle, "BSTS", NULL, &bsts_result); + if (ACPI_FAILURE(status)) + pr_warn("Error calling BSTS\n"); + else if (bsts_result) + pr_notice("BSTS called, 0x%02x returned\n", + (uint) bsts_result); + + /* This too ... */ + if (write_acpi_int(asus->handle, "CWAP", wapf)) + pr_err("Error calling CWAP(%d)\n", wapf); + /* + * Try to match the object returned by INIT to the specific model. + * Handle every possible object (or the lack of thereof) the DSDT + * writers might throw at us. When in trouble, we pass NULL to + * asus_model_match() and try something completely different. + */ + if (buffer.pointer) { + model = buffer.pointer; + switch (model->type) { + case ACPI_TYPE_STRING: + string = model->string.pointer; + break; + case ACPI_TYPE_BUFFER: + string = model->buffer.pointer; + break; + default: + string = ""; + break; + } + } + asus->name = kstrdup(string, GFP_KERNEL); + if (!asus->name) { + kfree(buffer.pointer); + return -ENOMEM; + } + + if (string) + pr_notice(" %s model detected\n", string); + + if (!acpi_check_handle(asus->handle, METHOD_WL_STATUS, NULL)) + asus->have_rsts = true; + + kfree(model); + + return AE_OK; +} + +static int asus_acpi_init(struct asus_laptop *asus) +{ + int result = 0; + + result = acpi_bus_get_status(asus->device); + if (result) + return result; + if (!asus->device->status.present) { + pr_err("Hotkey device not present, aborting\n"); + return -ENODEV; + } + + result = asus_laptop_get_info(asus); + if (result) + return result; + + if (!strcmp(bled_type, "led")) + asus->bled_type = TYPE_LED; + else if (!strcmp(bled_type, "rfkill")) + asus->bled_type = TYPE_RFKILL; + + if (!strcmp(wled_type, "led")) + asus->wled_type = TYPE_LED; + else if (!strcmp(wled_type, "rfkill")) + asus->wled_type = TYPE_RFKILL; + + if (bluetooth_status >= 0) + asus_bluetooth_set(asus, !!bluetooth_status); + + if (wlan_status >= 0) + asus_wlan_set(asus, !!wlan_status); + + if (wimax_status >= 0) + asus_wimax_set(asus, !!wimax_status); + + if (wwan_status >= 0) + asus_wwan_set(asus, !!wwan_status); + + /* Keyboard Backlight is on by default */ + if (!acpi_check_handle(asus->handle, METHOD_KBD_LIGHT_SET, NULL)) + asus_kled_set(asus, 1); + + /* LED display is off by default */ + asus->ledd_status = 0xFFF; + + /* Set initial values of light sensor and level */ + asus->light_switch = !!als_status; + asus->light_level = 5; /* level 5 for sensor sensitivity */ + + if (asus->is_pega_lucid) { + asus_als_switch(asus, asus->light_switch); + } else if (!acpi_check_handle(asus->handle, METHOD_ALS_CONTROL, NULL) && + !acpi_check_handle(asus->handle, METHOD_ALS_LEVEL, NULL)) { + asus_als_switch(asus, asus->light_switch); + asus_als_level(asus, asus->light_level); + } + + return result; +} + +static void asus_dmi_check(void) +{ + const char *model; + + model = dmi_get_system_info(DMI_PRODUCT_NAME); + if (!model) + return; + + /* On L1400B WLED control the sound card, don't mess with it ... */ + if (strncmp(model, "L1400B", 6) == 0) + wlan_status = -1; +} + +static bool asus_device_present; + +static int asus_acpi_probe(struct platform_device *pdev) +{ + struct acpi_device *device; + struct asus_laptop *asus; + int result; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + pr_notice("Asus Laptop Support version %s\n", + ASUS_LAPTOP_VERSION); + asus = kzalloc_obj(struct asus_laptop); + if (!asus) + return -ENOMEM; + asus->handle = device->handle; + strscpy(acpi_device_name(device), ASUS_LAPTOP_DEVICE_NAME); + strscpy(acpi_device_class(device), ASUS_LAPTOP_CLASS); + asus->device = device; + + asus_dmi_check(); + + result = asus_acpi_init(asus); + if (result) + goto fail_platform; + + platform_set_drvdata(pdev, asus); + + /* + * Need platform type detection first, then the platform + * device. It is used as a parent for the sub-devices below. + */ + asus->is_pega_lucid = asus_check_pega_lucid(asus); + result = asus_platform_init(asus); + if (result) + goto fail_platform; + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + result = asus_backlight_init(asus); + if (result) + goto fail_backlight; + } + + result = asus_input_init(asus); + if (result) + goto fail_input; + + result = asus_led_init(asus); + if (result) + goto fail_led; + + result = asus_rfkill_init(asus); + if (result && result != -ENODEV) + goto fail_rfkill; + + result = pega_accel_init(asus); + if (result && result != -ENODEV) + goto fail_pega_accel; + + result = pega_rfkill_init(asus); + if (result && result != -ENODEV) + goto fail_pega_rfkill; + + result = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + asus_acpi_notify, asus); + if (result) + goto fail_pega_rfkill; + + asus_device_present = true; + return 0; + +fail_pega_rfkill: + pega_accel_exit(asus); +fail_pega_accel: + asus_rfkill_exit(asus); +fail_rfkill: + asus_led_exit(asus); +fail_led: + asus_input_exit(asus); +fail_input: + asus_backlight_exit(asus); +fail_backlight: + asus_platform_exit(asus); +fail_platform: + kfree(asus); + + return result; +} + +static void asus_acpi_remove(struct platform_device *pdev) +{ + struct asus_laptop *asus = platform_get_drvdata(pdev); + + acpi_dev_remove_notify_handler(asus->device, ACPI_DEVICE_NOTIFY, + asus_acpi_notify); + asus_backlight_exit(asus); + asus_rfkill_exit(asus); + asus_led_exit(asus); + asus_input_exit(asus); + pega_accel_exit(asus); + asus_platform_exit(asus); + + kfree(asus->name); + kfree(asus); +} + +static const struct acpi_device_id asus_device_ids[] = { + {"ATK0100", 0}, + {"ATK0101", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, asus_device_ids); + +static struct platform_driver asus_acpi_driver = { + .probe = asus_acpi_probe, + .remove = asus_acpi_remove, + .driver = { + .name = ASUS_LAPTOP_NAME, + .acpi_match_table = asus_device_ids, + }, +}; + +static int __init asus_laptop_init(void) +{ + int result; + + result = platform_driver_register(&platform_driver); + if (result < 0) + return result; + + result = platform_driver_register(&asus_acpi_driver); + if (result < 0) + goto fail_acpi_driver; + if (!asus_device_present) { + result = -ENODEV; + goto fail_no_device; + } + return 0; + +fail_no_device: + platform_driver_unregister(&asus_acpi_driver); +fail_acpi_driver: + platform_driver_unregister(&platform_driver); + return result; +} + +static void __exit asus_laptop_exit(void) +{ + platform_driver_unregister(&asus_acpi_driver); + platform_driver_unregister(&platform_driver); +} + +module_init(asus_laptop_init); +module_exit(asus_laptop_exit); diff --git a/drivers/platform/x86/asus-nb-wmi.c b/drivers/platform/x86/asus-nb-wmi.c new file mode 100644 index 000000000..8005c088e --- /dev/null +++ b/drivers/platform/x86/asus-nb-wmi.c @@ -0,0 +1,719 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Asus Notebooks WMI hotkey driver + * + * Copyright(C) 2010 Corentin Chary + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "asus-wmi.h" + +#define ASUS_NB_WMI_FILE "asus-nb-wmi" + +MODULE_AUTHOR("Corentin Chary "); +MODULE_DESCRIPTION("Asus Notebooks WMI Hotkey Driver"); +MODULE_LICENSE("GPL"); + +#define ASUS_NB_WMI_EVENT_GUID "0B3CBB35-E3C2-45ED-91C2-4C5A6D195D1C" + +MODULE_ALIAS("wmi:"ASUS_NB_WMI_EVENT_GUID); + +/* + * WAPF defines the behavior of the Fn+Fx wlan key + * The significance of values is yet to be found, but + * most of the time: + * Bit | Bluetooth | WLAN + * 0 | Hardware | Hardware + * 1 | Hardware | Software + * 4 | Software | Software + */ +static int wapf = -1; +module_param(wapf, uint, 0444); +MODULE_PARM_DESC(wapf, "WAPF value"); + +static int tablet_mode_sw = -1; +module_param(tablet_mode_sw, uint, 0444); +MODULE_PARM_DESC(tablet_mode_sw, "Tablet mode detect: -1:auto 0:disable 1:kbd-dock 2:lid-flip 3:lid-flip-rog"); + +static struct quirk_entry *quirks; +static bool atkbd_reports_vol_keys; + +static bool asus_i8042_filter(unsigned char data, unsigned char str, struct serio *port, + void *context) +{ + static bool extended_e0; + static bool extended_e1; + + if (str & I8042_STR_AUXDATA) + return false; + + if (quirks->filter_i8042_e1_extended_codes) { + if (data == 0xe1) { + extended_e1 = true; + return true; + } + + if (extended_e1) { + extended_e1 = false; + return true; + } + } + + if (data == 0xe0) { + extended_e0 = true; + } else if (extended_e0) { + extended_e0 = false; + + switch (data & 0x7f) { + case 0x20: /* e0 20 / e0 a0, Volume Mute press / release */ + case 0x2e: /* e0 2e / e0 ae, Volume Down press / release */ + case 0x30: /* e0 30 / e0 b0, Volume Up press / release */ + atkbd_reports_vol_keys = true; + break; + } + } + + return false; +} + +static struct quirk_entry quirk_asus_unknown = { + .wapf = 0, + .wmi_backlight_set_devstate = true, +}; + +static struct quirk_entry quirk_asus_q500a = { + .filter_i8042_e1_extended_codes = true, + .wmi_backlight_set_devstate = true, +}; + +/* + * For those machines that need software to control bt/wifi status + * and have duplicate events(ACPI and WMI) for display toggle + */ +static struct quirk_entry quirk_asus_x55u = { + .wapf = 4, + .wmi_backlight_set_devstate = true, + .no_display_toggle = true, +}; + +static struct quirk_entry quirk_asus_wapf4 = { + .wapf = 4, + .wmi_backlight_set_devstate = true, +}; + +static struct quirk_entry quirk_asus_x200ca = { + .wapf = 2, + .wmi_backlight_set_devstate = true, +}; + +static struct quirk_entry quirk_asus_x550lb = { + .wmi_backlight_set_devstate = true, + .xusb2pr = 0x01D9, +}; + +static struct quirk_entry quirk_asus_forceals = { + .wmi_backlight_set_devstate = true, + .wmi_force_als_set = true, +}; + +static struct quirk_entry quirk_asus_use_kbd_dock_devid = { + .tablet_switch_mode = asus_wmi_kbd_dock_devid, +}; + +static struct quirk_entry quirk_asus_use_lid_flip_devid = { + .wmi_backlight_set_devstate = true, + .tablet_switch_mode = asus_wmi_lid_flip_devid, +}; + +static struct quirk_entry quirk_asus_tablet_mode = { + .wmi_backlight_set_devstate = true, + .tablet_switch_mode = asus_wmi_lid_flip_rog_devid, +}; + +static struct quirk_entry quirk_asus_ignore_fan = { + .wmi_ignore_fan = true, +}; + +static struct quirk_entry quirk_asus_zenbook_duo_kbd = { + .key_wlan_event = ASUS_WMI_KEY_IGNORE, +}; + +static struct quirk_entry quirk_asus_z13 = { + .key_wlan_event = ASUS_WMI_KEY_ARMOURY, + .tablet_switch_mode = asus_wmi_kbd_dock_devid, +}; + +static int dmi_matched(const struct dmi_system_id *dmi) +{ + pr_info("Identified laptop model '%s'\n", dmi->ident); + quirks = dmi->driver_data; + return 1; +} + +static const struct dmi_system_id asus_quirks[] = { + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. Q500A", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "Q500A"), + }, + .driver_data = &quirk_asus_q500a, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. U32U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK Computer Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "U32U"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X302UA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X302UA"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X401U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X401U"), + }, + .driver_data = &quirk_asus_x55u, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X401A", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X401A"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X401A1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X401A1"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X45U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X45U"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X456UA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X456UA"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X456UF", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X456UF"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X501U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X501U"), + }, + .driver_data = &quirk_asus_x55u, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X501A", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X501A"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X501A1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X501A1"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X550CA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X550CA"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X550CC", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X550CC"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X550CL", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X550CL"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X550VB", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X550VB"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X551CA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X551CA"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X55A", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X55A"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X55C", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X55C"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X55U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X55U"), + }, + .driver_data = &quirk_asus_x55u, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X55VD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X55VD"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X75A", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X75A"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X75VBP", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X75VBP"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X75VD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X75VD"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. 1015E", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "1015E"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. 1015U", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "1015U"), + }, + .driver_data = &quirk_asus_wapf4, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X200CA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X200CA"), + }, + .driver_data = &quirk_asus_x200ca, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. UX330UAK", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "UX330UAK"), + }, + .driver_data = &quirk_asus_forceals, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. X550LB", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "X550LB"), + }, + .driver_data = &quirk_asus_x550lb, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. UX430UQ", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "UX430UQ"), + }, + .driver_data = &quirk_asus_forceals, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK COMPUTER INC. UX430UNR", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "UX430UNR"), + }, + .driver_data = &quirk_asus_forceals, + }, + { + .callback = dmi_matched, + .ident = "Asus Transformer T100TA / T100HA / T100CHI", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + /* Match *T100* */ + DMI_MATCH(DMI_PRODUCT_NAME, "T100"), + }, + .driver_data = &quirk_asus_use_kbd_dock_devid, + }, + { + .callback = dmi_matched, + .ident = "Asus Transformer T101HA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "T101HA"), + }, + .driver_data = &quirk_asus_use_kbd_dock_devid, + }, + { + .callback = dmi_matched, + .ident = "Asus Transformer T200TA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "T200TA"), + }, + .driver_data = &quirk_asus_use_kbd_dock_devid, + }, + { + .callback = dmi_matched, + .ident = "ASUS ZenBook Flip UX360", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + /* Match UX360* */ + DMI_MATCH(DMI_PRODUCT_NAME, "UX360"), + }, + .driver_data = &quirk_asus_use_lid_flip_devid, + }, + { + .callback = dmi_matched, + .ident = "ASUS TP200s / E205SA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "E205SA"), + }, + .driver_data = &quirk_asus_use_lid_flip_devid, + }, + { + .callback = dmi_matched, + .ident = "ASUS ROG FLOW X13", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + /* Match GV301** */ + DMI_MATCH(DMI_PRODUCT_NAME, "GV301"), + }, + .driver_data = &quirk_asus_tablet_mode, + }, + { + .callback = dmi_matched, + .ident = "ASUS ROG FLOW X16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "GV601R"), + }, + .driver_data = &quirk_asus_tablet_mode, + }, + { + .callback = dmi_matched, + .ident = "ASUS ROG FLOW X16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "GV601V"), + }, + .driver_data = &quirk_asus_tablet_mode, + }, + { + .callback = dmi_matched, + .ident = "ASUS VivoBook E410MA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "E410MA"), + }, + .driver_data = &quirk_asus_ignore_fan, + }, + { + .callback = dmi_matched, + .ident = "ASUS Zenbook Duo UX8406MA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "UX8406MA"), + }, + .driver_data = &quirk_asus_zenbook_duo_kbd, + }, + { + .callback = dmi_matched, + .ident = "ASUS Zenbook Duo UX8406CA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "UX8406CA"), + }, + .driver_data = &quirk_asus_zenbook_duo_kbd, + }, + { + .callback = dmi_matched, + .ident = "ASUS Zenbook Duo UX8407AA", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUS"), + DMI_MATCH(DMI_PRODUCT_NAME, "Zenbook Duo UX8407AA"), + }, + .driver_data = &quirk_asus_zenbook_duo_kbd, + }, + { + .callback = dmi_matched, + .ident = "ASUS ROG Z13", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUS"), + DMI_MATCH(DMI_PRODUCT_NAME, "ROG Flow Z13"), + }, + .driver_data = &quirk_asus_z13, + }, + {}, +}; + +static void asus_nb_wmi_quirks(struct asus_wmi_driver *driver) +{ + quirks = &quirk_asus_unknown; + dmi_check_system(asus_quirks); + + driver->quirks = quirks; + driver->panel_power = BACKLIGHT_POWER_ON; + + /* overwrite the wapf setting if the wapf paramater is specified */ + if (wapf != -1) + quirks->wapf = wapf; + else + wapf = quirks->wapf; + + if (tablet_mode_sw != -1) + quirks->tablet_switch_mode = tablet_mode_sw; +} + +static const struct key_entry asus_nb_wmi_keymap[] = { + { KE_KEY, ASUS_WMI_BRN_DOWN, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, ASUS_WMI_BRN_UP, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x2a, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_IGNORE, 0x2b, }, /* PrintScreen (also send via PS/2) on newer models */ + { KE_IGNORE, 0x2c, }, /* CapsLock (also send via PS/2) on newer models */ + { KE_KEY, 0x2d, { KEY_DISPLAYTOGGLE } }, + { KE_KEY, 0x30, { KEY_VOLUMEUP } }, + { KE_KEY, 0x31, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0x32, { KEY_MUTE } }, + { KE_KEY, 0x33, { KEY_SCREENLOCK } }, + { KE_KEY, 0x35, { KEY_SCREENLOCK } }, + { KE_KEY, 0x38, { KEY_PROG3 } }, /* Armoury Crate */ + { KE_KEY, 0x40, { KEY_PREVIOUSSONG } }, + { KE_KEY, 0x41, { KEY_NEXTSONG } }, + { KE_KEY, 0x43, { KEY_STOPCD } }, /* Stop/Eject */ + { KE_KEY, 0x45, { KEY_PLAYPAUSE } }, + { KE_KEY, 0x4c, { KEY_MEDIA } }, /* WMP Key */ + { KE_KEY, 0x50, { KEY_EMAIL } }, + { KE_KEY, 0x51, { KEY_WWW } }, + { KE_KEY, 0x55, { KEY_CALC } }, + { KE_IGNORE, 0x57, }, /* Battery mode */ + { KE_IGNORE, 0x58, }, /* AC mode */ + { KE_KEY, 0x5C, { KEY_F15 } }, /* Power Gear key */ + { KE_KEY, 0x5D, { KEY_WLAN } }, /* Wireless console Toggle */ + { KE_KEY, 0x5E, { KEY_WLAN } }, /* Wireless console Enable */ + { KE_KEY, 0x5F, { KEY_WLAN } }, /* Wireless console Disable */ + { KE_KEY, 0x60, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 0x61, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD only */ + { KE_KEY, 0x62, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT only */ + { KE_KEY, 0x63, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT */ + { KE_KEY, 0x64, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV */ + { KE_KEY, 0x65, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV */ + { KE_KEY, 0x66, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV */ + { KE_KEY, 0x67, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV */ + { KE_KEY, 0x6B, { KEY_TOUCHPAD_TOGGLE } }, + { KE_IGNORE, 0x6E, }, /* Low Battery notification */ + { KE_KEY, 0x71, { KEY_F13 } }, /* General-purpose button */ + { KE_IGNORE, 0x79, }, /* Charger type dectection notification */ + { KE_KEY, 0x7a, { KEY_ALS_TOGGLE } }, /* Ambient Light Sensor Toggle */ + { KE_IGNORE, 0x7B, }, /* Charger connect/disconnect notification */ + { KE_KEY, 0x7c, { KEY_MICMUTE } }, + { KE_KEY, 0x7D, { KEY_BLUETOOTH } }, /* Bluetooth Enable */ + { KE_KEY, 0x7E, { KEY_BLUETOOTH } }, /* Bluetooth Disable */ + { KE_KEY, 0x82, { KEY_CAMERA } }, + { KE_KEY, 0x85, { KEY_CAMERA } }, + { KE_KEY, 0x86, { KEY_PROG1 } }, /* MyASUS Key */ + { KE_KEY, 0x88, { KEY_RFKILL } }, /* Radio Toggle Key */ + { KE_KEY, 0x8A, { KEY_PROG1 } }, /* Color enhancement mode */ + { KE_KEY, 0x8C, { KEY_SWITCHVIDEOMODE } }, /* SDSP DVI only */ + { KE_KEY, 0x8D, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + DVI */ + { KE_KEY, 0x8E, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + DVI */ + { KE_KEY, 0x8F, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV + DVI */ + { KE_KEY, 0x90, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + DVI */ + { KE_KEY, 0x91, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV + DVI */ + { KE_KEY, 0x92, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV + DVI */ + { KE_KEY, 0x93, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV + DVI */ + { KE_KEY, 0x95, { KEY_MEDIA } }, + { KE_KEY, 0x99, { KEY_PHONE } }, /* Conflicts with fan mode switch */ + { KE_KEY, 0X9D, { KEY_FN_F } }, + { KE_KEY, 0xA0, { KEY_SWITCHVIDEOMODE } }, /* SDSP HDMI only */ + { KE_KEY, 0xA1, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + HDMI */ + { KE_KEY, 0xA2, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + HDMI */ + { KE_KEY, 0xA3, { KEY_SWITCHVIDEOMODE } }, /* SDSP TV + HDMI */ + { KE_KEY, 0xA4, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + HDMI */ + { KE_KEY, 0xA5, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + TV + HDMI */ + { KE_KEY, 0xA6, { KEY_SWITCHVIDEOMODE } }, /* SDSP CRT + TV + HDMI */ + { KE_KEY, 0xA7, { KEY_SWITCHVIDEOMODE } }, /* SDSP LCD + CRT + TV + HDMI */ + { KE_KEY, 0xAE, { KEY_FN_F5 } }, /* Fn+F5 fan mode on 2020+ */ + { KE_KEY, 0xB3, { KEY_PROG4 } }, /* AURA */ + { KE_KEY, 0xB5, { KEY_CALC } }, + { KE_IGNORE, 0xC0, }, /* External display connect/disconnect notification */ + { KE_KEY, 0xC4, { KEY_KBDILLUMUP } }, + { KE_KEY, 0xC5, { KEY_KBDILLUMDOWN } }, + { KE_KEY, 0xCA, { KEY_F13 } }, /* Noise cancelling on Expertbook B9 */ + { KE_KEY, 0xCB, { KEY_F14 } }, /* Fn+noise-cancel */ + { KE_IGNORE, 0xC6, }, /* Ambient Light Sensor notification */ + { KE_IGNORE, 0xCF, }, /* AC mode */ + { KE_KEY, 0xFA, { KEY_PROG2 } }, /* Lid flip action */ + { KE_KEY, 0xBD, { KEY_PROG2 } }, /* Lid flip action on ROG xflow laptops */ + { KE_KEY, ASUS_WMI_KEY_ARMOURY, { KEY_PROG3 } }, + { KE_END, 0}, +}; + +static void asus_nb_wmi_key_filter(struct asus_wmi_driver *asus_wmi, int *code, + unsigned int *value, bool *autorelease) +{ + switch (*code) { + case ASUS_WMI_BRN_DOWN: + case ASUS_WMI_BRN_UP: + if (acpi_video_handles_brightness_key_presses()) + *code = ASUS_WMI_KEY_IGNORE; + + break; + case 0x30: /* Volume Up */ + case 0x31: /* Volume Down */ + case 0x32: /* Volume Mute */ + if (atkbd_reports_vol_keys) + *code = ASUS_WMI_KEY_IGNORE; + break; + case 0x5D: /* Wireless console Toggle */ + case 0x5E: /* Wireless console Enable / Keyboard Attach, Detach */ + case 0x5F: /* Wireless console Disable / Special Key */ + if (quirks->key_wlan_event) + *code = quirks->key_wlan_event; + break; + } +} + +static struct asus_wmi_driver asus_nb_wmi_driver = { + .name = ASUS_NB_WMI_FILE, + .owner = THIS_MODULE, + .event_guid = ASUS_NB_WMI_EVENT_GUID, + .keymap = asus_nb_wmi_keymap, + .input_name = "Asus WMI hotkeys", + .input_phys = ASUS_NB_WMI_FILE "/input0", + .detect_quirks = asus_nb_wmi_quirks, + .key_filter = asus_nb_wmi_key_filter, + .i8042_filter = asus_i8042_filter, +}; + + +static int __init asus_nb_wmi_init(void) +{ + return asus_wmi_register_driver(&asus_nb_wmi_driver); +} + +static void __exit asus_nb_wmi_exit(void) +{ + asus_wmi_unregister_driver(&asus_nb_wmi_driver); +} + +module_init(asus_nb_wmi_init); +module_exit(asus_nb_wmi_exit); diff --git a/drivers/platform/x86/asus-tf103c-dock.c b/drivers/platform/x86/asus-tf103c-dock.c new file mode 100644 index 000000000..92466477d --- /dev/null +++ b/drivers/platform/x86/asus-tf103c-dock.c @@ -0,0 +1,942 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * This is a driver for the keyboard, touchpad and USB port of the + * keyboard dock for the Asus TF103C 2-in-1 tablet. + * + * This keyboard dock has its own I2C attached embedded controller + * and the keyboard and touchpad are also connected over I2C, + * instead of using the usual USB connection. This means that the + * keyboard dock requires this special driver to function. + * + * Copyright (C) 2021 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool fnlock; +module_param(fnlock, bool, 0644); +MODULE_PARM_DESC(fnlock, + "By default the kbd toprow sends multimedia key presses. AltGr " + "can be pressed to change this to F1-F12. Set this to 1 to " + "change the default. Press AltGr + Esc to toggle at runtime."); + +#define TF103C_DOCK_DEV_NAME "NPCE69A:00" + +#define TF103C_DOCK_HPD_DEBOUNCE msecs_to_jiffies(20) + +/*** Touchpad I2C device defines ***/ +#define TF103C_DOCK_TP_ADDR 0x15 + +/*** Keyboard I2C device defines **A*/ +#define TF103C_DOCK_KBD_ADDR 0x16 + +#define TF103C_DOCK_KBD_DATA_REG 0x73 +#define TF103C_DOCK_KBD_DATA_MIN_LENGTH 4 +#define TF103C_DOCK_KBD_DATA_MAX_LENGTH 11 +#define TF103C_DOCK_KBD_DATA_MODIFIERS 3 +#define TF103C_DOCK_KBD_DATA_KEYS 5 +#define TF103C_DOCK_KBD_CMD_REG 0x75 + +#define TF103C_DOCK_KBD_CMD_ENABLE 0x0800 + +/*** EC innterrupt data I2C device defines ***/ +#define TF103C_DOCK_INTR_ADDR 0x19 +#define TF103C_DOCK_INTR_DATA_REG 0x6a + +#define TF103C_DOCK_INTR_DATA1_OBF_MASK 0x01 +#define TF103C_DOCK_INTR_DATA1_KEY_MASK 0x04 +#define TF103C_DOCK_INTR_DATA1_KBC_MASK 0x08 +#define TF103C_DOCK_INTR_DATA1_AUX_MASK 0x20 +#define TF103C_DOCK_INTR_DATA1_SCI_MASK 0x40 +#define TF103C_DOCK_INTR_DATA1_SMI_MASK 0x80 +/* Special values for the OOB data on kbd_client / tp_client */ +#define TF103C_DOCK_INTR_DATA1_OOB_VALUE 0xc1 +#define TF103C_DOCK_INTR_DATA2_OOB_VALUE 0x04 + +#define TF103C_DOCK_SMI_AC_EVENT 0x31 +#define TF103C_DOCK_SMI_HANDSHAKING 0x50 +#define TF103C_DOCK_SMI_EC_WAKEUP 0x53 +#define TF103C_DOCK_SMI_BOOTBLOCK_RESET 0x5e +#define TF103C_DOCK_SMI_WATCHDOG_RESET 0x5f +#define TF103C_DOCK_SMI_ADAPTER_CHANGE 0x60 +#define TF103C_DOCK_SMI_DOCK_INSERT 0x61 +#define TF103C_DOCK_SMI_DOCK_REMOVE 0x62 +#define TF103C_DOCK_SMI_PAD_BL_CHANGE 0x63 +#define TF103C_DOCK_SMI_HID_STATUS_CHANGED 0x64 +#define TF103C_DOCK_SMI_HID_WAKEUP 0x65 +#define TF103C_DOCK_SMI_S3 0x83 +#define TF103C_DOCK_SMI_S5 0x85 +#define TF103C_DOCK_SMI_NOTIFY_SHUTDOWN 0x90 +#define TF103C_DOCK_SMI_RESUME 0x91 + +/*** EC (dockram) I2C device defines ***/ +#define TF103C_DOCK_EC_ADDR 0x1b + +#define TF103C_DOCK_EC_CMD_REG 0x0a +#define TF103C_DOCK_EC_CMD_LEN 9 + +enum { + TF103C_DOCK_FLAG_HID_OPEN, +}; + +struct tf103c_dock_data { + struct delayed_work hpd_work; + struct irq_chip tp_irqchip; + struct irq_domain *tp_irq_domain; + struct i2c_client *ec_client; + struct i2c_client *intr_client; + struct i2c_client *kbd_client; + struct i2c_client *tp_client; + struct gpio_desc *pwr_en; + struct gpio_desc *irq_gpio; + struct gpio_desc *hpd_gpio; + struct input_dev *input; + struct hid_device *hid; + unsigned long flags; + int board_rev; + int irq; + int hpd_irq; + int tp_irq; + int last_press_0x13; + int last_press_0x14; + bool enabled; + bool tp_enabled; + bool altgr_pressed; + bool esc_pressed; + bool filter_esc; + u8 kbd_buf[TF103C_DOCK_KBD_DATA_MAX_LENGTH]; +}; + +static struct gpiod_lookup_table tf103c_dock_gpios = { + .dev_id = "i2c-" TF103C_DOCK_DEV_NAME, + .table = { + GPIO_LOOKUP("INT33FC:00", 55, "dock_pwr_en", GPIO_ACTIVE_HIGH), + GPIO_LOOKUP("INT33FC:02", 1, "dock_irq", GPIO_ACTIVE_HIGH), + GPIO_LOOKUP("INT33FC:02", 29, "dock_hpd", GPIO_ACTIVE_HIGH), + GPIO_LOOKUP("gpio_crystalcove", 2, "board_rev", GPIO_ACTIVE_HIGH), + {} + }, +}; + +/* Byte 0 is the length of the rest of the packet */ +static const u8 tf103c_dock_enable_cmd[9] = { 8, 0x20, 0, 0, 0, 0, 0x20, 0, 0 }; +static const u8 tf103c_dock_usb_enable_cmd[9] = { 8, 0, 0, 0, 0, 0, 0, 0x40, 0 }; +static const u8 tf103c_dock_suspend_cmd[9] = { 8, 0, 0x20, 0, 0, 0x22, 0, 0, 0 }; + +/*** keyboard related code ***/ + +static u8 tf103c_dock_kbd_hid_desc[] = { + 0x05, 0x01, /* Usage Page (Desktop), */ + 0x09, 0x06, /* Usage (Keyboard), */ + 0xA1, 0x01, /* Collection (Application), */ + 0x85, 0x11, /* Report ID (17), */ + 0x95, 0x08, /* Report Count (8), */ + 0x75, 0x01, /* Report Size (1), */ + 0x15, 0x00, /* Logical Minimum (0), */ + 0x25, 0x01, /* Logical Maximum (1), */ + 0x05, 0x07, /* Usage Page (Keyboard), */ + 0x19, 0xE0, /* Usage Minimum (KB Leftcontrol), */ + 0x29, 0xE7, /* Usage Maximum (KB Right GUI), */ + 0x81, 0x02, /* Input (Variable), */ + 0x95, 0x01, /* Report Count (1), */ + 0x75, 0x08, /* Report Size (8), */ + 0x81, 0x01, /* Input (Constant), */ + 0x95, 0x06, /* Report Count (6), */ + 0x75, 0x08, /* Report Size (8), */ + 0x15, 0x00, /* Logical Minimum (0), */ + 0x26, 0xFF, 0x00, /* Logical Maximum (255), */ + 0x05, 0x07, /* Usage Page (Keyboard), */ + 0x19, 0x00, /* Usage Minimum (None), */ + 0x2A, 0xFF, 0x00, /* Usage Maximum (FFh), */ + 0x81, 0x00, /* Input, */ + 0xC0 /* End Collection */ +}; + +static int tf103c_dock_kbd_read(struct tf103c_dock_data *dock) +{ + struct i2c_client *client = dock->kbd_client; + struct device *dev = &dock->ec_client->dev; + struct i2c_msg msgs[2]; + u8 reg[2]; + int ret; + + reg[0] = TF103C_DOCK_KBD_DATA_REG & 0xff; + reg[1] = TF103C_DOCK_KBD_DATA_REG >> 8; + + msgs[0].addr = client->addr; + msgs[0].flags = 0; + msgs[0].len = sizeof(reg); + msgs[0].buf = reg; + + msgs[1].addr = client->addr; + msgs[1].flags = I2C_M_RD; + msgs[1].len = TF103C_DOCK_KBD_DATA_MAX_LENGTH; + msgs[1].buf = dock->kbd_buf; + + ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs)); + if (ret != ARRAY_SIZE(msgs)) { + dev_err(dev, "error %d reading kbd data\n", ret); + return -EIO; + } + + return 0; +} + +static void tf103c_dock_kbd_write(struct tf103c_dock_data *dock, u16 cmd) +{ + struct device *dev = &dock->ec_client->dev; + u8 buf[4]; + int ret; + + put_unaligned_le16(TF103C_DOCK_KBD_CMD_REG, &buf[0]); + put_unaligned_le16(cmd, &buf[2]); + + ret = i2c_master_send(dock->kbd_client, buf, sizeof(buf)); + if (ret != sizeof(buf)) + dev_err(dev, "error %d writing kbd cmd\n", ret); +} + +/* HID ll_driver functions for forwarding input-reports from the kbd_client */ +static int tf103c_dock_hid_parse(struct hid_device *hid) +{ + return hid_parse_report(hid, tf103c_dock_kbd_hid_desc, + sizeof(tf103c_dock_kbd_hid_desc)); +} + +static int tf103c_dock_hid_start(struct hid_device *hid) +{ + return 0; +} + +static void tf103c_dock_hid_stop(struct hid_device *hid) +{ + hid->claimed = 0; +} + +static int tf103c_dock_hid_open(struct hid_device *hid) +{ + struct tf103c_dock_data *dock = hid->driver_data; + + set_bit(TF103C_DOCK_FLAG_HID_OPEN, &dock->flags); + return 0; +} + +static void tf103c_dock_hid_close(struct hid_device *hid) +{ + struct tf103c_dock_data *dock = hid->driver_data; + + clear_bit(TF103C_DOCK_FLAG_HID_OPEN, &dock->flags); +} + +/* Mandatory, but not used */ +static int tf103c_dock_hid_raw_request(struct hid_device *hid, u8 reportnum, + u8 *buf, size_t len, u8 rtype, int reqtype) +{ + return 0; +} + +static const struct hid_ll_driver tf103c_dock_hid_ll_driver = { + .parse = tf103c_dock_hid_parse, + .start = tf103c_dock_hid_start, + .stop = tf103c_dock_hid_stop, + .open = tf103c_dock_hid_open, + .close = tf103c_dock_hid_close, + .raw_request = tf103c_dock_hid_raw_request, +}; + +static const int tf103c_dock_toprow_codes[13][2] = { + /* Normal, AltGr pressed */ + { KEY_POWER, KEY_F1 }, + { KEY_RFKILL, KEY_F2 }, + { KEY_F21, KEY_F3 }, /* Touchpad toggle, userspace expects F21 */ + { KEY_BRIGHTNESSDOWN, KEY_F4 }, + { KEY_BRIGHTNESSUP, KEY_F5 }, + { KEY_CAMERA, KEY_F6 }, + { KEY_CONFIG, KEY_F7 }, + { KEY_PREVIOUSSONG, KEY_F8 }, + { KEY_PLAYPAUSE, KEY_F9 }, + { KEY_NEXTSONG, KEY_F10 }, + { KEY_MUTE, KEY_F11 }, + { KEY_VOLUMEDOWN, KEY_F12 }, + { KEY_VOLUMEUP, KEY_SYSRQ }, +}; + +static void tf103c_dock_report_toprow_kbd_hook(struct tf103c_dock_data *dock) +{ + u8 *esc, *buf = dock->kbd_buf; + int size; + + /* + * Stop AltGr reports from getting reported on the "Asus TF103C Dock + * Keyboard" input_dev, since this gets used as "Fn" key for the toprow + * keys. Instead we report this on the "Asus TF103C Dock Top Row Keys" + * input_dev, when not used to modify the toprow keys. + */ + dock->altgr_pressed = buf[TF103C_DOCK_KBD_DATA_MODIFIERS] & 0x40; + buf[TF103C_DOCK_KBD_DATA_MODIFIERS] &= ~0x40; + + input_report_key(dock->input, KEY_RIGHTALT, dock->altgr_pressed); + input_sync(dock->input); + + /* Toggle fnlock on AltGr + Esc press */ + buf = buf + TF103C_DOCK_KBD_DATA_KEYS; + size = TF103C_DOCK_KBD_DATA_MAX_LENGTH - TF103C_DOCK_KBD_DATA_KEYS; + esc = memchr(buf, 0x29, size); + if (!dock->esc_pressed && esc) { + if (dock->altgr_pressed) { + fnlock = !fnlock; + dock->filter_esc = true; + } + } + if (esc && dock->filter_esc) + *esc = 0; + else + dock->filter_esc = false; + + dock->esc_pressed = esc != NULL; +} + +static void tf103c_dock_toprow_press(struct tf103c_dock_data *dock, int key_code) +{ + /* + * Release AltGr before reporting the toprow key, so that userspace + * sees e.g. just KEY_SUSPEND and not AltGr + KEY_SUSPEND. + */ + if (dock->altgr_pressed) { + input_report_key(dock->input, KEY_RIGHTALT, false); + input_sync(dock->input); + } + + input_report_key(dock->input, key_code, true); + input_sync(dock->input); +} + +static void tf103c_dock_toprow_release(struct tf103c_dock_data *dock, int key_code) +{ + input_report_key(dock->input, key_code, false); + input_sync(dock->input); + + if (dock->altgr_pressed) { + input_report_key(dock->input, KEY_RIGHTALT, true); + input_sync(dock->input); + } +} + +static void tf103c_dock_toprow_event(struct tf103c_dock_data *dock, + int toprow_index, int *last_press) +{ + int key_code, fn = dock->altgr_pressed ^ fnlock; + + if (last_press && *last_press) { + tf103c_dock_toprow_release(dock, *last_press); + *last_press = 0; + } + + if (toprow_index < 0) + return; + + key_code = tf103c_dock_toprow_codes[toprow_index][fn]; + tf103c_dock_toprow_press(dock, key_code); + + if (last_press) + *last_press = key_code; + else + tf103c_dock_toprow_release(dock, key_code); +} + +/* + * The keyboard sends what appears to be standard I2C-HID input-reports, + * except that a 16 bit register address of where the I2C-HID format + * input-reports are stored must be send before reading it in a single + * (I2C repeated-start) I2C transaction. + * + * Its unknown how to get the HID descriptors but they are easy to reconstruct: + * + * Input report id 0x11 is 8 bytes long and contain standard USB HID intf-class, + * Boot Interface Subclass reports. + * Input report id 0x13 is 2 bytes long and sends Consumer Control events + * Input report id 0x14 is 1 byte long and sends System Control events + * + * However the top row keys (where a normal keyboard has F1-F12 + Print-Screen) + * are a mess, using a mix of the 0x13 and 0x14 input reports as well as EC SCI + * events; and these need special handling to allow actually sending F1-F12, + * since the Fn key on the keyboard only works on the cursor keys and the top + * row keys always send their special "Multimedia hotkey" codes. + * + * So only forward the 0x11 reports to HID and handle the top-row keys here. + */ +static void tf103c_dock_kbd_interrupt(struct tf103c_dock_data *dock) +{ + struct device *dev = &dock->ec_client->dev; + u8 *buf = dock->kbd_buf; + int size; + + if (tf103c_dock_kbd_read(dock)) + return; + + size = buf[0] | buf[1] << 8; + if (size < TF103C_DOCK_KBD_DATA_MIN_LENGTH || + size > TF103C_DOCK_KBD_DATA_MAX_LENGTH) { + dev_err(dev, "error reported kbd pkt size %d is out of range %d-%d\n", size, + TF103C_DOCK_KBD_DATA_MIN_LENGTH, + TF103C_DOCK_KBD_DATA_MAX_LENGTH); + return; + } + + switch (buf[2]) { + case 0x11: + if (size != 11) + break; + + tf103c_dock_report_toprow_kbd_hook(dock); + + if (test_bit(TF103C_DOCK_FLAG_HID_OPEN, &dock->flags)) + hid_input_report(dock->hid, HID_INPUT_REPORT, buf + 2, size - 2, 1); + return; + case 0x13: + if (size != 5) + break; + + switch (buf[3] | buf[4] << 8) { + case 0: + tf103c_dock_toprow_event(dock, -1, &dock->last_press_0x13); + return; + case 0x70: + tf103c_dock_toprow_event(dock, 3, &dock->last_press_0x13); + return; + case 0x6f: + tf103c_dock_toprow_event(dock, 4, &dock->last_press_0x13); + return; + case 0xb6: + tf103c_dock_toprow_event(dock, 7, &dock->last_press_0x13); + return; + case 0xcd: + tf103c_dock_toprow_event(dock, 8, &dock->last_press_0x13); + return; + case 0xb5: + tf103c_dock_toprow_event(dock, 9, &dock->last_press_0x13); + return; + case 0xe2: + tf103c_dock_toprow_event(dock, 10, &dock->last_press_0x13); + return; + case 0xea: + tf103c_dock_toprow_event(dock, 11, &dock->last_press_0x13); + return; + case 0xe9: + tf103c_dock_toprow_event(dock, 12, &dock->last_press_0x13); + return; + } + break; + case 0x14: + if (size != 4) + break; + + switch (buf[3]) { + case 0: + tf103c_dock_toprow_event(dock, -1, &dock->last_press_0x14); + return; + case 1: + tf103c_dock_toprow_event(dock, 0, &dock->last_press_0x14); + return; + } + break; + } + + dev_warn(dev, "warning unknown kbd data: %*ph\n", size, buf); +} + +/*** touchpad related code ***/ + +static const struct property_entry tf103c_dock_touchpad_props[] = { + PROPERTY_ENTRY_BOOL("elan,clickpad"), + { } +}; + +static const struct software_node tf103c_dock_touchpad_sw_node = { + .properties = tf103c_dock_touchpad_props, +}; + +/* + * tf103c_enable_touchpad() is only called from the threaded interrupt handler + * and tf103c_disable_touchpad() is only called after the irq is disabled, + * so no locking is necessary. + */ +static void tf103c_dock_enable_touchpad(struct tf103c_dock_data *dock) +{ + struct i2c_board_info board_info = { }; + struct device *dev = &dock->ec_client->dev; + int ret; + + if (dock->tp_enabled) { + /* Happens after resume, the tp needs to be reinitialized */ + ret = device_reprobe(&dock->tp_client->dev); + if (ret) + dev_err_probe(dev, ret, "reprobing tp-client\n"); + return; + } + + strscpy(board_info.type, "elan_i2c"); + board_info.addr = TF103C_DOCK_TP_ADDR; + board_info.dev_name = TF103C_DOCK_DEV_NAME "-tp"; + board_info.irq = dock->tp_irq; + board_info.swnode = &tf103c_dock_touchpad_sw_node; + + dock->tp_client = i2c_new_client_device(dock->ec_client->adapter, &board_info); + if (IS_ERR(dock->tp_client)) { + dev_err(dev, "error %ld creating tp client\n", PTR_ERR(dock->tp_client)); + return; + } + + dock->tp_enabled = true; +} + +static void tf103c_dock_disable_touchpad(struct tf103c_dock_data *dock) +{ + if (!dock->tp_enabled) + return; + + i2c_unregister_device(dock->tp_client); + + dock->tp_enabled = false; +} + +/*** interrupt handling code ***/ +static void tf103c_dock_ec_cmd(struct tf103c_dock_data *dock, const u8 *cmd) +{ + struct device *dev = &dock->ec_client->dev; + int ret; + + ret = i2c_smbus_write_i2c_block_data(dock->ec_client, TF103C_DOCK_EC_CMD_REG, + TF103C_DOCK_EC_CMD_LEN, cmd); + if (ret) + dev_err(dev, "error %d sending %*ph cmd\n", ret, + TF103C_DOCK_EC_CMD_LEN, cmd); +} + +static void tf103c_dock_sci(struct tf103c_dock_data *dock, u8 val) +{ + struct device *dev = &dock->ec_client->dev; + + switch (val) { + case 2: + tf103c_dock_toprow_event(dock, 1, NULL); + return; + case 4: + tf103c_dock_toprow_event(dock, 2, NULL); + return; + case 8: + tf103c_dock_toprow_event(dock, 5, NULL); + return; + case 17: + tf103c_dock_toprow_event(dock, 6, NULL); + return; + } + + dev_warn(dev, "warning unknown SCI value: 0x%02x\n", val); +} + +static void tf103c_dock_smi(struct tf103c_dock_data *dock, u8 val) +{ + struct device *dev = &dock->ec_client->dev; + + switch (val) { + case TF103C_DOCK_SMI_EC_WAKEUP: + tf103c_dock_ec_cmd(dock, tf103c_dock_enable_cmd); + tf103c_dock_ec_cmd(dock, tf103c_dock_usb_enable_cmd); + tf103c_dock_kbd_write(dock, TF103C_DOCK_KBD_CMD_ENABLE); + break; + case TF103C_DOCK_SMI_PAD_BL_CHANGE: + /* There is no backlight, but the EC still sends this */ + break; + case TF103C_DOCK_SMI_HID_STATUS_CHANGED: + tf103c_dock_enable_touchpad(dock); + break; + default: + dev_warn(dev, "warning unknown SMI value: 0x%02x\n", val); + break; + } +} + +static irqreturn_t tf103c_dock_irq(int irq, void *data) +{ + struct tf103c_dock_data *dock = data; + struct device *dev = &dock->ec_client->dev; + u8 intr_data[8]; + int ret; + + ret = i2c_smbus_read_i2c_block_data(dock->intr_client, TF103C_DOCK_INTR_DATA_REG, + sizeof(intr_data), intr_data); + if (ret != sizeof(intr_data)) { + dev_err(dev, "error %d reading intr data\n", ret); + return IRQ_NONE; + } + + if (!(intr_data[1] & TF103C_DOCK_INTR_DATA1_OBF_MASK)) + return IRQ_NONE; + + /* intr_data[0] is the length of the rest of the packet */ + if (intr_data[0] == 3 && intr_data[1] == TF103C_DOCK_INTR_DATA1_OOB_VALUE && + intr_data[2] == TF103C_DOCK_INTR_DATA2_OOB_VALUE) { + /* intr_data[3] seems to contain a HID input report id */ + switch (intr_data[3]) { + case 0x01: + handle_nested_irq(dock->tp_irq); + break; + case 0x11: + case 0x13: + case 0x14: + tf103c_dock_kbd_interrupt(dock); + break; + default: + dev_warn(dev, "warning unknown intr_data[3]: 0x%02x\n", intr_data[3]); + break; + } + return IRQ_HANDLED; + } + + if (intr_data[1] & TF103C_DOCK_INTR_DATA1_SCI_MASK) { + tf103c_dock_sci(dock, intr_data[2]); + return IRQ_HANDLED; + } + + if (intr_data[1] & TF103C_DOCK_INTR_DATA1_SMI_MASK) { + tf103c_dock_smi(dock, intr_data[2]); + return IRQ_HANDLED; + } + + dev_warn(dev, "warning unknown intr data: %*ph\n", 8, intr_data); + return IRQ_NONE; +} + +/* + * tf103c_dock_[dis|en]able only run from hpd_work or at times when + * hpd_work cannot run (hpd_irq disabled), so no locking is necessary. + */ +static void tf103c_dock_enable(struct tf103c_dock_data *dock) +{ + if (dock->enabled) + return; + + if (dock->board_rev != 2) + gpiod_set_value(dock->pwr_en, 1); + + msleep(500); + enable_irq(dock->irq); + + dock->enabled = true; +} + +static void tf103c_dock_disable(struct tf103c_dock_data *dock) +{ + if (!dock->enabled) + return; + + disable_irq(dock->irq); + tf103c_dock_disable_touchpad(dock); + if (dock->board_rev != 2) + gpiod_set_value(dock->pwr_en, 0); + + dock->enabled = false; +} + +static void tf103c_dock_hpd_work(struct work_struct *work) +{ + struct tf103c_dock_data *dock = + container_of(work, struct tf103c_dock_data, hpd_work.work); + + if (gpiod_get_value(dock->hpd_gpio)) + tf103c_dock_enable(dock); + else + tf103c_dock_disable(dock); +} + +static irqreturn_t tf103c_dock_hpd_irq(int irq, void *data) +{ + struct tf103c_dock_data *dock = data; + + mod_delayed_work(system_long_wq, &dock->hpd_work, TF103C_DOCK_HPD_DEBOUNCE); + return IRQ_HANDLED; +} + +static void tf103c_dock_start_hpd(struct tf103c_dock_data *dock) +{ + enable_irq(dock->hpd_irq); + /* Sync current HPD status */ + queue_delayed_work(system_long_wq, &dock->hpd_work, TF103C_DOCK_HPD_DEBOUNCE); +} + +static void tf103c_dock_stop_hpd(struct tf103c_dock_data *dock) +{ + disable_irq(dock->hpd_irq); + cancel_delayed_work_sync(&dock->hpd_work); +} + +/*** probe ***/ + +static const struct dmi_system_id tf103c_dock_dmi_ids[] = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "TF103C"), + }, + }, + { } +}; + +static void tf103c_dock_non_devm_cleanup(void *data) +{ + struct tf103c_dock_data *dock = data; + + if (dock->tp_irq_domain) + irq_domain_remove(dock->tp_irq_domain); + + if (!IS_ERR_OR_NULL(dock->hid)) + hid_destroy_device(dock->hid); + + i2c_unregister_device(dock->kbd_client); + i2c_unregister_device(dock->intr_client); + gpiod_remove_lookup_table(&tf103c_dock_gpios); +} + +static int tf103c_dock_probe(struct i2c_client *client) +{ + struct i2c_board_info board_info = { }; + struct device *dev = &client->dev; + struct gpio_desc *board_rev_gpio; + struct tf103c_dock_data *dock; + enum gpiod_flags flags; + int i, ret; + + /* GPIOs are hardcoded for the Asus TF103C, don't bind on other devs */ + if (!dmi_check_system(tf103c_dock_dmi_ids)) + return -ENODEV; + + dock = devm_kzalloc(dev, sizeof(*dock), GFP_KERNEL); + if (!dock) + return -ENOMEM; + + INIT_DELAYED_WORK(&dock->hpd_work, tf103c_dock_hpd_work); + + /* 1. Get GPIOs and their IRQs */ + gpiod_add_lookup_table(&tf103c_dock_gpios); + + ret = devm_add_action_or_reset(dev, tf103c_dock_non_devm_cleanup, dock); + if (ret) + return ret; + + /* + * The pin is configured as input by default, use ASIS because otherwise + * the gpio-crystalcove.c switches off the internal pull-down replacing + * it with a pull-up. + */ + board_rev_gpio = gpiod_get(dev, "board_rev", GPIOD_ASIS); + if (IS_ERR(board_rev_gpio)) + return dev_err_probe(dev, PTR_ERR(board_rev_gpio), "requesting board_rev GPIO\n"); + dock->board_rev = gpiod_get_value_cansleep(board_rev_gpio) + 1; + gpiod_put(board_rev_gpio); + + /* + * The Android driver drives the dock-pwr-en pin high at probe for + * revision 2 boards and then never touches it again? + * This code has only been tested on a revision 1 board, so for now + * just mimick what Android does on revision 2 boards. + */ + flags = (dock->board_rev == 2) ? GPIOD_OUT_HIGH : GPIOD_OUT_LOW; + dock->pwr_en = devm_gpiod_get(dev, "dock_pwr_en", flags); + if (IS_ERR(dock->pwr_en)) + return dev_err_probe(dev, PTR_ERR(dock->pwr_en), "requesting pwr_en GPIO\n"); + + dock->irq_gpio = devm_gpiod_get(dev, "dock_irq", GPIOD_IN); + if (IS_ERR(dock->irq_gpio)) + return dev_err_probe(dev, PTR_ERR(dock->irq_gpio), "requesting IRQ GPIO\n"); + + dock->irq = gpiod_to_irq(dock->irq_gpio); + if (dock->irq < 0) + return dev_err_probe(dev, dock->irq, "getting dock IRQ"); + + ret = devm_request_threaded_irq(dev, dock->irq, NULL, tf103c_dock_irq, + IRQF_TRIGGER_LOW | IRQF_ONESHOT | IRQF_NO_AUTOEN, + "dock_irq", dock); + if (ret) + return dev_err_probe(dev, ret, "requesting dock IRQ"); + + dock->hpd_gpio = devm_gpiod_get(dev, "dock_hpd", GPIOD_IN); + if (IS_ERR(dock->hpd_gpio)) + return dev_err_probe(dev, PTR_ERR(dock->hpd_gpio), "requesting HPD GPIO\n"); + + dock->hpd_irq = gpiod_to_irq(dock->hpd_gpio); + if (dock->hpd_irq < 0) + return dev_err_probe(dev, dock->hpd_irq, "getting HPD IRQ"); + + ret = devm_request_irq(dev, dock->hpd_irq, tf103c_dock_hpd_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_NO_AUTOEN, + "dock_hpd", dock); + if (ret) + return ret; + + /* + * 2. Create I2C clients. The dock uses 4 different i2c addresses, + * the ACPI NPCE69A node being probed points to the EC address. + */ + dock->ec_client = client; + + strscpy(board_info.type, "tf103c-dock-intr"); + board_info.addr = TF103C_DOCK_INTR_ADDR; + board_info.dev_name = TF103C_DOCK_DEV_NAME "-intr"; + + dock->intr_client = i2c_new_client_device(client->adapter, &board_info); + if (IS_ERR(dock->intr_client)) + return dev_err_probe(dev, PTR_ERR(dock->intr_client), "creating intr client\n"); + + strscpy(board_info.type, "tf103c-dock-kbd"); + board_info.addr = TF103C_DOCK_KBD_ADDR; + board_info.dev_name = TF103C_DOCK_DEV_NAME "-kbd"; + + dock->kbd_client = i2c_new_client_device(client->adapter, &board_info); + if (IS_ERR(dock->kbd_client)) + return dev_err_probe(dev, PTR_ERR(dock->kbd_client), "creating kbd client\n"); + + /* 3. Create input_dev for the top row of the keyboard */ + dock->input = devm_input_allocate_device(dev); + if (!dock->input) + return -ENOMEM; + + dock->input->name = "Asus TF103C Dock Top Row Keys"; + dock->input->phys = dev_name(dev); + dock->input->dev.parent = dev; + dock->input->id.bustype = BUS_I2C; + dock->input->id.vendor = /* USB_VENDOR_ID_ASUSTEK */ + dock->input->id.product = /* From TF-103-C */ + dock->input->id.version = 0x0100; /* 1.0 */ + + for (i = 0; i < ARRAY_SIZE(tf103c_dock_toprow_codes); i++) { + input_set_capability(dock->input, EV_KEY, tf103c_dock_toprow_codes[i][0]); + input_set_capability(dock->input, EV_KEY, tf103c_dock_toprow_codes[i][1]); + } + input_set_capability(dock->input, EV_KEY, KEY_RIGHTALT); + + ret = input_register_device(dock->input); + if (ret) + return ret; + + /* 4. Create HID device for the keyboard */ + dock->hid = hid_allocate_device(); + if (IS_ERR(dock->hid)) + return dev_err_probe(dev, PTR_ERR(dock->hid), "allocating hid dev\n"); + + dock->hid->driver_data = dock; + dock->hid->ll_driver = &tf103c_dock_hid_ll_driver; + dock->hid->dev.parent = &client->dev; + dock->hid->bus = BUS_I2C; + dock->hid->vendor = 0x0b05; /* USB_VENDOR_ID_ASUSTEK */ + dock->hid->product = 0x0103; /* From TF-103-C */ + dock->hid->version = 0x0100; /* 1.0 */ + strscpy(dock->hid->name, "Asus TF103C Dock Keyboard"); + strscpy(dock->hid->phys, dev_name(dev)); + + ret = hid_add_device(dock->hid); + if (ret) + return dev_err_probe(dev, ret, "adding hid dev\n"); + + /* 5. Setup irqchip for touchpad IRQ pass-through */ + dock->tp_irqchip.name = KBUILD_MODNAME; + + dock->tp_irq_domain = irq_domain_create_linear(NULL, 1, &irq_domain_simple_ops, NULL); + if (!dock->tp_irq_domain) + return -ENOMEM; + + dock->tp_irq = irq_create_mapping(dock->tp_irq_domain, 0); + if (!dock->tp_irq) + return -ENOMEM; + + irq_set_chip_data(dock->tp_irq, dock); + irq_set_chip_and_handler(dock->tp_irq, &dock->tp_irqchip, handle_simple_irq); + irq_set_nested_thread(dock->tp_irq, true); + irq_set_noprobe(dock->tp_irq); + + dev_info(dev, "Asus TF103C board-revision: %d\n", dock->board_rev); + + tf103c_dock_start_hpd(dock); + + device_init_wakeup(dev, true); + i2c_set_clientdata(client, dock); + return 0; +} + +static void tf103c_dock_remove(struct i2c_client *client) +{ + struct tf103c_dock_data *dock = i2c_get_clientdata(client); + + tf103c_dock_stop_hpd(dock); + tf103c_dock_disable(dock); +} + +static int __maybe_unused tf103c_dock_suspend(struct device *dev) +{ + struct tf103c_dock_data *dock = dev_get_drvdata(dev); + + tf103c_dock_stop_hpd(dock); + + if (dock->enabled) { + tf103c_dock_ec_cmd(dock, tf103c_dock_suspend_cmd); + + if (device_may_wakeup(dev)) + enable_irq_wake(dock->irq); + } + + return 0; +} + +static int __maybe_unused tf103c_dock_resume(struct device *dev) +{ + struct tf103c_dock_data *dock = dev_get_drvdata(dev); + + if (dock->enabled) { + if (device_may_wakeup(dev)) + disable_irq_wake(dock->irq); + + /* Don't try to resume if the dock was unplugged during suspend */ + if (gpiod_get_value(dock->hpd_gpio)) + tf103c_dock_ec_cmd(dock, tf103c_dock_enable_cmd); + } + + tf103c_dock_start_hpd(dock); + return 0; +} + +static SIMPLE_DEV_PM_OPS(tf103c_dock_pm_ops, tf103c_dock_suspend, tf103c_dock_resume); + +static const struct acpi_device_id tf103c_dock_acpi_match[] = { + {"NPCE69A"}, + { } +}; +MODULE_DEVICE_TABLE(acpi, tf103c_dock_acpi_match); + +static struct i2c_driver tf103c_dock_driver = { + .driver = { + .name = "asus-tf103c-dock", + .pm = &tf103c_dock_pm_ops, + .acpi_match_table = tf103c_dock_acpi_match, + }, + .probe = tf103c_dock_probe, + .remove = tf103c_dock_remove, +}; +module_i2c_driver(tf103c_dock_driver); + +MODULE_AUTHOR("Hans de Goede +#include +#include +#include +#include +#include +#include +#include +#include + +struct hswc_params { + u8 on; + u8 off; + u8 status; +}; + +struct asus_wireless_data { + struct input_dev *idev; + struct acpi_device *adev; + const struct hswc_params *hswc_params; + struct workqueue_struct *wq; + struct work_struct led_work; + struct led_classdev led; + int led_state; +}; + +static const struct hswc_params atk4001_id_params = { + .on = 0x0, + .off = 0x1, + .status = 0x2, +}; + +static const struct hswc_params atk4002_id_params = { + .on = 0x5, + .off = 0x4, + .status = 0x2, +}; + +static const struct acpi_device_id device_ids[] = { + {"ATK4001", (kernel_ulong_t)&atk4001_id_params}, + {"ATK4002", (kernel_ulong_t)&atk4002_id_params}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, device_ids); + +static acpi_status asus_wireless_method(acpi_handle handle, const char *method, + int param, u64 *ret) +{ + struct acpi_object_list p; + union acpi_object obj; + acpi_status s; + + acpi_handle_debug(handle, "Evaluating method %s, parameter %#x\n", + method, param); + obj.type = ACPI_TYPE_INTEGER; + obj.integer.value = param; + p.count = 1; + p.pointer = &obj; + + s = acpi_evaluate_integer(handle, (acpi_string) method, &p, ret); + if (ACPI_FAILURE(s)) + acpi_handle_err(handle, + "Failed to eval method %s, param %#x (%d)\n", + method, param, s); + else + acpi_handle_debug(handle, "%s returned %#llx\n", method, *ret); + + return s; +} + +static enum led_brightness led_state_get(struct led_classdev *led) +{ + struct asus_wireless_data *data; + acpi_status s; + u64 ret; + + data = container_of(led, struct asus_wireless_data, led); + s = asus_wireless_method(acpi_device_handle(data->adev), "HSWC", + data->hswc_params->status, &ret); + if (ACPI_SUCCESS(s) && ret == data->hswc_params->on) + return LED_FULL; + return LED_OFF; +} + +static void led_state_update(struct work_struct *work) +{ + struct asus_wireless_data *data; + u64 ret; + + data = container_of(work, struct asus_wireless_data, led_work); + asus_wireless_method(acpi_device_handle(data->adev), "HSWC", + data->led_state, &ret); +} + +static void led_state_set(struct led_classdev *led, enum led_brightness value) +{ + struct asus_wireless_data *data; + + data = container_of(led, struct asus_wireless_data, led); + data->led_state = value == LED_OFF ? data->hswc_params->off : + data->hswc_params->on; + queue_work(data->wq, &data->led_work); +} + +static void asus_wireless_notify(acpi_handle handle, u32 event, void *context) +{ + struct asus_wireless_data *data = context; + struct acpi_device *adev = data->adev; + + dev_dbg(&adev->dev, "event=%#x\n", event); + if (event != 0x88) { + dev_notice(&adev->dev, "Unknown ASHS event: %#x\n", event); + return; + } + input_report_key(data->idev, KEY_RFKILL, 1); + input_sync(data->idev); + input_report_key(data->idev, KEY_RFKILL, 0); + input_sync(data->idev); +} + +static int asus_wireless_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct asus_wireless_data *data; + const struct acpi_device_id *id; + int err; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + + data->adev = adev; + + data->idev = devm_input_allocate_device(&pdev->dev); + if (!data->idev) + return -ENOMEM; + data->idev->name = "Asus Wireless Radio Control"; + data->idev->phys = "asus-wireless/input0"; + data->idev->id.bustype = BUS_HOST; + data->idev->id.vendor = PCI_VENDOR_ID_ASUSTEK; + set_bit(EV_KEY, data->idev->evbit); + set_bit(KEY_RFKILL, data->idev->keybit); + err = input_register_device(data->idev); + if (err) + return err; + + id = acpi_match_acpi_device(device_ids, adev); + if (!id) + return 0; + + data->hswc_params = (const struct hswc_params *)id->driver_data; + + data->wq = create_singlethread_workqueue("asus_wireless_workqueue"); + if (!data->wq) + return -ENOMEM; + INIT_WORK(&data->led_work, led_state_update); + data->led.name = "asus-wireless::airplane"; + data->led.brightness_set = led_state_set; + data->led.brightness_get = led_state_get; + data->led.flags = LED_CORE_SUSPENDRESUME; + data->led.max_brightness = 1; + data->led.default_trigger = "rfkill-none"; + err = devm_led_classdev_register(&pdev->dev, &data->led); + if (err) + goto err; + + err = acpi_dev_install_notify_handler(adev, ACPI_DEVICE_NOTIFY, + asus_wireless_notify, data); + if (err) { + devm_led_classdev_unregister(&pdev->dev, &data->led); + goto err; + } + return 0; + +err: + destroy_workqueue(data->wq); + return err; +} + +static void asus_wireless_remove(struct platform_device *pdev) +{ + struct asus_wireless_data *data = platform_get_drvdata(pdev); + + acpi_dev_remove_notify_handler(data->adev, ACPI_DEVICE_NOTIFY, + asus_wireless_notify); + if (data->wq) { + devm_led_classdev_unregister(&pdev->dev, &data->led); + destroy_workqueue(data->wq); + } +} + +static struct platform_driver asus_wireless_driver = { + .probe = asus_wireless_probe, + .remove = asus_wireless_remove, + .driver = { + .name = "Asus Wireless Radio Control Driver", + .acpi_match_table = device_ids, + }, +}; +module_platform_driver(asus_wireless_driver); + +MODULE_DESCRIPTION("Asus Wireless Radio Control Driver"); +MODULE_AUTHOR("João Paulo Rechi Vita "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c new file mode 100644 index 000000000..e835779b6 --- /dev/null +++ b/drivers/platform/x86/asus-wmi.c @@ -0,0 +1,5491 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Asus PC WMI hotkey driver + * + * Copyright(C) 2010 Intel Corporation. + * Copyright(C) 2010-2011 Corentin Chary + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac + * Copyright (C) 2005 Bernhard Rosenkraenzer + * Copyright (C) 2005 Dmitry Torokhov + */ + +#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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "asus-wmi.h" + +MODULE_AUTHOR("Corentin Chary "); +MODULE_AUTHOR("Yong Wang "); +MODULE_DESCRIPTION("Asus Generic WMI Driver"); +MODULE_LICENSE("GPL"); + +static bool fnlock_default = true; +module_param(fnlock_default, bool, 0444); + +#define to_asus_wmi_driver(pdrv) \ + (container_of((pdrv), struct asus_wmi_driver, platform_driver)) + +#define NOTIFY_BRNUP_MIN 0x11 +#define NOTIFY_BRNUP_MAX 0x1f +#define NOTIFY_BRNDOWN_MIN 0x20 +#define NOTIFY_BRNDOWN_MAX 0x2e +#define NOTIFY_FNLOCK_TOGGLE 0x4e +#define NOTIFY_KBD_DOCK_CHANGE 0x75 +#define NOTIFY_KBD_BRTUP 0xc4 +#define NOTIFY_KBD_BRTDWN 0xc5 +#define NOTIFY_KBD_BRTTOGGLE 0xc7 +#define NOTIFY_KBD_FBM 0x99 +#define NOTIFY_KBD_TTP 0xae +#define NOTIFY_LID_FLIP 0xfa +#define NOTIFY_LID_FLIP_ROG 0xbd +#define NOTIFY_KEYSTONE 0xb4 + +#define ASUS_WMI_FNLOCK_BIOS_DISABLED BIT(0) + +#define ASUS_MID_FAN_DESC "mid_fan" +#define ASUS_GPU_FAN_DESC "gpu_fan" +#define ASUS_FAN_DESC "cpu_fan" +#define ASUS_FAN_MFUN 0x13 +#define ASUS_FAN_SFUN_READ 0x06 +#define ASUS_FAN_SFUN_WRITE 0x07 + +/* Based on standard hwmon pwmX_enable values */ +#define ASUS_FAN_CTRL_FULLSPEED 0 +#define ASUS_FAN_CTRL_MANUAL 1 +#define ASUS_FAN_CTRL_AUTO 2 + +#define ASUS_FAN_BOOST_MODE_NORMAL 0 +#define ASUS_FAN_BOOST_MODE_OVERBOOST 1 +#define ASUS_FAN_BOOST_MODE_OVERBOOST_MASK 0x01 +#define ASUS_FAN_BOOST_MODE_SILENT 2 +#define ASUS_FAN_BOOST_MODE_SILENT_MASK 0x02 +#define ASUS_FAN_BOOST_MODES_MASK 0x03 + +#define ASUS_THROTTLE_THERMAL_POLICY_DEFAULT 0 +#define ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST 1 +#define ASUS_THROTTLE_THERMAL_POLICY_SILENT 2 + +#define ASUS_THROTTLE_THERMAL_POLICY_DEFAULT_VIVO 0 +#define ASUS_THROTTLE_THERMAL_POLICY_SILENT_VIVO 1 +#define ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST_VIVO 2 + +#define PLATFORM_PROFILE_MAX 2 + +#define USB_INTEL_XUSB2PR 0xD0 +#define PCI_DEVICE_ID_INTEL_LYNXPOINT_LP_XHCI 0x9c31 + +#define WMI_EVENT_MASK 0xFFFF + +#define FAN_CURVE_POINTS 8 +#define FAN_CURVE_BUF_LEN 32 +#define FAN_CURVE_DEV_CPU 0x00 +#define FAN_CURVE_DEV_GPU 0x01 +#define FAN_CURVE_DEV_MID 0x02 +/* Mask to determine if setting temperature or percentage */ +#define FAN_CURVE_PWM_MASK 0x04 + +/* Limits for tunables available on ASUS ROG laptops */ +#define PPT_TOTAL_MIN 5 +#define PPT_TOTAL_MAX 250 +#define PPT_CPU_MIN 5 +#define PPT_CPU_MAX 130 +#define NVIDIA_BOOST_MIN 5 +#define NVIDIA_BOOST_MAX 25 +#define NVIDIA_TEMP_MIN 75 +#define NVIDIA_TEMP_MAX 87 + +#define ASUS_SCREENPAD_BRIGHT_MAX 255 +#define ASUS_SCREENPAD_BRIGHT_DEFAULT 60 + +#define ASUS_MINI_LED_MODE_MASK 0x03 +/* Standard modes for devices with only on/off */ +#define ASUS_MINI_LED_OFF 0x00 +#define ASUS_MINI_LED_ON 0x01 +/* New mode on some devices, define here to clarify remapping later */ +#define ASUS_MINI_LED_STRONG_MODE 0x02 +/* New modes for devices with 3 mini-led mode types */ +#define ASUS_MINI_LED_2024_WEAK 0x00 +#define ASUS_MINI_LED_2024_STRONG 0x01 +#define ASUS_MINI_LED_2024_OFF 0x02 + +#define ASUS_USB0_PWR_EC0_CSEE "\\_SB.PCI0.SBRG.EC0.CSEE" +/* + * The period required to wait after screen off/on/s2idle.check in MS. + * Time here greatly impacts the wake behaviour. Used in suspend/wake. + */ +#define ASUS_USB0_PWR_EC0_CSEE_WAIT 600 +#define ASUS_USB0_PWR_EC0_CSEE_OFF 0xB7 +#define ASUS_USB0_PWR_EC0_CSEE_ON 0xB8 + +static const char * const ashs_ids[] = { "ATK4001", "ATK4002", NULL }; + +static int throttle_thermal_policy_write(struct asus_wmi *); + +static const struct dmi_system_id asus_rog_ally_device[] = { + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "RC71L"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "RC72L"), + }, + }, + { }, +}; + +static bool ashs_present(void) +{ + int i = 0; + while (ashs_ids[i]) { + if (acpi_dev_found(ashs_ids[i++])) + return true; + } + return false; +} + +struct bios_args { + u32 arg0; + u32 arg1; + u32 arg2; /* At least TUF Gaming series uses 3 dword input buffer. */ + u32 arg3; + u32 arg4; /* Some ROG laptops require a full 5 input args */ + u32 arg5; +} __packed; + +/* + * Struct that's used for all methods called via AGFN. Naming is + * identically to the AML code. + */ +struct agfn_args { + u16 mfun; /* probably "Multi-function" to be called */ + u16 sfun; /* probably "Sub-function" to be called */ + u16 len; /* size of the hole struct, including subfunction fields */ + u8 stas; /* not used by now */ + u8 err; /* zero on success */ +} __packed; + +/* struct used for calling fan read and write methods */ +struct agfn_fan_args { + struct agfn_args agfn; /* common fields */ + u8 fan; /* fan number: 0: set auto mode 1: 1st fan */ + u32 speed; /* read: RPM/100 - write: 0-255 */ +} __packed; + +/* + * / - debugfs root directory + * dev_id - current dev_id + * ctrl_param - current ctrl_param + * method_id - current method_id + * devs - call DEVS(dev_id, ctrl_param) and print result + * dsts - call DSTS(dev_id) and print result + * call - call method_id(dev_id, ctrl_param) and print result + */ +struct asus_wmi_debug { + struct dentry *root; + u32 method_id; + u32 dev_id; + u32 ctrl_param; +}; + +struct asus_rfkill { + struct asus_wmi *asus; + struct rfkill *rfkill; + u32 dev_id; +}; + +enum fan_type { + FAN_TYPE_NONE = 0, + FAN_TYPE_AGFN, /* deprecated on newer platforms */ + FAN_TYPE_SPEC83, /* starting in Spec 8.3, use CPU_FAN_CTRL */ +}; + +struct fan_curve_data { + bool enabled; + u32 device_id; + u8 temps[FAN_CURVE_POINTS]; + u8 percents[FAN_CURVE_POINTS]; +}; + +struct asus_wmi { + int dsts_id; + int spec; + int sfun; + + struct input_dev *inputdev; + struct backlight_device *backlight_device; + struct backlight_device *screenpad_backlight_device; + struct platform_device *platform_device; + + struct led_classdev wlan_led; + int wlan_led_wk; + struct led_classdev tpd_led; + int tpd_led_wk; + struct led_classdev kbd_led; + int kbd_led_wk; + bool kbd_led_notify; + bool kbd_led_avail; + bool kbd_led_registered; + struct led_classdev lightbar_led; + int lightbar_led_wk; + struct led_classdev micmute_led; + struct led_classdev camera_led; + struct workqueue_struct *led_workqueue; + struct work_struct tpd_led_work; + struct work_struct wlan_led_work; + struct work_struct lightbar_led_work; + struct work_struct kbd_led_work; + + struct asus_rfkill wlan; + struct asus_rfkill bluetooth; + struct asus_rfkill wimax; + struct asus_rfkill wwan3g; + struct asus_rfkill gps; + struct asus_rfkill uwb; + + int tablet_switch_event_code; + u32 tablet_switch_dev_id; + bool tablet_switch_inverted; + + bool keystone_available; + + enum fan_type fan_type; + enum fan_type gpu_fan_type; + enum fan_type mid_fan_type; + int fan_pwm_mode; + int gpu_fan_pwm_mode; + int mid_fan_pwm_mode; + int agfn_pwm; + + bool fan_boost_mode_available; + u8 fan_boost_mode_mask; + u8 fan_boost_mode; + + bool egpu_enable_available; + bool dgpu_disable_available; + u32 gpu_mux_dev; + + /* Tunables provided by ASUS for gaming laptops */ + u32 ppt_pl2_sppt; + u32 ppt_pl1_spl; + u32 ppt_apu_sppt; + u32 ppt_platform_sppt; + u32 ppt_fppt; + u32 nv_dynamic_boost; + u32 nv_temp_target; + + u32 kbd_rgb_dev; + bool kbd_rgb_state_available; + bool oobe_state_available; + + u8 throttle_thermal_policy_mode; + u32 throttle_thermal_policy_dev; + + bool cpu_fan_curve_available; + bool gpu_fan_curve_available; + bool mid_fan_curve_available; + struct fan_curve_data custom_fan_curves[3]; + + struct device *ppdev; + bool platform_profile_support; + + // The RSOC controls the maximum charging percentage. + bool battery_rsoc_available; + + bool panel_overdrive_available; + u32 mini_led_dev_id; + + struct hotplug_slot hotplug_slot; + struct mutex hotplug_lock; + struct mutex wmi_lock; + struct workqueue_struct *hotplug_workqueue; + struct work_struct hotplug_work; + + bool fnlock_locked; + + struct asus_wmi_debug debug; + + struct asus_wmi_driver *driver; +}; + +/* Global to allow setting externally without requiring driver data */ +static enum asus_ally_mcu_hack use_ally_mcu_hack = ASUS_WMI_ALLY_MCU_HACK_INIT; + +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static void asus_wmi_show_deprecated(void) +{ + pr_notice_once("Accessing attributes through /sys/bus/platform/asus_wmi is deprecated and will be removed in a future release. Please switch over to /sys/class/firmware_attributes.\n"); +} +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* WMI ************************************************************************/ + +static int asus_wmi_evaluate_method3(u32 method_id, + u32 arg0, u32 arg1, u32 arg2, u32 *retval) +{ + struct bios_args args = { + .arg0 = arg0, + .arg1 = arg1, + .arg2 = arg2, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + union acpi_object *obj; + u32 tmp = 0; + + status = wmi_evaluate_method(ASUS_WMI_MGMT_GUID, 0, method_id, + &input, &output); + + pr_debug("%s called (0x%08x) with args: 0x%08x, 0x%08x, 0x%08x\n", + __func__, method_id, arg0, arg1, arg2); + if (ACPI_FAILURE(status)) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, -EIO); + return -EIO; + } + + obj = (union acpi_object *)output.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + tmp = (u32) obj->integer.value; + + pr_debug("Result: 0x%08x\n", tmp); + if (retval) + *retval = tmp; + + kfree(obj); + + if (tmp == ASUS_WMI_UNSUPPORTED_METHOD) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, -ENODEV); + return -ENODEV; + } + + return 0; +} + +int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1, u32 *retval) +{ + return asus_wmi_evaluate_method3(method_id, arg0, arg1, 0, retval); +} +EXPORT_SYMBOL_NS_GPL(asus_wmi_evaluate_method, "ASUS_WMI"); + +static int asus_wmi_evaluate_method5(u32 method_id, + u32 arg0, u32 arg1, u32 arg2, u32 arg3, u32 arg4, u32 *retval) +{ + struct bios_args args = { + .arg0 = arg0, + .arg1 = arg1, + .arg2 = arg2, + .arg3 = arg3, + .arg4 = arg4, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + union acpi_object *obj; + u32 tmp = 0; + + status = wmi_evaluate_method(ASUS_WMI_MGMT_GUID, 0, method_id, + &input, &output); + + pr_debug("%s called (0x%08x) with args: 0x%08x, 0x%08x, 0x%08x, 0x%08x, 0x%08x\n", + __func__, method_id, arg0, arg1, arg2, arg3, arg4); + if (ACPI_FAILURE(status)) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, -EIO); + return -EIO; + } + + obj = (union acpi_object *)output.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + tmp = (u32) obj->integer.value; + + pr_debug("Result: %x\n", tmp); + if (retval) + *retval = tmp; + + kfree(obj); + + if (tmp == ASUS_WMI_UNSUPPORTED_METHOD) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, -ENODEV); + return -ENODEV; + } + + return 0; +} + +/* + * Returns as an error if the method output is not a buffer. Typically this + * means that the method called is unsupported. + */ +static int asus_wmi_evaluate_method_buf(u32 method_id, + u32 arg0, u32 arg1, u8 *ret_buffer, size_t size) +{ + struct bios_args args = { + .arg0 = arg0, + .arg1 = arg1, + .arg2 = 0, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + union acpi_object *obj; + int err = 0; + + status = wmi_evaluate_method(ASUS_WMI_MGMT_GUID, 0, method_id, + &input, &output); + + pr_debug("%s called (0x%08x) with args: 0x%08x, 0x%08x\n", + __func__, method_id, arg0, arg1); + if (ACPI_FAILURE(status)) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, -EIO); + return -EIO; + } + + obj = (union acpi_object *)output.pointer; + + switch (obj->type) { + case ACPI_TYPE_BUFFER: + if (obj->buffer.length > size) { + err = -ENOSPC; + break; + } + if (obj->buffer.length == 0) { + err = -ENODATA; + break; + } + + memcpy(ret_buffer, obj->buffer.pointer, obj->buffer.length); + break; + case ACPI_TYPE_INTEGER: + err = (u32)obj->integer.value; + + if (err == ASUS_WMI_UNSUPPORTED_METHOD) + err = -ENODEV; + /* + * At least one method returns a 0 with no buffer if no arg + * is provided, such as ASUS_WMI_DEVID_CPU_FAN_CURVE + */ + if (err == 0) + err = -ENODATA; + break; + default: + err = -ENODATA; + break; + } + + kfree(obj); + + if (err) { + pr_debug("%s, (0x%08x), arg 0x%08x failed: %d\n", + __func__, method_id, arg0, err); + return err; + } + + return 0; +} + +static int asus_wmi_evaluate_method_agfn(const struct acpi_buffer args) +{ + struct acpi_buffer input; + u64 phys_addr; + u32 retval; + u32 status; + + /* + * Copy to dma capable address otherwise memory corruption occurs as + * bios has to be able to access it. + */ + input.pointer = kmemdup(args.pointer, args.length, GFP_DMA | GFP_KERNEL); + input.length = args.length; + if (!input.pointer) + return -ENOMEM; + phys_addr = virt_to_phys(input.pointer); + + status = asus_wmi_evaluate_method(ASUS_WMI_METHODID_AGFN, + phys_addr, 0, &retval); + if (!status) + memcpy(args.pointer, input.pointer, args.length); + + kfree(input.pointer); + if (status) + return -ENXIO; + + return retval; +} + +static int asus_wmi_get_devstate(struct asus_wmi *asus, u32 dev_id, u32 *retval) +{ + int err; + + err = asus_wmi_evaluate_method(asus->dsts_id, dev_id, 0, retval); + + if (err) + return err; + + if (*retval == ~0) + return -ENODEV; + + return 0; +} + +/** + * asus_wmi_get_devstate_dsts() - Get the WMI function state. + * @dev_id: The WMI method ID to call. + * @retval: A pointer to where to store the value returned from WMI. + * + * Returns: + * * %-ENODEV - method ID is unsupported. + * * %0 - successful and retval is filled. + * * %other - error from WMI call. + */ +int asus_wmi_get_devstate_dsts(u32 dev_id, u32 *retval) +{ + int err; + + err = asus_wmi_evaluate_method(ASUS_WMI_METHODID_DSTS, dev_id, 0, retval); + if (err) + return err; + + if ((*retval & ASUS_WMI_DSTS_PRESENCE_BIT) == 0x00) + return -ENODEV; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(asus_wmi_get_devstate_dsts, "ASUS_WMI"); + +/** + * asus_wmi_set_devstate() - Set the WMI function state. + * + * Note: an asus_wmi_set_devstate() call must be paired with a + * asus_wmi_get_devstate_dsts() to check if the WMI function is supported. + * + * @dev_id: The WMI function to call. + * @ctrl_param: The argument to be used for this WMI function. + * @retval: A pointer to where to store the value returned from WMI. + * + * Returns: + * * %-ENODEV - method ID is unsupported. + * * %0 - successful and retval is filled. + * * %other - error from WMI call. + */ +int asus_wmi_set_devstate(u32 dev_id, u32 ctrl_param, u32 *retval) +{ + return asus_wmi_evaluate_method(ASUS_WMI_METHODID_DEVS, dev_id, + ctrl_param, retval); +} +EXPORT_SYMBOL_NS_GPL(asus_wmi_set_devstate, "ASUS_WMI"); + +/* Helper for special devices with magic return codes */ +static int asus_wmi_get_devstate_bits(struct asus_wmi *asus, + u32 dev_id, u32 mask) +{ + u32 retval = 0; + int err; + + err = asus_wmi_get_devstate(asus, dev_id, &retval); + if (err < 0) + return err; + + if (!(retval & ASUS_WMI_DSTS_PRESENCE_BIT)) + return -ENODEV; + + if (mask == ASUS_WMI_DSTS_STATUS_BIT) { + if (retval & ASUS_WMI_DSTS_UNKNOWN_BIT) + return -ENODEV; + } + + return retval & mask; +} + +static int asus_wmi_get_devstate_simple(struct asus_wmi *asus, u32 dev_id) +{ + return asus_wmi_get_devstate_bits(asus, dev_id, + ASUS_WMI_DSTS_STATUS_BIT); +} + +static bool asus_wmi_dev_is_present(struct asus_wmi *asus, u32 dev_id) +{ + u32 retval; + int status = asus_wmi_get_devstate(asus, dev_id, &retval); + pr_debug("%s called (0x%08x), retval: 0x%08x\n", __func__, dev_id, retval); + + return status == 0 && (retval & ASUS_WMI_DSTS_PRESENCE_BIT); +} + +/* Keystone *******************************************************************/ + +static int keystone_check_present(struct asus_wmi *asus) +{ + u32 retval; + int err; + + asus->keystone_available = false; + + /* + * Use a raw devstate call rather than asus_wmi_dev_is_present(). + * For this devid, PRESENCE_BIT encodes current insert state, not + * feature presence, so asus_wmi_dev_is_present() would return false + * whenever the dongle is absent at boot, even on machines that have + * a keystone slot. + * -ENODEV means the firmware doesn't know this devid at all. + * retval is not examined here, only the return code matters. + */ + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_KEYSTONE, &retval); + if (err == -ENODEV) + return 0; + if (err) + return err; + + asus->keystone_available = true; + return 0; +} + +static ssize_t keystone_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u32 retval; + int err; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_KEYSTONE, &retval); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", !!(retval & ASUS_WMI_DSTS_PRESENCE_BIT)); +} + +static DEVICE_ATTR_RO(keystone); + +static void asus_wmi_keystone_notify(struct asus_wmi *asus) +{ + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "keystone"); +} + +/* Input **********************************************************************/ +static void asus_wmi_tablet_sw_report(struct asus_wmi *asus, bool value) +{ + input_report_switch(asus->inputdev, SW_TABLET_MODE, + asus->tablet_switch_inverted ? !value : value); + input_sync(asus->inputdev); +} + +static void asus_wmi_tablet_sw_init(struct asus_wmi *asus, u32 dev_id, int event_code) +{ + struct device *dev = &asus->platform_device->dev; + int result; + + result = asus_wmi_get_devstate_simple(asus, dev_id); + if (result >= 0) { + input_set_capability(asus->inputdev, EV_SW, SW_TABLET_MODE); + asus_wmi_tablet_sw_report(asus, result); + asus->tablet_switch_dev_id = dev_id; + asus->tablet_switch_event_code = event_code; + } else if (result == -ENODEV) { + dev_err(dev, "This device has tablet-mode-switch quirk but got ENODEV checking it. This is a bug."); + } else { + dev_err(dev, "Error checking for tablet-mode-switch: %d\n", result); + } +} + +static int asus_wmi_input_init(struct asus_wmi *asus) +{ + struct device *dev = &asus->platform_device->dev; + int err; + + asus->inputdev = input_allocate_device(); + if (!asus->inputdev) + return -ENOMEM; + + asus->inputdev->name = asus->driver->input_name; + asus->inputdev->phys = asus->driver->input_phys; + asus->inputdev->id.bustype = BUS_HOST; + asus->inputdev->dev.parent = dev; + set_bit(EV_REP, asus->inputdev->evbit); + + err = sparse_keymap_setup(asus->inputdev, asus->driver->keymap, NULL); + if (err) + goto err_free_dev; + + switch (asus->driver->quirks->tablet_switch_mode) { + case asus_wmi_no_tablet_switch: + break; + case asus_wmi_kbd_dock_devid: + asus->tablet_switch_inverted = true; + asus_wmi_tablet_sw_init(asus, ASUS_WMI_DEVID_KBD_DOCK, NOTIFY_KBD_DOCK_CHANGE); + break; + case asus_wmi_lid_flip_devid: + asus_wmi_tablet_sw_init(asus, ASUS_WMI_DEVID_LID_FLIP, NOTIFY_LID_FLIP); + break; + case asus_wmi_lid_flip_rog_devid: + asus_wmi_tablet_sw_init(asus, ASUS_WMI_DEVID_LID_FLIP_ROG, NOTIFY_LID_FLIP_ROG); + break; + } + + err = input_register_device(asus->inputdev); + if (err) + goto err_free_dev; + + return 0; + +err_free_dev: + input_free_device(asus->inputdev); + return err; +} + +static void asus_wmi_input_exit(struct asus_wmi *asus) +{ + if (asus->inputdev) + input_unregister_device(asus->inputdev); + + asus->inputdev = NULL; +} + +/* Tablet mode ****************************************************************/ + +static void asus_wmi_tablet_mode_get_state(struct asus_wmi *asus) +{ + int result; + + if (!asus->tablet_switch_dev_id) + return; + + result = asus_wmi_get_devstate_simple(asus, asus->tablet_switch_dev_id); + if (result >= 0) + asus_wmi_tablet_sw_report(asus, result); +} + +/* Charging mode, 1=Barrel, 2=USB ******************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t charge_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, value; + + result = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_CHARGE_MODE, &value); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", value & 0xff); +} + +static DEVICE_ATTR_RO(charge_mode); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* dGPU ********************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t dgpu_disable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_DGPU); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +/* + * A user may be required to store the value twice, typcial store first, then + * rescan PCI bus to activate power, then store a second time to save correctly. + * The reason for this is that an extra code path in the ACPI is enabled when + * the device and bus are powered. + */ +static ssize_t dgpu_disable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 disable; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + result = kstrtou32(buf, 10, &disable); + if (result) + return result; + + if (disable > 1) + return -EINVAL; + + if (asus->gpu_mux_dev) { + result = asus_wmi_get_devstate_simple(asus, asus->gpu_mux_dev); + if (result < 0) + /* An error here may signal greater failure of GPU handling */ + return result; + if (!result && disable) { + err = -ENODEV; + pr_warn("Can not disable dGPU when the MUX is in dGPU mode: %d\n", err); + return err; + } + } + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_DGPU, disable, &result); + if (err) { + pr_warn("Failed to set dgpu disable: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set dgpu disable (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "dgpu_disable"); + + return count; +} +static DEVICE_ATTR_RW(dgpu_disable); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* eGPU ********************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t egpu_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_EGPU); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +/* The ACPI call to enable the eGPU also disables the internal dGPU */ +static ssize_t egpu_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 enable; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + err = kstrtou32(buf, 10, &enable); + if (err) + return err; + + if (enable > 1) + return -EINVAL; + + err = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_EGPU_CONNECTED); + if (err < 0) { + pr_warn("Failed to get egpu connection status: %d\n", err); + return err; + } + + if (asus->gpu_mux_dev) { + result = asus_wmi_get_devstate_simple(asus, asus->gpu_mux_dev); + if (result < 0) { + /* An error here may signal greater failure of GPU handling */ + pr_warn("Failed to get gpu mux status: %d\n", result); + return result; + } + if (!result && enable) { + err = -ENODEV; + pr_warn("Can not enable eGPU when the MUX is in dGPU mode: %d\n", err); + return err; + } + } + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_EGPU, enable, &result); + if (err) { + pr_warn("Failed to set egpu state: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set egpu state (retval): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "egpu_enable"); + + return count; +} +static DEVICE_ATTR_RW(egpu_enable); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Is eGPU connected? *********************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t egpu_connected_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_EGPU_CONNECTED); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +static DEVICE_ATTR_RO(egpu_connected); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* gpu mux switch *************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t gpu_mux_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, asus->gpu_mux_dev); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +static ssize_t gpu_mux_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 optimus; + + err = kstrtou32(buf, 10, &optimus); + if (err) + return err; + + if (optimus > 1) + return -EINVAL; + + if (asus->dgpu_disable_available) { + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_DGPU); + if (result < 0) + /* An error here may signal greater failure of GPU handling */ + return result; + if (result && !optimus) { + err = -ENODEV; + pr_warn("Can not switch MUX to dGPU mode when dGPU is disabled: %d\n", err); + return err; + } + } + + if (asus->egpu_enable_available) { + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_EGPU); + if (result < 0) + /* An error here may signal greater failure of GPU handling */ + return result; + if (result && !optimus) { + err = -ENODEV; + pr_warn("Can not switch MUX to dGPU mode when eGPU is enabled: %d\n", err); + return err; + } + } + + err = asus_wmi_set_devstate(asus->gpu_mux_dev, optimus, &result); + if (err) { + dev_err(dev, "Failed to set GPU MUX mode: %d\n", err); + return err; + } + /* !1 is considered a fail by ASUS */ + if (result != 1) { + dev_warn(dev, "Failed to set GPU MUX mode (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "gpu_mux_mode"); + + return count; +} +static DEVICE_ATTR_RW(gpu_mux_mode); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* TUF Laptop Keyboard RGB Modes **********************************************/ +static ssize_t kbd_rgb_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 cmd, mode, r, g, b, speed; + struct led_classdev *led; + struct asus_wmi *asus; + int err; + + led = dev_get_drvdata(dev); + asus = container_of(led, struct asus_wmi, kbd_led); + + if (sscanf(buf, "%d %d %d %d %d %d", &cmd, &mode, &r, &g, &b, &speed) != 6) + return -EINVAL; + + /* B3 is set and B4 is save to BIOS */ + switch (cmd) { + case 0: + cmd = 0xb3; + break; + case 1: + cmd = 0xb4; + break; + default: + return -EINVAL; + } + + /* These are the known usable modes across all TUF/ROG */ + if (mode >= 12 || mode == 9) + mode = 10; + + switch (speed) { + case 0: + speed = 0xe1; + break; + case 1: + speed = 0xeb; + break; + case 2: + speed = 0xf5; + break; + default: + speed = 0xeb; + } + + err = asus_wmi_evaluate_method3(ASUS_WMI_METHODID_DEVS, asus->kbd_rgb_dev, + cmd | (mode << 8) | (r << 16) | (g << 24), b | (speed << 8), NULL); + if (err) + return err; + + return count; +} +static DEVICE_ATTR_WO(kbd_rgb_mode); + +static DEVICE_STRING_ATTR_RO(kbd_rgb_mode_index, 0444, + "cmd mode red green blue speed"); + +static struct attribute *kbd_rgb_mode_attrs[] = { + &dev_attr_kbd_rgb_mode.attr, + &dev_attr_kbd_rgb_mode_index.attr.attr, + NULL, +}; + +static const struct attribute_group kbd_rgb_mode_group = { + .attrs = kbd_rgb_mode_attrs, +}; + +/* TUF Laptop Keyboard RGB State **********************************************/ +static ssize_t kbd_rgb_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 flags, cmd, boot, awake, sleep, keyboard; + int err; + + if (sscanf(buf, "%d %d %d %d %d", &cmd, &boot, &awake, &sleep, &keyboard) != 5) + return -EINVAL; + + if (cmd) + cmd = BIT(2); + + flags = 0; + if (boot) + flags |= BIT(1); + if (awake) + flags |= BIT(3); + if (sleep) + flags |= BIT(5); + if (keyboard) + flags |= BIT(7); + + /* 0xbd is the required default arg0 for the method. Nothing happens otherwise */ + err = asus_wmi_evaluate_method3(ASUS_WMI_METHODID_DEVS, + ASUS_WMI_DEVID_TUF_RGB_STATE, 0xbd | cmd << 8 | (flags << 16), 0, NULL); + if (err) + return err; + + return count; +} +static DEVICE_ATTR_WO(kbd_rgb_state); + +static DEVICE_STRING_ATTR_RO(kbd_rgb_state_index, 0444, + "cmd boot awake sleep keyboard"); + +static struct attribute *kbd_rgb_state_attrs[] = { + &dev_attr_kbd_rgb_state.attr, + &dev_attr_kbd_rgb_state_index.attr.attr, + NULL, +}; + +static const struct attribute_group kbd_rgb_state_group = { + .attrs = kbd_rgb_state_attrs, +}; + +static const struct attribute_group *kbd_rgb_mode_groups[] = { + NULL, + NULL, + NULL, +}; + +/* Tunable: PPT: Intel=PL1, AMD=SPPT *****************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t ppt_pl2_sppt_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < PPT_TOTAL_MIN || value > PPT_TOTAL_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PPT_PL2_SPPT, value, &result); + if (err) { + pr_warn("Failed to set ppt_pl2_sppt: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set ppt_pl2_sppt (result): 0x%x\n", result); + return -EIO; + } + + asus->ppt_pl2_sppt = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "ppt_pl2_sppt"); + + return count; +} + +static ssize_t ppt_pl2_sppt_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->ppt_pl2_sppt); +} +static DEVICE_ATTR_RW(ppt_pl2_sppt); + +/* Tunable: PPT, Intel=PL1, AMD=SPL ******************************************/ +static ssize_t ppt_pl1_spl_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < PPT_TOTAL_MIN || value > PPT_TOTAL_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PPT_PL1_SPL, value, &result); + if (err) { + pr_warn("Failed to set ppt_pl1_spl: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set ppt_pl1_spl (result): 0x%x\n", result); + return -EIO; + } + + asus->ppt_pl1_spl = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "ppt_pl1_spl"); + + return count; +} +static ssize_t ppt_pl1_spl_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->ppt_pl1_spl); +} +static DEVICE_ATTR_RW(ppt_pl1_spl); + +/* Tunable: PPT APU FPPT ******************************************************/ +static ssize_t ppt_fppt_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < PPT_TOTAL_MIN || value > PPT_TOTAL_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PPT_PL3_FPPT, value, &result); + if (err) { + pr_warn("Failed to set ppt_fppt: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set ppt_fppt (result): 0x%x\n", result); + return -EIO; + } + + asus->ppt_fppt = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "ppt_fpu_sppt"); + + return count; +} + +static ssize_t ppt_fppt_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->ppt_fppt); +} +static DEVICE_ATTR_RW(ppt_fppt); + +/* Tunable: PPT APU SPPT *****************************************************/ +static ssize_t ppt_apu_sppt_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < PPT_CPU_MIN || value > PPT_CPU_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PPT_APU_SPPT, value, &result); + if (err) { + pr_warn("Failed to set ppt_apu_sppt: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set ppt_apu_sppt (result): 0x%x\n", result); + return -EIO; + } + + asus->ppt_apu_sppt = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "ppt_apu_sppt"); + + return count; +} + +static ssize_t ppt_apu_sppt_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->ppt_apu_sppt); +} +static DEVICE_ATTR_RW(ppt_apu_sppt); + +/* Tunable: PPT platform SPPT ************************************************/ +static ssize_t ppt_platform_sppt_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < PPT_CPU_MIN || value > PPT_CPU_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PPT_PLAT_SPPT, value, &result); + if (err) { + pr_warn("Failed to set ppt_platform_sppt: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set ppt_platform_sppt (result): 0x%x\n", result); + return -EIO; + } + + asus->ppt_platform_sppt = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "ppt_platform_sppt"); + + return count; +} + +static ssize_t ppt_platform_sppt_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->ppt_platform_sppt); +} +static DEVICE_ATTR_RW(ppt_platform_sppt); + +/* Tunable: NVIDIA dynamic boost *********************************************/ +static ssize_t nv_dynamic_boost_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < NVIDIA_BOOST_MIN || value > NVIDIA_BOOST_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_NV_DYN_BOOST, value, &result); + if (err) { + pr_warn("Failed to set nv_dynamic_boost: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set nv_dynamic_boost (result): 0x%x\n", result); + return -EIO; + } + + asus->nv_dynamic_boost = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "nv_dynamic_boost"); + + return count; +} + +static ssize_t nv_dynamic_boost_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->nv_dynamic_boost); +} +static DEVICE_ATTR_RW(nv_dynamic_boost); + +/* Tunable: NVIDIA temperature target ****************************************/ +static ssize_t nv_temp_target_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result, err; + u32 value; + + result = kstrtou32(buf, 10, &value); + if (result) + return result; + + if (value < NVIDIA_TEMP_MIN || value > NVIDIA_TEMP_MAX) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_NV_THERM_TARGET, value, &result); + if (err) { + pr_warn("Failed to set nv_temp_target: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set nv_temp_target (result): 0x%x\n", result); + return -EIO; + } + + asus->nv_temp_target = value; + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "nv_temp_target"); + + return count; +} + +static ssize_t nv_temp_target_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%u\n", asus->nv_temp_target); +} +static DEVICE_ATTR_RW(nv_temp_target); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Ally MCU Powersave ********************************************************/ + +/* + * The HID driver needs to check MCU version and set this to false if the MCU FW + * version is >= the minimum requirements. New FW do not need the hacks. + */ +void set_ally_mcu_hack(enum asus_ally_mcu_hack status) +{ + use_ally_mcu_hack = status; + pr_debug("%s Ally MCU suspend quirk\n", + status == ASUS_WMI_ALLY_MCU_HACK_ENABLED ? "Enabled" : "Disabled"); +} +EXPORT_SYMBOL_NS_GPL(set_ally_mcu_hack, "ASUS_WMI"); + +/* + * mcu_powersave should be enabled always, as it is fixed in MCU FW versions: + * - v313 for Ally X + * - v319 for Ally 1 + * The HID driver checks MCU versions and so should set this if requirements match + */ +void set_ally_mcu_powersave(bool enabled) +{ + int result, err; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_MCU_POWERSAVE, enabled, &result); + if (err) { + pr_warn("Failed to set MCU powersave: %d\n", err); + return; + } + if (result > 1) { + pr_warn("Failed to set MCU powersave (result): 0x%x\n", result); + return; + } + + pr_debug("%s MCU Powersave\n", + enabled ? "Enabled" : "Disabled"); +} +EXPORT_SYMBOL_NS_GPL(set_ally_mcu_powersave, "ASUS_WMI"); + +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t mcu_powersave_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_MCU_POWERSAVE); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +static ssize_t mcu_powersave_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 enable; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + result = kstrtou32(buf, 10, &enable); + if (result) + return result; + + if (enable > 1) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_MCU_POWERSAVE, enable, &result); + if (err) { + pr_warn("Failed to set MCU powersave: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set MCU powersave (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "mcu_powersave"); + + return count; +} +static DEVICE_ATTR_RW(mcu_powersave); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Battery ********************************************************************/ + +/* The battery maximum charging percentage */ +static int charge_end_threshold; + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int value, ret, rv; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value < 0 || value > 100) + return -EINVAL; + + ret = asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, value, &rv); + if (ret) + return ret; + + if (rv != 1) + return -EIO; + + /* There isn't any method in the DSDT to read the threshold, so we + * save the threshold. + */ + charge_end_threshold = value; + return count; +} + +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + if ((charge_end_threshold >= 0) && (charge_end_threshold <= 100)) + return sysfs_emit(buf, "%d\n", charge_end_threshold); + + return -ENODATA; +} + +static DEVICE_ATTR_RW(charge_control_end_threshold); + +static int asus_wmi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + int ret, rv; + + /* The WMI method does not provide a way to specific a battery, so we + * just assume it is the first battery. + * Note: On some newer ASUS laptops (Zenbook UM431DA), the primary/first + * battery is named BATT. + */ + if (strcmp(battery->desc->name, "BAT0") != 0 && + strcmp(battery->desc->name, "BAT1") != 0 && + strcmp(battery->desc->name, "BATC") != 0 && + strcmp(battery->desc->name, "BATT") != 0) + return -ENODEV; + + if (device_create_file(&battery->dev, + &dev_attr_charge_control_end_threshold)) + return -ENODEV; + + /* The charge threshold is only reset when the system is power cycled, + * and we can't read the current threshold, however the majority of + * platforms retains it. + * + * Setting a negative value would signal the threshold as unknown + * until user explicitly sets it to a new value, however to avoid + * regressing userspace, we initialize it to a value of 100. + */ + charge_end_threshold = 100; + ret = asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, charge_end_threshold, &rv); + if (ret) { + pr_err("Failed to reset battery charge threshold\n"); + goto asus_wmi_battery_add_err; + } + + if (rv != 1) { + pr_err("Error in battery charge threshold reset\n"); + ret = -EIO; + goto asus_wmi_battery_add_err; + } + + return 0; +asus_wmi_battery_add_err: + device_remove_file(&battery->dev, + &dev_attr_charge_control_end_threshold); + return ret; +} + +static int asus_wmi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_file(&battery->dev, + &dev_attr_charge_control_end_threshold); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = asus_wmi_battery_add, + .remove_battery = asus_wmi_battery_remove, + .name = "ASUS Battery Extension", +}; + +static void asus_wmi_battery_init(struct asus_wmi *asus) +{ + asus->battery_rsoc_available = false; + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_RSOC)) { + asus->battery_rsoc_available = true; + battery_hook_register(&battery_hook); + } +} + +static void asus_wmi_battery_exit(struct asus_wmi *asus) +{ + if (asus->battery_rsoc_available) + battery_hook_unregister(&battery_hook); +} + +/* LEDs ***********************************************************************/ + +struct asus_hid_ref { + struct list_head listeners; + struct asus_wmi *asus; + /* Protects concurrent access from hid-asus and asus-wmi to leds */ + spinlock_t lock; +}; + +static struct asus_hid_ref asus_ref = { + .listeners = LIST_HEAD_INIT(asus_ref.listeners), + .asus = NULL, + /* + * Protects .asus, .asus.kbd_led_{wk,notify}, and .listener refs. Other + * asus variables are read-only after .asus is set. + * + * The led cdev device is not protected because it calls backlight_get + * during initialization, which would result in a nested lock attempt. + * + * The led cdev is safe to access without a lock because if + * kbd_led_avail is true it is initialized before .asus is set and never + * changed until .asus is dropped. If kbd_led_avail is false, the led + * cdev is registered by the workqueue, which is single-threaded and + * cancelled before asus-wmi would access the led cdev to unregister it. + * + * A spinlock is used, because the protected variables can be accessed + * from an IRQ context from asus-hid. + */ + .lock = __SPIN_LOCK_UNLOCKED(asus_ref.lock), +}; + +/* + * Allows registering hid-asus listeners that want to be notified of + * keyboard backlight changes. + */ +int asus_hid_register_listener(struct asus_hid_listener *bdev) +{ + struct asus_wmi *asus; + + guard(spinlock_irqsave)(&asus_ref.lock); + list_add_tail(&bdev->list, &asus_ref.listeners); + asus = asus_ref.asus; + if (asus) + queue_work(asus->led_workqueue, &asus->kbd_led_work); + return 0; +} +EXPORT_SYMBOL_GPL(asus_hid_register_listener); + +/* + * Allows unregistering hid-asus listeners that were added with + * asus_hid_register_listener(). + */ +void asus_hid_unregister_listener(struct asus_hid_listener *bdev) +{ + guard(spinlock_irqsave)(&asus_ref.lock); + list_del(&bdev->list); +} +EXPORT_SYMBOL_GPL(asus_hid_unregister_listener); + +static void do_kbd_led_set(struct led_classdev *led_cdev, int value); + +static void kbd_led_update_all(struct work_struct *work) +{ + struct asus_wmi *asus; + bool registered, notify; + int ret, value; + + asus = container_of(work, struct asus_wmi, kbd_led_work); + + scoped_guard(spinlock_irqsave, &asus_ref.lock) { + registered = asus->kbd_led_registered; + value = asus->kbd_led_wk; + notify = asus->kbd_led_notify; + } + + if (!registered) { + /* + * This workqueue runs under asus-wmi, which means probe has + * completed and asus-wmi will keep running until it finishes. + * Therefore, we can safely register the LED without holding + * a spinlock. + */ + ret = devm_led_classdev_register(&asus->platform_device->dev, + &asus->kbd_led); + if (!ret) { + scoped_guard(spinlock_irqsave, &asus_ref.lock) + asus->kbd_led_registered = true; + } else { + pr_warn("Failed to register keyboard backlight LED: %d\n", ret); + return; + } + } + + if (value >= 0) + do_kbd_led_set(&asus->kbd_led, value); + if (notify) { + scoped_guard(spinlock_irqsave, &asus_ref.lock) + asus->kbd_led_notify = false; + led_classdev_notify_brightness_hw_changed(&asus->kbd_led, value); + } +} + +/* + * This function is called from hid-asus to inform asus-wmi of brightness + * changes initiated by the keyboard backlight keys. + */ +int asus_hid_event(enum asus_hid_event event) +{ + struct asus_wmi *asus; + int brightness; + + guard(spinlock_irqsave)(&asus_ref.lock); + asus = asus_ref.asus; + if (!asus || !asus->kbd_led_registered) + return -EBUSY; + + brightness = asus->kbd_led_wk; + + switch (event) { + case ASUS_EV_BRTUP: + brightness += 1; + break; + case ASUS_EV_BRTDOWN: + brightness -= 1; + break; + case ASUS_EV_BRTTOGGLE: + if (brightness >= ASUS_EV_MAX_BRIGHTNESS) + brightness = 0; + else + brightness += 1; + break; + } + + asus->kbd_led_wk = clamp_val(brightness, 0, ASUS_EV_MAX_BRIGHTNESS); + asus->kbd_led_notify = true; + queue_work(asus->led_workqueue, &asus->kbd_led_work); + return 0; +} +EXPORT_SYMBOL_GPL(asus_hid_event); + +/* + * These functions actually update the LED's, and are called from a + * workqueue. By doing this as separate work rather than when the LED + * subsystem asks, we avoid messing with the Asus ACPI stuff during a + * potentially bad time, such as a timer interrupt. + */ +static void tpd_led_update(struct work_struct *work) +{ + int ctrl_param; + struct asus_wmi *asus; + + asus = container_of(work, struct asus_wmi, tpd_led_work); + + ctrl_param = asus->tpd_led_wk; + asus_wmi_set_devstate(ASUS_WMI_DEVID_TOUCHPAD_LED, ctrl_param, NULL); +} + +static void tpd_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct asus_wmi *asus; + + asus = container_of(led_cdev, struct asus_wmi, tpd_led); + + asus->tpd_led_wk = !!value; + queue_work(asus->led_workqueue, &asus->tpd_led_work); +} + +static int read_tpd_led_state(struct asus_wmi *asus) +{ + return asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_TOUCHPAD_LED); +} + +static enum led_brightness tpd_led_get(struct led_classdev *led_cdev) +{ + struct asus_wmi *asus; + + asus = container_of(led_cdev, struct asus_wmi, tpd_led); + + return read_tpd_led_state(asus); +} + +static void kbd_led_update(struct asus_wmi *asus) +{ + int ctrl_param = 0; + + scoped_guard(spinlock_irqsave, &asus_ref.lock) + ctrl_param = 0x80 | (asus->kbd_led_wk & 0x7F); + asus_wmi_set_devstate(ASUS_WMI_DEVID_KBD_BACKLIGHT, ctrl_param, NULL); +} + +static int kbd_led_read(struct asus_wmi *asus, int *level, int *env) +{ + int retval; + + /* + * bits 0-2: level + * bit 7: light on/off + * bit 8-10: environment (0: dark, 1: normal, 2: light) + * bit 17: status unknown + */ + retval = asus_wmi_get_devstate_bits(asus, ASUS_WMI_DEVID_KBD_BACKLIGHT, + 0xFFFF); + + /* Unknown status is considered as off */ + if (retval == 0x8000) + retval = 0; + + if (retval < 0) + return retval; + + if (level) + *level = retval & 0x7F; + if (env) + *env = (retval >> 8) & 0x7F; + return 0; +} + +static void do_kbd_led_set(struct led_classdev *led_cdev, int value) +{ + struct asus_hid_listener *listener; + struct asus_wmi *asus; + + asus = container_of(led_cdev, struct asus_wmi, kbd_led); + + scoped_guard(spinlock_irqsave, &asus_ref.lock) + asus->kbd_led_wk = clamp_val(value, 0, ASUS_EV_MAX_BRIGHTNESS); + + if (asus->kbd_led_avail) + kbd_led_update(asus); + + scoped_guard(spinlock_irqsave, &asus_ref.lock) { + list_for_each_entry(listener, &asus_ref.listeners, list) + listener->brightness_set(listener, asus->kbd_led_wk); + } +} + +static int kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value) +{ + /* Prevent disabling keyboard backlight on module unregister */ + if (led_cdev->flags & LED_UNREGISTERING) + return 0; + + do_kbd_led_set(led_cdev, value); + return 0; +} + +static void kbd_led_set_by_kbd(struct asus_wmi *asus, enum led_brightness value) +{ + scoped_guard(spinlock_irqsave, &asus_ref.lock) { + asus->kbd_led_wk = value; + asus->kbd_led_notify = true; + } + queue_work(asus->led_workqueue, &asus->kbd_led_work); +} + +static enum led_brightness kbd_led_get(struct led_classdev *led_cdev) +{ + struct asus_wmi *asus; + int retval, value; + + asus = container_of(led_cdev, struct asus_wmi, kbd_led); + + scoped_guard(spinlock_irqsave, &asus_ref.lock) { + if (!asus->kbd_led_avail) + return asus->kbd_led_wk; + } + + retval = kbd_led_read(asus, &value, NULL); + if (retval < 0) + return retval; + + scoped_guard(spinlock_irqsave, &asus_ref.lock) + asus->kbd_led_wk = value; + + return value; +} + +static int wlan_led_unknown_state(struct asus_wmi *asus) +{ + u32 result; + + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_WIRELESS_LED, &result); + + return result & ASUS_WMI_DSTS_UNKNOWN_BIT; +} + +static void wlan_led_update(struct work_struct *work) +{ + int ctrl_param; + struct asus_wmi *asus; + + asus = container_of(work, struct asus_wmi, wlan_led_work); + + ctrl_param = asus->wlan_led_wk; + asus_wmi_set_devstate(ASUS_WMI_DEVID_WIRELESS_LED, ctrl_param, NULL); +} + +static void wlan_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct asus_wmi *asus; + + asus = container_of(led_cdev, struct asus_wmi, wlan_led); + + asus->wlan_led_wk = !!value; + queue_work(asus->led_workqueue, &asus->wlan_led_work); +} + +static enum led_brightness wlan_led_get(struct led_classdev *led_cdev) +{ + struct asus_wmi *asus; + u32 result; + + asus = container_of(led_cdev, struct asus_wmi, wlan_led); + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_WIRELESS_LED, &result); + + return result & ASUS_WMI_DSTS_BRIGHTNESS_MASK; +} + +static void lightbar_led_update(struct work_struct *work) +{ + struct asus_wmi *asus; + int ctrl_param; + + asus = container_of(work, struct asus_wmi, lightbar_led_work); + + ctrl_param = asus->lightbar_led_wk; + asus_wmi_set_devstate(ASUS_WMI_DEVID_LIGHTBAR, ctrl_param, NULL); +} + +static void lightbar_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct asus_wmi *asus; + + asus = container_of(led_cdev, struct asus_wmi, lightbar_led); + + asus->lightbar_led_wk = !!value; + queue_work(asus->led_workqueue, &asus->lightbar_led_work); +} + +static enum led_brightness lightbar_led_get(struct led_classdev *led_cdev) +{ + struct asus_wmi *asus; + u32 result; + + asus = container_of(led_cdev, struct asus_wmi, lightbar_led); + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_LIGHTBAR, &result); + + return result & ASUS_WMI_DSTS_LIGHTBAR_MASK; +} + +static int micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + int state = brightness != LED_OFF; + int err; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_MICMUTE_LED, state, NULL); + return err < 0 ? err : 0; +} + +static enum led_brightness camera_led_get(struct led_classdev *led_cdev) +{ + struct asus_wmi *asus; + u32 result; + + asus = container_of(led_cdev, struct asus_wmi, camera_led); + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_CAMERA_LED, &result); + + return result & ASUS_WMI_DSTS_BRIGHTNESS_MASK; +} + +static int camera_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + int state = brightness != LED_OFF; + int err; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_CAMERA_LED, state, NULL); + return err < 0 ? err : 0; +} + +static void asus_wmi_led_exit(struct asus_wmi *asus) +{ + scoped_guard(spinlock_irqsave, &asus_ref.lock) + asus_ref.asus = NULL; + + led_classdev_unregister(&asus->tpd_led); + led_classdev_unregister(&asus->wlan_led); + led_classdev_unregister(&asus->lightbar_led); + led_classdev_unregister(&asus->micmute_led); + led_classdev_unregister(&asus->camera_led); + + if (asus->led_workqueue) + destroy_workqueue(asus->led_workqueue); +} + +static int asus_wmi_led_init(struct asus_wmi *asus) +{ + int rv = 0, num_rgb_groups = 0, led_val; + + if (asus->kbd_rgb_dev) + kbd_rgb_mode_groups[num_rgb_groups++] = &kbd_rgb_mode_group; + if (asus->kbd_rgb_state_available) + kbd_rgb_mode_groups[num_rgb_groups++] = &kbd_rgb_state_group; + + asus->led_workqueue = create_singlethread_workqueue("led_workqueue"); + if (!asus->led_workqueue) + return -ENOMEM; + + if (read_tpd_led_state(asus) >= 0) { + INIT_WORK(&asus->tpd_led_work, tpd_led_update); + + asus->tpd_led.name = "asus::touchpad"; + asus->tpd_led.brightness_set = tpd_led_set; + asus->tpd_led.brightness_get = tpd_led_get; + asus->tpd_led.max_brightness = 1; + + rv = led_classdev_register(&asus->platform_device->dev, + &asus->tpd_led); + if (rv) + goto error; + } + + asus->kbd_led.name = "asus::kbd_backlight"; + asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED; + asus->kbd_led.brightness_set_blocking = kbd_led_set; + asus->kbd_led.brightness_get = kbd_led_get; + asus->kbd_led.max_brightness = ASUS_EV_MAX_BRIGHTNESS; + asus->kbd_led_avail = !kbd_led_read(asus, &led_val, NULL); + INIT_WORK(&asus->kbd_led_work, kbd_led_update_all); + + if (asus->kbd_led_avail) { + asus->kbd_led_wk = led_val; + if (num_rgb_groups != 0) + asus->kbd_led.groups = kbd_rgb_mode_groups; + } else { + asus->kbd_led_wk = -1; + } + + scoped_guard(spinlock_irqsave, &asus_ref.lock) { + asus_ref.asus = asus; + if (asus->kbd_led_avail || !list_empty(&asus_ref.listeners)) + queue_work(asus->led_workqueue, &asus->kbd_led_work); + } + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_WIRELESS_LED) + && (asus->driver->quirks->wapf > 0)) { + INIT_WORK(&asus->wlan_led_work, wlan_led_update); + + asus->wlan_led.name = "asus::wlan"; + asus->wlan_led.brightness_set = wlan_led_set; + if (!wlan_led_unknown_state(asus)) + asus->wlan_led.brightness_get = wlan_led_get; + asus->wlan_led.flags = LED_CORE_SUSPENDRESUME; + asus->wlan_led.max_brightness = 1; + asus->wlan_led.default_trigger = "asus-wlan"; + + rv = led_classdev_register(&asus->platform_device->dev, + &asus->wlan_led); + if (rv) + goto error; + } + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_LIGHTBAR)) { + INIT_WORK(&asus->lightbar_led_work, lightbar_led_update); + + asus->lightbar_led.name = "asus::lightbar"; + asus->lightbar_led.brightness_set = lightbar_led_set; + asus->lightbar_led.brightness_get = lightbar_led_get; + asus->lightbar_led.max_brightness = 1; + + rv = led_classdev_register(&asus->platform_device->dev, + &asus->lightbar_led); + } + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_MICMUTE_LED)) { + asus->micmute_led.name = "platform::micmute"; + asus->micmute_led.max_brightness = 1; + asus->micmute_led.brightness_set_blocking = micmute_led_set; + asus->micmute_led.default_trigger = "audio-micmute"; + + rv = led_classdev_register(&asus->platform_device->dev, + &asus->micmute_led); + if (rv) + goto error; + } + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_CAMERA_LED)) { + asus->camera_led.name = "asus::camera"; + asus->camera_led.max_brightness = 1; + asus->camera_led.brightness_get = camera_led_get; + asus->camera_led.brightness_set_blocking = camera_led_set; + + rv = led_classdev_register(&asus->platform_device->dev, + &asus->camera_led); + if (rv) + goto error; + } + + if (asus->oobe_state_available) { + /* + * Disable OOBE state, so that e.g. the keyboard backlight + * works. + */ + rv = asus_wmi_set_devstate(ASUS_WMI_DEVID_OOBE, 1, NULL); + if (rv) + goto error; + } + +error: + if (rv) + asus_wmi_led_exit(asus); + + return rv; +} + +/* RF *************************************************************************/ + +/* + * PCI hotplug (for wlan rfkill) + */ +static bool asus_wlan_rfkill_blocked(struct asus_wmi *asus) +{ + int result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_WLAN); + + if (result < 0) + return false; + return !result; +} + +static void asus_rfkill_hotplug(struct asus_wmi *asus) +{ + struct pci_dev *dev; + struct pci_bus *bus; + bool blocked; + bool absent; + u32 l; + + mutex_lock(&asus->wmi_lock); + blocked = asus_wlan_rfkill_blocked(asus); + mutex_unlock(&asus->wmi_lock); + + mutex_lock(&asus->hotplug_lock); + pci_lock_rescan_remove(); + + if (asus->wlan.rfkill) + rfkill_set_sw_state(asus->wlan.rfkill, blocked); + + if (asus->hotplug_slot.ops) { + bus = pci_find_bus(0, 1); + if (!bus) { + pr_warn("Unable to find PCI bus 1?\n"); + goto out_unlock; + } + + if (pci_bus_read_config_dword(bus, 0, PCI_VENDOR_ID, &l)) { + pr_err("Unable to read PCI config space?\n"); + goto out_unlock; + } + absent = (l == 0xffffffff); + + if (blocked != absent) { + pr_warn("BIOS says wireless lan is %s, but the pci device is %s\n", + blocked ? "blocked" : "unblocked", + absent ? "absent" : "present"); + pr_warn("skipped wireless hotplug as probably inappropriate for this model\n"); + goto out_unlock; + } + + if (!blocked) { + dev = pci_get_slot(bus, 0); + if (dev) { + /* Device already present */ + pci_dev_put(dev); + goto out_unlock; + } + dev = pci_scan_single_device(bus, 0); + if (dev) { + pci_bus_assign_resources(bus); + pci_bus_add_device(dev); + } + } else { + dev = pci_get_slot(bus, 0); + if (dev) { + pci_stop_and_remove_bus_device(dev); + pci_dev_put(dev); + } + } + } + +out_unlock: + pci_unlock_rescan_remove(); + mutex_unlock(&asus->hotplug_lock); +} + +static void asus_rfkill_notify(acpi_handle handle, u32 event, void *data) +{ + struct asus_wmi *asus = data; + + if (event != ACPI_NOTIFY_BUS_CHECK) + return; + + /* + * We can't call directly asus_rfkill_hotplug because most + * of the time WMBC is still being executed and not reetrant. + * There is currently no way to tell ACPICA that we want this + * method to be serialized, we schedule a asus_rfkill_hotplug + * call later, in a safer context. + */ + queue_work(asus->hotplug_workqueue, &asus->hotplug_work); +} + +static int asus_register_rfkill_notifier(struct asus_wmi *asus, char *node) +{ + acpi_status status; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + if (ACPI_FAILURE(status)) + return -ENODEV; + + status = acpi_install_notify_handler(handle, ACPI_SYSTEM_NOTIFY, + asus_rfkill_notify, asus); + if (ACPI_FAILURE(status)) + pr_warn("Failed to register notify on %s\n", node); + + return 0; +} + +static void asus_unregister_rfkill_notifier(struct asus_wmi *asus, char *node) +{ + acpi_status status = AE_OK; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + if (ACPI_FAILURE(status)) + return; + + status = acpi_remove_notify_handler(handle, ACPI_SYSTEM_NOTIFY, + asus_rfkill_notify); + if (ACPI_FAILURE(status)) + pr_err("Error removing rfkill notify handler %s\n", node); +} + +static int asus_get_adapter_status(struct hotplug_slot *hotplug_slot, + u8 *value) +{ + struct asus_wmi *asus = container_of(hotplug_slot, + struct asus_wmi, hotplug_slot); + int result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_WLAN); + + if (result < 0) + return result; + + *value = !!result; + return 0; +} + +static const struct hotplug_slot_ops asus_hotplug_slot_ops = { + .get_adapter_status = asus_get_adapter_status, + .get_power_status = asus_get_adapter_status, +}; + +static void asus_hotplug_work(struct work_struct *work) +{ + struct asus_wmi *asus; + + asus = container_of(work, struct asus_wmi, hotplug_work); + asus_rfkill_hotplug(asus); +} + +static int asus_setup_pci_hotplug(struct asus_wmi *asus) +{ + int ret = -ENOMEM; + struct pci_bus *bus = pci_find_bus(0, 1); + + if (!bus) { + pr_err("Unable to find wifi PCI bus\n"); + return -ENODEV; + } + + asus->hotplug_workqueue = + create_singlethread_workqueue("hotplug_workqueue"); + if (!asus->hotplug_workqueue) + goto error_workqueue; + + INIT_WORK(&asus->hotplug_work, asus_hotplug_work); + + asus->hotplug_slot.ops = &asus_hotplug_slot_ops; + + ret = pci_hp_register(&asus->hotplug_slot, bus, 0, "asus-wifi"); + if (ret) { + pr_err("Unable to register hotplug slot - %d\n", ret); + goto error_register; + } + + return 0; + +error_register: + asus->hotplug_slot.ops = NULL; + destroy_workqueue(asus->hotplug_workqueue); +error_workqueue: + return ret; +} + +/* + * Rfkill devices + */ +static int asus_rfkill_set(void *data, bool blocked) +{ + struct asus_rfkill *priv = data; + u32 ctrl_param = !blocked; + u32 dev_id = priv->dev_id; + + /* + * If the user bit is set, BIOS can't set and record the wlan status, + * it will report the value read from id ASUS_WMI_DEVID_WLAN_LED + * while we query the wlan status through WMI(ASUS_WMI_DEVID_WLAN). + * So, we have to record wlan status in id ASUS_WMI_DEVID_WLAN_LED + * while setting the wlan status through WMI. + * This is also the behavior that windows app will do. + */ + if ((dev_id == ASUS_WMI_DEVID_WLAN) && + priv->asus->driver->wlan_ctrl_by_user) + dev_id = ASUS_WMI_DEVID_WLAN_LED; + + return asus_wmi_set_devstate(dev_id, ctrl_param, NULL); +} + +static void asus_rfkill_query(struct rfkill *rfkill, void *data) +{ + struct asus_rfkill *priv = data; + int result; + + result = asus_wmi_get_devstate_simple(priv->asus, priv->dev_id); + + if (result < 0) + return; + + rfkill_set_sw_state(priv->rfkill, !result); +} + +static int asus_rfkill_wlan_set(void *data, bool blocked) +{ + struct asus_rfkill *priv = data; + struct asus_wmi *asus = priv->asus; + int ret; + + /* + * This handler is enabled only if hotplug is enabled. + * In this case, the asus_wmi_set_devstate() will + * trigger a wmi notification and we need to wait + * this call to finish before being able to call + * any wmi method + */ + mutex_lock(&asus->wmi_lock); + ret = asus_rfkill_set(data, blocked); + mutex_unlock(&asus->wmi_lock); + return ret; +} + +static const struct rfkill_ops asus_rfkill_wlan_ops = { + .set_block = asus_rfkill_wlan_set, + .query = asus_rfkill_query, +}; + +static const struct rfkill_ops asus_rfkill_ops = { + .set_block = asus_rfkill_set, + .query = asus_rfkill_query, +}; + +static int asus_new_rfkill(struct asus_wmi *asus, + struct asus_rfkill *arfkill, + const char *name, enum rfkill_type type, int dev_id) +{ + int result = asus_wmi_get_devstate_simple(asus, dev_id); + struct rfkill **rfkill = &arfkill->rfkill; + + if (result < 0) + return result; + + arfkill->dev_id = dev_id; + arfkill->asus = asus; + + if (dev_id == ASUS_WMI_DEVID_WLAN && + asus->driver->quirks->hotplug_wireless) + *rfkill = rfkill_alloc(name, &asus->platform_device->dev, type, + &asus_rfkill_wlan_ops, arfkill); + else + *rfkill = rfkill_alloc(name, &asus->platform_device->dev, type, + &asus_rfkill_ops, arfkill); + + if (!*rfkill) + return -EINVAL; + + if ((dev_id == ASUS_WMI_DEVID_WLAN) && + (asus->driver->quirks->wapf > 0)) + rfkill_set_led_trigger_name(*rfkill, "asus-wlan"); + + rfkill_init_sw_state(*rfkill, !result); + result = rfkill_register(*rfkill); + if (result) { + rfkill_destroy(*rfkill); + *rfkill = NULL; + return result; + } + return 0; +} + +static void asus_wmi_rfkill_exit(struct asus_wmi *asus) +{ + if (asus->driver->wlan_ctrl_by_user && ashs_present()) + return; + + asus_unregister_rfkill_notifier(asus, "\\_SB.PCI0.P0P5"); + asus_unregister_rfkill_notifier(asus, "\\_SB.PCI0.P0P6"); + asus_unregister_rfkill_notifier(asus, "\\_SB.PCI0.P0P7"); + if (asus->wlan.rfkill) { + rfkill_unregister(asus->wlan.rfkill); + rfkill_destroy(asus->wlan.rfkill); + asus->wlan.rfkill = NULL; + } + /* + * Refresh pci hotplug in case the rfkill state was changed after + * asus_unregister_rfkill_notifier() + */ + asus_rfkill_hotplug(asus); + if (asus->hotplug_slot.ops) + pci_hp_deregister(&asus->hotplug_slot); + if (asus->hotplug_workqueue) + destroy_workqueue(asus->hotplug_workqueue); + + if (asus->bluetooth.rfkill) { + rfkill_unregister(asus->bluetooth.rfkill); + rfkill_destroy(asus->bluetooth.rfkill); + asus->bluetooth.rfkill = NULL; + } + if (asus->wimax.rfkill) { + rfkill_unregister(asus->wimax.rfkill); + rfkill_destroy(asus->wimax.rfkill); + asus->wimax.rfkill = NULL; + } + if (asus->wwan3g.rfkill) { + rfkill_unregister(asus->wwan3g.rfkill); + rfkill_destroy(asus->wwan3g.rfkill); + asus->wwan3g.rfkill = NULL; + } + if (asus->gps.rfkill) { + rfkill_unregister(asus->gps.rfkill); + rfkill_destroy(asus->gps.rfkill); + asus->gps.rfkill = NULL; + } + if (asus->uwb.rfkill) { + rfkill_unregister(asus->uwb.rfkill); + rfkill_destroy(asus->uwb.rfkill); + asus->uwb.rfkill = NULL; + } +} + +static int asus_wmi_rfkill_init(struct asus_wmi *asus) +{ + int result = 0; + + mutex_init(&asus->hotplug_lock); + mutex_init(&asus->wmi_lock); + + result = asus_new_rfkill(asus, &asus->wlan, "asus-wlan", + RFKILL_TYPE_WLAN, ASUS_WMI_DEVID_WLAN); + + if (result && result != -ENODEV) + goto exit; + + result = asus_new_rfkill(asus, &asus->bluetooth, + "asus-bluetooth", RFKILL_TYPE_BLUETOOTH, + ASUS_WMI_DEVID_BLUETOOTH); + + if (result && result != -ENODEV) + goto exit; + + result = asus_new_rfkill(asus, &asus->wimax, "asus-wimax", + RFKILL_TYPE_WIMAX, ASUS_WMI_DEVID_WIMAX); + + if (result && result != -ENODEV) + goto exit; + + result = asus_new_rfkill(asus, &asus->wwan3g, "asus-wwan3g", + RFKILL_TYPE_WWAN, ASUS_WMI_DEVID_WWAN3G); + + if (result && result != -ENODEV) + goto exit; + + result = asus_new_rfkill(asus, &asus->gps, "asus-gps", + RFKILL_TYPE_GPS, ASUS_WMI_DEVID_GPS); + + if (result && result != -ENODEV) + goto exit; + + result = asus_new_rfkill(asus, &asus->uwb, "asus-uwb", + RFKILL_TYPE_UWB, ASUS_WMI_DEVID_UWB); + + if (result && result != -ENODEV) + goto exit; + + if (!asus->driver->quirks->hotplug_wireless) + goto exit; + + result = asus_setup_pci_hotplug(asus); + /* + * If we get -EBUSY then something else is handling the PCI hotplug - + * don't fail in this case + */ + if (result == -EBUSY) + result = 0; + + asus_register_rfkill_notifier(asus, "\\_SB.PCI0.P0P5"); + asus_register_rfkill_notifier(asus, "\\_SB.PCI0.P0P6"); + asus_register_rfkill_notifier(asus, "\\_SB.PCI0.P0P7"); + /* + * Refresh pci hotplug in case the rfkill state was changed during + * setup. + */ + asus_rfkill_hotplug(asus); + +exit: + if (result && result != -ENODEV) + asus_wmi_rfkill_exit(asus); + + if (result == -ENODEV) + result = 0; + + return result; +} + +/* Panel Overdrive ************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t panel_od_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_PANEL_OD); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +static ssize_t panel_od_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 overdrive; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + result = kstrtou32(buf, 10, &overdrive); + if (result) + return result; + + if (overdrive > 1) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_PANEL_OD, overdrive, &result); + + if (err) { + pr_warn("Failed to set panel overdrive: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set panel overdrive (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "panel_od"); + + return count; +} +static DEVICE_ATTR_RW(panel_od); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Bootup sound ***************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t boot_sound_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int result; + + result = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_BOOT_SOUND); + if (result < 0) + return result; + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", result); +} + +static ssize_t boot_sound_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 snd; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + result = kstrtou32(buf, 10, &snd); + if (result) + return result; + + if (snd > 1) + return -EINVAL; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_BOOT_SOUND, snd, &result); + if (err) { + pr_warn("Failed to set boot sound: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set panel boot sound (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "boot_sound"); + + return count; +} +static DEVICE_ATTR_RW(boot_sound); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Mini-LED mode **************************************************************/ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t mini_led_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u32 value; + int err; + + err = asus_wmi_get_devstate(asus, asus->mini_led_dev_id, &value); + if (err < 0) + return err; + value = value & ASUS_MINI_LED_MODE_MASK; + + /* + * Remap the mode values to match previous generation mini-led. The last gen + * WMI 0 == off, while on this version WMI 2 ==off (flipped). + */ + if (asus->mini_led_dev_id == ASUS_WMI_DEVID_MINI_LED_MODE2) { + switch (value) { + case ASUS_MINI_LED_2024_WEAK: + value = ASUS_MINI_LED_ON; + break; + case ASUS_MINI_LED_2024_STRONG: + value = ASUS_MINI_LED_STRONG_MODE; + break; + case ASUS_MINI_LED_2024_OFF: + value = ASUS_MINI_LED_OFF; + break; + } + } + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t mini_led_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int result, err; + u32 mode; + + struct asus_wmi *asus = dev_get_drvdata(dev); + + result = kstrtou32(buf, 10, &mode); + if (result) + return result; + + if (asus->mini_led_dev_id == ASUS_WMI_DEVID_MINI_LED_MODE && + mode > ASUS_MINI_LED_ON) + return -EINVAL; + if (asus->mini_led_dev_id == ASUS_WMI_DEVID_MINI_LED_MODE2 && + mode > ASUS_MINI_LED_STRONG_MODE) + return -EINVAL; + + /* + * Remap the mode values so expected behaviour is the same as the last + * generation of mini-LED with 0 == off, 1 == on. + */ + if (asus->mini_led_dev_id == ASUS_WMI_DEVID_MINI_LED_MODE2) { + switch (mode) { + case ASUS_MINI_LED_OFF: + mode = ASUS_MINI_LED_2024_OFF; + break; + case ASUS_MINI_LED_ON: + mode = ASUS_MINI_LED_2024_WEAK; + break; + case ASUS_MINI_LED_STRONG_MODE: + mode = ASUS_MINI_LED_2024_STRONG; + break; + } + } + + err = asus_wmi_set_devstate(asus->mini_led_dev_id, mode, &result); + if (err) { + pr_warn("Failed to set mini-LED: %d\n", err); + return err; + } + + if (result > 1) { + pr_warn("Failed to set mini-LED mode (result): 0x%x\n", result); + return -EIO; + } + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, "mini_led_mode"); + + return count; +} +static DEVICE_ATTR_RW(mini_led_mode); + +static ssize_t available_mini_led_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + switch (asus->mini_led_dev_id) { + case ASUS_WMI_DEVID_MINI_LED_MODE: + return sysfs_emit(buf, "0 1\n"); + case ASUS_WMI_DEVID_MINI_LED_MODE2: + return sysfs_emit(buf, "0 1 2\n"); + } + + asus_wmi_show_deprecated(); + + return sysfs_emit(buf, "0\n"); +} + +static DEVICE_ATTR_RO(available_mini_led_mode); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Quirks *********************************************************************/ + +static void asus_wmi_set_xusb2pr(struct asus_wmi *asus) +{ + struct pci_dev *xhci_pdev; + u32 orig_ports_available; + u32 ports_available = asus->driver->quirks->xusb2pr; + + xhci_pdev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_LYNXPOINT_LP_XHCI, + NULL); + + if (!xhci_pdev) + return; + + pci_read_config_dword(xhci_pdev, USB_INTEL_XUSB2PR, + &orig_ports_available); + + pci_write_config_dword(xhci_pdev, USB_INTEL_XUSB2PR, + cpu_to_le32(ports_available)); + + pci_dev_put(xhci_pdev); + + pr_info("set USB_INTEL_XUSB2PR old: 0x%04x, new: 0x%04x\n", + orig_ports_available, ports_available); +} + +/* + * Some devices dont support or have borcken get_als method + * but still support set method. + */ +static void asus_wmi_set_als(void) +{ + asus_wmi_set_devstate(ASUS_WMI_DEVID_ALS_ENABLE, 1, NULL); +} + +/* Hwmon device ***************************************************************/ + +static int asus_agfn_fan_speed_read(struct asus_wmi *asus, int fan, + int *speed) +{ + struct agfn_fan_args args = { + .agfn.len = sizeof(args), + .agfn.mfun = ASUS_FAN_MFUN, + .agfn.sfun = ASUS_FAN_SFUN_READ, + .fan = fan, + .speed = 0, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + int status; + + if (fan != 1) + return -EINVAL; + + status = asus_wmi_evaluate_method_agfn(input); + + if (status || args.agfn.err) + return -ENXIO; + + if (speed) + *speed = args.speed; + + return 0; +} + +static int asus_agfn_fan_speed_write(struct asus_wmi *asus, int fan, + int *speed) +{ + struct agfn_fan_args args = { + .agfn.len = sizeof(args), + .agfn.mfun = ASUS_FAN_MFUN, + .agfn.sfun = ASUS_FAN_SFUN_WRITE, + .fan = fan, + .speed = speed ? *speed : 0, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + int status; + + /* 1: for setting 1st fan's speed 0: setting auto mode */ + if (fan != 1 && fan != 0) + return -EINVAL; + + status = asus_wmi_evaluate_method_agfn(input); + + if (status || args.agfn.err) + return -ENXIO; + + if (speed && fan == 1) + asus->agfn_pwm = *speed; + + return 0; +} + +/* + * Check if we can read the speed of one fan. If true we assume we can also + * control it. + */ +static bool asus_wmi_has_agfn_fan(struct asus_wmi *asus) +{ + int status; + int speed; + u32 value; + + status = asus_agfn_fan_speed_read(asus, 1, &speed); + if (status != 0) + return false; + + status = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_FAN_CTRL, &value); + if (status != 0) + return false; + + /* + * We need to find a better way, probably using sfun, + * bits or spec ... + * Currently we disable it if: + * - ASUS_WMI_UNSUPPORTED_METHOD is returned + * - reverved bits are non-zero + * - sfun and presence bit are not set + */ + return !(value == ASUS_WMI_UNSUPPORTED_METHOD || value & 0xFFF80000 + || (!asus->sfun && !(value & ASUS_WMI_DSTS_PRESENCE_BIT))); +} + +static int asus_fan_set_auto(struct asus_wmi *asus) +{ + int status; + u32 retval; + + switch (asus->fan_type) { + case FAN_TYPE_SPEC83: + status = asus_wmi_set_devstate(ASUS_WMI_DEVID_CPU_FAN_CTRL, + 0, &retval); + if (status) + return status; + + if (retval != 1) + return -EIO; + break; + + case FAN_TYPE_AGFN: + status = asus_agfn_fan_speed_write(asus, 0, NULL); + if (status) + return -ENXIO; + break; + + default: + return -ENXIO; + } + + /* + * Modern models like the G713 also have GPU fan control (this is not AGFN) + */ + if (asus->gpu_fan_type == FAN_TYPE_SPEC83) { + status = asus_wmi_set_devstate(ASUS_WMI_DEVID_GPU_FAN_CTRL, + 0, &retval); + if (status) + return status; + + if (retval != 1) + return -EIO; + } + + return 0; +} + +static ssize_t pwm1_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int err; + int value; + + /* If we already set a value then just return it */ + if (asus->agfn_pwm >= 0) + return sysfs_emit(buf, "%d\n", asus->agfn_pwm); + + /* + * If we haven't set already set a value through the AGFN interface, + * we read a current value through the (now-deprecated) FAN_CTRL device. + */ + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_FAN_CTRL, &value); + if (err < 0) + return err; + + value &= 0xFF; + + if (value == 1) /* Low Speed */ + value = 85; + else if (value == 2) + value = 170; + else if (value == 3) + value = 255; + else if (value) { + pr_err("Unknown fan speed %#x\n", value); + value = -1; + } + + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t pwm1_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { + struct asus_wmi *asus = dev_get_drvdata(dev); + int value; + int state; + int ret; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + value = clamp(value, 0, 255); + + state = asus_agfn_fan_speed_write(asus, 1, &value); + if (state) + pr_warn("Setting fan speed failed: %d\n", state); + else + asus->fan_pwm_mode = ASUS_FAN_CTRL_MANUAL; + + return count; +} + +static ssize_t fan1_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int value; + int ret; + + switch (asus->fan_type) { + case FAN_TYPE_SPEC83: + ret = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_CPU_FAN_CTRL, + &value); + if (ret < 0) + return ret; + + value &= 0xffff; + break; + + case FAN_TYPE_AGFN: + /* no speed readable on manual mode */ + if (asus->fan_pwm_mode == ASUS_FAN_CTRL_MANUAL) + return -ENXIO; + + ret = asus_agfn_fan_speed_read(asus, 1, &value); + if (ret) { + pr_warn("reading fan speed failed: %d\n", ret); + return -ENXIO; + } + break; + + default: + return -ENXIO; + } + + return sysfs_emit(buf, "%d\n", value < 0 ? -1 : value * 100); +} + +static ssize_t pwm1_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + /* + * Just read back the cached pwm mode. + * + * For the CPU_FAN device, the spec indicates that we should be + * able to read the device status and consult bit 19 to see if we + * are in Full On or Automatic mode. However, this does not work + * in practice on X532FL at least (the bit is always 0) and there's + * also nothing in the DSDT to indicate that this behaviour exists. + */ + return sysfs_emit(buf, "%d\n", asus->fan_pwm_mode); +} + +static ssize_t pwm1_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int status = 0; + int state; + int value; + int ret; + u32 retval; + + ret = kstrtouint(buf, 10, &state); + if (ret) + return ret; + + if (asus->fan_type == FAN_TYPE_SPEC83) { + switch (state) { /* standard documented hwmon values */ + case ASUS_FAN_CTRL_FULLSPEED: + value = 1; + break; + case ASUS_FAN_CTRL_AUTO: + value = 0; + break; + default: + return -EINVAL; + } + + ret = asus_wmi_set_devstate(ASUS_WMI_DEVID_CPU_FAN_CTRL, + value, &retval); + if (ret) + return ret; + + if (retval != 1) + return -EIO; + } else if (asus->fan_type == FAN_TYPE_AGFN) { + switch (state) { + case ASUS_FAN_CTRL_MANUAL: + break; + + case ASUS_FAN_CTRL_AUTO: + status = asus_fan_set_auto(asus); + if (status) + return status; + break; + + default: + return -EINVAL; + } + } + + asus->fan_pwm_mode = state; + + /* Must set to disabled if mode is toggled */ + if (asus->cpu_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_CPU].enabled = false; + if (asus->gpu_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_GPU].enabled = false; + if (asus->mid_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_MID].enabled = false; + + return count; +} + +static ssize_t asus_hwmon_temp1(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u32 value; + int err; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_THERMAL_CTRL, &value); + if (err < 0) + return err; + + return sysfs_emit(buf, "%ld\n", + deci_kelvin_to_millicelsius(value & 0xFFFF)); +} + +/* GPU fan on modern ROG laptops */ +static ssize_t fan2_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int value; + int ret; + + ret = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_GPU_FAN_CTRL, &value); + if (ret < 0) + return ret; + + value &= 0xffff; + + return sysfs_emit(buf, "%d\n", value * 100); +} + +/* Middle/Center fan on modern ROG laptops */ +static ssize_t fan3_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int value; + int ret; + + ret = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_MID_FAN_CTRL, &value); + if (ret < 0) + return ret; + + value &= 0xffff; + + return sysfs_emit(buf, "%d\n", value * 100); +} + +static ssize_t pwm2_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus->gpu_fan_pwm_mode); +} + +static ssize_t pwm2_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int state; + int value; + int ret; + u32 retval; + + ret = kstrtouint(buf, 10, &state); + if (ret) + return ret; + + switch (state) { /* standard documented hwmon values */ + case ASUS_FAN_CTRL_FULLSPEED: + value = 1; + break; + case ASUS_FAN_CTRL_AUTO: + value = 0; + break; + default: + return -EINVAL; + } + + ret = asus_wmi_set_devstate(ASUS_WMI_DEVID_GPU_FAN_CTRL, + value, &retval); + if (ret) + return ret; + + if (retval != 1) + return -EIO; + + asus->gpu_fan_pwm_mode = state; + return count; +} + +static ssize_t pwm3_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus->mid_fan_pwm_mode); +} + +static ssize_t pwm3_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + int state; + int value; + int ret; + u32 retval; + + ret = kstrtouint(buf, 10, &state); + if (ret) + return ret; + + switch (state) { /* standard documented hwmon values */ + case ASUS_FAN_CTRL_FULLSPEED: + value = 1; + break; + case ASUS_FAN_CTRL_AUTO: + value = 0; + break; + default: + return -EINVAL; + } + + ret = asus_wmi_set_devstate(ASUS_WMI_DEVID_MID_FAN_CTRL, + value, &retval); + if (ret) + return ret; + + if (retval != 1) + return -EIO; + + asus->mid_fan_pwm_mode = state; + return count; +} + +/* Fan1 */ +static DEVICE_ATTR_RW(pwm1); +static DEVICE_ATTR_RW(pwm1_enable); +static DEVICE_ATTR_RO(fan1_input); +static DEVICE_STRING_ATTR_RO(fan1_label, 0444, ASUS_FAN_DESC); + +/* Fan2 - GPU fan */ +static DEVICE_ATTR_RW(pwm2_enable); +static DEVICE_ATTR_RO(fan2_input); +static DEVICE_STRING_ATTR_RO(fan2_label, 0444, ASUS_GPU_FAN_DESC); +/* Fan3 - Middle/center fan */ +static DEVICE_ATTR_RW(pwm3_enable); +static DEVICE_ATTR_RO(fan3_input); +static DEVICE_STRING_ATTR_RO(fan3_label, 0444, ASUS_MID_FAN_DESC); + +/* Temperature */ +static DEVICE_ATTR(temp1_input, S_IRUGO, asus_hwmon_temp1, NULL); + +static struct attribute *hwmon_attributes[] = { + &dev_attr_pwm1.attr, + &dev_attr_pwm1_enable.attr, + &dev_attr_pwm2_enable.attr, + &dev_attr_pwm3_enable.attr, + &dev_attr_fan1_input.attr, + &dev_attr_fan1_label.attr.attr, + &dev_attr_fan2_input.attr, + &dev_attr_fan2_label.attr.attr, + &dev_attr_fan3_input.attr, + &dev_attr_fan3_label.attr.attr, + + &dev_attr_temp1_input.attr, + NULL +}; + +static umode_t asus_hwmon_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct asus_wmi *asus = dev_get_drvdata(dev->parent); + u32 value = ASUS_WMI_UNSUPPORTED_METHOD; + + if (attr == &dev_attr_pwm1.attr) { + if (asus->fan_type != FAN_TYPE_AGFN) + return 0; + } else if (attr == &dev_attr_fan1_input.attr + || attr == &dev_attr_fan1_label.attr.attr + || attr == &dev_attr_pwm1_enable.attr) { + if (asus->fan_type == FAN_TYPE_NONE) + return 0; + } else if (attr == &dev_attr_fan2_input.attr + || attr == &dev_attr_fan2_label.attr.attr + || attr == &dev_attr_pwm2_enable.attr) { + if (asus->gpu_fan_type == FAN_TYPE_NONE) + return 0; + } else if (attr == &dev_attr_fan3_input.attr + || attr == &dev_attr_fan3_label.attr.attr + || attr == &dev_attr_pwm3_enable.attr) { + if (asus->mid_fan_type == FAN_TYPE_NONE) + return 0; + } else if (attr == &dev_attr_temp1_input.attr) { + int err = asus_wmi_get_devstate(asus, + ASUS_WMI_DEVID_THERMAL_CTRL, + &value); + + if (err < 0) + return 0; /* can't return negative here */ + + /* + * If the temperature value in deci-Kelvin is near the absolute + * zero temperature, something is clearly wrong + */ + if (value == 0 || value == 1) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group hwmon_attribute_group = { + .is_visible = asus_hwmon_sysfs_is_visible, + .attrs = hwmon_attributes +}; +__ATTRIBUTE_GROUPS(hwmon_attribute); + +static int asus_wmi_hwmon_init(struct asus_wmi *asus) +{ + struct device *dev = &asus->platform_device->dev; + struct device *hwmon; + + hwmon = devm_hwmon_device_register_with_groups(dev, "asus", asus, + hwmon_attribute_groups); + + if (IS_ERR(hwmon)) { + pr_err("Could not register asus hwmon device\n"); + return PTR_ERR(hwmon); + } + return 0; +} + +static int asus_wmi_fan_init(struct asus_wmi *asus) +{ + asus->gpu_fan_type = FAN_TYPE_NONE; + asus->mid_fan_type = FAN_TYPE_NONE; + asus->fan_type = FAN_TYPE_NONE; + asus->agfn_pwm = -1; + + if (asus->driver->quirks->wmi_ignore_fan) + asus->fan_type = FAN_TYPE_NONE; + else if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_CPU_FAN_CTRL)) + asus->fan_type = FAN_TYPE_SPEC83; + else if (asus_wmi_has_agfn_fan(asus)) + asus->fan_type = FAN_TYPE_AGFN; + + /* Modern models like G713 also have GPU fan control */ + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_GPU_FAN_CTRL)) + asus->gpu_fan_type = FAN_TYPE_SPEC83; + + /* Some models also have a center/middle fan */ + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_MID_FAN_CTRL)) + asus->mid_fan_type = FAN_TYPE_SPEC83; + + if (asus->fan_type == FAN_TYPE_NONE) + return -ENODEV; + + asus_fan_set_auto(asus); + asus->fan_pwm_mode = ASUS_FAN_CTRL_AUTO; + return 0; +} + +/* Fan mode *******************************************************************/ + +static int fan_boost_mode_check_present(struct asus_wmi *asus) +{ + u32 result; + int err; + + asus->fan_boost_mode_available = false; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_FAN_BOOST_MODE, + &result); + if (err) { + if (err == -ENODEV) + return 0; + else + return err; + } + + if ((result & ASUS_WMI_DSTS_PRESENCE_BIT) && + (result & ASUS_FAN_BOOST_MODES_MASK)) { + asus->fan_boost_mode_available = true; + asus->fan_boost_mode_mask = result & ASUS_FAN_BOOST_MODES_MASK; + } + + return 0; +} + +static int fan_boost_mode_write(struct asus_wmi *asus) +{ + u32 retval; + u8 value; + int err; + + value = asus->fan_boost_mode; + + pr_info("Set fan boost mode: %u\n", value); + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_FAN_BOOST_MODE, value, + &retval); + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, + "fan_boost_mode"); + + if (err) { + pr_warn("Failed to set fan boost mode: %d\n", err); + return err; + } + + if (retval != 1) { + pr_warn("Failed to set fan boost mode (retval): 0x%x\n", + retval); + return -EIO; + } + + return 0; +} + +static int fan_boost_mode_switch_next(struct asus_wmi *asus) +{ + u8 mask = asus->fan_boost_mode_mask; + + if (asus->fan_boost_mode == ASUS_FAN_BOOST_MODE_NORMAL) { + if (mask & ASUS_FAN_BOOST_MODE_OVERBOOST_MASK) + asus->fan_boost_mode = ASUS_FAN_BOOST_MODE_OVERBOOST; + else if (mask & ASUS_FAN_BOOST_MODE_SILENT_MASK) + asus->fan_boost_mode = ASUS_FAN_BOOST_MODE_SILENT; + } else if (asus->fan_boost_mode == ASUS_FAN_BOOST_MODE_OVERBOOST) { + if (mask & ASUS_FAN_BOOST_MODE_SILENT_MASK) + asus->fan_boost_mode = ASUS_FAN_BOOST_MODE_SILENT; + else + asus->fan_boost_mode = ASUS_FAN_BOOST_MODE_NORMAL; + } else { + asus->fan_boost_mode = ASUS_FAN_BOOST_MODE_NORMAL; + } + + return fan_boost_mode_write(asus); +} + +static ssize_t fan_boost_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", asus->fan_boost_mode); +} + +static ssize_t fan_boost_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u8 mask = asus->fan_boost_mode_mask; + u8 new_mode; + int result; + + result = kstrtou8(buf, 10, &new_mode); + if (result < 0) { + pr_warn("Trying to store invalid value\n"); + return result; + } + + if (new_mode == ASUS_FAN_BOOST_MODE_OVERBOOST) { + if (!(mask & ASUS_FAN_BOOST_MODE_OVERBOOST_MASK)) + return -EINVAL; + } else if (new_mode == ASUS_FAN_BOOST_MODE_SILENT) { + if (!(mask & ASUS_FAN_BOOST_MODE_SILENT_MASK)) + return -EINVAL; + } else if (new_mode != ASUS_FAN_BOOST_MODE_NORMAL) { + return -EINVAL; + } + + asus->fan_boost_mode = new_mode; + fan_boost_mode_write(asus); + + return count; +} + +// Fan boost mode: 0 - normal, 1 - overboost, 2 - silent +static DEVICE_ATTR_RW(fan_boost_mode); + +/* Custom fan curves **********************************************************/ + +static void fan_curve_copy_from_buf(struct fan_curve_data *data, u8 *buf) +{ + int i; + + for (i = 0; i < FAN_CURVE_POINTS; i++) { + data->temps[i] = buf[i]; + } + + for (i = 0; i < FAN_CURVE_POINTS; i++) { + data->percents[i] = + 255 * buf[i + FAN_CURVE_POINTS] / 100; + } +} + +static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev) +{ + struct fan_curve_data *curves; + u8 buf[FAN_CURVE_BUF_LEN]; + int err, fan_idx; + u8 mode = 0; + + if (asus->throttle_thermal_policy_dev) + mode = asus->throttle_thermal_policy_mode; + /* DEVID_PU_FAN_CURVE is switched for OVERBOOST vs SILENT */ + if (mode == 2) + mode = 1; + else if (mode == 1) + mode = 2; + + err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, mode, buf, + FAN_CURVE_BUF_LEN); + if (err) { + pr_warn("%s (0x%08x) failed: %d\n", __func__, fan_dev, err); + return err; + } + + fan_idx = FAN_CURVE_DEV_CPU; + if (fan_dev == ASUS_WMI_DEVID_GPU_FAN_CURVE) + fan_idx = FAN_CURVE_DEV_GPU; + + if (fan_dev == ASUS_WMI_DEVID_MID_FAN_CURVE) + fan_idx = FAN_CURVE_DEV_MID; + + curves = &asus->custom_fan_curves[fan_idx]; + curves->device_id = fan_dev; + + fan_curve_copy_from_buf(curves, buf); + return 0; +} + +/* Check if capability exists, and populate defaults */ +static int fan_curve_check_present(struct asus_wmi *asus, bool *available, + u32 fan_dev) +{ + int err; + + *available = false; + + if (asus->fan_type == FAN_TYPE_NONE) + return 0; + + err = fan_curve_get_factory_default(asus, fan_dev); + if (err) { + return 0; + } + + *available = true; + return 0; +} + +/* Determine which fan the attribute is for if SENSOR_ATTR */ +static struct fan_curve_data *fan_curve_attr_select(struct asus_wmi *asus, + struct device_attribute *attr) +{ + int index = to_sensor_dev_attr(attr)->index; + + return &asus->custom_fan_curves[index]; +} + +/* Determine which fan the attribute is for if SENSOR_ATTR_2 */ +static struct fan_curve_data *fan_curve_attr_2_select(struct asus_wmi *asus, + struct device_attribute *attr) +{ + int nr = to_sensor_dev_attr_2(attr)->nr; + + return &asus->custom_fan_curves[nr & ~FAN_CURVE_PWM_MASK]; +} + +static ssize_t fan_curve_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute_2 *dev_attr = to_sensor_dev_attr_2(attr); + struct asus_wmi *asus = dev_get_drvdata(dev); + struct fan_curve_data *data; + int value, pwm, index; + + data = fan_curve_attr_2_select(asus, attr); + pwm = dev_attr->nr & FAN_CURVE_PWM_MASK; + index = dev_attr->index; + + if (pwm) + value = data->percents[index]; + else + value = data->temps[index]; + + return sysfs_emit(buf, "%d\n", value); +} + +/* + * "fan_dev" is the related WMI method such as ASUS_WMI_DEVID_CPU_FAN_CURVE. + */ +static int fan_curve_write(struct asus_wmi *asus, + struct fan_curve_data *data) +{ + u32 arg1 = 0, arg2 = 0, arg3 = 0, arg4 = 0; + u8 *percents = data->percents; + u8 *temps = data->temps; + int ret, i, shift = 0; + + if (!data->enabled) + return 0; + + for (i = 0; i < FAN_CURVE_POINTS / 2; i++) { + arg1 += (temps[i]) << shift; + arg2 += (temps[i + 4]) << shift; + /* Scale to percentage for device */ + arg3 += (100 * percents[i] / 255) << shift; + arg4 += (100 * percents[i + 4] / 255) << shift; + shift += 8; + } + + return asus_wmi_evaluate_method5(ASUS_WMI_METHODID_DEVS, + data->device_id, + arg1, arg2, arg3, arg4, &ret); +} + +static ssize_t fan_curve_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct sensor_device_attribute_2 *dev_attr = to_sensor_dev_attr_2(attr); + struct asus_wmi *asus = dev_get_drvdata(dev); + struct fan_curve_data *data; + int err, pwm, index; + u8 value; + + data = fan_curve_attr_2_select(asus, attr); + pwm = dev_attr->nr & FAN_CURVE_PWM_MASK; + index = dev_attr->index; + + err = kstrtou8(buf, 10, &value); + if (err < 0) + return err; + + if (pwm) + data->percents[index] = value; + else + data->temps[index] = value; + + /* + * Mark as disabled so the user has to explicitly enable to apply a + * changed fan curve. This prevents potential lockups from writing out + * many changes as one-write-per-change. + */ + data->enabled = false; + + return count; +} + +static ssize_t fan_curve_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + struct fan_curve_data *data; + int out = 2; + + data = fan_curve_attr_select(asus, attr); + + if (data->enabled) + out = 1; + + return sysfs_emit(buf, "%d\n", out); +} + +static ssize_t fan_curve_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + struct fan_curve_data *data; + int value, err; + + data = fan_curve_attr_select(asus, attr); + + err = kstrtoint(buf, 10, &value); + if (err < 0) + return err; + + switch (value) { + case 1: + data->enabled = true; + break; + case 2: + data->enabled = false; + break; + /* + * Auto + reset the fan curve data to defaults. Make it an explicit + * option so that users don't accidentally overwrite a set fan curve. + */ + case 3: + err = fan_curve_get_factory_default(asus, data->device_id); + if (err) + return err; + data->enabled = false; + break; + default: + return -EINVAL; + } + + if (data->enabled) { + err = fan_curve_write(asus, data); + if (err) + return err; + } else { + /* + * For machines with throttle this is the only way to reset fans + * to default mode of operation (does not erase curve data). + */ + if (asus->throttle_thermal_policy_dev) { + err = throttle_thermal_policy_write(asus); + if (err) + return err; + /* Similar is true for laptops with this fan */ + } else if (asus->fan_type == FAN_TYPE_SPEC83) { + err = asus_fan_set_auto(asus); + if (err) + return err; + } else { + /* Safeguard against fautly ACPI tables */ + err = fan_curve_get_factory_default(asus, data->device_id); + if (err) + return err; + err = fan_curve_write(asus, data); + if (err) + return err; + } + } + return count; +} + +/* CPU */ +static SENSOR_DEVICE_ATTR_RW(pwm1_enable, fan_curve_enable, FAN_CURVE_DEV_CPU); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp, fan_curve, + FAN_CURVE_DEV_CPU, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp, fan_curve, + FAN_CURVE_DEV_CPU, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp, fan_curve, + FAN_CURVE_DEV_CPU, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp, fan_curve, + FAN_CURVE_DEV_CPU, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp, fan_curve, + FAN_CURVE_DEV_CPU, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp, fan_curve, + FAN_CURVE_DEV_CPU, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_temp, fan_curve, + FAN_CURVE_DEV_CPU, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_temp, fan_curve, + FAN_CURVE_DEV_CPU, 7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_pwm, fan_curve, + FAN_CURVE_DEV_CPU | FAN_CURVE_PWM_MASK, 7); + +/* GPU */ +static SENSOR_DEVICE_ATTR_RW(pwm2_enable, fan_curve_enable, FAN_CURVE_DEV_GPU); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp, fan_curve, + FAN_CURVE_DEV_GPU, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp, fan_curve, + FAN_CURVE_DEV_GPU, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp, fan_curve, + FAN_CURVE_DEV_GPU, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp, fan_curve, + FAN_CURVE_DEV_GPU, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp, fan_curve, + FAN_CURVE_DEV_GPU, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp, fan_curve, + FAN_CURVE_DEV_GPU, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point7_temp, fan_curve, + FAN_CURVE_DEV_GPU, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point8_temp, fan_curve, + FAN_CURVE_DEV_GPU, 7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point7_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point8_pwm, fan_curve, + FAN_CURVE_DEV_GPU | FAN_CURVE_PWM_MASK, 7); + +/* MID */ +static SENSOR_DEVICE_ATTR_RW(pwm3_enable, fan_curve_enable, FAN_CURVE_DEV_MID); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point1_temp, fan_curve, + FAN_CURVE_DEV_MID, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point2_temp, fan_curve, + FAN_CURVE_DEV_MID, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point3_temp, fan_curve, + FAN_CURVE_DEV_MID, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point4_temp, fan_curve, + FAN_CURVE_DEV_MID, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point5_temp, fan_curve, + FAN_CURVE_DEV_MID, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point6_temp, fan_curve, + FAN_CURVE_DEV_MID, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_temp, fan_curve, + FAN_CURVE_DEV_MID, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_temp, fan_curve, + FAN_CURVE_DEV_MID, 7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point1_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 0); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point2_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 1); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point3_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 2); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point4_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 3); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point5_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 4); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point6_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 5); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 6); +static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_pwm, fan_curve, + FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 7); + +static struct attribute *asus_fan_curve_attr[] = { + /* CPU */ + &sensor_dev_attr_pwm1_enable.dev_attr.attr, + &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_point7_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point8_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_pwm1_auto_point7_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point8_pwm.dev_attr.attr, + /* GPU */ + &sensor_dev_attr_pwm2_enable.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_point7_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point8_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, + &sensor_dev_attr_pwm2_auto_point7_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point8_pwm.dev_attr.attr, + /* MID */ + &sensor_dev_attr_pwm3_enable.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point6_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point7_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point8_temp.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point6_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point7_pwm.dev_attr.attr, + &sensor_dev_attr_pwm3_auto_point8_pwm.dev_attr.attr, + NULL +}; + +static umode_t asus_fan_curve_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct asus_wmi *asus = dev_get_drvdata(dev->parent); + + /* + * Check the char instead of casting attr as there are two attr types + * involved here (attr1 and attr2) + */ + if (asus->cpu_fan_curve_available && attr->name[3] == '1') + return 0644; + + if (asus->gpu_fan_curve_available && attr->name[3] == '2') + return 0644; + + if (asus->mid_fan_curve_available && attr->name[3] == '3') + return 0644; + + return 0; +} + +static const struct attribute_group asus_fan_curve_attr_group = { + .is_visible = asus_fan_curve_is_visible, + .attrs = asus_fan_curve_attr, +}; +__ATTRIBUTE_GROUPS(asus_fan_curve_attr); + +/* + * Must be initialised after throttle_thermal_policy_dev is set as + * we check the status of throttle_thermal_policy_dev during init. + */ +static int asus_wmi_custom_fan_curve_init(struct asus_wmi *asus) +{ + struct device *dev = &asus->platform_device->dev; + struct device *hwmon; + int err; + + err = fan_curve_check_present(asus, &asus->cpu_fan_curve_available, + ASUS_WMI_DEVID_CPU_FAN_CURVE); + if (err) { + pr_debug("%s, checked 0x%08x, failed: %d\n", + __func__, ASUS_WMI_DEVID_CPU_FAN_CURVE, err); + return err; + } + + err = fan_curve_check_present(asus, &asus->gpu_fan_curve_available, + ASUS_WMI_DEVID_GPU_FAN_CURVE); + if (err) { + pr_debug("%s, checked 0x%08x, failed: %d\n", + __func__, ASUS_WMI_DEVID_GPU_FAN_CURVE, err); + return err; + } + + err = fan_curve_check_present(asus, &asus->mid_fan_curve_available, + ASUS_WMI_DEVID_MID_FAN_CURVE); + if (err) { + pr_debug("%s, checked 0x%08x, failed: %d\n", + __func__, ASUS_WMI_DEVID_MID_FAN_CURVE, err); + return err; + } + + if (!asus->cpu_fan_curve_available + && !asus->gpu_fan_curve_available + && !asus->mid_fan_curve_available) + return 0; + + hwmon = devm_hwmon_device_register_with_groups( + dev, "asus_custom_fan_curve", asus, asus_fan_curve_attr_groups); + + if (IS_ERR(hwmon)) { + dev_err(dev, + "Could not register asus_custom_fan_curve device\n"); + return PTR_ERR(hwmon); + } + + return 0; +} + +/* Throttle thermal policy ****************************************************/ +static int throttle_thermal_policy_write(struct asus_wmi *asus) +{ + u8 value; + int err; + + if (asus->throttle_thermal_policy_dev == ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY_VIVO) { + switch (asus->throttle_thermal_policy_mode) { + case ASUS_THROTTLE_THERMAL_POLICY_DEFAULT: + value = ASUS_THROTTLE_THERMAL_POLICY_DEFAULT_VIVO; + break; + case ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST: + value = ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST_VIVO; + break; + case ASUS_THROTTLE_THERMAL_POLICY_SILENT: + value = ASUS_THROTTLE_THERMAL_POLICY_SILENT_VIVO; + break; + default: + return -EINVAL; + } + } else { + value = asus->throttle_thermal_policy_mode; + } + + /* Some machines do not return an error code as a result, so we ignore it */ + err = asus_wmi_set_devstate(asus->throttle_thermal_policy_dev, value, NULL); + + sysfs_notify(&asus->platform_device->dev.kobj, NULL, + "throttle_thermal_policy"); + + if (err) { + pr_warn("Failed to set throttle thermal policy: %d\n", err); + return err; + } + + /* Must set to disabled if mode is toggled */ + if (asus->cpu_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_CPU].enabled = false; + if (asus->gpu_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_GPU].enabled = false; + if (asus->mid_fan_curve_available) + asus->custom_fan_curves[FAN_CURVE_DEV_MID].enabled = false; + + return 0; +} + +static int throttle_thermal_policy_set_default(struct asus_wmi *asus) +{ + if (!asus->throttle_thermal_policy_dev) + return 0; + + asus->throttle_thermal_policy_mode = ASUS_THROTTLE_THERMAL_POLICY_DEFAULT; + return throttle_thermal_policy_write(asus); +} + +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) +static ssize_t throttle_thermal_policy_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u8 mode = asus->throttle_thermal_policy_mode; + + return sysfs_emit(buf, "%d\n", mode); +} + +static ssize_t throttle_thermal_policy_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct asus_wmi *asus = dev_get_drvdata(dev); + u8 new_mode; + int result; + int err; + + result = kstrtou8(buf, 10, &new_mode); + if (result < 0) + return result; + + if (new_mode > PLATFORM_PROFILE_MAX) + return -EINVAL; + + asus->throttle_thermal_policy_mode = new_mode; + err = throttle_thermal_policy_write(asus); + if (err) + return err; + + /* + * Ensure that platform_profile updates userspace with the change to ensure + * that platform_profile and throttle_thermal_policy_mode are in sync. + */ + platform_profile_notify(asus->ppdev); + + return count; +} + +/* + * Throttle thermal policy: 0 - default, 1 - overboost, 2 - silent + */ +static DEVICE_ATTR_RW(throttle_thermal_policy); +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + +/* Platform profile ***********************************************************/ +static int asus_wmi_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct asus_wmi *asus; + int tp; + + asus = dev_get_drvdata(dev); + tp = asus->throttle_thermal_policy_mode; + + switch (tp) { + case ASUS_THROTTLE_THERMAL_POLICY_DEFAULT: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case ASUS_THROTTLE_THERMAL_POLICY_SILENT: + *profile = PLATFORM_PROFILE_QUIET; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int asus_wmi_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct asus_wmi *asus; + int tp; + + asus = dev_get_drvdata(dev); + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + tp = ASUS_THROTTLE_THERMAL_POLICY_OVERBOOST; + break; + case PLATFORM_PROFILE_BALANCED: + tp = ASUS_THROTTLE_THERMAL_POLICY_DEFAULT; + break; + case PLATFORM_PROFILE_QUIET: + tp = ASUS_THROTTLE_THERMAL_POLICY_SILENT; + break; + default: + return -EOPNOTSUPP; + } + + asus->throttle_thermal_policy_mode = tp; + return throttle_thermal_policy_write(asus); +} + +static int asus_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_QUIET, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + + return 0; +} + +static const struct platform_profile_ops asus_wmi_platform_profile_ops = { + .probe = asus_wmi_platform_profile_probe, + .profile_get = asus_wmi_platform_profile_get, + .profile_set = asus_wmi_platform_profile_set, +}; + +static int platform_profile_setup(struct asus_wmi *asus) +{ + struct device *dev = &asus->platform_device->dev; + int err; + + /* + * Not an error if a component platform_profile relies on is unavailable + * so early return, skipping the setup of platform_profile. + */ + if (!asus->throttle_thermal_policy_dev) + return 0; + + /* + * We need to set the default thermal profile during probe or otherwise + * the system will often remain in silent mode, causing low performance. + */ + err = throttle_thermal_policy_set_default(asus); + if (err < 0) { + pr_warn("Failed to set default thermal profile\n"); + return err; + } + + dev_info(dev, "Using throttle_thermal_policy for platform_profile support\n"); + + asus->ppdev = devm_platform_profile_register(dev, "asus-wmi", asus, + &asus_wmi_platform_profile_ops); + if (IS_ERR(asus->ppdev)) { + dev_err(dev, "Failed to register a platform_profile class device\n"); + return PTR_ERR(asus->ppdev); + } + + asus->platform_profile_support = true; + return 0; +} + +/* Backlight ******************************************************************/ + +static int read_backlight_power(struct asus_wmi *asus) +{ + int ret; + + if (asus->driver->quirks->store_backlight_power) + ret = !asus->driver->panel_power; + else + ret = asus_wmi_get_devstate_simple(asus, + ASUS_WMI_DEVID_BACKLIGHT); + + if (ret < 0) + return ret; + + return ret ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; +} + +static int read_brightness_max(struct asus_wmi *asus) +{ + u32 retval; + int err; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_BRIGHTNESS, &retval); + if (err < 0) + return err; + + retval = retval & ASUS_WMI_DSTS_MAX_BRIGTH_MASK; + retval >>= 8; + + if (!retval) + return -ENODEV; + + return retval; +} + +static int read_brightness(struct backlight_device *bd) +{ + struct asus_wmi *asus = bl_get_data(bd); + u32 retval; + int err; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_BRIGHTNESS, &retval); + if (err < 0) + return err; + + return retval & ASUS_WMI_DSTS_BRIGHTNESS_MASK; +} + +static u32 get_scalar_command(struct backlight_device *bd) +{ + struct asus_wmi *asus = bl_get_data(bd); + u32 ctrl_param = 0; + + if ((asus->driver->brightness < bd->props.brightness) || + bd->props.brightness == bd->props.max_brightness) + ctrl_param = 0x00008001; + else if ((asus->driver->brightness > bd->props.brightness) || + bd->props.brightness == 0) + ctrl_param = 0x00008000; + + asus->driver->brightness = bd->props.brightness; + + return ctrl_param; +} + +static int update_bl_status(struct backlight_device *bd) +{ + struct asus_wmi *asus = bl_get_data(bd); + u32 ctrl_param; + int power, err = 0; + + power = read_backlight_power(asus); + if (power != -ENODEV && bd->props.power != power) { + ctrl_param = !!(bd->props.power == BACKLIGHT_POWER_ON); + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_BACKLIGHT, + ctrl_param, NULL); + if (asus->driver->quirks->store_backlight_power) + asus->driver->panel_power = bd->props.power; + + /* When using scalar brightness, updating the brightness + * will mess with the backlight power */ + if (asus->driver->quirks->scalar_panel_brightness) + return err; + } + + if (asus->driver->quirks->scalar_panel_brightness) + ctrl_param = get_scalar_command(bd); + else + ctrl_param = bd->props.brightness; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_BRIGHTNESS, + ctrl_param, NULL); + + return err; +} + +static const struct backlight_ops asus_wmi_bl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +static int asus_wmi_backlight_notify(struct asus_wmi *asus, int code) +{ + struct backlight_device *bd = asus->backlight_device; + int old = bd->props.brightness; + int new = old; + + if (code >= NOTIFY_BRNUP_MIN && code <= NOTIFY_BRNUP_MAX) + new = code - NOTIFY_BRNUP_MIN + 1; + else if (code >= NOTIFY_BRNDOWN_MIN && code <= NOTIFY_BRNDOWN_MAX) + new = code - NOTIFY_BRNDOWN_MIN; + + bd->props.brightness = new; + backlight_update_status(bd); + backlight_force_update(bd, BACKLIGHT_UPDATE_HOTKEY); + + return old; +} + +static int asus_wmi_backlight_init(struct asus_wmi *asus) +{ + struct backlight_device *bd; + struct backlight_properties props; + int max; + int power; + + max = read_brightness_max(asus); + if (max < 0) + return max; + + power = read_backlight_power(asus); + if (power == -ENODEV) + power = BACKLIGHT_POWER_ON; + else if (power < 0) + return power; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = max; + bd = backlight_device_register(asus->driver->name, + &asus->platform_device->dev, asus, + &asus_wmi_bl_ops, &props); + if (IS_ERR(bd)) { + pr_err("Could not register backlight device\n"); + return PTR_ERR(bd); + } + + asus->backlight_device = bd; + + if (asus->driver->quirks->store_backlight_power) + asus->driver->panel_power = power; + + bd->props.brightness = read_brightness(bd); + bd->props.power = power; + backlight_update_status(bd); + + asus->driver->brightness = bd->props.brightness; + + return 0; +} + +static void asus_wmi_backlight_exit(struct asus_wmi *asus) +{ + backlight_device_unregister(asus->backlight_device); + + asus->backlight_device = NULL; +} + +static int is_display_toggle(int code) +{ + /* display toggle keys */ + if ((code >= 0x61 && code <= 0x67) || + (code >= 0x8c && code <= 0x93) || + (code >= 0xa0 && code <= 0xa7) || + (code >= 0xd0 && code <= 0xd5)) + return 1; + + return 0; +} + +/* Screenpad backlight *******************************************************/ + +static int read_screenpad_backlight_power(struct asus_wmi *asus) +{ + int ret; + + ret = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_SCREENPAD_POWER); + if (ret < 0) + return ret; + /* 1 == powered */ + return ret ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; +} + +static int read_screenpad_brightness(struct backlight_device *bd) +{ + struct asus_wmi *asus = bl_get_data(bd); + u32 retval; + int err; + + err = read_screenpad_backlight_power(asus); + if (err < 0) + return err; + /* The device brightness can only be read if powered, so return stored */ + if (err == BACKLIGHT_POWER_OFF) + return bd->props.brightness; + + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_SCREENPAD_LIGHT, &retval); + if (err < 0) + return err; + + return retval & ASUS_WMI_DSTS_BRIGHTNESS_MASK; +} + +static int update_screenpad_bl_status(struct backlight_device *bd) +{ + u32 ctrl_param = bd->props.brightness; + int err = 0; + + if (bd->props.power) { + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_SCREENPAD_POWER, 1, NULL); + if (err < 0) + return err; + + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_SCREENPAD_LIGHT, ctrl_param, NULL); + if (err < 0) + return err; + } + + if (!bd->props.power) { + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_SCREENPAD_POWER, 0, NULL); + if (err < 0) + return err; + } + + return err; +} + +static const struct backlight_ops asus_screenpad_bl_ops = { + .get_brightness = read_screenpad_brightness, + .update_status = update_screenpad_bl_status, + .options = BL_CORE_SUSPENDRESUME, +}; + +static int asus_screenpad_init(struct asus_wmi *asus) +{ + struct backlight_device *bd; + struct backlight_properties props; + int err, power; + int brightness = 0; + + power = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_SCREENPAD_POWER); + if (power < 0) + return power; + + if (power) { + err = asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_SCREENPAD_LIGHT, &brightness); + if (err < 0) + return err; + } + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_RAW; /* ensure this bd is last to be picked */ + props.max_brightness = ASUS_SCREENPAD_BRIGHT_MAX; + bd = backlight_device_register("asus_screenpad", + &asus->platform_device->dev, asus, + &asus_screenpad_bl_ops, &props); + if (IS_ERR(bd)) { + pr_err("Could not register backlight device\n"); + return PTR_ERR(bd); + } + + asus->screenpad_backlight_device = bd; + asus->driver->screenpad_brightness = brightness; + bd->props.brightness = brightness; + bd->props.power = power; + backlight_update_status(bd); + + return 0; +} + +static void asus_screenpad_exit(struct asus_wmi *asus) +{ + backlight_device_unregister(asus->screenpad_backlight_device); + + asus->screenpad_backlight_device = NULL; +} + +/* Fn-lock ********************************************************************/ + +static bool asus_wmi_has_fnlock_key(struct asus_wmi *asus) +{ + u32 result; + + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_FNLOCK, &result); + + return (result & ASUS_WMI_DSTS_PRESENCE_BIT) && + !(result & ASUS_WMI_FNLOCK_BIOS_DISABLED); +} + +static void asus_wmi_fnlock_update(struct asus_wmi *asus) +{ + int mode = asus->fnlock_locked; + + asus_wmi_set_devstate(ASUS_WMI_DEVID_FNLOCK, mode, NULL); +} + +/* WMI events *****************************************************************/ + +static int asus_wmi_get_event_code(union acpi_object *obj) +{ + int code; + + if (obj && obj->type == ACPI_TYPE_INTEGER) + code = (int)(obj->integer.value & WMI_EVENT_MASK); + else + code = -EIO; + + return code; +} + +static void asus_wmi_handle_event_code(int code, struct asus_wmi *asus) +{ + enum led_brightness led_value; + unsigned int key_value = 1; + bool autorelease = 1; + + if (asus->driver->key_filter) { + asus->driver->key_filter(asus->driver, &code, &key_value, + &autorelease); + if (code == ASUS_WMI_KEY_IGNORE) + return; + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor && + code >= NOTIFY_BRNUP_MIN && code <= NOTIFY_BRNDOWN_MAX) { + asus_wmi_backlight_notify(asus, code); + return; + } + + scoped_guard(spinlock_irqsave, &asus_ref.lock) + led_value = asus->kbd_led_wk; + + if (code == NOTIFY_KBD_BRTUP) { + kbd_led_set_by_kbd(asus, led_value + 1); + return; + } + if (code == NOTIFY_KBD_BRTDWN) { + kbd_led_set_by_kbd(asus, led_value - 1); + return; + } + if (code == NOTIFY_KBD_BRTTOGGLE) { + if (led_value >= ASUS_EV_MAX_BRIGHTNESS) + kbd_led_set_by_kbd(asus, 0); + else + kbd_led_set_by_kbd(asus, led_value + 1); + return; + } + + if (code == NOTIFY_FNLOCK_TOGGLE) { + asus->fnlock_locked = !asus->fnlock_locked; + asus_wmi_fnlock_update(asus); + return; + } + + if (code == asus->tablet_switch_event_code) { + asus_wmi_tablet_mode_get_state(asus); + return; + } + + if (code == NOTIFY_KEYSTONE) { + if (asus->keystone_available) + asus_wmi_keystone_notify(asus); + return; + } + + if (code == NOTIFY_KBD_FBM || code == NOTIFY_KBD_TTP) { + if (asus->fan_boost_mode_available) + fan_boost_mode_switch_next(asus); + if (asus->throttle_thermal_policy_dev) + platform_profile_cycle(); + return; + + } + + if (is_display_toggle(code) && asus->driver->quirks->no_display_toggle) + return; + + if (!sparse_keymap_report_event(asus->inputdev, code, + key_value, autorelease)) + pr_info("Unknown key code 0x%x\n", code); +} + +static void asus_wmi_notify(union acpi_object *obj, void *context) +{ + struct asus_wmi *asus = context; + int code = asus_wmi_get_event_code(obj); + + if (code < 0) { + pr_warn("Failed to get notify code: %d\n", code); + return; + } + + asus_wmi_handle_event_code(code, asus); +} + +/* Sysfs **********************************************************************/ + +static ssize_t store_sys_wmi(struct asus_wmi *asus, int devid, + const char *buf, size_t count) +{ + u32 retval; + int err, value; + + value = asus_wmi_get_devstate_simple(asus, devid); + if (value < 0) + return value; + + err = kstrtoint(buf, 0, &value); + if (err) + return err; + + err = asus_wmi_set_devstate(devid, value, &retval); + if (err < 0) + return err; + + return count; +} + +static ssize_t show_sys_wmi(struct asus_wmi *asus, int devid, char *buf) +{ + int value = asus_wmi_get_devstate_simple(asus, devid); + + if (value < 0) + return value; + + return sysfs_emit(buf, "%d\n", value); +} + +#define ASUS_WMI_CREATE_DEVICE_ATTR(_name, _mode, _cm) \ + static ssize_t show_##_name(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct asus_wmi *asus = dev_get_drvdata(dev); \ + \ + return show_sys_wmi(asus, _cm, buf); \ + } \ + static ssize_t store_##_name(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct asus_wmi *asus = dev_get_drvdata(dev); \ + \ + return store_sys_wmi(asus, _cm, buf, count); \ + } \ + static struct device_attribute dev_attr_##_name = { \ + .attr = { \ + .name = __stringify(_name), \ + .mode = _mode }, \ + .show = show_##_name, \ + .store = store_##_name, \ + } + +ASUS_WMI_CREATE_DEVICE_ATTR(touchpad, 0644, ASUS_WMI_DEVID_TOUCHPAD); +ASUS_WMI_CREATE_DEVICE_ATTR(camera, 0644, ASUS_WMI_DEVID_CAMERA); +ASUS_WMI_CREATE_DEVICE_ATTR(cardr, 0644, ASUS_WMI_DEVID_CARDREADER); +ASUS_WMI_CREATE_DEVICE_ATTR(lid_resume, 0644, ASUS_WMI_DEVID_LID_RESUME); +ASUS_WMI_CREATE_DEVICE_ATTR(als_enable, 0644, ASUS_WMI_DEVID_ALS_ENABLE); + +static ssize_t cpufv_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int value, rv; + + rv = kstrtoint(buf, 0, &value); + if (rv) + return rv; + + if (value < 0 || value > 2) + return -EINVAL; + + rv = asus_wmi_evaluate_method(ASUS_WMI_METHODID_CFVS, value, 0, NULL); + if (rv < 0) + return rv; + + return count; +} + +static DEVICE_ATTR_WO(cpufv); + +static struct attribute *platform_attributes[] = { + &dev_attr_cpufv.attr, + &dev_attr_camera.attr, + &dev_attr_cardr.attr, + &dev_attr_touchpad.attr, + &dev_attr_lid_resume.attr, + &dev_attr_als_enable.attr, + &dev_attr_fan_boost_mode.attr, + &dev_attr_keystone.attr, +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) + &dev_attr_charge_mode.attr, + &dev_attr_egpu_enable.attr, + &dev_attr_egpu_connected.attr, + &dev_attr_dgpu_disable.attr, + &dev_attr_gpu_mux_mode.attr, + &dev_attr_ppt_pl2_sppt.attr, + &dev_attr_ppt_pl1_spl.attr, + &dev_attr_ppt_fppt.attr, + &dev_attr_ppt_apu_sppt.attr, + &dev_attr_ppt_platform_sppt.attr, + &dev_attr_nv_dynamic_boost.attr, + &dev_attr_nv_temp_target.attr, + &dev_attr_mcu_powersave.attr, + &dev_attr_boot_sound.attr, + &dev_attr_panel_od.attr, + &dev_attr_mini_led_mode.attr, + &dev_attr_available_mini_led_mode.attr, + &dev_attr_throttle_thermal_policy.attr, +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + NULL +}; + +static umode_t asus_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct asus_wmi *asus = dev_get_drvdata(dev); + bool ok = true; + int devid = -1; + + if (attr == &dev_attr_camera.attr) + devid = ASUS_WMI_DEVID_CAMERA; + else if (attr == &dev_attr_cardr.attr) + devid = ASUS_WMI_DEVID_CARDREADER; + else if (attr == &dev_attr_touchpad.attr) + devid = ASUS_WMI_DEVID_TOUCHPAD; + else if (attr == &dev_attr_lid_resume.attr) + devid = ASUS_WMI_DEVID_LID_RESUME; + else if (attr == &dev_attr_als_enable.attr) + devid = ASUS_WMI_DEVID_ALS_ENABLE; + else if (attr == &dev_attr_fan_boost_mode.attr) + ok = asus->fan_boost_mode_available; + else if (attr == &dev_attr_keystone.attr) + ok = asus->keystone_available; + +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) + if (attr == &dev_attr_charge_mode.attr) + devid = ASUS_WMI_DEVID_CHARGE_MODE; + else if (attr == &dev_attr_egpu_enable.attr) + ok = asus->egpu_enable_available; + else if (attr == &dev_attr_egpu_connected.attr) + devid = ASUS_WMI_DEVID_EGPU_CONNECTED; + else if (attr == &dev_attr_dgpu_disable.attr) + ok = asus->dgpu_disable_available; + else if (attr == &dev_attr_gpu_mux_mode.attr) + ok = asus->gpu_mux_dev != 0; + else if (attr == &dev_attr_fan_boost_mode.attr) + ok = asus->fan_boost_mode_available; + else if (attr == &dev_attr_throttle_thermal_policy.attr) + ok = asus->throttle_thermal_policy_dev != 0; + else if (attr == &dev_attr_ppt_pl2_sppt.attr) + devid = ASUS_WMI_DEVID_PPT_PL2_SPPT; + else if (attr == &dev_attr_ppt_pl1_spl.attr) + devid = ASUS_WMI_DEVID_PPT_PL1_SPL; + else if (attr == &dev_attr_ppt_fppt.attr) + devid = ASUS_WMI_DEVID_PPT_PL3_FPPT; + else if (attr == &dev_attr_ppt_apu_sppt.attr) + devid = ASUS_WMI_DEVID_PPT_APU_SPPT; + else if (attr == &dev_attr_ppt_platform_sppt.attr) + devid = ASUS_WMI_DEVID_PPT_PLAT_SPPT; + else if (attr == &dev_attr_nv_dynamic_boost.attr) + devid = ASUS_WMI_DEVID_NV_DYN_BOOST; + else if (attr == &dev_attr_nv_temp_target.attr) + devid = ASUS_WMI_DEVID_NV_THERM_TARGET; + else if (attr == &dev_attr_mcu_powersave.attr) + devid = ASUS_WMI_DEVID_MCU_POWERSAVE; + else if (attr == &dev_attr_boot_sound.attr) + devid = ASUS_WMI_DEVID_BOOT_SOUND; + else if (attr == &dev_attr_panel_od.attr) + devid = ASUS_WMI_DEVID_PANEL_OD; + else if (attr == &dev_attr_mini_led_mode.attr) + ok = asus->mini_led_dev_id != 0; + else if (attr == &dev_attr_available_mini_led_mode.attr) + ok = asus->mini_led_dev_id != 0; +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + + if (devid != -1) { + ok = !(asus_wmi_get_devstate_simple(asus, devid) < 0); + pr_debug("%s called 0x%08x, ok: %x\n", __func__, devid, ok); + } + + return ok ? attr->mode : 0; +} + +static const struct attribute_group platform_attribute_group = { + .is_visible = asus_sysfs_is_visible, + .attrs = platform_attributes +}; + +static void asus_wmi_sysfs_exit(struct platform_device *device) +{ + sysfs_remove_group(&device->dev.kobj, &platform_attribute_group); +} + +static int asus_wmi_sysfs_init(struct platform_device *device) +{ + return sysfs_create_group(&device->dev.kobj, &platform_attribute_group); +} + +/* Platform device ************************************************************/ + +static int asus_wmi_platform_init(struct asus_wmi *asus) +{ + struct device *dev = &asus->platform_device->dev; + char *wmi_uid; + int rv; + + /* INIT enable hotkeys on some models */ + if (!asus_wmi_evaluate_method(ASUS_WMI_METHODID_INIT, 0, 0, &rv)) + pr_info("Initialization: %#x\n", rv); + + /* We don't know yet what to do with this version... */ + if (!asus_wmi_evaluate_method(ASUS_WMI_METHODID_SPEC, 0, 0x9, &rv)) { + pr_info("BIOS WMI version: %d.%d\n", rv >> 16, rv & 0xFF); + asus->spec = rv; + } + + /* + * The SFUN method probably allows the original driver to get the list + * of features supported by a given model. For now, 0x0100 or 0x0800 + * bit signifies that the laptop is equipped with a Wi-Fi MiniPCI card. + * The significance of others is yet to be found. + */ + if (!asus_wmi_evaluate_method(ASUS_WMI_METHODID_SFUN, 0, 0, &rv)) { + pr_info("SFUN value: %#x\n", rv); + asus->sfun = rv; + } + + /* + * Eee PC and Notebooks seems to have different method_id for DSTS, + * but it may also be related to the BIOS's SPEC. + * Note, on most Eeepc, there is no way to check if a method exist + * or note, while on notebooks, they returns 0xFFFFFFFE on failure, + * but once again, SPEC may probably be used for that kind of things. + * + * Additionally at least TUF Gaming series laptops return nothing for + * unknown methods, so the detection in this way is not possible. + * + * There is strong indication that only ACPI WMI devices that have _UID + * equal to "ASUSWMI" use DCTS whereas those with "ATK" use DSTS. + */ + wmi_uid = wmi_get_acpi_device_uid(ASUS_WMI_MGMT_GUID); + if (!wmi_uid) + return -ENODEV; + + if (!strcmp(wmi_uid, ASUS_ACPI_UID_ASUSWMI)) { + dev_info(dev, "Detected ASUSWMI, use DCTS\n"); + asus->dsts_id = ASUS_WMI_METHODID_DCTS; + } else { + dev_info(dev, "Detected %s, not ASUSWMI, use DSTS\n", wmi_uid); + asus->dsts_id = ASUS_WMI_METHODID_DSTS; + } + + /* CWAP allow to define the behavior of the Fn+F2 key, + * this method doesn't seems to be present on Eee PCs */ + if (asus->driver->quirks->wapf >= 0) + asus_wmi_set_devstate(ASUS_WMI_DEVID_CWAP, + asus->driver->quirks->wapf, NULL); + + return 0; +} + +/* debugfs ********************************************************************/ + +struct asus_wmi_debugfs_node { + struct asus_wmi *asus; + char *name; + int (*show) (struct seq_file *m, void *data); +}; + +static int show_dsts(struct seq_file *m, void *data) +{ + struct asus_wmi *asus = m->private; + int err; + u32 retval = -1; + + err = asus_wmi_get_devstate(asus, asus->debug.dev_id, &retval); + if (err < 0) + return err; + + seq_printf(m, "DSTS(%#x) = %#x\n", asus->debug.dev_id, retval); + + return 0; +} + +static int show_devs(struct seq_file *m, void *data) +{ + struct asus_wmi *asus = m->private; + int err; + u32 retval = -1; + + err = asus_wmi_set_devstate(asus->debug.dev_id, asus->debug.ctrl_param, + &retval); + if (err < 0) + return err; + + seq_printf(m, "DEVS(%#x, %#x) = %#x\n", asus->debug.dev_id, + asus->debug.ctrl_param, retval); + + return 0; +} + +static int show_call(struct seq_file *m, void *data) +{ + struct asus_wmi *asus = m->private; + struct bios_args args = { + .arg0 = asus->debug.dev_id, + .arg1 = asus->debug.ctrl_param, + }; + struct acpi_buffer input = { (acpi_size) sizeof(args), &args }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + + status = wmi_evaluate_method(ASUS_WMI_MGMT_GUID, + 0, asus->debug.method_id, + &input, &output); + + if (ACPI_FAILURE(status)) + return -EIO; + + obj = (union acpi_object *)output.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + seq_printf(m, "%#x(%#x, %#x) = %#x\n", asus->debug.method_id, + asus->debug.dev_id, asus->debug.ctrl_param, + (u32) obj->integer.value); + else + seq_printf(m, "%#x(%#x, %#x) = t:%d\n", asus->debug.method_id, + asus->debug.dev_id, asus->debug.ctrl_param, + obj ? obj->type : -1); + + kfree(obj); + + return 0; +} + +static struct asus_wmi_debugfs_node asus_wmi_debug_files[] = { + {NULL, "devs", show_devs}, + {NULL, "dsts", show_dsts}, + {NULL, "call", show_call}, +}; + +static int asus_wmi_debugfs_open(struct inode *inode, struct file *file) +{ + struct asus_wmi_debugfs_node *node = inode->i_private; + + return single_open(file, node->show, node->asus); +} + +static const struct file_operations asus_wmi_debugfs_io_ops = { + .owner = THIS_MODULE, + .open = asus_wmi_debugfs_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static void asus_wmi_debugfs_exit(struct asus_wmi *asus) +{ + debugfs_remove_recursive(asus->debug.root); +} + +static void asus_wmi_debugfs_init(struct asus_wmi *asus) +{ + int i; + + asus->debug.root = debugfs_create_dir(asus->driver->name, NULL); + + debugfs_create_x32("method_id", S_IRUGO | S_IWUSR, asus->debug.root, + &asus->debug.method_id); + + debugfs_create_x32("dev_id", S_IRUGO | S_IWUSR, asus->debug.root, + &asus->debug.dev_id); + + debugfs_create_x32("ctrl_param", S_IRUGO | S_IWUSR, asus->debug.root, + &asus->debug.ctrl_param); + + for (i = 0; i < ARRAY_SIZE(asus_wmi_debug_files); i++) { + struct asus_wmi_debugfs_node *node = &asus_wmi_debug_files[i]; + + node->asus = asus; + debugfs_create_file(node->name, S_IFREG | S_IRUGO, + asus->debug.root, node, + &asus_wmi_debugfs_io_ops); + } +} + +/* Init / exit ****************************************************************/ + +static int asus_wmi_add(struct platform_device *pdev) +{ + struct platform_driver *pdrv = to_platform_driver(pdev->dev.driver); + struct asus_wmi_driver *wdrv = to_asus_wmi_driver(pdrv); + struct asus_wmi *asus; + acpi_status status; + int err; + u32 result; + + asus = kzalloc_obj(struct asus_wmi); + if (!asus) + return -ENOMEM; + + asus->driver = wdrv; + asus->platform_device = pdev; + wdrv->platform_device = pdev; + platform_set_drvdata(asus->platform_device, asus); + + if (wdrv->detect_quirks) + wdrv->detect_quirks(asus->driver); + + err = asus_wmi_platform_init(asus); + if (err) + goto fail_platform; + + if (use_ally_mcu_hack == ASUS_WMI_ALLY_MCU_HACK_INIT) { + if (acpi_has_method(NULL, ASUS_USB0_PWR_EC0_CSEE) + && dmi_check_system(asus_rog_ally_device)) + use_ally_mcu_hack = ASUS_WMI_ALLY_MCU_HACK_ENABLED; + if (dmi_match(DMI_BOARD_NAME, "RC71")) { + /* + * These steps ensure the device is in a valid good state, this is + * especially important for the Ally 1 after a reboot. + */ + acpi_execute_simple_method(NULL, ASUS_USB0_PWR_EC0_CSEE, + ASUS_USB0_PWR_EC0_CSEE_ON); + msleep(ASUS_USB0_PWR_EC0_CSEE_WAIT); + } + } + + /* ensure defaults for tunables */ +#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) + asus->ppt_pl2_sppt = 5; + asus->ppt_pl1_spl = 5; + asus->ppt_apu_sppt = 5; + asus->ppt_platform_sppt = 5; + asus->ppt_fppt = 5; + asus->nv_dynamic_boost = 5; + asus->nv_temp_target = 75; + + asus->egpu_enable_available = asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_EGPU); + asus->dgpu_disable_available = asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_DGPU); + asus->kbd_rgb_state_available = asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_TUF_RGB_STATE); + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_MINI_LED_MODE)) + asus->mini_led_dev_id = ASUS_WMI_DEVID_MINI_LED_MODE; + else if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_MINI_LED_MODE2)) + asus->mini_led_dev_id = ASUS_WMI_DEVID_MINI_LED_MODE2; + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_GPU_MUX)) + asus->gpu_mux_dev = ASUS_WMI_DEVID_GPU_MUX; + else if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_GPU_MUX_VIVO)) + asus->gpu_mux_dev = ASUS_WMI_DEVID_GPU_MUX_VIVO; +#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ + + asus->oobe_state_available = asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_OOBE); + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY)) + asus->throttle_thermal_policy_dev = ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY; + else if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY_VIVO)) + asus->throttle_thermal_policy_dev = ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY_VIVO; + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_TUF_RGB_MODE)) + asus->kbd_rgb_dev = ASUS_WMI_DEVID_TUF_RGB_MODE; + else if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_TUF_RGB_MODE2)) + asus->kbd_rgb_dev = ASUS_WMI_DEVID_TUF_RGB_MODE2; + + err = fan_boost_mode_check_present(asus); + if (err) + goto fail_fan_boost_mode; + + err = platform_profile_setup(asus); + if (err) + goto fail_platform_profile_setup; + + err = keystone_check_present(asus); + if (err) + dev_warn(&pdev->dev, "Failed to check Keystone presence: %d\n", err); + + err = asus_wmi_sysfs_init(asus->platform_device); + if (err) + goto fail_sysfs; + + err = asus_wmi_input_init(asus); + if (err) + goto fail_input; + + err = asus_wmi_fan_init(asus); /* probably no problems on error */ + + err = asus_wmi_hwmon_init(asus); + if (err) + goto fail_hwmon; + + err = asus_wmi_custom_fan_curve_init(asus); + if (err) + goto fail_custom_fan_curve; + + err = asus_wmi_led_init(asus); + if (err) + goto fail_leds; + + asus_wmi_get_devstate(asus, ASUS_WMI_DEVID_WLAN, &result); + if ((result & (ASUS_WMI_DSTS_PRESENCE_BIT | ASUS_WMI_DSTS_USER_BIT)) == + (ASUS_WMI_DSTS_PRESENCE_BIT | ASUS_WMI_DSTS_USER_BIT)) + asus->driver->wlan_ctrl_by_user = 1; + + if (!(asus->driver->wlan_ctrl_by_user && ashs_present())) { + err = asus_wmi_rfkill_init(asus); + if (err) + goto fail_rfkill; + } + + if (asus->driver->quirks->wmi_force_als_set) + asus_wmi_set_als(); + + if (asus->driver->quirks->xusb2pr) + asus_wmi_set_xusb2pr(asus); + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + err = asus_wmi_backlight_init(asus); + if (err && err != -ENODEV) + goto fail_backlight; + } else if (asus->driver->quirks->wmi_backlight_set_devstate) + err = asus_wmi_set_devstate(ASUS_WMI_DEVID_BACKLIGHT, 2, NULL); + + if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_SCREENPAD_LIGHT)) { + err = asus_screenpad_init(asus); + if (err && err != -ENODEV) + goto fail_screenpad; + } + + if (asus_wmi_has_fnlock_key(asus)) { + asus->fnlock_locked = fnlock_default; + asus_wmi_fnlock_update(asus); + } + + status = wmi_install_notify_handler(asus->driver->event_guid, + asus_wmi_notify, asus); + if (ACPI_FAILURE(status)) { + pr_err("Unable to register notify handler - %d\n", status); + err = -ENODEV; + goto fail_wmi_handler; + } + + if (asus->driver->i8042_filter) { + err = i8042_install_filter(asus->driver->i8042_filter, NULL); + if (err) + pr_warn("Unable to install key filter - %d\n", err); + } + + asus_wmi_battery_init(asus); + + asus_wmi_debugfs_init(asus); + + return 0; + +fail_wmi_handler: + asus_wmi_backlight_exit(asus); +fail_backlight: + asus_wmi_rfkill_exit(asus); +fail_screenpad: + asus_screenpad_exit(asus); +fail_rfkill: + asus_wmi_led_exit(asus); +fail_leds: +fail_hwmon: + asus_wmi_input_exit(asus); +fail_input: + asus_wmi_sysfs_exit(asus->platform_device); +fail_sysfs: +fail_custom_fan_curve: +fail_platform_profile_setup: +fail_fan_boost_mode: +fail_platform: + kfree(asus); + return err; +} + +static void asus_wmi_remove(struct platform_device *device) +{ + struct asus_wmi *asus; + + asus = platform_get_drvdata(device); + if (asus->driver->i8042_filter) + i8042_remove_filter(asus->driver->i8042_filter); + wmi_remove_notify_handler(asus->driver->event_guid); + asus_wmi_backlight_exit(asus); + asus_screenpad_exit(asus); + asus_wmi_input_exit(asus); + asus_wmi_led_exit(asus); + asus_wmi_rfkill_exit(asus); + asus_wmi_debugfs_exit(asus); + asus_wmi_sysfs_exit(asus->platform_device); + asus_fan_set_auto(asus); + throttle_thermal_policy_set_default(asus); + asus_wmi_battery_exit(asus); + + kfree(asus); +} + +/* Platform driver - hibernate/resume callbacks *******************************/ + +static int asus_hotk_thaw(struct device *device) +{ + struct asus_wmi *asus = dev_get_drvdata(device); + + if (asus->wlan.rfkill) { + bool wlan; + + /* + * Work around bios bug - acpi _PTS turns off the wireless led + * during suspend. Normally it restores it on resume, but + * we should kick it ourselves in case hibernation is aborted. + */ + wlan = asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_WLAN); + asus_wmi_set_devstate(ASUS_WMI_DEVID_WLAN, wlan, NULL); + } + + return 0; +} + +static int asus_hotk_resume(struct device *device) +{ + struct asus_wmi *asus = dev_get_drvdata(device); + + if (!IS_ERR_OR_NULL(asus->kbd_led.dev)) + kbd_led_update(asus); + + if (asus_wmi_has_fnlock_key(asus)) + asus_wmi_fnlock_update(asus); + + asus_wmi_tablet_mode_get_state(asus); + + return 0; +} + +static int asus_hotk_restore(struct device *device) +{ + struct asus_wmi *asus = dev_get_drvdata(device); + int bl; + + /* Refresh both wlan rfkill state and pci hotplug */ + if (asus->wlan.rfkill) + asus_rfkill_hotplug(asus); + + if (asus->bluetooth.rfkill) { + bl = !asus_wmi_get_devstate_simple(asus, + ASUS_WMI_DEVID_BLUETOOTH); + rfkill_set_sw_state(asus->bluetooth.rfkill, bl); + } + if (asus->wimax.rfkill) { + bl = !asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_WIMAX); + rfkill_set_sw_state(asus->wimax.rfkill, bl); + } + if (asus->wwan3g.rfkill) { + bl = !asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_WWAN3G); + rfkill_set_sw_state(asus->wwan3g.rfkill, bl); + } + if (asus->gps.rfkill) { + bl = !asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_GPS); + rfkill_set_sw_state(asus->gps.rfkill, bl); + } + if (asus->uwb.rfkill) { + bl = !asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_UWB); + rfkill_set_sw_state(asus->uwb.rfkill, bl); + } + if (!IS_ERR_OR_NULL(asus->kbd_led.dev)) + kbd_led_update(asus); + if (asus->oobe_state_available) { + /* + * Disable OOBE state, so that e.g. the keyboard backlight + * works. + */ + asus_wmi_set_devstate(ASUS_WMI_DEVID_OOBE, 1, NULL); + } + + if (asus_wmi_has_fnlock_key(asus)) + asus_wmi_fnlock_update(asus); + + asus_wmi_tablet_mode_get_state(asus); + return 0; +} + +static int asus_hotk_prepare(struct device *device) +{ + if (use_ally_mcu_hack == ASUS_WMI_ALLY_MCU_HACK_ENABLED) { + acpi_execute_simple_method(NULL, ASUS_USB0_PWR_EC0_CSEE, + ASUS_USB0_PWR_EC0_CSEE_OFF); + msleep(ASUS_USB0_PWR_EC0_CSEE_WAIT); + } + return 0; +} + +#if defined(CONFIG_SUSPEND) +static void asus_ally_s2idle_restore(void) +{ + if (use_ally_mcu_hack == ASUS_WMI_ALLY_MCU_HACK_ENABLED) { + acpi_execute_simple_method(NULL, ASUS_USB0_PWR_EC0_CSEE, + ASUS_USB0_PWR_EC0_CSEE_ON); + msleep(ASUS_USB0_PWR_EC0_CSEE_WAIT); + } +} + +/* Use only for Ally devices due to the wake_on_ac */ +static struct acpi_s2idle_dev_ops asus_ally_s2idle_dev_ops = { + .restore = asus_ally_s2idle_restore, +}; + +static void asus_s2idle_check_register(void) +{ + if (acpi_register_lps0_dev(&asus_ally_s2idle_dev_ops)) + pr_warn("failed to register LPS0 sleep handler in asus-wmi\n"); +} + +static void asus_s2idle_check_unregister(void) +{ + acpi_unregister_lps0_dev(&asus_ally_s2idle_dev_ops); +} +#else +static void asus_s2idle_check_register(void) {} +static void asus_s2idle_check_unregister(void) {} +#endif /* CONFIG_SUSPEND */ + +static const struct dev_pm_ops asus_pm_ops = { + .thaw = asus_hotk_thaw, + .restore = asus_hotk_restore, + .resume = asus_hotk_resume, + .prepare = asus_hotk_prepare, +}; + +/* Registration ***************************************************************/ + +static int asus_wmi_probe(struct platform_device *pdev) +{ + struct platform_driver *pdrv = to_platform_driver(pdev->dev.driver); + struct asus_wmi_driver *wdrv = to_asus_wmi_driver(pdrv); + int ret; + + if (!wmi_has_guid(ASUS_WMI_MGMT_GUID)) { + pr_warn("ASUS Management GUID not found\n"); + return -ENODEV; + } + + if (wdrv->event_guid && !wmi_has_guid(wdrv->event_guid)) { + pr_warn("ASUS Event GUID not found\n"); + return -ENODEV; + } + + if (wdrv->probe) { + ret = wdrv->probe(pdev); + if (ret) + return ret; + } + + asus_s2idle_check_register(); + + ret = asus_wmi_add(pdev); + if (ret) + asus_s2idle_check_unregister(); + + return ret; +} + +static bool used; +static DEFINE_MUTEX(register_mutex); + +int __init_or_module asus_wmi_register_driver(struct asus_wmi_driver *driver) +{ + struct platform_driver *platform_driver; + struct platform_device *platform_device; + + guard(mutex)(®ister_mutex); + if (used) + return -EBUSY; + + platform_driver = &driver->platform_driver; + platform_driver->remove = asus_wmi_remove; + platform_driver->driver.owner = driver->owner; + platform_driver->driver.name = driver->name; + platform_driver->driver.pm = &asus_pm_ops; + + platform_device = platform_create_bundle(platform_driver, + asus_wmi_probe, + NULL, 0, NULL, 0); + if (IS_ERR(platform_device)) + return PTR_ERR(platform_device); + + used = true; + return 0; +} +EXPORT_SYMBOL_GPL(asus_wmi_register_driver); + +void asus_wmi_unregister_driver(struct asus_wmi_driver *driver) +{ + guard(mutex)(®ister_mutex); + asus_s2idle_check_unregister(); + + platform_device_unregister(driver->platform_device); + platform_driver_unregister(&driver->platform_driver); + used = false; +} +EXPORT_SYMBOL_GPL(asus_wmi_unregister_driver); diff --git a/drivers/platform/x86/asus-wmi.h b/drivers/platform/x86/asus-wmi.h new file mode 100644 index 000000000..5cd4392b9 --- /dev/null +++ b/drivers/platform/x86/asus-wmi.h @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Asus PC WMI hotkey driver + * + * Copyright(C) 2010 Intel Corporation. + * Copyright(C) 2010-2011 Corentin Chary + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac + * Copyright (C) 2005 Bernhard Rosenkraenzer + * Copyright (C) 2005 Dmitry Torokhov + */ + +#ifndef _ASUS_WMI_H_ +#define _ASUS_WMI_H_ + +#include +#include + +#define ASUS_WMI_KEY_IGNORE (-1) +#define ASUS_WMI_KEY_ARMOURY 0xffff01 +#define ASUS_WMI_BRN_DOWN 0x2e +#define ASUS_WMI_BRN_UP 0x2f + +struct module; +struct key_entry; +struct asus_wmi; + +enum asus_wmi_tablet_switch_mode { + asus_wmi_no_tablet_switch, + asus_wmi_kbd_dock_devid, + asus_wmi_lid_flip_devid, + asus_wmi_lid_flip_rog_devid, +}; + +struct quirk_entry { + bool hotplug_wireless; + bool scalar_panel_brightness; + bool store_backlight_power; + bool wmi_backlight_set_devstate; + bool wmi_force_als_set; + bool wmi_ignore_fan; + bool filter_i8042_e1_extended_codes; + int key_wlan_event; + enum asus_wmi_tablet_switch_mode tablet_switch_mode; + int wapf; + /* + * For machines with AMD graphic chips, it will send out WMI event + * and ACPI interrupt at the same time while hitting the hotkey. + * To simplify the problem, we just have to ignore the WMI event, + * and let the ACPI interrupt to send out the key event. + */ + int no_display_toggle; + u32 xusb2pr; +}; + +struct asus_wmi_driver { + int brightness; + int panel_power; + int screenpad_brightness; + int wlan_ctrl_by_user; + + const char *name; + struct module *owner; + + const char *event_guid; + + const struct key_entry *keymap; + const char *input_name; + const char *input_phys; + struct quirk_entry *quirks; + /* Returns new code, value, and autorelease values in arguments. + * Return ASUS_WMI_KEY_IGNORE in code if event should be ignored. */ + void (*key_filter) (struct asus_wmi_driver *driver, int *code, + unsigned int *value, bool *autorelease); + /* Optional standard i8042 filter */ + i8042_filter_t i8042_filter; + + int (*probe) (struct platform_device *device); + void (*detect_quirks) (struct asus_wmi_driver *driver); + + struct platform_driver platform_driver; + struct platform_device *platform_device; +}; + +int asus_wmi_register_driver(struct asus_wmi_driver *driver); +void asus_wmi_unregister_driver(struct asus_wmi_driver *driver); + +#endif /* !_ASUS_WMI_H_ */ diff --git a/drivers/platform/x86/ayaneo-ec.c b/drivers/platform/x86/ayaneo-ec.c new file mode 100644 index 000000000..41a24e091 --- /dev/null +++ b/drivers/platform/x86/ayaneo-ec.c @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Platform driver for the Embedded Controller (EC) of Ayaneo devices. Handles + * hwmon (fan speed, fan control), battery charge limits, and magic module + * control (connected modules, controller disconnection). + * + * Copyright (C) 2025 Antheas Kapenekakis + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AYANEO_PWM_ENABLE_REG 0x4A +#define AYANEO_PWM_REG 0x4B +#define AYANEO_PWM_MODE_AUTO 0x00 +#define AYANEO_PWM_MODE_MANUAL 0x01 + +#define AYANEO_FAN_REG 0x76 + +#define EC_CHARGE_CONTROL_BEHAVIOURS \ + (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \ + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) +#define AYANEO_CHARGE_REG 0x1e +#define AYANEO_CHARGE_VAL_AUTO 0xaa +#define AYANEO_CHARGE_VAL_INHIBIT 0x55 + +#define AYANEO_POWER_REG 0x2d +#define AYANEO_POWER_OFF 0xfe +#define AYANEO_POWER_ON 0xff +#define AYANEO_MODULE_REG 0x2f +#define AYANEO_MODULE_LEFT BIT(0) +#define AYANEO_MODULE_RIGHT BIT(1) +#define AYANEO_MODULE_MASK (AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT) + +struct ayaneo_ec_quirk { + bool has_fan_control; + bool has_charge_control; + bool has_magic_modules; +}; + +struct ayaneo_ec_platform_data { + struct platform_device *pdev; + struct ayaneo_ec_quirk *quirks; + struct acpi_battery_hook battery_hook; + + // Protects access to restore_pwm + struct mutex hwmon_lock; + bool restore_charge_limit; + bool restore_pwm; +}; + +static const struct ayaneo_ec_quirk quirk_fan = { + .has_fan_control = true, +}; + +static const struct ayaneo_ec_quirk quirk_charge_limit = { + .has_fan_control = true, + .has_charge_control = true, +}; + +static const struct ayaneo_ec_quirk quirk_ayaneo3 = { + .has_fan_control = true, + .has_charge_control = true, + .has_magic_modules = true, +}; + +static const struct dmi_system_id dmi_table[] = { + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), + }, + .driver_data = (void *)&quirk_fan, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), + }, + .driver_data = (void *)&quirk_fan, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), + }, + .driver_data = (void *)&quirk_fan, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), + }, + .driver_data = (void *)&quirk_charge_limit, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR 1S"), + }, + .driver_data = (void *)&quirk_charge_limit, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AB05-Mendocino"), + }, + .driver_data = (void *)&quirk_charge_limit, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), + }, + .driver_data = (void *)&quirk_charge_limit, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), + }, + .driver_data = (void *)&quirk_charge_limit, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), + }, + .driver_data = (void *)&quirk_ayaneo3, + }, + {}, +}; + +/* Callbacks for hwmon interface */ +static umode_t ayaneo_ec_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + switch (type) { + case hwmon_fan: + return 0444; + case hwmon_pwm: + return 0644; + default: + return 0; + } +} + +static int ayaneo_ec_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + u8 tmp; + int ret; + + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + ret = ec_read(AYANEO_FAN_REG, &tmp); + if (ret) + return ret; + *val = tmp << 8; + ret = ec_read(AYANEO_FAN_REG + 1, &tmp); + if (ret) + return ret; + *val |= tmp; + return 0; + default: + break; + } + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + ret = ec_read(AYANEO_PWM_REG, &tmp); + if (ret) + return ret; + if (tmp > 100) + return -EIO; + *val = (255 * tmp) / 100; + return 0; + case hwmon_pwm_enable: + ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp); + if (ret) + return ret; + if (tmp == AYANEO_PWM_MODE_MANUAL) + *val = 1; + else if (tmp == AYANEO_PWM_MODE_AUTO) + *val = 2; + else + return -EIO; + return 0; + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +static int ayaneo_ec_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct ayaneo_ec_platform_data *data = dev_get_drvdata(dev); + int ret; + + guard(mutex)(&data->hwmon_lock); + + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + data->restore_pwm = false; + switch (val) { + case 1: + return ec_write(AYANEO_PWM_ENABLE_REG, + AYANEO_PWM_MODE_MANUAL); + case 2: + return ec_write(AYANEO_PWM_ENABLE_REG, + AYANEO_PWM_MODE_AUTO); + default: + return -EINVAL; + } + case hwmon_pwm_input: + if (val < 0 || val > 255) + return -EINVAL; + if (data->restore_pwm) { + /* + * Defer restoring PWM control to after + * userspace resumes successfully + */ + ret = ec_write(AYANEO_PWM_ENABLE_REG, + AYANEO_PWM_MODE_MANUAL); + if (ret) + return ret; + data->restore_pwm = false; + } + return ec_write(AYANEO_PWM_REG, (val * 100) / 255); + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +static const struct hwmon_ops ayaneo_ec_hwmon_ops = { + .is_visible = ayaneo_ec_hwmon_is_visible, + .read = ayaneo_ec_read, + .write = ayaneo_ec_write, +}; + +static const struct hwmon_channel_info *const ayaneo_ec_sensors[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL, +}; + +static const struct hwmon_chip_info ayaneo_ec_chip_info = { + .ops = &ayaneo_ec_hwmon_ops, + .info = ayaneo_ec_sensors, +}; + +static int ayaneo_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) +{ + int ret; + u8 tmp; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + ret = ec_read(AYANEO_CHARGE_REG, &tmp); + if (ret) + return ret; + + if (tmp == AYANEO_CHARGE_VAL_INHIBIT) + val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + else + val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + return 0; + default: + return -EINVAL; + } +} + +static int ayaneo_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) +{ + u8 raw_val; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + switch (val->intval) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + raw_val = AYANEO_CHARGE_VAL_AUTO; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: + raw_val = AYANEO_CHARGE_VAL_INHIBIT; + break; + default: + return -EINVAL; + } + return ec_write(AYANEO_CHARGE_REG, raw_val); + default: + return -EINVAL; + } +} + +static int ayaneo_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 ayaneo_psy_ext_props[] = { + POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, +}; + +static const struct power_supply_ext ayaneo_psy_ext = { + .name = "ayaneo-charge-control", + .properties = ayaneo_psy_ext_props, + .num_properties = ARRAY_SIZE(ayaneo_psy_ext_props), + .charge_behaviours = EC_CHARGE_CONTROL_BEHAVIOURS, + .get_property = ayaneo_psy_ext_get_prop, + .set_property = ayaneo_psy_ext_set_prop, + .property_is_writeable = ayaneo_psy_prop_is_writeable, +}; + +static int ayaneo_add_battery(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + struct ayaneo_ec_platform_data *data = + container_of(hook, struct ayaneo_ec_platform_data, battery_hook); + + return power_supply_register_extension(battery, &ayaneo_psy_ext, + &data->pdev->dev, NULL); +} + +static int ayaneo_remove_battery(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &ayaneo_psy_ext); + return 0; +} + +static ssize_t controller_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + bool value; + int ret; + + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + ret = ec_write(AYANEO_POWER_REG, value ? AYANEO_POWER_ON : AYANEO_POWER_OFF); + if (ret) + return ret; + + return count; +} + +static ssize_t controller_power_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 val; + + ret = ec_read(AYANEO_POWER_REG, &val); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", val == AYANEO_POWER_ON); +} + +static DEVICE_ATTR_RW(controller_power); + +static ssize_t controller_modules_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u8 unconnected_modules; + char *out; + int ret; + + ret = ec_read(AYANEO_MODULE_REG, &unconnected_modules); + if (ret) + return ret; + + switch (~unconnected_modules & AYANEO_MODULE_MASK) { + case AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT: + out = "both"; + break; + case AYANEO_MODULE_LEFT: + out = "left"; + break; + case AYANEO_MODULE_RIGHT: + out = "right"; + break; + default: + out = "none"; + break; + } + + return sysfs_emit(buf, "%s\n", out); +} + +static DEVICE_ATTR_RO(controller_modules); + +static struct attribute *aya_mm_attrs[] = { + &dev_attr_controller_power.attr, + &dev_attr_controller_modules.attr, + NULL +}; + +static umode_t aya_mm_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct platform_device *pdev = to_platform_device(dev); + struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev); + + if (data->quirks->has_magic_modules) + return attr->mode; + return 0; +} + +static const struct attribute_group aya_mm_attribute_group = { + .is_visible = aya_mm_is_visible, + .attrs = aya_mm_attrs, +}; + +static const struct attribute_group *ayaneo_ec_groups[] = { + &aya_mm_attribute_group, + NULL +}; + +static int ayaneo_ec_probe(struct platform_device *pdev) +{ + const struct dmi_system_id *dmi_entry; + struct ayaneo_ec_platform_data *data; + struct device *hwdev; + int ret; + + dmi_entry = dmi_first_match(dmi_table); + if (!dmi_entry) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->pdev = pdev; + data->quirks = dmi_entry->driver_data; + ret = devm_mutex_init(&pdev->dev, &data->hwmon_lock); + if (ret) + return ret; + platform_set_drvdata(pdev, data); + + if (data->quirks->has_fan_control) { + hwdev = devm_hwmon_device_register_with_info(&pdev->dev, + "ayaneo_ec", data, &ayaneo_ec_chip_info, NULL); + if (IS_ERR(hwdev)) + return PTR_ERR(hwdev); + } + + if (data->quirks->has_charge_control) { + data->battery_hook.add_battery = ayaneo_add_battery; + data->battery_hook.remove_battery = ayaneo_remove_battery; + data->battery_hook.name = "Ayaneo Battery"; + ret = devm_battery_hook_register(&pdev->dev, &data->battery_hook); + if (ret) + return ret; + } + + return 0; +} + +static int ayaneo_freeze(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev); + int ret; + u8 tmp; + + if (data->quirks->has_charge_control) { + ret = ec_read(AYANEO_CHARGE_REG, &tmp); + if (ret) + return ret; + + data->restore_charge_limit = tmp == AYANEO_CHARGE_VAL_INHIBIT; + } + + if (data->quirks->has_fan_control) { + ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp); + if (ret) + return ret; + + data->restore_pwm = tmp == AYANEO_PWM_MODE_MANUAL; + + /* + * Release the fan when entering hibernation to avoid + * overheating if hibernation fails and hangs. + */ + if (data->restore_pwm) { + ret = ec_write(AYANEO_PWM_ENABLE_REG, AYANEO_PWM_MODE_AUTO); + if (ret) + return ret; + } + } + + return 0; +} + +static int ayaneo_restore(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev); + int ret; + + if (data->quirks->has_charge_control && data->restore_charge_limit) { + ret = ec_write(AYANEO_CHARGE_REG, AYANEO_CHARGE_VAL_INHIBIT); + if (ret) + return ret; + } + + return 0; +} + +static const struct dev_pm_ops ayaneo_pm_ops = { + .freeze = ayaneo_freeze, + .restore = ayaneo_restore, +}; + +static struct platform_driver ayaneo_platform_driver = { + .driver = { + .name = "ayaneo-ec", + .dev_groups = ayaneo_ec_groups, + .pm = pm_sleep_ptr(&ayaneo_pm_ops), + }, + .probe = ayaneo_ec_probe, +}; + +static struct platform_device *ayaneo_platform_device; + +static int __init ayaneo_ec_init(void) +{ + ayaneo_platform_device = + platform_create_bundle(&ayaneo_platform_driver, + ayaneo_ec_probe, NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(ayaneo_platform_device); +} + +static void __exit ayaneo_ec_exit(void) +{ + platform_device_unregister(ayaneo_platform_device); + platform_driver_unregister(&ayaneo_platform_driver); +} + +MODULE_DEVICE_TABLE(dmi, dmi_table); + +module_init(ayaneo_ec_init); +module_exit(ayaneo_ec_exit); + +MODULE_AUTHOR("Antheas Kapenekakis "); +MODULE_DESCRIPTION("Ayaneo Embedded Controller (EC) platform features"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/barco-p50-gpio.c b/drivers/platform/x86/barco-p50-gpio.c new file mode 100644 index 000000000..e14003824 --- /dev/null +++ b/drivers/platform/x86/barco-p50-gpio.c @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * Support for EC-connected GPIOs for identify + * LED/button on Barco P50 board + * + * Copyright (C) 2021 Barco NV + * Author: Santosh Kumar Yadav + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define DRIVER_NAME "barco-p50-gpio" + +/* GPIO lines */ +#define P50_GPIO_LINE_LED 0 +#define P50_GPIO_LINE_BTN 1 + +/* GPIO IO Ports */ +#define P50_GPIO_IO_PORT_BASE 0x299 + +#define P50_PORT_DATA 0x00 +#define P50_PORT_CMD 0x01 + +#define P50_STATUS_OBF 0x01 /* EC output buffer full */ +#define P50_STATUS_IBF 0x02 /* EC input buffer full */ + +#define P50_CMD_READ 0xa0 +#define P50_CMD_WRITE 0x50 + +/* EC mailbox registers */ +#define P50_MBOX_REG_CMD 0x00 +#define P50_MBOX_REG_STATUS 0x01 +#define P50_MBOX_REG_PARAM 0x02 +#define P50_MBOX_REG_DATA 0x03 + +#define P50_MBOX_CMD_READ_GPIO 0x11 +#define P50_MBOX_CMD_WRITE_GPIO 0x12 +#define P50_MBOX_CMD_CLEAR 0xff + +#define P50_MBOX_STATUS_SUCCESS 0x01 + +#define P50_MBOX_PARAM_LED 0x12 +#define P50_MBOX_PARAM_BTN 0x13 + + +struct p50_gpio { + struct gpio_chip gc; + struct mutex lock; + unsigned long base; + struct platform_device *leds_pdev; + struct platform_device *keys_pdev; +}; + +static struct platform_device *gpio_pdev; + +static int gpio_params[] = { + [P50_GPIO_LINE_LED] = P50_MBOX_PARAM_LED, + [P50_GPIO_LINE_BTN] = P50_MBOX_PARAM_BTN, +}; + +static const char * const gpio_names[] = { + [P50_GPIO_LINE_LED] = "identify-led", + [P50_GPIO_LINE_BTN] = "identify-button", +}; + +static const struct software_node gpiochip_node = { + .name = DRIVER_NAME, +}; + +/* GPIO LEDs */ +static const struct software_node gpio_leds_node = { + .name = "gpio-leds-identify", +}; + +static const struct property_entry identify_led_props[] = { + PROPERTY_ENTRY_GPIO("gpios", &gpiochip_node, P50_GPIO_LINE_LED, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node identify_led_node = { + .parent = &gpio_leds_node, + .name = "identify", + .properties = identify_led_props, +}; + +/* GPIO keyboard */ +static const struct property_entry gpio_keys_props[] = { + PROPERTY_ENTRY_STRING("label", "identify"), + PROPERTY_ENTRY_U32("poll-interval", 100), + { } +}; + +static const struct software_node gpio_keys_node = { + .name = "gpio-keys-identify", + .properties = gpio_keys_props, +}; + +static struct property_entry vendor_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_VENDOR), + PROPERTY_ENTRY_GPIO("gpios", &gpiochip_node, P50_GPIO_LINE_BTN, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node vendor_key_node = { + .parent = &gpio_keys_node, + .properties = vendor_key_props, +}; + +static const struct software_node *p50_swnodes[] = { + &gpio_leds_node, + &identify_led_node, + &gpio_keys_node, + &vendor_key_node, + NULL +}; + +/* low level access routines */ + +static int p50_wait_ec(struct p50_gpio *p50, int mask, int expected) +{ + int i, val; + + for (i = 0; i < 100; i++) { + val = inb(p50->base + P50_PORT_CMD) & mask; + if (val == expected) + return 0; + usleep_range(500, 2000); + } + + dev_err(p50->gc.parent, "Timed out waiting for EC (0x%x)\n", val); + return -ETIMEDOUT; +} + + +static int p50_read_mbox_reg(struct p50_gpio *p50, int reg) +{ + int ret; + + ret = p50_wait_ec(p50, P50_STATUS_IBF, 0); + if (ret) + return ret; + + /* clear output buffer flag, prevent unfinished commands */ + inb(p50->base + P50_PORT_DATA); + + /* cmd/address */ + outb(P50_CMD_READ | reg, p50->base + P50_PORT_CMD); + + ret = p50_wait_ec(p50, P50_STATUS_OBF, P50_STATUS_OBF); + if (ret) + return ret; + + return inb(p50->base + P50_PORT_DATA); +} + +static int p50_write_mbox_reg(struct p50_gpio *p50, int reg, int val) +{ + int ret; + + ret = p50_wait_ec(p50, P50_STATUS_IBF, 0); + if (ret) + return ret; + + /* cmd/address */ + outb(P50_CMD_WRITE | reg, p50->base + P50_PORT_CMD); + + ret = p50_wait_ec(p50, P50_STATUS_IBF, 0); + if (ret) + return ret; + + /* data */ + outb(val, p50->base + P50_PORT_DATA); + + return 0; +} + + +/* mbox routines */ + +static int p50_wait_mbox_idle(struct p50_gpio *p50) +{ + int i, val; + + for (i = 0; i < 1000; i++) { + val = p50_read_mbox_reg(p50, P50_MBOX_REG_CMD); + /* cmd is 0 when idle */ + if (val <= 0) + return val; + + usleep_range(500, 2000); + } + + dev_err(p50->gc.parent, "Timed out waiting for EC mbox idle (CMD: 0x%x)\n", val); + + return -ETIMEDOUT; +} + +static int p50_send_mbox_cmd(struct p50_gpio *p50, int cmd, int param, int data) +{ + int ret; + + ret = p50_wait_mbox_idle(p50); + if (ret) + return ret; + + ret = p50_write_mbox_reg(p50, P50_MBOX_REG_DATA, data); + if (ret) + return ret; + + ret = p50_write_mbox_reg(p50, P50_MBOX_REG_PARAM, param); + if (ret) + return ret; + + ret = p50_write_mbox_reg(p50, P50_MBOX_REG_CMD, cmd); + if (ret) + return ret; + + ret = p50_wait_mbox_idle(p50); + if (ret) + return ret; + + ret = p50_read_mbox_reg(p50, P50_MBOX_REG_STATUS); + if (ret < 0) + return ret; + + if (ret == P50_MBOX_STATUS_SUCCESS) + return 0; + + dev_err(p50->gc.parent, "Mbox command failed (CMD=0x%x STAT=0x%x PARAM=0x%x DATA=0x%x)\n", + cmd, ret, param, data); + + return -EIO; +} + + +/* gpio routines */ + +static int p50_gpio_get_direction(struct gpio_chip *gc, unsigned int offset) +{ + switch (offset) { + case P50_GPIO_LINE_BTN: + return GPIO_LINE_DIRECTION_IN; + + case P50_GPIO_LINE_LED: + return GPIO_LINE_DIRECTION_OUT; + + default: + return -EINVAL; + } +} + +static int p50_gpio_get(struct gpio_chip *gc, unsigned int offset) +{ + struct p50_gpio *p50 = gpiochip_get_data(gc); + int ret; + + guard(mutex)(&p50->lock); + + ret = p50_send_mbox_cmd(p50, P50_MBOX_CMD_READ_GPIO, gpio_params[offset], 0); + if (ret < 0) + return ret; + + ret = p50_read_mbox_reg(p50, P50_MBOX_REG_DATA); + if (ret < 0) + return ret; + + return !!ret; +} + +static int p50_gpio_set(struct gpio_chip *gc, unsigned int offset, int value) +{ + struct p50_gpio *p50 = gpiochip_get_data(gc); + + guard(mutex)(&p50->lock); + + return p50_send_mbox_cmd(p50, P50_MBOX_CMD_WRITE_GPIO, + gpio_params[offset], value); +} + +static int p50_gpio_probe(struct platform_device *pdev) +{ + struct platform_device_info key_info = { + .name = "gpio-keys-polled", + .id = PLATFORM_DEVID_NONE, + .parent = &pdev->dev, + }; + struct platform_device_info led_info = { + .name = "leds-gpio", + .id = PLATFORM_DEVID_NONE, + .parent = &pdev->dev, + }; + struct p50_gpio *p50; + struct resource *res; + int ret; + + res = platform_get_resource(pdev, IORESOURCE_IO, 0); + if (!res) { + dev_err(&pdev->dev, "Cannot get I/O ports\n"); + return -ENODEV; + } + + if (!devm_request_region(&pdev->dev, res->start, resource_size(res), pdev->name)) { + dev_err(&pdev->dev, "Unable to reserve I/O region\n"); + return -EBUSY; + } + + p50 = devm_kzalloc(&pdev->dev, sizeof(*p50), GFP_KERNEL); + if (!p50) + return -ENOMEM; + + platform_set_drvdata(pdev, p50); + mutex_init(&p50->lock); + p50->base = res->start; + p50->gc.owner = THIS_MODULE; + p50->gc.parent = &pdev->dev; + p50->gc.label = dev_name(&pdev->dev); + p50->gc.ngpio = ARRAY_SIZE(gpio_names); + p50->gc.names = gpio_names; + p50->gc.can_sleep = true; + p50->gc.base = -1; + p50->gc.get_direction = p50_gpio_get_direction; + p50->gc.get = p50_gpio_get; + p50->gc.set = p50_gpio_set; + + + /* reset mbox */ + ret = p50_wait_mbox_idle(p50); + if (ret) + return ret; + + ret = p50_write_mbox_reg(p50, P50_MBOX_REG_CMD, P50_MBOX_CMD_CLEAR); + if (ret) + return ret; + + ret = p50_wait_mbox_idle(p50); + if (ret) + return ret; + + + ret = devm_gpiochip_add_data(&pdev->dev, &p50->gc, p50); + if (ret < 0) { + dev_err(&pdev->dev, "Could not register gpiochip: %d\n", ret); + return ret; + } + + ret = software_node_register_node_group(p50_swnodes); + if (ret) + return dev_err_probe(&pdev->dev, ret, "failed to register software nodes"); + + led_info.fwnode = software_node_fwnode(&gpio_leds_node); + p50->leds_pdev = platform_device_register_full(&led_info); + if (IS_ERR(p50->leds_pdev)) { + ret = PTR_ERR(p50->leds_pdev); + dev_err(&pdev->dev, "Could not register leds-gpio: %d\n", ret); + goto err_leds; + } + + key_info.fwnode = software_node_fwnode(&gpio_keys_node); + p50->keys_pdev = platform_device_register_full(&key_info); + if (IS_ERR(p50->keys_pdev)) { + ret = PTR_ERR(p50->keys_pdev); + dev_err(&pdev->dev, "Could not register gpio-keys-polled: %d\n", ret); + goto err_keys; + } + + return 0; + +err_keys: + platform_device_unregister(p50->leds_pdev); +err_leds: + software_node_unregister_node_group(p50_swnodes); + + return ret; +} + +static void p50_gpio_remove(struct platform_device *pdev) +{ + struct p50_gpio *p50 = platform_get_drvdata(pdev); + + platform_device_unregister(p50->keys_pdev); + platform_device_unregister(p50->leds_pdev); + + software_node_unregister_node_group(p50_swnodes); +} + +static struct platform_driver p50_gpio_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = p50_gpio_probe, + .remove = p50_gpio_remove, +}; + +/* Board setup */ +static const struct dmi_system_id dmi_ids[] __initconst = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Barco"), + DMI_EXACT_MATCH(DMI_PRODUCT_FAMILY, "P50") + }, + }, + {} +}; +MODULE_DEVICE_TABLE(dmi, dmi_ids); + +static int __init p50_module_init(void) +{ + struct resource res = DEFINE_RES_IO(P50_GPIO_IO_PORT_BASE, P50_PORT_CMD + 1); + struct platform_device_info pdevinfo = { + .name = DRIVER_NAME, + .id = PLATFORM_DEVID_NONE, + .res = &res, + .num_res = 1, + .swnode = &gpiochip_node, + }; + int ret; + + if (!dmi_first_match(dmi_ids)) + return -ENODEV; + + ret = platform_driver_register(&p50_gpio_driver); + if (ret) + return ret; + + gpio_pdev = platform_device_register_full(&pdevinfo); + if (IS_ERR(gpio_pdev)) { + pr_err("failed registering %s: %ld\n", DRIVER_NAME, PTR_ERR(gpio_pdev)); + platform_driver_unregister(&p50_gpio_driver); + return PTR_ERR(gpio_pdev); + } + + return 0; +} + +static void __exit p50_module_exit(void) +{ + platform_device_unregister(gpio_pdev); + platform_driver_unregister(&p50_gpio_driver); +} + +module_init(p50_module_init); +module_exit(p50_module_exit); + +MODULE_AUTHOR("Santosh Kumar Yadav, Barco NV "); +MODULE_DESCRIPTION("Barco P50 identify GPIOs driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/bitland-mifs-wmi.c b/drivers/platform/x86/bitland-mifs-wmi.c new file mode 100644 index 000000000..3a3731845 --- /dev/null +++ b/drivers/platform/x86/bitland-mifs-wmi.c @@ -0,0 +1,845 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Linux driver for Bitland notebooks. + * + * Copyright (C) 2026 2 Mingyou Chen + */ + +#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 +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "bitland-mifs-wmi" +#define BITLAND_MIFS_GUID "B60BFB48-3E5B-49E4-A0E9-8CFFE1B3434B" +#define BITLAND_EVENT_GUID "46C93E13-EE9B-4262-8488-563BCA757FEF" + +enum bitland_mifs_operation { + WMI_METHOD_GET = 250, + WMI_METHOD_SET = 251, +}; + +enum bitland_mifs_function { + WMI_FN_SYSTEM_PER_MODE = 8, + WMI_FN_GPU_MODE = 9, + WMI_FN_KBD_TYPE = 10, + WMI_FN_FN_LOCK = 11, + WMI_FN_TP_LOCK = 12, + WMI_FN_FAN_SPEEDS = 13, + WMI_FN_RGB_KB_MODE = 16, + WMI_FN_RGB_KB_COLOR = 17, + WMI_FN_RGB_KB_BRIGHTNESS = 18, + WMI_FN_SYSTEM_AC_TYPE = 19, + WMI_FN_MAX_FAN_SWITCH = 20, + WMI_FN_MAX_FAN_SPEED = 21, + WMI_FN_CPU_THERMOMETER = 22, + WMI_FN_CPU_POWER = 23, +}; + +enum bitland_system_ac_mode { + WMI_SYSTEM_AC_TYPEC = 1, + /* Unknown type, this is unused in the original driver */ + WMI_SYSTEM_AC_CIRCULARHOLE = 2, +}; + +enum bitland_mifs_power_profile { + WMI_PP_BALANCED = 0, + WMI_PP_PERFORMANCE = 1, + WMI_PP_QUIET = 2, + WMI_PP_FULL_SPEED = 3, +}; + +enum bitland_mifs_event_id { + WMI_EVENT_RESERVED_1 = 1, + WMI_EVENT_RESERVED_2 = 2, + WMI_EVENT_RESERVED_3 = 3, + WMI_EVENT_AIRPLANE_MODE = 4, + WMI_EVENT_KBD_BRIGHTNESS = 5, + WMI_EVENT_TOUCHPAD_STATE = 6, + WMI_EVENT_FNLOCK_STATE = 7, + WMI_EVENT_KBD_MODE = 8, + WMI_EVENT_CAPSLOCK_STATE = 9, + WMI_EVENT_CALCULATOR_START = 11, + WMI_EVENT_BROWSER_START = 12, + WMI_EVENT_NUMLOCK_STATE = 13, + WMI_EVENT_SCROLLLOCK_STATE = 14, + WMI_EVENT_PERFORMANCE_PLAN = 15, + WMI_EVENT_FN_J = 16, + WMI_EVENT_FN_F = 17, + WMI_EVENT_FN_0 = 18, + WMI_EVENT_FN_1 = 19, + WMI_EVENT_FN_2 = 20, + WMI_EVENT_FN_3 = 21, + WMI_EVENT_FN_4 = 22, + WMI_EVENT_FN_5 = 24, + WMI_EVENT_REFRESH_RATE = 25, + WMI_EVENT_CPU_FAN_SPEED = 26, + WMI_EVENT_GPU_FAN_SPEED = 32, + WMI_EVENT_WIN_KEY_LOCK = 33, + WMI_EVENT_RESERVED_23 = 34, + WMI_EVENT_OPEN_APP = 35, +}; + +enum bitland_mifs_event_type { + WMI_EVENT_TYPE_HOTKEY = 1, +}; + +enum bitland_wmi_device_type { + BITLAND_WMI_CONTROL = 0, + BITLAND_WMI_EVENT = 1, +}; + +struct bitland_mifs_input { + u8 reserved1; + u8 operation; + u8 reserved2; + u8 function; + u8 payload[28]; +} __packed; + +struct bitland_mifs_output { + u8 reserved1; + u8 operation; + u8 reserved2; + u8 function; + u8 data[28]; +} __packed; + +struct bitland_mifs_event { + u8 event_type; + u8 event_id; + u8 value_low; /* For most events, this is the value */ + u8 value_high; /* For fan speed events, combined with value_low */ + u8 reserved[4]; +} __packed; + +static BLOCKING_NOTIFIER_HEAD(bitland_notifier_list); + +enum bitland_notifier_actions { + BITLAND_NOTIFY_KBD_BRIGHTNESS, + BITLAND_NOTIFY_PLATFORM_PROFILE, + BITLAND_NOTIFY_HWMON, +}; + +struct bitland_fan_notify_data { + int channel; /* 0 = CPU, 1 = GPU */ + u16 speed; +}; + +struct bitland_mifs_wmi_data { + struct wmi_device *wdev; + struct mutex lock; /* Protects WMI calls */ + struct led_classdev kbd_led; + struct notifier_block notifier; + struct input_dev *input_dev; + struct device *hwmon_dev; + struct device *pp_dev; + enum platform_profile_option saved_profile; +}; + +static int bitland_mifs_wmi_call(struct bitland_mifs_wmi_data *data, + const struct bitland_mifs_input *input, + struct bitland_mifs_output *output) +{ + struct wmi_buffer in_buf = { .length = sizeof(*input), .data = (void *)input }; + struct wmi_buffer out_buf = { 0 }; + int ret; + + guard(mutex)(&data->lock); + + if (!output) + return wmidev_invoke_procedure(data->wdev, 0, 1, &in_buf); + + ret = wmidev_invoke_method(data->wdev, 0, 1, &in_buf, &out_buf, sizeof(*output)); + if (ret) + return ret; + + memcpy(output, out_buf.data, sizeof(*output)); + kfree(out_buf.data); + + return 0; +} + +static int laptop_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_SYSTEM_PER_MODE, + }; + struct bitland_mifs_output result; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &result); + if (ret) + return ret; + + switch (result.data[0]) { + case WMI_PP_BALANCED: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case WMI_PP_PERFORMANCE: + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + break; + case WMI_PP_QUIET: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case WMI_PP_FULL_SPEED: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + default: + return -EINVAL; + } + return 0; +} + +static int bitland_check_performance_capability(struct bitland_mifs_wmi_data *data) +{ + struct bitland_mifs_input input = { + .operation = WMI_METHOD_GET, + .function = WMI_FN_SYSTEM_AC_TYPE, + }; + struct bitland_mifs_output output; + int ret; + + /* Full-speed/performance mode requires DC power (not USB-C) */ + if (!power_supply_is_system_supplied()) + return -EOPNOTSUPP; + + ret = bitland_mifs_wmi_call(data, &input, &output); + if (ret) + return ret; + + if (output.data[0] != WMI_SYSTEM_AC_CIRCULARHOLE) + return -EOPNOTSUPP; + + return 0; +} + +static int laptop_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_SYSTEM_PER_MODE, + }; + int ret; + u8 val; + + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + val = WMI_PP_QUIET; + break; + case PLATFORM_PROFILE_BALANCED: + val = WMI_PP_BALANCED; + break; + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + ret = bitland_check_performance_capability(data); + if (ret) + return ret; + val = WMI_PP_PERFORMANCE; + break; + case PLATFORM_PROFILE_PERFORMANCE: + ret = bitland_check_performance_capability(data); + if (ret) + return ret; + val = WMI_PP_FULL_SPEED; + break; + default: + return -EOPNOTSUPP; + } + + input.payload[0] = val; + + return bitland_mifs_wmi_call(data, &input, NULL); +} + +static int 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 bitland_mifs_wmi_suspend(struct device *dev) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + enum platform_profile_option profile; + int ret; + + /* Skip event device */ + if (!data->pp_dev) + return 0; + + ret = laptop_profile_get(data->pp_dev, &profile); + if (ret == 0) + data->saved_profile = profile; + + return ret; +} + +static int bitland_mifs_wmi_resume(struct device *dev) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + + /* Skip event device */ + if (!data->pp_dev) + return 0; + + dev_dbg(dev, "Resuming, restoring profile %d\n", data->saved_profile); + return laptop_profile_set(dev, data->saved_profile); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(bitland_mifs_wmi_pm_ops, + bitland_mifs_wmi_suspend, + bitland_mifs_wmi_resume); + +static const struct platform_profile_ops laptop_profile_ops = { + .probe = platform_profile_probe, + .profile_get = laptop_profile_get, + .profile_set = laptop_profile_set, +}; + +static const char *const fan_labels[] = { + "CPU", /* 0 */ + "GPU", /* 1 */ + "SYS", /* 2 */ +}; + +static int laptop_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + }; + struct bitland_mifs_output res; + int ret; + + switch (type) { + case hwmon_temp: + input.function = WMI_FN_CPU_THERMOMETER; + ret = bitland_mifs_wmi_call(data, &input, &res); + if (!ret) + *val = res.data[0] * MILLIDEGREE_PER_DEGREE; + return ret; + case hwmon_fan: + input.function = WMI_FN_FAN_SPEEDS; + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + switch (channel) { + case 0: /* CPU */ + *val = get_unaligned_le16(&res.data[0]); + return 0; + case 1: /* GPU */ + *val = get_unaligned_le16(&res.data[2]); + return 0; + case 2: /* SYS */ + *val = get_unaligned_le16(&res.data[6]); + return 0; + default: + return -EINVAL; + } + default: + return -EINVAL; + } +} + +static int laptop_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + if (type == hwmon_fan && attr == hwmon_fan_label) { + if (channel >= 0 && channel < ARRAY_SIZE(fan_labels)) { + *str = fan_labels[channel]; + return 0; + } + } + return -EINVAL; +} + +static const struct hwmon_channel_info *laptop_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + NULL +}; + +static const struct hwmon_ops laptop_hwmon_ops = { + .visible = 0444, + .read = laptop_hwmon_read, + .read_string = laptop_hwmon_read_string, +}; + +static const struct hwmon_chip_info laptop_chip_info = { + .ops = &laptop_hwmon_ops, + .info = laptop_hwmon_info, +}; + +static int laptop_kbd_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct bitland_mifs_wmi_data *data = + container_of(led_cdev, struct bitland_mifs_wmi_data, kbd_led); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_RGB_KB_BRIGHTNESS, + }; + + input.payload[0] = (u8)value; + + return bitland_mifs_wmi_call(data, &input, NULL); +} + +static enum led_brightness laptop_kbd_led_get(struct led_classdev *led_cdev) +{ + struct bitland_mifs_wmi_data *data = + container_of(led_cdev, struct bitland_mifs_wmi_data, kbd_led); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_RGB_KB_BRIGHTNESS, + }; + struct bitland_mifs_output res; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + return res.data[0]; +} + +static const char *const gpu_mode_strings[] = { + "hybrid", + "discrete", + "uma", +}; + +/* GPU Mode: 0:Hybrid, 1:Discrete, 2:UMA */ +static ssize_t gpu_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_GPU_MODE, + }; + struct bitland_mifs_output res; + u8 mode_val; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + mode_val = res.data[0]; + if (mode_val >= ARRAY_SIZE(gpu_mode_strings)) + return -EPROTO; + + return sysfs_emit(buf, "%s\n", gpu_mode_strings[mode_val]); +} + +static ssize_t gpu_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_GPU_MODE, + }; + int val; + int ret; + + val = sysfs_match_string(gpu_mode_strings, buf); + if (val < 0) + return -EINVAL; + + input.payload[0] = (u8)val; + + ret = bitland_mifs_wmi_call(data, &input, NULL); + if (ret) + return ret; + + return count; +} + +static const char *const kb_mode_strings[] = { + "off", /* 0 */ + "cyclic", /* 1 */ + "fixed", /* 2 */ + "custom", /* 3 */ +}; + +static ssize_t kb_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_GET, + .reserved2 = 0, + .function = WMI_FN_RGB_KB_MODE, + }; + struct bitland_mifs_output res; + u8 mode_val; + int ret; + + ret = bitland_mifs_wmi_call(data, &input, &res); + if (ret) + return ret; + + mode_val = res.data[0]; + if (mode_val >= ARRAY_SIZE(kb_mode_strings)) + return -EPROTO; + + return sysfs_emit(buf, "%s\n", kb_mode_strings[mode_val]); +} + +static ssize_t kb_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_RGB_KB_MODE, + }; + // the wmi value (0, 1, 2 or 3) + int val; + int ret; + + val = sysfs_match_string(kb_mode_strings, buf); + if (val < 0) + return -EINVAL; + + input.payload[0] = (u8)val; + + ret = bitland_mifs_wmi_call(data, &input, NULL); + if (ret) + return ret; + + return count; +} + +/* Fan Boost: 0:Normal, 1:Max Speed */ +static ssize_t fan_boost_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev); + struct bitland_mifs_input input = { + .reserved1 = 0, + .operation = WMI_METHOD_SET, + .reserved2 = 0, + .function = WMI_FN_MAX_FAN_SWITCH, + }; + bool val; + int ret; + + if (kstrtobool(buf, &val)) + return -EINVAL; + + input.payload[0] = 0; /* CPU/GPU Fan */ + input.payload[1] = val; + + ret = bitland_mifs_wmi_call(data, &input, NULL); + if (ret) + return ret; + + return count; +} + +static const DEVICE_ATTR_RW(gpu_mode); +static const DEVICE_ATTR_RW(kb_mode); +static const DEVICE_ATTR_WO(fan_boost); + +static const struct attribute *const laptop_attrs[] = { + &dev_attr_gpu_mode.attr, + &dev_attr_kb_mode.attr, + &dev_attr_fan_boost.attr, + NULL, +}; +ATTRIBUTE_GROUPS(laptop); + +static const struct key_entry bitland_mifs_wmi_keymap[] = { + { KE_KEY, WMI_EVENT_OPEN_APP, { KEY_PROG1 } }, + { KE_KEY, WMI_EVENT_CALCULATOR_START, { KEY_CALC } }, + { KE_KEY, WMI_EVENT_BROWSER_START, { KEY_WWW } }, + { KE_IGNORE, WMI_EVENT_FN_J, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_F, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_0, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_1, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_2, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_3, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_4, { KEY_RESERVED } }, + { KE_IGNORE, WMI_EVENT_FN_5, { KEY_RESERVED } }, + { KE_END, 0 } +}; + +static void bitland_notifier_unregister(void *data) +{ + struct notifier_block *nb = data; + + blocking_notifier_chain_unregister(&bitland_notifier_list, nb); +} + +static int bitland_notifier_callback(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct bitland_mifs_wmi_data *data_ctx = + container_of(nb, struct bitland_mifs_wmi_data, notifier); + struct bitland_fan_notify_data *fan_info; + u8 *brightness; + + switch (action) { + case BITLAND_NOTIFY_KBD_BRIGHTNESS: + brightness = data; + led_classdev_notify_brightness_hw_changed(&data_ctx->kbd_led, + *brightness); + break; + case BITLAND_NOTIFY_PLATFORM_PROFILE: + platform_profile_notify(data_ctx->pp_dev); + break; + case BITLAND_NOTIFY_HWMON: + fan_info = data; + + hwmon_notify_event(data_ctx->hwmon_dev, hwmon_fan, + hwmon_fan_input, fan_info->channel); + break; + } + + return NOTIFY_OK; +} + +static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct bitland_mifs_wmi_data *drv_data; + enum bitland_wmi_device_type dev_type = + (enum bitland_wmi_device_type)(unsigned long)context; + struct led_init_data init_data = { + .devicename = DRV_NAME, + .default_label = ":" LED_FUNCTION_KBD_BACKLIGHT, + .devname_mandatory = true, + }; + int ret; + + drv_data = devm_kzalloc(&wdev->dev, sizeof(*drv_data), GFP_KERNEL); + if (!drv_data) + return -ENOMEM; + + drv_data->wdev = wdev; + + ret = devm_mutex_init(&wdev->dev, &drv_data->lock); + if (ret) + return ret; + + dev_set_drvdata(&wdev->dev, drv_data); + + if (dev_type == BITLAND_WMI_EVENT) { + /* Register input device for hotkeys */ + drv_data->input_dev = devm_input_allocate_device(&wdev->dev); + if (!drv_data->input_dev) + return -ENOMEM; + + drv_data->input_dev->name = "Bitland MIFS WMI hotkeys"; + drv_data->input_dev->phys = "wmi/input0"; + drv_data->input_dev->id.bustype = BUS_HOST; + drv_data->input_dev->dev.parent = &wdev->dev; + + ret = sparse_keymap_setup(drv_data->input_dev, + bitland_mifs_wmi_keymap, NULL); + if (ret) + return ret; + + return input_register_device(drv_data->input_dev); + } + + /* Register platform profile */ + drv_data->pp_dev = devm_platform_profile_register(&wdev->dev, DRV_NAME, drv_data, + &laptop_profile_ops); + if (IS_ERR(drv_data->pp_dev)) + return PTR_ERR(drv_data->pp_dev); + + /* Register hwmon */ + drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, + "bitland_mifs", + drv_data, + &laptop_chip_info, + NULL); + if (IS_ERR(drv_data->hwmon_dev)) + return PTR_ERR(drv_data->hwmon_dev); + + /* Register keyboard LED */ + drv_data->kbd_led.max_brightness = 3; + drv_data->kbd_led.brightness_set_blocking = laptop_kbd_led_set; + drv_data->kbd_led.brightness_get = laptop_kbd_led_get; + drv_data->kbd_led.brightness = laptop_kbd_led_get(&drv_data->kbd_led); + drv_data->kbd_led.flags = LED_CORE_SUSPENDRESUME | + LED_BRIGHT_HW_CHANGED | + LED_REJECT_NAME_CONFLICT; + ret = devm_led_classdev_register_ext(&wdev->dev, &drv_data->kbd_led, &init_data); + if (ret) + return ret; + + drv_data->notifier.notifier_call = bitland_notifier_callback; + ret = blocking_notifier_chain_register(&bitland_notifier_list, &drv_data->notifier); + if (ret) + return ret; + + return devm_add_action_or_reset(&wdev->dev, + bitland_notifier_unregister, + &drv_data->notifier); +} + +static void bitland_mifs_wmi_notify(struct wmi_device *wdev, + const struct wmi_buffer *buffer) +{ + struct bitland_mifs_wmi_data *data = dev_get_drvdata(&wdev->dev); + const struct bitland_mifs_event *event = buffer->data; + struct bitland_fan_notify_data fan_data; + u8 brightness; + + /* Validate event type */ + if (event->event_type != WMI_EVENT_TYPE_HOTKEY) + return; + + dev_dbg(&wdev->dev, + "WMI event: id=0x%02x value_low=0x%02x value_high=0x%02x\n", + event->event_id, event->value_low, event->value_high); + + switch (event->event_id) { + case WMI_EVENT_KBD_BRIGHTNESS: + brightness = event->value_low; + blocking_notifier_call_chain(&bitland_notifier_list, + BITLAND_NOTIFY_KBD_BRIGHTNESS, + &brightness); + break; + + case WMI_EVENT_PERFORMANCE_PLAN: + blocking_notifier_call_chain(&bitland_notifier_list, + BITLAND_NOTIFY_PLATFORM_PROFILE, + NULL); + break; + + case WMI_EVENT_OPEN_APP: + case WMI_EVENT_CALCULATOR_START: + case WMI_EVENT_BROWSER_START: { + guard(mutex)(&data->lock); + if (!sparse_keymap_report_event(data->input_dev, + event->event_id, 1, true)) + dev_warn(&wdev->dev, "Unknown key pressed: 0x%02x\n", + event->event_id); + break; + } + + /* + * The device has 3 fans (CPU, GPU, SYS), + * but there are only the CPU and GPU fan has events + */ + case WMI_EVENT_CPU_FAN_SPEED: + case WMI_EVENT_GPU_FAN_SPEED: + if (event->event_id == WMI_EVENT_CPU_FAN_SPEED) + fan_data.channel = 0; + else + fan_data.channel = 1; + + /* Fan speed is 16-bit value (value_low is LSB, value_high is MSB) */ + fan_data.speed = (event->value_high << 8) | event->value_low; + blocking_notifier_call_chain(&bitland_notifier_list, + BITLAND_NOTIFY_HWMON, + &fan_data); + break; + + case WMI_EVENT_AIRPLANE_MODE: + case WMI_EVENT_TOUCHPAD_STATE: + case WMI_EVENT_FNLOCK_STATE: + case WMI_EVENT_KBD_MODE: + case WMI_EVENT_CAPSLOCK_STATE: + case WMI_EVENT_NUMLOCK_STATE: + case WMI_EVENT_SCROLLLOCK_STATE: + case WMI_EVENT_REFRESH_RATE: + case WMI_EVENT_WIN_KEY_LOCK: + /* These events are informational or handled by firmware */ + dev_dbg(&wdev->dev, "State change event: id=%d value=%d\n", + event->event_id, event->value_low); + break; + + default: + dev_dbg(&wdev->dev, "Unknown event: id=0x%02x value=0x%02x\n", + event->event_id, event->value_low); + break; + } +} + +static const struct wmi_device_id bitland_mifs_wmi_id_table[] = { + { BITLAND_MIFS_GUID, (void *)BITLAND_WMI_CONTROL }, + { BITLAND_EVENT_GUID, (void *)BITLAND_WMI_EVENT }, + {} +}; +MODULE_DEVICE_TABLE(wmi, bitland_mifs_wmi_id_table); + +static struct wmi_driver bitland_mifs_wmi_driver = { + .no_singleton = true, + .driver = { + .name = DRV_NAME, + .dev_groups = laptop_groups, + .pm = pm_sleep_ptr(&bitland_mifs_wmi_pm_ops), + }, + .id_table = bitland_mifs_wmi_id_table, + .min_event_size = sizeof(struct bitland_mifs_event), + .probe = bitland_mifs_wmi_probe, + .notify_new = bitland_mifs_wmi_notify, +}; + +module_wmi_driver(bitland_mifs_wmi_driver); + +MODULE_AUTHOR("Mingyou Chen "); +MODULE_DESCRIPTION("Bitland MIFS (MiInterface) WMI driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/classmate-laptop.c b/drivers/platform/x86/classmate-laptop.c new file mode 100644 index 000000000..058908157 --- /dev/null +++ b/drivers/platform/x86/classmate-laptop.c @@ -0,0 +1,1233 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2009 Thadeu Lima de Souza Cascardo + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct cmpc_accel { + int sensitivity; + int g_select; + int inputdev_state; +}; + +#define CMPC_ACCEL_DEV_STATE_CLOSED 0 +#define CMPC_ACCEL_DEV_STATE_OPEN 1 + +#define CMPC_ACCEL_SENSITIVITY_DEFAULT 5 +#define CMPC_ACCEL_G_SELECT_DEFAULT 0 + +#define CMPC_ACCEL_HID "ACCE0000" +#define CMPC_ACCEL_HID_V4 "ACCE0001" +#define CMPC_TABLET_HID "TBLT0000" +#define CMPC_IPML_HID "IPML200" +#define CMPC_KEYS_HID "FNBT0000" + +/* + * Generic input device code. + */ + +typedef void (*input_device_init)(struct input_dev *dev); + +static int cmpc_add_notify_device(struct device *dev, char *name, + input_device_init idev_init) +{ + struct input_dev *inputdev; + int error; + + inputdev = input_allocate_device(); + if (!inputdev) + return -ENOMEM; + inputdev->name = name; + inputdev->dev.parent = dev; + idev_init(inputdev); + error = input_register_device(inputdev); + if (error) { + input_free_device(inputdev); + return error; + } + dev_set_drvdata(dev, inputdev); + return 0; +} + +static void cmpc_remove_notify_device(struct device *dev) +{ + input_unregister_device(dev_get_drvdata(dev)); +} + +/* + * Accelerometer code for Classmate V4 + */ +static acpi_status cmpc_start_accel_v4(acpi_handle handle) +{ + union acpi_object param[4]; + struct acpi_object_list input; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x3; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = 0; + param[2].type = ACPI_TYPE_INTEGER; + param[2].integer.value = 0; + param[3].type = ACPI_TYPE_INTEGER; + param[3].integer.value = 0; + input.count = 4; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, NULL); + return status; +} + +static acpi_status cmpc_stop_accel_v4(acpi_handle handle) +{ + union acpi_object param[4]; + struct acpi_object_list input; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x4; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = 0; + param[2].type = ACPI_TYPE_INTEGER; + param[2].integer.value = 0; + param[3].type = ACPI_TYPE_INTEGER; + param[3].integer.value = 0; + input.count = 4; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, NULL); + return status; +} + +static acpi_status cmpc_accel_set_sensitivity_v4(acpi_handle handle, int val) +{ + union acpi_object param[4]; + struct acpi_object_list input; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x02; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = val; + param[2].type = ACPI_TYPE_INTEGER; + param[2].integer.value = 0; + param[3].type = ACPI_TYPE_INTEGER; + param[3].integer.value = 0; + input.count = 4; + input.pointer = param; + return acpi_evaluate_object(handle, "ACMD", &input, NULL); +} + +static acpi_status cmpc_accel_set_g_select_v4(acpi_handle handle, int val) +{ + union acpi_object param[4]; + struct acpi_object_list input; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x05; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = val; + param[2].type = ACPI_TYPE_INTEGER; + param[2].integer.value = 0; + param[3].type = ACPI_TYPE_INTEGER; + param[3].integer.value = 0; + input.count = 4; + input.pointer = param; + return acpi_evaluate_object(handle, "ACMD", &input, NULL); +} + +static acpi_status cmpc_get_accel_v4(acpi_handle handle, + int16_t *x, + int16_t *y, + int16_t *z) +{ + union acpi_object param[4]; + struct acpi_object_list input; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + int16_t *locs; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x01; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = 0; + param[2].type = ACPI_TYPE_INTEGER; + param[2].integer.value = 0; + param[3].type = ACPI_TYPE_INTEGER; + param[3].integer.value = 0; + input.count = 4; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, &output); + if (ACPI_SUCCESS(status)) { + union acpi_object *obj; + obj = output.pointer; + locs = (int16_t *) obj->buffer.pointer; + *x = locs[0]; + *y = locs[1]; + *z = locs[2]; + kfree(output.pointer); + } + return status; +} + +static void cmpc_accel_handler_v4(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + + if (event == 0x81) { + int16_t x, y, z; + acpi_status status; + + status = cmpc_get_accel_v4(ACPI_HANDLE(dev), &x, &y, &z); + if (ACPI_SUCCESS(status)) { + struct input_dev *inputdev = dev_get_drvdata(dev); + + input_report_abs(inputdev, ABS_X, x); + input_report_abs(inputdev, ABS_Y, y); + input_report_abs(inputdev, ABS_Z, z); + input_sync(inputdev); + } + } +} + +static ssize_t cmpc_accel_sensitivity_show_v4(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + return sysfs_emit(buf, "%d\n", accel->sensitivity); +} + +static ssize_t cmpc_accel_sensitivity_store_v4(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + unsigned long sensitivity; + int r; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + r = kstrtoul(buf, 0, &sensitivity); + if (r) + return r; + + /* sensitivity must be between 1 and 127 */ + if (sensitivity < 1 || sensitivity > 127) + return -EINVAL; + + accel->sensitivity = sensitivity; + cmpc_accel_set_sensitivity_v4(acpi->handle, sensitivity); + + return strnlen(buf, count); +} + +static struct device_attribute cmpc_accel_sensitivity_attr_v4 = { + .attr = { .name = "sensitivity", .mode = 0660 }, + .show = cmpc_accel_sensitivity_show_v4, + .store = cmpc_accel_sensitivity_store_v4 +}; + +static ssize_t cmpc_accel_g_select_show_v4(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + return sysfs_emit(buf, "%d\n", accel->g_select); +} + +static ssize_t cmpc_accel_g_select_store_v4(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + unsigned long g_select; + int r; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + r = kstrtoul(buf, 0, &g_select); + if (r) + return r; + + /* 0 means 1.5g, 1 means 6g, everything else is wrong */ + if (g_select != 0 && g_select != 1) + return -EINVAL; + + accel->g_select = g_select; + cmpc_accel_set_g_select_v4(acpi->handle, g_select); + + return strnlen(buf, count); +} + +static struct device_attribute cmpc_accel_g_select_attr_v4 = { + .attr = { .name = "g_select", .mode = 0660 }, + .show = cmpc_accel_g_select_show_v4, + .store = cmpc_accel_g_select_store_v4 +}; + +static int cmpc_accel_open_v4(struct input_dev *input) +{ + acpi_handle handle = ACPI_HANDLE(input->dev.parent); + struct cmpc_accel *accel; + + accel = dev_get_drvdata(&input->dev); + if (!accel) + return -ENXIO; + + cmpc_accel_set_sensitivity_v4(handle, accel->sensitivity); + cmpc_accel_set_g_select_v4(handle, accel->g_select); + + if (ACPI_SUCCESS(cmpc_start_accel_v4(handle))) { + accel->inputdev_state = CMPC_ACCEL_DEV_STATE_OPEN; + return 0; + } + return -EIO; +} + +static void cmpc_accel_close_v4(struct input_dev *input) +{ + struct cmpc_accel *accel; + + accel = dev_get_drvdata(&input->dev); + + cmpc_stop_accel_v4(ACPI_HANDLE(input->dev.parent)); + accel->inputdev_state = CMPC_ACCEL_DEV_STATE_CLOSED; +} + +static void cmpc_accel_idev_init_v4(struct input_dev *inputdev) +{ + set_bit(EV_ABS, inputdev->evbit); + input_set_abs_params(inputdev, ABS_X, -255, 255, 16, 0); + input_set_abs_params(inputdev, ABS_Y, -255, 255, 16, 0); + input_set_abs_params(inputdev, ABS_Z, -255, 255, 16, 0); + inputdev->open = cmpc_accel_open_v4; + inputdev->close = cmpc_accel_close_v4; +} + +#ifdef CONFIG_PM_SLEEP +static int cmpc_accel_suspend_v4(struct device *dev) +{ + struct input_dev *inputdev; + struct cmpc_accel *accel; + + inputdev = dev_get_drvdata(dev); + accel = dev_get_drvdata(&inputdev->dev); + + if (accel->inputdev_state == CMPC_ACCEL_DEV_STATE_OPEN) + return cmpc_stop_accel_v4(ACPI_HANDLE(dev)); + + return 0; +} + +static int cmpc_accel_resume_v4(struct device *dev) +{ + struct input_dev *inputdev; + struct cmpc_accel *accel; + + inputdev = dev_get_drvdata(dev); + accel = dev_get_drvdata(&inputdev->dev); + + if (accel->inputdev_state == CMPC_ACCEL_DEV_STATE_OPEN) { + acpi_handle handle = ACPI_HANDLE(dev); + + cmpc_accel_set_sensitivity_v4(handle, accel->sensitivity); + cmpc_accel_set_g_select_v4(handle, accel->g_select); + + if (ACPI_FAILURE(cmpc_start_accel_v4(handle))) + return -EIO; + } + + return 0; +} +#endif + +static int cmpc_accel_probe_v4(struct platform_device *pdev) +{ + int error; + struct input_dev *inputdev; + struct cmpc_accel *accel; + struct acpi_device *acpi; + + acpi = ACPI_COMPANION(&pdev->dev); + if (!acpi) + return -ENODEV; + + accel = devm_kzalloc(&pdev->dev, sizeof(*accel), GFP_KERNEL); + if (!accel) + return -ENOMEM; + + accel->inputdev_state = CMPC_ACCEL_DEV_STATE_CLOSED; + + error = cmpc_add_notify_device(&pdev->dev, "cmpc_accel_v4", cmpc_accel_idev_init_v4); + if (error) + return error; + + inputdev = dev_get_drvdata(&pdev->dev); + dev_set_drvdata(&acpi->dev, inputdev); + + accel->sensitivity = CMPC_ACCEL_SENSITIVITY_DEFAULT; + cmpc_accel_set_sensitivity_v4(acpi->handle, accel->sensitivity); + + error = device_create_file(&acpi->dev, &cmpc_accel_sensitivity_attr_v4); + if (error) + goto failed_sensitivity; + + accel->g_select = CMPC_ACCEL_G_SELECT_DEFAULT; + cmpc_accel_set_g_select_v4(acpi->handle, accel->g_select); + + error = device_create_file(&acpi->dev, &cmpc_accel_g_select_attr_v4); + if (error) + goto failed_g_select; + + error = acpi_dev_install_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_accel_handler_v4, &pdev->dev); + if (error) + goto failed_notify_handler; + + dev_set_drvdata(&inputdev->dev, accel); + + return 0; + +failed_notify_handler: + device_remove_file(&acpi->dev, &cmpc_accel_g_select_attr_v4); +failed_g_select: + device_remove_file(&acpi->dev, &cmpc_accel_sensitivity_attr_v4); +failed_sensitivity: + dev_set_drvdata(&acpi->dev, NULL); + cmpc_remove_notify_device(&pdev->dev); + return error; +} + +static void cmpc_accel_remove_v4(struct platform_device *pdev) +{ + struct acpi_device *acpi = ACPI_COMPANION(&pdev->dev); + + acpi_dev_remove_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_accel_handler_v4); + device_remove_file(&acpi->dev, &cmpc_accel_g_select_attr_v4); + device_remove_file(&acpi->dev, &cmpc_accel_sensitivity_attr_v4); + dev_set_drvdata(&acpi->dev, NULL); + cmpc_remove_notify_device(&pdev->dev); +} + +static SIMPLE_DEV_PM_OPS(cmpc_accel_pm, cmpc_accel_suspend_v4, + cmpc_accel_resume_v4); + +static const struct acpi_device_id cmpc_accel_device_ids_v4[] = { + {CMPC_ACCEL_HID_V4, 0}, + {"", 0} +}; + +static struct platform_driver cmpc_accel_acpi_driver_v4 = { + .probe = cmpc_accel_probe_v4, + .remove = cmpc_accel_remove_v4, + .driver = { + .name = "cmpc_accel_v4", + .acpi_match_table = cmpc_accel_device_ids_v4, + .pm = &cmpc_accel_pm, + }, +}; + + +/* + * Accelerometer code for Classmate versions prior to V4 + */ +static acpi_status cmpc_start_accel(acpi_handle handle) +{ + union acpi_object param[2]; + struct acpi_object_list input; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x3; + param[1].type = ACPI_TYPE_INTEGER; + input.count = 2; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, NULL); + return status; +} + +static acpi_status cmpc_stop_accel(acpi_handle handle) +{ + union acpi_object param[2]; + struct acpi_object_list input; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x4; + param[1].type = ACPI_TYPE_INTEGER; + input.count = 2; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, NULL); + return status; +} + +static acpi_status cmpc_accel_set_sensitivity(acpi_handle handle, int val) +{ + union acpi_object param[2]; + struct acpi_object_list input; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x02; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = val; + input.count = 2; + input.pointer = param; + return acpi_evaluate_object(handle, "ACMD", &input, NULL); +} + +static acpi_status cmpc_get_accel(acpi_handle handle, + unsigned char *x, + unsigned char *y, + unsigned char *z) +{ + union acpi_object param[2]; + struct acpi_object_list input; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + unsigned char *locs; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x01; + param[1].type = ACPI_TYPE_INTEGER; + input.count = 2; + input.pointer = param; + status = acpi_evaluate_object(handle, "ACMD", &input, &output); + if (ACPI_SUCCESS(status)) { + union acpi_object *obj; + obj = output.pointer; + locs = obj->buffer.pointer; + *x = locs[0]; + *y = locs[1]; + *z = locs[2]; + kfree(output.pointer); + } + return status; +} + +static void cmpc_accel_handler(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + + if (event == 0x81) { + unsigned char x, y, z; + acpi_status status; + + status = cmpc_get_accel(ACPI_HANDLE(dev), &x, &y, &z); + if (ACPI_SUCCESS(status)) { + struct input_dev *inputdev = dev_get_drvdata(dev); + + input_report_abs(inputdev, ABS_X, x); + input_report_abs(inputdev, ABS_Y, y); + input_report_abs(inputdev, ABS_Z, z); + input_sync(inputdev); + } + } +} + +static ssize_t cmpc_accel_sensitivity_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + return sysfs_emit(buf, "%d\n", accel->sensitivity); +} + +static ssize_t cmpc_accel_sensitivity_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi; + struct input_dev *inputdev; + struct cmpc_accel *accel; + unsigned long sensitivity; + int r; + + acpi = to_acpi_device(dev); + inputdev = dev_get_drvdata(&acpi->dev); + if (!inputdev) + return -ENXIO; + + accel = dev_get_drvdata(&inputdev->dev); + if (!accel) + return -ENXIO; + + r = kstrtoul(buf, 0, &sensitivity); + if (r) + return r; + + accel->sensitivity = sensitivity; + cmpc_accel_set_sensitivity(acpi->handle, sensitivity); + + return strnlen(buf, count); +} + +static struct device_attribute cmpc_accel_sensitivity_attr = { + .attr = { .name = "sensitivity", .mode = 0660 }, + .show = cmpc_accel_sensitivity_show, + .store = cmpc_accel_sensitivity_store +}; + +static int cmpc_accel_open(struct input_dev *input) +{ + if (ACPI_SUCCESS(cmpc_start_accel(ACPI_HANDLE(input->dev.parent)))) + return 0; + return -EIO; +} + +static void cmpc_accel_close(struct input_dev *input) +{ + cmpc_stop_accel(ACPI_HANDLE(input->dev.parent)); +} + +static void cmpc_accel_idev_init(struct input_dev *inputdev) +{ + set_bit(EV_ABS, inputdev->evbit); + input_set_abs_params(inputdev, ABS_X, 0, 255, 8, 0); + input_set_abs_params(inputdev, ABS_Y, 0, 255, 8, 0); + input_set_abs_params(inputdev, ABS_Z, 0, 255, 8, 0); + inputdev->open = cmpc_accel_open; + inputdev->close = cmpc_accel_close; +} + +static int cmpc_accel_probe(struct platform_device *pdev) +{ + int error; + struct input_dev *inputdev; + struct cmpc_accel *accel; + struct acpi_device *acpi; + + acpi = ACPI_COMPANION(&pdev->dev); + if (!acpi) + return -ENODEV; + + accel = devm_kzalloc(&pdev->dev, sizeof(*accel), GFP_KERNEL); + if (!accel) + return -ENOMEM; + + error = cmpc_add_notify_device(&pdev->dev, "cmpc_accel", cmpc_accel_idev_init); + if (error) + return error; + + inputdev = dev_get_drvdata(&pdev->dev); + dev_set_drvdata(&acpi->dev, inputdev); + + accel->sensitivity = CMPC_ACCEL_SENSITIVITY_DEFAULT; + cmpc_accel_set_sensitivity(acpi->handle, accel->sensitivity); + + error = device_create_file(&acpi->dev, &cmpc_accel_sensitivity_attr); + if (error) + goto failed_file; + + error = acpi_dev_install_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_accel_handler, &pdev->dev); + if (error) + goto failed_notify_handler; + + dev_set_drvdata(&inputdev->dev, accel); + + return 0; + +failed_notify_handler: + device_remove_file(&acpi->dev, &cmpc_accel_sensitivity_attr); +failed_file: + dev_set_drvdata(&acpi->dev, NULL); + cmpc_remove_notify_device(&pdev->dev); + return error; +} + +static void cmpc_accel_remove(struct platform_device *pdev) +{ + struct acpi_device *acpi = ACPI_COMPANION(&pdev->dev); + + acpi_dev_remove_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_accel_handler); + device_remove_file(&acpi->dev, &cmpc_accel_sensitivity_attr); + dev_set_drvdata(&acpi->dev, NULL); + cmpc_remove_notify_device(&pdev->dev); +} + +static const struct acpi_device_id cmpc_accel_device_ids[] = { + {CMPC_ACCEL_HID, 0}, + {"", 0} +}; + +static struct platform_driver cmpc_accel_acpi_driver = { + .probe = cmpc_accel_probe, + .remove = cmpc_accel_remove, + .driver = { + .name = "cmpc_accel", + .acpi_match_table = cmpc_accel_device_ids, + }, +}; + + +/* + * Tablet mode code. + */ +static acpi_status cmpc_get_tablet(acpi_handle handle, + unsigned long long *value) +{ + union acpi_object param; + struct acpi_object_list input; + unsigned long long output; + acpi_status status; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = 0x01; + input.count = 1; + input.pointer = ¶m; + status = acpi_evaluate_integer(handle, "TCMD", &input, &output); + if (ACPI_SUCCESS(status)) + *value = output; + return status; +} + +static void cmpc_tablet_handler(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + unsigned long long val = 0; + struct input_dev *inputdev = dev_get_drvdata(dev); + + if (event == 0x81) { + if (ACPI_SUCCESS(cmpc_get_tablet(ACPI_HANDLE(dev), &val))) { + input_report_switch(inputdev, SW_TABLET_MODE, !val); + input_sync(inputdev); + } + } +} + +static void cmpc_tablet_idev_init(struct input_dev *inputdev) +{ + acpi_handle handle = ACPI_HANDLE(inputdev->dev.parent); + unsigned long long val = 0; + + set_bit(EV_SW, inputdev->evbit); + set_bit(SW_TABLET_MODE, inputdev->swbit); + + if (ACPI_SUCCESS(cmpc_get_tablet(handle, &val))) { + input_report_switch(inputdev, SW_TABLET_MODE, !val); + input_sync(inputdev); + } +} + +static int cmpc_tablet_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi; + int error; + + acpi = ACPI_COMPANION(&pdev->dev); + if (!acpi) + return -ENODEV; + + error = cmpc_add_notify_device(&pdev->dev, "cmpc_tablet", cmpc_tablet_idev_init); + if (error) + return error; + + error = acpi_dev_install_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_tablet_handler, &pdev->dev); + if (error) + cmpc_remove_notify_device(&pdev->dev); + + return error; +} + +static void cmpc_tablet_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, cmpc_tablet_handler); + cmpc_remove_notify_device(&pdev->dev); +} + +#ifdef CONFIG_PM_SLEEP +static int cmpc_tablet_resume(struct device *dev) +{ + struct input_dev *inputdev = dev_get_drvdata(dev); + + unsigned long long val = 0; + if (ACPI_SUCCESS(cmpc_get_tablet(ACPI_HANDLE(dev), &val))) { + input_report_switch(inputdev, SW_TABLET_MODE, !val); + input_sync(inputdev); + } + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(cmpc_tablet_pm, NULL, cmpc_tablet_resume); + +static const struct acpi_device_id cmpc_tablet_device_ids[] = { + {CMPC_TABLET_HID, 0}, + {"", 0} +}; + +static struct platform_driver cmpc_tablet_acpi_driver = { + .probe = cmpc_tablet_probe, + .remove = cmpc_tablet_remove, + .driver = { + .name = "cmpc_tablet", + .acpi_match_table = cmpc_tablet_device_ids, + .pm = &cmpc_tablet_pm, + }, +}; + + +/* + * Backlight code. + */ + +static acpi_status cmpc_get_brightness(acpi_handle handle, + unsigned long long *value) +{ + union acpi_object param; + struct acpi_object_list input; + unsigned long long output; + acpi_status status; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = 0xC0; + input.count = 1; + input.pointer = ¶m; + status = acpi_evaluate_integer(handle, "GRDI", &input, &output); + if (ACPI_SUCCESS(status)) + *value = output; + return status; +} + +static acpi_status cmpc_set_brightness(acpi_handle handle, + unsigned long long value) +{ + union acpi_object param[2]; + struct acpi_object_list input; + acpi_status status; + unsigned long long output; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0xC0; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = value; + input.count = 2; + input.pointer = param; + status = acpi_evaluate_integer(handle, "GWRI", &input, &output); + return status; +} + +static int cmpc_bl_get_brightness(struct backlight_device *bd) +{ + acpi_status status; + acpi_handle handle; + unsigned long long brightness; + + handle = bl_get_data(bd); + status = cmpc_get_brightness(handle, &brightness); + if (ACPI_SUCCESS(status)) + return brightness; + else + return -1; +} + +static int cmpc_bl_update_status(struct backlight_device *bd) +{ + acpi_status status; + acpi_handle handle; + + handle = bl_get_data(bd); + status = cmpc_set_brightness(handle, bd->props.brightness); + if (ACPI_SUCCESS(status)) + return 0; + else + return -1; +} + +static const struct backlight_ops cmpc_bl_ops = { + .get_brightness = cmpc_bl_get_brightness, + .update_status = cmpc_bl_update_status +}; + +/* + * RFKILL code. + */ + +static acpi_status cmpc_get_rfkill_wlan(acpi_handle handle, + unsigned long long *value) +{ + union acpi_object param; + struct acpi_object_list input; + unsigned long long output; + acpi_status status; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = 0xC1; + input.count = 1; + input.pointer = ¶m; + status = acpi_evaluate_integer(handle, "GRDI", &input, &output); + if (ACPI_SUCCESS(status)) + *value = output; + return status; +} + +static acpi_status cmpc_set_rfkill_wlan(acpi_handle handle, + unsigned long long value) +{ + union acpi_object param[2]; + struct acpi_object_list input; + acpi_status status; + unsigned long long output; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0xC1; + param[1].type = ACPI_TYPE_INTEGER; + param[1].integer.value = value; + input.count = 2; + input.pointer = param; + status = acpi_evaluate_integer(handle, "GWRI", &input, &output); + return status; +} + +static void cmpc_rfkill_query(struct rfkill *rfkill, void *data) +{ + acpi_status status; + acpi_handle handle; + unsigned long long state; + bool blocked; + + handle = data; + status = cmpc_get_rfkill_wlan(handle, &state); + if (ACPI_SUCCESS(status)) { + blocked = state & 1 ? false : true; + rfkill_set_sw_state(rfkill, blocked); + } +} + +static int cmpc_rfkill_block(void *data, bool blocked) +{ + acpi_status status; + acpi_handle handle; + unsigned long long state; + bool is_blocked; + + handle = data; + status = cmpc_get_rfkill_wlan(handle, &state); + if (ACPI_FAILURE(status)) + return -ENODEV; + /* Check if we really need to call cmpc_set_rfkill_wlan */ + is_blocked = state & 1 ? false : true; + if (is_blocked != blocked) { + state = blocked ? 0 : 1; + status = cmpc_set_rfkill_wlan(handle, state); + if (ACPI_FAILURE(status)) + return -ENODEV; + } + return 0; +} + +static const struct rfkill_ops cmpc_rfkill_ops = { + .query = cmpc_rfkill_query, + .set_block = cmpc_rfkill_block, +}; + +/* + * Common backlight and rfkill code. + */ + +struct ipml200_dev { + struct backlight_device *bd; + struct rfkill *rf; +}; + +static int cmpc_ipml_probe(struct platform_device *pdev) +{ + int retval; + struct ipml200_dev *ipml; + struct backlight_properties props; + acpi_handle handle; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + ipml = kmalloc_obj(*ipml); + if (ipml == NULL) + return -ENOMEM; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = 7; + ipml->bd = backlight_device_register("cmpc_bl", &pdev->dev, + handle, &cmpc_bl_ops, + &props); + if (IS_ERR(ipml->bd)) { + retval = PTR_ERR(ipml->bd); + goto out_bd; + } + + ipml->rf = rfkill_alloc("cmpc_rfkill", &pdev->dev, RFKILL_TYPE_WLAN, + &cmpc_rfkill_ops, handle); + /* + * If RFKILL is disabled, rfkill_alloc will return ERR_PTR(-ENODEV). + * This is OK, however, since all other uses of the device will not + * dereference it. + */ + if (ipml->rf) { + retval = rfkill_register(ipml->rf); + if (retval) { + rfkill_destroy(ipml->rf); + ipml->rf = NULL; + } + } + + platform_set_drvdata(pdev, ipml); + return 0; + +out_bd: + kfree(ipml); + return retval; +} + +static void cmpc_ipml_remove(struct platform_device *pdev) +{ + struct ipml200_dev *ipml; + + ipml = platform_get_drvdata(pdev); + + backlight_device_unregister(ipml->bd); + + if (ipml->rf) { + rfkill_unregister(ipml->rf); + rfkill_destroy(ipml->rf); + } + + kfree(ipml); +} + +static const struct acpi_device_id cmpc_ipml_device_ids[] = { + {CMPC_IPML_HID, 0}, + {"", 0} +}; + +static struct platform_driver cmpc_ipml_acpi_driver = { + .probe = cmpc_ipml_probe, + .remove = cmpc_ipml_remove, + .driver = { + .name = "cmpc", + .acpi_match_table = cmpc_ipml_device_ids, + }, +}; + + +/* + * Extra keys code. + */ +static int cmpc_keys_codes[] = { + KEY_UNKNOWN, + KEY_WLAN, + KEY_SWITCHVIDEOMODE, + KEY_BRIGHTNESSDOWN, + KEY_BRIGHTNESSUP, + KEY_VENDOR, + KEY_UNKNOWN, + KEY_CAMERA, + KEY_BACK, + KEY_FORWARD, + KEY_UNKNOWN, + KEY_WLAN, /* NL3: 0x8b (press), 0x9b (release) */ + KEY_MAX +}; + +static void cmpc_keys_handler(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct input_dev *inputdev; + int code = KEY_MAX; + + if ((event & 0x0F) < ARRAY_SIZE(cmpc_keys_codes)) + code = cmpc_keys_codes[event & 0x0F]; + inputdev = dev_get_drvdata(dev); + input_report_key(inputdev, code, !(event & 0x10)); + input_sync(inputdev); +} + +static void cmpc_keys_idev_init(struct input_dev *inputdev) +{ + int i; + + set_bit(EV_KEY, inputdev->evbit); + for (i = 0; cmpc_keys_codes[i] != KEY_MAX; i++) + set_bit(cmpc_keys_codes[i], inputdev->keybit); +} + +static int cmpc_keys_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi; + int error; + + acpi = ACPI_COMPANION(&pdev->dev); + if (!acpi) + return -ENODEV; + + error = cmpc_add_notify_device(&pdev->dev, "cmpc_keys", cmpc_keys_idev_init); + if (error) + return error; + + error = acpi_dev_install_notify_handler(acpi, ACPI_DEVICE_NOTIFY, + cmpc_keys_handler, &pdev->dev); + if (error) + cmpc_remove_notify_device(&pdev->dev); + + return error; +} + +static void cmpc_keys_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, cmpc_keys_handler); + cmpc_remove_notify_device(&pdev->dev); +} + +static const struct acpi_device_id cmpc_keys_device_ids[] = { + {CMPC_KEYS_HID, 0}, + {"", 0} +}; + +static struct platform_driver cmpc_keys_acpi_driver = { + .probe = cmpc_keys_probe, + .remove = cmpc_keys_remove, + .driver = { + .name = "cmpc_keys", + .acpi_match_table = cmpc_keys_device_ids, + }, +}; + + +/* + * General init/exit code. + */ + +static int cmpc_init(void) +{ + int r; + + r = platform_driver_register(&cmpc_keys_acpi_driver); + if (r) + goto failed_keys; + + r = platform_driver_register(&cmpc_ipml_acpi_driver); + if (r) + goto failed_bl; + + r = platform_driver_register(&cmpc_tablet_acpi_driver); + if (r) + goto failed_tablet; + + r = platform_driver_register(&cmpc_accel_acpi_driver); + if (r) + goto failed_accel; + + r = platform_driver_register(&cmpc_accel_acpi_driver_v4); + if (r) + goto failed_accel_v4; + + return r; + +failed_accel_v4: + platform_driver_unregister(&cmpc_accel_acpi_driver); + +failed_accel: + platform_driver_unregister(&cmpc_tablet_acpi_driver); + +failed_tablet: + platform_driver_unregister(&cmpc_ipml_acpi_driver); + +failed_bl: + platform_driver_unregister(&cmpc_keys_acpi_driver); + +failed_keys: + return r; +} + +static void cmpc_exit(void) +{ + platform_driver_unregister(&cmpc_accel_acpi_driver_v4); + platform_driver_unregister(&cmpc_accel_acpi_driver); + platform_driver_unregister(&cmpc_tablet_acpi_driver); + platform_driver_unregister(&cmpc_ipml_acpi_driver); + platform_driver_unregister(&cmpc_keys_acpi_driver); +} + +module_init(cmpc_init); +module_exit(cmpc_exit); + +static const struct acpi_device_id cmpc_device_ids[] __maybe_unused = { + {CMPC_ACCEL_HID, 0}, + {CMPC_ACCEL_HID_V4, 0}, + {CMPC_TABLET_HID, 0}, + {CMPC_IPML_HID, 0}, + {CMPC_KEYS_HID, 0}, + {"", 0} +}; + +MODULE_DEVICE_TABLE(acpi, cmpc_device_ids); +MODULE_DESCRIPTION("Support for Intel Classmate PC ACPI devices"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/compal-laptop.c b/drivers/platform/x86/compal-laptop.c new file mode 100644 index 000000000..abbebd4bf --- /dev/null +++ b/drivers/platform/x86/compal-laptop.c @@ -0,0 +1,1113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2008 Cezary Jackiewicz + + based on MSI driver + + Copyright (C) 2006 Lennart Poettering + + */ + +/* + * compal-laptop.c - Compal laptop support. + * + * This driver exports a few files in /sys/devices/platform/compal-laptop/: + * wake_up_XXX Whether or not we listen to such wake up events (rw) + * + * In addition to these platform device attributes the driver + * registers itself in the Linux backlight control, power_supply, rfkill + * and hwmon subsystem and is available to userspace under: + * + * /sys/class/backlight/compal-laptop/ + * /sys/class/power_supply/compal-laptop/ + * /sys/class/rfkill/rfkillX/ + * /sys/class/hwmon/hwmonX/ + * + * Notes on the power_supply battery interface: + * - the "minimum" design voltage is *the* design voltage + * - the ambient temperature is the average battery temperature + * and the value is an educated guess (see commented code below) + * + * + * This driver might work on other laptops produced by Compal. If you + * want to try it you can pass force=1 as argument to the module which + * will force it to load even when the DMI data doesn't identify the + * laptop as compatible. + * + * Lots of data available at: + * http://service1.marasst.com/Compal/JHL90_91/Service%20Manual/ + * JHL90%20service%20manual-Final-0725.pdf + * + * + * + * Support for the Compal JHL90 added by Roald Frederickx + * (roald.frederickx@gmail.com): + * Driver got large revision. Added functionalities: backlight + * power, wake_on_XXX, a hwmon and power_supply interface. + * + * In case this gets merged into the kernel source: I want to dedicate this + * to Kasper Meerts, the awesome guy who showed me Linux and C! + */ + +/* NOTE: currently the wake_on_XXX, hwmon and power_supply interfaces are + * only enabled on a JHL90 board until it is verified that they work on the + * other boards too. See the extra_features variable. */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ======= */ +/* Defines */ +/* ======= */ +#define DRIVER_NAME "compal-laptop" +#define DRIVER_VERSION "0.2.7" + +#define BACKLIGHT_LEVEL_ADDR 0xB9 +#define BACKLIGHT_LEVEL_MAX 7 +#define BACKLIGHT_STATE_ADDR 0x59 +#define BACKLIGHT_STATE_ON_DATA 0xE1 +#define BACKLIGHT_STATE_OFF_DATA 0xE2 + +#define WAKE_UP_ADDR 0xA4 +#define WAKE_UP_PME (1 << 0) +#define WAKE_UP_MODEM (1 << 1) +#define WAKE_UP_LAN (1 << 2) +#define WAKE_UP_WLAN (1 << 4) +#define WAKE_UP_KEY (1 << 6) +#define WAKE_UP_MOUSE (1 << 7) + +#define WIRELESS_ADDR 0xBB +#define WIRELESS_WLAN (1 << 0) +#define WIRELESS_BT (1 << 1) +#define WIRELESS_WLAN_EXISTS (1 << 2) +#define WIRELESS_BT_EXISTS (1 << 3) +#define WIRELESS_KILLSWITCH (1 << 4) + +#define PWM_ADDRESS 0x46 +#define PWM_DISABLE_ADDR 0x59 +#define PWM_DISABLE_DATA 0xA5 +#define PWM_ENABLE_ADDR 0x59 +#define PWM_ENABLE_DATA 0xA8 + +#define FAN_ADDRESS 0x46 +#define FAN_DATA 0x81 +#define FAN_FULL_ON_CMD 0x59 /* Doesn't seem to work. Just */ +#define FAN_FULL_ON_ENABLE 0x76 /* force the pwm signal to its */ +#define FAN_FULL_ON_DISABLE 0x77 /* maximum value instead */ + +#define TEMP_CPU 0xB0 +#define TEMP_CPU_LOCAL 0xB1 +#define TEMP_CPU_DTS 0xB5 +#define TEMP_NORTHBRIDGE 0xB6 +#define TEMP_VGA 0xB4 +#define TEMP_SKIN 0xB2 + +#define BAT_MANUFACTURER_NAME_ADDR 0x10 +#define BAT_MANUFACTURER_NAME_LEN 9 +#define BAT_MODEL_NAME_ADDR 0x19 +#define BAT_MODEL_NAME_LEN 6 +#define BAT_SERIAL_NUMBER_ADDR 0xC4 +#define BAT_SERIAL_NUMBER_LEN 5 +#define BAT_CHARGE_NOW 0xC2 +#define BAT_CHARGE_DESIGN 0xCA +#define BAT_VOLTAGE_NOW 0xC6 +#define BAT_VOLTAGE_DESIGN 0xC8 +#define BAT_CURRENT_NOW 0xD0 +#define BAT_CURRENT_AVG 0xD2 +#define BAT_POWER 0xD4 +#define BAT_CAPACITY 0xCE +#define BAT_TEMP 0xD6 +#define BAT_TEMP_AVG 0xD7 +#define BAT_STATUS0 0xC1 +#define BAT_STATUS1 0xF0 +#define BAT_STATUS2 0xF1 +#define BAT_STOP_CHARGE1 0xF2 +#define BAT_STOP_CHARGE2 0xF3 +#define BAT_CHARGE_LIMIT 0x03 +#define BAT_CHARGE_LIMIT_MAX 100 + +#define BAT_S0_DISCHARGE (1 << 0) +#define BAT_S0_DISCHRG_CRITICAL (1 << 2) +#define BAT_S0_LOW (1 << 3) +#define BAT_S0_CHARGING (1 << 1) +#define BAT_S0_AC (1 << 7) +#define BAT_S1_EXISTS (1 << 0) +#define BAT_S1_FULL (1 << 1) +#define BAT_S1_EMPTY (1 << 2) +#define BAT_S1_LiION_OR_NiMH (1 << 7) +#define BAT_S2_LOW_LOW (1 << 0) +#define BAT_STOP_CHRG1_BAD_CELL (1 << 1) +#define BAT_STOP_CHRG1_COMM_FAIL (1 << 2) +#define BAT_STOP_CHRG1_OVERVOLTAGE (1 << 6) +#define BAT_STOP_CHRG1_OVERTEMPERATURE (1 << 7) + + +/* ======= */ +/* Structs */ +/* ======= */ +struct compal_data{ + /* Fan control */ + int pwm_enable; /* 0:full on, 1:set by pwm1, 2:control by motherboard */ + unsigned char curr_pwm; + + /* Power supply */ + struct power_supply *psy; + struct power_supply_info psy_info; + char bat_model_name[BAT_MODEL_NAME_LEN + 1]; + char bat_manufacturer_name[BAT_MANUFACTURER_NAME_LEN + 1]; + char bat_serial_number[BAT_SERIAL_NUMBER_LEN + 1]; +}; + + +/* =============== */ +/* General globals */ +/* =============== */ +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +/* Support for the wake_on_XXX, hwmon and power_supply interface. Currently + * only gets enabled on a JHL90 board. Might work with the others too */ +static bool extra_features; + +/* Nasty stuff. For some reason the fan control is very un-linear. I've + * come up with these values by looping through the possible inputs and + * watching the output of address 0x4F (do an ec_transaction writing 0x33 + * into 0x4F and read a few bytes from the output, like so: + * u8 writeData = 0x33; + * ec_transaction(0x4F, &writeData, 1, buffer, 32); + * That address is labeled "fan1 table information" in the service manual. + * It should be clear which value in 'buffer' changes). This seems to be + * related to fan speed. It isn't a proper 'realtime' fan speed value + * though, because physically stopping or speeding up the fan doesn't + * change it. It might be the average voltage or current of the pwm output. + * Nevertheless, it is more fine-grained than the actual RPM reading */ +static const unsigned char pwm_lookup_table[256] = { + 0, 0, 0, 1, 1, 1, 2, 253, 254, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, + 7, 7, 7, 8, 86, 86, 9, 9, 9, 10, 10, 10, 11, 92, 92, 12, 12, 95, + 13, 66, 66, 14, 14, 98, 15, 15, 15, 16, 16, 67, 17, 17, 72, 18, 70, + 75, 19, 90, 90, 73, 73, 73, 21, 21, 91, 91, 91, 96, 23, 94, 94, 94, + 94, 94, 94, 94, 94, 94, 94, 141, 141, 238, 223, 192, 139, 139, 139, + 139, 139, 142, 142, 142, 142, 142, 78, 78, 78, 78, 78, 76, 76, 76, + 76, 76, 79, 79, 79, 79, 79, 79, 79, 20, 20, 20, 20, 20, 22, 22, 22, + 22, 22, 24, 24, 24, 24, 24, 24, 219, 219, 219, 219, 219, 219, 219, + 219, 27, 27, 188, 188, 28, 28, 28, 29, 186, 186, 186, 186, 186, + 186, 186, 186, 186, 186, 31, 31, 31, 31, 31, 32, 32, 32, 41, 33, + 33, 33, 33, 33, 252, 252, 34, 34, 34, 43, 35, 35, 35, 36, 36, 38, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 37, 37, 37, 46, 46, + 47, 47, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 48, 48, + 48, 48, 48, 40, 40, 40, 49, 42, 42, 42, 42, 42, 42, 42, 42, 44, + 189, 189, 189, 189, 54, 54, 45, 45, 45, 45, 45, 45, 45, 45, 251, + 191, 199, 199, 199, 199, 199, 215, 215, 215, 215, 187, 187, 187, + 187, 187, 193, 50 +}; + + + + +/* ========================= */ +/* Hardware access functions */ +/* ========================= */ +/* General access */ +static u8 ec_read_u8(u8 addr) +{ + u8 value = 0; + ec_read(addr, &value); + return value; +} + +static s8 ec_read_s8(u8 addr) +{ + return (s8)ec_read_u8(addr); +} + +static u16 ec_read_u16(u8 addr) +{ + int hi, lo; + lo = ec_read_u8(addr); + hi = ec_read_u8(addr + 1); + return (hi << 8) + lo; +} + +static s16 ec_read_s16(u8 addr) +{ + return (s16) ec_read_u16(addr); +} + +static void ec_read_sequence(u8 addr, u8 *buf, int len) +{ + int i; + for (i = 0; i < len; i++) + ec_read(addr + i, buf + i); +} + + +/* Backlight access */ +static int set_backlight_level(int level) +{ + if (level < 0 || level > BACKLIGHT_LEVEL_MAX) + return -EINVAL; + + ec_write(BACKLIGHT_LEVEL_ADDR, level); + + return 0; +} + +static int get_backlight_level(void) +{ + return (int) ec_read_u8(BACKLIGHT_LEVEL_ADDR); +} + +static void set_backlight_state(bool on) +{ + u8 data = on ? BACKLIGHT_STATE_ON_DATA : BACKLIGHT_STATE_OFF_DATA; + ec_transaction(BACKLIGHT_STATE_ADDR, &data, 1, NULL, 0); +} + + +/* Fan control access */ +static void pwm_enable_control(void) +{ + unsigned char writeData = PWM_ENABLE_DATA; + ec_transaction(PWM_ENABLE_ADDR, &writeData, 1, NULL, 0); +} + +static void pwm_disable_control(void) +{ + unsigned char writeData = PWM_DISABLE_DATA; + ec_transaction(PWM_DISABLE_ADDR, &writeData, 1, NULL, 0); +} + +static void set_pwm(int pwm) +{ + ec_transaction(PWM_ADDRESS, &pwm_lookup_table[pwm], 1, NULL, 0); +} + +static int get_fan_rpm(void) +{ + u8 value, data = FAN_DATA; + ec_transaction(FAN_ADDRESS, &data, 1, &value, 1); + return 100 * (int)value; +} + + + + +/* =================== */ +/* Interface functions */ +/* =================== */ + +/* Backlight interface */ +static int bl_get_brightness(struct backlight_device *b) +{ + return get_backlight_level(); +} + +static int bl_update_status(struct backlight_device *b) +{ + int ret = set_backlight_level(b->props.brightness); + if (ret) + return ret; + + set_backlight_state(!backlight_is_blank(b)); + return 0; +} + +static const struct backlight_ops compalbl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + + +/* Wireless interface */ +static int compal_rfkill_set(void *data, bool blocked) +{ + unsigned long radio = (unsigned long) data; + u8 result = ec_read_u8(WIRELESS_ADDR); + u8 value; + + if (!blocked) + value = (u8) (result | radio); + else + value = (u8) (result & ~radio); + ec_write(WIRELESS_ADDR, value); + + return 0; +} + +static void compal_rfkill_poll(struct rfkill *rfkill, void *data) +{ + u8 result = ec_read_u8(WIRELESS_ADDR); + bool hw_blocked = !(result & WIRELESS_KILLSWITCH); + rfkill_set_hw_state(rfkill, hw_blocked); +} + +static const struct rfkill_ops compal_rfkill_ops = { + .poll = compal_rfkill_poll, + .set_block = compal_rfkill_set, +}; + + +/* Wake_up interface */ +#define SIMPLE_MASKED_STORE_SHOW(NAME, ADDR, MASK) \ +static ssize_t NAME##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ +{ \ + return sysfs_emit(buf, "%d\n", ((ec_read_u8(ADDR) & MASK) != 0)); \ +} \ +static ssize_t NAME##_store(struct device *dev, \ + struct device_attribute *attr, const char *buf, size_t count) \ +{ \ + int state; \ + u8 old_val = ec_read_u8(ADDR); \ + if (sscanf(buf, "%d", &state) != 1 || (state < 0 || state > 1)) \ + return -EINVAL; \ + ec_write(ADDR, state ? (old_val | MASK) : (old_val & ~MASK)); \ + return count; \ +} + +SIMPLE_MASKED_STORE_SHOW(wake_up_pme, WAKE_UP_ADDR, WAKE_UP_PME) +SIMPLE_MASKED_STORE_SHOW(wake_up_modem, WAKE_UP_ADDR, WAKE_UP_MODEM) +SIMPLE_MASKED_STORE_SHOW(wake_up_lan, WAKE_UP_ADDR, WAKE_UP_LAN) +SIMPLE_MASKED_STORE_SHOW(wake_up_wlan, WAKE_UP_ADDR, WAKE_UP_WLAN) +SIMPLE_MASKED_STORE_SHOW(wake_up_key, WAKE_UP_ADDR, WAKE_UP_KEY) +SIMPLE_MASKED_STORE_SHOW(wake_up_mouse, WAKE_UP_ADDR, WAKE_UP_MOUSE) + +/* Fan control interface */ +static ssize_t pwm_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct compal_data *data = dev_get_drvdata(dev); + return sysfs_emit(buf, "%d\n", data->pwm_enable); +} + +static ssize_t pwm_enable_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct compal_data *data = dev_get_drvdata(dev); + long val; + int err; + + err = kstrtol(buf, 10, &val); + if (err) + return err; + if (val < 0) + return -EINVAL; + + data->pwm_enable = val; + + switch (val) { + case 0: /* Full speed */ + pwm_enable_control(); + set_pwm(255); + break; + case 1: /* As set by pwm1 */ + pwm_enable_control(); + set_pwm(data->curr_pwm); + break; + default: /* Control by motherboard */ + pwm_disable_control(); + break; + } + + return count; +} + +static ssize_t pwm_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct compal_data *data = dev_get_drvdata(dev); + return sysfs_emit(buf, "%hhu\n", data->curr_pwm); +} + +static ssize_t pwm_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct compal_data *data = dev_get_drvdata(dev); + long val; + int err; + + err = kstrtol(buf, 10, &val); + if (err) + return err; + if (val < 0 || val > 255) + return -EINVAL; + + data->curr_pwm = val; + + if (data->pwm_enable != 1) + return count; + set_pwm(val); + + return count; +} + +static ssize_t fan_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", get_fan_rpm()); +} + + +/* Temperature interface */ +#define TEMPERATURE_SHOW_TEMP_AND_LABEL(POSTFIX, ADDRESS, LABEL) \ +static ssize_t temp_##POSTFIX(struct device *dev, \ + struct device_attribute *attr, char *buf) \ +{ \ + return sysfs_emit(buf, "%d\n", 1000 * (int)ec_read_s8(ADDRESS)); \ +} \ +static ssize_t label_##POSTFIX(struct device *dev, \ + struct device_attribute *attr, char *buf) \ +{ \ + return sysfs_emit(buf, "%s\n", LABEL); \ +} + +/* Labels as in service guide */ +TEMPERATURE_SHOW_TEMP_AND_LABEL(cpu, TEMP_CPU, "CPU_TEMP"); +TEMPERATURE_SHOW_TEMP_AND_LABEL(cpu_local, TEMP_CPU_LOCAL, "CPU_TEMP_LOCAL"); +TEMPERATURE_SHOW_TEMP_AND_LABEL(cpu_DTS, TEMP_CPU_DTS, "CPU_DTS"); +TEMPERATURE_SHOW_TEMP_AND_LABEL(northbridge,TEMP_NORTHBRIDGE,"NorthBridge"); +TEMPERATURE_SHOW_TEMP_AND_LABEL(vga, TEMP_VGA, "VGA_TEMP"); +TEMPERATURE_SHOW_TEMP_AND_LABEL(SKIN, TEMP_SKIN, "SKIN_TEMP90"); + + +/* Power supply interface */ +static int bat_status(void) +{ + u8 status0 = ec_read_u8(BAT_STATUS0); + u8 status1 = ec_read_u8(BAT_STATUS1); + + if (status0 & BAT_S0_CHARGING) + return POWER_SUPPLY_STATUS_CHARGING; + if (status0 & BAT_S0_DISCHARGE) + return POWER_SUPPLY_STATUS_DISCHARGING; + if (status1 & BAT_S1_FULL) + return POWER_SUPPLY_STATUS_FULL; + return POWER_SUPPLY_STATUS_NOT_CHARGING; +} + +static int bat_health(void) +{ + u8 status = ec_read_u8(BAT_STOP_CHARGE1); + + if (status & BAT_STOP_CHRG1_OVERTEMPERATURE) + return POWER_SUPPLY_HEALTH_OVERHEAT; + if (status & BAT_STOP_CHRG1_OVERVOLTAGE) + return POWER_SUPPLY_HEALTH_OVERVOLTAGE; + if (status & BAT_STOP_CHRG1_BAD_CELL) + return POWER_SUPPLY_HEALTH_DEAD; + if (status & BAT_STOP_CHRG1_COMM_FAIL) + return POWER_SUPPLY_HEALTH_UNKNOWN; + return POWER_SUPPLY_HEALTH_GOOD; +} + +static int bat_is_present(void) +{ + u8 status = ec_read_u8(BAT_STATUS2); + return ((status & BAT_S1_EXISTS) != 0); +} + +static int bat_technology(void) +{ + u8 status = ec_read_u8(BAT_STATUS1); + + if (status & BAT_S1_LiION_OR_NiMH) + return POWER_SUPPLY_TECHNOLOGY_LION; + return POWER_SUPPLY_TECHNOLOGY_NiMH; +} + +static int bat_capacity_level(void) +{ + u8 status0 = ec_read_u8(BAT_STATUS0); + u8 status1 = ec_read_u8(BAT_STATUS1); + u8 status2 = ec_read_u8(BAT_STATUS2); + + if (status0 & BAT_S0_DISCHRG_CRITICAL + || status1 & BAT_S1_EMPTY + || status2 & BAT_S2_LOW_LOW) + return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + if (status0 & BAT_S0_LOW) + return POWER_SUPPLY_CAPACITY_LEVEL_LOW; + if (status1 & BAT_S1_FULL) + return POWER_SUPPLY_CAPACITY_LEVEL_FULL; + return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; +} + +static int bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct compal_data *data = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = bat_status(); + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = bat_health(); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = bat_is_present(); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = bat_technology(); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: /* THE design voltage... */ + val->intval = ec_read_u16(BAT_VOLTAGE_DESIGN) * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = ec_read_u16(BAT_VOLTAGE_NOW) * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = ec_read_s16(BAT_CURRENT_NOW) * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + val->intval = ec_read_s16(BAT_CURRENT_AVG) * 1000; + break; + case POWER_SUPPLY_PROP_POWER_NOW: + val->intval = ec_read_u8(BAT_POWER) * 1000000; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = ec_read_u16(BAT_CHARGE_DESIGN) * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = ec_read_u16(BAT_CHARGE_NOW) * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + val->intval = ec_read_u8(BAT_CHARGE_LIMIT); + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX: + val->intval = BAT_CHARGE_LIMIT_MAX; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = ec_read_u8(BAT_CAPACITY); + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + val->intval = bat_capacity_level(); + break; + /* It smees that BAT_TEMP_AVG is a (2's complement?) value showing + * the number of degrees, whereas BAT_TEMP is somewhat more + * complicated. It looks like this is a negative nember with a + * 100/256 divider and an offset of 222. Both were determined + * experimentally by comparing BAT_TEMP and BAT_TEMP_AVG. */ + case POWER_SUPPLY_PROP_TEMP: + val->intval = ((222 - (int)ec_read_u8(BAT_TEMP)) * 1000) >> 8; + break; + case POWER_SUPPLY_PROP_TEMP_AMBIENT: /* Ambient, Avg, ... same thing */ + val->intval = ec_read_s8(BAT_TEMP_AVG) * 10; + break; + /* Neither the model name nor manufacturer name work for me. */ + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = data->bat_model_name; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = data->bat_manufacturer_name; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = data->bat_serial_number; + break; + default: + break; + } + return 0; +} + +static int bat_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + int level; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + level = val->intval; + if (level < 0 || level > BAT_CHARGE_LIMIT_MAX) + return -EINVAL; + if (ec_write(BAT_CHARGE_LIMIT, level) < 0) + return -EIO; + break; + default: + break; + } + return 0; +} + +static int bat_writeable_property(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + return 1; + default: + return 0; + } +} + + + + +/* ============== */ +/* Driver Globals */ +/* ============== */ +static DEVICE_ATTR_RW(wake_up_pme); +static DEVICE_ATTR_RW(wake_up_modem); +static DEVICE_ATTR_RW(wake_up_lan); +static DEVICE_ATTR_RW(wake_up_wlan); +static DEVICE_ATTR_RW(wake_up_key); +static DEVICE_ATTR_RW(wake_up_mouse); + +static DEVICE_ATTR(fan1_input, S_IRUGO, fan_show, NULL); +static DEVICE_ATTR(temp1_input, S_IRUGO, temp_cpu, NULL); +static DEVICE_ATTR(temp2_input, S_IRUGO, temp_cpu_local, NULL); +static DEVICE_ATTR(temp3_input, S_IRUGO, temp_cpu_DTS, NULL); +static DEVICE_ATTR(temp4_input, S_IRUGO, temp_northbridge, NULL); +static DEVICE_ATTR(temp5_input, S_IRUGO, temp_vga, NULL); +static DEVICE_ATTR(temp6_input, S_IRUGO, temp_SKIN, NULL); +static DEVICE_ATTR(temp1_label, S_IRUGO, label_cpu, NULL); +static DEVICE_ATTR(temp2_label, S_IRUGO, label_cpu_local, NULL); +static DEVICE_ATTR(temp3_label, S_IRUGO, label_cpu_DTS, NULL); +static DEVICE_ATTR(temp4_label, S_IRUGO, label_northbridge, NULL); +static DEVICE_ATTR(temp5_label, S_IRUGO, label_vga, NULL); +static DEVICE_ATTR(temp6_label, S_IRUGO, label_SKIN, NULL); +static DEVICE_ATTR(pwm1, S_IRUGO | S_IWUSR, pwm_show, pwm_store); +static DEVICE_ATTR(pwm1_enable, + S_IRUGO | S_IWUSR, pwm_enable_show, pwm_enable_store); + +static struct attribute *compal_platform_attrs[] = { + &dev_attr_wake_up_pme.attr, + &dev_attr_wake_up_modem.attr, + &dev_attr_wake_up_lan.attr, + &dev_attr_wake_up_wlan.attr, + &dev_attr_wake_up_key.attr, + &dev_attr_wake_up_mouse.attr, + NULL +}; +static const struct attribute_group compal_platform_attr_group = { + .attrs = compal_platform_attrs +}; + +static struct attribute *compal_hwmon_attrs[] = { + &dev_attr_pwm1_enable.attr, + &dev_attr_pwm1.attr, + &dev_attr_fan1_input.attr, + &dev_attr_temp1_input.attr, + &dev_attr_temp2_input.attr, + &dev_attr_temp3_input.attr, + &dev_attr_temp4_input.attr, + &dev_attr_temp5_input.attr, + &dev_attr_temp6_input.attr, + &dev_attr_temp1_label.attr, + &dev_attr_temp2_label.attr, + &dev_attr_temp3_label.attr, + &dev_attr_temp4_label.attr, + &dev_attr_temp5_label.attr, + &dev_attr_temp6_label.attr, + NULL +}; +ATTRIBUTE_GROUPS(compal_hwmon); + +static enum power_supply_property compal_bat_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_POWER_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TEMP_AMBIENT, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, +}; + +static struct backlight_device *compalbl_device; + +static struct platform_device *compal_device; + +static struct rfkill *wifi_rfkill; +static struct rfkill *bt_rfkill; + + + + + +/* =================================== */ +/* Initialization & clean-up functions */ +/* =================================== */ + +static int dmi_check_cb(const struct dmi_system_id *id) +{ + pr_info("Identified laptop model '%s'\n", id->ident); + extra_features = false; + return 1; +} + +static int dmi_check_cb_extra(const struct dmi_system_id *id) +{ + pr_info("Identified laptop model '%s', enabling extra features\n", + id->ident); + extra_features = true; + return 1; +} + +static const struct dmi_system_id compal_dmi_table[] __initconst = { + { + .ident = "FL90/IFL90", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL90"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL90/IFL90", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL90"), + DMI_MATCH(DMI_BOARD_VERSION, "REFERENCE"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL91/IFL91", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFL91"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FL92/JFL92", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "JFL92"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "FT00/IFT00", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "IFT00"), + DMI_MATCH(DMI_BOARD_VERSION, "IFT00"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Mini 9", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 910"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Mini 10", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1010"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Mini 10v", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1011"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Mini 1012", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1012"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Inspiron 11z", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1110"), + }, + .callback = dmi_check_cb + }, + { + .ident = "Dell Mini 12", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1210"), + }, + .callback = dmi_check_cb + }, + { + .ident = "JHL90", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "JHL90"), + DMI_MATCH(DMI_BOARD_VERSION, "REFERENCE"), + }, + .callback = dmi_check_cb_extra + }, + { + .ident = "KHLB2", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "KHLB2"), + DMI_MATCH(DMI_BOARD_VERSION, "REFERENCE"), + }, + .callback = dmi_check_cb_extra + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, compal_dmi_table); + +static const struct power_supply_desc psy_bat_desc = { + .name = DRIVER_NAME, + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = compal_bat_properties, + .num_properties = ARRAY_SIZE(compal_bat_properties), + .get_property = bat_get_property, + .set_property = bat_set_property, + .property_is_writeable = bat_writeable_property, +}; + +static void initialize_power_supply_data(struct compal_data *data) +{ + ec_read_sequence(BAT_MANUFACTURER_NAME_ADDR, + data->bat_manufacturer_name, + BAT_MANUFACTURER_NAME_LEN); + data->bat_manufacturer_name[BAT_MANUFACTURER_NAME_LEN] = 0; + + ec_read_sequence(BAT_MODEL_NAME_ADDR, + data->bat_model_name, + BAT_MODEL_NAME_LEN); + data->bat_model_name[BAT_MODEL_NAME_LEN] = 0; + + scnprintf(data->bat_serial_number, BAT_SERIAL_NUMBER_LEN + 1, "%d", + ec_read_u16(BAT_SERIAL_NUMBER_ADDR)); +} + +static void initialize_fan_control_data(struct compal_data *data) +{ + data->pwm_enable = 2; /* Keep motherboard in control for now */ + data->curr_pwm = 255; /* Try not to cause a CPU_on_fire exception + if we take over... */ +} + +static int setup_rfkill(void) +{ + int ret; + + wifi_rfkill = rfkill_alloc("compal-wifi", &compal_device->dev, + RFKILL_TYPE_WLAN, &compal_rfkill_ops, + (void *) WIRELESS_WLAN); + if (!wifi_rfkill) + return -ENOMEM; + + ret = rfkill_register(wifi_rfkill); + if (ret) + goto err_wifi; + + bt_rfkill = rfkill_alloc("compal-bluetooth", &compal_device->dev, + RFKILL_TYPE_BLUETOOTH, &compal_rfkill_ops, + (void *) WIRELESS_BT); + if (!bt_rfkill) { + ret = -ENOMEM; + goto err_allocate_bt; + } + ret = rfkill_register(bt_rfkill); + if (ret) + goto err_register_bt; + + return 0; + +err_register_bt: + rfkill_destroy(bt_rfkill); + +err_allocate_bt: + rfkill_unregister(wifi_rfkill); + +err_wifi: + rfkill_destroy(wifi_rfkill); + + return ret; +} + +static int compal_probe(struct platform_device *pdev) +{ + int err; + struct compal_data *data; + struct device *hwmon_dev; + struct power_supply_config psy_cfg = {}; + + if (!extra_features) + return 0; + + /* Fan control */ + data = devm_kzalloc(&pdev->dev, sizeof(struct compal_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + initialize_fan_control_data(data); + + err = sysfs_create_group(&pdev->dev.kobj, &compal_platform_attr_group); + if (err) + return err; + + hwmon_dev = devm_hwmon_device_register_with_groups(&pdev->dev, + "compal", data, + compal_hwmon_groups); + if (IS_ERR(hwmon_dev)) { + err = PTR_ERR(hwmon_dev); + goto remove; + } + + /* Power supply */ + initialize_power_supply_data(data); + psy_cfg.drv_data = data; + data->psy = power_supply_register(&compal_device->dev, &psy_bat_desc, + &psy_cfg); + if (IS_ERR(data->psy)) { + err = PTR_ERR(data->psy); + goto remove; + } + + platform_set_drvdata(pdev, data); + + return 0; + +remove: + sysfs_remove_group(&pdev->dev.kobj, &compal_platform_attr_group); + return err; +} + +static void compal_remove(struct platform_device *pdev) +{ + struct compal_data *data; + + if (!extra_features) + return; + + pr_info("Unloading: resetting fan control to motherboard\n"); + pwm_disable_control(); + + data = platform_get_drvdata(pdev); + power_supply_unregister(data->psy); + + sysfs_remove_group(&pdev->dev.kobj, &compal_platform_attr_group); +} + +static struct platform_driver compal_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = compal_probe, + .remove = compal_remove, +}; + +static int __init compal_init(void) +{ + int ret; + + if (acpi_disabled) { + pr_err("ACPI needs to be enabled for this driver to work!\n"); + return -ENODEV; + } + + if (!force && !dmi_check_system(compal_dmi_table)) { + pr_err("Motherboard not recognized (You could try the module's force-parameter)\n"); + return -ENODEV; + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + struct backlight_properties props; + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = BACKLIGHT_LEVEL_MAX; + compalbl_device = backlight_device_register(DRIVER_NAME, + NULL, NULL, + &compalbl_ops, + &props); + if (IS_ERR(compalbl_device)) + return PTR_ERR(compalbl_device); + } + + ret = platform_driver_register(&compal_driver); + if (ret) + goto err_backlight; + + compal_device = platform_device_alloc(DRIVER_NAME, PLATFORM_DEVID_NONE); + if (!compal_device) { + ret = -ENOMEM; + goto err_platform_driver; + } + + ret = platform_device_add(compal_device); /* This calls compal_probe */ + if (ret) + goto err_platform_device; + + ret = setup_rfkill(); + if (ret) + goto err_rfkill; + + pr_info("Driver " DRIVER_VERSION " successfully loaded\n"); + return 0; + +err_rfkill: + platform_device_del(compal_device); + +err_platform_device: + platform_device_put(compal_device); + +err_platform_driver: + platform_driver_unregister(&compal_driver); + +err_backlight: + backlight_device_unregister(compalbl_device); + + return ret; +} + +static void __exit compal_cleanup(void) +{ + platform_device_unregister(compal_device); + platform_driver_unregister(&compal_driver); + backlight_device_unregister(compalbl_device); + rfkill_unregister(wifi_rfkill); + rfkill_unregister(bt_rfkill); + rfkill_destroy(wifi_rfkill); + rfkill_destroy(bt_rfkill); + + pr_info("Driver unloaded\n"); +} + +module_init(compal_init); +module_exit(compal_cleanup); + +MODULE_AUTHOR("Cezary Jackiewicz"); +MODULE_AUTHOR("Roald Frederickx "); +MODULE_DESCRIPTION("Compal Laptop Support"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dasharo-acpi.c b/drivers/platform/x86/dasharo-acpi.c new file mode 100644 index 000000000..f0c5136af --- /dev/null +++ b/drivers/platform/x86/dasharo-acpi.c @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Dasharo ACPI Driver + */ + +#include +#include +#include +#include +#include +#include +#include + +enum dasharo_feature { + DASHARO_FEATURE_TEMPERATURE = 0, + DASHARO_FEATURE_FAN_PWM, + DASHARO_FEATURE_FAN_TACH, + DASHARO_FEATURE_MAX +}; + +enum dasharo_temperature { + DASHARO_TEMPERATURE_CPU_PACKAGE = 0, + DASHARO_TEMPERATURE_CPU_CORE, + DASHARO_TEMPERATURE_GPU, + DASHARO_TEMPERATURE_BOARD, + DASHARO_TEMPERATURE_CHASSIS, + DASHARO_TEMPERATURE_MAX +}; + +enum dasharo_fan { + DASHARO_FAN_CPU = 0, + DASHARO_FAN_GPU, + DASHARO_FAN_CHASSIS, + DASHARO_FAN_MAX +}; + +#define MAX_GROUPS_PER_FEAT 8 + +static const char * const dasharo_group_names[DASHARO_FEATURE_MAX][MAX_GROUPS_PER_FEAT] = { + [DASHARO_FEATURE_TEMPERATURE] = { + [DASHARO_TEMPERATURE_CPU_PACKAGE] = "CPU Package", + [DASHARO_TEMPERATURE_CPU_CORE] = "CPU Core", + [DASHARO_TEMPERATURE_GPU] = "GPU", + [DASHARO_TEMPERATURE_BOARD] = "Board", + [DASHARO_TEMPERATURE_CHASSIS] = "Chassis", + }, + [DASHARO_FEATURE_FAN_PWM] = { + [DASHARO_FAN_CPU] = "CPU", + [DASHARO_FAN_GPU] = "GPU", + [DASHARO_FAN_CHASSIS] = "Chassis", + }, + [DASHARO_FEATURE_FAN_TACH] = { + [DASHARO_FAN_CPU] = "CPU", + [DASHARO_FAN_GPU] = "GPU", + [DASHARO_FAN_CHASSIS] = "Chassis", + }, +}; + +struct dasharo_capability { + unsigned int group; + unsigned int index; + char name[16]; +}; + +#define MAX_CAPS_PER_FEAT 24 + +struct dasharo_data { + struct platform_device *pdev; + int caps_found[DASHARO_FEATURE_MAX]; + struct dasharo_capability capabilities[DASHARO_FEATURE_MAX][MAX_CAPS_PER_FEAT]; +}; + +static int dasharo_get_feature_cap_count(struct dasharo_data *data, enum dasharo_feature feat, int cap) +{ + struct acpi_object_list obj_list; + union acpi_object obj[2]; + acpi_handle handle; + acpi_status status; + u64 count; + + obj[0].type = ACPI_TYPE_INTEGER; + obj[0].integer.value = feat; + obj[1].type = ACPI_TYPE_INTEGER; + obj[1].integer.value = cap; + obj_list.count = 2; + obj_list.pointer = &obj[0]; + + handle = ACPI_HANDLE(&data->pdev->dev); + status = acpi_evaluate_integer(handle, "GFCP", &obj_list, &count); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return count; +} + +static int dasharo_read_channel(struct dasharo_data *data, char *method, enum dasharo_feature feat, int channel, long *value) +{ + struct acpi_object_list obj_list; + union acpi_object obj[2]; + acpi_handle handle; + acpi_status status; + u64 val; + + if (feat >= ARRAY_SIZE(data->capabilities)) + return -EINVAL; + + if (channel >= data->caps_found[feat]) + return -EINVAL; + + obj[0].type = ACPI_TYPE_INTEGER; + obj[0].integer.value = data->capabilities[feat][channel].group; + obj[1].type = ACPI_TYPE_INTEGER; + obj[1].integer.value = data->capabilities[feat][channel].index; + obj_list.count = 2; + obj_list.pointer = &obj[0]; + + handle = ACPI_HANDLE(&data->pdev->dev); + status = acpi_evaluate_integer(handle, method, &obj_list, &val); + if (ACPI_FAILURE(status)) + return -ENODEV; + + *value = val; + return 0; +} + +static int dasharo_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct dasharo_data *data = dev_get_drvdata(dev); + long value; + int ret; + + switch (type) { + case hwmon_temp: + ret = dasharo_read_channel(data, "GTMP", DASHARO_FEATURE_TEMPERATURE, channel, &value); + if (!ret) + *val = value * MILLIDEGREE_PER_DEGREE; + break; + case hwmon_fan: + ret = dasharo_read_channel(data, "GFTH", DASHARO_FEATURE_FAN_TACH, channel, &value); + if (!ret) + *val = value; + break; + case hwmon_pwm: + ret = dasharo_read_channel(data, "GFDC", DASHARO_FEATURE_FAN_PWM, channel, &value); + if (!ret) + *val = value; + break; + default: + return -ENODEV; + break; + } + + return ret; +} + +static int dasharo_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + struct dasharo_data *data = dev_get_drvdata(dev); + + switch (type) { + case hwmon_temp: + if (channel >= data->caps_found[DASHARO_FEATURE_TEMPERATURE]) + return -EINVAL; + + *str = data->capabilities[DASHARO_FEATURE_TEMPERATURE][channel].name; + break; + case hwmon_fan: + if (channel >= data->caps_found[DASHARO_FEATURE_FAN_TACH]) + return -EINVAL; + + *str = data->capabilities[DASHARO_FEATURE_FAN_TACH][channel].name; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static umode_t dasharo_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct dasharo_data *data = drvdata; + + switch (type) { + case hwmon_temp: + if (channel < data->caps_found[DASHARO_FEATURE_TEMPERATURE]) + return 0444; + break; + case hwmon_pwm: + if (channel < data->caps_found[DASHARO_FEATURE_FAN_PWM]) + return 0444; + break; + case hwmon_fan: + if (channel < data->caps_found[DASHARO_FEATURE_FAN_TACH]) + return 0444; + break; + default: + break; + } + + return 0; +} + +static const struct hwmon_ops dasharo_hwmon_ops = { + .is_visible = dasharo_hwmon_is_visible, + .read_string = dasharo_hwmon_read_string, + .read = dasharo_hwmon_read, +}; + +// Max 24 capabilities per feature +static const struct hwmon_channel_info * const dasharo_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT), + NULL +}; + +static const struct hwmon_chip_info dasharo_hwmon_chip_info = { + .ops = &dasharo_hwmon_ops, + .info = dasharo_hwmon_info, +}; + +static void dasharo_fill_feature_caps(struct dasharo_data *data, enum dasharo_feature feat) +{ + struct dasharo_capability *cap; + int cap_count = 0; + int count; + + for (int group = 0; group < MAX_GROUPS_PER_FEAT; ++group) { + count = dasharo_get_feature_cap_count(data, feat, group); + if (count <= 0) + continue; + + for (unsigned int i = 0; i < count; ++i) { + if (cap_count >= ARRAY_SIZE(data->capabilities[feat])) + break; + + cap = &data->capabilities[feat][cap_count]; + cap->group = group; + cap->index = i; + scnprintf(cap->name, sizeof(cap->name), "%s %d", + dasharo_group_names[feat][group], i); + cap_count++; + } + } + data->caps_found[feat] = cap_count; +} + +static int dasharo_probe(struct platform_device *pdev) +{ + struct dasharo_data *data; + struct device *hwmon; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + data->pdev = pdev; + + for (unsigned int i = 0; i < DASHARO_FEATURE_MAX; ++i) + dasharo_fill_feature_caps(data, i); + + hwmon = devm_hwmon_device_register_with_info(&pdev->dev, "dasharo_acpi", data, + &dasharo_hwmon_chip_info, NULL); + + return PTR_ERR_OR_ZERO(hwmon); +} + +static const struct acpi_device_id dasharo_device_ids[] = { + {"DSHR0001", 0}, + {} +}; +MODULE_DEVICE_TABLE(acpi, dasharo_device_ids); + +static struct platform_driver dasharo_driver = { + .driver = { + .name = "dasharo-acpi", + .acpi_match_table = dasharo_device_ids, + }, + .probe = dasharo_probe, +}; +module_platform_driver(dasharo_driver); + +MODULE_DESCRIPTION("Dasharo ACPI Driver"); +MODULE_AUTHOR("Michał Kopeć "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/Kconfig b/drivers/platform/x86/dell/Kconfig new file mode 100644 index 000000000..9f0016056 --- /dev/null +++ b/drivers/platform/x86/dell/Kconfig @@ -0,0 +1,284 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Dell X86 Platform Specific Drivers +# + +menuconfig X86_PLATFORM_DRIVERS_DELL + bool "Dell X86 Platform Specific Device Drivers" + help + Say Y here to get to see options for device drivers for various + Dell x86 platforms, including vendor-specific laptop extension drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if X86_PLATFORM_DRIVERS_DELL + +config ALIENWARE_WMI + tristate "Alienware Special feature control" + default m + depends on ACPI + depends on ACPI_WMI + depends on DMI + depends on LEDS_CLASS + depends on NEW_LEDS + depends on HWMON + help + This is a driver for controlling Alienware WMI driven features. + + On legacy devices, it exposes an interface for controlling the AlienFX + zones on Alienware machines that don't contain a dedicated + AlienFX USB MCU such as the X51 and X51-R2. + + On newer devices, it exposes the AWCC thermal control interface through + known Kernel APIs. + +config ALIENWARE_WMI_LEGACY + bool "Alienware Legacy WMI device driver" + default y + depends on ALIENWARE_WMI + help + Legacy Alienware WMI driver with AlienFX LED control capabilities. + +config ALIENWARE_WMI_WMAX + bool "Alienware WMAX WMI device driver" + default y + depends on ALIENWARE_WMI + select ACPI_PLATFORM_PROFILE + help + Alienware WMI driver with AlienFX LED, HDMI, amplifier, deep sleep and + AWCC thermal control capabilities. + +config DCDBAS + tristate "Dell Systems Management Base Driver" + default m + depends on X86 + help + The Dell Systems Management Base Driver provides a sysfs interface + for systems management software to perform System Management + Interrupts (SMIs) and Host Control Actions (system power cycle or + power off after OS shutdown) on certain Dell systems. + + See for more details on the driver + and the Dell systems on which Dell systems management software makes + use of this driver. + + Say Y or M here to enable the driver for use by Dell systems + management software such as Dell OpenManage. + +config DELL_LAPTOP + tristate "Dell Laptop Extras" + default m + depends on DMI + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_BATTERY + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on RFKILL || RFKILL = n + depends on DELL_WMI || DELL_WMI = n + depends on SERIO_I8042 + depends on DELL_SMBIOS + select POWER_SUPPLY + select LEDS_CLASS + select NEW_LEDS + help + This driver adds support for rfkill and backlight control to Dell + laptops (except for some models covered by the Compal driver). + +config DELL_RBU + tristate "BIOS update support for DELL systems via sysfs" + default m + depends on X86 + select FW_LOADER + select FW_LOADER_USER_HELPER + help + Say m if you want to have the option of updating the BIOS for your + DELL system. Note you need a Dell OpenManage or Dell Update package (DUP) + supporting application to communicate with the BIOS regarding the new + image for the image update to take effect. + See for more details on the driver. + +config DELL_RBTN + tristate "Dell Airplane Mode Switch driver" + default m + depends on ACPI + depends on INPUT + depends on RFKILL + help + Say Y here if you want to support Dell Airplane Mode Switch ACPI + device on Dell laptops. Sometimes it has names: DELLABCE or DELRBTN. + This driver register rfkill device or input hotkey device depending + on hardware type (hw switch slider or keyboard toggle button). For + rfkill devices it receive HW switch events and set correct hard + rfkill state. + + To compile this driver as a module, choose M here: the module will + be called dell-rbtn. + +config DELL_PC + tristate "Dell PC Extras" + default m + depends on ACPI + depends on DMI + depends on DELL_SMBIOS + select ACPI_PLATFORM_PROFILE + help + This driver adds support for controlling the fan modes via platform_profile + on supported Dell systems regardless of formfactor. + Module will simply do nothing if thermal management commands are not + supported. + +# +# The DELL_SMBIOS driver depends on ACPI_WMI and/or DCDBAS if those +# backends are selected. The "depends" line prevents a configuration +# where DELL_SMBIOS=y while either of those dependencies =m. +# +config DELL_SMBIOS + tristate "Dell SMBIOS driver" + default m + depends on DCDBAS || DCDBAS=n + depends on ACPI_WMI || ACPI_WMI=n + help + This provides support for the Dell SMBIOS calling interface. + If you have a Dell computer you should enable this option. + + Be sure to select at least one backend for it to work properly. + +config DELL_SMBIOS_WMI + bool "Dell SMBIOS driver WMI backend" + default y + depends on ACPI_WMI + select DELL_WMI_DESCRIPTOR + depends on DELL_SMBIOS + help + This provides an implementation for the Dell SMBIOS calling interface + communicated over ACPI-WMI. + + If you have a Dell computer from >2007 you should say Y here. + If you aren't sure and this module doesn't work for your computer + it just won't load. + +config DELL_SMBIOS_SMM + bool "Dell SMBIOS driver SMM backend" + default y + depends on DCDBAS + depends on DELL_SMBIOS + help + This provides an implementation for the Dell SMBIOS calling interface + communicated over SMI/SMM. + + If you have a Dell computer from <=2017 you should say Y here. + If you aren't sure and this module doesn't work for your computer + it just won't load. + +config DELL_SMO8800 + tristate "Dell Latitude freefall driver (ACPI SMO88XX)" + default m if ACPI + depends on I2C + depends on ACPI || COMPILE_TEST + help + Say Y here if you want to support SMO88XX freefall devices + on Dell Latitude laptops. + + To compile this driver as a module, choose M here: the module will + be called dell-smo8800. + +config DELL_UART_BACKLIGHT + tristate "Dell AIO UART Backlight driver" + depends on ACPI + depends on ACPI_VIDEO + depends on BACKLIGHT_CLASS_DEVICE + depends on SERIAL_DEV_BUS + help + Say Y here if you want to support Dell AIO UART backlight interface. + The Dell AIO machines released after 2017 come with a UART interface + to communicate with the backlight scalar board. This driver creates + a standard backlight interface and talks to the scalar board through + UART to adjust the AIO screen brightness. + + To compile this driver as a module, choose M here: the module will + be called dell_uart_backlight. + +config DELL_WMI + tristate "Dell WMI notifications" + default m + depends on ACPI_WMI + depends on DMI + depends on INPUT + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on DELL_SMBIOS + select DELL_WMI_DESCRIPTOR + select INPUT_SPARSEKMAP + help + Say Y here if you want to support WMI-based hotkeys on Dell laptops. + + To compile this driver as a module, choose M here: the module will + be called dell-wmi. + +config DELL_WMI_PRIVACY + bool "Dell WMI Hardware Privacy Support" + depends on DELL_WMI + depends on ACPI_EC + help + This option adds integration with the "Dell Hardware Privacy" + feature of Dell laptops to the dell-wmi driver. + +config DELL_WMI_AIO + tristate "WMI Hotkeys for Dell All-In-One series" + default m + depends on ACPI_WMI + depends on INPUT + select INPUT_SPARSEKMAP + help + Say Y here if you want to support WMI-based hotkeys on Dell + All-In-One machines. + + To compile this driver as a module, choose M here: the module will + be called dell-wmi-aio. + +config DELL_WMI_DESCRIPTOR + tristate + default n + depends on ACPI_WMI + +config DELL_WMI_DDV + tristate "Dell WMI sensors Support" + default m + depends on ACPI_WMI + depends on ACPI_BATTERY || HWMON + help + This option adds support for WMI-based fan and thermal sensors + found on some Dell notebooks. It also supports various WMI-based battery + extras like reading of the battery temperature and ePPID. + + To compile this drivers as a module, choose M here: the module will + be called dell-wmi-ddv. + +config DELL_WMI_LED + tristate "External LED on Dell Business Netbooks" + default m + depends on LEDS_CLASS + depends on ACPI_WMI + help + This adds support for the Latitude 2100 and similar + notebooks that have an external LED. + +config DELL_WMI_SYSMAN + tristate "Dell WMI-based Systems management driver" + default m + depends on ACPI_WMI + depends on DMI + select NLS + select FW_ATTR_CLASS + help + This driver allows changing BIOS settings on many Dell machines from + 2018 and newer without the use of any additional software. + + To compile this driver as a module, choose M here: the module will + be called dell-wmi-sysman. + +config DELL_DW5826E_RESET + tristate "Dell DW5826e PLDR reset support" + depends on ACPI + help + This adds support for the Dell DW5826e PLDR reset via ACPI +endif # X86_PLATFORM_DRIVERS_DELL diff --git a/drivers/platform/x86/dell/Makefile b/drivers/platform/x86/dell/Makefile new file mode 100644 index 000000000..470874c6e --- /dev/null +++ b/drivers/platform/x86/dell/Makefile @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/dell +# Dell x86 Platform-Specific Drivers +# + +obj-$(CONFIG_ALIENWARE_WMI) += alienware-wmi.o +alienware-wmi-y := alienware-wmi-base.o +alienware-wmi-$(CONFIG_ALIENWARE_WMI_LEGACY) += alienware-wmi-legacy.o +alienware-wmi-$(CONFIG_ALIENWARE_WMI_WMAX) += alienware-wmi-wmax.o +obj-$(CONFIG_DCDBAS) += dcdbas.o +obj-$(CONFIG_DELL_LAPTOP) += dell-laptop.o +obj-$(CONFIG_DELL_RBTN) += dell-rbtn.o +obj-$(CONFIG_DELL_RBU) += dell_rbu.o +obj-$(CONFIG_DELL_PC) += dell-pc.o +obj-$(CONFIG_DELL_SMBIOS) += dell-smbios.o +dell-smbios-y := dell-smbios-base.o +dell-smbios-$(CONFIG_DELL_SMBIOS_WMI) += dell-smbios-wmi.o +dell-smbios-$(CONFIG_DELL_SMBIOS_SMM) += dell-smbios-smm.o +obj-$(CONFIG_DELL_SMO8800) += dell-smo8800.o +obj-$(CONFIG_DELL_SMO8800) += dell-lis3lv02d.o +obj-$(CONFIG_DELL_UART_BACKLIGHT) += dell-uart-backlight.o +obj-$(CONFIG_DELL_WMI) += dell-wmi.o +dell-wmi-y := dell-wmi-base.o +dell-wmi-$(CONFIG_DELL_WMI_PRIVACY) += dell-wmi-privacy.o +obj-$(CONFIG_DELL_WMI_AIO) += dell-wmi-aio.o +obj-$(CONFIG_DELL_WMI_DESCRIPTOR) += dell-wmi-descriptor.o +obj-$(CONFIG_DELL_WMI_DDV) += dell-wmi-ddv.o +obj-$(CONFIG_DELL_WMI_LED) += dell-wmi-led.o +obj-$(CONFIG_DELL_WMI_SYSMAN) += dell-wmi-sysman/ +obj-$(CONFIG_DELL_DW5826E_RESET) += dell-dw5826e-reset.o diff --git a/drivers/platform/x86/dell/alienware-wmi-base.c b/drivers/platform/x86/dell/alienware-wmi-base.c new file mode 100644 index 000000000..19cadf212 --- /dev/null +++ b/drivers/platform/x86/dell/alienware-wmi-base.c @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Alienware special feature control + * + * Copyright (C) 2014 Dell Inc + * Copyright (C) 2025 Kurt Borja + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "alienware-wmi.h" + +MODULE_AUTHOR("Mario Limonciello "); +MODULE_AUTHOR("Kurt Borja "); +MODULE_DESCRIPTION("Alienware special feature control"); +MODULE_LICENSE("GPL"); + +struct alienfx_quirks *alienfx; + +static struct alienfx_quirks quirk_inspiron5675 = { + .num_zones = 2, + .hdmi_mux = false, + .amplifier = false, + .deepslp = false, +}; + +static struct alienfx_quirks quirk_unknown = { + .num_zones = 2, + .hdmi_mux = false, + .amplifier = false, + .deepslp = false, +}; + +static struct alienfx_quirks quirk_x51_r1_r2 = { + .num_zones = 3, + .hdmi_mux = false, + .amplifier = false, + .deepslp = false, +}; + +static struct alienfx_quirks quirk_x51_r3 = { + .num_zones = 4, + .hdmi_mux = false, + .amplifier = true, + .deepslp = false, +}; + +static struct alienfx_quirks quirk_asm100 = { + .num_zones = 2, + .hdmi_mux = true, + .amplifier = false, + .deepslp = false, +}; + +static struct alienfx_quirks quirk_asm200 = { + .num_zones = 2, + .hdmi_mux = true, + .amplifier = false, + .deepslp = true, +}; + +static struct alienfx_quirks quirk_asm201 = { + .num_zones = 2, + .hdmi_mux = true, + .amplifier = true, + .deepslp = true, +}; + +static int __init dmi_matched(const struct dmi_system_id *dmi) +{ + alienfx = dmi->driver_data; + return 1; +} + +static const struct dmi_system_id alienware_quirks[] __initconst = { + { + .callback = dmi_matched, + .ident = "Alienware ASM100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "ASM100"), + }, + .driver_data = &quirk_asm100, + }, + { + .callback = dmi_matched, + .ident = "Alienware ASM200", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "ASM200"), + }, + .driver_data = &quirk_asm200, + }, + { + .callback = dmi_matched, + .ident = "Alienware ASM201", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "ASM201"), + }, + .driver_data = &quirk_asm201, + }, + { + .callback = dmi_matched, + .ident = "Alienware X51 R1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51"), + }, + .driver_data = &quirk_x51_r1_r2, + }, + { + .callback = dmi_matched, + .ident = "Alienware X51 R2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R2"), + }, + .driver_data = &quirk_x51_r1_r2, + }, + { + .callback = dmi_matched, + .ident = "Alienware X51 R3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R3"), + }, + .driver_data = &quirk_x51_r3, + }, + { + .callback = dmi_matched, + .ident = "Dell Inc. Inspiron 5675", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5675"), + }, + .driver_data = &quirk_inspiron5675, + }, + {} +}; + +u8 alienware_interface; + +int alienware_wmi_command(struct wmi_device *wdev, u32 method_id, + void *in_args, size_t in_size, u32 *out_data) +{ + struct wmi_buffer out, in = { + .data = in_args, + .length = in_size, + }; + int ret; + + if (!out_data) + return wmidev_invoke_procedure(wdev, 0, method_id, &in); + + ret = wmidev_invoke_method(wdev, 0, method_id, &in, &out, + sizeof(*out_data)); + if (ret) + return ret; + + __le32 *data __free(kfree) = out.data; + *out_data = le32_to_cpu(*data); + + return 0; +} + +/* + * Helpers used for zone control + */ +static int parse_rgb(const char *buf, struct color_platform *colors) +{ + long unsigned int rgb; + int ret; + union color_union { + struct color_platform cp; + int package; + } repackager; + + ret = kstrtoul(buf, 16, &rgb); + if (ret) + return ret; + + /* RGB triplet notation is 24-bit hexadecimal */ + if (rgb > 0xFFFFFF) + return -EINVAL; + + repackager.package = rgb & 0x0f0f0f0f; + pr_debug("alienware-wmi: r: %d g:%d b: %d\n", + repackager.cp.red, repackager.cp.green, repackager.cp.blue); + *colors = repackager.cp; + return 0; +} + +/* + * Individual RGB zone control + */ +static ssize_t zone_show(struct device *dev, struct device_attribute *attr, + char *buf, u8 location) +{ + struct alienfx_priv *priv = dev_get_drvdata(dev); + struct color_platform *colors = &priv->colors[location]; + + return sprintf(buf, "red: %d, green: %d, blue: %d\n", + colors->red, colors->green, colors->blue); + +} + +static ssize_t zone_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count, u8 location) +{ + struct alienfx_priv *priv = dev_get_drvdata(dev); + struct color_platform *colors = &priv->colors[location]; + struct alienfx_platdata *pdata = dev_get_platdata(dev); + int ret; + + ret = parse_rgb(buf, colors); + if (ret) + return ret; + + ret = pdata->ops.upd_led(priv, pdata->wdev, location); + + return ret ? ret : count; +} + +static ssize_t zone00_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return zone_show(dev, attr, buf, 0); +} + +static ssize_t zone00_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return zone_store(dev, attr, buf, count, 0); +} + +static DEVICE_ATTR_RW(zone00); + +static ssize_t zone01_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return zone_show(dev, attr, buf, 1); +} + +static ssize_t zone01_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return zone_store(dev, attr, buf, count, 1); +} + +static DEVICE_ATTR_RW(zone01); + +static ssize_t zone02_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return zone_show(dev, attr, buf, 2); +} + +static ssize_t zone02_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return zone_store(dev, attr, buf, count, 2); +} + +static DEVICE_ATTR_RW(zone02); + +static ssize_t zone03_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return zone_show(dev, attr, buf, 3); +} + +static ssize_t zone03_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + return zone_store(dev, attr, buf, count, 3); +} + +static DEVICE_ATTR_RW(zone03); + +/* + * Lighting control state device attribute (Global) + */ +static ssize_t lighting_control_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct alienfx_priv *priv = dev_get_drvdata(dev); + + if (priv->lighting_control_state == LEGACY_BOOTING) + return sysfs_emit(buf, "[booting] running suspend\n"); + else if (priv->lighting_control_state == LEGACY_SUSPEND) + return sysfs_emit(buf, "booting running [suspend]\n"); + + return sysfs_emit(buf, "booting [running] suspend\n"); +} + +static ssize_t lighting_control_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct alienfx_priv *priv = dev_get_drvdata(dev); + u8 val; + + if (strcmp(buf, "booting\n") == 0) + val = LEGACY_BOOTING; + else if (strcmp(buf, "suspend\n") == 0) + val = LEGACY_SUSPEND; + else if (alienware_interface == LEGACY) + val = LEGACY_RUNNING; + else + val = WMAX_RUNNING; + + priv->lighting_control_state = val; + pr_debug("alienware-wmi: updated control state to %d\n", + priv->lighting_control_state); + + return count; +} + +static DEVICE_ATTR_RW(lighting_control_state); + +static umode_t zone_attr_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (n < alienfx->num_zones + 1) + return attr->mode; + + return 0; +} + +static bool zone_group_visible(struct kobject *kobj) +{ + return alienfx->num_zones > 0; +} +DEFINE_SYSFS_GROUP_VISIBLE(zone); + +static struct attribute *zone_attrs[] = { + &dev_attr_lighting_control_state.attr, + &dev_attr_zone00.attr, + &dev_attr_zone01.attr, + &dev_attr_zone02.attr, + &dev_attr_zone03.attr, + NULL +}; + +static struct attribute_group zone_attribute_group = { + .name = "rgb_zones", + .is_visible = SYSFS_GROUP_VISIBLE(zone), + .attrs = zone_attrs, +}; + +/* + * LED Brightness (Global) + */ +static void global_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv, + global_led); + struct alienfx_platdata *pdata = dev_get_platdata(&priv->pdev->dev); + int ret; + + priv->global_brightness = brightness; + + ret = pdata->ops.upd_brightness(priv, pdata->wdev, brightness); + if (ret) + pr_err("LED brightness update failed\n"); +} + +static enum led_brightness global_led_get(struct led_classdev *led_cdev) +{ + struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv, + global_led); + + return priv->global_brightness; +} + +/* + * Platform Driver + */ +static int alienfx_probe(struct platform_device *pdev) +{ + struct alienfx_priv *priv; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + if (alienware_interface == WMAX) + priv->lighting_control_state = WMAX_RUNNING; + else + priv->lighting_control_state = LEGACY_RUNNING; + + priv->pdev = pdev; + priv->global_led.name = "alienware::global_brightness"; + priv->global_led.brightness_set = global_led_set; + priv->global_led.brightness_get = global_led_get; + priv->global_led.max_brightness = 0x0F; + priv->global_brightness = priv->global_led.max_brightness; + platform_set_drvdata(pdev, priv); + + return devm_led_classdev_register(&pdev->dev, &priv->global_led); +} + +static const struct attribute_group *alienfx_groups[] = { + &zone_attribute_group, + WMAX_DEV_GROUPS + NULL +}; + +static struct platform_driver platform_driver = { + .driver = { + .name = "alienware-wmi", + .dev_groups = alienfx_groups, + }, + .probe = alienfx_probe, +}; + +static void alienware_alienfx_remove(void *data) +{ + struct platform_device *pdev = data; + + platform_device_unregister(pdev); +} + +int alienware_alienfx_setup(struct alienfx_platdata *pdata) +{ + struct device *dev = &pdata->wdev->dev; + struct platform_device *pdev; + int ret; + + pdev = platform_device_register_data(NULL, "alienware-wmi", + PLATFORM_DEVID_NONE, pdata, + sizeof(*pdata)); + if (IS_ERR(pdev)) + return PTR_ERR(pdev); + + dev_set_drvdata(dev, pdev); + ret = devm_add_action_or_reset(dev, alienware_alienfx_remove, pdev); + if (ret) + return ret; + + return 0; +} + +static int __init alienware_wmi_init(void) +{ + int ret; + + dmi_check_system(alienware_quirks); + if (!alienfx) + alienfx = &quirk_unknown; + + ret = platform_driver_register(&platform_driver); + if (ret < 0) + return ret; + + if (wmi_has_guid(WMAX_CONTROL_GUID)) { + alienware_interface = WMAX; + ret = alienware_wmax_wmi_init(); + } else { + alienware_interface = LEGACY; + ret = alienware_legacy_wmi_init(); + } + + if (ret < 0) + platform_driver_unregister(&platform_driver); + + return ret; +} + +module_init(alienware_wmi_init); + +static void __exit alienware_wmi_exit(void) +{ + if (alienware_interface == WMAX) + alienware_wmax_wmi_exit(); + else + alienware_legacy_wmi_exit(); + + platform_driver_unregister(&platform_driver); +} + +module_exit(alienware_wmi_exit); diff --git a/drivers/platform/x86/dell/alienware-wmi-legacy.c b/drivers/platform/x86/dell/alienware-wmi-legacy.c new file mode 100644 index 000000000..4a84a2fe9 --- /dev/null +++ b/drivers/platform/x86/dell/alienware-wmi-legacy.c @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Alienware LEGACY WMI device driver + * + * Copyright (C) 2025 Kurt Borja + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include "alienware-wmi.h" + +struct legacy_led_args { + struct color_platform colors; + u8 brightness; + u8 state; +} __packed; + + +/* + * Legacy WMI driver + */ +static int legacy_wmi_update_led(struct alienfx_priv *priv, + struct wmi_device *wdev, u8 location) +{ + struct legacy_led_args legacy_args = { + .colors = priv->colors[location], + .brightness = priv->global_brightness, + .state = 0, + }; + struct acpi_buffer input; + acpi_status status; + + if (legacy_args.state != LEGACY_RUNNING) { + legacy_args.state = priv->lighting_control_state; + + input.length = sizeof(legacy_args); + input.pointer = &legacy_args; + + status = wmi_evaluate_method(LEGACY_POWER_CONTROL_GUID, 0, + location + 1, &input, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; + } + + return alienware_wmi_command(wdev, location + 1, &legacy_args, + sizeof(legacy_args), NULL); +} + +static int legacy_wmi_update_brightness(struct alienfx_priv *priv, + struct wmi_device *wdev, u8 brightness) +{ + return legacy_wmi_update_led(priv, wdev, 0); +} + +static int legacy_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct alienfx_platdata pdata = { + .wdev = wdev, + .ops = { + .upd_led = legacy_wmi_update_led, + .upd_brightness = legacy_wmi_update_brightness, + }, + }; + + return alienware_alienfx_setup(&pdata); +} + +static const struct wmi_device_id alienware_legacy_device_id_table[] = { + { LEGACY_CONTROL_GUID, NULL }, + { }, +}; +MODULE_DEVICE_TABLE(wmi, alienware_legacy_device_id_table); + +static struct wmi_driver alienware_legacy_wmi_driver = { + .driver = { + .name = "alienware-wmi-alienfx", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = alienware_legacy_device_id_table, + .probe = legacy_wmi_probe, + .no_singleton = true, +}; + +int __init alienware_legacy_wmi_init(void) +{ + return wmi_driver_register(&alienware_legacy_wmi_driver); +} + +void __exit alienware_legacy_wmi_exit(void) +{ + wmi_driver_unregister(&alienware_legacy_wmi_driver); +} diff --git a/drivers/platform/x86/dell/alienware-wmi-wmax.c b/drivers/platform/x86/dell/alienware-wmi-wmax.c new file mode 100644 index 000000000..d1b4df914 --- /dev/null +++ b/drivers/platform/x86/dell/alienware-wmi-wmax.c @@ -0,0 +1,1684 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Alienware WMAX WMI device driver + * + * Copyright (C) 2014 Dell Inc + * Copyright (C) 2025 Kurt Borja + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "alienware-wmi.h" + +#define WMAX_METHOD_HDMI_SOURCE 0x1 +#define WMAX_METHOD_HDMI_STATUS 0x2 +#define WMAX_METHOD_HDMI_CABLE 0x5 +#define WMAX_METHOD_AMPLIFIER_CABLE 0x6 +#define WMAX_METHOD_DEEP_SLEEP_CONTROL 0x0B +#define WMAX_METHOD_DEEP_SLEEP_STATUS 0x0C +#define WMAX_METHOD_BRIGHTNESS 0x3 +#define WMAX_METHOD_ZONE_CONTROL 0x4 + +#define AWCC_METHOD_GET_FAN_SENSORS 0x13 +#define AWCC_METHOD_THERMAL_INFORMATION 0x14 +#define AWCC_METHOD_THERMAL_CONTROL 0x15 +#define AWCC_METHOD_FWUP_GPIO_CONTROL 0x20 +#define AWCC_METHOD_READ_TOTAL_GPIOS 0x21 +#define AWCC_METHOD_READ_GPIO_STATUS 0x22 +#define AWCC_METHOD_GAME_SHIFT_STATUS 0x25 + +#define AWCC_FAILURE_CODE 0xFFFFFFFF +#define AWCC_FAILURE_CODE_2 0xFFFFFFFE + +#define AWCC_SENSOR_ID_FLAG BIT(8) +#define AWCC_THERMAL_MODE_MASK GENMASK(3, 0) +#define AWCC_THERMAL_TABLE_MASK GENMASK(7, 4) +#define AWCC_RESOURCE_ID_MASK GENMASK(7, 0) + +/* Arbitrary limit based on supported models */ +#define AWCC_MAX_RES_COUNT 16 +#define AWCC_ID_BITMAP_SIZE (U8_MAX + 1) +#define AWCC_ID_BITMAP_LONGS BITS_TO_LONGS(AWCC_ID_BITMAP_SIZE) + +static bool force_hwmon; +module_param_unsafe(force_hwmon, bool, 0); +MODULE_PARM_DESC(force_hwmon, "Force probing for HWMON support without checking if the WMI backend is available"); + +static bool force_platform_profile; +module_param_unsafe(force_platform_profile, bool, 0); +MODULE_PARM_DESC(force_platform_profile, "Forces auto-detecting thermal profiles without checking if WMI thermal backend is available"); + +static bool force_gmode; +module_param_unsafe(force_gmode, bool, 0); +MODULE_PARM_DESC(force_gmode, "Forces G-Mode when performance profile is selected"); + +struct awcc_quirks { + bool hwmon; + bool pprof; + bool gmode; +}; + +static struct awcc_quirks g_series_quirks = { + .hwmon = true, + .pprof = true, + .gmode = true, +}; + +static struct awcc_quirks generic_quirks = { + .hwmon = true, + .pprof = true, + .gmode = false, +}; + +static struct awcc_quirks empty_quirks; + +static const struct dmi_system_id awcc_dmi_table[] __initconst = { + { + .ident = "Alienware 16 Area-51", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware 16 Area-51"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware 16X Aurora", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware 16X Aurora"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware 18 Area-51", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware 18 Area-51"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware 16 Aurora", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware 16 Aurora"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware Area-51m", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware Area-51m"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware m15", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m15"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware m16 R1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m16 R1 AMD"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware m16 R1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m16 R1"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware m16 R2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m16 R2"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware m17", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m17"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware m18", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware m18"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware x15", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware x15"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Alienware x16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware x16"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Alienware x17", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), + DMI_MATCH(DMI_PRODUCT_NAME, "Alienware x17"), + }, + .driver_data = &generic_quirks, + }, + { + .ident = "Dell Inc. G15", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell G15"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Dell Inc. G16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell G16"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Dell Inc. G3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "G3"), + }, + .driver_data = &g_series_quirks, + }, + { + .ident = "Dell Inc. G5", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "G5"), + }, + .driver_data = &g_series_quirks, + }, + {} +}; + +enum AWCC_GET_FAN_SENSORS_OPERATIONS { + AWCC_OP_GET_TOTAL_FAN_TEMPS = 0x01, + AWCC_OP_GET_FAN_TEMP_ID = 0x02, +}; + +enum AWCC_THERMAL_INFORMATION_OPERATIONS { + AWCC_OP_GET_SYSTEM_DESCRIPTION = 0x02, + AWCC_OP_GET_RESOURCE_ID = 0x03, + AWCC_OP_GET_TEMPERATURE = 0x04, + AWCC_OP_GET_FAN_RPM = 0x05, + AWCC_OP_GET_FAN_MIN_RPM = 0x08, + AWCC_OP_GET_FAN_MAX_RPM = 0x09, + AWCC_OP_GET_CURRENT_PROFILE = 0x0B, + AWCC_OP_GET_FAN_BOOST = 0x0C, +}; + +enum AWCC_THERMAL_CONTROL_OPERATIONS { + AWCC_OP_ACTIVATE_PROFILE = 0x01, + AWCC_OP_SET_FAN_BOOST = 0x02, +}; + +enum AWCC_GAME_SHIFT_STATUS_OPERATIONS { + AWCC_OP_TOGGLE_GAME_SHIFT = 0x01, + AWCC_OP_GET_GAME_SHIFT_STATUS = 0x02, +}; + +enum AWCC_THERMAL_TABLES { + AWCC_THERMAL_TABLE_LEGACY = 0x9, + AWCC_THERMAL_TABLE_USTT = 0xA, +}; + +enum AWCC_TEMP_SENSOR_TYPES { + AWCC_TEMP_SENSOR_CPU = 0x01, + AWCC_TEMP_SENSOR_FRONT = 0x03, + AWCC_TEMP_SENSOR_GPU = 0x06, +}; + +enum AWCC_FAN_TYPES { + AWCC_FAN_CPU_1 = 0x32, + AWCC_FAN_GPU_1 = 0x33, + AWCC_FAN_PCI = 0x34, + AWCC_FAN_MID = 0x35, + AWCC_FAN_TOP_1 = 0x36, + AWCC_FAN_SIDE = 0x37, + AWCC_FAN_U2_1 = 0x38, + AWCC_FAN_U2_2 = 0x39, + AWCC_FAN_FRONT_1 = 0x3A, + AWCC_FAN_CPU_2 = 0x3B, + AWCC_FAN_GPU_2 = 0x3C, + AWCC_FAN_TOP_2 = 0x3D, + AWCC_FAN_TOP_3 = 0x3E, + AWCC_FAN_FRONT_2 = 0x3F, + AWCC_FAN_BOTTOM_1 = 0x40, + AWCC_FAN_BOTTOM_2 = 0x41, +}; + +enum awcc_thermal_profile { + AWCC_PROFILE_SPECIAL_CUSTOM = 0x00, + AWCC_PROFILE_LEGACY_QUIET = 0x96, + AWCC_PROFILE_LEGACY_BALANCED = 0x97, + AWCC_PROFILE_LEGACY_BALANCED_PERFORMANCE = 0x98, + AWCC_PROFILE_LEGACY_PERFORMANCE = 0x99, + AWCC_PROFILE_USTT_BALANCED = 0xA0, + AWCC_PROFILE_USTT_BALANCED_PERFORMANCE = 0xA1, + AWCC_PROFILE_USTT_COOL = 0xA2, + AWCC_PROFILE_USTT_QUIET = 0xA3, + AWCC_PROFILE_USTT_PERFORMANCE = 0xA4, + AWCC_PROFILE_USTT_LOW_POWER = 0xA5, + AWCC_PROFILE_SPECIAL_GMODE = 0xAB, +}; + +struct wmax_led_args { + u32 led_mask; + struct color_platform colors; + u8 state; +} __packed; + +struct wmax_brightness_args { + u32 led_mask; + u32 percentage; +}; + +struct wmax_basic_args { + u8 arg; +}; + +struct wmax_u32_args { + u8 operation; + u8 arg1; + u8 arg2; + u8 arg3; +}; + +struct awcc_fan_data { + unsigned long auto_channels_temp; + u32 min_rpm; + u32 max_rpm; + u8 suspend_cache; + u8 id; +}; + +struct awcc_priv { + struct wmi_device *wdev; + union { + u32 system_description; + struct { + u8 fan_count; + u8 temp_count; + u8 unknown_count; + u8 profile_count; + }; + u8 res_count[4]; + }; + + struct device *ppdev; + u8 supported_profiles[PLATFORM_PROFILE_LAST]; + + struct device *hwdev; + struct awcc_fan_data **fan_data; + unsigned long temp_sensors[AWCC_ID_BITMAP_LONGS]; + + u32 gpio_count; +}; + +static struct awcc_quirks *awcc; + +/* + * The HDMI mux sysfs node indicates the status of the HDMI input mux. + * It can toggle between standard system GPU output and HDMI input. + */ +static ssize_t cable_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args in_args = { + .arg = 0, + }; + u32 out_data; + int ret; + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_HDMI_CABLE, + &in_args, sizeof(in_args), &out_data); + if (!ret) { + if (out_data == 0) + return sysfs_emit(buf, "[unconnected] connected unknown\n"); + else if (out_data == 1) + return sysfs_emit(buf, "unconnected [connected] unknown\n"); + } + + pr_err("alienware-wmi: unknown HDMI cable status: %d\n", ret); + return sysfs_emit(buf, "unconnected connected [unknown]\n"); +} + +static ssize_t source_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args in_args = { + .arg = 0, + }; + u32 out_data; + int ret; + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_HDMI_STATUS, + &in_args, sizeof(in_args), &out_data); + if (!ret) { + if (out_data == 1) + return sysfs_emit(buf, "[input] gpu unknown\n"); + else if (out_data == 2) + return sysfs_emit(buf, "input [gpu] unknown\n"); + } + + pr_err("alienware-wmi: unknown HDMI source status: %u\n", ret); + return sysfs_emit(buf, "input gpu [unknown]\n"); +} + +static ssize_t source_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args args; + int ret; + + if (strcmp(buf, "gpu\n") == 0) + args.arg = 1; + else if (strcmp(buf, "input\n") == 0) + args.arg = 2; + else + args.arg = 3; + pr_debug("alienware-wmi: setting hdmi to %d : %s", args.arg, buf); + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_HDMI_SOURCE, &args, + sizeof(args), NULL); + if (ret < 0) + pr_err("alienware-wmi: HDMI toggle failed: results: %u\n", ret); + + return count; +} + +static DEVICE_ATTR_RO(cable); +static DEVICE_ATTR_RW(source); + +static bool hdmi_group_visible(struct kobject *kobj) +{ + return alienware_interface == WMAX && alienfx->hdmi_mux; +} +DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(hdmi); + +static struct attribute *hdmi_attrs[] = { + &dev_attr_cable.attr, + &dev_attr_source.attr, + NULL, +}; + +const struct attribute_group wmax_hdmi_attribute_group = { + .name = "hdmi", + .is_visible = SYSFS_GROUP_VISIBLE(hdmi), + .attrs = hdmi_attrs, +}; + +/* + * Alienware GFX amplifier support + * - Currently supports reading cable status + * - Leaving expansion room to possibly support dock/undock events later + */ +static ssize_t status_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args in_args = { + .arg = 0, + }; + u32 out_data; + int ret; + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_AMPLIFIER_CABLE, + &in_args, sizeof(in_args), &out_data); + if (!ret) { + if (out_data == 0) + return sysfs_emit(buf, "[unconnected] connected unknown\n"); + else if (out_data == 1) + return sysfs_emit(buf, "unconnected [connected] unknown\n"); + } + + pr_err("alienware-wmi: unknown amplifier cable status: %d\n", ret); + return sysfs_emit(buf, "unconnected connected [unknown]\n"); +} + +static DEVICE_ATTR_RO(status); + +static bool amplifier_group_visible(struct kobject *kobj) +{ + return alienware_interface == WMAX && alienfx->amplifier; +} +DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(amplifier); + +static struct attribute *amplifier_attrs[] = { + &dev_attr_status.attr, + NULL, +}; + +const struct attribute_group wmax_amplifier_attribute_group = { + .name = "amplifier", + .is_visible = SYSFS_GROUP_VISIBLE(amplifier), + .attrs = amplifier_attrs, +}; + +/* + * Deep Sleep Control support + * - Modifies BIOS setting for deep sleep control allowing extra wakeup events + */ +static ssize_t deepsleep_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args in_args = { + .arg = 0, + }; + u32 out_data; + int ret; + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_DEEP_SLEEP_STATUS, + &in_args, sizeof(in_args), &out_data); + if (!ret) { + if (out_data == 0) + return sysfs_emit(buf, "[disabled] s5 s5_s4\n"); + else if (out_data == 1) + return sysfs_emit(buf, "disabled [s5] s5_s4\n"); + else if (out_data == 2) + return sysfs_emit(buf, "disabled s5 [s5_s4]\n"); + } + + pr_err("alienware-wmi: unknown deep sleep status: %d\n", ret); + return sysfs_emit(buf, "disabled s5 s5_s4 [unknown]\n"); +} + +static ssize_t deepsleep_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct alienfx_platdata *pdata = dev_get_platdata(dev); + struct wmax_basic_args args; + int ret; + + if (strcmp(buf, "disabled\n") == 0) + args.arg = 0; + else if (strcmp(buf, "s5\n") == 0) + args.arg = 1; + else + args.arg = 2; + pr_debug("alienware-wmi: setting deep sleep to %d : %s", args.arg, buf); + + ret = alienware_wmi_command(pdata->wdev, WMAX_METHOD_DEEP_SLEEP_CONTROL, + &args, sizeof(args), NULL); + if (!ret) + pr_err("alienware-wmi: deep sleep control failed: results: %u\n", ret); + + return count; +} + +static DEVICE_ATTR_RW(deepsleep); + +static bool deepsleep_group_visible(struct kobject *kobj) +{ + return alienware_interface == WMAX && alienfx->deepslp; +} +DEFINE_SIMPLE_SYSFS_GROUP_VISIBLE(deepsleep); + +static struct attribute *deepsleep_attrs[] = { + &dev_attr_deepsleep.attr, + NULL, +}; + +const struct attribute_group wmax_deepsleep_attribute_group = { + .name = "deepsleep", + .is_visible = SYSFS_GROUP_VISIBLE(deepsleep), + .attrs = deepsleep_attrs, +}; + +/* + * AWCC Helpers + */ +static int awcc_profile_to_pprof(enum awcc_thermal_profile profile, + enum platform_profile_option *pprof) +{ + switch (profile) { + case AWCC_PROFILE_SPECIAL_CUSTOM: + *pprof = PLATFORM_PROFILE_CUSTOM; + break; + case AWCC_PROFILE_LEGACY_QUIET: + case AWCC_PROFILE_USTT_QUIET: + *pprof = PLATFORM_PROFILE_QUIET; + break; + case AWCC_PROFILE_LEGACY_BALANCED: + case AWCC_PROFILE_USTT_BALANCED: + *pprof = PLATFORM_PROFILE_BALANCED; + break; + case AWCC_PROFILE_LEGACY_BALANCED_PERFORMANCE: + case AWCC_PROFILE_USTT_BALANCED_PERFORMANCE: + *pprof = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + break; + case AWCC_PROFILE_LEGACY_PERFORMANCE: + case AWCC_PROFILE_USTT_PERFORMANCE: + case AWCC_PROFILE_SPECIAL_GMODE: + *pprof = PLATFORM_PROFILE_PERFORMANCE; + break; + case AWCC_PROFILE_USTT_COOL: + *pprof = PLATFORM_PROFILE_COOL; + break; + case AWCC_PROFILE_USTT_LOW_POWER: + *pprof = PLATFORM_PROFILE_LOW_POWER; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int awcc_wmi_command(struct wmi_device *wdev, u32 method_id, + struct wmax_u32_args *args, u32 *out) +{ + int ret; + + ret = alienware_wmi_command(wdev, method_id, args, sizeof(*args), out); + if (ret) + return ret; + + if (*out == AWCC_FAILURE_CODE || *out == AWCC_FAILURE_CODE_2) + return -EBADRQC; + + return 0; +} + +static int awcc_get_fan_sensors(struct wmi_device *wdev, u8 operation, + u8 fan_id, u8 index, u32 *out) +{ + struct wmax_u32_args args = { + .operation = operation, + .arg1 = fan_id, + .arg2 = index, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_GET_FAN_SENSORS, &args, out); +} + +static int awcc_thermal_information(struct wmi_device *wdev, u8 operation, u8 arg, + u32 *out) +{ + struct wmax_u32_args args = { + .operation = operation, + .arg1 = arg, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, out); +} + +static int awcc_fwup_gpio_control(struct wmi_device *wdev, u8 pin, u8 status) +{ + struct wmax_u32_args args = { + .operation = pin, + .arg1 = status, + .arg2 = 0, + .arg3 = 0, + }; + u32 out; + + return awcc_wmi_command(wdev, AWCC_METHOD_FWUP_GPIO_CONTROL, &args, &out); +} + +static int awcc_read_total_gpios(struct wmi_device *wdev, u32 *count) +{ + struct wmax_u32_args args = {}; + + return awcc_wmi_command(wdev, AWCC_METHOD_READ_TOTAL_GPIOS, &args, count); +} + +static int awcc_read_gpio_status(struct wmi_device *wdev, u8 pin, u32 *status) +{ + struct wmax_u32_args args = { + .operation = pin, + .arg1 = 0, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_READ_GPIO_STATUS, &args, status); +} + +static int awcc_game_shift_status(struct wmi_device *wdev, u8 operation, + u32 *out) +{ + struct wmax_u32_args args = { + .operation = operation, + .arg1 = 0, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_GAME_SHIFT_STATUS, &args, out); +} + +/** + * awcc_op_get_resource_id - Get the resource ID at a given index + * @wdev: AWCC WMI device + * @index: Index + * @out: Value returned by the WMI call + * + * Get the resource ID at a given @index. Resource IDs are listed in the + * following order: + * + * - Fan IDs + * - Sensor IDs + * - Unknown IDs + * - Thermal Profile IDs + * + * The total number of IDs of a given type can be obtained with + * AWCC_OP_GET_SYSTEM_DESCRIPTION. + * + * Return: 0 on success, -errno on failure + */ +static int awcc_op_get_resource_id(struct wmi_device *wdev, u8 index, u8 *out) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_GET_RESOURCE_ID, + .arg1 = index, + .arg2 = 0, + .arg3 = 0, + }; + u32 out_data; + int ret; + + ret = awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, &out_data); + if (ret) + return ret; + + *out = FIELD_GET(AWCC_RESOURCE_ID_MASK, out_data); + + return 0; +} + +static int awcc_op_get_fan_rpm(struct wmi_device *wdev, u8 fan_id, u32 *out) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_GET_FAN_RPM, + .arg1 = fan_id, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, out); +} + +static int awcc_op_get_temperature(struct wmi_device *wdev, u8 temp_id, u32 *out) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_GET_TEMPERATURE, + .arg1 = temp_id, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, out); +} + +static int awcc_op_get_fan_boost(struct wmi_device *wdev, u8 fan_id, u32 *out) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_GET_FAN_BOOST, + .arg1 = fan_id, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, out); +} + +static int awcc_op_get_current_profile(struct wmi_device *wdev, u32 *out) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_GET_CURRENT_PROFILE, + .arg1 = 0, + .arg2 = 0, + .arg3 = 0, + }; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_INFORMATION, &args, out); +} + +static int awcc_op_activate_profile(struct wmi_device *wdev, u8 profile) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_ACTIVATE_PROFILE, + .arg1 = profile, + .arg2 = 0, + .arg3 = 0, + }; + u32 out; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_CONTROL, &args, &out); +} + +static int awcc_op_set_fan_boost(struct wmi_device *wdev, u8 fan_id, u8 boost) +{ + struct wmax_u32_args args = { + .operation = AWCC_OP_SET_FAN_BOOST, + .arg1 = fan_id, + .arg2 = boost, + .arg3 = 0, + }; + u32 out; + + return awcc_wmi_command(wdev, AWCC_METHOD_THERMAL_CONTROL, &args, &out); +} + +/* + * HWMON + * - Provides temperature and fan speed monitoring as well as manual fan + * control + */ +static umode_t awcc_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct awcc_priv *priv = drvdata; + unsigned int temp_count; + + switch (type) { + case hwmon_temp: + temp_count = bitmap_weight(priv->temp_sensors, AWCC_ID_BITMAP_SIZE); + + return channel < temp_count ? 0444 : 0; + case hwmon_fan: + return channel < priv->fan_count ? 0444 : 0; + case hwmon_pwm: + return channel < priv->fan_count ? 0444 : 0; + default: + return 0; + } +} + +static int awcc_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + const struct awcc_fan_data *fan; + u32 state; + int ret; + u8 temp; + + switch (type) { + case hwmon_temp: + temp = find_nth_bit(priv->temp_sensors, AWCC_ID_BITMAP_SIZE, channel); + + switch (attr) { + case hwmon_temp_input: + ret = awcc_op_get_temperature(priv->wdev, temp, &state); + if (ret) + return ret; + + *val = state * MILLIDEGREE_PER_DEGREE; + break; + default: + return -EOPNOTSUPP; + } + + break; + case hwmon_fan: + fan = priv->fan_data[channel]; + + switch (attr) { + case hwmon_fan_input: + ret = awcc_op_get_fan_rpm(priv->wdev, fan->id, &state); + if (ret) + return ret; + + *val = state; + break; + case hwmon_fan_min: + *val = fan->min_rpm; + break; + case hwmon_fan_max: + *val = fan->max_rpm; + break; + default: + return -EOPNOTSUPP; + } + + break; + case hwmon_pwm: + fan = priv->fan_data[channel]; + + switch (attr) { + case hwmon_pwm_auto_channels_temp: + *val = fan->auto_channels_temp; + break; + default: + return -EOPNOTSUPP; + } + + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int awcc_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + u8 temp; + + switch (type) { + case hwmon_temp: + temp = find_nth_bit(priv->temp_sensors, AWCC_ID_BITMAP_SIZE, channel); + + switch (temp) { + case AWCC_TEMP_SENSOR_CPU: + *str = "CPU"; + break; + case AWCC_TEMP_SENSOR_FRONT: + *str = "Front"; + break; + case AWCC_TEMP_SENSOR_GPU: + *str = "GPU"; + break; + default: + *str = "Unknown"; + break; + } + + break; + case hwmon_fan: + switch (priv->fan_data[channel]->id) { + case AWCC_FAN_CPU_1: + case AWCC_FAN_CPU_2: + *str = "CPU Fan"; + break; + case AWCC_FAN_GPU_1: + case AWCC_FAN_GPU_2: + *str = "GPU Fan"; + break; + case AWCC_FAN_PCI: + *str = "PCI Fan"; + break; + case AWCC_FAN_MID: + *str = "Mid Fan"; + break; + case AWCC_FAN_TOP_1: + case AWCC_FAN_TOP_2: + case AWCC_FAN_TOP_3: + *str = "Top Fan"; + break; + case AWCC_FAN_SIDE: + *str = "Side Fan"; + break; + case AWCC_FAN_U2_1: + case AWCC_FAN_U2_2: + *str = "U.2 Fan"; + break; + case AWCC_FAN_FRONT_1: + case AWCC_FAN_FRONT_2: + *str = "Front Fan"; + break; + case AWCC_FAN_BOTTOM_1: + case AWCC_FAN_BOTTOM_2: + *str = "Bottom Fan"; + break; + default: + *str = "Unknown Fan"; + break; + } + + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static const struct hwmon_ops awcc_hwmon_ops = { + .is_visible = awcc_hwmon_is_visible, + .read = awcc_hwmon_read, + .read_string = awcc_hwmon_read_string, +}; + +static const struct hwmon_channel_info * const awcc_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_LABEL | HWMON_T_INPUT, + HWMON_T_LABEL | HWMON_T_INPUT, + HWMON_T_LABEL | HWMON_T_INPUT, + HWMON_T_LABEL | HWMON_T_INPUT, + HWMON_T_LABEL | HWMON_T_INPUT, + HWMON_T_LABEL | HWMON_T_INPUT + ), + HWMON_CHANNEL_INFO(fan, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_LABEL | HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX + ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_AUTO_CHANNELS_TEMP, + HWMON_PWM_AUTO_CHANNELS_TEMP, + HWMON_PWM_AUTO_CHANNELS_TEMP, + HWMON_PWM_AUTO_CHANNELS_TEMP, + HWMON_PWM_AUTO_CHANNELS_TEMP, + HWMON_PWM_AUTO_CHANNELS_TEMP + ), + NULL +}; + +static const struct hwmon_chip_info awcc_hwmon_chip_info = { + .ops = &awcc_hwmon_ops, + .info = awcc_hwmon_info, +}; + +static ssize_t fan_boost_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + int index = to_sensor_dev_attr(attr)->index; + struct awcc_fan_data *fan = priv->fan_data[index]; + u32 boost; + int ret; + + ret = awcc_op_get_fan_boost(priv->wdev, fan->id, &boost); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", boost); +} + +static ssize_t fan_boost_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + int index = to_sensor_dev_attr(attr)->index; + struct awcc_fan_data *fan = priv->fan_data[index]; + unsigned long val; + int ret; + + ret = kstrtoul(buf, 0, &val); + if (ret) + return ret; + + ret = awcc_op_set_fan_boost(priv->wdev, fan->id, clamp_val(val, 0, 255)); + + return ret ? ret : count; +} + +static SENSOR_DEVICE_ATTR_RW(fan1_boost, fan_boost, 0); +static SENSOR_DEVICE_ATTR_RW(fan2_boost, fan_boost, 1); +static SENSOR_DEVICE_ATTR_RW(fan3_boost, fan_boost, 2); +static SENSOR_DEVICE_ATTR_RW(fan4_boost, fan_boost, 3); +static SENSOR_DEVICE_ATTR_RW(fan5_boost, fan_boost, 4); +static SENSOR_DEVICE_ATTR_RW(fan6_boost, fan_boost, 5); + +static umode_t fan_boost_attr_visible(struct kobject *kobj, struct attribute *attr, int n) +{ + struct awcc_priv *priv = dev_get_drvdata(kobj_to_dev(kobj)); + + return n < priv->fan_count ? attr->mode : 0; +} + +static bool fan_boost_group_visible(struct kobject *kobj) +{ + return true; +} + +DEFINE_SYSFS_GROUP_VISIBLE(fan_boost); + +static struct attribute *fan_boost_attrs[] = { + &sensor_dev_attr_fan1_boost.dev_attr.attr, + &sensor_dev_attr_fan2_boost.dev_attr.attr, + &sensor_dev_attr_fan3_boost.dev_attr.attr, + &sensor_dev_attr_fan4_boost.dev_attr.attr, + &sensor_dev_attr_fan5_boost.dev_attr.attr, + &sensor_dev_attr_fan6_boost.dev_attr.attr, + NULL +}; + +static const struct attribute_group fan_boost_group = { + .attrs = fan_boost_attrs, + .is_visible = SYSFS_GROUP_VISIBLE(fan_boost), +}; + +static const struct attribute_group *awcc_hwmon_groups[] = { + &fan_boost_group, + NULL +}; + +static int awcc_hwmon_temps_init(struct wmi_device *wdev) +{ + struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); + unsigned int i; + int ret; + u8 id; + + for (i = 0; i < priv->temp_count; i++) { + /* + * Temperature sensors IDs are listed after the fan IDs at + * offset `fan_count` + */ + ret = awcc_op_get_resource_id(wdev, i + priv->fan_count, &id); + if (ret) + return ret; + + __set_bit(id, priv->temp_sensors); + } + + return 0; +} + +static int awcc_hwmon_fans_init(struct wmi_device *wdev) +{ + struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); + unsigned long fan_temps[AWCC_ID_BITMAP_LONGS]; + unsigned long gather[AWCC_ID_BITMAP_LONGS]; + u32 min_rpm, max_rpm, temp_count, temp_id; + struct awcc_fan_data *fan_data; + unsigned int i, j; + int ret; + u8 id; + + for (i = 0; i < priv->fan_count; i++) { + fan_data = devm_kzalloc(&wdev->dev, sizeof(*fan_data), GFP_KERNEL); + if (!fan_data) + return -ENOMEM; + + /* + * Fan IDs are listed first at offset 0 + */ + ret = awcc_op_get_resource_id(wdev, i, &id); + if (ret) + return ret; + + ret = awcc_thermal_information(wdev, AWCC_OP_GET_FAN_MIN_RPM, id, + &min_rpm); + if (ret) + return ret; + + ret = awcc_thermal_information(wdev, AWCC_OP_GET_FAN_MAX_RPM, id, + &max_rpm); + if (ret) + return ret; + + ret = awcc_get_fan_sensors(wdev, AWCC_OP_GET_TOTAL_FAN_TEMPS, id, + 0, &temp_count); + if (ret) + return ret; + + bitmap_zero(fan_temps, AWCC_ID_BITMAP_SIZE); + + for (j = 0; j < temp_count; j++) { + ret = awcc_get_fan_sensors(wdev, AWCC_OP_GET_FAN_TEMP_ID, + id, j, &temp_id); + if (ret) + break; + + temp_id = FIELD_GET(AWCC_RESOURCE_ID_MASK, temp_id); + __set_bit(temp_id, fan_temps); + } + + fan_data->id = id; + fan_data->min_rpm = min_rpm; + fan_data->max_rpm = max_rpm; + bitmap_gather(gather, fan_temps, priv->temp_sensors, AWCC_ID_BITMAP_SIZE); + bitmap_copy(&fan_data->auto_channels_temp, gather, BITS_PER_LONG); + priv->fan_data[i] = fan_data; + } + + return 0; +} + +static int awcc_hwmon_init(struct wmi_device *wdev) +{ + struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); + int ret; + + priv->fan_data = devm_kcalloc(&wdev->dev, priv->fan_count, + sizeof(*priv->fan_data), GFP_KERNEL); + if (!priv->fan_data) + return -ENOMEM; + + ret = awcc_hwmon_temps_init(wdev); + if (ret) + return ret; + + ret = awcc_hwmon_fans_init(wdev); + if (ret) + return ret; + + priv->hwdev = devm_hwmon_device_register_with_info(&wdev->dev, "alienware_wmi", + priv, &awcc_hwmon_chip_info, + awcc_hwmon_groups); + + return PTR_ERR_OR_ZERO(priv->hwdev); +} + +static void awcc_hwmon_suspend(struct device *dev) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + struct awcc_fan_data *fan; + unsigned int i; + u32 boost; + int ret; + + for (i = 0; i < priv->fan_count; i++) { + fan = priv->fan_data[i]; + + ret = awcc_thermal_information(priv->wdev, AWCC_OP_GET_FAN_BOOST, + fan->id, &boost); + if (ret) + dev_err(dev, "Failed to store Fan %u boost while suspending\n", i); + + fan->suspend_cache = ret ? 0 : clamp_val(boost, 0, 255); + + awcc_op_set_fan_boost(priv->wdev, fan->id, 0); + if (ret) + dev_err(dev, "Failed to set Fan %u boost to 0 while suspending\n", i); + } +} + +static void awcc_hwmon_resume(struct device *dev) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + struct awcc_fan_data *fan; + unsigned int i; + int ret; + + for (i = 0; i < priv->fan_count; i++) { + fan = priv->fan_data[i]; + + if (!fan->suspend_cache) + continue; + + ret = awcc_op_set_fan_boost(priv->wdev, fan->id, fan->suspend_cache); + if (ret) + dev_err(dev, "Failed to restore Fan %u boost while resuming\n", i); + } +} + +/* + * Thermal Profile control + * - Provides thermal profile control through the Platform Profile API + */ +static int awcc_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + u32 out_data; + int ret; + + ret = awcc_op_get_current_profile(priv->wdev, &out_data); + if (ret) + return ret; + + return awcc_profile_to_pprof(out_data, profile); +} + +static int awcc_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct awcc_priv *priv = dev_get_drvdata(dev); + + if (awcc->gmode) { + u32 gmode_status; + int ret; + + ret = awcc_game_shift_status(priv->wdev, + AWCC_OP_GET_GAME_SHIFT_STATUS, + &gmode_status); + + if (ret < 0) + return ret; + + if ((profile == PLATFORM_PROFILE_PERFORMANCE && !gmode_status) || + (profile != PLATFORM_PROFILE_PERFORMANCE && gmode_status)) { + ret = awcc_game_shift_status(priv->wdev, + AWCC_OP_TOGGLE_GAME_SHIFT, + &gmode_status); + + if (ret < 0) + return ret; + } + } + + return awcc_op_activate_profile(priv->wdev, priv->supported_profiles[profile]); +} + +static int awcc_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + enum platform_profile_option profile; + struct awcc_priv *priv = drvdata; + u8 id, offset = 0; + int ret; + + /* + * Thermal profile IDs are listed last at offset + * fan_count + temp_count + unknown_count + */ + for (unsigned int i = 0; i < ARRAY_SIZE(priv->res_count) - 1; i++) + offset += priv->res_count[i]; + + for (unsigned int i = 0; i < priv->profile_count; i++) { + ret = awcc_op_get_resource_id(priv->wdev, i + offset, &id); + /* + * Some devices report an incorrect number of thermal profiles + * so the resource ID list may end prematurely + */ + if (ret == -EBADRQC) + break; + if (ret) + return ret; + + /* + * G-Mode profile ID is not listed consistently across modeles + * that support it, therefore we handle it through quirks. + */ + if (id == AWCC_PROFILE_SPECIAL_GMODE) + continue; + + ret = awcc_profile_to_pprof(id, &profile); + if (ret) { + dev_dbg(&priv->wdev->dev, "Unmapped thermal profile ID 0x%02x\n", id); + continue; + } + + priv->supported_profiles[profile] = id; + __set_bit(profile, choices); + } + + if (bitmap_empty(choices, PLATFORM_PROFILE_LAST)) + return -ENODEV; + + if (awcc->gmode) { + priv->supported_profiles[PLATFORM_PROFILE_PERFORMANCE] = + AWCC_PROFILE_SPECIAL_GMODE; + + __set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + } + + /* Every model supports the "custom" profile */ + priv->supported_profiles[PLATFORM_PROFILE_CUSTOM] = + AWCC_PROFILE_SPECIAL_CUSTOM; + + __set_bit(PLATFORM_PROFILE_CUSTOM, choices); + + return 0; +} + +static const struct platform_profile_ops awcc_platform_profile_ops = { + .probe = awcc_platform_profile_probe, + .profile_get = awcc_platform_profile_get, + .profile_set = awcc_platform_profile_set, +}; + +static int awcc_platform_profile_init(struct wmi_device *wdev) +{ + struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); + + priv->ppdev = devm_platform_profile_register(&wdev->dev, "alienware-wmi", + priv, &awcc_platform_profile_ops); + + return PTR_ERR_OR_ZERO(priv->ppdev); +} + +/* + * DebugFS + */ +static int awcc_debugfs_system_description_read(struct seq_file *seq, void *data) +{ + struct device *dev = seq->private; + struct awcc_priv *priv = dev_get_drvdata(dev); + + seq_printf(seq, "0x%08x\n", priv->system_description); + + return 0; +} + +static int awcc_debugfs_hwmon_data_read(struct seq_file *seq, void *data) +{ + struct device *dev = seq->private; + struct awcc_priv *priv = dev_get_drvdata(dev); + const struct awcc_fan_data *fan; + unsigned int bit; + + seq_printf(seq, "Number of fans: %u\n", priv->fan_count); + seq_printf(seq, "Number of temperature sensors: %u\n\n", priv->temp_count); + + for (u32 i = 0; i < priv->fan_count; i++) { + fan = priv->fan_data[i]; + + seq_printf(seq, "Fan %u:\n", i); + seq_printf(seq, " ID: 0x%02x\n", fan->id); + seq_printf(seq, " Related temperature sensors bitmap: %lu\n", + fan->auto_channels_temp); + } + + seq_puts(seq, "\nTemperature sensor IDs:\n"); + for_each_set_bit(bit, priv->temp_sensors, AWCC_ID_BITMAP_SIZE) + seq_printf(seq, " 0x%02x\n", bit); + + return 0; +} + +static int awcc_debugfs_pprof_data_read(struct seq_file *seq, void *data) +{ + struct device *dev = seq->private; + struct awcc_priv *priv = dev_get_drvdata(dev); + + seq_printf(seq, "Number of thermal profiles: %u\n\n", priv->profile_count); + + for (u32 i = 0; i < PLATFORM_PROFILE_LAST; i++) { + if (!priv->supported_profiles[i]) + continue; + + seq_printf(seq, "Platform profile %u:\n", i); + seq_printf(seq, " ID: 0x%02x\n", priv->supported_profiles[i]); + } + + return 0; +} + +static int awcc_gpio_pin_show(struct seq_file *seq, void *data) +{ + unsigned long pin = debugfs_get_aux_num(seq->file); + struct wmi_device *wdev = seq->private; + u32 status; + int ret; + + ret = awcc_read_gpio_status(wdev, pin, &status); + if (ret) + return ret; + + seq_printf(seq, "%u\n", status); + + return 0; +} + +static ssize_t awcc_gpio_pin_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + unsigned long pin = debugfs_get_aux_num(file); + struct seq_file *seq = file->private_data; + struct wmi_device *wdev = seq->private; + bool status; + int ret; + + if (!ppos || *ppos) + return -EINVAL; + + ret = kstrtobool_from_user(buf, count, &status); + if (ret) + return ret; + + ret = awcc_fwup_gpio_control(wdev, pin, status); + if (ret) + return ret; + + return count; +} + +DEFINE_SHOW_STORE_ATTRIBUTE(awcc_gpio_pin); + +static void awcc_debugfs_remove(void *data) +{ + struct dentry *root = data; + + debugfs_remove(root); +} + +static void awcc_debugfs_init(struct wmi_device *wdev) +{ + struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); + struct dentry *root, *gpio_ctl; + u32 gpio_count; + char name[64]; + int ret; + + scnprintf(name, sizeof(name), "%s-%s", "alienware-wmi", dev_name(&wdev->dev)); + root = debugfs_create_dir(name, NULL); + + debugfs_create_devm_seqfile(&wdev->dev, "system_description", root, + awcc_debugfs_system_description_read); + + if (awcc->hwmon) + debugfs_create_devm_seqfile(&wdev->dev, "hwmon_data", root, + awcc_debugfs_hwmon_data_read); + + if (awcc->pprof) + debugfs_create_devm_seqfile(&wdev->dev, "pprof_data", root, + awcc_debugfs_pprof_data_read); + + ret = awcc_read_total_gpios(wdev, &gpio_count); + if (ret) { + dev_dbg(&wdev->dev, "Failed to get total GPIO Pin count\n"); + goto out_add_action; + } else if (gpio_count > AWCC_MAX_RES_COUNT) { + dev_dbg(&wdev->dev, "Reported GPIO Pin count may be incorrect: %u\n", gpio_count); + goto out_add_action; + } + + gpio_ctl = debugfs_create_dir("gpio_ctl", root); + + priv->gpio_count = gpio_count; + debugfs_create_u32("total_gpios", 0444, gpio_ctl, &priv->gpio_count); + + for (unsigned int i = 0; i < gpio_count; i++) { + scnprintf(name, sizeof(name), "pin%u", i); + debugfs_create_file_aux_num(name, 0644, gpio_ctl, wdev, i, + &awcc_gpio_pin_fops); + } + +out_add_action: + devm_add_action_or_reset(&wdev->dev, awcc_debugfs_remove, root); +} + +static int alienware_awcc_setup(struct wmi_device *wdev) +{ + struct awcc_priv *priv; + int ret; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + ret = awcc_thermal_information(wdev, AWCC_OP_GET_SYSTEM_DESCRIPTION, + 0, &priv->system_description); + if (ret < 0) + return ret; + + /* Sanity check */ + for (unsigned int i = 0; i < ARRAY_SIZE(priv->res_count); i++) { + if (priv->res_count[i] > AWCC_MAX_RES_COUNT) { + dev_err(&wdev->dev, "Malformed system description: 0x%08x\n", + priv->system_description); + return -ENXIO; + } + } + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + if (awcc->hwmon) { + ret = awcc_hwmon_init(wdev); + if (ret) + return ret; + } + + if (awcc->pprof) { + ret = awcc_platform_profile_init(wdev); + if (ret) + return ret; + } + + awcc_debugfs_init(wdev); + + return 0; +} + +/* + * WMAX WMI driver + */ +static int wmax_wmi_update_led(struct alienfx_priv *priv, + struct wmi_device *wdev, u8 location) +{ + struct wmax_led_args in_args = { + .led_mask = 1 << location, + .colors = priv->colors[location], + .state = priv->lighting_control_state, + }; + + return alienware_wmi_command(wdev, WMAX_METHOD_ZONE_CONTROL, &in_args, + sizeof(in_args), NULL); +} + +static int wmax_wmi_update_brightness(struct alienfx_priv *priv, + struct wmi_device *wdev, u8 brightness) +{ + struct wmax_brightness_args in_args = { + .led_mask = 0xFF, + .percentage = brightness, + }; + + return alienware_wmi_command(wdev, WMAX_METHOD_BRIGHTNESS, &in_args, + sizeof(in_args), NULL); +} + +static int wmax_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct alienfx_platdata pdata = { + .wdev = wdev, + .ops = { + .upd_led = wmax_wmi_update_led, + .upd_brightness = wmax_wmi_update_brightness, + }, + }; + int ret; + + if (awcc) + ret = alienware_awcc_setup(wdev); + else + ret = alienware_alienfx_setup(&pdata); + + return ret; +} + +static int wmax_wmi_suspend(struct device *dev) +{ + if (awcc && awcc->hwmon) + awcc_hwmon_suspend(dev); + + return 0; +} + +static int wmax_wmi_resume(struct device *dev) +{ + if (awcc && awcc->hwmon) + awcc_hwmon_resume(dev); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(wmax_wmi_pm_ops, wmax_wmi_suspend, wmax_wmi_resume); + +static const struct wmi_device_id alienware_wmax_device_id_table[] = { + { WMAX_CONTROL_GUID, NULL }, + { }, +}; +MODULE_DEVICE_TABLE(wmi, alienware_wmax_device_id_table); + +static struct wmi_driver alienware_wmax_wmi_driver = { + .driver = { + .name = "alienware-wmi-wmax", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .pm = pm_sleep_ptr(&wmax_wmi_pm_ops), + }, + .id_table = alienware_wmax_device_id_table, + .probe = wmax_wmi_probe, + .no_singleton = true, +}; + +int __init alienware_wmax_wmi_init(void) +{ + const struct dmi_system_id *id; + + id = dmi_first_match(awcc_dmi_table); + if (id) + awcc = id->driver_data; + + if (force_hwmon) { + if (!awcc) + awcc = &empty_quirks; + + awcc->hwmon = true; + } + + if (force_platform_profile) { + if (!awcc) + awcc = &empty_quirks; + + awcc->pprof = true; + } + + if (force_gmode) { + if (awcc) + awcc->gmode = true; + else + pr_warn("force_gmode requires platform profile support\n"); + } + + return wmi_driver_register(&alienware_wmax_wmi_driver); +} + +void __exit alienware_wmax_wmi_exit(void) +{ + wmi_driver_unregister(&alienware_wmax_wmi_driver); +} diff --git a/drivers/platform/x86/dell/alienware-wmi.h b/drivers/platform/x86/dell/alienware-wmi.h new file mode 100644 index 000000000..68d424221 --- /dev/null +++ b/drivers/platform/x86/dell/alienware-wmi.h @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Alienware WMI special features driver + * + * Copyright (C) 2014 Dell Inc + * Copyright (C) 2024 Kurt Borja + */ + +#ifndef _ALIENWARE_WMI_H_ +#define _ALIENWARE_WMI_H_ + +#include +#include +#include + +#define LEGACY_CONTROL_GUID "A90597CE-A997-11DA-B012-B622A1EF5492" +#define LEGACY_POWER_CONTROL_GUID "A80593CE-A997-11DA-B012-B622A1EF5492" +#define WMAX_CONTROL_GUID "A70591CE-A997-11DA-B012-B622A1EF5492" + +enum INTERFACE_FLAGS { + LEGACY, + WMAX, +}; + +enum LEGACY_CONTROL_STATES { + LEGACY_RUNNING = 1, + LEGACY_BOOTING = 0, + LEGACY_SUSPEND = 3, +}; + +enum WMAX_CONTROL_STATES { + WMAX_RUNNING = 0xFF, + WMAX_BOOTING = 0, + WMAX_SUSPEND = 3, +}; + +struct alienfx_quirks { + u8 num_zones; + bool hdmi_mux; + bool amplifier; + bool deepslp; +}; + +struct color_platform { + u8 blue; + u8 green; + u8 red; +} __packed; + +struct alienfx_priv { + struct platform_device *pdev; + struct led_classdev global_led; + struct color_platform colors[4]; + u8 global_brightness; + u8 lighting_control_state; +}; + +struct alienfx_ops { + int (*upd_led)(struct alienfx_priv *priv, struct wmi_device *wdev, + u8 location); + int (*upd_brightness)(struct alienfx_priv *priv, struct wmi_device *wdev, + u8 brightness); +}; + +struct alienfx_platdata { + struct wmi_device *wdev; + struct alienfx_ops ops; +}; + +extern u8 alienware_interface; +extern struct alienfx_quirks *alienfx; + +int alienware_wmi_command(struct wmi_device *wdev, u32 method_id, + void *in_args, size_t in_size, u32 *out_data); + +int alienware_alienfx_setup(struct alienfx_platdata *pdata); + +#if IS_ENABLED(CONFIG_ALIENWARE_WMI_LEGACY) +int __init alienware_legacy_wmi_init(void); +void __exit alienware_legacy_wmi_exit(void); +#else +static inline int alienware_legacy_wmi_init(void) +{ + return -ENODEV; +} + +static inline void alienware_legacy_wmi_exit(void) +{ +} +#endif + +#if IS_ENABLED(CONFIG_ALIENWARE_WMI_WMAX) +extern const struct attribute_group wmax_hdmi_attribute_group; +extern const struct attribute_group wmax_amplifier_attribute_group; +extern const struct attribute_group wmax_deepsleep_attribute_group; + +#define WMAX_DEV_GROUPS &wmax_hdmi_attribute_group, \ + &wmax_amplifier_attribute_group, \ + &wmax_deepsleep_attribute_group, + +int __init alienware_wmax_wmi_init(void); +void __exit alienware_wmax_wmi_exit(void); +#else +#define WMAX_DEV_GROUPS + +static inline int alienware_wmax_wmi_init(void) +{ + return -ENODEV; +} + + +static inline void alienware_wmax_wmi_exit(void) +{ +} +#endif + +#endif diff --git a/drivers/platform/x86/dell/dcdbas.c b/drivers/platform/x86/dell/dcdbas.c new file mode 100644 index 000000000..678f44252 --- /dev/null +++ b/drivers/platform/x86/dell/dcdbas.c @@ -0,0 +1,781 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * dcdbas.c: Dell Systems Management Base Driver + * + * The Dell Systems Management Base Driver provides a sysfs interface for + * systems management software to perform System Management Interrupts (SMIs) + * and Host Control Actions (power cycle or power off after OS shutdown) on + * Dell systems. + * + * See Documentation/userspace-api/dcdbas.rst for more information. + * + * Copyright (C) 1995-2006 Dell Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dcdbas.h" + +#define DRIVER_NAME "dcdbas" +#define DRIVER_VERSION "5.6.0-3.4" +#define DRIVER_DESCRIPTION "Dell Systems Management Base Driver" + +static struct platform_device *dcdbas_pdev; + +static unsigned long max_smi_data_buf_size = MAX_SMI_DATA_BUF_SIZE; +static DEFINE_MUTEX(smi_data_lock); +static u8 *bios_buffer; +static struct smi_buffer smi_buf; + +static unsigned int host_control_action; +static unsigned int host_control_smi_type; +static unsigned int host_control_on_shutdown; + +static bool wsmt_enabled; + +int dcdbas_smi_alloc(struct smi_buffer *smi_buffer, unsigned long size) +{ + smi_buffer->virt = dma_alloc_coherent(&dcdbas_pdev->dev, size, + &smi_buffer->dma, GFP_KERNEL); + if (!smi_buffer->virt) { + dev_dbg(&dcdbas_pdev->dev, + "%s: failed to allocate memory size %lu\n", + __func__, size); + return -ENOMEM; + } + smi_buffer->size = size; + + dev_dbg(&dcdbas_pdev->dev, "%s: phys: %x size: %lu\n", + __func__, (u32)smi_buffer->dma, smi_buffer->size); + + return 0; +} +EXPORT_SYMBOL_GPL(dcdbas_smi_alloc); + +void dcdbas_smi_free(struct smi_buffer *smi_buffer) +{ + if (!smi_buffer->virt) + return; + + dev_dbg(&dcdbas_pdev->dev, "%s: phys: %x size: %lu\n", + __func__, (u32)smi_buffer->dma, smi_buffer->size); + dma_free_coherent(&dcdbas_pdev->dev, smi_buffer->size, + smi_buffer->virt, smi_buffer->dma); + smi_buffer->virt = NULL; + smi_buffer->dma = 0; + smi_buffer->size = 0; +} +EXPORT_SYMBOL_GPL(dcdbas_smi_free); + +/** + * smi_data_buf_free: free SMI data buffer + */ +static void smi_data_buf_free(void) +{ + if (!smi_buf.virt || wsmt_enabled) + return; + + dcdbas_smi_free(&smi_buf); +} + +/** + * smi_data_buf_realloc: grow SMI data buffer if needed + */ +static int smi_data_buf_realloc(unsigned long size) +{ + struct smi_buffer tmp; + int ret; + + if (smi_buf.size >= size) + return 0; + + if (size > max_smi_data_buf_size) + return -EINVAL; + + /* new buffer is needed */ + ret = dcdbas_smi_alloc(&tmp, size); + if (ret) + return ret; + + /* memory zeroed by dma_alloc_coherent */ + if (smi_buf.virt) + memcpy(tmp.virt, smi_buf.virt, smi_buf.size); + + /* free any existing buffer */ + smi_data_buf_free(); + + /* set up new buffer for use */ + smi_buf = tmp; + + return 0; +} + +static ssize_t smi_data_buf_phys_addr_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%x\n", (u32)smi_buf.dma); +} + +static ssize_t smi_data_buf_size_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%lu\n", smi_buf.size); +} + +static ssize_t smi_data_buf_size_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long buf_size; + ssize_t ret; + + buf_size = simple_strtoul(buf, NULL, 10); + + /* make sure SMI data buffer is at least buf_size */ + mutex_lock(&smi_data_lock); + ret = smi_data_buf_realloc(buf_size); + mutex_unlock(&smi_data_lock); + if (ret) + return ret; + + return count; +} + +static ssize_t smi_data_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buf, loff_t pos, size_t count) +{ + ssize_t ret; + + mutex_lock(&smi_data_lock); + ret = memory_read_from_buffer(buf, count, &pos, smi_buf.virt, + smi_buf.size); + mutex_unlock(&smi_data_lock); + return ret; +} + +static ssize_t smi_data_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buf, loff_t pos, size_t count) +{ + ssize_t ret; + + if ((pos + count) > max_smi_data_buf_size) + return -EINVAL; + + mutex_lock(&smi_data_lock); + + ret = smi_data_buf_realloc(pos + count); + if (ret) + goto out; + + memcpy(smi_buf.virt + pos, buf, count); + ret = count; +out: + mutex_unlock(&smi_data_lock); + return ret; +} + +static ssize_t host_control_action_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u\n", host_control_action); +} + +static ssize_t host_control_action_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + ssize_t ret; + + /* make sure buffer is available for host control command */ + mutex_lock(&smi_data_lock); + ret = smi_data_buf_realloc(sizeof(struct apm_cmd)); + mutex_unlock(&smi_data_lock); + if (ret) + return ret; + + host_control_action = simple_strtoul(buf, NULL, 10); + return count; +} + +static ssize_t host_control_smi_type_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u\n", host_control_smi_type); +} + +static ssize_t host_control_smi_type_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + host_control_smi_type = simple_strtoul(buf, NULL, 10); + return count; +} + +static ssize_t host_control_on_shutdown_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u\n", host_control_on_shutdown); +} + +static ssize_t host_control_on_shutdown_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + host_control_on_shutdown = simple_strtoul(buf, NULL, 10); + return count; +} + +static int raise_smi(void *par) +{ + struct smi_cmd *smi_cmd = par; + + if (smp_processor_id() != 0) { + dev_dbg(&dcdbas_pdev->dev, "%s: failed to get CPU 0\n", + __func__); + return -EBUSY; + } + + /* generate SMI */ + /* inb to force posted write through and make SMI happen now */ + asm volatile ( + "outb %b0,%w1\n" + "inb %w1" + : /* no output args */ + : "a" (smi_cmd->command_code), + "d" (smi_cmd->command_address), + "b" (smi_cmd->ebx), + "c" (smi_cmd->ecx) + : "memory" + ); + + return 0; +} +/** + * dcdbas_smi_request: generate SMI request + * + * Called with smi_data_lock. + */ +int dcdbas_smi_request(struct smi_cmd *smi_cmd) +{ + int ret; + + if (smi_cmd->magic != SMI_CMD_MAGIC) { + dev_info(&dcdbas_pdev->dev, "%s: invalid magic value\n", + __func__); + return -EBADR; + } + + /* SMI requires CPU 0 */ + cpus_read_lock(); + ret = smp_call_on_cpu(0, raise_smi, smi_cmd, true); + cpus_read_unlock(); + + return ret; +} +EXPORT_SYMBOL(dcdbas_smi_request); + +/** + * smi_request_store: + * + * The valid values are: + * 0: zero SMI data buffer + * 1: generate calling interface SMI + * 2: generate raw SMI + * + * User application writes smi_cmd to smi_data before telling driver + * to generate SMI. + */ +static ssize_t smi_request_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct smi_cmd *smi_cmd; + unsigned long val = simple_strtoul(buf, NULL, 10); + ssize_t ret; + + mutex_lock(&smi_data_lock); + + if (smi_buf.size < sizeof(struct smi_cmd)) { + ret = -ENODEV; + goto out; + } + smi_cmd = (struct smi_cmd *)smi_buf.virt; + + switch (val) { + case 2: + /* Raw SMI */ + ret = dcdbas_smi_request(smi_cmd); + if (!ret) + ret = count; + break; + case 1: + /* + * Calling Interface SMI + * + * Provide physical address of command buffer field within + * the struct smi_cmd to BIOS. + * + * Because the address that smi_cmd (smi_buf.virt) points to + * will be from memremap() of a non-memory address if WSMT + * is present, we can't use virt_to_phys() on smi_cmd, so + * we have to use the physical address that was saved when + * the virtual address for smi_cmd was received. + */ + smi_cmd->ebx = (u32)smi_buf.dma + + offsetof(struct smi_cmd, command_buffer); + ret = dcdbas_smi_request(smi_cmd); + if (!ret) + ret = count; + break; + case 0: + memset(smi_buf.virt, 0, smi_buf.size); + ret = count; + break; + default: + ret = -EINVAL; + break; + } + +out: + mutex_unlock(&smi_data_lock); + return ret; +} + +/** + * host_control_smi: generate host control SMI + * + * Caller must set up the host control command in smi_buf.virt. + */ +static int host_control_smi(void) +{ + struct apm_cmd *apm_cmd; + u8 *data; + unsigned long flags; + u32 num_ticks; + s8 cmd_status; + u8 index; + + apm_cmd = (struct apm_cmd *)smi_buf.virt; + apm_cmd->status = ESM_STATUS_CMD_UNSUCCESSFUL; + + switch (host_control_smi_type) { + case HC_SMITYPE_TYPE1: + spin_lock_irqsave(&rtc_lock, flags); + /* write SMI data buffer physical address */ + data = (u8 *)&smi_buf.dma; + for (index = PE1300_CMOS_CMD_STRUCT_PTR; + index < (PE1300_CMOS_CMD_STRUCT_PTR + 4); + index++, data++) { + outb(index, + (CMOS_BASE_PORT + CMOS_PAGE2_INDEX_PORT_PIIX4)); + outb(*data, + (CMOS_BASE_PORT + CMOS_PAGE2_DATA_PORT_PIIX4)); + } + + /* first set status to -1 as called by spec */ + cmd_status = ESM_STATUS_CMD_UNSUCCESSFUL; + outb((u8) cmd_status, PCAT_APM_STATUS_PORT); + + /* generate SMM call */ + outb(ESM_APM_CMD, PCAT_APM_CONTROL_PORT); + spin_unlock_irqrestore(&rtc_lock, flags); + + /* wait a few to see if it executed */ + num_ticks = TIMEOUT_USEC_SHORT_SEMA_BLOCKING; + while ((s8)inb(PCAT_APM_STATUS_PORT) == ESM_STATUS_CMD_UNSUCCESSFUL) { + num_ticks--; + if (num_ticks == EXPIRED_TIMER) + return -ETIME; + } + break; + + case HC_SMITYPE_TYPE2: + case HC_SMITYPE_TYPE3: + spin_lock_irqsave(&rtc_lock, flags); + /* write SMI data buffer physical address */ + data = (u8 *)&smi_buf.dma; + for (index = PE1400_CMOS_CMD_STRUCT_PTR; + index < (PE1400_CMOS_CMD_STRUCT_PTR + 4); + index++, data++) { + outb(index, (CMOS_BASE_PORT + CMOS_PAGE1_INDEX_PORT)); + outb(*data, (CMOS_BASE_PORT + CMOS_PAGE1_DATA_PORT)); + } + + /* generate SMM call */ + if (host_control_smi_type == HC_SMITYPE_TYPE3) + outb(ESM_APM_CMD, PCAT_APM_CONTROL_PORT); + else + outb(ESM_APM_CMD, PE1400_APM_CONTROL_PORT); + + /* restore RTC index pointer since it was written to above */ + CMOS_READ(RTC_REG_C); + spin_unlock_irqrestore(&rtc_lock, flags); + + /* read control port back to serialize write */ + cmd_status = inb(PE1400_APM_CONTROL_PORT); + + /* wait a few to see if it executed */ + num_ticks = TIMEOUT_USEC_SHORT_SEMA_BLOCKING; + while (apm_cmd->status == ESM_STATUS_CMD_UNSUCCESSFUL) { + num_ticks--; + if (num_ticks == EXPIRED_TIMER) + return -ETIME; + } + break; + + default: + dev_dbg(&dcdbas_pdev->dev, "%s: invalid SMI type %u\n", + __func__, host_control_smi_type); + return -ENOSYS; + } + + return 0; +} + +/** + * dcdbas_host_control: initiate host control + * + * This function is called by the driver after the system has + * finished shutting down if the user application specified a + * host control action to perform on shutdown. It is safe to + * use smi_buf.virt at this point because the system has finished + * shutting down and no userspace apps are running. + */ +static void dcdbas_host_control(void) +{ + struct apm_cmd *apm_cmd; + u8 action; + + if (host_control_action == HC_ACTION_NONE) + return; + + action = host_control_action; + host_control_action = HC_ACTION_NONE; + + if (!smi_buf.virt) { + dev_dbg(&dcdbas_pdev->dev, "%s: no SMI buffer\n", __func__); + return; + } + + if (smi_buf.size < sizeof(struct apm_cmd)) { + dev_dbg(&dcdbas_pdev->dev, "%s: SMI buffer too small\n", + __func__); + return; + } + + apm_cmd = (struct apm_cmd *)smi_buf.virt; + + /* power off takes precedence */ + if (action & HC_ACTION_HOST_CONTROL_POWEROFF) { + apm_cmd->command = ESM_APM_POWER_CYCLE; + apm_cmd->reserved = 0; + *((s16 *)&apm_cmd->parameters.shortreq.parm[0]) = (s16) 0; + host_control_smi(); + } else if (action & HC_ACTION_HOST_CONTROL_POWERCYCLE) { + apm_cmd->command = ESM_APM_POWER_CYCLE; + apm_cmd->reserved = 0; + *((s16 *)&apm_cmd->parameters.shortreq.parm[0]) = (s16) 20; + host_control_smi(); + } +} + +/* WSMT */ + +static u8 checksum(u8 *buffer, u8 length) +{ + u8 sum = 0; + u8 *end = buffer + length; + + while (buffer < end) + sum += *buffer++; + return sum; +} + +static inline struct smm_eps_table *check_eps_table(u8 *addr) +{ + struct smm_eps_table *eps = (struct smm_eps_table *)addr; + + if (strncmp(eps->smm_comm_buff_anchor, SMM_EPS_SIG, 4) != 0) + return NULL; + + if (checksum(addr, eps->length) != 0) + return NULL; + + return eps; +} + +static int dcdbas_check_wsmt(void) +{ + const struct dmi_device *dev = NULL; + struct acpi_table_wsmt *wsmt = NULL; + struct smm_eps_table *eps = NULL; + u64 bios_buf_paddr; + u64 remap_size; + u8 *addr; + + acpi_get_table(ACPI_SIG_WSMT, 0, (struct acpi_table_header **)&wsmt); + if (!wsmt) + return 0; + + /* Check if WSMT ACPI table shows that protection is enabled */ + if (!(wsmt->protection_flags & ACPI_WSMT_FIXED_COMM_BUFFERS) || + !(wsmt->protection_flags & ACPI_WSMT_COMM_BUFFER_NESTED_PTR_PROTECTION)) + return 0; + + /* + * BIOS could provide the address/size of the protected buffer + * in an SMBIOS string or in an EPS structure in 0xFxxxx. + */ + + /* Check SMBIOS for buffer address */ + while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) + if (sscanf(dev->name, "30[%16llx;%8llx]", &bios_buf_paddr, + &remap_size) == 2) + goto remap; + + /* Scan for EPS (entry point structure) */ + for (addr = (u8 *)__va(0xf0000); + addr < (u8 *)__va(0x100000 - sizeof(struct smm_eps_table)); + addr += 16) { + eps = check_eps_table(addr); + if (eps) + break; + } + + if (!eps) { + dev_dbg(&dcdbas_pdev->dev, "found WSMT, but no firmware buffer found\n"); + return -ENODEV; + } + bios_buf_paddr = eps->smm_comm_buff_addr; + remap_size = eps->num_of_4k_pages * PAGE_SIZE; + +remap: + /* + * Get physical address of buffer and map to virtual address. + * Table gives size in 4K pages, regardless of actual system page size. + */ + if (upper_32_bits(bios_buf_paddr + 8)) { + dev_warn(&dcdbas_pdev->dev, "found WSMT, but buffer address is above 4GB\n"); + return -EINVAL; + } + /* + * Limit remap size to MAX_SMI_DATA_BUF_SIZE + 8 (since the first 8 + * bytes are used for a semaphore, not the data buffer itself). + */ + if (remap_size > MAX_SMI_DATA_BUF_SIZE + 8) + remap_size = MAX_SMI_DATA_BUF_SIZE + 8; + + bios_buffer = memremap(bios_buf_paddr, remap_size, MEMREMAP_WB); + if (!bios_buffer) { + dev_warn(&dcdbas_pdev->dev, "found WSMT, but failed to map buffer\n"); + return -ENOMEM; + } + + /* First 8 bytes is for a semaphore, not part of the smi_buf.virt */ + smi_buf.dma = bios_buf_paddr + 8; + smi_buf.virt = bios_buffer + 8; + smi_buf.size = remap_size - 8; + max_smi_data_buf_size = smi_buf.size; + wsmt_enabled = true; + dev_info(&dcdbas_pdev->dev, + "WSMT found, using firmware-provided SMI buffer.\n"); + return 1; +} + +/** + * dcdbas_reboot_notify: handle reboot notification for host control + */ +static int dcdbas_reboot_notify(struct notifier_block *nb, unsigned long code, + void *unused) +{ + switch (code) { + case SYS_DOWN: + case SYS_HALT: + case SYS_POWER_OFF: + if (host_control_on_shutdown) { + /* firmware is going to perform host control action */ + printk(KERN_WARNING "Please wait for shutdown " + "action to complete...\n"); + dcdbas_host_control(); + } + break; + } + + return NOTIFY_DONE; +} + +static struct notifier_block dcdbas_reboot_nb = { + .notifier_call = dcdbas_reboot_notify, + .next = NULL, + .priority = INT_MIN +}; + +static const BIN_ATTR_ADMIN_RW(smi_data, 0); + +static const struct bin_attribute *const dcdbas_bin_attrs[] = { + &bin_attr_smi_data, + NULL +}; + +static DCDBAS_DEV_ATTR_RW(smi_data_buf_size); +static DCDBAS_DEV_ATTR_RO(smi_data_buf_phys_addr); +static DCDBAS_DEV_ATTR_WO(smi_request); +static DCDBAS_DEV_ATTR_RW(host_control_action); +static DCDBAS_DEV_ATTR_RW(host_control_smi_type); +static DCDBAS_DEV_ATTR_RW(host_control_on_shutdown); + +static struct attribute *dcdbas_dev_attrs[] = { + &dev_attr_smi_data_buf_size.attr, + &dev_attr_smi_data_buf_phys_addr.attr, + &dev_attr_smi_request.attr, + &dev_attr_host_control_action.attr, + &dev_attr_host_control_smi_type.attr, + &dev_attr_host_control_on_shutdown.attr, + NULL +}; + +static const struct attribute_group dcdbas_attr_group = { + .attrs = dcdbas_dev_attrs, + .bin_attrs = dcdbas_bin_attrs, +}; + +static int dcdbas_probe(struct platform_device *dev) +{ + int error; + + host_control_action = HC_ACTION_NONE; + host_control_smi_type = HC_SMITYPE_NONE; + + dcdbas_pdev = dev; + + /* Check if ACPI WSMT table specifies protected SMI buffer address */ + error = dcdbas_check_wsmt(); + if (error < 0) + return error; + + /* + * BIOS SMI calls require buffer addresses be in 32-bit address space. + * This is done by setting the DMA mask below. + */ + error = dma_set_coherent_mask(&dcdbas_pdev->dev, DMA_BIT_MASK(32)); + if (error) + return error; + + error = sysfs_create_group(&dev->dev.kobj, &dcdbas_attr_group); + if (error) + return error; + + register_reboot_notifier(&dcdbas_reboot_nb); + + dev_info(&dev->dev, "%s (version %s)\n", + DRIVER_DESCRIPTION, DRIVER_VERSION); + + return 0; +} + +static void dcdbas_remove(struct platform_device *dev) +{ + unregister_reboot_notifier(&dcdbas_reboot_nb); + sysfs_remove_group(&dev->dev.kobj, &dcdbas_attr_group); +} + +static struct platform_driver dcdbas_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = dcdbas_probe, + .remove = dcdbas_remove, +}; + +static const struct platform_device_info dcdbas_dev_info __initconst = { + .name = DRIVER_NAME, + .id = PLATFORM_DEVID_NONE, + .dma_mask = DMA_BIT_MASK(32), +}; + +static struct platform_device *dcdbas_pdev_reg; + +/** + * dcdbas_init: initialize driver + */ +static int __init dcdbas_init(void) +{ + int error; + + error = platform_driver_register(&dcdbas_driver); + if (error) + return error; + + dcdbas_pdev_reg = platform_device_register_full(&dcdbas_dev_info); + if (IS_ERR(dcdbas_pdev_reg)) { + error = PTR_ERR(dcdbas_pdev_reg); + goto err_unregister_driver; + } + + return 0; + + err_unregister_driver: + platform_driver_unregister(&dcdbas_driver); + return error; +} + +/** + * dcdbas_exit: perform driver cleanup + */ +static void __exit dcdbas_exit(void) +{ + /* + * make sure functions that use dcdbas_pdev are called + * before platform_device_unregister + */ + unregister_reboot_notifier(&dcdbas_reboot_nb); + + /* + * We have to free the buffer here instead of dcdbas_remove + * because only in module exit function we can be sure that + * all sysfs attributes belonging to this module have been + * released. + */ + if (dcdbas_pdev) + smi_data_buf_free(); + if (bios_buffer) + memunmap(bios_buffer); + platform_device_unregister(dcdbas_pdev_reg); + platform_driver_unregister(&dcdbas_driver); +} + +subsys_initcall_sync(dcdbas_init); +module_exit(dcdbas_exit); + +MODULE_DESCRIPTION(DRIVER_DESCRIPTION " (version " DRIVER_VERSION ")"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_AUTHOR("Dell Inc."); +MODULE_LICENSE("GPL"); +/* Any System or BIOS claiming to be by Dell */ +MODULE_ALIAS("dmi:*:[bs]vnD[Ee][Ll][Ll]*:*"); diff --git a/drivers/platform/x86/dell/dcdbas.h b/drivers/platform/x86/dell/dcdbas.h new file mode 100644 index 000000000..a05d7f667 --- /dev/null +++ b/drivers/platform/x86/dell/dcdbas.h @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * dcdbas.h: Definitions for Dell Systems Management Base driver + * + * Copyright (C) 1995-2005 Dell Inc. + */ + +#ifndef _DCDBAS_H_ +#define _DCDBAS_H_ + +#include +#include +#include + +#define MAX_SMI_DATA_BUF_SIZE (256 * 1024) + +#define HC_ACTION_NONE (0) +#define HC_ACTION_HOST_CONTROL_POWEROFF BIT(1) +#define HC_ACTION_HOST_CONTROL_POWERCYCLE BIT(2) + +#define HC_SMITYPE_NONE (0) +#define HC_SMITYPE_TYPE1 (1) +#define HC_SMITYPE_TYPE2 (2) +#define HC_SMITYPE_TYPE3 (3) + +#define ESM_APM_CMD (0x0A0) +#define ESM_APM_POWER_CYCLE (0x10) +#define ESM_STATUS_CMD_UNSUCCESSFUL (-1) + +#define CMOS_BASE_PORT (0x070) +#define CMOS_PAGE1_INDEX_PORT (0) +#define CMOS_PAGE1_DATA_PORT (1) +#define CMOS_PAGE2_INDEX_PORT_PIIX4 (2) +#define CMOS_PAGE2_DATA_PORT_PIIX4 (3) +#define PE1400_APM_CONTROL_PORT (0x0B0) +#define PCAT_APM_CONTROL_PORT (0x0B2) +#define PCAT_APM_STATUS_PORT (0x0B3) +#define PE1300_CMOS_CMD_STRUCT_PTR (0x38) +#define PE1400_CMOS_CMD_STRUCT_PTR (0x70) + +#define MAX_SYSMGMT_SHORTCMD_PARMBUF_LEN (14) +#define MAX_SYSMGMT_LONGCMD_SGENTRY_NUM (16) + +#define TIMEOUT_USEC_SHORT_SEMA_BLOCKING (10000) +#define EXPIRED_TIMER (0) + +#define SMI_CMD_MAGIC (0x534D4931) +#define SMM_EPS_SIG "$SCB" + +#define DCDBAS_DEV_ATTR_RW(_name) \ + DEVICE_ATTR(_name,0600,_name##_show,_name##_store); + +#define DCDBAS_DEV_ATTR_RO(_name) \ + DEVICE_ATTR(_name,0400,_name##_show,NULL); + +#define DCDBAS_DEV_ATTR_WO(_name) \ + DEVICE_ATTR(_name,0200,NULL,_name##_store); + +struct smi_cmd { + __u32 magic; + __u32 ebx; + __u32 ecx; + __u16 command_address; + __u8 command_code; + __u8 reserved; + __u8 command_buffer[1]; +} __attribute__ ((packed)); + +struct apm_cmd { + __u8 command; + __s8 status; + __u16 reserved; + union { + struct { + __u8 parm[MAX_SYSMGMT_SHORTCMD_PARMBUF_LEN]; + } __attribute__ ((packed)) shortreq; + + struct { + __u16 num_sg_entries; + struct { + __u32 size; + __u64 addr; + } __attribute__ ((packed)) + sglist[MAX_SYSMGMT_LONGCMD_SGENTRY_NUM]; + } __attribute__ ((packed)) longreq; + } __attribute__ ((packed)) parameters; +} __attribute__ ((packed)); + +int dcdbas_smi_request(struct smi_cmd *smi_cmd); + +struct smm_eps_table { + char smm_comm_buff_anchor[4]; + u8 length; + u8 checksum; + u8 version; + u64 smm_comm_buff_addr; + u64 num_of_4k_pages; +} __packed; + +struct smi_buffer { + u8 *virt; + unsigned long size; + dma_addr_t dma; +}; + +int dcdbas_smi_alloc(struct smi_buffer *smi_buffer, unsigned long size); +void dcdbas_smi_free(struct smi_buffer *smi_buffer); + +#endif /* _DCDBAS_H_ */ + diff --git a/drivers/platform/x86/dell/dell-dw5826e-reset.c b/drivers/platform/x86/dell/dell-dw5826e-reset.c new file mode 100644 index 000000000..64e6e9b1d --- /dev/null +++ b/drivers/platform/x86/dell/dell-dw5826e-reset.c @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * dell-dw5826e-reset.c - Dell DW5826e reset driver + * + * Copyright (C) 2026 Jackbb Wu + */ + +#include +#include +#include +#include +#include +#include +#include + +#define PALC_DSM_FN_TRIGGER_PLDR 1 + +static guid_t palc_dsm_guid = + GUID_INIT(0x5a1a4bba, 0x8006, 0x487e, 0xbe, 0x0a, 0xac, 0xf5, 0xd8, 0xfd, 0xfe, 0x59); + +static int trigger_palc_pldr(struct device *dev, acpi_handle handle) +{ + union acpi_object *obj; + int ret = 0; + + obj = acpi_evaluate_dsm(handle, &palc_dsm_guid, 1, PALC_DSM_FN_TRIGGER_PLDR, NULL); + if (!obj) { + dev_err(dev, "Failed to evaluate _DSM\n"); + return -EIO; + } + + if (obj->type != ACPI_TYPE_BUFFER) { + dev_err(dev, "Unexpected _DSM return type: %d\n", obj->type); + ret = -EINVAL; + } + + ACPI_FREE(obj); + return ret; +} + +static ssize_t wwan_reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + acpi_handle handle = ACPI_HANDLE(dev); + int ret; + + ret = trigger_palc_pldr(dev, handle); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(wwan_reset); + +static struct attribute *palc_attrs[] = { + &dev_attr_wwan_reset.attr, + NULL +}; +ATTRIBUTE_GROUPS(palc); + +static int palc_probe(struct platform_device *pdev) +{ + acpi_handle handle; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + if (!acpi_check_dsm(handle, &palc_dsm_guid, 1, BIT(PALC_DSM_FN_TRIGGER_PLDR))) + return -ENODEV; + + return 0; +} + +static const struct acpi_device_id palc_acpi_ids[] = { + { "PALC0001", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, palc_acpi_ids); + +static struct platform_driver palc_driver = { + .driver = { + .name = "dell-dw5826e-reset", + .acpi_match_table = palc_acpi_ids, + .dev_groups = palc_groups, + }, + .probe = palc_probe, +}; +module_platform_driver(palc_driver); + +MODULE_DESCRIPTION("Dell DW5826e reset driver"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("JackBB Wu"); diff --git a/drivers/platform/x86/dell/dell-laptop.c b/drivers/platform/x86/dell/dell-laptop.c new file mode 100644 index 000000000..89e85c7f7 --- /dev/null +++ b/drivers/platform/x86/dell/dell-laptop.c @@ -0,0 +1,2614 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for Dell laptop extras + * + * Copyright (c) Red Hat + * Copyright (c) 2014 Gabriele Mazzotta + * Copyright (c) 2014 Pali Rohár + * + * Based on documentation in the libsmbios package: + * Copyright (C) 2005-2014 Dell Inc. + */ + +#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 "dell-rbtn.h" +#include "dell-smbios.h" + +#include "dell-wmi-privacy.h" + +struct quirk_entry { + bool touchpad_led; + bool kbd_led_not_present; + bool kbd_led_levels_off_1; + bool kbd_missing_ac_tag; + + bool needs_kbd_timeouts; + /* + * Ordered list of timeouts expressed in seconds. + * The list must end with -1 + */ + int kbd_timeouts[]; +}; + +static struct quirk_entry *quirks; + +static struct quirk_entry quirk_dell_vostro_v130 = { + .touchpad_led = true, +}; + +static int __init dmi_matched(const struct dmi_system_id *dmi) +{ + quirks = dmi->driver_data; + return 1; +} + +/* + * These values come from Windows utility provided by Dell. If any other value + * is used then BIOS silently set timeout to 0 without any error message. + */ +static struct quirk_entry quirk_dell_xps13_9333 = { + .needs_kbd_timeouts = true, + .kbd_timeouts = { 0, 5, 15, 60, 5 * 60, 15 * 60, -1 }, +}; + +static struct quirk_entry quirk_dell_xps13_9370 = { + .kbd_missing_ac_tag = true, +}; + +static struct quirk_entry quirk_dell_latitude_e6410 = { + .kbd_led_levels_off_1 = true, +}; + +static struct quirk_entry quirk_dell_inspiron_1012 = { + .kbd_led_not_present = true, +}; + +static struct quirk_entry quirk_dell_latitude_7520 = { + .kbd_missing_ac_tag = true, +}; + +static struct platform_driver platform_driver = { + .driver = { + .name = "dell-laptop", + } +}; + +static struct platform_device *platform_device; +static struct backlight_device *dell_backlight_device; +static struct rfkill *wifi_rfkill; +static struct rfkill *bluetooth_rfkill; +static struct rfkill *wwan_rfkill; +static bool force_rfkill; +static bool micmute_led_registered; +static bool mute_led_registered; + +struct battery_mode_info { + int token; + enum power_supply_charge_type charge_type; +}; + +static const struct battery_mode_info battery_modes[] = { + { BAT_PRI_AC_MODE_TOKEN, POWER_SUPPLY_CHARGE_TYPE_TRICKLE }, + { BAT_EXPRESS_MODE_TOKEN, POWER_SUPPLY_CHARGE_TYPE_FAST }, + { BAT_STANDARD_MODE_TOKEN, POWER_SUPPLY_CHARGE_TYPE_STANDARD }, + { BAT_ADAPTIVE_MODE_TOKEN, POWER_SUPPLY_CHARGE_TYPE_ADAPTIVE }, + { BAT_CUSTOM_MODE_TOKEN, POWER_SUPPLY_CHARGE_TYPE_CUSTOM }, +}; +static u32 battery_supported_modes; + +module_param(force_rfkill, bool, 0444); +MODULE_PARM_DESC(force_rfkill, "enable rfkill on non whitelisted models"); + +static const struct dmi_system_id dell_device_table[] __initconst = { + { + .ident = "Dell laptop", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "8"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "9"), /*Laptop*/ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "10"), /*Notebook*/ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "30"), /*Tablet*/ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "31"), /*Convertible*/ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_CHASSIS_TYPE, "32"), /*Detachable*/ + }, + }, + { + .ident = "Dell Computer Corporation", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Computer Corporation"), + DMI_MATCH(DMI_CHASSIS_TYPE, "8"), + }, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, dell_device_table); + +static const struct dmi_system_id dell_quirks[] __initconst = { + { + .callback = dmi_matched, + .ident = "Dell Vostro V130", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro V130"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro V131", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro V131"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3350", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro 3350"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3555", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro 3555"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron N311z", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron N311z"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron M5110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron M5110"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron N5110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron N5110"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3360", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro 3360"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3460", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro 3460"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3560", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro 3560"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro 3450", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell System Vostro 3450"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 5420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5420"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 5520", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5520"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 5720", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5720"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 7420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 7420"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 7520", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 7520"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 7720", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 7720"), + }, + .driver_data = &quirk_dell_vostro_v130, + }, + { + .callback = dmi_matched, + .ident = "Dell XPS13 9333", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "XPS13 9333"), + }, + .driver_data = &quirk_dell_xps13_9333, + }, + { + .callback = dmi_matched, + .ident = "Dell XPS 13 9370", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "XPS 13 9370"), + }, + .driver_data = &quirk_dell_xps13_9370, + }, + { + .callback = dmi_matched, + .ident = "Dell Latitude E6410", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Latitude E6410"), + }, + .driver_data = &quirk_dell_latitude_e6410, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 1012", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1012"), + }, + .driver_data = &quirk_dell_inspiron_1012, + }, + { + .callback = dmi_matched, + .ident = "Dell Inspiron 1018", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 1018"), + }, + .driver_data = &quirk_dell_inspiron_1012, + }, + { + .callback = dmi_matched, + .ident = "Dell Latitude 7520", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Latitude 7520"), + }, + .driver_data = &quirk_dell_latitude_7520, + }, + { } +}; + +/* -1 is a sentinel value, telling us to use token->value */ +#define USE_TVAL ((u32) -1) +static int dell_send_request_for_tokenid(struct calling_interface_buffer *buffer, + u16 class, u16 select, u16 tokenid, + u32 val) +{ + struct calling_interface_token *token; + + token = dell_smbios_find_token(tokenid); + if (!token) + return -ENODEV; + + if (val == USE_TVAL) + val = token->value; + + dell_fill_request(buffer, token->location, val, 0, 0); + return dell_send_request(buffer, class, select); +} + +static inline int dell_set_std_token_value(struct calling_interface_buffer *buffer, + u16 tokenid, u32 value) +{ + return dell_send_request_for_tokenid(buffer, CLASS_TOKEN_WRITE, + SELECT_TOKEN_STD, tokenid, value); +} + +/* + * Derived from information in smbios-wireless-ctl: + * + * cbSelect 17, Value 11 + * + * Return Wireless Info + * cbArg1, byte0 = 0x00 + * + * cbRes1 Standard return codes (0, -1, -2) + * cbRes2 Info bit flags: + * + * 0 Hardware switch supported (1) + * 1 WiFi locator supported (1) + * 2 WLAN supported (1) + * 3 Bluetooth (BT) supported (1) + * 4 WWAN supported (1) + * 5 Wireless KBD supported (1) + * 6 Uw b supported (1) + * 7 WiGig supported (1) + * 8 WLAN installed (1) + * 9 BT installed (1) + * 10 WWAN installed (1) + * 11 Uw b installed (1) + * 12 WiGig installed (1) + * 13-15 Reserved (0) + * 16 Hardware (HW) switch is On (1) + * 17 WLAN disabled (1) + * 18 BT disabled (1) + * 19 WWAN disabled (1) + * 20 Uw b disabled (1) + * 21 WiGig disabled (1) + * 20-31 Reserved (0) + * + * cbRes3 NVRAM size in bytes + * cbRes4, byte 0 NVRAM format version number + * + * + * Set QuickSet Radio Disable Flag + * cbArg1, byte0 = 0x01 + * cbArg1, byte1 + * Radio ID value: + * 0 Radio Status + * 1 WLAN ID + * 2 BT ID + * 3 WWAN ID + * 4 UWB ID + * 5 WIGIG ID + * cbArg1, byte2 Flag bits: + * 0 QuickSet disables radio (1) + * 1-7 Reserved (0) + * + * cbRes1 Standard return codes (0, -1, -2) + * cbRes2 QuickSet (QS) radio disable bit map: + * 0 QS disables WLAN + * 1 QS disables BT + * 2 QS disables WWAN + * 3 QS disables UWB + * 4 QS disables WIGIG + * 5-31 Reserved (0) + * + * Wireless Switch Configuration + * cbArg1, byte0 = 0x02 + * + * cbArg1, byte1 + * Subcommand: + * 0 Get config + * 1 Set config + * 2 Set WiFi locator enable/disable + * cbArg1,byte2 + * Switch settings (if byte 1==1): + * 0 WLAN sw itch control (1) + * 1 BT sw itch control (1) + * 2 WWAN sw itch control (1) + * 3 UWB sw itch control (1) + * 4 WiGig sw itch control (1) + * 5-7 Reserved (0) + * cbArg1, byte2 Enable bits (if byte 1==2): + * 0 Enable WiFi locator (1) + * + * cbRes1 Standard return codes (0, -1, -2) + * cbRes2 QuickSet radio disable bit map: + * 0 WLAN controlled by sw itch (1) + * 1 BT controlled by sw itch (1) + * 2 WWAN controlled by sw itch (1) + * 3 UWB controlled by sw itch (1) + * 4 WiGig controlled by sw itch (1) + * 5-6 Reserved (0) + * 7 Wireless sw itch config locked (1) + * 8 WiFi locator enabled (1) + * 9-14 Reserved (0) + * 15 WiFi locator setting locked (1) + * 16-31 Reserved (0) + * + * Read Local Config Data (LCD) + * cbArg1, byte0 = 0x10 + * cbArg1, byte1 NVRAM index low byte + * cbArg1, byte2 NVRAM index high byte + * cbRes1 Standard return codes (0, -1, -2) + * cbRes2 4 bytes read from LCD[index] + * cbRes3 4 bytes read from LCD[index+4] + * cbRes4 4 bytes read from LCD[index+8] + * + * Write Local Config Data (LCD) + * cbArg1, byte0 = 0x11 + * cbArg1, byte1 NVRAM index low byte + * cbArg1, byte2 NVRAM index high byte + * cbArg2 4 bytes to w rite at LCD[index] + * cbArg3 4 bytes to w rite at LCD[index+4] + * cbArg4 4 bytes to w rite at LCD[index+8] + * cbRes1 Standard return codes (0, -1, -2) + * + * Populate Local Config Data from NVRAM + * cbArg1, byte0 = 0x12 + * cbRes1 Standard return codes (0, -1, -2) + * + * Commit Local Config Data to NVRAM + * cbArg1, byte0 = 0x13 + * cbRes1 Standard return codes (0, -1, -2) + */ + +static int dell_rfkill_set(void *data, bool blocked) +{ + int disable = blocked ? 1 : 0; + unsigned long radio = (unsigned long)data; + int hwswitch_bit = (unsigned long)data - 1; + struct calling_interface_buffer buffer; + int hwswitch; + int status; + int ret; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + if (ret) + return ret; + status = buffer.output[1]; + + dell_fill_request(&buffer, 0x2, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + if (ret) + return ret; + hwswitch = buffer.output[1]; + + /* If the hardware switch controls this radio, and the hardware + switch is disabled, always disable the radio */ + if (ret == 0 && (hwswitch & BIT(hwswitch_bit)) && + (status & BIT(0)) && !(status & BIT(16))) + disable = 1; + + dell_fill_request(&buffer, 1 | (radio<<8) | (disable << 16), 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + return ret; +} + +static void dell_rfkill_update_sw_state(struct rfkill *rfkill, int radio, + int status) +{ + if (status & BIT(0)) { + /* Has hw-switch, sync sw_state to BIOS */ + struct calling_interface_buffer buffer; + int block = rfkill_blocked(rfkill); + dell_fill_request(&buffer, + 1 | (radio << 8) | (block << 16), 0, 0, 0); + dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + } else { + /* No hw-switch, sync BIOS state to sw_state */ + rfkill_set_sw_state(rfkill, !!(status & BIT(radio + 16))); + } +} + +static void dell_rfkill_update_hw_state(struct rfkill *rfkill, int radio, + int status, int hwswitch) +{ + if (hwswitch & (BIT(radio - 1))) + rfkill_set_hw_state(rfkill, !(status & BIT(16))); +} + +static void dell_rfkill_query(struct rfkill *rfkill, void *data) +{ + int radio = ((unsigned long)data & 0xF); + struct calling_interface_buffer buffer; + int hwswitch; + int status; + int ret; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + status = buffer.output[1]; + + if (ret != 0 || !(status & BIT(0))) { + return; + } + + dell_fill_request(&buffer, 0x2, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + hwswitch = buffer.output[1]; + + if (ret != 0) + return; + + dell_rfkill_update_hw_state(rfkill, radio, status, hwswitch); +} + +static const struct rfkill_ops dell_rfkill_ops = { + .set_block = dell_rfkill_set, + .query = dell_rfkill_query, +}; + +static struct dentry *dell_laptop_dir; + +static int dell_debugfs_show(struct seq_file *s, void *data) +{ + struct calling_interface_buffer buffer; + int hwswitch_state; + int hwswitch_ret; + int status; + int ret; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + if (ret) + return ret; + status = buffer.output[1]; + + dell_fill_request(&buffer, 0x2, 0, 0, 0); + hwswitch_ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + if (hwswitch_ret) + return hwswitch_ret; + hwswitch_state = buffer.output[1]; + + seq_printf(s, "return:\t%d\n", ret); + seq_printf(s, "status:\t0x%X\n", status); + seq_printf(s, "Bit 0 : Hardware switch supported: %lu\n", + status & BIT(0)); + seq_printf(s, "Bit 1 : Wifi locator supported: %lu\n", + (status & BIT(1)) >> 1); + seq_printf(s, "Bit 2 : Wifi is supported: %lu\n", + (status & BIT(2)) >> 2); + seq_printf(s, "Bit 3 : Bluetooth is supported: %lu\n", + (status & BIT(3)) >> 3); + seq_printf(s, "Bit 4 : WWAN is supported: %lu\n", + (status & BIT(4)) >> 4); + seq_printf(s, "Bit 5 : Wireless keyboard supported: %lu\n", + (status & BIT(5)) >> 5); + seq_printf(s, "Bit 6 : UWB supported: %lu\n", + (status & BIT(6)) >> 6); + seq_printf(s, "Bit 7 : WiGig supported: %lu\n", + (status & BIT(7)) >> 7); + seq_printf(s, "Bit 8 : Wifi is installed: %lu\n", + (status & BIT(8)) >> 8); + seq_printf(s, "Bit 9 : Bluetooth is installed: %lu\n", + (status & BIT(9)) >> 9); + seq_printf(s, "Bit 10: WWAN is installed: %lu\n", + (status & BIT(10)) >> 10); + seq_printf(s, "Bit 11: UWB installed: %lu\n", + (status & BIT(11)) >> 11); + seq_printf(s, "Bit 12: WiGig installed: %lu\n", + (status & BIT(12)) >> 12); + + seq_printf(s, "Bit 16: Hardware switch is on: %lu\n", + (status & BIT(16)) >> 16); + seq_printf(s, "Bit 17: Wifi is blocked: %lu\n", + (status & BIT(17)) >> 17); + seq_printf(s, "Bit 18: Bluetooth is blocked: %lu\n", + (status & BIT(18)) >> 18); + seq_printf(s, "Bit 19: WWAN is blocked: %lu\n", + (status & BIT(19)) >> 19); + seq_printf(s, "Bit 20: UWB is blocked: %lu\n", + (status & BIT(20)) >> 20); + seq_printf(s, "Bit 21: WiGig is blocked: %lu\n", + (status & BIT(21)) >> 21); + + seq_printf(s, "\nhwswitch_return:\t%d\n", hwswitch_ret); + seq_printf(s, "hwswitch_state:\t0x%X\n", hwswitch_state); + seq_printf(s, "Bit 0 : Wifi controlled by switch: %lu\n", + hwswitch_state & BIT(0)); + seq_printf(s, "Bit 1 : Bluetooth controlled by switch: %lu\n", + (hwswitch_state & BIT(1)) >> 1); + seq_printf(s, "Bit 2 : WWAN controlled by switch: %lu\n", + (hwswitch_state & BIT(2)) >> 2); + seq_printf(s, "Bit 3 : UWB controlled by switch: %lu\n", + (hwswitch_state & BIT(3)) >> 3); + seq_printf(s, "Bit 4 : WiGig controlled by switch: %lu\n", + (hwswitch_state & BIT(4)) >> 4); + seq_printf(s, "Bit 7 : Wireless switch config locked: %lu\n", + (hwswitch_state & BIT(7)) >> 7); + seq_printf(s, "Bit 8 : Wifi locator enabled: %lu\n", + (hwswitch_state & BIT(8)) >> 8); + seq_printf(s, "Bit 15: Wifi locator setting locked: %lu\n", + (hwswitch_state & BIT(15)) >> 15); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(dell_debugfs); + +static void dell_update_rfkill(struct work_struct *ignored) +{ + struct calling_interface_buffer buffer; + int hwswitch = 0; + int status; + int ret; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + status = buffer.output[1]; + + if (ret != 0) + return; + + dell_fill_request(&buffer, 0x2, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + + if (ret == 0 && (status & BIT(0))) + hwswitch = buffer.output[1]; + + if (wifi_rfkill) { + dell_rfkill_update_hw_state(wifi_rfkill, 1, status, hwswitch); + dell_rfkill_update_sw_state(wifi_rfkill, 1, status); + } + if (bluetooth_rfkill) { + dell_rfkill_update_hw_state(bluetooth_rfkill, 2, status, + hwswitch); + dell_rfkill_update_sw_state(bluetooth_rfkill, 2, status); + } + if (wwan_rfkill) { + dell_rfkill_update_hw_state(wwan_rfkill, 3, status, hwswitch); + dell_rfkill_update_sw_state(wwan_rfkill, 3, status); + } +} +static DECLARE_DELAYED_WORK(dell_rfkill_work, dell_update_rfkill); + +static bool dell_laptop_i8042_filter(unsigned char data, unsigned char str, struct serio *port, + void *context) +{ + static bool extended; + + if (str & I8042_STR_AUXDATA) + return false; + + if (unlikely(data == 0xe0)) { + extended = true; + return false; + } else if (unlikely(extended)) { + switch (data) { + case 0x8: + schedule_delayed_work(&dell_rfkill_work, + round_jiffies_relative(HZ / 4)); + break; + } + extended = false; + } + + return false; +} + +static int (*dell_rbtn_notifier_register_func)(struct notifier_block *); +static int (*dell_rbtn_notifier_unregister_func)(struct notifier_block *); + +static int dell_laptop_rbtn_notifier_call(struct notifier_block *nb, + unsigned long action, void *data) +{ + schedule_delayed_work(&dell_rfkill_work, 0); + return NOTIFY_OK; +} + +static struct notifier_block dell_laptop_rbtn_notifier = { + .notifier_call = dell_laptop_rbtn_notifier_call, +}; + +static int __init dell_setup_rfkill(void) +{ + struct calling_interface_buffer buffer; + int status, ret, whitelisted; + const char *product; + + /* + * rfkill support causes trouble on various models, mostly Inspirons. + * So we whitelist certain series, and don't support rfkill on others. + */ + whitelisted = 0; + product = dmi_get_system_info(DMI_PRODUCT_NAME); + if (product && (strncmp(product, "Latitude", 8) == 0 || + strncmp(product, "Precision", 9) == 0)) + whitelisted = 1; + if (!force_rfkill && !whitelisted) + return 0; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_RFKILL); + status = buffer.output[1]; + + /* dell wireless info smbios call is not supported */ + if (ret != 0) + return 0; + + /* rfkill is only tested on laptops with a hwswitch */ + if (!(status & BIT(0)) && !force_rfkill) + return 0; + + if ((status & (1<<2|1<<8)) == (1<<2|1<<8)) { + wifi_rfkill = rfkill_alloc("dell-wifi", &platform_device->dev, + RFKILL_TYPE_WLAN, + &dell_rfkill_ops, (void *) 1); + if (!wifi_rfkill) { + ret = -ENOMEM; + goto err_wifi; + } + ret = rfkill_register(wifi_rfkill); + if (ret) + goto err_wifi; + } + + if ((status & (1<<3|1<<9)) == (1<<3|1<<9)) { + bluetooth_rfkill = rfkill_alloc("dell-bluetooth", + &platform_device->dev, + RFKILL_TYPE_BLUETOOTH, + &dell_rfkill_ops, (void *) 2); + if (!bluetooth_rfkill) { + ret = -ENOMEM; + goto err_bluetooth; + } + ret = rfkill_register(bluetooth_rfkill); + if (ret) + goto err_bluetooth; + } + + if ((status & (1<<4|1<<10)) == (1<<4|1<<10)) { + wwan_rfkill = rfkill_alloc("dell-wwan", + &platform_device->dev, + RFKILL_TYPE_WWAN, + &dell_rfkill_ops, (void *) 3); + if (!wwan_rfkill) { + ret = -ENOMEM; + goto err_wwan; + } + ret = rfkill_register(wwan_rfkill); + if (ret) + goto err_wwan; + } + + /* + * Dell Airplane Mode Switch driver (dell-rbtn) supports ACPI devices + * which can receive events from HW slider switch. + * + * Dell SMBIOS on whitelisted models supports controlling radio devices + * but does not support receiving HW button switch events. We can use + * i8042 filter hook function to receive keyboard data and handle + * keycode for HW button. + * + * So if it is possible we will use Dell Airplane Mode Switch ACPI + * driver for receiving HW events and Dell SMBIOS for setting rfkill + * states. If ACPI driver or device is not available we will fallback to + * i8042 filter hook function. + * + * To prevent duplicate rfkill devices which control and do same thing, + * dell-rbtn driver will automatically remove its own rfkill devices + * once function dell_rbtn_notifier_register() is called. + */ + + dell_rbtn_notifier_register_func = + symbol_request(dell_rbtn_notifier_register); + if (dell_rbtn_notifier_register_func) { + dell_rbtn_notifier_unregister_func = + symbol_request(dell_rbtn_notifier_unregister); + if (!dell_rbtn_notifier_unregister_func) { + symbol_put(dell_rbtn_notifier_register); + dell_rbtn_notifier_register_func = NULL; + } + } + + if (dell_rbtn_notifier_register_func) { + ret = dell_rbtn_notifier_register_func( + &dell_laptop_rbtn_notifier); + symbol_put(dell_rbtn_notifier_register); + dell_rbtn_notifier_register_func = NULL; + if (ret != 0) { + symbol_put(dell_rbtn_notifier_unregister); + dell_rbtn_notifier_unregister_func = NULL; + } + } else { + pr_info("Symbols from dell-rbtn acpi driver are not available\n"); + ret = -ENODEV; + } + + if (ret == 0) { + pr_info("Using dell-rbtn acpi driver for receiving events\n"); + } else if (ret != -ENODEV) { + pr_warn("Unable to register dell rbtn notifier\n"); + goto err_filter; + } else { + ret = i8042_install_filter(dell_laptop_i8042_filter, NULL); + if (ret) { + pr_warn("Unable to install key filter\n"); + goto err_filter; + } + pr_info("Using i8042 filter function for receiving events\n"); + } + + return 0; +err_filter: + if (wwan_rfkill) + rfkill_unregister(wwan_rfkill); +err_wwan: + rfkill_destroy(wwan_rfkill); + if (bluetooth_rfkill) + rfkill_unregister(bluetooth_rfkill); +err_bluetooth: + rfkill_destroy(bluetooth_rfkill); + if (wifi_rfkill) + rfkill_unregister(wifi_rfkill); +err_wifi: + rfkill_destroy(wifi_rfkill); + + return ret; +} + +static void dell_cleanup_rfkill(void) +{ + if (dell_rbtn_notifier_unregister_func) { + dell_rbtn_notifier_unregister_func(&dell_laptop_rbtn_notifier); + symbol_put(dell_rbtn_notifier_unregister); + dell_rbtn_notifier_unregister_func = NULL; + } else { + i8042_remove_filter(dell_laptop_i8042_filter); + } + cancel_delayed_work_sync(&dell_rfkill_work); + if (wifi_rfkill) { + rfkill_unregister(wifi_rfkill); + rfkill_destroy(wifi_rfkill); + } + if (bluetooth_rfkill) { + rfkill_unregister(bluetooth_rfkill); + rfkill_destroy(bluetooth_rfkill); + } + if (wwan_rfkill) { + rfkill_unregister(wwan_rfkill); + rfkill_destroy(wwan_rfkill); + } +} + +static int dell_send_intensity(struct backlight_device *bd) +{ + struct calling_interface_buffer buffer; + u16 select; + + select = power_supply_is_system_supplied() > 0 ? + SELECT_TOKEN_AC : SELECT_TOKEN_BAT; + return dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_WRITE, + select, BRIGHTNESS_TOKEN, bd->props.brightness); +} + +static int dell_get_intensity(struct backlight_device *bd) +{ + struct calling_interface_buffer buffer; + int ret; + u16 select; + + select = power_supply_is_system_supplied() > 0 ? + SELECT_TOKEN_AC : SELECT_TOKEN_BAT; + ret = dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_READ, + select, BRIGHTNESS_TOKEN, 0); + if (ret == 0) + ret = buffer.output[1]; + + return ret; +} + +static const struct backlight_ops dell_ops = { + .get_brightness = dell_get_intensity, + .update_status = dell_send_intensity, +}; + +static void touchpad_led_on(void) +{ + int command = 0x97; + char data = 1; + i8042_command(&data, command | 1 << 12); +} + +static void touchpad_led_off(void) +{ + int command = 0x97; + char data = 2; + i8042_command(&data, command | 1 << 12); +} + +static void touchpad_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value > 0) + touchpad_led_on(); + else + touchpad_led_off(); +} + +static struct led_classdev touchpad_led = { + .name = "dell-laptop::touchpad", + .brightness_set = touchpad_led_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int __init touchpad_led_init(struct device *dev) +{ + return led_classdev_register(dev, &touchpad_led); +} + +static void touchpad_led_exit(void) +{ + led_classdev_unregister(&touchpad_led); +} + +/* + * Derived from information in smbios-keyboard-ctl: + * + * cbClass 4 + * cbSelect 11 + * Keyboard illumination + * cbArg1 determines the function to be performed + * + * cbArg1 0x0 = Get Feature Information + * cbRES1 Standard return codes (0, -1, -2) + * cbRES2, word0 Bitmap of user-selectable modes + * bit 0 Always off (All systems) + * bit 1 Always on (Travis ATG, Siberia) + * bit 2 Auto: ALS-based On; ALS-based Off (Travis ATG) + * bit 3 Auto: ALS- and input-activity-based On; input-activity based Off + * bit 4 Auto: Input-activity-based On; input-activity based Off + * bit 5 Auto: Input-activity-based On (illumination level 25%); input-activity based Off + * bit 6 Auto: Input-activity-based On (illumination level 50%); input-activity based Off + * bit 7 Auto: Input-activity-based On (illumination level 75%); input-activity based Off + * bit 8 Auto: Input-activity-based On (illumination level 100%); input-activity based Off + * bits 9-15 Reserved for future use + * cbRES2, byte2 Reserved for future use + * cbRES2, byte3 Keyboard illumination type + * 0 Reserved + * 1 Tasklight + * 2 Backlight + * 3-255 Reserved for future use + * cbRES3, byte0 Supported auto keyboard illumination trigger bitmap. + * bit 0 Any keystroke + * bit 1 Touchpad activity + * bit 2 Pointing stick + * bit 3 Any mouse + * bits 4-7 Reserved for future use + * cbRES3, byte1 Supported timeout unit bitmap + * bit 0 Seconds + * bit 1 Minutes + * bit 2 Hours + * bit 3 Days + * bits 4-7 Reserved for future use + * cbRES3, byte2 Number of keyboard light brightness levels + * cbRES4, byte0 Maximum acceptable seconds value (0 if seconds not supported). + * cbRES4, byte1 Maximum acceptable minutes value (0 if minutes not supported). + * cbRES4, byte2 Maximum acceptable hours value (0 if hours not supported). + * cbRES4, byte3 Maximum acceptable days value (0 if days not supported) + * + * cbArg1 0x1 = Get Current State + * cbRES1 Standard return codes (0, -1, -2) + * cbRES2, word0 Bitmap of current mode state + * bit 0 Always off (All systems) + * bit 1 Always on (Travis ATG, Siberia) + * bit 2 Auto: ALS-based On; ALS-based Off (Travis ATG) + * bit 3 Auto: ALS- and input-activity-based On; input-activity based Off + * bit 4 Auto: Input-activity-based On; input-activity based Off + * bit 5 Auto: Input-activity-based On (illumination level 25%); input-activity based Off + * bit 6 Auto: Input-activity-based On (illumination level 50%); input-activity based Off + * bit 7 Auto: Input-activity-based On (illumination level 75%); input-activity based Off + * bit 8 Auto: Input-activity-based On (illumination level 100%); input-activity based Off + * bits 9-15 Reserved for future use + * Note: Only One bit can be set + * cbRES2, byte2 Currently active auto keyboard illumination triggers. + * bit 0 Any keystroke + * bit 1 Touchpad activity + * bit 2 Pointing stick + * bit 3 Any mouse + * bits 4-7 Reserved for future use + * cbRES2, byte3 Current Timeout on battery + * bits 7:6 Timeout units indicator: + * 00b Seconds + * 01b Minutes + * 10b Hours + * 11b Days + * bits 5:0 Timeout value (0-63) in sec/min/hr/day + * NOTE: A value of 0 means always on (no timeout) if any bits of RES3 byte + * are set upon return from the [Get feature information] call. + * cbRES3, byte0 Current setting of ALS value that turns the light on or off. + * cbRES3, byte1 Current ALS reading + * cbRES3, byte2 Current keyboard light level. + * cbRES3, byte3 Current timeout on AC Power + * bits 7:6 Timeout units indicator: + * 00b Seconds + * 01b Minutes + * 10b Hours + * 11b Days + * Bits 5:0 Timeout value (0-63) in sec/min/hr/day + * NOTE: A value of 0 means always on (no timeout) if any bits of RES3 byte2 + * are set upon return from the upon return from the [Get Feature information] call. + * + * cbArg1 0x2 = Set New State + * cbRES1 Standard return codes (0, -1, -2) + * cbArg2, word0 Bitmap of current mode state + * bit 0 Always off (All systems) + * bit 1 Always on (Travis ATG, Siberia) + * bit 2 Auto: ALS-based On; ALS-based Off (Travis ATG) + * bit 3 Auto: ALS- and input-activity-based On; input-activity based Off + * bit 4 Auto: Input-activity-based On; input-activity based Off + * bit 5 Auto: Input-activity-based On (illumination level 25%); input-activity based Off + * bit 6 Auto: Input-activity-based On (illumination level 50%); input-activity based Off + * bit 7 Auto: Input-activity-based On (illumination level 75%); input-activity based Off + * bit 8 Auto: Input-activity-based On (illumination level 100%); input-activity based Off + * bits 9-15 Reserved for future use + * Note: Only One bit can be set + * cbArg2, byte2 Desired auto keyboard illumination triggers. Must remain inactive to allow + * keyboard to turn off automatically. + * bit 0 Any keystroke + * bit 1 Touchpad activity + * bit 2 Pointing stick + * bit 3 Any mouse + * bits 4-7 Reserved for future use + * cbArg2, byte3 Desired Timeout on battery + * bits 7:6 Timeout units indicator: + * 00b Seconds + * 01b Minutes + * 10b Hours + * 11b Days + * bits 5:0 Timeout value (0-63) in sec/min/hr/day + * cbArg3, byte0 Desired setting of ALS value that turns the light on or off. + * cbArg3, byte2 Desired keyboard light level. + * cbArg3, byte3 Desired Timeout on AC power + * bits 7:6 Timeout units indicator: + * 00b Seconds + * 01b Minutes + * 10b Hours + * 11b Days + * bits 5:0 Timeout value (0-63) in sec/min/hr/day + */ + + +enum kbd_timeout_unit { + KBD_TIMEOUT_SECONDS = 0, + KBD_TIMEOUT_MINUTES, + KBD_TIMEOUT_HOURS, + KBD_TIMEOUT_DAYS, +}; + +enum kbd_mode_bit { + KBD_MODE_BIT_OFF = 0, + KBD_MODE_BIT_ON, + KBD_MODE_BIT_ALS, + KBD_MODE_BIT_TRIGGER_ALS, + KBD_MODE_BIT_TRIGGER, + KBD_MODE_BIT_TRIGGER_25, + KBD_MODE_BIT_TRIGGER_50, + KBD_MODE_BIT_TRIGGER_75, + KBD_MODE_BIT_TRIGGER_100, +}; + +#define kbd_is_als_mode_bit(bit) \ + ((bit) == KBD_MODE_BIT_ALS || (bit) == KBD_MODE_BIT_TRIGGER_ALS) +#define kbd_is_trigger_mode_bit(bit) \ + ((bit) >= KBD_MODE_BIT_TRIGGER_ALS && (bit) <= KBD_MODE_BIT_TRIGGER_100) +#define kbd_is_level_mode_bit(bit) \ + ((bit) >= KBD_MODE_BIT_TRIGGER_25 && (bit) <= KBD_MODE_BIT_TRIGGER_100) + +struct kbd_info { + u16 modes; + u8 type; + u8 triggers; + u8 levels; + u8 seconds; + u8 minutes; + u8 hours; + u8 days; +}; + +struct kbd_state { + u8 mode_bit; + u8 triggers; + u8 timeout_value; + u8 timeout_unit; + u8 timeout_value_ac; + u8 timeout_unit_ac; + u8 als_setting; + u8 als_value; + u8 level; +}; + +static const int kbd_tokens[] = { + KBD_LED_OFF_TOKEN, + KBD_LED_AUTO_25_TOKEN, + KBD_LED_AUTO_50_TOKEN, + KBD_LED_AUTO_75_TOKEN, + KBD_LED_AUTO_100_TOKEN, + KBD_LED_ON_TOKEN, +}; + +static u16 kbd_token_bits; + +static struct kbd_info kbd_info; +static bool kbd_als_supported; +static bool kbd_triggers_supported; +static bool kbd_timeout_ac_supported; + +static u8 kbd_mode_levels[16]; +static int kbd_mode_levels_count; + +static u8 kbd_previous_level; +static u8 kbd_previous_mode_bit; + +static bool kbd_led_present; +static DEFINE_MUTEX(kbd_led_mutex); +static enum led_brightness kbd_led_level; + +/* + * NOTE: there are three ways to set the keyboard backlight level. + * First, via kbd_state.mode_bit (assigning KBD_MODE_BIT_TRIGGER_* value). + * Second, via kbd_state.level (assigning numerical value <= kbd_info.levels). + * Third, via SMBIOS tokens (KBD_LED_* in kbd_tokens) + * + * There are laptops which support only one of these methods. If we want to + * support as many machines as possible we need to implement all three methods. + * The first two methods use the kbd_state structure. The third uses SMBIOS + * tokens. If kbd_info.levels == 0, the machine does not support setting the + * keyboard backlight level via kbd_state.level. + */ + +static int kbd_get_info(struct kbd_info *info) +{ + struct calling_interface_buffer buffer; + u8 units; + int ret; + + dell_fill_request(&buffer, 0, 0, 0, 0); + ret = dell_send_request(&buffer, + CLASS_KBD_BACKLIGHT, SELECT_KBD_BACKLIGHT); + if (ret) + return ret; + + info->modes = buffer.output[1] & 0xFFFF; + info->type = (buffer.output[1] >> 24) & 0xFF; + info->triggers = buffer.output[2] & 0xFF; + units = (buffer.output[2] >> 8) & 0xFF; + info->levels = (buffer.output[2] >> 16) & 0xFF; + + if (quirks && quirks->kbd_led_levels_off_1 && info->levels) + info->levels--; + + if (units & BIT(0)) + info->seconds = (buffer.output[3] >> 0) & 0xFF; + if (units & BIT(1)) + info->minutes = (buffer.output[3] >> 8) & 0xFF; + if (units & BIT(2)) + info->hours = (buffer.output[3] >> 16) & 0xFF; + if (units & BIT(3)) + info->days = (buffer.output[3] >> 24) & 0xFF; + + return ret; +} + +static unsigned int kbd_get_max_level(void) +{ + if (kbd_info.levels != 0) + return kbd_info.levels; + if (kbd_mode_levels_count > 0) + return kbd_mode_levels_count - 1; + return 0; +} + +static int kbd_get_level(struct kbd_state *state) +{ + int i; + + if (kbd_info.levels != 0) + return state->level; + + if (kbd_mode_levels_count > 0) { + for (i = 0; i < kbd_mode_levels_count; ++i) + if (kbd_mode_levels[i] == state->mode_bit) + return i; + return 0; + } + + return -EINVAL; +} + +static int kbd_set_level(struct kbd_state *state, u8 level) +{ + if (kbd_info.levels != 0) { + if (level != 0) + kbd_previous_level = level; + if (state->level == level) + return 0; + state->level = level; + if (level != 0 && state->mode_bit == KBD_MODE_BIT_OFF) + state->mode_bit = kbd_previous_mode_bit; + else if (level == 0 && state->mode_bit != KBD_MODE_BIT_OFF) { + kbd_previous_mode_bit = state->mode_bit; + state->mode_bit = KBD_MODE_BIT_OFF; + } + return 0; + } + + if (kbd_mode_levels_count > 0 && level < kbd_mode_levels_count) { + if (level != 0) + kbd_previous_level = level; + state->mode_bit = kbd_mode_levels[level]; + return 0; + } + + return -EINVAL; +} + +static int kbd_get_state(struct kbd_state *state) +{ + struct calling_interface_buffer buffer; + int ret; + + dell_fill_request(&buffer, 0x1, 0, 0, 0); + ret = dell_send_request(&buffer, + CLASS_KBD_BACKLIGHT, SELECT_KBD_BACKLIGHT); + if (ret) + return ret; + + state->mode_bit = ffs(buffer.output[1] & 0xFFFF); + if (state->mode_bit != 0) + state->mode_bit--; + + state->triggers = (buffer.output[1] >> 16) & 0xFF; + state->timeout_value = (buffer.output[1] >> 24) & 0x3F; + state->timeout_unit = (buffer.output[1] >> 30) & 0x3; + state->als_setting = buffer.output[2] & 0xFF; + state->als_value = (buffer.output[2] >> 8) & 0xFF; + state->level = (buffer.output[2] >> 16) & 0xFF; + state->timeout_value_ac = (buffer.output[2] >> 24) & 0x3F; + state->timeout_unit_ac = (buffer.output[2] >> 30) & 0x3; + + return ret; +} + +static int kbd_set_state(struct kbd_state *state) +{ + struct calling_interface_buffer buffer; + int ret; + u32 input1; + u32 input2; + + input1 = BIT(state->mode_bit) & 0xFFFF; + input1 |= (state->triggers & 0xFF) << 16; + input1 |= (state->timeout_value & 0x3F) << 24; + input1 |= (state->timeout_unit & 0x3) << 30; + input2 = state->als_setting & 0xFF; + input2 |= (state->level & 0xFF) << 16; + input2 |= (state->timeout_value_ac & 0x3F) << 24; + input2 |= (state->timeout_unit_ac & 0x3) << 30; + dell_fill_request(&buffer, 0x2, input1, input2, 0); + ret = dell_send_request(&buffer, + CLASS_KBD_BACKLIGHT, SELECT_KBD_BACKLIGHT); + + return ret; +} + +static int kbd_set_state_safe(struct kbd_state *state, struct kbd_state *old) +{ + int ret; + + ret = kbd_set_state(state); + if (ret == 0) + return 0; + + /* + * When setting the new state fails,try to restore the previous one. + * This is needed on some machines where BIOS sets a default state when + * setting a new state fails. This default state could be all off. + */ + + if (kbd_set_state(old)) + pr_err("Setting old previous keyboard state failed\n"); + + return ret; +} + +static int kbd_set_token_bit(u8 bit) +{ + struct calling_interface_buffer buffer; + + if (bit >= ARRAY_SIZE(kbd_tokens)) + return -EINVAL; + + return dell_set_std_token_value(&buffer, kbd_tokens[bit], USE_TVAL); +} + +static int kbd_get_token_bit(u8 bit) +{ + struct calling_interface_buffer buffer; + struct calling_interface_token *token; + int ret; + int val; + + if (bit >= ARRAY_SIZE(kbd_tokens)) + return -EINVAL; + + token = dell_smbios_find_token(kbd_tokens[bit]); + if (!token) + return -EINVAL; + + dell_fill_request(&buffer, token->location, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_TOKEN_READ, SELECT_TOKEN_STD); + if (ret) + return ret; + + val = buffer.output[1]; + return (val == token->value); +} + +static int kbd_get_first_active_token_bit(void) +{ + int i; + int ret; + + for (i = 0; i < ARRAY_SIZE(kbd_tokens); ++i) { + ret = kbd_get_token_bit(i); + if (ret == 1) + return i; + } + + return ret; +} + +static int kbd_get_valid_token_counts(void) +{ + return hweight16(kbd_token_bits); +} + +static inline int kbd_init_info(void) +{ + struct kbd_state state; + int ret; + int i; + + ret = kbd_get_info(&kbd_info); + if (ret) + return ret; + + /* NOTE: Old models without KBD_LED_AC_TOKEN token supports only one + * timeout value which is shared for both battery and AC power + * settings. So do not try to set AC values on old models. + */ + if ((quirks && quirks->kbd_missing_ac_tag) || + dell_smbios_find_token(KBD_LED_AC_TOKEN)) + kbd_timeout_ac_supported = true; + + kbd_get_state(&state); + + /* NOTE: timeout value is stored in 6 bits so max value is 63 */ + if (kbd_info.seconds > 63) + kbd_info.seconds = 63; + if (kbd_info.minutes > 63) + kbd_info.minutes = 63; + if (kbd_info.hours > 63) + kbd_info.hours = 63; + if (kbd_info.days > 63) + kbd_info.days = 63; + + /* NOTE: On tested machines ON mode did not work and caused + * problems (turned backlight off) so do not use it + */ + kbd_info.modes &= ~BIT(KBD_MODE_BIT_ON); + + kbd_previous_level = kbd_get_level(&state); + kbd_previous_mode_bit = state.mode_bit; + + if (kbd_previous_level == 0 && kbd_get_max_level() != 0) + kbd_previous_level = 1; + + if (kbd_previous_mode_bit == KBD_MODE_BIT_OFF) { + kbd_previous_mode_bit = + ffs(kbd_info.modes & ~BIT(KBD_MODE_BIT_OFF)); + if (kbd_previous_mode_bit != 0) + kbd_previous_mode_bit--; + } + + if (kbd_info.modes & (BIT(KBD_MODE_BIT_ALS) | + BIT(KBD_MODE_BIT_TRIGGER_ALS))) + kbd_als_supported = true; + + if (kbd_info.modes & ( + BIT(KBD_MODE_BIT_TRIGGER_ALS) | BIT(KBD_MODE_BIT_TRIGGER) | + BIT(KBD_MODE_BIT_TRIGGER_25) | BIT(KBD_MODE_BIT_TRIGGER_50) | + BIT(KBD_MODE_BIT_TRIGGER_75) | BIT(KBD_MODE_BIT_TRIGGER_100) + )) + kbd_triggers_supported = true; + + /* kbd_mode_levels[0] is reserved, see below */ + for (i = 0; i < 16; ++i) + if (kbd_is_level_mode_bit(i) && (BIT(i) & kbd_info.modes)) + kbd_mode_levels[1 + kbd_mode_levels_count++] = i; + + /* + * Find the first supported mode and assign to kbd_mode_levels[0]. + * This should be 0 (off), but we cannot depend on the BIOS to + * support 0. + */ + if (kbd_mode_levels_count > 0) { + for (i = 0; i < 16; ++i) { + if (BIT(i) & kbd_info.modes) { + kbd_mode_levels[0] = i; + break; + } + } + kbd_mode_levels_count++; + } + + return 0; + +} + +static inline void __init kbd_init_tokens(void) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(kbd_tokens); ++i) + if (dell_smbios_find_token(kbd_tokens[i])) + kbd_token_bits |= BIT(i); +} + +static void __init kbd_init(void) +{ + int ret; + + if (quirks && quirks->kbd_led_not_present) + return; + + ret = kbd_init_info(); + kbd_init_tokens(); + + /* + * Only supports keyboard backlight when it has at least two modes. + */ + if ((ret == 0 && (kbd_info.levels != 0 || kbd_mode_levels_count >= 2)) + || kbd_get_valid_token_counts() >= 2) + kbd_led_present = true; +} + +static ssize_t kbd_led_timeout_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct kbd_state new_state; + struct kbd_state state; + bool convert; + int value; + int ret; + char ch; + u8 unit; + int i; + + ret = sscanf(buf, "%d %c", &value, &ch); + if (ret < 1) + return -EINVAL; + else if (ret == 1) + ch = 's'; + + if (value < 0) + return -EINVAL; + + convert = false; + + switch (ch) { + case 's': + if (value > kbd_info.seconds) + convert = true; + unit = KBD_TIMEOUT_SECONDS; + break; + case 'm': + if (value > kbd_info.minutes) + convert = true; + unit = KBD_TIMEOUT_MINUTES; + break; + case 'h': + if (value > kbd_info.hours) + convert = true; + unit = KBD_TIMEOUT_HOURS; + break; + case 'd': + if (value > kbd_info.days) + convert = true; + unit = KBD_TIMEOUT_DAYS; + break; + default: + return -EINVAL; + } + + if (quirks && quirks->needs_kbd_timeouts) + convert = true; + + if (convert) { + /* Convert value from current units to seconds */ + switch (unit) { + case KBD_TIMEOUT_DAYS: + value *= 24; + fallthrough; + case KBD_TIMEOUT_HOURS: + value *= 60; + fallthrough; + case KBD_TIMEOUT_MINUTES: + value *= 60; + unit = KBD_TIMEOUT_SECONDS; + } + + if (quirks && quirks->needs_kbd_timeouts) { + for (i = 0; quirks->kbd_timeouts[i] != -1; i++) { + if (value <= quirks->kbd_timeouts[i]) { + value = quirks->kbd_timeouts[i]; + break; + } + } + } + + if (value <= kbd_info.seconds && kbd_info.seconds) { + unit = KBD_TIMEOUT_SECONDS; + } else if (value / 60 <= kbd_info.minutes && kbd_info.minutes) { + value /= 60; + unit = KBD_TIMEOUT_MINUTES; + } else if (value / (60 * 60) <= kbd_info.hours && kbd_info.hours) { + value /= (60 * 60); + unit = KBD_TIMEOUT_HOURS; + } else if (value / (60 * 60 * 24) <= kbd_info.days && kbd_info.days) { + value /= (60 * 60 * 24); + unit = KBD_TIMEOUT_DAYS; + } else { + return -EINVAL; + } + } + + mutex_lock(&kbd_led_mutex); + + ret = kbd_get_state(&state); + if (ret) + goto out; + + new_state = state; + + if (kbd_timeout_ac_supported && power_supply_is_system_supplied() > 0) { + new_state.timeout_value_ac = value; + new_state.timeout_unit_ac = unit; + } else { + new_state.timeout_value = value; + new_state.timeout_unit = unit; + } + + ret = kbd_set_state_safe(&new_state, &state); + if (ret) + goto out; + + ret = count; +out: + mutex_unlock(&kbd_led_mutex); + return ret; +} + +static ssize_t kbd_led_timeout_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct kbd_state state; + int value; + int ret; + int len; + u8 unit; + + ret = kbd_get_state(&state); + if (ret) + return ret; + + if (kbd_timeout_ac_supported && power_supply_is_system_supplied() > 0) { + value = state.timeout_value_ac; + unit = state.timeout_unit_ac; + } else { + value = state.timeout_value; + unit = state.timeout_unit; + } + + len = sprintf(buf, "%d", value); + + switch (unit) { + case KBD_TIMEOUT_SECONDS: + return len + sprintf(buf+len, "s\n"); + case KBD_TIMEOUT_MINUTES: + return len + sprintf(buf+len, "m\n"); + case KBD_TIMEOUT_HOURS: + return len + sprintf(buf+len, "h\n"); + case KBD_TIMEOUT_DAYS: + return len + sprintf(buf+len, "d\n"); + default: + return -EINVAL; + } + + return len; +} + +static DEVICE_ATTR(stop_timeout, S_IRUGO | S_IWUSR, + kbd_led_timeout_show, kbd_led_timeout_store); + +static const char * const kbd_led_triggers[] = { + "keyboard", + "touchpad", + /*"trackstick"*/ NULL, /* NOTE: trackstick is just alias for touchpad */ + "mouse", +}; + +static ssize_t kbd_led_triggers_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct kbd_state new_state; + struct kbd_state state; + bool triggers_enabled = false; + int trigger_bit = -1; + char trigger[21]; + int i, ret; + + ret = sscanf(buf, "%20s", trigger); + if (ret != 1) + return -EINVAL; + + if (trigger[0] != '+' && trigger[0] != '-') + return -EINVAL; + + mutex_lock(&kbd_led_mutex); + + ret = kbd_get_state(&state); + if (ret) + goto out; + + if (kbd_triggers_supported) + triggers_enabled = kbd_is_trigger_mode_bit(state.mode_bit); + + if (kbd_triggers_supported) { + for (i = 0; i < ARRAY_SIZE(kbd_led_triggers); ++i) { + if (!(kbd_info.triggers & BIT(i))) + continue; + if (!kbd_led_triggers[i]) + continue; + if (strcmp(trigger+1, kbd_led_triggers[i]) != 0) + continue; + if (trigger[0] == '+' && + triggers_enabled && (state.triggers & BIT(i))) { + ret = count; + goto out; + } + if (trigger[0] == '-' && + (!triggers_enabled || !(state.triggers & BIT(i)))) { + ret = count; + goto out; + } + trigger_bit = i; + break; + } + } + + if (trigger_bit == -1) { + ret = -EINVAL; + goto out; + } + + new_state = state; + if (trigger[0] == '+') + new_state.triggers |= BIT(trigger_bit); + else { + new_state.triggers &= ~BIT(trigger_bit); + /* + * NOTE: trackstick bit (2) must be disabled when + * disabling touchpad bit (1), otherwise touchpad + * bit (1) will not be disabled + */ + if (trigger_bit == 1) + new_state.triggers &= ~BIT(2); + } + if ((kbd_info.triggers & new_state.triggers) != + new_state.triggers) { + ret = -EINVAL; + goto out; + } + if (new_state.triggers && !triggers_enabled) { + new_state.mode_bit = KBD_MODE_BIT_TRIGGER; + kbd_set_level(&new_state, kbd_previous_level); + } else if (new_state.triggers == 0) { + kbd_set_level(&new_state, 0); + } + if (!(kbd_info.modes & BIT(new_state.mode_bit))) { + ret = -EINVAL; + goto out; + } + ret = kbd_set_state_safe(&new_state, &state); + if (ret) + goto out; + if (new_state.mode_bit != KBD_MODE_BIT_OFF) + kbd_previous_mode_bit = new_state.mode_bit; + ret = count; +out: + mutex_unlock(&kbd_led_mutex); + return ret; +} + +static ssize_t kbd_led_triggers_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct kbd_state state; + bool triggers_enabled; + int level, i, ret; + int len = 0; + + ret = kbd_get_state(&state); + if (ret) + return ret; + + len = 0; + + if (kbd_triggers_supported) { + triggers_enabled = kbd_is_trigger_mode_bit(state.mode_bit); + level = kbd_get_level(&state); + for (i = 0; i < ARRAY_SIZE(kbd_led_triggers); ++i) { + if (!(kbd_info.triggers & BIT(i))) + continue; + if (!kbd_led_triggers[i]) + continue; + if ((triggers_enabled || level <= 0) && + (state.triggers & BIT(i))) + buf[len++] = '+'; + else + buf[len++] = '-'; + len += sprintf(buf+len, "%s ", kbd_led_triggers[i]); + } + } + + if (len) + buf[len - 1] = '\n'; + + return len; +} + +static DEVICE_ATTR(start_triggers, S_IRUGO | S_IWUSR, + kbd_led_triggers_show, kbd_led_triggers_store); + +static ssize_t kbd_led_als_enabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct kbd_state new_state; + struct kbd_state state; + bool triggers_enabled = false; + int enable; + int ret; + + ret = kstrtoint(buf, 0, &enable); + if (ret) + return ret; + + mutex_lock(&kbd_led_mutex); + + ret = kbd_get_state(&state); + if (ret) + goto out; + + if (enable == kbd_is_als_mode_bit(state.mode_bit)) { + ret = count; + goto out; + } + + new_state = state; + + if (kbd_triggers_supported) + triggers_enabled = kbd_is_trigger_mode_bit(state.mode_bit); + + if (enable) { + if (triggers_enabled) + new_state.mode_bit = KBD_MODE_BIT_TRIGGER_ALS; + else + new_state.mode_bit = KBD_MODE_BIT_ALS; + } else { + if (triggers_enabled) { + new_state.mode_bit = KBD_MODE_BIT_TRIGGER; + kbd_set_level(&new_state, kbd_previous_level); + } else { + new_state.mode_bit = KBD_MODE_BIT_ON; + } + } + if (!(kbd_info.modes & BIT(new_state.mode_bit))) { + ret = -EINVAL; + goto out; + } + + ret = kbd_set_state_safe(&new_state, &state); + if (ret) + goto out; + kbd_previous_mode_bit = new_state.mode_bit; + + ret = count; +out: + mutex_unlock(&kbd_led_mutex); + return ret; +} + +static ssize_t kbd_led_als_enabled_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct kbd_state state; + bool enabled = false; + int ret; + + ret = kbd_get_state(&state); + if (ret) + return ret; + enabled = kbd_is_als_mode_bit(state.mode_bit); + + return sprintf(buf, "%d\n", enabled ? 1 : 0); +} + +static DEVICE_ATTR(als_enabled, S_IRUGO | S_IWUSR, + kbd_led_als_enabled_show, kbd_led_als_enabled_store); + +static ssize_t kbd_led_als_setting_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct kbd_state state; + struct kbd_state new_state; + u8 setting; + int ret; + + ret = kstrtou8(buf, 10, &setting); + if (ret) + return ret; + + mutex_lock(&kbd_led_mutex); + + ret = kbd_get_state(&state); + if (ret) + goto out; + + new_state = state; + new_state.als_setting = setting; + + ret = kbd_set_state_safe(&new_state, &state); + if (ret) + goto out; + + ret = count; +out: + mutex_unlock(&kbd_led_mutex); + return ret; +} + +static ssize_t kbd_led_als_setting_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct kbd_state state; + int ret; + + ret = kbd_get_state(&state); + if (ret) + return ret; + + return sprintf(buf, "%d\n", state.als_setting); +} + +static DEVICE_ATTR(als_setting, S_IRUGO | S_IWUSR, + kbd_led_als_setting_show, kbd_led_als_setting_store); + +static struct attribute *kbd_led_attrs[] = { + &dev_attr_stop_timeout.attr, + &dev_attr_start_triggers.attr, + NULL, +}; + +static const struct attribute_group kbd_led_group = { + .attrs = kbd_led_attrs, +}; + +static struct attribute *kbd_led_als_attrs[] = { + &dev_attr_als_enabled.attr, + &dev_attr_als_setting.attr, + NULL, +}; + +static const struct attribute_group kbd_led_als_group = { + .attrs = kbd_led_als_attrs, +}; + +static const struct attribute_group *kbd_led_groups[] = { + &kbd_led_group, + &kbd_led_als_group, + NULL, +}; + +static enum led_brightness kbd_led_level_get(struct led_classdev *led_cdev) +{ + int ret; + u16 num; + struct kbd_state state; + + if (kbd_get_max_level()) { + ret = kbd_get_state(&state); + if (ret) + return 0; + ret = kbd_get_level(&state); + if (ret < 0) + return 0; + return ret; + } + + if (kbd_get_valid_token_counts()) { + ret = kbd_get_first_active_token_bit(); + if (ret < 0) + return 0; + for (num = kbd_token_bits; num != 0 && ret > 0; --ret) + num &= num - 1; /* clear the first bit set */ + if (num == 0) + return 0; + return ffs(num) - 1; + } + + pr_warn("Keyboard brightness level control not supported\n"); + return 0; +} + +static int kbd_led_level_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + enum led_brightness new_value = value; + struct kbd_state state; + struct kbd_state new_state; + u16 num; + int ret; + + mutex_lock(&kbd_led_mutex); + + if (kbd_get_max_level()) { + ret = kbd_get_state(&state); + if (ret) + goto out; + new_state = state; + ret = kbd_set_level(&new_state, value); + if (ret) + goto out; + ret = kbd_set_state_safe(&new_state, &state); + } else if (kbd_get_valid_token_counts()) { + for (num = kbd_token_bits; num != 0 && value > 0; --value) + num &= num - 1; /* clear the first bit set */ + if (num == 0) + ret = 0; + else + ret = kbd_set_token_bit(ffs(num) - 1); + } else { + pr_warn("Keyboard brightness level control not supported\n"); + ret = -ENXIO; + } + +out: + if (ret == 0) + kbd_led_level = new_value; + + mutex_unlock(&kbd_led_mutex); + return ret; +} + +static struct led_classdev kbd_led = { + .name = "dell::kbd_backlight", + .flags = LED_BRIGHT_HW_CHANGED, + .brightness_set_blocking = kbd_led_level_set, + .brightness_get = kbd_led_level_get, + .groups = kbd_led_groups, +}; + +static int __init kbd_led_init(struct device *dev) +{ + int ret; + + kbd_init(); + if (!kbd_led_present) + return -ENODEV; + if (!kbd_als_supported) + kbd_led_groups[1] = NULL; + kbd_led.max_brightness = kbd_get_max_level(); + if (!kbd_led.max_brightness) { + kbd_led.max_brightness = kbd_get_valid_token_counts(); + if (kbd_led.max_brightness) + kbd_led.max_brightness--; + } + + kbd_led_level = kbd_led_level_get(NULL); + + ret = led_classdev_register(dev, &kbd_led); + if (ret) + kbd_led_present = false; + + return ret; +} + +static void brightness_set_exit(struct led_classdev *led_cdev, + enum led_brightness value) +{ + /* Don't change backlight level on exit */ +}; + +static void kbd_led_exit(void) +{ + if (!kbd_led_present) + return; + kbd_led.brightness_set = brightness_set_exit; + led_classdev_unregister(&kbd_led); +} + +static int dell_laptop_notifier_call(struct notifier_block *nb, + unsigned long action, void *data) +{ + bool changed = false; + enum led_brightness new_kbd_led_level; + + switch (action) { + case DELL_LAPTOP_KBD_BACKLIGHT_BRIGHTNESS_CHANGED: + if (!kbd_led_present) + break; + + mutex_lock(&kbd_led_mutex); + new_kbd_led_level = kbd_led_level_get(&kbd_led); + if (kbd_led_level != new_kbd_led_level) { + kbd_led_level = new_kbd_led_level; + changed = true; + } + mutex_unlock(&kbd_led_mutex); + + if (changed) + led_classdev_notify_brightness_hw_changed(&kbd_led, + kbd_led_level); + break; + } + + return NOTIFY_OK; +} + +static struct notifier_block dell_laptop_notifier = { + .notifier_call = dell_laptop_notifier_call, +}; + +static int micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct calling_interface_buffer buffer; + u32 tokenid; + + tokenid = brightness == LED_OFF ? + GLOBAL_MIC_MUTE_DISABLE : GLOBAL_MIC_MUTE_ENABLE; + return dell_set_std_token_value(&buffer, tokenid, USE_TVAL); +} + +static struct led_classdev micmute_led_cdev = { + .name = "platform::micmute", + .max_brightness = 1, + .brightness_set_blocking = micmute_led_set, + .default_trigger = "audio-micmute", +}; + +static int mute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct calling_interface_buffer buffer; + u32 tokenid; + + tokenid = brightness == LED_OFF ? + GLOBAL_MUTE_DISABLE : GLOBAL_MUTE_ENABLE; + return dell_set_std_token_value(&buffer, tokenid, USE_TVAL); +} + +static struct led_classdev mute_led_cdev = { + .name = "platform::mute", + .max_brightness = 1, + .brightness_set_blocking = mute_led_set, + .default_trigger = "audio-mute", +}; + +static int dell_battery_set_mode(const u16 tokenid) +{ + struct calling_interface_buffer buffer; + + return dell_set_std_token_value(&buffer, tokenid, USE_TVAL); +} + +static int dell_battery_read(const u16 tokenid) +{ + struct calling_interface_buffer buffer; + int err; + + err = dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_READ, + SELECT_TOKEN_STD, tokenid, 0); + if (err) + return err; + + if (buffer.output[1] > INT_MAX) + return -EIO; + + return buffer.output[1]; +} + +static bool dell_battery_mode_is_active(const u16 tokenid) +{ + struct calling_interface_token *token; + int ret; + + ret = dell_battery_read(tokenid); + if (ret < 0) + return false; + + token = dell_smbios_find_token(tokenid); + /* token's already verified by dell_battery_read() */ + + return token->value == (u16) ret; +} + +/* + * The rules: the minimum start charging value is 50%. The maximum + * start charging value is 95%. The minimum end charging value is + * 55%. The maximum end charging value is 100%. And finally, there + * has to be at least a 5% difference between start & end values. + */ +#define CHARGE_START_MIN 50 +#define CHARGE_START_MAX 95 +#define CHARGE_END_MIN 55 +#define CHARGE_END_MAX 100 +#define CHARGE_MIN_DIFF 5 + +static int dell_battery_set_custom_charge_start(int start) +{ + struct calling_interface_buffer buffer; + int end; + + start = clamp(start, CHARGE_START_MIN, CHARGE_START_MAX); + end = dell_battery_read(BAT_CUSTOM_CHARGE_END); + if (end < 0) + return end; + if ((end - start) < CHARGE_MIN_DIFF) + start = end - CHARGE_MIN_DIFF; + + return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_START, + start); +} + +static int dell_battery_set_custom_charge_end(int end) +{ + struct calling_interface_buffer buffer; + int start; + + end = clamp(end, CHARGE_END_MIN, CHARGE_END_MAX); + start = dell_battery_read(BAT_CUSTOM_CHARGE_START); + if (start < 0) + return start; + if ((end - start) < CHARGE_MIN_DIFF) + end = start + CHARGE_MIN_DIFF; + + return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_END, end); +} + +static ssize_t charge_types_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + enum power_supply_charge_type charge_type; + int i; + + for (i = 0; i < ARRAY_SIZE(battery_modes); i++) { + charge_type = battery_modes[i].charge_type; + + if (!(battery_supported_modes & BIT(charge_type))) + continue; + + if (!dell_battery_mode_is_active(battery_modes[i].token)) + continue; + + return power_supply_charge_types_show(dev, battery_supported_modes, + charge_type, buf); + } + + /* No active mode found */ + return -EIO; +} + +static ssize_t charge_types_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int charge_type, err, i; + + charge_type = power_supply_charge_types_parse(battery_supported_modes, buf); + if (charge_type < 0) + return charge_type; + + for (i = 0; i < ARRAY_SIZE(battery_modes); i++) { + if (battery_modes[i].charge_type == charge_type) + break; + } + if (i == ARRAY_SIZE(battery_modes)) + return -ENOENT; + + err = dell_battery_set_mode(battery_modes[i].token); + if (err) + return err; + + return size; +} + +static ssize_t charge_control_start_threshold_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int start; + + start = dell_battery_read(BAT_CUSTOM_CHARGE_START); + if (start < 0) + return start; + + if (start > CHARGE_START_MAX) + return -EIO; + + return sysfs_emit(buf, "%d\n", start); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int ret, start; + + ret = kstrtoint(buf, 10, &start); + if (ret) + return ret; + if (start < 0 || start > 100) + return -EINVAL; + + ret = dell_battery_set_custom_charge_start(start); + if (ret) + return ret; + + return size; +} + +static ssize_t charge_control_end_threshold_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int end; + + end = dell_battery_read(BAT_CUSTOM_CHARGE_END); + if (end < 0) + return end; + + if (end > CHARGE_END_MAX) + return -EIO; + + return sysfs_emit(buf, "%d\n", end); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int ret, end; + + ret = kstrtouint(buf, 10, &end); + if (ret) + return ret; + if (end < 0 || end > 100) + return -EINVAL; + + ret = dell_battery_set_custom_charge_end(end); + if (ret) + return ret; + + return size; +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); +static DEVICE_ATTR_RW(charge_control_end_threshold); +static DEVICE_ATTR_RW(charge_types); + +static struct attribute *dell_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + &dev_attr_charge_types.attr, + NULL, +}; +ATTRIBUTE_GROUPS(dell_battery); + +static bool dell_battery_supported(struct power_supply *battery) +{ + /* We currently only support the primary battery */ + return strcmp(battery->desc->name, "BAT0") == 0; +} + +static int dell_battery_add(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + /* Return 0 instead of an error to avoid being unloaded */ + if (!dell_battery_supported(battery)) + return 0; + + return device_add_groups(&battery->dev, dell_battery_groups); +} + +static int dell_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + if (!dell_battery_supported(battery)) + return 0; + + device_remove_groups(&battery->dev, dell_battery_groups); + return 0; +} + +static struct acpi_battery_hook dell_battery_hook = { + .add_battery = dell_battery_add, + .remove_battery = dell_battery_remove, + .name = "Dell Primary Battery Extension", +}; + +static u32 __init battery_get_supported_modes(void) +{ + u32 modes = 0; + int i; + + for (i = 0; i < ARRAY_SIZE(battery_modes); i++) { + if (dell_smbios_find_token(battery_modes[i].token)) + modes |= BIT(battery_modes[i].charge_type); + } + + return modes; +} + +static void __init dell_battery_init(struct device *dev) +{ + battery_supported_modes = battery_get_supported_modes(); + + if (battery_supported_modes != 0) + battery_hook_register(&dell_battery_hook); +} + +static void dell_battery_exit(void) +{ + if (battery_supported_modes != 0) + battery_hook_unregister(&dell_battery_hook); +} + +static int __init dell_init(void) +{ + struct calling_interface_buffer buffer; + int max_intensity = 0; + int ret; + + if (!dmi_check_system(dell_device_table)) + return -ENODEV; + + quirks = NULL; + /* find if this machine support other functions */ + dmi_check_system(dell_quirks); + + ret = platform_driver_register(&platform_driver); + if (ret) + goto fail_platform_driver; + platform_device = platform_device_alloc("dell-laptop", PLATFORM_DEVID_NONE); + if (!platform_device) { + ret = -ENOMEM; + goto fail_platform_device1; + } + ret = platform_device_add(platform_device); + if (ret) + goto fail_platform_device2; + + ret = dell_setup_rfkill(); + + if (ret) { + pr_warn("Unable to setup rfkill\n"); + goto fail_rfkill; + } + + if (quirks && quirks->touchpad_led) + touchpad_led_init(&platform_device->dev); + + kbd_led_init(&platform_device->dev); + dell_battery_init(&platform_device->dev); + + dell_laptop_dir = debugfs_create_dir("dell_laptop", NULL); + debugfs_create_file("rfkill", 0444, dell_laptop_dir, NULL, + &dell_debugfs_fops); + + dell_laptop_register_notifier(&dell_laptop_notifier); + + if (dell_smbios_find_token(GLOBAL_MIC_MUTE_DISABLE) && + dell_smbios_find_token(GLOBAL_MIC_MUTE_ENABLE) && + !dell_privacy_has_mic_mute()) { + ret = led_classdev_register(&platform_device->dev, &micmute_led_cdev); + if (ret < 0) + goto fail_led; + micmute_led_registered = true; + } + + if (dell_smbios_find_token(GLOBAL_MUTE_DISABLE) && + dell_smbios_find_token(GLOBAL_MUTE_ENABLE)) { + ret = led_classdev_register(&platform_device->dev, &mute_led_cdev); + if (ret < 0) + goto fail_backlight; + mute_led_registered = true; + } + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) + return 0; + + ret = dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_READ, + SELECT_TOKEN_AC, BRIGHTNESS_TOKEN, 0); + if (ret == 0) + max_intensity = buffer.output[3]; + + if (max_intensity) { + struct backlight_properties props; + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = max_intensity; + dell_backlight_device = backlight_device_register("dell_backlight", + &platform_device->dev, + NULL, + &dell_ops, + &props); + + if (IS_ERR(dell_backlight_device)) { + ret = PTR_ERR(dell_backlight_device); + dell_backlight_device = NULL; + goto fail_backlight; + } + + dell_backlight_device->props.brightness = + dell_get_intensity(dell_backlight_device); + if (dell_backlight_device->props.brightness < 0) { + ret = dell_backlight_device->props.brightness; + goto fail_get_brightness; + } + backlight_update_status(dell_backlight_device); + } + + return 0; + +fail_get_brightness: + backlight_device_unregister(dell_backlight_device); +fail_backlight: + if (micmute_led_registered) + led_classdev_unregister(&micmute_led_cdev); + if (mute_led_registered) + led_classdev_unregister(&mute_led_cdev); +fail_led: + dell_laptop_unregister_notifier(&dell_laptop_notifier); + debugfs_remove_recursive(dell_laptop_dir); + dell_battery_exit(); + kbd_led_exit(); + if (quirks && quirks->touchpad_led) + touchpad_led_exit(); + dell_cleanup_rfkill(); +fail_rfkill: + platform_device_del(platform_device); +fail_platform_device2: + platform_device_put(platform_device); +fail_platform_device1: + platform_driver_unregister(&platform_driver); +fail_platform_driver: + return ret; +} + +static void __exit dell_exit(void) +{ + dell_laptop_unregister_notifier(&dell_laptop_notifier); + debugfs_remove_recursive(dell_laptop_dir); + if (quirks && quirks->touchpad_led) + touchpad_led_exit(); + kbd_led_exit(); + dell_battery_exit(); + backlight_device_unregister(dell_backlight_device); + if (micmute_led_registered) + led_classdev_unregister(&micmute_led_cdev); + if (mute_led_registered) + led_classdev_unregister(&mute_led_cdev); + dell_cleanup_rfkill(); + if (platform_device) { + platform_device_unregister(platform_device); + platform_driver_unregister(&platform_driver); + } +} + +/* dell-rbtn.c driver export functions which will not work correctly (and could + * cause kernel crash) if they are called before dell-rbtn.c init code. This is + * not problem when dell-rbtn.c is compiled as external module. When both files + * (dell-rbtn.c and dell-laptop.c) are compiled statically into kernel, then we + * need to ensure that dell_init() will be called after initializing dell-rbtn. + * This can be achieved by late_initcall() instead module_init(). + */ +late_initcall(dell_init); +module_exit(dell_exit); + +MODULE_AUTHOR("Matthew Garrett "); +MODULE_AUTHOR("Gabriele Mazzotta "); +MODULE_AUTHOR("Pali Rohár "); +MODULE_DESCRIPTION("Dell laptop driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-lis3lv02d.c b/drivers/platform/x86/dell/dell-lis3lv02d.c new file mode 100644 index 000000000..fe52bcd89 --- /dev/null +++ b/drivers/platform/x86/dell/dell-lis3lv02d.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * lis3lv02d i2c-client instantiation for ACPI SMO88xx devices without I2C resources. + * + * Copyright (C) 2024 Hans de Goede + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include "dell-smo8800-ids.h" + +#define LIS3_WHO_AM_I 0x0f + +#define DELL_LIS3LV02D_DMI_ENTRY(product_name, i2c_addr) \ + { \ + .matches = { \ + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Dell Inc."), \ + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, product_name), \ + }, \ + .driver_data = (void *)(uintptr_t)(i2c_addr), \ + } + +/* + * Accelerometer's I2C address is not specified in DMI nor ACPI, + * so it is needed to define mapping table based on DMI product names. + */ +static const struct dmi_system_id lis3lv02d_devices[] __initconst = { + /* + * Dell platform team told us that these Latitude devices have + * ST microelectronics accelerometer at I2C address 0x29. + */ + DELL_LIS3LV02D_DMI_ENTRY("Latitude E5250", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E5450", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E5550", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440 ATG", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6540", 0x29), + /* + * Additional individual entries were added after verification. + */ + DELL_LIS3LV02D_DMI_ENTRY("Latitude 5400", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude 5480", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude 5500", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6330", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6430", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Latitude E6530", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Precision 3540", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Precision 3551", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Precision M6800", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("Vostro V131", 0x1d), + DELL_LIS3LV02D_DMI_ENTRY("Vostro 5568", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("XPS 15 7590", 0x29), + DELL_LIS3LV02D_DMI_ENTRY("XPS 15 9550", 0x29), + { } +}; + +static u8 i2c_addr; +static struct i2c_client *i2c_dev; +static bool notifier_registered; + +static bool probe_i2c_addr; +module_param(probe_i2c_addr, bool, 0444); +MODULE_PARM_DESC(probe_i2c_addr, "Probe the i801 I2C bus for the accelerometer on models where the address is unknown, this may be dangerous."); + +static int detect_lis3lv02d(struct i2c_adapter *adap, unsigned short addr) +{ + union i2c_smbus_data smbus_data; + int err; + + dev_info(&adap->dev, "Probing for lis3lv02d on address 0x%02x\n", addr); + + err = i2c_smbus_xfer(adap, addr, 0, I2C_SMBUS_READ, LIS3_WHO_AM_I, + I2C_SMBUS_BYTE_DATA, &smbus_data); + if (err < 0) + return 0; /* Not found */ + + /* valid who-am-i values are from drivers/misc/lis3lv02d/lis3lv02d.c */ + switch (smbus_data.byte) { + case 0x32: + case 0x33: + case 0x3a: + case 0x3b: + break; + default: + dev_warn(&adap->dev, "Unknown who-am-i register value 0x%02x\n", + smbus_data.byte); + return 0; /* Not found */ + } + + dev_info(&adap->dev, + "Detected lis3lv02d on address 0x%02x, please report this upstream to platform-driver-x86@vger.kernel.org so that a quirk can be added\n", + addr); + + return 1; /* Found */ +} + +static bool i2c_adapter_is_main_i801(struct i2c_adapter *adap) +{ + /* + * Only match the main I801 adapter and reject secondary adapters + * which names start with "SMBus I801 IDF adapter". + */ + return strstarts(adap->name, "SMBus I801 adapter"); +} + +static int find_i801(struct device *dev, void *data) +{ + struct i2c_adapter *adap, **adap_ret = data; + + adap = i2c_verify_adapter(dev); + if (!adap) + return 0; + + if (!i2c_adapter_is_main_i801(adap)) + return 0; + + *adap_ret = i2c_get_adapter(adap->nr); + return 1; +} + +static void instantiate_i2c_client(struct work_struct *work) +{ + struct i2c_board_info info = { }; + struct i2c_adapter *adap = NULL; + + if (i2c_dev) + return; + + /* + * bus_for_each_dev() and not i2c_for_each_dev() to avoid + * a deadlock when find_i801() calls i2c_get_adapter(). + */ + bus_for_each_dev(&i2c_bus_type, NULL, &adap, find_i801); + if (!adap) + return; + + strscpy(info.type, "lis3lv02d", I2C_NAME_SIZE); + + if (i2c_addr) { + info.addr = i2c_addr; + i2c_dev = i2c_new_client_device(adap, &info); + } else { + /* First try address 0x29 (most used) and then try 0x1d */ + static const unsigned short addr_list[] = { 0x29, 0x1d, I2C_CLIENT_END }; + + i2c_dev = i2c_new_scanned_device(adap, &info, addr_list, detect_lis3lv02d); + } + + if (IS_ERR(i2c_dev)) { + dev_err(&adap->dev, "error %ld registering i2c_client\n", PTR_ERR(i2c_dev)); + i2c_dev = NULL; + } else { + dev_dbg(&adap->dev, "registered lis3lv02d on address 0x%02x\n", info.addr); + } + + i2c_put_adapter(adap); +} +static DECLARE_WORK(i2c_work, instantiate_i2c_client); + +static int i2c_bus_notify(struct notifier_block *nb, unsigned long action, void *data) +{ + struct device *dev = data; + struct i2c_client *client; + struct i2c_adapter *adap; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + adap = i2c_verify_adapter(dev); + if (!adap) + break; + + if (i2c_adapter_is_main_i801(adap)) + queue_work(system_long_wq, &i2c_work); + break; + case BUS_NOTIFY_REMOVED_DEVICE: + client = i2c_verify_client(dev); + if (!client) + break; + + if (i2c_dev == client) { + dev_dbg(&client->adapter->dev, "lis3lv02d i2c_client removed\n"); + i2c_dev = NULL; + } + break; + default: + break; + } + + return 0; +} +static struct notifier_block i2c_nb = { .notifier_call = i2c_bus_notify }; + +static int __init match_acpi_device_ids(struct device *dev, const void *data) +{ + return acpi_match_device(data, dev) ? 1 : 0; +} + +static int __init dell_lis3lv02d_init(void) +{ + const struct dmi_system_id *lis3lv02d_dmi_id; + struct device *dev; + int err; + + /* + * First check for a matching platform_device. This protects against + * SMO88xx ACPI fwnodes which actually do have an I2C resource, which + * will already have an i2c_client instantiated (not a platform_device). + */ + dev = bus_find_device(&platform_bus_type, NULL, smo8800_ids, match_acpi_device_ids); + if (!dev) { + pr_debug("No SMO88xx platform-device found\n"); + return 0; + } + put_device(dev); + + lis3lv02d_dmi_id = dmi_first_match(lis3lv02d_devices); + if (!lis3lv02d_dmi_id && !probe_i2c_addr) { + pr_warn("accelerometer is present on SMBus but its address is unknown, skipping registration\n"); + pr_info("Pass dell_lis3lv02d.probe_i2c_addr=1 on the kernel command line to probe, this may be dangerous!\n"); + return 0; + } + + if (lis3lv02d_dmi_id) + i2c_addr = (long)lis3lv02d_dmi_id->driver_data; + + /* + * Register i2c-bus notifier + queue initial scan for lis3lv02d + * i2c_client instantiation. + */ + err = bus_register_notifier(&i2c_bus_type, &i2c_nb); + if (err) + return err; + + notifier_registered = true; + + queue_work(system_long_wq, &i2c_work); + return 0; +} +module_init(dell_lis3lv02d_init); + +static void __exit dell_lis3lv02d_module_exit(void) +{ + if (!notifier_registered) + return; + + bus_unregister_notifier(&i2c_bus_type, &i2c_nb); + cancel_work_sync(&i2c_work); + i2c_unregister_device(i2c_dev); +} +module_exit(dell_lis3lv02d_module_exit); + +MODULE_DESCRIPTION("lis3lv02d i2c-client instantiation for ACPI SMO88xx devices"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-pc.c b/drivers/platform/x86/dell/dell-pc.c new file mode 100644 index 000000000..becdd9aae --- /dev/null +++ b/drivers/platform/x86/dell/dell-pc.c @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for Dell laptop extras + * + * Copyright (c) Lyndon Sanche + * + * Based on documentation in the libsmbios package: + * Copyright (C) 2005-2014 Dell Inc. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dell-smbios.h" + +static struct faux_device *dell_pc_fdev; +static int supported_modes; + +static const struct dmi_system_id dell_device_table[] __initconst = { + { + .ident = "Dell Inc.", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + }, + }, + { + .ident = "Dell Computer Corporation", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Computer Corporation"), + }, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, dell_device_table); + +/* Derived from smbios-thermal-ctl + * + * cbClass 17 + * cbSelect 19 + * User Selectable Thermal Tables(USTT) + * cbArg1 determines the function to be performed + * cbArg1 0x0 = Get Thermal Information + * cbRES1 Standard return codes (0, -1, -2) + * cbRES2, byte 0 Bitmap of supported thermal modes. A mode is supported if + * its bit is set to 1 + * Bit 0 Balanced + * Bit 1 Cool Bottom + * Bit 2 Quiet + * Bit 3 Performance + * cbRES2, byte 1 Bitmap of supported Active Acoustic Controller (AAC) modes. + * Each mode corresponds to the supported thermal modes in + * byte 0. A mode is supported if its bit is set to 1. + * Bit 0 AAC (Balanced) + * Bit 1 AAC (Cool Bottom + * Bit 2 AAC (Quiet) + * Bit 3 AAC (Performance) + * cbRes3, byte 0 Current Thermal Mode + * Bit 0 Balanced + * Bit 1 Cool Bottom + * Bit 2 Quiet + * Bit 3 Performanc + * cbRes3, byte 1 AAC Configuration type + * 0 Global (AAC enable/disable applies to all supported USTT modes) + * 1 USTT mode specific + * cbRes3, byte 2 Current Active Acoustic Controller (AAC) Mode + * If AAC Configuration Type is Global, + * 0 AAC mode disabled + * 1 AAC mode enabled + * If AAC Configuration Type is USTT mode specific (multiple bits may be set), + * Bit 0 AAC (Balanced) + * Bit 1 AAC (Cool Bottom + * Bit 2 AAC (Quiet) + * Bit 3 AAC (Performance) + * cbRes3, byte 3 Current Fan Failure Mode + * Bit 0 Minimal Fan Failure (at least one fan has failed, one fan working) + * Bit 1 Catastrophic Fan Failure (all fans have failed) + * + * cbArg1 0x1 (Set Thermal Information), both desired thermal mode and + * desired AAC mode shall be applied + * cbArg2, byte 0 Desired Thermal Mode to set + * (only one bit may be set for this parameter) + * Bit 0 Balanced + * Bit 1 Cool Bottom + * Bit 2 Quiet + * Bit 3 Performance + * cbArg2, byte 1 Desired Active Acoustic Controller (AAC) Mode to set + * If AAC Configuration Type is Global, + * 0 AAC mode disabled + * 1 AAC mode enabled + * If AAC Configuration Type is USTT mode specific + * (multiple bits may be set for this parameter), + * Bit 0 AAC (Balanced) + * Bit 1 AAC (Cool Bottom + * Bit 2 AAC (Quiet) + * Bit 3 AAC (Performance) + */ + +#define DELL_ACC_GET_FIELD GENMASK(19, 16) +#define DELL_ACC_SET_FIELD GENMASK(11, 8) +#define DELL_THERMAL_SUPPORTED GENMASK(3, 0) + +enum thermal_mode_bits { + DELL_BALANCED = BIT(0), + DELL_COOL_BOTTOM = BIT(1), + DELL_QUIET = BIT(2), + DELL_PERFORMANCE = BIT(3), +}; + +static int thermal_get_mode(void) +{ + struct calling_interface_buffer buffer; + int state; + int ret; + + dell_fill_request(&buffer, 0x0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT); + if (ret) + return ret; + state = buffer.output[2]; + if (state & DELL_BALANCED) + return DELL_BALANCED; + else if (state & DELL_COOL_BOTTOM) + return DELL_COOL_BOTTOM; + else if (state & DELL_QUIET) + return DELL_QUIET; + else if (state & DELL_PERFORMANCE) + return DELL_PERFORMANCE; + else + return -ENXIO; +} + +static int thermal_get_supported_modes(int *supported_bits) +{ + struct calling_interface_buffer buffer; + int ret; + + dell_fill_request(&buffer, 0x0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT); + if (ret) + return ret; + *supported_bits = FIELD_GET(DELL_THERMAL_SUPPORTED, buffer.output[1]); + return 0; +} + +static int thermal_get_acc_mode(int *acc_mode) +{ + struct calling_interface_buffer buffer; + int ret; + + dell_fill_request(&buffer, 0x0, 0, 0, 0); + ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT); + if (ret) + return ret; + *acc_mode = FIELD_GET(DELL_ACC_GET_FIELD, buffer.output[3]); + return 0; +} + +static int thermal_set_mode(enum thermal_mode_bits state) +{ + struct calling_interface_buffer buffer; + int ret; + int acc_mode; + + ret = thermal_get_acc_mode(&acc_mode); + if (ret) + return ret; + + dell_fill_request(&buffer, 0x1, FIELD_PREP(DELL_ACC_SET_FIELD, acc_mode) | state, 0, 0); + return dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT); +} + +static int thermal_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + switch (profile) { + case PLATFORM_PROFILE_BALANCED: + return thermal_set_mode(DELL_BALANCED); + case PLATFORM_PROFILE_PERFORMANCE: + return thermal_set_mode(DELL_PERFORMANCE); + case PLATFORM_PROFILE_QUIET: + return thermal_set_mode(DELL_QUIET); + case PLATFORM_PROFILE_COOL: + return thermal_set_mode(DELL_COOL_BOTTOM); + default: + return -EOPNOTSUPP; + } +} + +static int thermal_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + int ret; + + ret = thermal_get_mode(); + if (ret < 0) + return ret; + + switch (ret) { + case DELL_BALANCED: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case DELL_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case DELL_COOL_BOTTOM: + *profile = PLATFORM_PROFILE_COOL; + break; + case DELL_QUIET: + *profile = PLATFORM_PROFILE_QUIET; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int thermal_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + int current_mode; + + if (supported_modes & DELL_QUIET) + __set_bit(PLATFORM_PROFILE_QUIET, choices); + if (supported_modes & DELL_COOL_BOTTOM) + __set_bit(PLATFORM_PROFILE_COOL, choices); + if (supported_modes & DELL_BALANCED) + __set_bit(PLATFORM_PROFILE_BALANCED, choices); + if (supported_modes & DELL_PERFORMANCE) + __set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + + /* Make sure that ACPI is in sync with the profile set by USTT */ + current_mode = thermal_get_mode(); + if (current_mode < 0) + return current_mode; + + thermal_set_mode(current_mode); + + return 0; +} + +static const struct platform_profile_ops dell_pc_platform_profile_ops = { + .probe = thermal_platform_profile_probe, + .profile_get = thermal_platform_profile_get, + .profile_set = thermal_platform_profile_set, +}; + +static int dell_pc_faux_probe(struct faux_device *fdev) +{ + struct device *ppdev; + int ret; + + if (!dell_smbios_class_is_supported(CLASS_INFO)) + return -ENODEV; + + ret = thermal_get_supported_modes(&supported_modes); + if (ret < 0) + return ret; + + ppdev = devm_platform_profile_register(&fdev->dev, "dell-pc", NULL, + &dell_pc_platform_profile_ops); + + return PTR_ERR_OR_ZERO(ppdev); +} + +static const struct faux_device_ops dell_pc_faux_ops = { + .probe = dell_pc_faux_probe, +}; + +static int __init dell_init(void) +{ + if (!dmi_check_system(dell_device_table)) + return -ENODEV; + + dell_pc_fdev = faux_device_create("dell-pc", NULL, &dell_pc_faux_ops); + if (!dell_pc_fdev) + return -ENODEV; + + return 0; +} + +static void __exit dell_exit(void) +{ + faux_device_destroy(dell_pc_fdev); +} + +module_init(dell_init); +module_exit(dell_exit); + +MODULE_AUTHOR("Lyndon Sanche "); +MODULE_DESCRIPTION("Dell PC driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-rbtn.c b/drivers/platform/x86/dell/dell-rbtn.c new file mode 100644 index 000000000..180b8c672 --- /dev/null +++ b/drivers/platform/x86/dell/dell-rbtn.c @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Dell Airplane Mode Switch driver + Copyright (C) 2014-2015 Pali Rohár + +*/ + +#include +#include +#include +#include +#include + +#include "dell-rbtn.h" + +enum rbtn_type { + RBTN_UNKNOWN, + RBTN_TOGGLE, + RBTN_SLIDER, +}; + +struct rbtn_data { + enum rbtn_type type; + struct rfkill *rfkill; + struct input_dev *input_dev; + bool suspended; +}; + + +/* + * acpi functions + */ + +static enum rbtn_type rbtn_check(struct acpi_device *device) +{ + unsigned long long output; + acpi_status status; + + status = acpi_evaluate_integer(device->handle, "CRBT", NULL, &output); + if (ACPI_FAILURE(status)) + return RBTN_UNKNOWN; + + switch (output) { + case 0: + case 1: + return RBTN_TOGGLE; + case 2: + case 3: + return RBTN_SLIDER; + default: + return RBTN_UNKNOWN; + } +} + +static int rbtn_get(struct acpi_device *device) +{ + unsigned long long output; + acpi_status status; + + status = acpi_evaluate_integer(device->handle, "GRBT", NULL, &output); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return !output; +} + +static int rbtn_acquire(struct acpi_device *device, bool enable) +{ + struct acpi_object_list input; + union acpi_object param; + acpi_status status; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = enable; + input.count = 1; + input.pointer = ¶m; + + status = acpi_evaluate_object(device->handle, "ARBT", &input, NULL); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return 0; +} + + +/* + * rfkill device + */ + +static void rbtn_rfkill_query(struct rfkill *rfkill, void *data) +{ + struct acpi_device *device = data; + int state; + + state = rbtn_get(device); + if (state < 0) + return; + + rfkill_set_states(rfkill, state, state); +} + +static int rbtn_rfkill_set_block(void *data, bool blocked) +{ + /* NOTE: setting soft rfkill state is not supported */ + return -EINVAL; +} + +static const struct rfkill_ops rbtn_ops = { + .query = rbtn_rfkill_query, + .set_block = rbtn_rfkill_set_block, +}; + +static int rbtn_rfkill_init(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + int ret; + + if (rbtn_data->rfkill) + return 0; + + /* + * NOTE: rbtn controls all radio devices, not only WLAN + * but rfkill interface does not support "ANY" type + * so "WLAN" type is used + */ + rbtn_data->rfkill = rfkill_alloc("dell-rbtn", dev, RFKILL_TYPE_WLAN, + &rbtn_ops, ACPI_COMPANION(dev)); + if (!rbtn_data->rfkill) + return -ENOMEM; + + ret = rfkill_register(rbtn_data->rfkill); + if (ret) { + rfkill_destroy(rbtn_data->rfkill); + rbtn_data->rfkill = NULL; + return ret; + } + + return 0; +} + +static void rbtn_rfkill_exit(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + + if (!rbtn_data->rfkill) + return; + + rfkill_unregister(rbtn_data->rfkill); + rfkill_destroy(rbtn_data->rfkill); + rbtn_data->rfkill = NULL; +} + +static void rbtn_rfkill_event(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + + if (rbtn_data->rfkill) + rbtn_rfkill_query(rbtn_data->rfkill, ACPI_COMPANION(dev)); +} + + +/* + * input device + */ + +static int rbtn_input_init(struct rbtn_data *rbtn_data) +{ + int ret; + + rbtn_data->input_dev = input_allocate_device(); + if (!rbtn_data->input_dev) + return -ENOMEM; + + rbtn_data->input_dev->name = "DELL Wireless hotkeys"; + rbtn_data->input_dev->phys = "dellabce/input0"; + rbtn_data->input_dev->id.bustype = BUS_HOST; + rbtn_data->input_dev->evbit[0] = BIT(EV_KEY); + set_bit(KEY_RFKILL, rbtn_data->input_dev->keybit); + + ret = input_register_device(rbtn_data->input_dev); + if (ret) { + input_free_device(rbtn_data->input_dev); + rbtn_data->input_dev = NULL; + return ret; + } + + return 0; +} + +static void rbtn_input_exit(struct rbtn_data *rbtn_data) +{ + input_unregister_device(rbtn_data->input_dev); + rbtn_data->input_dev = NULL; +} + +static void rbtn_input_event(struct rbtn_data *rbtn_data) +{ + input_report_key(rbtn_data->input_dev, KEY_RFKILL, 1); + input_sync(rbtn_data->input_dev); + input_report_key(rbtn_data->input_dev, KEY_RFKILL, 0); + input_sync(rbtn_data->input_dev); +} + + +/* + * acpi driver + */ + +static int rbtn_probe(struct platform_device *pdev); +static void rbtn_remove(struct platform_device *pdev); +static void rbtn_notify(acpi_handle handle, u32 event, void *data); + +static const struct acpi_device_id rbtn_ids[] = { + { "DELRBTN", 0 }, + { "DELLABCE", 0 }, + + /* + * This driver can also handle the "DELLABC6" device that + * appears on the XPS 13 9350, but that device is disabled by + * the DSDT unless booted with acpi_osi="!Windows 2012" + * acpi_osi="!Windows 2013". + * + * According to Mario at Dell: + * + * DELLABC6 is a custom interface that was created solely to + * have airplane mode support for Windows 7. For Windows 10 + * the proper interface is to use that which is handled by + * intel-hid. A OEM airplane mode driver is not used. + * + * Since the kernel doesn't identify as Windows 7 it would be + * incorrect to do attempt to use that interface. + * + * Even if we override _OSI and bind to DELLABC6, we end up with + * inconsistent behavior in which userspace can get out of sync + * with the rfkill state as it conflicts with events from + * intel-hid. + * + * The upshot is that it is better to just ignore DELLABC6 + * devices. + */ + + { "", 0 }, +}; + +#ifdef CONFIG_PM_SLEEP +static void ACPI_SYSTEM_XFACE rbtn_clear_suspended_flag(void *context) +{ + struct rbtn_data *rbtn_data = context; + + rbtn_data->suspended = false; +} + +static int rbtn_suspend(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + + rbtn_data->suspended = true; + + return 0; +} + +static int rbtn_resume(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + acpi_status status; + + /* + * Upon resume, some BIOSes send an ACPI notification thet triggers + * an unwanted input event. In order to ignore it, we use a flag + * that we set at suspend and clear once we have received the extra + * ACPI notification. Since ACPI notifications are delivered + * asynchronously to drivers, we clear the flag from the workqueue + * used to deliver the notifications. This should be enough + * to have the flag cleared only after we received the extra + * notification, if any. + */ + status = acpi_os_execute(OSL_NOTIFY_HANDLER, + rbtn_clear_suspended_flag, rbtn_data); + if (ACPI_FAILURE(status)) + rbtn_clear_suspended_flag(rbtn_data); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(rbtn_pm_ops, rbtn_suspend, rbtn_resume); + +static struct platform_driver rbtn_driver = { + .probe = rbtn_probe, + .remove = rbtn_remove, + .driver = { + .name = "dell-rbtn", + .acpi_match_table = rbtn_ids, + .pm = &rbtn_pm_ops, + }, +}; + + +/* + * notifier export functions + */ + +static bool auto_remove_rfkill = true; + +static ATOMIC_NOTIFIER_HEAD(rbtn_chain_head); + +static int rbtn_inc_count(struct device *dev, void *data) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + int *count = data; + + if (rbtn_data->type == RBTN_SLIDER) + (*count)++; + + return 0; +} + +static int rbtn_switch_dev(struct device *dev, void *data) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + bool enable = data; + + if (rbtn_data->type != RBTN_SLIDER) + return 0; + + if (enable) + rbtn_rfkill_init(dev); + else + rbtn_rfkill_exit(dev); + + return 0; +} + +int dell_rbtn_notifier_register(struct notifier_block *nb) +{ + bool first; + int count; + int ret; + + count = 0; + ret = driver_for_each_device(&rbtn_driver.driver, NULL, &count, + rbtn_inc_count); + if (ret || count == 0) + return -ENODEV; + + first = !rbtn_chain_head.head; + + ret = atomic_notifier_chain_register(&rbtn_chain_head, nb); + if (ret != 0) + return ret; + + if (auto_remove_rfkill && first) + ret = driver_for_each_device(&rbtn_driver.driver, NULL, + (void *)false, rbtn_switch_dev); + + return ret; +} +EXPORT_SYMBOL_GPL(dell_rbtn_notifier_register); + +int dell_rbtn_notifier_unregister(struct notifier_block *nb) +{ + int ret; + + ret = atomic_notifier_chain_unregister(&rbtn_chain_head, nb); + if (ret != 0) + return ret; + + if (auto_remove_rfkill && !rbtn_chain_head.head) + ret = driver_for_each_device(&rbtn_driver.driver, NULL, + (void *)true, rbtn_switch_dev); + + return ret; +} +EXPORT_SYMBOL_GPL(dell_rbtn_notifier_unregister); + + +/* + * acpi driver functions + */ + +static void rbtn_cleanup(struct device *dev) +{ + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + + switch (rbtn_data->type) { + case RBTN_TOGGLE: + rbtn_input_exit(rbtn_data); + break; + case RBTN_SLIDER: + rbtn_rfkill_exit(dev); + break; + default: + break; + } +} + +static int rbtn_probe(struct platform_device *pdev) +{ + struct rbtn_data *rbtn_data; + struct acpi_device *device; + enum rbtn_type type; + int ret = 0; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + type = rbtn_check(device); + if (type == RBTN_UNKNOWN) { + dev_info(&pdev->dev, "Unknown device type\n"); + return -EINVAL; + } + + rbtn_data = devm_kzalloc(&pdev->dev, sizeof(*rbtn_data), GFP_KERNEL); + if (!rbtn_data) + return -ENOMEM; + + ret = rbtn_acquire(device, true); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot enable device\n"); + return ret; + } + + platform_set_drvdata(pdev, rbtn_data); + + rbtn_data->type = type; + + switch (rbtn_data->type) { + case RBTN_TOGGLE: + ret = rbtn_input_init(rbtn_data); + break; + case RBTN_SLIDER: + if (auto_remove_rfkill && rbtn_chain_head.head) + ret = 0; + else + ret = rbtn_rfkill_init(&pdev->dev); + break; + default: + ret = -EINVAL; + break; + } + if (ret) + goto err; + + ret = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + rbtn_notify, &pdev->dev); + if (ret) + goto err_cleanup; + + return 0; + +err_cleanup: + rbtn_cleanup(&pdev->dev); +err: + rbtn_acquire(device, false); + return ret; +} + +static void rbtn_remove(struct platform_device *pdev) +{ + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, rbtn_notify); + rbtn_cleanup(&pdev->dev); + rbtn_acquire(device, false); +} + +static void rbtn_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct rbtn_data *rbtn_data = dev_get_drvdata(dev); + + /* + * Some BIOSes send a notification at resume. + * Ignore it to prevent unwanted input events. + */ + if (rbtn_data->suspended) { + dev_dbg(dev, "ACPI notification ignored\n"); + return; + } + + if (event != 0x80) { + dev_info(dev, "Received unknown event (0x%x)\n", + event); + return; + } + + switch (rbtn_data->type) { + case RBTN_TOGGLE: + rbtn_input_event(rbtn_data); + break; + case RBTN_SLIDER: + rbtn_rfkill_event(dev); + atomic_notifier_call_chain(&rbtn_chain_head, event, NULL); + break; + default: + break; + } +} + +module_platform_driver(rbtn_driver); + +module_param(auto_remove_rfkill, bool, 0444); + +MODULE_PARM_DESC(auto_remove_rfkill, "Automatically remove rfkill devices when " + "other modules start receiving events " + "from this module and re-add them when " + "the last module stops receiving events " + "(default true)"); +MODULE_DEVICE_TABLE(acpi, rbtn_ids); +MODULE_DESCRIPTION("Dell Airplane Mode Switch driver"); +MODULE_AUTHOR("Pali Rohár "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-rbtn.h b/drivers/platform/x86/dell/dell-rbtn.h new file mode 100644 index 000000000..5e030f926 --- /dev/null +++ b/drivers/platform/x86/dell/dell-rbtn.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + Dell Airplane Mode Switch driver + Copyright (C) 2014-2015 Pali Rohár + +*/ + +#ifndef _DELL_RBTN_H_ +#define _DELL_RBTN_H_ + +struct notifier_block; + +int dell_rbtn_notifier_register(struct notifier_block *nb); +int dell_rbtn_notifier_unregister(struct notifier_block *nb); + +#endif diff --git a/drivers/platform/x86/dell/dell-smbios-base.c b/drivers/platform/x86/dell/dell-smbios-base.c new file mode 100644 index 000000000..4b7c54b76 --- /dev/null +++ b/drivers/platform/x86/dell/dell-smbios-base.c @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Common functions for kernel modules using Dell SMBIOS + * + * Copyright (c) Red Hat + * Copyright (c) 2014 Gabriele Mazzotta + * Copyright (c) 2014 Pali Rohár + * + * Based on documentation in the libsmbios package: + * Copyright (C) 2005-2014 Dell Inc. + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dell-smbios.h" + +static u32 da_supported_commands; +static int da_num_tokens; +static struct platform_device *platform_device; +static struct calling_interface_token *da_tokens; +static struct token_sysfs_data *token_entries; +static struct attribute **token_attrs; +static DEFINE_MUTEX(smbios_mutex); + +struct token_sysfs_data { + struct device_attribute location_attr; + struct device_attribute value_attr; + struct calling_interface_token *token; +}; + +struct smbios_device { + struct list_head list; + struct device *device; + int priority; + int (*call_fn)(struct calling_interface_buffer *arg); +}; + +struct smbios_call { + u32 need_capability; + int cmd_class; + int cmd_select; +}; + +/* calls that are whitelisted for given capabilities */ +static struct smbios_call call_whitelist[] = { + /* generally tokens are allowed, but may be further filtered or + * restricted by token blacklist or whitelist + */ + {CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_STD}, + {CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_AC}, + {CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_BAT}, + {CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_STD}, + {CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_AC}, + {CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_BAT}, + /* used by userspace: fwupdate */ + {CAP_SYS_ADMIN, CLASS_ADMIN_PROP, SELECT_ADMIN_PROP}, + /* used by userspace: fwupd */ + {CAP_SYS_ADMIN, CLASS_INFO, SELECT_DOCK}, + {CAP_SYS_ADMIN, CLASS_FLASH_INTERFACE, SELECT_FLASH_INTERFACE}, +}; + +/* calls that are explicitly blacklisted */ +static struct smbios_call call_blacklist[] = { + {0x0000, 1, 7}, /* manufacturing use */ + {0x0000, 6, 5}, /* manufacturing use */ + {0x0000, 11, 3}, /* write once */ + {0x0000, 11, 7}, /* write once */ + {0x0000, 11, 11}, /* write once */ + {0x0000, 19, -1}, /* diagnostics */ + /* handled by kernel: dell-laptop */ + {0x0000, CLASS_INFO, SELECT_RFKILL}, + {0x0000, CLASS_KBD_BACKLIGHT, SELECT_KBD_BACKLIGHT}, + {0x0000, CLASS_INFO, SELECT_THERMAL_MANAGEMENT}, +}; + +struct token_range { + u32 need_capability; + u16 min; + u16 max; +}; + +/* tokens that are whitelisted for given capabilities */ +static struct token_range token_whitelist[] = { + /* used by userspace: fwupdate */ + {CAP_SYS_ADMIN, CAPSULE_EN_TOKEN, CAPSULE_DIS_TOKEN}, + /* can indicate to userspace that WMI is needed */ + {0x0000, WSMT_EN_TOKEN, WSMT_DIS_TOKEN} +}; + +/* tokens that are explicitly blacklisted */ +static struct token_range token_blacklist[] = { + {0x0000, 0x0058, 0x0059}, /* ME use */ + {0x0000, 0x00CD, 0x00D0}, /* raid shadow copy */ + {0x0000, 0x013A, 0x01FF}, /* sata shadow copy */ + {0x0000, 0x0175, 0x0176}, /* write once */ + {0x0000, 0x0195, 0x0197}, /* diagnostics */ + {0x0000, 0x01DC, 0x01DD}, /* manufacturing use */ + {0x0000, 0x027D, 0x0284}, /* diagnostics */ + {0x0000, 0x02E3, 0x02E3}, /* manufacturing use */ + {0x0000, 0x02FF, 0x02FF}, /* manufacturing use */ + {0x0000, 0x0300, 0x0302}, /* manufacturing use */ + {0x0000, 0x0325, 0x0326}, /* manufacturing use */ + {0x0000, 0x0332, 0x0335}, /* fan control */ + {0x0000, 0x0350, 0x0350}, /* manufacturing use */ + {0x0000, 0x0363, 0x0363}, /* manufacturing use */ + {0x0000, 0x0368, 0x0368}, /* manufacturing use */ + {0x0000, 0x03F6, 0x03F7}, /* manufacturing use */ + {0x0000, 0x049E, 0x049F}, /* manufacturing use */ + {0x0000, 0x04A0, 0x04A3}, /* disagnostics */ + {0x0000, 0x04E6, 0x04E7}, /* manufacturing use */ + {0x0000, 0x4000, 0x7FFF}, /* internal BIOS use */ + {0x0000, 0x9000, 0x9001}, /* internal BIOS use */ + {0x0000, 0xA000, 0xBFFF}, /* write only */ + {0x0000, 0xEFF0, 0xEFFF}, /* internal BIOS use */ + /* handled by kernel: dell-laptop */ + {0x0000, BRIGHTNESS_TOKEN, BRIGHTNESS_TOKEN}, + {0x0000, KBD_LED_OFF_TOKEN, KBD_LED_AUTO_TOKEN}, + {0x0000, KBD_LED_AC_TOKEN, KBD_LED_AC_TOKEN}, + {0x0000, KBD_LED_AUTO_25_TOKEN, KBD_LED_AUTO_75_TOKEN}, + {0x0000, KBD_LED_AUTO_100_TOKEN, KBD_LED_AUTO_100_TOKEN}, + {0x0000, GLOBAL_MIC_MUTE_ENABLE, GLOBAL_MIC_MUTE_DISABLE}, +}; + +static LIST_HEAD(smbios_device_list); + +int dell_smbios_error(int value) +{ + switch (value) { + case 0: /* Completed successfully */ + return 0; + case -1: /* Completed with error */ + return -EIO; + case -2: /* Function not supported */ + return -ENXIO; + default: /* Unknown error */ + return -EINVAL; + } +} +EXPORT_SYMBOL_GPL(dell_smbios_error); + +int dell_smbios_register_device(struct device *d, int priority, void *call_fn) +{ + struct smbios_device *priv; + + priv = devm_kzalloc(d, sizeof(struct smbios_device), GFP_KERNEL); + if (!priv) + return -ENOMEM; + get_device(d); + priv->device = d; + priv->priority = priority; + priv->call_fn = call_fn; + mutex_lock(&smbios_mutex); + list_add_tail(&priv->list, &smbios_device_list); + mutex_unlock(&smbios_mutex); + dev_dbg(d, "Added device: %s\n", d->driver->name); + return 0; +} +EXPORT_SYMBOL_GPL(dell_smbios_register_device); + +void dell_smbios_unregister_device(struct device *d) +{ + struct smbios_device *priv; + + mutex_lock(&smbios_mutex); + list_for_each_entry(priv, &smbios_device_list, list) { + if (priv->device == d) { + list_del(&priv->list); + put_device(d); + break; + } + } + mutex_unlock(&smbios_mutex); + dev_dbg(d, "Remove device: %s\n", d->driver->name); +} +EXPORT_SYMBOL_GPL(dell_smbios_unregister_device); + +int dell_smbios_call_filter(struct device *d, + struct calling_interface_buffer *buffer) +{ + u16 t = 0; + int i; + + /* can't make calls over 30 */ + if (buffer->cmd_class > 30) { + dev_dbg(d, "class too big: %u\n", buffer->cmd_class); + return -EINVAL; + } + + /* supported calls on the particular system */ + if (!(da_supported_commands & (1 << buffer->cmd_class))) { + dev_dbg(d, "invalid command, supported commands: 0x%8x\n", + da_supported_commands); + return -EINVAL; + } + + /* match against call blacklist */ + for (i = 0; i < ARRAY_SIZE(call_blacklist); i++) { + if (buffer->cmd_class != call_blacklist[i].cmd_class) + continue; + if (buffer->cmd_select != call_blacklist[i].cmd_select && + call_blacklist[i].cmd_select != -1) + continue; + dev_dbg(d, "blacklisted command: %u/%u\n", + buffer->cmd_class, buffer->cmd_select); + return -EINVAL; + } + + /* if a token call, find token ID */ + + if ((buffer->cmd_class == CLASS_TOKEN_READ || + buffer->cmd_class == CLASS_TOKEN_WRITE) && + buffer->cmd_select < 3) { + /* tokens enabled ? */ + if (!da_tokens) { + dev_dbg(d, "no token support on this system\n"); + return -EINVAL; + } + + /* find the matching token ID */ + for (i = 0; i < da_num_tokens; i++) { + if (da_tokens[i].location != buffer->input[0]) + continue; + t = da_tokens[i].tokenID; + break; + } + + /* token call; but token didn't exist */ + if (!t) { + dev_dbg(d, "token at location %04x doesn't exist\n", + buffer->input[0]); + return -EINVAL; + } + + /* match against token blacklist */ + for (i = 0; i < ARRAY_SIZE(token_blacklist); i++) { + if (!token_blacklist[i].min || !token_blacklist[i].max) + continue; + if (t >= token_blacklist[i].min && + t <= token_blacklist[i].max) + return -EINVAL; + } + + /* match against token whitelist */ + for (i = 0; i < ARRAY_SIZE(token_whitelist); i++) { + if (!token_whitelist[i].min || !token_whitelist[i].max) + continue; + if (t < token_whitelist[i].min || + t > token_whitelist[i].max) + continue; + if (!token_whitelist[i].need_capability || + capable(token_whitelist[i].need_capability)) { + dev_dbg(d, "whitelisted token: %x\n", t); + return 0; + } + + } + } + /* match against call whitelist */ + for (i = 0; i < ARRAY_SIZE(call_whitelist); i++) { + if (buffer->cmd_class != call_whitelist[i].cmd_class) + continue; + if (buffer->cmd_select != call_whitelist[i].cmd_select) + continue; + if (!call_whitelist[i].need_capability || + capable(call_whitelist[i].need_capability)) { + dev_dbg(d, "whitelisted capable command: %u/%u\n", + buffer->cmd_class, buffer->cmd_select); + return 0; + } + dev_dbg(d, "missing capability %d for %u/%u\n", + call_whitelist[i].need_capability, + buffer->cmd_class, buffer->cmd_select); + + } + + /* not in a whitelist, only allow processes with capabilities */ + if (capable(CAP_SYS_RAWIO)) { + dev_dbg(d, "Allowing %u/%u due to CAP_SYS_RAWIO\n", + buffer->cmd_class, buffer->cmd_select); + return 0; + } + + return -EACCES; +} +EXPORT_SYMBOL_GPL(dell_smbios_call_filter); + +int dell_smbios_call(struct calling_interface_buffer *buffer) +{ + struct smbios_device *selected = NULL; + struct smbios_device *priv; + int ret; + + mutex_lock(&smbios_mutex); + list_for_each_entry(priv, &smbios_device_list, list) { + if (!selected || priv->priority >= selected->priority) { + dev_dbg(priv->device, "Trying device ID: %d\n", priv->priority); + selected = priv; + } + } + + if (!selected) { + ret = -ENODEV; + pr_err("No dell-smbios drivers are loaded\n"); + goto out_smbios_call; + } + + ret = selected->call_fn(buffer); + +out_smbios_call: + mutex_unlock(&smbios_mutex); + return ret; +} +EXPORT_SYMBOL_GPL(dell_smbios_call); + +void dell_fill_request(struct calling_interface_buffer *buffer, + u32 arg0, u32 arg1, u32 arg2, u32 arg3) +{ + memset(buffer, 0, sizeof(struct calling_interface_buffer)); + buffer->input[0] = arg0; + buffer->input[1] = arg1; + buffer->input[2] = arg2; + buffer->input[3] = arg3; +} +EXPORT_SYMBOL_GPL(dell_fill_request); + +int dell_send_request(struct calling_interface_buffer *buffer, + u16 class, u16 select) +{ + int ret; + + buffer->cmd_class = class; + buffer->cmd_select = select; + ret = dell_smbios_call(buffer); + if (ret != 0) + return ret; + return dell_smbios_error(buffer->output[0]); +} +EXPORT_SYMBOL_GPL(dell_send_request); + +struct calling_interface_token *dell_smbios_find_token(int tokenid) +{ + int i; + + if (!da_tokens) + return NULL; + + for (i = 0; i < da_num_tokens; i++) { + if (da_tokens[i].tokenID == tokenid) + return &da_tokens[i]; + } + + return NULL; +} +EXPORT_SYMBOL_GPL(dell_smbios_find_token); + +static BLOCKING_NOTIFIER_HEAD(dell_laptop_chain_head); + +int dell_laptop_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&dell_laptop_chain_head, nb); +} +EXPORT_SYMBOL_GPL(dell_laptop_register_notifier); + +int dell_laptop_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&dell_laptop_chain_head, nb); +} +EXPORT_SYMBOL_GPL(dell_laptop_unregister_notifier); + +void dell_laptop_call_notifier(unsigned long action, void *data) +{ + blocking_notifier_call_chain(&dell_laptop_chain_head, action, data); +} +EXPORT_SYMBOL_GPL(dell_laptop_call_notifier); + +bool dell_smbios_class_is_supported(u16 class) +{ + /* Classes over 30 always unsupported */ + if (class > 30) + return false; + return da_supported_commands & (1 << class); +} +EXPORT_SYMBOL_GPL(dell_smbios_class_is_supported); + +static void __init parse_da_table(const struct dmi_header *dm) +{ + /* Final token is a terminator, so we don't want to copy it */ + int tokens = (dm->length-11)/sizeof(struct calling_interface_token)-1; + struct calling_interface_token *new_da_tokens; + struct calling_interface_structure *table = + container_of(dm, struct calling_interface_structure, header); + + /* + * 4 bytes of table header, plus 7 bytes of Dell header + * plus at least 6 bytes of entry + */ + + if (dm->length < 17) + return; + + da_supported_commands = table->supportedCmds; + + new_da_tokens = krealloc(da_tokens, (da_num_tokens + tokens) * + sizeof(struct calling_interface_token), + GFP_KERNEL); + + if (!new_da_tokens) + return; + da_tokens = new_da_tokens; + + memcpy(da_tokens+da_num_tokens, table->tokens, + sizeof(struct calling_interface_token) * tokens); + + da_num_tokens += tokens; +} + +static void zero_duplicates(struct device *dev) +{ + int i, j; + + for (i = 0; i < da_num_tokens; i++) { + if (da_tokens[i].tokenID == 0) + continue; + for (j = i+1; j < da_num_tokens; j++) { + if (da_tokens[j].tokenID == 0) + continue; + if (da_tokens[i].tokenID == da_tokens[j].tokenID) { + dev_dbg(dev, "Zeroing dup token ID %x(%x/%x)\n", + da_tokens[j].tokenID, + da_tokens[j].location, + da_tokens[j].value); + da_tokens[j].tokenID = 0; + } + } + } +} + +static void __init find_tokens(const struct dmi_header *dm, void *dummy) +{ + switch (dm->type) { + case 0xd4: /* Indexed IO */ + case 0xd5: /* Protected Area Type 1 */ + case 0xd6: /* Protected Area Type 2 */ + break; + case 0xda: /* Calling interface */ + parse_da_table(dm); + break; + } +} + +static ssize_t location_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct token_sysfs_data *data = container_of(attr, struct token_sysfs_data, location_attr); + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + return sysfs_emit(buf, "%08x", data->token->location); +} + +static ssize_t value_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct token_sysfs_data *data = container_of(attr, struct token_sysfs_data, value_attr); + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + return sysfs_emit(buf, "%08x", data->token->value); +} + +static struct attribute_group smbios_attribute_group = { + .name = "tokens" +}; + +static struct platform_driver platform_driver = { + .driver = { + .name = "dell-smbios", + }, +}; + +static int build_tokens_sysfs(struct platform_device *dev) +{ + char *location_name; + char *value_name; + int ret; + int i, j; + + token_entries = kzalloc_objs(*token_entries, da_num_tokens); + if (!token_entries) + return -ENOMEM; + + /* need to store both location and value + terminator*/ + token_attrs = kzalloc_objs(*token_attrs, (2 * da_num_tokens) + 1); + if (!token_attrs) + goto out_allocate_attrs; + + for (i = 0, j = 0; i < da_num_tokens; i++) { + /* skip empty */ + if (da_tokens[i].tokenID == 0) + continue; + + token_entries[i].token = &da_tokens[i]; + + /* add location */ + location_name = kasprintf(GFP_KERNEL, "%04x_location", + da_tokens[i].tokenID); + if (location_name == NULL) + goto out_unwind_strings; + + sysfs_attr_init(&token_entries[i].location_attr.attr); + token_entries[i].location_attr.attr.name = location_name; + token_entries[i].location_attr.attr.mode = 0444; + token_entries[i].location_attr.show = location_show; + token_attrs[j++] = &token_entries[i].location_attr.attr; + + /* add value */ + value_name = kasprintf(GFP_KERNEL, "%04x_value", + da_tokens[i].tokenID); + if (!value_name) { + kfree(location_name); + goto out_unwind_strings; + } + + sysfs_attr_init(&token_entries[i].value_attr.attr); + token_entries[i].value_attr.attr.name = value_name; + token_entries[i].value_attr.attr.mode = 0444; + token_entries[i].value_attr.show = value_show; + token_attrs[j++] = &token_entries[i].value_attr.attr; + } + smbios_attribute_group.attrs = token_attrs; + + ret = sysfs_create_group(&dev->dev.kobj, &smbios_attribute_group); + if (ret) + goto out_unwind_strings; + return 0; + +out_unwind_strings: + while (i--) { + kfree(token_entries[i].location_attr.attr.name); + kfree(token_entries[i].value_attr.attr.name); + } + kfree(token_attrs); +out_allocate_attrs: + kfree(token_entries); + + return -ENOMEM; +} + +static void free_group(struct platform_device *pdev) +{ + int i; + + sysfs_remove_group(&pdev->dev.kobj, + &smbios_attribute_group); + for (i = 0; i < da_num_tokens; i++) { + kfree(token_entries[i].location_attr.attr.name); + kfree(token_entries[i].value_attr.attr.name); + } + kfree(token_attrs); + kfree(token_entries); +} + +static int __init dell_smbios_init(void) +{ + int ret, wmi, smm; + + if (!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Dell System", NULL) && + !dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Alienware", NULL) && + !dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "www.dell.com", NULL)) { + pr_err("Unable to run on non-Dell system\n"); + return -ENODEV; + } + + dmi_walk(find_tokens, NULL); + + ret = platform_driver_register(&platform_driver); + if (ret) + goto fail_platform_driver; + + platform_device = platform_device_alloc("dell-smbios", 0); + if (!platform_device) { + ret = -ENOMEM; + goto fail_platform_device_alloc; + } + ret = platform_device_add(platform_device); + if (ret) + goto fail_platform_device_add; + + /* register backends */ + wmi = init_dell_smbios_wmi(); + if (wmi) + pr_debug("Failed to initialize WMI backend: %d\n", wmi); + smm = init_dell_smbios_smm(); + if (smm) + pr_debug("Failed to initialize SMM backend: %d\n", smm); + if (wmi && smm) { + pr_err("No SMBIOS backends available (wmi: %d, smm: %d)\n", + wmi, smm); + ret = -ENODEV; + goto fail_create_group; + } + + if (da_tokens) { + /* duplicate tokens will cause problems building sysfs files */ + zero_duplicates(&platform_device->dev); + + ret = build_tokens_sysfs(platform_device); + if (ret) + goto fail_sysfs; + } + + return 0; + +fail_sysfs: + if (!wmi) + exit_dell_smbios_wmi(); + if (!smm) + exit_dell_smbios_smm(); + +fail_create_group: + platform_device_del(platform_device); + +fail_platform_device_add: + platform_device_put(platform_device); + +fail_platform_device_alloc: + platform_driver_unregister(&platform_driver); + +fail_platform_driver: + kfree(da_tokens); + return ret; +} + +static void __exit dell_smbios_exit(void) +{ + exit_dell_smbios_wmi(); + exit_dell_smbios_smm(); + mutex_lock(&smbios_mutex); + if (platform_device) { + if (da_tokens) + free_group(platform_device); + platform_device_unregister(platform_device); + platform_driver_unregister(&platform_driver); + } + kfree(da_tokens); + mutex_unlock(&smbios_mutex); +} + +module_init(dell_smbios_init); +module_exit(dell_smbios_exit); + +MODULE_AUTHOR("Matthew Garrett "); +MODULE_AUTHOR("Gabriele Mazzotta "); +MODULE_AUTHOR("Pali Rohár "); +MODULE_AUTHOR("Mario Limonciello "); +MODULE_DESCRIPTION("Common functions for kernel modules using Dell SMBIOS"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-smbios-smm.c b/drivers/platform/x86/dell/dell-smbios-smm.c new file mode 100644 index 000000000..7055e2c40 --- /dev/null +++ b/drivers/platform/x86/dell/dell-smbios-smm.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * SMI methods for use with dell-smbios + * + * Copyright (c) Red Hat + * Copyright (c) 2014 Gabriele Mazzotta + * Copyright (c) 2014 Pali Rohár + * Copyright (c) 2017 Dell Inc. + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include "dcdbas.h" +#include "dell-smbios.h" + +static int da_command_address; +static int da_command_code; +static struct smi_buffer smi_buf; +static struct calling_interface_buffer *buffer; +static struct platform_device *platform_device; +static DEFINE_MUTEX(smm_mutex); + +static void parse_da_table(const struct dmi_header *dm) +{ + struct calling_interface_structure *table = + container_of(dm, struct calling_interface_structure, header); + + /* 4 bytes of table header, plus 7 bytes of Dell header, plus at least + * 6 bytes of entry + */ + if (dm->length < 17) + return; + + da_command_address = table->cmdIOAddress; + da_command_code = table->cmdIOCode; +} + +static void find_cmd_address(const struct dmi_header *dm, void *dummy) +{ + switch (dm->type) { + case 0xda: /* Calling interface */ + parse_da_table(dm); + break; + } +} + +static int dell_smbios_smm_call(struct calling_interface_buffer *input) +{ + struct smi_cmd command; + size_t size; + + size = sizeof(struct calling_interface_buffer); + command.magic = SMI_CMD_MAGIC; + command.command_address = da_command_address; + command.command_code = da_command_code; + command.ebx = smi_buf.dma; + command.ecx = 0x42534931; + + mutex_lock(&smm_mutex); + memcpy(buffer, input, size); + dcdbas_smi_request(&command); + memcpy(input, buffer, size); + mutex_unlock(&smm_mutex); + return 0; +} + +/* When enabled this indicates that SMM won't work */ +static bool test_wsmt_enabled(void) +{ + struct calling_interface_token *wsmt; + + /* if token doesn't exist, SMM will work */ + wsmt = dell_smbios_find_token(WSMT_EN_TOKEN); + if (!wsmt) + return false; + + /* If token exists, try to access over SMM but set a dummy return. + * - If WSMT disabled it will be overwritten by SMM + * - If WSMT enabled then dummy value will remain + */ + buffer->cmd_class = CLASS_TOKEN_READ; + buffer->cmd_select = SELECT_TOKEN_STD; + memset(buffer, 0, sizeof(struct calling_interface_buffer)); + buffer->input[0] = wsmt->location; + buffer->output[0] = 99; + dell_smbios_smm_call(buffer); + if (buffer->output[0] == 99) + return true; + + return false; +} + +int init_dell_smbios_smm(void) +{ + int ret; + /* + * Allocate buffer below 4GB for SMI data--only 32-bit physical addr + * is passed to SMI handler. + */ + ret = dcdbas_smi_alloc(&smi_buf, PAGE_SIZE); + if (ret) + return ret; + buffer = (void *)smi_buf.virt; + + dmi_walk(find_cmd_address, NULL); + + if (test_wsmt_enabled()) { + pr_debug("Disabling due to WSMT enabled\n"); + ret = -ENODEV; + goto fail_wsmt; + } + + platform_device = platform_device_alloc("dell-smbios", 1); + if (!platform_device) { + ret = -ENOMEM; + goto fail_platform_device_alloc; + } + + ret = platform_device_add(platform_device); + if (ret) + goto fail_platform_device_add; + + ret = dell_smbios_register_device(&platform_device->dev, 0, &dell_smbios_smm_call); + if (ret) + goto fail_register; + + return 0; + +fail_register: + platform_device_del(platform_device); + +fail_platform_device_add: + platform_device_put(platform_device); + +fail_wsmt: +fail_platform_device_alloc: + dcdbas_smi_free(&smi_buf); + return ret; +} + +void exit_dell_smbios_smm(void) +{ + if (platform_device) { + dell_smbios_unregister_device(&platform_device->dev); + platform_device_unregister(platform_device); + dcdbas_smi_free(&smi_buf); + } +} diff --git a/drivers/platform/x86/dell/dell-smbios-wmi.c b/drivers/platform/x86/dell/dell-smbios-wmi.c new file mode 100644 index 000000000..64d0871b7 --- /dev/null +++ b/drivers/platform/x86/dell/dell-smbios-wmi.c @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * WMI methods for use with dell-smbios + * + * Copyright (c) 2017 Dell Inc. + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dell-smbios.h" +#include "dell-wmi-descriptor.h" + +static DEFINE_MUTEX(call_mutex); +static DEFINE_MUTEX(list_mutex); +static int wmi_supported; + +struct misc_bios_flags_structure { + struct dmi_header header; + u16 flags0; +} __packed; +#define FLAG_HAS_ACPI_WMI 0x02 + +#define DELL_WMI_SMBIOS_GUID "A80593CE-A997-11DA-B012-B622A1EF5492" + +struct wmi_smbios_priv { + struct dell_wmi_smbios_buffer *buf; + struct list_head list; + struct wmi_device *wdev; + struct device *child; + u64 req_buf_size; + struct miscdevice char_dev; +}; +static LIST_HEAD(wmi_list); + +static inline struct wmi_smbios_priv *get_first_smbios_priv(void) +{ + return list_first_entry_or_null(&wmi_list, + struct wmi_smbios_priv, + list); +} + +static int run_smbios_call(struct wmi_device *wdev) +{ + struct wmi_smbios_priv *priv = dev_get_drvdata(&wdev->dev); + const struct wmi_buffer input = { + .length = priv->req_buf_size - sizeof(u64), + .data = &priv->buf->std, + }; + struct wmi_buffer output; + int ret; + + dev_dbg(&wdev->dev, "evaluating: %u/%u [%x,%x,%x,%x]\n", + priv->buf->std.cmd_class, priv->buf->std.cmd_select, + priv->buf->std.input[0], priv->buf->std.input[1], + priv->buf->std.input[2], priv->buf->std.input[3]); + + /* + * The output buffer returned by the WMI method should have at least the size + * of the input buffer. + */ + ret = wmidev_invoke_method(wdev, 0, 1, &input, &output, input.length); + if (ret < 0) + return ret; + + memcpy(input.data, output.data, input.length); + kfree(output.data); + dev_dbg(&wdev->dev, "result: [%08x,%08x,%08x,%08x]\n", + priv->buf->std.output[0], priv->buf->std.output[1], + priv->buf->std.output[2], priv->buf->std.output[3]); + + return 0; +} + +static int dell_smbios_wmi_call(struct calling_interface_buffer *buffer) +{ + struct wmi_smbios_priv *priv; + size_t difference; + size_t size; + int ret; + + mutex_lock(&call_mutex); + priv = get_first_smbios_priv(); + if (!priv) { + ret = -ENODEV; + goto out_wmi_call; + } + + size = sizeof(struct calling_interface_buffer); + difference = priv->req_buf_size - sizeof(u64) - size; + + memset(&priv->buf->ext, 0, difference); + memcpy(&priv->buf->std, buffer, size); + ret = run_smbios_call(priv->wdev); + memcpy(buffer, &priv->buf->std, size); +out_wmi_call: + mutex_unlock(&call_mutex); + + return ret; +} + +static int dell_smbios_wmi_open(struct inode *inode, struct file *filp) +{ + struct wmi_smbios_priv *priv; + + priv = container_of(filp->private_data, struct wmi_smbios_priv, char_dev); + filp->private_data = priv; + + return nonseekable_open(inode, filp); +} + +static ssize_t dell_smbios_wmi_read(struct file *filp, char __user *buffer, size_t length, + loff_t *offset) +{ + struct wmi_smbios_priv *priv = filp->private_data; + + return simple_read_from_buffer(buffer, length, offset, &priv->req_buf_size, + sizeof(priv->req_buf_size)); +} + +static long dell_smbios_wmi_do_ioctl(struct wmi_smbios_priv *priv, + struct dell_wmi_smbios_buffer __user *arg) +{ + long ret; + + if (get_user(priv->buf->length, &arg->length)) + return -EFAULT; + + if (priv->buf->length < priv->req_buf_size) + return -EINVAL; + + /* if it's too big, warn, driver will only use what is needed */ + if (priv->buf->length > priv->req_buf_size) + dev_err(&priv->wdev->dev, "Buffer %llu is bigger than required %llu\n", + priv->buf->length, priv->req_buf_size); + + if (copy_from_user(priv->buf, arg, priv->req_buf_size)) + return -EFAULT; + + if (dell_smbios_call_filter(&priv->wdev->dev, &priv->buf->std)) { + dev_err(&priv->wdev->dev, "Invalid call %d/%d:%8x\n", + priv->buf->std.cmd_class, + priv->buf->std.cmd_select, + priv->buf->std.input[0]); + + return -EINVAL; + } + + ret = run_smbios_call(priv->wdev); + if (ret) + return ret; + + if (copy_to_user(arg, priv->buf, priv->req_buf_size)) + return -EFAULT; + + return 0; +} + +static long dell_smbios_wmi_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) +{ + struct dell_wmi_smbios_buffer __user *input = (struct dell_wmi_smbios_buffer __user *)arg; + struct wmi_smbios_priv *priv = filp->private_data; + long ret; + + if (cmd != DELL_WMI_SMBIOS_CMD) + return -ENOIOCTLCMD; + + mutex_lock(&call_mutex); + ret = dell_smbios_wmi_do_ioctl(priv, input); + mutex_unlock(&call_mutex); + + return ret; +} + +static const struct file_operations dell_smbios_wmi_fops = { + .owner = THIS_MODULE, + .open = dell_smbios_wmi_open, + .read = dell_smbios_wmi_read, + .unlocked_ioctl = dell_smbios_wmi_ioctl, + .compat_ioctl = compat_ptr_ioctl, +}; + +static void dell_smbios_wmi_unregister_chardev(void *data) +{ + struct miscdevice *char_dev = data; + + misc_deregister(char_dev); +} + +static int dell_smbios_wmi_register_chardev(struct wmi_smbios_priv *priv) +{ + int ret; + + priv->char_dev.minor = MISC_DYNAMIC_MINOR; + priv->char_dev.name = "wmi/dell-smbios"; + priv->char_dev.fops = &dell_smbios_wmi_fops; + priv->char_dev.mode = 0444; + + ret = misc_register(&priv->char_dev); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(&priv->wdev->dev, dell_smbios_wmi_unregister_chardev, + &priv->char_dev); +} + +static int dell_smbios_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct wmi_smbios_priv *priv; + u32 buffer_size, hotfix; + int count; + int ret; + + ret = dell_wmi_get_descriptor_valid(); + if (ret) + return ret; + + priv = devm_kzalloc(&wdev->dev, sizeof(struct wmi_smbios_priv), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + /* WMI buffer size will be either 4k or 32k depending on machine */ + if (!dell_wmi_get_size(&buffer_size)) + return -EPROBE_DEFER; + + priv->req_buf_size = buffer_size; + + /* some SMBIOS calls fail unless BIOS contains hotfix */ + if (!dell_wmi_get_hotfix(&hotfix)) + return -EPROBE_DEFER; + + if (!hotfix) + dev_warn(&wdev->dev, + "WMI SMBIOS userspace interface not supported(%u), try upgrading to a newer BIOS\n", + hotfix); + + /* add in the length object we will use internally with ioctl */ + priv->req_buf_size += sizeof(u64); + + count = get_order(priv->req_buf_size); + priv->buf = (void *)devm_get_free_pages(&wdev->dev, GFP_KERNEL, count); + if (!priv->buf) + return -ENOMEM; + + ret = dell_smbios_wmi_register_chardev(priv); + if (ret) + return ret; + + ret = dell_smbios_register_device(&wdev->dev, 1, &dell_smbios_wmi_call); + if (ret) + return ret; + + mutex_lock(&list_mutex); + list_add_tail(&priv->list, &wmi_list); + mutex_unlock(&list_mutex); + + return 0; +} + +static void dell_smbios_wmi_remove(struct wmi_device *wdev) +{ + struct wmi_smbios_priv *priv = dev_get_drvdata(&wdev->dev); + + mutex_lock(&call_mutex); + mutex_lock(&list_mutex); + list_del(&priv->list); + mutex_unlock(&list_mutex); + dell_smbios_unregister_device(&wdev->dev); + mutex_unlock(&call_mutex); +} + +static const struct wmi_device_id dell_smbios_wmi_id_table[] = { + { .guid_string = DELL_WMI_SMBIOS_GUID }, + { }, +}; + +static void parse_b1_table(const struct dmi_header *dm) +{ + struct misc_bios_flags_structure *flags = + container_of(dm, struct misc_bios_flags_structure, header); + + /* 4 bytes header, 8 bytes flags */ + if (dm->length < 12) + return; + if (dm->handle != 0xb100) + return; + if ((flags->flags0 & FLAG_HAS_ACPI_WMI)) + wmi_supported = 1; +} + +static void find_b1(const struct dmi_header *dm, void *dummy) +{ + switch (dm->type) { + case 0xb1: /* misc bios flags */ + parse_b1_table(dm); + break; + } +} + +static struct wmi_driver dell_smbios_wmi_driver = { + .driver = { + .name = "dell-smbios", + }, + .probe = dell_smbios_wmi_probe, + .remove = dell_smbios_wmi_remove, + .id_table = dell_smbios_wmi_id_table, +}; + +int init_dell_smbios_wmi(void) +{ + dmi_walk(find_b1, NULL); + + if (!wmi_supported) + return -ENODEV; + + return wmi_driver_register(&dell_smbios_wmi_driver); +} + +void exit_dell_smbios_wmi(void) +{ + if (wmi_supported) + wmi_driver_unregister(&dell_smbios_wmi_driver); +} + +MODULE_DEVICE_TABLE(wmi, dell_smbios_wmi_id_table); diff --git a/drivers/platform/x86/dell/dell-smbios.h b/drivers/platform/x86/dell/dell-smbios.h new file mode 100644 index 000000000..f421b8533 --- /dev/null +++ b/drivers/platform/x86/dell/dell-smbios.h @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Common functions for kernel modules using Dell SMBIOS + * + * Copyright (c) Red Hat + * Copyright (c) 2014 Gabriele Mazzotta + * Copyright (c) 2014 Pali Rohár + * + * Based on documentation in the libsmbios package: + * Copyright (C) 2005-2014 Dell Inc. + */ + +#ifndef _DELL_SMBIOS_H_ +#define _DELL_SMBIOS_H_ + +#include +#include + +/* Classes and selects used only in kernel drivers */ +#define CLASS_KBD_BACKLIGHT 4 +#define SELECT_KBD_BACKLIGHT 11 +#define SELECT_THERMAL_MANAGEMENT 19 + +/* Tokens used in kernel drivers, any of these + * should be filtered from userspace access + */ +#define BRIGHTNESS_TOKEN 0x007d +#define KBD_LED_AC_TOKEN 0x0451 +#define KBD_LED_OFF_TOKEN 0x01E1 +#define KBD_LED_ON_TOKEN 0x01E2 +#define KBD_LED_AUTO_TOKEN 0x01E3 +#define KBD_LED_AUTO_25_TOKEN 0x02EA +#define KBD_LED_AUTO_50_TOKEN 0x02EB +#define KBD_LED_AUTO_75_TOKEN 0x02EC +#define KBD_LED_AUTO_100_TOKEN 0x02F6 +#define BAT_PRI_AC_MODE_TOKEN 0x0341 +#define BAT_ADAPTIVE_MODE_TOKEN 0x0342 +#define BAT_CUSTOM_MODE_TOKEN 0x0343 +#define BAT_STANDARD_MODE_TOKEN 0x0346 +#define BAT_EXPRESS_MODE_TOKEN 0x0347 +#define BAT_CUSTOM_CHARGE_START 0x0349 +#define BAT_CUSTOM_CHARGE_END 0x034A +#define GLOBAL_MIC_MUTE_ENABLE 0x0364 +#define GLOBAL_MIC_MUTE_DISABLE 0x0365 +#define GLOBAL_MUTE_ENABLE 0x058C +#define GLOBAL_MUTE_DISABLE 0x058D + +struct notifier_block; + +struct calling_interface_token { + u16 tokenID; + u16 location; + union { + u16 value; + u16 stringlength; + }; +}; + +struct calling_interface_structure { + struct dmi_header header; + u16 cmdIOAddress; + u8 cmdIOCode; + u32 supportedCmds; + struct calling_interface_token tokens[]; +} __packed; + +int dell_smbios_register_device(struct device *d, int priority, void *call_fn); +void dell_smbios_unregister_device(struct device *d); + +int dell_smbios_error(int value); +int dell_smbios_call_filter(struct device *d, + struct calling_interface_buffer *buffer); +int dell_smbios_call(struct calling_interface_buffer *buffer); + +void dell_fill_request(struct calling_interface_buffer *buffer, + u32 arg0, u32 arg1, u32 arg2, u32 arg3); +int dell_send_request(struct calling_interface_buffer *buffer, + u16 class, u16 select); + +struct calling_interface_token *dell_smbios_find_token(int tokenid); + +enum dell_laptop_notifier_actions { + DELL_LAPTOP_KBD_BACKLIGHT_BRIGHTNESS_CHANGED, +}; + +int dell_laptop_register_notifier(struct notifier_block *nb); +int dell_laptop_unregister_notifier(struct notifier_block *nb); +void dell_laptop_call_notifier(unsigned long action, void *data); +bool dell_smbios_class_is_supported(u16 class); + +/* for the supported backends */ +#ifdef CONFIG_DELL_SMBIOS_WMI +int init_dell_smbios_wmi(void); +void exit_dell_smbios_wmi(void); +#else /* CONFIG_DELL_SMBIOS_WMI */ +static inline int init_dell_smbios_wmi(void) +{ + return -ENODEV; +} +static inline void exit_dell_smbios_wmi(void) +{} +#endif /* CONFIG_DELL_SMBIOS_WMI */ + +#ifdef CONFIG_DELL_SMBIOS_SMM +int init_dell_smbios_smm(void); +void exit_dell_smbios_smm(void); +#else /* CONFIG_DELL_SMBIOS_SMM */ +static inline int init_dell_smbios_smm(void) +{ + return -ENODEV; +} +static inline void exit_dell_smbios_smm(void) +{} +#endif /* CONFIG_DELL_SMBIOS_SMM */ + +#endif /* _DELL_SMBIOS_H_ */ diff --git a/drivers/platform/x86/dell/dell-smo8800-ids.h b/drivers/platform/x86/dell/dell-smo8800-ids.h new file mode 100644 index 000000000..ec58e229b --- /dev/null +++ b/drivers/platform/x86/dell/dell-smo8800-ids.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * ACPI SMO88XX lis3lv02d freefall / accelerometer device-ids. + * + * Copyright (C) 2012 Sonal Santan + * Copyright (C) 2014 Pali Rohár + */ +#ifndef _DELL_SMO8800_IDS_H_ +#define _DELL_SMO8800_IDS_H_ + +#include +#include + +static const struct acpi_device_id smo8800_ids[] = { + { "SMO8800" }, + { "SMO8801" }, + { "SMO8810" }, + { "SMO8811" }, + { "SMO8820" }, + { "SMO8821" }, + { "SMO8830" }, + { "SMO8831" }, + { } +}; +MODULE_DEVICE_TABLE(acpi, smo8800_ids); + +#endif diff --git a/drivers/platform/x86/dell/dell-smo8800.c b/drivers/platform/x86/dell/dell-smo8800.c new file mode 100644 index 000000000..8872f9b57 --- /dev/null +++ b/drivers/platform/x86/dell/dell-smo8800.c @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * dell-smo8800.c - Dell Latitude ACPI SMO88XX freefall sensor driver + * + * Copyright (C) 2012 Sonal Santan + * Copyright (C) 2014 Pali Rohár + * + * This is loosely based on lis3lv02d driver. + */ + +#define DRIVER_NAME "smo8800" + +#include +#include +#include +#include +#include +#include +#include +#include "dell-smo8800-ids.h" + +struct smo8800_device { + u32 irq; /* acpi device irq */ + atomic_t counter; /* count after last read */ + struct miscdevice miscdev; /* for /dev/freefall */ + unsigned long misc_opened; /* whether the device is open */ + wait_queue_head_t misc_wait; /* Wait queue for the misc dev */ + struct device *dev; /* acpi device */ +}; + +static irqreturn_t smo8800_interrupt_quick(int irq, void *data) +{ + struct smo8800_device *smo8800 = data; + + atomic_inc(&smo8800->counter); + wake_up_interruptible(&smo8800->misc_wait); + return IRQ_WAKE_THREAD; +} + +static irqreturn_t smo8800_interrupt_thread(int irq, void *data) +{ + struct smo8800_device *smo8800 = data; + + dev_info(smo8800->dev, "detected free fall\n"); + return IRQ_HANDLED; +} + +static ssize_t smo8800_misc_read(struct file *file, char __user *buf, + size_t count, loff_t *pos) +{ + struct smo8800_device *smo8800 = container_of(file->private_data, + struct smo8800_device, miscdev); + + u32 data = 0; + unsigned char byte_data; + ssize_t retval = 1; + + if (count < 1) + return -EINVAL; + + atomic_set(&smo8800->counter, 0); + retval = wait_event_interruptible(smo8800->misc_wait, + (data = atomic_xchg(&smo8800->counter, 0))); + + if (retval) + return retval; + + retval = 1; + + byte_data = min_t(u32, data, 255); + + if (put_user(byte_data, buf)) + retval = -EFAULT; + + return retval; +} + +static int smo8800_misc_open(struct inode *inode, struct file *file) +{ + struct smo8800_device *smo8800 = container_of(file->private_data, + struct smo8800_device, miscdev); + + if (test_and_set_bit(0, &smo8800->misc_opened)) + return -EBUSY; /* already open */ + + atomic_set(&smo8800->counter, 0); + return 0; +} + +static int smo8800_misc_release(struct inode *inode, struct file *file) +{ + struct smo8800_device *smo8800 = container_of(file->private_data, + struct smo8800_device, miscdev); + + clear_bit(0, &smo8800->misc_opened); /* release the device */ + return 0; +} + +static const struct file_operations smo8800_misc_fops = { + .owner = THIS_MODULE, + .read = smo8800_misc_read, + .open = smo8800_misc_open, + .release = smo8800_misc_release, +}; + +static int smo8800_probe(struct platform_device *device) +{ + int err; + struct smo8800_device *smo8800; + + smo8800 = devm_kzalloc(&device->dev, sizeof(*smo8800), GFP_KERNEL); + if (!smo8800) { + dev_err(&device->dev, "failed to allocate device data\n"); + return -ENOMEM; + } + + smo8800->dev = &device->dev; + smo8800->miscdev.minor = MISC_DYNAMIC_MINOR; + smo8800->miscdev.name = "freefall"; + smo8800->miscdev.fops = &smo8800_misc_fops; + + init_waitqueue_head(&smo8800->misc_wait); + + err = misc_register(&smo8800->miscdev); + if (err) { + dev_err(&device->dev, "failed to register misc dev: %d\n", err); + return err; + } + + platform_set_drvdata(device, smo8800); + + err = platform_get_irq(device, 0); + if (err < 0) + goto error; + smo8800->irq = err; + + err = request_threaded_irq(smo8800->irq, smo8800_interrupt_quick, + smo8800_interrupt_thread, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + DRIVER_NAME, smo8800); + if (err) { + dev_err(&device->dev, + "failed to request thread for IRQ %d: %d\n", + smo8800->irq, err); + goto error; + } + + dev_dbg(&device->dev, "device /dev/freefall registered with IRQ %d\n", + smo8800->irq); + return 0; + +error: + misc_deregister(&smo8800->miscdev); + return err; +} + +static void smo8800_remove(struct platform_device *device) +{ + struct smo8800_device *smo8800 = platform_get_drvdata(device); + + free_irq(smo8800->irq, smo8800); + misc_deregister(&smo8800->miscdev); + dev_dbg(&device->dev, "device /dev/freefall unregistered\n"); +} + +static struct platform_driver smo8800_driver = { + .probe = smo8800_probe, + .remove = smo8800_remove, + .driver = { + .name = DRIVER_NAME, + .acpi_match_table = smo8800_ids, + }, +}; +module_platform_driver(smo8800_driver); + +MODULE_DESCRIPTION("Dell Latitude freefall driver (ACPI SMO88XX)"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Sonal Santan, Pali Rohár"); diff --git a/drivers/platform/x86/dell/dell-uart-backlight.c b/drivers/platform/x86/dell/dell-uart-backlight.c new file mode 100644 index 000000000..f323a667d --- /dev/null +++ b/drivers/platform/x86/dell/dell-uart-backlight.c @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Dell AIO Serial Backlight Driver + * + * Copyright (C) 2024 Hans de Goede + * Copyright (C) 2017 AceLan Kao + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../serdev_helpers.h" + +/* The backlight controller must respond within 1 second */ +#define DELL_BL_TIMEOUT msecs_to_jiffies(1000) +#define DELL_BL_MAX_BRIGHTNESS 100 + +/* Defines for the commands send to the controller */ + +/* 1st byte Start Of Frame 3 MSB bits: cmd-len + 01010 SOF marker */ +#define DELL_SOF(len) (((len) << 5) | 0x0a) +#define GET_CMD_LEN 3 +#define SET_CMD_LEN 4 + +/* 2nd byte command */ +#define CMD_GET_VERSION 0x06 +#define CMD_SET_BRIGHTNESS 0x0b +#define CMD_GET_BRIGHTNESS 0x0c +#define CMD_SET_BL_POWER 0x0e + +/* Indexes and other defines for response received from the controller */ +#define RESP_LEN 0 +#define RESP_CMD 1 /* Echo of CMD byte from command */ +#define RESP_DATA 2 /* Start of received data */ + +#define SET_RESP_LEN 3 +#define GET_RESP_LEN 4 +#define MIN_RESP_LEN 3 +#define MAX_RESP_LEN 80 + +struct dell_uart_backlight { + struct mutex mutex; + wait_queue_head_t wait_queue; + struct device *dev; + struct backlight_device *bl; + u8 *resp; + u8 resp_idx; + u8 resp_len; + u8 resp_max_len; + u8 pending_cmd; + int status; + int power; +}; + +/* Checksum: SUM(Length and Cmd and Data) xor 0xFF */ +static u8 dell_uart_checksum(u8 *buf, int len) +{ + u8 val = 0; + + while (len-- > 0) + val += buf[len]; + + return val ^ 0xff; +} + +static int dell_uart_bl_command(struct dell_uart_backlight *dell_bl, + const u8 *cmd, int cmd_len, + u8 *resp, int resp_max_len) +{ + int ret; + + ret = mutex_lock_killable(&dell_bl->mutex); + if (ret) + return ret; + + dell_bl->status = -EBUSY; + dell_bl->resp = resp; + dell_bl->resp_idx = 0; + dell_bl->resp_len = -1; /* Invalid / unset */ + dell_bl->resp_max_len = resp_max_len; + dell_bl->pending_cmd = cmd[1]; + + /* The TTY buffer should be big enough to take the entire cmd in one go */ + ret = serdev_device_write_buf(to_serdev_device(dell_bl->dev), cmd, cmd_len); + if (ret != cmd_len) { + dev_err(dell_bl->dev, "Error writing command: %d\n", ret); + dell_bl->status = (ret < 0) ? ret : -EIO; + goto out; + } + + ret = wait_event_timeout(dell_bl->wait_queue, dell_bl->status != -EBUSY, + DELL_BL_TIMEOUT); + if (ret == 0) { + dev_err(dell_bl->dev, "Timed out waiting for response.\n"); + /* Clear busy status to discard bytes received after this */ + dell_bl->status = -ETIMEDOUT; + } + +out: + mutex_unlock(&dell_bl->mutex); + return dell_bl->status; +} + +static int dell_uart_set_brightness(struct dell_uart_backlight *dell_bl, int brightness) +{ + u8 set_brightness[SET_CMD_LEN], resp[SET_RESP_LEN]; + + set_brightness[0] = DELL_SOF(SET_CMD_LEN); + set_brightness[1] = CMD_SET_BRIGHTNESS; + set_brightness[2] = brightness; + set_brightness[3] = dell_uart_checksum(set_brightness, 3); + + return dell_uart_bl_command(dell_bl, set_brightness, SET_CMD_LEN, resp, SET_RESP_LEN); +} + +static int dell_uart_get_brightness(struct dell_uart_backlight *dell_bl) +{ + struct device *dev = dell_bl->dev; + u8 get_brightness[GET_CMD_LEN], resp[GET_RESP_LEN]; + int ret; + + get_brightness[0] = DELL_SOF(GET_CMD_LEN); + get_brightness[1] = CMD_GET_BRIGHTNESS; + get_brightness[2] = dell_uart_checksum(get_brightness, 2); + + ret = dell_uart_bl_command(dell_bl, get_brightness, GET_CMD_LEN, resp, GET_RESP_LEN); + if (ret) + return ret; + + if (resp[RESP_LEN] != GET_RESP_LEN) { + dev_err(dev, "Unexpected get brightness response length: %d\n", resp[RESP_LEN]); + return -EIO; + } + + if (resp[RESP_DATA] > DELL_BL_MAX_BRIGHTNESS) { + dev_err(dev, "Unexpected get brightness response: %d\n", resp[RESP_DATA]); + return -EIO; + } + + return resp[RESP_DATA]; +} + +static int dell_uart_set_bl_power(struct dell_uart_backlight *dell_bl, int power) +{ + u8 set_power[SET_CMD_LEN], resp[SET_RESP_LEN]; + int ret; + + set_power[0] = DELL_SOF(SET_CMD_LEN); + set_power[1] = CMD_SET_BL_POWER; + set_power[2] = (power == BACKLIGHT_POWER_ON) ? 1 : 0; + set_power[3] = dell_uart_checksum(set_power, 3); + + ret = dell_uart_bl_command(dell_bl, set_power, SET_CMD_LEN, resp, SET_RESP_LEN); + if (ret) + return ret; + + dell_bl->power = power; + return 0; +} + +/* + * There is no command to get backlight power status, + * so we set the backlight power to "on" while initializing, + * and then track and report its status by power variable. + */ +static int dell_uart_get_bl_power(struct dell_uart_backlight *dell_bl) +{ + return dell_bl->power; +} + +static int dell_uart_update_status(struct backlight_device *bd) +{ + struct dell_uart_backlight *dell_bl = bl_get_data(bd); + int ret; + + ret = dell_uart_set_brightness(dell_bl, bd->props.brightness); + if (ret) + return ret; + + if (bd->props.power != dell_uart_get_bl_power(dell_bl)) + return dell_uart_set_bl_power(dell_bl, bd->props.power); + + return 0; +} + +static int dell_uart_get_brightness_op(struct backlight_device *bd) +{ + return dell_uart_get_brightness(bl_get_data(bd)); +} + +static const struct backlight_ops dell_uart_backlight_ops = { + .update_status = dell_uart_update_status, + .get_brightness = dell_uart_get_brightness_op, +}; + +static size_t dell_uart_bl_receive(struct serdev_device *serdev, const u8 *data, size_t len) +{ + struct dell_uart_backlight *dell_bl = serdev_device_get_drvdata(serdev); + size_t i; + u8 csum; + + dev_dbg(dell_bl->dev, "Recv: %*ph\n", (int)len, data); + + /* Throw away unexpected bytes / remainder of response after an error */ + if (dell_bl->status != -EBUSY) { + dev_warn(dell_bl->dev, "Bytes received out of band, dropping them.\n"); + return len; + } + + i = 0; + while (i < len && dell_bl->resp_idx != dell_bl->resp_len) { + dell_bl->resp[dell_bl->resp_idx] = data[i++]; + + switch (dell_bl->resp_idx) { + case RESP_LEN: /* Length byte */ + dell_bl->resp_len = dell_bl->resp[RESP_LEN]; + if (dell_bl->resp_len < MIN_RESP_LEN || + dell_bl->resp_len > dell_bl->resp_max_len) { + dev_err(dell_bl->dev, "Response length %d out if range %d - %d\n", + dell_bl->resp_len, MIN_RESP_LEN, dell_bl->resp_max_len); + dell_bl->status = -EIO; + goto wakeup; + } + break; + case RESP_CMD: /* CMD byte */ + if (dell_bl->resp[RESP_CMD] != dell_bl->pending_cmd) { + dev_err(dell_bl->dev, "Response cmd 0x%02x != pending 0x%02x\n", + dell_bl->resp[RESP_CMD], dell_bl->pending_cmd); + dell_bl->status = -EIO; + goto wakeup; + } + break; + } + dell_bl->resp_idx++; + } + + if (dell_bl->resp_idx != dell_bl->resp_len) + return len; /* Response not complete yet */ + + csum = dell_uart_checksum(dell_bl->resp, dell_bl->resp_len - 1); + if (dell_bl->resp[dell_bl->resp_len - 1] == csum) { + dell_bl->status = 0; /* Success */ + } else { + dev_err(dell_bl->dev, "Checksum mismatch got 0x%02x expected 0x%02x\n", + dell_bl->resp[dell_bl->resp_len - 1], csum); + dell_bl->status = -EIO; + } +wakeup: + wake_up(&dell_bl->wait_queue); + return i; +} + +static const struct serdev_device_ops dell_uart_bl_serdev_ops = { + .receive_buf = dell_uart_bl_receive, + .write_wakeup = serdev_device_write_wakeup, +}; + +static int dell_uart_bl_serdev_probe(struct serdev_device *serdev) +{ + u8 get_version[GET_CMD_LEN], resp[MAX_RESP_LEN]; + struct backlight_properties props = {}; + struct dell_uart_backlight *dell_bl; + struct device *dev = &serdev->dev; + int ret; + + dell_bl = devm_kzalloc(dev, sizeof(*dell_bl), GFP_KERNEL); + if (!dell_bl) + return -ENOMEM; + + mutex_init(&dell_bl->mutex); + init_waitqueue_head(&dell_bl->wait_queue); + dell_bl->dev = dev; + + serdev_device_set_drvdata(serdev, dell_bl); + serdev_device_set_client_ops(serdev, &dell_uart_bl_serdev_ops); + + ret = devm_serdev_device_open(dev, serdev); + if (ret) + return dev_err_probe(dev, ret, "opening UART device\n"); + + /* 9600 bps, no flow control, these are the default but set them to be sure */ + serdev_device_set_baudrate(serdev, 9600); + serdev_device_set_flow_control(serdev, false); + + get_version[0] = DELL_SOF(GET_CMD_LEN); + get_version[1] = CMD_GET_VERSION; + get_version[2] = dell_uart_checksum(get_version, 2); + + ret = dell_uart_bl_command(dell_bl, get_version, GET_CMD_LEN, resp, MAX_RESP_LEN); + if (ret) + return dev_err_probe(dev, ret, "getting firmware version\n"); + + dev_dbg(dev, "Firmware version: %.*s\n", resp[RESP_LEN] - 3, resp + RESP_DATA); + + /* Initialize bl_power to a known value */ + ret = dell_uart_set_bl_power(dell_bl, BACKLIGHT_POWER_ON); + if (ret) + return ret; + + ret = dell_uart_get_brightness(dell_bl); + if (ret < 0) + return ret; + + props.type = BACKLIGHT_PLATFORM; + props.brightness = ret; + props.max_brightness = DELL_BL_MAX_BRIGHTNESS; + props.power = dell_bl->power; + + dell_bl->bl = devm_backlight_device_register(dev, "dell_uart_backlight", + dev, dell_bl, + &dell_uart_backlight_ops, + &props); + return PTR_ERR_OR_ZERO(dell_bl->bl); +} + +static struct serdev_device_driver dell_uart_bl_serdev_driver = { + .probe = dell_uart_bl_serdev_probe, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +static int dell_uart_bl_pdev_probe(struct platform_device *pdev) +{ + enum acpi_backlight_type bl_type; + struct serdev_device *serdev; + struct device *ctrl_dev; + int ret; + + bl_type = acpi_video_get_backlight_type(); + if (bl_type != acpi_backlight_dell_uart) { + dev_dbg(&pdev->dev, "Not loading (ACPI backlight type = %d)\n", bl_type); + return -ENODEV; + } + + ctrl_dev = get_serdev_controller("DELL0501", NULL, 0, "serial0"); + if (IS_ERR(ctrl_dev)) + return PTR_ERR(ctrl_dev); + + serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); + put_device(ctrl_dev); + if (!serdev) + return -ENOMEM; + + ret = serdev_device_add(serdev); + if (ret) { + dev_err(&pdev->dev, "error %d adding serdev\n", ret); + serdev_device_put(serdev); + return ret; + } + + ret = serdev_device_driver_register(&dell_uart_bl_serdev_driver); + if (ret) + goto err_remove_serdev; + + /* + * serdev device <-> driver matching relies on OF or ACPI matches and + * neither is available here, manually bind the driver. + */ + ret = device_driver_attach(&dell_uart_bl_serdev_driver.driver, &serdev->dev); + if (ret) + goto err_unregister_serdev_driver; + + /* So that dell_uart_bl_pdev_remove() can remove the serdev */ + platform_set_drvdata(pdev, serdev); + return 0; + +err_unregister_serdev_driver: + serdev_device_driver_unregister(&dell_uart_bl_serdev_driver); +err_remove_serdev: + serdev_device_remove(serdev); + return ret; +} + +static void dell_uart_bl_pdev_remove(struct platform_device *pdev) +{ + struct serdev_device *serdev = platform_get_drvdata(pdev); + + serdev_device_driver_unregister(&dell_uart_bl_serdev_driver); + serdev_device_remove(serdev); +} + +static struct platform_driver dell_uart_bl_pdev_driver = { + .probe = dell_uart_bl_pdev_probe, + .remove = dell_uart_bl_pdev_remove, + .driver = { + .name = "dell-uart-backlight", + }, +}; +module_platform_driver(dell_uart_bl_pdev_driver); + +MODULE_ALIAS("platform:dell-uart-backlight"); +MODULE_DESCRIPTION("Dell AIO Serial Backlight driver"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-wmi-aio.c b/drivers/platform/x86/dell/dell-wmi-aio.c new file mode 100644 index 000000000..540964957 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-aio.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * WMI hotkeys support for Dell All-In-One series + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_DESCRIPTION("WMI hotkeys driver for Dell All-In-One series"); +MODULE_LICENSE("GPL"); + +#define EVENT_GUID1 "284A0E6B-380E-472A-921F-E52786257FB4" +#define EVENT_GUID2 "02314822-307C-4F66-BF0E-48AEAEB26CC8" + +struct dell_wmi_event { + u16 length; + /* 0x000: A hot key pressed or an event occurred + * 0x00F: A sequence of hot keys are pressed */ + u16 type; + u16 event[]; +}; + +static const char *dell_wmi_aio_guids[] = { + EVENT_GUID1, + EVENT_GUID2, + NULL +}; + +MODULE_ALIAS("wmi:"EVENT_GUID1); +MODULE_ALIAS("wmi:"EVENT_GUID2); + +static const struct key_entry dell_wmi_aio_keymap[] = { + { KE_KEY, 0xc0, { KEY_VOLUMEUP } }, + { KE_KEY, 0xc1, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0xe030, { KEY_VOLUMEUP } }, + { KE_KEY, 0xe02e, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0xe020, { KEY_MUTE } }, + { KE_KEY, 0xe027, { KEY_DISPLAYTOGGLE } }, + { KE_KEY, 0xe006, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0xe005, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0xe00b, { KEY_SWITCHVIDEOMODE } }, + { KE_END, 0 } +}; + +static struct input_dev *dell_wmi_aio_input_dev; + +/* + * The new WMI event data format will follow the dell_wmi_event structure + * So, we will check if the buffer matches the format + */ +static bool dell_wmi_aio_event_check(u8 *buffer, int length) +{ + struct dell_wmi_event *event = (struct dell_wmi_event *)buffer; + + if (event == NULL || length < 6) + return false; + + if ((event->type == 0 || event->type == 0xf) && + event->length >= 2) + return true; + + return false; +} + +static void dell_wmi_aio_notify(union acpi_object *obj, void *context) +{ + struct dell_wmi_event *event; + + if (obj) { + unsigned int scancode = 0; + + switch (obj->type) { + case ACPI_TYPE_INTEGER: + /* Most All-In-One correctly return integer scancode */ + scancode = obj->integer.value; + sparse_keymap_report_event(dell_wmi_aio_input_dev, + scancode, 1, true); + break; + case ACPI_TYPE_BUFFER: + if (dell_wmi_aio_event_check(obj->buffer.pointer, + obj->buffer.length)) { + event = (struct dell_wmi_event *) + obj->buffer.pointer; + scancode = event->event[0]; + } else { + /* Broken machines return the scancode in a + buffer */ + if (obj->buffer.pointer && + obj->buffer.length > 0) + scancode = obj->buffer.pointer[0]; + } + if (scancode) + sparse_keymap_report_event( + dell_wmi_aio_input_dev, + scancode, 1, true); + break; + } + } +} + +static int __init dell_wmi_aio_input_setup(void) +{ + int err; + + dell_wmi_aio_input_dev = input_allocate_device(); + + if (!dell_wmi_aio_input_dev) + return -ENOMEM; + + dell_wmi_aio_input_dev->name = "Dell AIO WMI hotkeys"; + dell_wmi_aio_input_dev->phys = "wmi/input0"; + dell_wmi_aio_input_dev->id.bustype = BUS_HOST; + + err = sparse_keymap_setup(dell_wmi_aio_input_dev, + dell_wmi_aio_keymap, NULL); + if (err) { + pr_err("Unable to setup input device keymap\n"); + goto err_free_dev; + } + err = input_register_device(dell_wmi_aio_input_dev); + if (err) { + pr_info("Unable to register input device\n"); + goto err_free_dev; + } + return 0; + +err_free_dev: + input_free_device(dell_wmi_aio_input_dev); + return err; +} + +static const char *dell_wmi_aio_find(void) +{ + int i; + + for (i = 0; dell_wmi_aio_guids[i] != NULL; i++) + if (wmi_has_guid(dell_wmi_aio_guids[i])) + return dell_wmi_aio_guids[i]; + + return NULL; +} + +static int __init dell_wmi_aio_init(void) +{ + int err; + const char *guid; + + guid = dell_wmi_aio_find(); + if (!guid) { + pr_warn("No known WMI GUID found\n"); + return -ENXIO; + } + + err = dell_wmi_aio_input_setup(); + if (err) + return err; + + err = wmi_install_notify_handler(guid, dell_wmi_aio_notify, NULL); + if (err) { + pr_err("Unable to register notify handler - %d\n", err); + input_unregister_device(dell_wmi_aio_input_dev); + return err; + } + + return 0; +} + +static void __exit dell_wmi_aio_exit(void) +{ + const char *guid; + + guid = dell_wmi_aio_find(); + wmi_remove_notify_handler(guid); + input_unregister_device(dell_wmi_aio_input_dev); +} + +module_init(dell_wmi_aio_init); +module_exit(dell_wmi_aio_exit); diff --git a/drivers/platform/x86/dell/dell-wmi-base.c b/drivers/platform/x86/dell/dell-wmi-base.c new file mode 100644 index 000000000..997383ba1 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-base.c @@ -0,0 +1,862 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Dell WMI hotkeys + * + * Copyright (C) 2008 Red Hat + * Copyright (C) 2014-2015 Pali Rohár + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac + * Copyright (C) 2005 Bernhard Rosenkraenzer + * Copyright (C) 2005 Dmitry Torokhov + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dell-smbios.h" +#include "dell-wmi-descriptor.h" +#include "dell-wmi-privacy.h" + +MODULE_AUTHOR("Matthew Garrett "); +MODULE_AUTHOR("Pali Rohár "); +MODULE_DESCRIPTION("Dell laptop WMI hotkeys driver"); +MODULE_LICENSE("GPL"); + +#define DELL_EVENT_GUID "9DBB5994-A997-11DA-B012-B622A1EF5492" + +static bool wmi_requires_smbios_request; + +struct dell_wmi_priv { + struct input_dev *input_dev; + struct input_dev *tabletswitch_dev; + u32 interface_version; +}; + +static int __init dmi_matched(const struct dmi_system_id *dmi) +{ + wmi_requires_smbios_request = 1; + return 1; +} + +static const struct dmi_system_id dell_wmi_smbios_list[] __initconst = { + { + .callback = dmi_matched, + .ident = "Dell Inspiron M5110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron M5110"), + }, + }, + { + .callback = dmi_matched, + .ident = "Dell Vostro V131", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Vostro V131"), + }, + }, + { } +}; + +/* + * Keymap for WMI events of type 0x0000 + * + * Certain keys are flagged as KE_IGNORE. All of these are either + * notifications (rather than requests for change) or are also sent + * via the keyboard controller so should not be sent again. + */ +static const struct key_entry dell_wmi_keymap_type_0000[] = { + { KE_IGNORE, 0x003a, { KEY_CAPSLOCK } }, + + /* Audio mute toggle */ + { KE_KEY, 0x0109, { KEY_MUTE } }, + + /* Mic mute toggle */ + { KE_KEY, 0x0150, { KEY_MICMUTE } }, + + /* Meta key lock */ + { KE_IGNORE, 0xe000, { KEY_RIGHTMETA } }, + + /* Meta key unlock */ + { KE_IGNORE, 0xe001, { KEY_RIGHTMETA } }, + + /* Key code is followed by brightness level */ + { KE_KEY, 0xe005, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0xe006, { KEY_BRIGHTNESSUP } }, + + /* Battery health status button */ + { KE_KEY, 0xe007, { KEY_BATTERY } }, + + /* Radio devices state change, key code is followed by other values */ + { KE_IGNORE, 0xe008, { KEY_RFKILL } }, + + { KE_KEY, 0xe009, { KEY_EJECTCD } }, + + /* Key code is followed by: next, active and attached devices */ + { KE_KEY, 0xe00b, { KEY_SWITCHVIDEOMODE } }, + + /* Key code is followed by keyboard illumination level */ + { KE_IGNORE, 0xe00c, { KEY_KBDILLUMTOGGLE } }, + + /* BIOS error detected */ + { KE_IGNORE, 0xe00d, { KEY_RESERVED } }, + + /* Battery was removed or inserted */ + { KE_IGNORE, 0xe00e, { KEY_RESERVED } }, + + /* Wifi Catcher */ + { KE_KEY, 0xe011, { KEY_WLAN } }, + + /* Ambient light sensor toggle */ + { KE_IGNORE, 0xe013, { KEY_RESERVED } }, + + { KE_IGNORE, 0xe020, { KEY_MUTE } }, + + /* Unknown, defined in ACPI DSDT */ + /* { KE_IGNORE, 0xe023, { KEY_RESERVED } }, */ + + /* Untested, Dell Instant Launch key on Inspiron 7520 */ + /* { KE_IGNORE, 0xe024, { KEY_RESERVED } }, */ + + /* Dell Instant Launch key */ + { KE_KEY, 0xe025, { KEY_PROG4 } }, + + /* Audio panel key */ + { KE_IGNORE, 0xe026, { KEY_RESERVED } }, + + /* LCD Display On/Off Control key */ + { KE_KEY, 0xe027, { KEY_DISPLAYTOGGLE } }, + + /* Untested, Multimedia key on Dell Vostro 3560 */ + /* { KE_IGNORE, 0xe028, { KEY_RESERVED } }, */ + + /* Dell Instant Launch key */ + { KE_KEY, 0xe029, { KEY_PROG4 } }, + + /* Untested, Windows Mobility Center button on Inspiron 7520 */ + /* { KE_IGNORE, 0xe02a, { KEY_RESERVED } }, */ + + /* Unknown, defined in ACPI DSDT */ + /* { KE_IGNORE, 0xe02b, { KEY_RESERVED } }, */ + + /* Untested, Dell Audio With Preset Switch button on Inspiron 7520 */ + /* { KE_IGNORE, 0xe02c, { KEY_RESERVED } }, */ + + { KE_IGNORE, 0xe02e, { KEY_VOLUMEDOWN } }, + { KE_IGNORE, 0xe030, { KEY_VOLUMEUP } }, + { KE_IGNORE, 0xe033, { KEY_KBDILLUMUP } }, + { KE_IGNORE, 0xe034, { KEY_KBDILLUMDOWN } }, + { KE_IGNORE, 0xe03a, { KEY_CAPSLOCK } }, + + /* NIC Link is Up */ + { KE_IGNORE, 0xe043, { KEY_RESERVED } }, + + /* NIC Link is Down */ + { KE_IGNORE, 0xe044, { KEY_RESERVED } }, + + /* + * This entry is very suspicious! + * Originally Matthew Garrett created this dell-wmi driver specially for + * "button with a picture of a battery" which has event code 0xe045. + * Later Mario Limonciello from Dell told us that event code 0xe045 is + * reported by Num Lock and should be ignored because key is send also + * by keyboard controller. + * So for now we will ignore this event to prevent potential double + * Num Lock key press. + */ + { KE_IGNORE, 0xe045, { KEY_NUMLOCK } }, + + /* Scroll lock and also going to tablet mode on portable devices */ + { KE_IGNORE, 0xe046, { KEY_SCROLLLOCK } }, + + /* Untested, going from tablet mode on portable devices */ + /* { KE_IGNORE, 0xe047, { KEY_RESERVED } }, */ + + /* Dell Support Center key */ + { KE_IGNORE, 0xe06e, { KEY_RESERVED } }, + + { KE_IGNORE, 0xe0f7, { KEY_MUTE } }, + { KE_IGNORE, 0xe0f8, { KEY_VOLUMEDOWN } }, + { KE_IGNORE, 0xe0f9, { KEY_VOLUMEUP } }, +}; + +struct dell_bios_keymap_entry { + u16 scancode; + u16 keycode; +}; + +struct dell_bios_hotkey_table { + struct dmi_header header; + struct dell_bios_keymap_entry keymap[]; + +}; + +struct dell_dmi_results { + int err; + int keymap_size; + struct key_entry *keymap; +}; + +/* Uninitialized entries here are KEY_RESERVED == 0. */ +static const u16 bios_to_linux_keycode[256] = { + [0] = KEY_MEDIA, + [1] = KEY_NEXTSONG, + [2] = KEY_PLAYPAUSE, + [3] = KEY_PREVIOUSSONG, + [4] = KEY_STOPCD, + [5] = KEY_UNKNOWN, + [6] = KEY_UNKNOWN, + [7] = KEY_UNKNOWN, + [8] = KEY_WWW, + [9] = KEY_UNKNOWN, + [10] = KEY_VOLUMEDOWN, + [11] = KEY_MUTE, + [12] = KEY_VOLUMEUP, + [13] = KEY_UNKNOWN, + [14] = KEY_BATTERY, + [15] = KEY_EJECTCD, + [16] = KEY_UNKNOWN, + [17] = KEY_SLEEP, + [18] = KEY_PROG1, + [19] = KEY_BRIGHTNESSDOWN, + [20] = KEY_BRIGHTNESSUP, + [21] = KEY_BRIGHTNESS_AUTO, + [22] = KEY_KBDILLUMTOGGLE, + [23] = KEY_UNKNOWN, + [24] = KEY_SWITCHVIDEOMODE, + [25] = KEY_UNKNOWN, + [26] = KEY_UNKNOWN, + [27] = KEY_SWITCHVIDEOMODE, + [28] = KEY_UNKNOWN, + [29] = KEY_UNKNOWN, + [30] = KEY_PROG2, + [31] = KEY_UNKNOWN, + [32] = KEY_UNKNOWN, + [33] = KEY_UNKNOWN, + [34] = KEY_UNKNOWN, + [35] = KEY_UNKNOWN, + [36] = KEY_UNKNOWN, + [37] = KEY_UNKNOWN, + [38] = KEY_MICMUTE, + [255] = KEY_PROG3, +}; + +/* + * Keymap for WMI events of type 0x0010 + * + * These are applied if the 0xB2 DMI hotkey table is present and doesn't + * override them. + */ +static const struct key_entry dell_wmi_keymap_type_0010[] = { + /* Fn-lock switched to function keys */ + { KE_IGNORE, 0x0, { KEY_RESERVED } }, + + /* Fn-lock switched to multimedia keys */ + { KE_IGNORE, 0x1, { KEY_RESERVED } }, + + /* Keyboard backlight change notification */ + { KE_IGNORE, 0x3f, { KEY_RESERVED } }, + + /* Backlight brightness level */ + { KE_KEY, 0x57, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x58, { KEY_BRIGHTNESSUP } }, + + /*Speaker Mute*/ + { KE_KEY, 0x109, { KEY_MUTE} }, + + /* S2Idle screen off */ + { KE_IGNORE, 0x120, { KEY_RESERVED }}, + + /* Leaving S4 or S2Idle suspend */ + { KE_IGNORE, 0x130, { KEY_RESERVED }}, + + /* Entering S2Idle suspend */ + { KE_IGNORE, 0x140, { KEY_RESERVED }}, + + /* Mic mute */ + { KE_KEY, 0x150, { KEY_MICMUTE } }, + + /* Fn-lock */ + { KE_IGNORE, 0x151, { KEY_RESERVED } }, + + /* Change keyboard illumination */ + { KE_IGNORE, 0x152, { KEY_KBDILLUMTOGGLE } }, + + /* + * Radio disable (notify only -- there is no model for which the + * WMI event is supposed to trigger an action). + */ + { KE_IGNORE, 0x153, { KEY_RFKILL } }, + + /* RGB keyboard backlight control */ + { KE_IGNORE, 0x154, { KEY_RESERVED } }, + + /* + * Stealth mode toggle. This will "disable all lights and sounds". + * The action is performed by the BIOS and EC; the WMI event is just + * a notification. On the XPS 13 9350, this is Fn+F7, and there's + * a BIOS setting to enable and disable the hotkey. + */ + { KE_IGNORE, 0x155, { KEY_RESERVED } }, + + /* Rugged magnetic dock attach/detach events */ + { KE_IGNORE, 0x156, { KEY_RESERVED } }, + { KE_IGNORE, 0x157, { KEY_RESERVED } }, + + /* Rugged programmable (P1/P2/P3 keys) */ + { KE_KEY, 0x850, { KEY_PROG1 } }, + { KE_KEY, 0x851, { KEY_PROG2 } }, + { KE_KEY, 0x852, { KEY_PROG3 } }, + + /* + * Radio disable (notify only -- there is no model for which the + * WMI event is supposed to trigger an action). + */ + { KE_IGNORE, 0xe008, { KEY_RFKILL } }, + + /* Fn-lock */ + { KE_IGNORE, 0xe035, { KEY_RESERVED } }, +}; + +/* + * Keymap for WMI events of type 0x0011 + */ +static const struct key_entry dell_wmi_keymap_type_0011[] = { + /* Reflex keyboard switch on 2n1 devices */ + { KE_IGNORE, 0xe070, { KEY_RESERVED } }, + + /* Battery unplugged */ + { KE_IGNORE, 0xfff0, { KEY_RESERVED } }, + + /* Battery inserted */ + { KE_IGNORE, 0xfff1, { KEY_RESERVED } }, + + /* + * Detachable keyboard detached / undocked + * Note SW_TABLET_MODE is already reported through the intel_vbtn + * driver for this, so we ignore it. + */ + { KE_IGNORE, 0xfff2, { KEY_RESERVED } }, + + /* Detachable keyboard attached / docked */ + { KE_IGNORE, 0xfff3, { KEY_RESERVED } }, + + /* Keyboard backlight level changed */ + { KE_IGNORE, KBD_LED_OFF_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_ON_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_AUTO_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_AUTO_25_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_AUTO_50_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_AUTO_75_TOKEN, { KEY_RESERVED } }, + { KE_IGNORE, KBD_LED_AUTO_100_TOKEN, { KEY_RESERVED } }, +}; + +/* + * Keymap for WMI events of type 0x0012 + * They are events with extended data + */ +static const struct key_entry dell_wmi_keymap_type_0012[] = { + /* Backlight brightness change event */ + { KE_IGNORE, 0x0003, { KEY_RESERVED } }, + + /* + * Electronic privacy screen toggled, extended data gives state, + * separate entries for on/off see handling in dell_wmi_process_key(). + */ + { KE_KEY, 0x000c, { KEY_EPRIVACY_SCREEN_OFF } }, + { KE_KEY, 0x000c, { KEY_EPRIVACY_SCREEN_ON } }, + + /* Ultra-performance mode switch request */ + { KE_IGNORE, 0x000d, { KEY_RESERVED } }, + + /* Fn-lock button pressed */ + { KE_IGNORE, 0xe035, { KEY_RESERVED } }, +}; + +static void dell_wmi_switch_event(struct input_dev **subdev, + const char *devname, + int switchid, + int value) +{ + if (!*subdev) { + struct input_dev *dev = input_allocate_device(); + + if (!dev) { + pr_warn("could not allocate device for %s\n", devname); + return; + } + __set_bit(EV_SW, (dev)->evbit); + __set_bit(switchid, (dev)->swbit); + + (dev)->name = devname; + (dev)->id.bustype = BUS_HOST; + if (input_register_device(dev)) { + input_free_device(dev); + pr_warn("could not register device for %s\n", devname); + return; + } + *subdev = dev; + } + + input_report_switch(*subdev, switchid, value); + input_sync(*subdev); +} + +static int dell_wmi_process_key(struct wmi_device *wdev, int type, int code, __le16 *buffer, + int remaining) +{ + struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + const struct key_entry *key; + int used = 0; + int value = 1; + + key = sparse_keymap_entry_from_scancode(priv->input_dev, + (type << 16) | code); + if (!key) { + pr_info("Unknown key with type 0x%04x and code 0x%04x pressed\n", + type, code); + return 0; + } + + pr_debug("Key with type 0x%04x and code 0x%04x pressed\n", type, code); + + /* Don't report brightness notifications that will also come via ACPI */ + if ((key->keycode == KEY_BRIGHTNESSUP || + key->keycode == KEY_BRIGHTNESSDOWN) && + acpi_video_handles_brightness_key_presses()) + return 0; + + if (type == 0x0000 && code == 0xe025 && !wmi_requires_smbios_request) + return 0; + + if (key->keycode == KEY_KBDILLUMTOGGLE) { + dell_laptop_call_notifier( + DELL_LAPTOP_KBD_BACKLIGHT_BRIGHTNESS_CHANGED, NULL); + } else if (type == 0x0011 && code == 0xe070 && remaining > 0) { + dell_wmi_switch_event(&priv->tabletswitch_dev, + "Dell tablet mode switch", + SW_TABLET_MODE, !le16_to_cpu(buffer[0])); + return 1; + } else if (type == 0x0012 && code == 0x000c && remaining > 0) { + /* Eprivacy toggle, switch to "on" key entry for on events */ + if (le16_to_cpu(buffer[0]) == 2) + key++; + used = 1; + } else if (type == 0x0012 && code == 0x000d && remaining > 0) { + value = (le16_to_cpu(buffer[2]) == 2); + used = 1; + } + + sparse_keymap_report_entry(priv->input_dev, key, value, true); + + return used; +} + +static void dell_wmi_notify(struct wmi_device *wdev, const struct wmi_buffer *buffer) +{ + struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + __le16 *buffer_entry, *buffer_end; + size_t buffer_size; + int len, i; + + pr_debug("Received WMI event (%*ph)\n", (int)buffer->length, buffer->data); + + buffer_entry = buffer->data; + buffer_size = buffer->length / 2; + buffer_end = buffer_entry + buffer_size; + + /* + * BIOS/ACPI on devices with WMI interface version 0 does not clear + * buffer before filling it. So next time when BIOS/ACPI send WMI event + * which is smaller as previous then it contains garbage in buffer from + * previous event. + * + * BIOS/ACPI on devices with WMI interface version 1 clears buffer and + * sometimes send more events in buffer at one call. + * + * So to prevent reading garbage from buffer we will process only first + * one event on devices with WMI interface version 0. + */ + if (priv->interface_version == 0 && buffer_entry < buffer_end) + if (buffer_end > buffer_entry + le16_to_cpu(buffer_entry[0]) + 1) + buffer_end = buffer_entry + le16_to_cpu(buffer_entry[0]) + 1; + + while (buffer_entry < buffer_end) { + + len = le16_to_cpu(buffer_entry[0]); + if (len == 0) + break; + + len++; + + if (buffer_entry + len > buffer_end) { + pr_warn("Invalid length of WMI event\n"); + break; + } + + pr_debug("Process buffer (%*ph)\n", len*2, buffer_entry); + + switch (le16_to_cpu(buffer_entry[1])) { + case 0x0000: /* One key pressed or event occurred */ + if (len > 2) + dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]), + le16_to_cpu(buffer_entry[2]), + buffer_entry + 3, + len - 3); + /* Extended data is currently ignored */ + break; + case 0x0010: /* Sequence of keys pressed */ + case 0x0011: /* Sequence of events occurred */ + for (i = 2; i < len; ++i) + i += dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]), + le16_to_cpu(buffer_entry[i]), + buffer_entry + i, + len - i - 1); + break; + case 0x0012: + if ((len > 4) && dell_privacy_process_event(le16_to_cpu(buffer_entry[1]), + le16_to_cpu(buffer_entry[3]), + le16_to_cpu(buffer_entry[4]))) + /* dell_privacy_process_event has handled the event */; + else if (len > 2) + dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]), + le16_to_cpu(buffer_entry[2]), + buffer_entry + 3, len - 3); + break; + default: /* Unknown event */ + pr_info("Unknown WMI event type 0x%x\n", le16_to_cpu(buffer_entry[1])); + break; + } + + buffer_entry += len; + + } + +} + +static bool have_scancode(u32 scancode, const struct key_entry *keymap, int len) +{ + int i; + + for (i = 0; i < len; i++) + if (keymap[i].code == scancode) + return true; + + return false; +} + +static void handle_dmi_entry(const struct dmi_header *dm, void *opaque) +{ + struct dell_dmi_results *results = opaque; + struct dell_bios_hotkey_table *table; + int hotkey_num, i, pos = 0; + struct key_entry *keymap; + + if (results->err || results->keymap) + return; /* We already found the hotkey table. */ + + /* The Dell hotkey table is type 0xB2. Scan until we find it. */ + if (dm->type != 0xb2) + return; + + table = container_of(dm, struct dell_bios_hotkey_table, header); + + hotkey_num = (table->header.length - + sizeof(struct dell_bios_hotkey_table)) / + sizeof(struct dell_bios_keymap_entry); + if (hotkey_num < 1) { + /* + * Historically, dell-wmi would ignore a DMI entry of + * fewer than 7 bytes. Sizes between 4 and 8 bytes are + * nonsensical (both the header and all entries are 4 + * bytes), so we approximate the old behavior by + * ignoring tables with fewer than one entry. + */ + return; + } + + keymap = kzalloc_objs(struct key_entry, hotkey_num); + if (!keymap) { + results->err = -ENOMEM; + return; + } + + for (i = 0; i < hotkey_num; i++) { + const struct dell_bios_keymap_entry *bios_entry = + &table->keymap[i]; + + /* Uninitialized entries are 0 aka KEY_RESERVED. */ + u16 keycode = (bios_entry->keycode < + ARRAY_SIZE(bios_to_linux_keycode)) ? + bios_to_linux_keycode[bios_entry->keycode] : + (bios_entry->keycode == 0xffff ? KEY_UNKNOWN : KEY_RESERVED); + + /* + * Log if we find an entry in the DMI table that we don't + * understand. If this happens, we should figure out what + * the entry means and add it to bios_to_linux_keycode. + */ + if (keycode == KEY_RESERVED) { + pr_info("firmware scancode 0x%x maps to unrecognized keycode 0x%x\n", + bios_entry->scancode, bios_entry->keycode); + continue; + } + + if (keycode == KEY_KBDILLUMTOGGLE) + keymap[pos].type = KE_IGNORE; + else + keymap[pos].type = KE_KEY; + keymap[pos].code = bios_entry->scancode; + keymap[pos].keycode = keycode; + + pos++; + } + + results->keymap = keymap; + results->keymap_size = pos; +} + +static int dell_wmi_input_setup(struct wmi_device *wdev) +{ + struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + struct dell_dmi_results dmi_results = {}; + struct key_entry *keymap; + int err, i, pos = 0; + + priv->input_dev = input_allocate_device(); + if (!priv->input_dev) + return -ENOMEM; + + priv->input_dev->name = "Dell WMI hotkeys"; + priv->input_dev->id.bustype = BUS_HOST; + priv->input_dev->dev.parent = &wdev->dev; + + if (dmi_walk(handle_dmi_entry, &dmi_results)) { + /* + * Historically, dell-wmi ignored dmi_walk errors. A failure + * is certainly surprising, but it probably just indicates + * a very old laptop. + */ + pr_warn("no DMI; using the old-style hotkey interface\n"); + } + + if (dmi_results.err) { + err = dmi_results.err; + goto err_free_dev; + } + + keymap = kzalloc_objs(struct key_entry, + dmi_results.keymap_size + ARRAY_SIZE(dell_wmi_keymap_type_0000) + ARRAY_SIZE(dell_wmi_keymap_type_0010) + ARRAY_SIZE(dell_wmi_keymap_type_0011) + ARRAY_SIZE(dell_wmi_keymap_type_0012) + 1); + if (!keymap) { + kfree(dmi_results.keymap); + err = -ENOMEM; + goto err_free_dev; + } + + /* Append table with events of type 0x0010 which comes from DMI */ + for (i = 0; i < dmi_results.keymap_size; i++) { + keymap[pos] = dmi_results.keymap[i]; + keymap[pos].code |= (0x0010 << 16); + pos++; + } + + kfree(dmi_results.keymap); + + /* Append table with extra events of type 0x0010 which are not in DMI */ + for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0010); i++) { + const struct key_entry *entry = &dell_wmi_keymap_type_0010[i]; + + /* + * Check if we've already found this scancode. This takes + * quadratic time, but it doesn't matter unless the list + * of extra keys gets very long. + */ + if (dmi_results.keymap_size && + have_scancode(entry->code | (0x0010 << 16), + keymap, dmi_results.keymap_size) + ) + continue; + + keymap[pos] = *entry; + keymap[pos].code |= (0x0010 << 16); + pos++; + } + + /* Append table with events of type 0x0011 */ + for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0011); i++) { + keymap[pos] = dell_wmi_keymap_type_0011[i]; + keymap[pos].code |= (0x0011 << 16); + pos++; + } + + /* Append table with events of type 0x0012 */ + for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0012); i++) { + keymap[pos] = dell_wmi_keymap_type_0012[i]; + keymap[pos].code |= (0x0012 << 16); + pos++; + } + + /* + * Now append also table with "legacy" events of type 0x0000. Some of + * them are reported also on laptops which have scancodes in DMI. + */ + for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0000); i++) { + keymap[pos] = dell_wmi_keymap_type_0000[i]; + pos++; + } + + keymap[pos].type = KE_END; + + err = sparse_keymap_setup(priv->input_dev, keymap, NULL); + /* + * Sparse keymap library makes a copy of keymap so we don't need the + * original one that was allocated. + */ + kfree(keymap); + if (err) + goto err_free_dev; + + err = input_register_device(priv->input_dev); + if (err) + goto err_free_dev; + + return 0; + + err_free_dev: + input_free_device(priv->input_dev); + return err; +} + +static void dell_wmi_input_destroy(struct wmi_device *wdev) +{ + struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + + input_unregister_device(priv->input_dev); + if (priv->tabletswitch_dev) + input_unregister_device(priv->tabletswitch_dev); +} + +/* + * According to Dell SMBIOS documentation: + * + * 17 3 Application Program Registration + * + * cbArg1 Application ID 1 = 0x00010000 + * cbArg2 Application ID 2 + * QUICKSET/DCP = 0x51534554 "QSET" + * ALS Driver = 0x416c7353 "AlsS" + * Latitude ON = 0x4c6f6e52 "LonR" + * cbArg3 Application version or revision number + * cbArg4 0 = Unregister application + * 1 = Register application + * cbRes1 Standard return codes (0, -1, -2) + */ + +static int dell_wmi_events_set_enabled(bool enable) +{ + struct calling_interface_buffer *buffer; + int ret; + + buffer = kzalloc_obj(struct calling_interface_buffer); + if (!buffer) + return -ENOMEM; + buffer->cmd_class = CLASS_INFO; + buffer->cmd_select = SELECT_APP_REGISTRATION; + buffer->input[0] = 0x10000; + buffer->input[1] = 0x51534554; + buffer->input[3] = enable; + ret = dell_smbios_call(buffer); + if (ret == 0) + ret = buffer->output[0]; + kfree(buffer); + + return dell_smbios_error(ret); +} + +static int dell_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct dell_wmi_priv *priv; + int ret; + + ret = dell_wmi_get_descriptor_valid(); + if (ret) + return ret; + + priv = devm_kzalloc( + &wdev->dev, sizeof(struct dell_wmi_priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + dev_set_drvdata(&wdev->dev, priv); + + if (!dell_wmi_get_interface_version(&priv->interface_version)) + return -EPROBE_DEFER; + + return dell_wmi_input_setup(wdev); +} + +static void dell_wmi_remove(struct wmi_device *wdev) +{ + dell_wmi_input_destroy(wdev); +} +static const struct wmi_device_id dell_wmi_id_table[] = { + { .guid_string = DELL_EVENT_GUID }, + { }, +}; + +static struct wmi_driver dell_wmi_driver = { + .driver = { + .name = "dell-wmi", + }, + .id_table = dell_wmi_id_table, + .min_event_size = sizeof(__le16), + .probe = dell_wmi_probe, + .remove = dell_wmi_remove, + .notify_new = dell_wmi_notify, +}; + +static int __init dell_wmi_init(void) +{ + int err; + + dmi_check_system(dell_wmi_smbios_list); + + if (wmi_requires_smbios_request) { + err = dell_wmi_events_set_enabled(true); + if (err) { + pr_err("Failed to enable WMI events\n"); + return err; + } + } + + err = dell_privacy_register_driver(); + if (err) + return err; + + return wmi_driver_register(&dell_wmi_driver); +} +late_initcall(dell_wmi_init); + +static void __exit dell_wmi_exit(void) +{ + if (wmi_requires_smbios_request) + dell_wmi_events_set_enabled(false); + + wmi_driver_unregister(&dell_wmi_driver); + dell_privacy_unregister_driver(); +} +module_exit(dell_wmi_exit); + +MODULE_DEVICE_TABLE(wmi, dell_wmi_id_table); diff --git a/drivers/platform/x86/dell/dell-wmi-ddv.c b/drivers/platform/x86/dell/dell-wmi-ddv.c new file mode 100644 index 000000000..736d9b1fd --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-ddv.c @@ -0,0 +1,1114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Linux driver for WMI sensor information on Dell notebooks. + * + * Copyright (C) 2022 Armin Wolf + */ + +#define pr_format(fmt) KBUILD_MODNAME ": " fmt + +#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 + +#define DRIVER_NAME "dell-wmi-ddv" + +#define DELL_DDV_SUPPORTED_VERSION_MIN 2 +#define DELL_DDV_SUPPORTED_VERSION_MAX 3 +#define DELL_DDV_GUID "8A42EA14-4F2A-FD45-6422-0087F7A7E608" + +/* Battery indices 1, 2 and 3 */ +#define DELL_DDV_NUM_BATTERIES 3 + +#define SBS_MANUFACTURE_YEAR_MASK GENMASK(15, 9) +#define SBS_MANUFACTURE_MONTH_MASK GENMASK(8, 5) +#define SBS_MANUFACTURE_DAY_MASK GENMASK(4, 0) + +#define MA_FAILURE_MODE_MASK GENMASK(11, 8) +#define MA_FAILURE_MODE_PERMANENT 0x9 +#define MA_FAILURE_MODE_OVERHEAT 0xA +#define MA_FAILURE_MODE_OVERCURRENT 0xB + +#define MA_PERMANENT_FAILURE_CODE_MASK GENMASK(13, 12) +#define MA_PERMANENT_FAILURE_FUSE_BLOWN 0x0 +#define MA_PERMANENT_FAILURE_CELL_IMBALANCE 0x1 +#define MA_PERMANENT_FAILURE_OVERVOLTAGE 0x2 +#define MA_PERMANENT_FAILURE_FET_FAILURE 0x3 + +#define MA_OVERHEAT_FAILURE_CODE_MASK GENMASK(15, 12) +#define MA_OVERHEAT_FAILURE_START 0x5 +#define MA_OVERHEAT_FAILURE_CHARGING 0x7 +#define MA_OVERHEAT_FAILURE_DISCHARGING 0x8 + +#define MA_OVERCURRENT_FAILURE_CODE_MASK GENMASK(15, 12) +#define MA_OVERCURRENT_FAILURE_CHARGING 0x6 +#define MA_OVERCURRENT_FAILURE_DISCHARGING 0xB + +#define DELL_EPPID_LENGTH 20 +#define DELL_EPPID_EXT_LENGTH 23 + +static bool force; +module_param_unsafe(force, bool, 0); +MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); + +enum dell_ddv_method { + DELL_DDV_BATTERY_DESIGN_CAPACITY = 0x01, + DELL_DDV_BATTERY_FULL_CHARGE_CAPACITY = 0x02, + DELL_DDV_BATTERY_MANUFACTURE_NAME = 0x03, + DELL_DDV_BATTERY_MANUFACTURE_DATE = 0x04, + DELL_DDV_BATTERY_SERIAL_NUMBER = 0x05, + DELL_DDV_BATTERY_CHEMISTRY_VALUE = 0x06, + DELL_DDV_BATTERY_TEMPERATURE = 0x07, + DELL_DDV_BATTERY_CURRENT = 0x08, + DELL_DDV_BATTERY_VOLTAGE = 0x09, + DELL_DDV_BATTERY_MANUFACTURER_ACCESS = 0x0A, + DELL_DDV_BATTERY_RELATIVE_CHARGE_STATE = 0x0B, + DELL_DDV_BATTERY_CYCLE_COUNT = 0x0C, + DELL_DDV_BATTERY_EPPID = 0x0D, + DELL_DDV_BATTERY_RAW_ANALYTICS_START = 0x0E, + DELL_DDV_BATTERY_RAW_ANALYTICS = 0x0F, + DELL_DDV_BATTERY_DESIGN_VOLTAGE = 0x10, + DELL_DDV_BATTERY_RAW_ANALYTICS_A_BLOCK = 0x11, /* version 3 */ + + DELL_DDV_INTERFACE_VERSION = 0x12, + + DELL_DDV_FAN_SENSOR_INFORMATION = 0x20, + DELL_DDV_THERMAL_SENSOR_INFORMATION = 0x22, +}; + +struct dell_wmi_buffer { + __le32 raw_size; + u8 raw_data[]; +} __packed; + +struct fan_sensor_entry { + u8 type; + __le16 rpm; +} __packed; + +struct thermal_sensor_entry { + u8 type; + s8 now; + s8 min; + s8 max; + u8 unknown; +} __packed; + +struct combined_channel_info { + struct hwmon_channel_info info; + u32 config[]; +}; + +struct combined_chip_info { + struct hwmon_chip_info chip; + const struct hwmon_channel_info *info[]; +}; + +struct dell_wmi_ddv_sensors { + bool active; + struct mutex lock; /* protect caching */ + unsigned long timestamp; + struct dell_wmi_buffer *buffer; + u64 entries; +}; + +struct dell_wmi_ddv_data { + struct acpi_battery_hook hook; + struct device_attribute eppid_attr; + struct mutex translation_cache_lock; /* Protects the translation cache */ + struct power_supply *translation_cache[DELL_DDV_NUM_BATTERIES]; + struct dell_wmi_ddv_sensors fans; + struct dell_wmi_ddv_sensors temps; + struct wmi_device *wdev; +}; + +static const char * const fan_labels[] = { + "CPU Fan", + "Chassis Motherboard Fan", + "Video Fan", + "Power Supply Fan", + "Chipset Fan", + "Memory Fan", + "PCI Fan", + "HDD Fan", +}; + +static const char * const fan_dock_labels[] = { + "Docking Chassis/Motherboard Fan", + "Docking Video Fan", + "Docking Power Supply Fan", + "Docking Chipset Fan", +}; + +static int dell_wmi_ddv_query(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg, + struct wmi_buffer *output, size_t min_size) +{ + __le32 arg2 = cpu_to_le32(arg); + const struct wmi_buffer input = { + .length = sizeof(arg2), + .data = &arg2, + }; + + return wmidev_invoke_method(wdev, 0x0, method, &input, output, min_size); +} + +static int dell_wmi_ddv_query_integer(struct wmi_device *wdev, enum dell_ddv_method method, + u32 arg, u32 *res) +{ + struct wmi_buffer output; + int ret; + + ret = dell_wmi_ddv_query(wdev, method, arg, &output, sizeof(__le32)); + if (ret < 0) + return ret; + + __le32 *argr __free(kfree) = output.data; + + *res = le32_to_cpu(*argr); + + return 0; +} + +static int dell_wmi_ddv_query_buffer(struct wmi_device *wdev, enum dell_ddv_method method, + u32 arg, struct dell_wmi_buffer **result) +{ + struct dell_wmi_buffer *buffer; + struct wmi_buffer output; + size_t buffer_size; + int ret; + + ret = dell_wmi_ddv_query(wdev, method, arg, &output, sizeof(*buffer)); + if (ret < 0) + return ret; + + buffer = output.data; + if (!le32_to_cpu(buffer->raw_size)) { + ret = -ENODATA; + + goto err_free; + } + + buffer_size = struct_size(buffer, raw_data, le32_to_cpu(buffer->raw_size)); + if (buffer_size > output.length) { + dev_warn(&wdev->dev, + FW_WARN "Dell WMI buffer size (%zu) exceeds WMI buffer size (%zu)\n", + buffer_size, output.length); + ret = -EMSGSIZE; + + goto err_free; + } + + *result = buffer; + + return 0; + +err_free: + kfree(output.data); + + return ret; +} + +static ssize_t dell_wmi_ddv_query_string(struct wmi_device *wdev, enum dell_ddv_method method, + u32 arg, char *buf, size_t length) +{ + struct wmi_buffer output; + size_t str_size; + int ret; + + ret = dell_wmi_ddv_query(wdev, method, arg, &output, sizeof(struct wmi_string)); + if (ret < 0) + return ret; + + struct wmi_string *str __free(kfree) = output.data; + + str_size = sizeof(*str) + le16_to_cpu(str->length); + if (str_size > output.length) { + dev_warn(&wdev->dev, + FW_WARN "WMI string size (%zu) exceeds WMI buffer size (%zu)\n", + str_size, output.length); + return -EMSGSIZE; + } + + return wmi_string_to_utf8s(str, buf, length); +} + +/* + * Needs to be called with lock held, except during initialization. + */ +static int dell_wmi_ddv_update_sensors(struct wmi_device *wdev, enum dell_ddv_method method, + struct dell_wmi_ddv_sensors *sensors, size_t entry_size) +{ + struct dell_wmi_buffer *buffer; + u64 buffer_size, rem, entries; + int ret; + + if (sensors->buffer) { + if (time_before(jiffies, sensors->timestamp + HZ)) + return 0; + + kfree(sensors->buffer); + sensors->buffer = NULL; + } + + ret = dell_wmi_ddv_query_buffer(wdev, method, 0, &buffer); + if (ret < 0) + return ret; + + /* buffer format sanity check */ + buffer_size = le32_to_cpu(buffer->raw_size); + entries = div64_u64_rem(buffer_size, entry_size, &rem); + if (rem != 1 || buffer->raw_data[buffer_size - 1] != 0xff) { + ret = -ENOMSG; + goto err_free; + } + + if (!entries) { + ret = -ENODATA; + goto err_free; + } + + sensors->buffer = buffer; + sensors->entries = entries; + sensors->timestamp = jiffies; + + return 0; + +err_free: + kfree(buffer); + + return ret; +} + +static umode_t dell_wmi_ddv_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, + int channel) +{ + return 0444; +} + +static int dell_wmi_ddv_fan_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel, + long *val) +{ + struct fan_sensor_entry *entry; + int ret; + + ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION, + &data->fans, sizeof(*entry)); + if (ret < 0) + return ret; + + if (channel >= data->fans.entries) + return -ENXIO; + + entry = (struct fan_sensor_entry *)data->fans.buffer->raw_data; + switch (attr) { + case hwmon_fan_input: + *val = get_unaligned_le16(&entry[channel].rpm); + return 0; + default: + break; + } + + return -EOPNOTSUPP; +} + +static int dell_wmi_ddv_temp_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel, + long *val) +{ + struct thermal_sensor_entry *entry; + int ret; + + ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, + &data->temps, sizeof(*entry)); + if (ret < 0) + return ret; + + if (channel >= data->temps.entries) + return -ENXIO; + + entry = (struct thermal_sensor_entry *)data->temps.buffer->raw_data; + switch (attr) { + case hwmon_temp_input: + *val = entry[channel].now * 1000; + return 0; + case hwmon_temp_min: + *val = entry[channel].min * 1000; + return 0; + case hwmon_temp_max: + *val = entry[channel].max * 1000; + return 0; + default: + break; + } + + return -EOPNOTSUPP; +} + +static int dell_wmi_ddv_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long *val) +{ + struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); + int ret; + + switch (type) { + case hwmon_fan: + mutex_lock(&data->fans.lock); + ret = dell_wmi_ddv_fan_read_channel(data, attr, channel, val); + mutex_unlock(&data->fans.lock); + return ret; + case hwmon_temp: + mutex_lock(&data->temps.lock); + ret = dell_wmi_ddv_temp_read_channel(data, attr, channel, val); + mutex_unlock(&data->temps.lock); + return ret; + default: + break; + } + + return -EOPNOTSUPP; +} + +static int dell_wmi_ddv_fan_read_string(struct dell_wmi_ddv_data *data, int channel, + const char **str) +{ + struct fan_sensor_entry *entry; + int ret; + u8 type; + + ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION, + &data->fans, sizeof(*entry)); + if (ret < 0) + return ret; + + if (channel >= data->fans.entries) + return -ENXIO; + + entry = (struct fan_sensor_entry *)data->fans.buffer->raw_data; + type = entry[channel].type; + switch (type) { + case 0x00 ... 0x07: + *str = fan_labels[type]; + break; + case 0x11 ... 0x14: + *str = fan_dock_labels[type - 0x11]; + break; + default: + *str = "Unknown Fan"; + break; + } + + return 0; +} + +static int dell_wmi_ddv_temp_read_string(struct dell_wmi_ddv_data *data, int channel, + const char **str) +{ + struct thermal_sensor_entry *entry; + int ret; + + ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, + &data->temps, sizeof(*entry)); + if (ret < 0) + return ret; + + if (channel >= data->temps.entries) + return -ENXIO; + + entry = (struct thermal_sensor_entry *)data->temps.buffer->raw_data; + switch (entry[channel].type) { + case 0x00: + *str = "CPU"; + break; + case 0x11: + *str = "Video"; + break; + case 0x22: + *str = "Memory"; /* sometimes called DIMM */ + break; + case 0x33: + *str = "Other"; + break; + case 0x44: + *str = "Ambient"; /* sometimes called SKIN */ + break; + case 0x52: + *str = "SODIMM"; + break; + case 0x55: + *str = "HDD"; + break; + case 0x62: + *str = "SODIMM 2"; + break; + case 0x73: + *str = "NB"; + break; + case 0x83: + *str = "Charger"; + break; + case 0xbb: + *str = "Memory 3"; + break; + default: + *str = "Unknown"; + break; + } + + return 0; +} + +static int dell_wmi_ddv_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); + int ret; + + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_label: + mutex_lock(&data->fans.lock); + ret = dell_wmi_ddv_fan_read_string(data, channel, str); + mutex_unlock(&data->fans.lock); + return ret; + default: + break; + } + break; + case hwmon_temp: + switch (attr) { + case hwmon_temp_label: + mutex_lock(&data->temps.lock); + ret = dell_wmi_ddv_temp_read_string(data, channel, str); + mutex_unlock(&data->temps.lock); + return ret; + default: + break; + } + break; + default: + break; + } + + return -EOPNOTSUPP; +} + +static const struct hwmon_ops dell_wmi_ddv_ops = { + .is_visible = dell_wmi_ddv_is_visible, + .read = dell_wmi_ddv_read, + .read_string = dell_wmi_ddv_read_string, +}; + +static struct hwmon_channel_info *dell_wmi_ddv_channel_create(struct device *dev, u64 count, + enum hwmon_sensor_types type, + u32 config) +{ + struct combined_channel_info *cinfo; + int i; + + cinfo = devm_kzalloc(dev, struct_size(cinfo, config, count + 1), GFP_KERNEL); + if (!cinfo) + return ERR_PTR(-ENOMEM); + + cinfo->info.type = type; + cinfo->info.config = cinfo->config; + + for (i = 0; i < count; i++) + cinfo->config[i] = config; + + return &cinfo->info; +} + +static void dell_wmi_ddv_hwmon_cache_invalidate(struct dell_wmi_ddv_sensors *sensors) +{ + if (!sensors->active) + return; + + mutex_lock(&sensors->lock); + kfree(sensors->buffer); + sensors->buffer = NULL; + mutex_unlock(&sensors->lock); +} + +static void dell_wmi_ddv_hwmon_cache_destroy(void *data) +{ + struct dell_wmi_ddv_sensors *sensors = data; + + sensors->active = false; + mutex_destroy(&sensors->lock); + kfree(sensors->buffer); +} + +static struct hwmon_channel_info *dell_wmi_ddv_channel_init(struct wmi_device *wdev, + enum dell_ddv_method method, + struct dell_wmi_ddv_sensors *sensors, + size_t entry_size, + enum hwmon_sensor_types type, + u32 config) +{ + struct hwmon_channel_info *info; + int ret; + + ret = dell_wmi_ddv_update_sensors(wdev, method, sensors, entry_size); + if (ret < 0) + return ERR_PTR(ret); + + mutex_init(&sensors->lock); + sensors->active = true; + + ret = devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors); + if (ret < 0) + return ERR_PTR(ret); + + info = dell_wmi_ddv_channel_create(&wdev->dev, sensors->entries, type, config); + if (IS_ERR(info)) + devm_release_action(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors); + + return info; +} + +static int dell_wmi_ddv_hwmon_add(struct dell_wmi_ddv_data *data) +{ + struct wmi_device *wdev = data->wdev; + struct combined_chip_info *cinfo; + struct hwmon_channel_info *info; + struct device *hdev; + int index = 0; + int ret; + + if (!devres_open_group(&wdev->dev, dell_wmi_ddv_hwmon_add, GFP_KERNEL)) + return -ENOMEM; + + cinfo = devm_kzalloc(&wdev->dev, struct_size(cinfo, info, 4), GFP_KERNEL); + if (!cinfo) { + ret = -ENOMEM; + + goto err_release; + } + + cinfo->chip.ops = &dell_wmi_ddv_ops; + cinfo->chip.info = cinfo->info; + + info = dell_wmi_ddv_channel_create(&wdev->dev, 1, hwmon_chip, HWMON_C_REGISTER_TZ); + if (IS_ERR(info)) { + ret = PTR_ERR(info); + + goto err_release; + } + + cinfo->info[index] = info; + index++; + + info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_FAN_SENSOR_INFORMATION, &data->fans, + sizeof(struct fan_sensor_entry), hwmon_fan, + (HWMON_F_INPUT | HWMON_F_LABEL)); + if (!IS_ERR(info)) { + cinfo->info[index] = info; + index++; + } + + info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, &data->temps, + sizeof(struct thermal_sensor_entry), hwmon_temp, + (HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX | + HWMON_T_LABEL)); + if (!IS_ERR(info)) { + cinfo->info[index] = info; + index++; + } + + if (index < 2) { + /* Finding no available sensors is not an error */ + ret = 0; + + goto err_release; + } + + hdev = devm_hwmon_device_register_with_info(&wdev->dev, "dell_ddv", data, &cinfo->chip, + NULL); + if (IS_ERR(hdev)) { + ret = PTR_ERR(hdev); + + goto err_release; + } + + devres_close_group(&wdev->dev, dell_wmi_ddv_hwmon_add); + + return 0; + +err_release: + devres_release_group(&wdev->dev, dell_wmi_ddv_hwmon_add); + + return ret; +} + +static int dell_wmi_ddv_battery_translate(struct dell_wmi_ddv_data *data, + struct power_supply *battery, u32 *index) +{ + u32 serial_dec, serial_hex, serial; + union power_supply_propval val; + int ret; + + guard(mutex)(&data->translation_cache_lock); + + for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) { + if (data->translation_cache[i] == battery) { + dev_dbg(&data->wdev->dev, "Translation cache hit for battery index %u\n", + i + 1); + *index = i + 1; + return 0; + } + } + + dev_dbg(&data->wdev->dev, "Translation cache miss\n"); + + /* + * Perform a translation between a ACPI battery and a battery index. + * We have to use power_supply_get_property_direct() here because this + * function will also get called from the callbacks of the power supply + * extension. + */ + ret = power_supply_get_property_direct(battery, POWER_SUPPLY_PROP_SERIAL_NUMBER, &val); + if (ret < 0) + return ret; + + /* + * Some devices display the serial number of the ACPI battery (string!) as a decimal + * number while other devices display it as a hexadecimal number. Because of this we + * have to check both cases. + */ + ret = kstrtou32(val.strval, 16, &serial_hex); + if (ret < 0) + return ret; /* Should never fail */ + + ret = kstrtou32(val.strval, 10, &serial_dec); + if (ret < 0) + serial_dec = 0; /* Can fail, thus we only mark serial_dec as invalid */ + + for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) { + ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_SERIAL_NUMBER, i + 1, + &serial); + if (ret < 0) + return ret; + + /* A serial number of 0 signals that this index is not associated with a battery */ + if (!serial) + continue; + + if (serial == serial_dec || serial == serial_hex) { + dev_dbg(&data->wdev->dev, "Translation cache update for battery index %u\n", + i + 1); + data->translation_cache[i] = battery; + *index = i + 1; + return 0; + } + } + + return -ENODEV; +} + +static void dell_wmi_battery_invalidate(struct dell_wmi_ddv_data *data, + struct power_supply *battery) +{ + guard(mutex)(&data->translation_cache_lock); + + for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) { + if (data->translation_cache[i] == battery) { + data->translation_cache[i] = NULL; + return; + } + } +} + +static ssize_t eppid_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct dell_wmi_ddv_data *data = container_of(attr, struct dell_wmi_ddv_data, eppid_attr); + ssize_t count; + u32 index; + int ret; + + ret = dell_wmi_ddv_battery_translate(data, to_power_supply(dev), &index); + if (ret < 0) + return ret; + + count = dell_wmi_ddv_query_string(data->wdev, DELL_DDV_BATTERY_EPPID, index, buf, + PAGE_SIZE); + if (count < 0) + return count; + + if (count != DELL_EPPID_LENGTH && count != DELL_EPPID_EXT_LENGTH) + dev_info_once(&data->wdev->dev, FW_INFO "Suspicious ePPID length (%zd)\n", count); + + ret = sysfs_emit_at(buf, count, "\n"); + if (ret < 0) + return ret; + + return count + ret; +} + +static int dell_wmi_ddv_get_health(struct dell_wmi_ddv_data *data, u32 index, + union power_supply_propval *val) +{ + u32 value, code; + int ret; + + ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_MANUFACTURER_ACCESS, index, + &value); + if (ret < 0) + return ret; + + switch (FIELD_GET(MA_FAILURE_MODE_MASK, value)) { + case MA_FAILURE_MODE_PERMANENT: + code = FIELD_GET(MA_PERMANENT_FAILURE_CODE_MASK, value); + switch (code) { + case MA_PERMANENT_FAILURE_FUSE_BLOWN: + val->intval = POWER_SUPPLY_HEALTH_BLOWN_FUSE; + return 0; + case MA_PERMANENT_FAILURE_CELL_IMBALANCE: + val->intval = POWER_SUPPLY_HEALTH_CELL_IMBALANCE; + return 0; + case MA_PERMANENT_FAILURE_OVERVOLTAGE: + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + return 0; + case MA_PERMANENT_FAILURE_FET_FAILURE: + val->intval = POWER_SUPPLY_HEALTH_DEAD; + return 0; + default: + dev_notice_once(&data->wdev->dev, "Unknown permanent failure code %u\n", + code); + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + case MA_FAILURE_MODE_OVERHEAT: + code = FIELD_GET(MA_OVERHEAT_FAILURE_CODE_MASK, value); + switch (code) { + case MA_OVERHEAT_FAILURE_START: + case MA_OVERHEAT_FAILURE_CHARGING: + case MA_OVERHEAT_FAILURE_DISCHARGING: + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + return 0; + default: + dev_notice_once(&data->wdev->dev, "Unknown overheat failure code %u\n", + code); + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + case MA_FAILURE_MODE_OVERCURRENT: + code = FIELD_GET(MA_OVERCURRENT_FAILURE_CODE_MASK, value); + switch (code) { + case MA_OVERCURRENT_FAILURE_CHARGING: + case MA_OVERCURRENT_FAILURE_DISCHARGING: + val->intval = POWER_SUPPLY_HEALTH_OVERCURRENT; + return 0; + default: + dev_notice_once(&data->wdev->dev, "Unknown overcurrent failure code %u\n", + code); + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + default: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return 0; + } +} + +static int dell_wmi_ddv_get_manufacture_date(struct dell_wmi_ddv_data *data, u32 index, + enum power_supply_property psp, + union power_supply_propval *val) +{ + u16 year, month, day; + u32 value; + int ret; + + ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_MANUFACTURE_DATE, + index, &value); + if (ret < 0) + return ret; + if (value > U16_MAX) + return -ENXIO; + + /* + * Some devices report a invalid manufacture date value + * like 0.0.1980. Because of this we have to check the + * whole value before exposing parts of it to user space. + */ + year = FIELD_GET(SBS_MANUFACTURE_YEAR_MASK, value) + 1980; + month = FIELD_GET(SBS_MANUFACTURE_MONTH_MASK, value); + if (month < 1 || month > 12) + return -ENODATA; + + day = FIELD_GET(SBS_MANUFACTURE_DAY_MASK, value); + if (day < 1 || day > 31) + return -ENODATA; + + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + val->intval = year; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + val->intval = month; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + val->intval = day; + return 0; + default: + return -EINVAL; + } +} + +static int dell_wmi_ddv_get_property(struct power_supply *psy, const struct power_supply_ext *ext, + void *drvdata, enum power_supply_property psp, + union power_supply_propval *val) +{ + struct dell_wmi_ddv_data *data = drvdata; + u32 index, value; + int ret; + + ret = dell_wmi_ddv_battery_translate(data, psy, &index); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + return dell_wmi_ddv_get_health(data, index, val); + case POWER_SUPPLY_PROP_TEMP: + ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_TEMPERATURE, index, + &value); + if (ret < 0) + return ret; + + /* Use 2732 instead of 2731.5 to avoid unnecessary rounding and to emulate + * the behaviour of the OEM application which seems to round down the result. + */ + val->intval = value - 2732; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + return dell_wmi_ddv_get_manufacture_date(data, index, psp, val); + default: + return -EINVAL; + } +} + +static const enum power_supply_property dell_wmi_ddv_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_MANUFACTURE_YEAR, + POWER_SUPPLY_PROP_MANUFACTURE_MONTH, + POWER_SUPPLY_PROP_MANUFACTURE_DAY, +}; + +static const struct power_supply_ext dell_wmi_ddv_extension = { + .name = DRIVER_NAME, + .properties = dell_wmi_ddv_properties, + .num_properties = ARRAY_SIZE(dell_wmi_ddv_properties), + .get_property = dell_wmi_ddv_get_property, +}; + +static int dell_wmi_ddv_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook); + int ret; + + /* + * We cannot do the battery matching here since the battery might be absent, preventing + * us from reading the serial number. + */ + + ret = device_create_file(&battery->dev, &data->eppid_attr); + if (ret < 0) + return ret; + + ret = power_supply_register_extension(battery, &dell_wmi_ddv_extension, &data->wdev->dev, + data); + if (ret < 0) { + device_remove_file(&battery->dev, &data->eppid_attr); + + return ret; + } + + return 0; +} + +static int dell_wmi_ddv_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook); + + device_remove_file(&battery->dev, &data->eppid_attr); + power_supply_unregister_extension(battery, &dell_wmi_ddv_extension); + + dell_wmi_battery_invalidate(data, battery); + + return 0; +} + +static int dell_wmi_ddv_battery_add(struct dell_wmi_ddv_data *data) +{ + int ret; + + ret = devm_mutex_init(&data->wdev->dev, &data->translation_cache_lock); + if (ret < 0) + return ret; + + data->hook.name = "Dell DDV Battery Extension"; + data->hook.add_battery = dell_wmi_ddv_add_battery; + data->hook.remove_battery = dell_wmi_ddv_remove_battery; + + sysfs_attr_init(&data->eppid_attr.attr); + data->eppid_attr.attr.name = "eppid"; + data->eppid_attr.attr.mode = 0444; + data->eppid_attr.show = eppid_show; + + return devm_battery_hook_register(&data->wdev->dev, &data->hook); +} + +static int dell_wmi_ddv_buffer_read(struct seq_file *seq, enum dell_ddv_method method) +{ + struct device *dev = seq->private; + struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); + struct dell_wmi_buffer *buffer; + int ret; + + ret = dell_wmi_ddv_query_buffer(data->wdev, method, 0, &buffer); + if (ret < 0) + return ret; + + ret = seq_write(seq, buffer->raw_data, le32_to_cpu(buffer->raw_size)); + kfree(buffer); + + return ret; +} + +static int dell_wmi_ddv_fan_read(struct seq_file *seq, void *offset) +{ + return dell_wmi_ddv_buffer_read(seq, DELL_DDV_FAN_SENSOR_INFORMATION); +} + +static int dell_wmi_ddv_temp_read(struct seq_file *seq, void *offset) +{ + return dell_wmi_ddv_buffer_read(seq, DELL_DDV_THERMAL_SENSOR_INFORMATION); +} + +static void dell_wmi_ddv_debugfs_remove(void *data) +{ + struct dentry *entry = data; + + debugfs_remove(entry); +} + +static void dell_wmi_ddv_debugfs_init(struct wmi_device *wdev) +{ + struct dentry *entry; + char name[64]; + + scnprintf(name, ARRAY_SIZE(name), "%s-%s", DRIVER_NAME, dev_name(&wdev->dev)); + entry = debugfs_create_dir(name, NULL); + + debugfs_create_devm_seqfile(&wdev->dev, "fan_sensor_information", entry, + dell_wmi_ddv_fan_read); + debugfs_create_devm_seqfile(&wdev->dev, "thermal_sensor_information", entry, + dell_wmi_ddv_temp_read); + + devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_debugfs_remove, entry); +} + +static int dell_wmi_ddv_probe(struct wmi_device *wdev, const void *context) +{ + struct dell_wmi_ddv_data *data; + u32 version; + int ret; + + ret = dell_wmi_ddv_query_integer(wdev, DELL_DDV_INTERFACE_VERSION, 0, &version); + if (ret < 0) + return ret; + + dev_dbg(&wdev->dev, "WMI interface version: %d\n", version); + if (version < DELL_DDV_SUPPORTED_VERSION_MIN || version > DELL_DDV_SUPPORTED_VERSION_MAX) { + if (!force) + return -ENODEV; + + dev_warn(&wdev->dev, "Loading despite unsupported WMI interface version (%u)\n", + version); + } + + data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, data); + data->wdev = wdev; + + dell_wmi_ddv_debugfs_init(wdev); + + if (IS_REACHABLE(CONFIG_ACPI_BATTERY)) { + ret = dell_wmi_ddv_battery_add(data); + if (ret < 0) + dev_warn(&wdev->dev, "Unable to register ACPI battery hook: %d\n", ret); + } + + if (IS_REACHABLE(CONFIG_HWMON)) { + ret = dell_wmi_ddv_hwmon_add(data); + if (ret < 0) + dev_warn(&wdev->dev, "Unable to register hwmon interface: %d\n", ret); + } + + return 0; +} + +static int dell_wmi_ddv_resume(struct device *dev) +{ + struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); + + /* Force re-reading of all active sensors */ + dell_wmi_ddv_hwmon_cache_invalidate(&data->fans); + dell_wmi_ddv_hwmon_cache_invalidate(&data->temps); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(dell_wmi_ddv_dev_pm_ops, NULL, dell_wmi_ddv_resume); + +static const struct wmi_device_id dell_wmi_ddv_id_table[] = { + { DELL_DDV_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, dell_wmi_ddv_id_table); + +static struct wmi_driver dell_wmi_ddv_driver = { + .driver = { + .name = DRIVER_NAME, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .pm = pm_sleep_ptr(&dell_wmi_ddv_dev_pm_ops), + }, + .id_table = dell_wmi_ddv_id_table, + .probe = dell_wmi_ddv_probe, + .no_singleton = true, +}; +module_wmi_driver(dell_wmi_ddv_driver); + +MODULE_AUTHOR("Armin Wolf "); +MODULE_DESCRIPTION("Dell WMI sensor driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-wmi-descriptor.c b/drivers/platform/x86/dell/dell-wmi-descriptor.c new file mode 100644 index 000000000..179d5678b --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-descriptor.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Dell WMI descriptor driver + * + * Copyright (C) 2017 Dell Inc. All Rights Reserved. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include "dell-wmi-descriptor.h" + +#define DELL_WMI_DESCRIPTOR_GUID "8D9DDCBC-A997-11DA-B012-B622A1EF5492" + +/* + * Descriptor buffer is 128 byte long and contains: + * + * Name Offset Length Value + * Vendor Signature 0 4 "DELL" + * Object Signature 4 4 " WMI" + * WMI Interface Version 8 4 + * WMI buffer length 12 4 + * WMI hotfix number 16 4 + */ +struct descriptor { + /* Both fields are NOT null-terminated */ + char vendor_signature[4] __nonstring; + char object_signature[4] __nonstring; + __le32 interface_version; + __le32 buffer_length; + __le32 hotfix_number; +} __packed; + +struct descriptor_priv { + struct list_head list; + u32 interface_version; + u32 size; + u32 hotfix; +}; +static int descriptor_valid = -EPROBE_DEFER; +static LIST_HEAD(wmi_list); +static DEFINE_MUTEX(list_mutex); + +int dell_wmi_get_descriptor_valid(void) +{ + if (!wmi_has_guid(DELL_WMI_DESCRIPTOR_GUID)) + return -ENODEV; + + return descriptor_valid; +} +EXPORT_SYMBOL_GPL(dell_wmi_get_descriptor_valid); + +bool dell_wmi_get_interface_version(u32 *version) +{ + struct descriptor_priv *priv; + bool ret = false; + + mutex_lock(&list_mutex); + priv = list_first_entry_or_null(&wmi_list, + struct descriptor_priv, + list); + if (priv) { + *version = priv->interface_version; + ret = true; + } + mutex_unlock(&list_mutex); + return ret; +} +EXPORT_SYMBOL_GPL(dell_wmi_get_interface_version); + +bool dell_wmi_get_size(u32 *size) +{ + struct descriptor_priv *priv; + bool ret = false; + + mutex_lock(&list_mutex); + priv = list_first_entry_or_null(&wmi_list, + struct descriptor_priv, + list); + if (priv) { + *size = priv->size; + ret = true; + } + mutex_unlock(&list_mutex); + return ret; +} +EXPORT_SYMBOL_GPL(dell_wmi_get_size); + +bool dell_wmi_get_hotfix(u32 *hotfix) +{ + struct descriptor_priv *priv; + bool ret = false; + + mutex_lock(&list_mutex); + priv = list_first_entry_or_null(&wmi_list, + struct descriptor_priv, + list); + if (priv) { + *hotfix = priv->hotfix; + ret = true; + } + mutex_unlock(&list_mutex); + return ret; +} +EXPORT_SYMBOL_GPL(dell_wmi_get_hotfix); + +static int dell_wmi_descriptor_probe(struct wmi_device *wdev, const void *context) +{ + struct descriptor_priv *priv; + struct wmi_buffer buffer; + int ret; + + ret = wmidev_query_block(wdev, 0, &buffer, sizeof(struct descriptor)); + if (ret < 0) { + descriptor_valid = ret; + return ret; + } + + struct descriptor *desc __free(kfree) = buffer.data; + + if (strncmp(desc->vendor_signature, "DELL", sizeof(desc->vendor_signature))) { + dev_err(&wdev->dev, "Dell descriptor buffer has invalid vendor signature (%4ph)\n", + desc->vendor_signature); + descriptor_valid = -ENOMSG; + return -ENOMSG; + } + + if (strncmp(desc->object_signature, " WMI", sizeof(desc->object_signature))) { + dev_err(&wdev->dev, "Dell descriptor buffer has invalid object signature (%4ph)\n", + desc->object_signature); + descriptor_valid = -ENOMSG; + return -ENOMSG; + } + descriptor_valid = 0; + + if (le32_to_cpu(desc->interface_version) > 1) + dev_warn(&wdev->dev, "Dell descriptor buffer has unknown version (%u)\n", + le32_to_cpu(desc->interface_version)); + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->interface_version = le32_to_cpu(desc->interface_version); + priv->size = le32_to_cpu(desc->buffer_length); + priv->hotfix = le32_to_cpu(desc->hotfix_number); + dev_set_drvdata(&wdev->dev, priv); + mutex_lock(&list_mutex); + list_add_tail(&priv->list, &wmi_list); + mutex_unlock(&list_mutex); + + dev_dbg(&wdev->dev, "Detected Dell WMI interface version %lu, buffer size %lu, hotfix %lu\n", + (unsigned long) priv->interface_version, + (unsigned long) priv->size, + (unsigned long) priv->hotfix); + + return ret; +} + +static void dell_wmi_descriptor_remove(struct wmi_device *wdev) +{ + struct descriptor_priv *priv = dev_get_drvdata(&wdev->dev); + + mutex_lock(&list_mutex); + list_del(&priv->list); + mutex_unlock(&list_mutex); +} + +static const struct wmi_device_id dell_wmi_descriptor_id_table[] = { + { .guid_string = DELL_WMI_DESCRIPTOR_GUID }, + { }, +}; + +static struct wmi_driver dell_wmi_descriptor_driver = { + .driver = { + .name = "dell-wmi-descriptor", + }, + .probe = dell_wmi_descriptor_probe, + .remove = dell_wmi_descriptor_remove, + .id_table = dell_wmi_descriptor_id_table, +}; + +module_wmi_driver(dell_wmi_descriptor_driver); + +MODULE_DEVICE_TABLE(wmi, dell_wmi_descriptor_id_table); +MODULE_AUTHOR("Mario Limonciello "); +MODULE_DESCRIPTION("Dell WMI descriptor driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell-wmi-descriptor.h b/drivers/platform/x86/dell/dell-wmi-descriptor.h new file mode 100644 index 000000000..1f469fef1 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-descriptor.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Dell WMI descriptor driver + * + * Copyright (c) 2017 Dell Inc. + */ + +#ifndef _DELL_WMI_DESCRIPTOR_H_ +#define _DELL_WMI_DESCRIPTOR_H_ + +#include + +/* possible return values: + * -ENODEV: Descriptor GUID missing from WMI bus + * -EPROBE_DEFER: probing for dell-wmi-descriptor not yet run + * 0: valid descriptor, successfully probed + * < 0: invalid descriptor, don't probe dependent devices + */ +int dell_wmi_get_descriptor_valid(void); + +bool dell_wmi_get_interface_version(u32 *version); +bool dell_wmi_get_size(u32 *size); +bool dell_wmi_get_hotfix(u32 *hotfix); + +#endif diff --git a/drivers/platform/x86/dell/dell-wmi-led.c b/drivers/platform/x86/dell/dell-wmi-led.c new file mode 100644 index 000000000..5bedaf7f0 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-led.c @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2010 Dell Inc. + * Louis Davis + * Jim Dailey + * + * 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. + * + */ + +#include +#include +#include +#include + +MODULE_AUTHOR("Louis Davis/Jim Dailey"); +MODULE_DESCRIPTION("Dell LED Control Driver"); +MODULE_LICENSE("GPL"); + +#define DELL_LED_BIOS_GUID "F6E4FE6E-909D-47cb-8BAB-C9F6F2F8D396" +MODULE_ALIAS("wmi:" DELL_LED_BIOS_GUID); + +/* Error Result Codes: */ +#define INVALID_DEVICE_ID 250 +#define INVALID_PARAMETER 251 +#define INVALID_BUFFER 252 +#define INTERFACE_ERROR 253 +#define UNSUPPORTED_COMMAND 254 +#define UNSPECIFIED_ERROR 255 + +/* Device ID */ +#define DEVICE_ID_PANEL_BACK 1 + +/* LED Commands */ +#define CMD_LED_ON 16 +#define CMD_LED_OFF 17 +#define CMD_LED_BLINK 18 + +struct bios_args { + unsigned char length; + unsigned char result_code; + unsigned char device_id; + unsigned char command; + unsigned char on_time; + unsigned char off_time; +}; + +static int dell_led_perform_fn(u8 length, u8 result_code, u8 device_id, + u8 command, u8 on_time, u8 off_time) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct bios_args *bios_return; + struct acpi_buffer input; + union acpi_object *obj; + acpi_status status; + u8 return_code; + + struct bios_args args = { + .length = length, + .result_code = result_code, + .device_id = device_id, + .command = command, + .on_time = on_time, + .off_time = off_time + }; + + input.length = sizeof(struct bios_args); + input.pointer = &args; + + status = wmi_evaluate_method(DELL_LED_BIOS_GUID, 0, 1, &input, &output); + if (ACPI_FAILURE(status)) + return status; + + obj = output.pointer; + + if (!obj) + return -EINVAL; + if (obj->type != ACPI_TYPE_BUFFER) { + kfree(obj); + return -EINVAL; + } + + bios_return = ((struct bios_args *)obj->buffer.pointer); + return_code = bios_return->result_code; + + kfree(obj); + + return return_code; +} + +static int led_on(void) +{ + return dell_led_perform_fn(3, /* Length of command */ + INTERFACE_ERROR, /* Init to INTERFACE_ERROR */ + DEVICE_ID_PANEL_BACK, /* Device ID */ + CMD_LED_ON, /* Command */ + 0, /* not used */ + 0); /* not used */ +} + +static int led_off(void) +{ + return dell_led_perform_fn(3, /* Length of command */ + INTERFACE_ERROR, /* Init to INTERFACE_ERROR */ + DEVICE_ID_PANEL_BACK, /* Device ID */ + CMD_LED_OFF, /* Command */ + 0, /* not used */ + 0); /* not used */ +} + +static int led_blink(unsigned char on_eighths, unsigned char off_eighths) +{ + return dell_led_perform_fn(5, /* Length of command */ + INTERFACE_ERROR, /* Init to INTERFACE_ERROR */ + DEVICE_ID_PANEL_BACK, /* Device ID */ + CMD_LED_BLINK, /* Command */ + on_eighths, /* blink on in eigths of a second */ + off_eighths); /* blink off in eights of a second */ +} + +static void dell_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value == LED_OFF) + led_off(); + else + led_on(); +} + +static int dell_led_blink(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + unsigned long on_eighths; + unsigned long off_eighths; + + /* + * The Dell LED delay is based on 125ms intervals. + * Need to round up to next interval. + */ + + on_eighths = DIV_ROUND_UP(*delay_on, 125); + on_eighths = clamp_t(unsigned long, on_eighths, 1, 255); + *delay_on = on_eighths * 125; + + off_eighths = DIV_ROUND_UP(*delay_off, 125); + off_eighths = clamp_t(unsigned long, off_eighths, 1, 255); + *delay_off = off_eighths * 125; + + led_blink(on_eighths, off_eighths); + + return 0; +} + +static struct led_classdev dell_led = { + .name = "dell::lid", + .brightness = LED_OFF, + .max_brightness = 1, + .brightness_set = dell_led_set, + .blink_set = dell_led_blink, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int __init dell_led_init(void) +{ + int error = 0; + + if (!wmi_has_guid(DELL_LED_BIOS_GUID)) + return -ENODEV; + + error = led_off(); + if (error != 0) + return -ENODEV; + + return led_classdev_register(NULL, &dell_led); +} + +static void __exit dell_led_exit(void) +{ + led_classdev_unregister(&dell_led); + + led_off(); +} + +module_init(dell_led_init); +module_exit(dell_led_exit); diff --git a/drivers/platform/x86/dell/dell-wmi-privacy.c b/drivers/platform/x86/dell/dell-wmi-privacy.c new file mode 100644 index 000000000..f9d275b2f --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-privacy.c @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Dell privacy notification driver + * + * Copyright (C) 2021 Dell Inc. All Rights Reserved. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dell-wmi-privacy.h" + +#define DELL_PRIVACY_GUID "6932965F-1671-4CEB-B988-D3AB0A901919" +#define MICROPHONE_STATUS BIT(0) +#define CAMERA_STATUS BIT(1) +#define DELL_PRIVACY_AUDIO_EVENT 0x1 +#define DELL_PRIVACY_CAMERA_EVENT 0x2 +#define led_to_priv(c) container_of(c, struct privacy_wmi_data, cdev) + +/* + * Describes the Device State class exposed by BIOS which can be consumed by + * various applications interested in knowing the Privacy feature capabilities. + * class DeviceState + * { + * [key, read] string InstanceName; + * [read] boolean ReadOnly; + * + * [WmiDataId(1), read] uint32 DevicesSupported; + * 0 - None; 0x1 - Microphone; 0x2 - Camera; 0x4 - ePrivacy Screen + * + * [WmiDataId(2), read] uint32 CurrentState; + * 0 - Off; 1 - On; Bit0 - Microphone; Bit1 - Camera; Bit2 - ePrivacyScreen + * }; + */ +struct device_state { + __le32 devices_supported; + __le32 current_state; +} __packed; + +/* + * The wmi_list is used to store the privacy_priv struct with mutex protecting + */ +static LIST_HEAD(wmi_list); +static DEFINE_MUTEX(list_mutex); + +struct privacy_wmi_data { + struct input_dev *input_dev; + struct wmi_device *wdev; + struct list_head list; + struct led_classdev cdev; + u32 features_present; + u32 last_status; +}; + +/* DELL Privacy Type */ +enum dell_hardware_privacy_type { + DELL_PRIVACY_TYPE_AUDIO = 0, + DELL_PRIVACY_TYPE_CAMERA, + DELL_PRIVACY_TYPE_SCREEN, + DELL_PRIVACY_TYPE_MAX, +}; + +static const char * const privacy_types[DELL_PRIVACY_TYPE_MAX] = { + [DELL_PRIVACY_TYPE_AUDIO] = "Microphone", + [DELL_PRIVACY_TYPE_CAMERA] = "Camera Shutter", + [DELL_PRIVACY_TYPE_SCREEN] = "ePrivacy Screen", +}; + +/* + * Keymap for WMI privacy events of type 0x0012 + */ +static const struct key_entry dell_wmi_keymap_type_0012[] = { + /* privacy mic mute */ + { KE_KEY, 0x0001, { KEY_MICMUTE } }, + /* privacy camera mute */ + { KE_VSW, 0x0002, { SW_CAMERA_LENS_COVER } }, + { KE_END, 0}, +}; + +bool dell_privacy_has_mic_mute(void) +{ + struct privacy_wmi_data *priv; + + mutex_lock(&list_mutex); + priv = list_first_entry_or_null(&wmi_list, + struct privacy_wmi_data, + list); + mutex_unlock(&list_mutex); + + return priv && (priv->features_present & BIT(DELL_PRIVACY_TYPE_AUDIO)); +} +EXPORT_SYMBOL_GPL(dell_privacy_has_mic_mute); + +/* + * The flow of privacy event: + * 1) User presses key. HW does stuff with this key (timeout is started) + * 2) WMI event is emitted from BIOS + * 3) WMI event is received by dell-privacy + * 4) KEY_MICMUTE emitted from dell-privacy + * 5) Userland picks up key and modifies kcontrol for SW mute + * 6) Codec kernel driver catches and calls ledtrig_audio_set which will call + * led_set_brightness() on the LED registered by dell_privacy_leds_setup() + * 7) dell-privacy notifies EC, the timeout is cancelled and the HW mute activates. + * If the EC is not notified then the HW mic mute will activate when the timeout + * triggers, just a bit later than with the active ack. + */ +bool dell_privacy_process_event(int type, int code, int status) +{ + struct privacy_wmi_data *priv; + const struct key_entry *key; + bool ret = false; + + mutex_lock(&list_mutex); + priv = list_first_entry_or_null(&wmi_list, + struct privacy_wmi_data, + list); + if (!priv) + goto error; + + key = sparse_keymap_entry_from_scancode(priv->input_dev, (type << 16) | code); + if (!key) { + dev_warn(&priv->wdev->dev, "Unknown key with type 0x%04x and code 0x%04x pressed\n", + type, code); + goto error; + } + dev_dbg(&priv->wdev->dev, "Key with type 0x%04x and code 0x%04x pressed\n", type, code); + + switch (code) { + case DELL_PRIVACY_AUDIO_EVENT: /* Mic mute */ + priv->last_status = status; + sparse_keymap_report_entry(priv->input_dev, key, 1, true); + ret = true; + break; + case DELL_PRIVACY_CAMERA_EVENT: /* Camera mute */ + priv->last_status = status; + sparse_keymap_report_entry(priv->input_dev, key, !(status & CAMERA_STATUS), false); + ret = true; + break; + default: + dev_dbg(&priv->wdev->dev, "unknown event type 0x%04x 0x%04x\n", type, code); + } + +error: + mutex_unlock(&list_mutex); + return ret; +} + +static ssize_t dell_privacy_supported_type_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct privacy_wmi_data *priv = dev_get_drvdata(dev); + enum dell_hardware_privacy_type type; + u32 privacy_list; + int len = 0; + + privacy_list = priv->features_present; + for (type = DELL_PRIVACY_TYPE_AUDIO; type < DELL_PRIVACY_TYPE_MAX; type++) { + if (privacy_list & BIT(type)) + len += sysfs_emit_at(buf, len, "[%s] [supported]\n", privacy_types[type]); + else + len += sysfs_emit_at(buf, len, "[%s] [unsupported]\n", privacy_types[type]); + } + + return len; +} + +static ssize_t dell_privacy_current_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct privacy_wmi_data *priv = dev_get_drvdata(dev); + u32 privacy_supported = priv->features_present; + enum dell_hardware_privacy_type type; + u32 privacy_state = priv->last_status; + int len = 0; + + for (type = DELL_PRIVACY_TYPE_AUDIO; type < DELL_PRIVACY_TYPE_MAX; type++) { + if (privacy_supported & BIT(type)) { + if (privacy_state & BIT(type)) + len += sysfs_emit_at(buf, len, "[%s] [unmuted]\n", privacy_types[type]); + else + len += sysfs_emit_at(buf, len, "[%s] [muted]\n", privacy_types[type]); + } + } + + return len; +} + +static DEVICE_ATTR_RO(dell_privacy_supported_type); +static DEVICE_ATTR_RO(dell_privacy_current_state); + +static struct attribute *privacy_attrs[] = { + &dev_attr_dell_privacy_supported_type.attr, + &dev_attr_dell_privacy_current_state.attr, + NULL, +}; +ATTRIBUTE_GROUPS(privacy); + +static int get_current_status(struct wmi_device *wdev) +{ + struct privacy_wmi_data *priv = dev_get_drvdata(&wdev->dev); + struct wmi_buffer buffer; + int ret; + + if (!priv) { + dev_err(&wdev->dev, "dell privacy priv is NULL\n"); + return -EINVAL; + } + + /* check privacy support features and device states */ + ret = wmidev_query_block(wdev, 0, &buffer, sizeof(struct device_state)); + if (ret < 0) + return ret; + + struct device_state *state __free(kfree) = buffer.data; + + priv->features_present = le32_to_cpu(state->devices_supported); + priv->last_status = le32_to_cpu(state->current_state); + + return 0; +} + +static int dell_privacy_micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct privacy_wmi_data *priv = led_to_priv(led_cdev); + static char *acpi_method = (char *)"ECAK"; + acpi_status status; + acpi_handle handle; + + handle = ec_get_handle(); + if (!handle) + return -EIO; + + if (!acpi_has_method(handle, acpi_method)) + return -EIO; + + status = acpi_evaluate_object(handle, acpi_method, NULL, NULL); + if (ACPI_FAILURE(status)) { + dev_err(&priv->wdev->dev, "Error setting privacy EC ack value: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + return 0; +} + +/* + * Pressing the mute key activates a time delayed circuit to physically cut + * off the mute. The LED is in the same circuit, so it reflects the true + * state of the HW mute. The reason for the EC "ack" is so that software + * can first invoke a SW mute before the HW circuit is cut off. Without SW + * cutting this off first does not affect the time delayed muting or status + * of the LED but there is a possibility of a "popping" noise. + * + * If the EC receives the SW ack, the circuit will be activated before the + * delay completed. + * + * Exposing as an LED device allows the codec drivers notification path to + * EC ACK to work + */ +static int dell_privacy_leds_setup(struct device *dev) +{ + struct privacy_wmi_data *priv = dev_get_drvdata(dev); + + priv->cdev.name = "dell-privacy::micmute"; + priv->cdev.max_brightness = 1; + priv->cdev.brightness_set_blocking = dell_privacy_micmute_led_set; + priv->cdev.default_trigger = "audio-micmute"; + return devm_led_classdev_register(dev, &priv->cdev); +} + +static int dell_privacy_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct privacy_wmi_data *priv; + struct key_entry *keymap; + int ret, i, j; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, priv); + priv->wdev = wdev; + + ret = get_current_status(priv->wdev); + if (ret) + return ret; + + /* create evdev passing interface */ + priv->input_dev = devm_input_allocate_device(&wdev->dev); + if (!priv->input_dev) + return -ENOMEM; + + /* remap the wmi keymap event to new keymap */ + keymap = kzalloc_objs(struct key_entry, + ARRAY_SIZE(dell_wmi_keymap_type_0012)); + if (!keymap) + return -ENOMEM; + + /* remap the keymap code with Dell privacy key type 0x12 as prefix + * KEY_MICMUTE scancode will be reported as 0x120001 + */ + for (i = 0, j = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0012); i++) { + /* + * Unlike keys where only presses matter, userspace may act + * on switches in both of their positions. Only register + * SW_CAMERA_LENS_COVER if it is actually there. + */ + if (dell_wmi_keymap_type_0012[i].type == KE_VSW && + dell_wmi_keymap_type_0012[i].sw.code == SW_CAMERA_LENS_COVER && + !(priv->features_present & BIT(DELL_PRIVACY_TYPE_CAMERA))) + continue; + + keymap[j] = dell_wmi_keymap_type_0012[i]; + keymap[j].code |= (0x0012 << 16); + j++; + } + ret = sparse_keymap_setup(priv->input_dev, keymap, NULL); + kfree(keymap); + if (ret) + return ret; + + priv->input_dev->dev.parent = &wdev->dev; + priv->input_dev->name = "Dell Privacy Driver"; + priv->input_dev->id.bustype = BUS_HOST; + + /* Report initial camera-cover status */ + if (priv->features_present & BIT(DELL_PRIVACY_TYPE_CAMERA)) + input_report_switch(priv->input_dev, SW_CAMERA_LENS_COVER, + !(priv->last_status & CAMERA_STATUS)); + + ret = input_register_device(priv->input_dev); + if (ret) + return ret; + + if (priv->features_present & BIT(DELL_PRIVACY_TYPE_AUDIO)) { + ret = dell_privacy_leds_setup(&priv->wdev->dev); + if (ret) + return ret; + } + mutex_lock(&list_mutex); + list_add_tail(&priv->list, &wmi_list); + mutex_unlock(&list_mutex); + return 0; +} + +static void dell_privacy_wmi_remove(struct wmi_device *wdev) +{ + struct privacy_wmi_data *priv = dev_get_drvdata(&wdev->dev); + + mutex_lock(&list_mutex); + list_del(&priv->list); + mutex_unlock(&list_mutex); +} + +static const struct wmi_device_id dell_wmi_privacy_wmi_id_table[] = { + { .guid_string = DELL_PRIVACY_GUID }, + { }, +}; + +static struct wmi_driver dell_privacy_wmi_driver = { + .driver = { + .name = "dell-privacy", + .dev_groups = privacy_groups, + }, + .probe = dell_privacy_wmi_probe, + .remove = dell_privacy_wmi_remove, + .id_table = dell_wmi_privacy_wmi_id_table, +}; + +int dell_privacy_register_driver(void) +{ + return wmi_driver_register(&dell_privacy_wmi_driver); +} + +void dell_privacy_unregister_driver(void) +{ + wmi_driver_unregister(&dell_privacy_wmi_driver); +} diff --git a/drivers/platform/x86/dell/dell-wmi-privacy.h b/drivers/platform/x86/dell/dell-wmi-privacy.h new file mode 100644 index 000000000..ca242e1fe --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-privacy.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Dell privacy notification driver + * + * Copyright (C) 2021 Dell Inc. All Rights Reserved. + */ + +#ifndef _DELL_PRIVACY_WMI_H_ +#define _DELL_PRIVACY_WMI_H_ + +#if IS_ENABLED(CONFIG_DELL_WMI_PRIVACY) +bool dell_privacy_has_mic_mute(void); +bool dell_privacy_process_event(int type, int code, int status); +int dell_privacy_register_driver(void); +void dell_privacy_unregister_driver(void); +#else /* CONFIG_DELL_WMI_PRIVACY */ +static inline bool dell_privacy_has_mic_mute(void) +{ + return false; +} + +static inline bool dell_privacy_process_event(int type, int code, int status) +{ + return false; +} + +static inline int dell_privacy_register_driver(void) +{ + return 0; +} + +static inline void dell_privacy_unregister_driver(void) +{ +} +#endif /* CONFIG_DELL_WMI_PRIVACY */ +#endif diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/Makefile b/drivers/platform/x86/dell/dell-wmi-sysman/Makefile new file mode 100644 index 000000000..0a6df449e --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/Makefile @@ -0,0 +1,8 @@ +obj-$(CONFIG_DELL_WMI_SYSMAN) += dell-wmi-sysman.o +dell-wmi-sysman-y := sysman.o \ + enum-attributes.o \ + int-attributes.o \ + string-attributes.o \ + passobj-attributes.o \ + biosattr-interface.o \ + passwordattr-interface.o diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/biosattr-interface.c b/drivers/platform/x86/dell/dell-wmi-sysman/biosattr-interface.c new file mode 100644 index 000000000..db278ff4c --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/biosattr-interface.c @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to SET methods under BIOS attributes interface GUID for use + * with dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#include +#include "dell-wmi-sysman.h" + +#define SETDEFAULTVALUES_METHOD_ID 0x02 +#define SETBIOSDEFAULTS_METHOD_ID 0x03 +#define SETATTRIBUTE_METHOD_ID 0x04 + +static int call_biosattributes_interface(struct wmi_device *wdev, u8 *in_args, + size_t size, int method_id) +{ + struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL}; + struct acpi_buffer input; + union acpi_object *obj; + acpi_status status; + int ret = -EIO; + + input.length = (acpi_size) size; + input.pointer = in_args; + status = wmidev_evaluate_method(wdev, 0, method_id, &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + obj = (union acpi_object *)output.pointer; + if (obj->type == ACPI_TYPE_INTEGER) + ret = obj->integer.value; + + if (wmi_priv.pending_changes == 0) { + wmi_priv.pending_changes = 1; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&wmi_priv.class_dev->kobj, KOBJ_CHANGE); + } + kfree(output.pointer); + return map_wmi_error(ret); +} + +/** + * set_attribute() - Update an attribute value + * @a_name: The attribute name + * @a_value: The attribute value + * + * Sets an attribute to new value + */ +int set_attribute(const char *a_name, const char *a_value) +{ + size_t security_area_size, buffer_size; + size_t a_name_size, a_value_size; + u8 *buffer = NULL, *start; + int ret; + + mutex_lock(&wmi_priv.mutex); + if (!wmi_priv.bios_attr_wdev) { + ret = -ENODEV; + goto out; + } + + /* build/calculate buffer */ + security_area_size = calculate_security_buffer(wmi_priv.current_admin_password); + a_name_size = calculate_string_buffer(a_name); + a_value_size = calculate_string_buffer(a_value); + buffer_size = security_area_size + a_name_size + a_value_size; + buffer = kzalloc(buffer_size, GFP_KERNEL); + if (!buffer) { + ret = -ENOMEM; + goto out; + } + + /* build security area */ + populate_security_buffer(buffer, wmi_priv.current_admin_password); + + /* build variables to set */ + start = buffer + security_area_size; + ret = populate_string_buffer(start, a_name_size, a_name); + if (ret < 0) + goto out; + start += ret; + ret = populate_string_buffer(start, a_value_size, a_value); + if (ret < 0) + goto out; + + print_hex_dump_bytes("set attribute data: ", DUMP_PREFIX_NONE, buffer, buffer_size); + ret = call_biosattributes_interface(wmi_priv.bios_attr_wdev, + buffer, buffer_size, + SETATTRIBUTE_METHOD_ID); + if (ret == -EOPNOTSUPP) + dev_err(&wmi_priv.bios_attr_wdev->dev, "admin password must be configured\n"); + else if (ret == -EACCES) + dev_err(&wmi_priv.bios_attr_wdev->dev, "invalid password\n"); + +out: + kfree(buffer); + mutex_unlock(&wmi_priv.mutex); + return ret; +} + +/** + * set_bios_defaults() - Resets BIOS defaults + * @deftype: the type of BIOS value reset to issue. + * + * Resets BIOS defaults + */ +int set_bios_defaults(u8 deftype) +{ + size_t security_area_size, buffer_size; + size_t integer_area_size = sizeof(u8); + u8 *buffer = NULL; + u8 *defaultType; + int ret; + + mutex_lock(&wmi_priv.mutex); + if (!wmi_priv.bios_attr_wdev) { + ret = -ENODEV; + goto out; + } + + security_area_size = calculate_security_buffer(wmi_priv.current_admin_password); + buffer_size = security_area_size + integer_area_size; + buffer = kzalloc(buffer_size, GFP_KERNEL); + if (!buffer) { + ret = -ENOMEM; + goto out; + } + + /* build security area */ + populate_security_buffer(buffer, wmi_priv.current_admin_password); + + defaultType = buffer + security_area_size; + *defaultType = deftype; + + ret = call_biosattributes_interface(wmi_priv.bios_attr_wdev, buffer, buffer_size, + SETBIOSDEFAULTS_METHOD_ID); + if (ret) + dev_err(&wmi_priv.bios_attr_wdev->dev, "reset BIOS defaults failed: %d\n", ret); + + kfree(buffer); +out: + mutex_unlock(&wmi_priv.mutex); + return ret; +} + +static int bios_attr_set_interface_probe(struct wmi_device *wdev, const void *context) +{ + mutex_lock(&wmi_priv.mutex); + wmi_priv.bios_attr_wdev = wdev; + mutex_unlock(&wmi_priv.mutex); + return 0; +} + +static void bios_attr_set_interface_remove(struct wmi_device *wdev) +{ + mutex_lock(&wmi_priv.mutex); + wmi_priv.bios_attr_wdev = NULL; + mutex_unlock(&wmi_priv.mutex); +} + +static const struct wmi_device_id bios_attr_set_interface_id_table[] = { + { .guid_string = DELL_WMI_BIOS_ATTRIBUTES_INTERFACE_GUID }, + { }, +}; +static struct wmi_driver bios_attr_set_interface_driver = { + .driver = { + .name = DRIVER_NAME + }, + .probe = bios_attr_set_interface_probe, + .remove = bios_attr_set_interface_remove, + .id_table = bios_attr_set_interface_id_table, +}; + +int init_bios_attr_set_interface(void) +{ + return wmi_driver_register(&bios_attr_set_interface_driver); +} + +void exit_bios_attr_set_interface(void) +{ + wmi_driver_unregister(&bios_attr_set_interface_driver); +} + +MODULE_DEVICE_TABLE(wmi, bios_attr_set_interface_id_table); diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/dell-wmi-sysman.h b/drivers/platform/x86/dell/dell-wmi-sysman/dell-wmi-sysman.h new file mode 100644 index 000000000..3bddedad5 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/dell-wmi-sysman.h @@ -0,0 +1,199 @@ +/* SPDX-License-Identifier: GPL-2.0 + * Definitions for kernel modules using Dell WMI System Management Driver + * + * Copyright (c) 2020 Dell Inc. + */ + +#ifndef _DELL_WMI_BIOS_ATTR_H_ +#define _DELL_WMI_BIOS_ATTR_H_ + +#include +#include +#include +#include +#include + +#define DRIVER_NAME "dell-wmi-sysman" +#define MAX_BUFF 512 + +#define DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF5" +#define DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BFA" +#define DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF9" +#define DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID "0894B8D6-44A6-4719-97D7-6AD24108BFD4" +#define DELL_WMI_BIOS_ATTRIBUTES_INTERFACE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF4" +#define DELL_WMI_BIOS_PASSWORD_INTERFACE_GUID "70FE8229-D03B-4214-A1C6-1F884B1A892A" + +struct enumeration_data { + struct kobject *attr_name_kobj; + char display_name_language_code[MAX_BUFF]; + char dell_value_modifier[MAX_BUFF]; + char possible_values[MAX_BUFF]; + char attribute_name[MAX_BUFF]; + char default_value[MAX_BUFF]; + char dell_modifier[MAX_BUFF]; + char display_name[MAX_BUFF]; +}; + +struct integer_data { + struct kobject *attr_name_kobj; + char display_name_language_code[MAX_BUFF]; + char attribute_name[MAX_BUFF]; + char dell_modifier[MAX_BUFF]; + char display_name[MAX_BUFF]; + int scalar_increment; + int default_value; + int min_value; + int max_value; +}; + +struct str_data { + struct kobject *attr_name_kobj; + char display_name_language_code[MAX_BUFF]; + char attribute_name[MAX_BUFF]; + char display_name[MAX_BUFF]; + char default_value[MAX_BUFF]; + char dell_modifier[MAX_BUFF]; + int min_length; + int max_length; +}; + +struct po_data { + struct kobject *attr_name_kobj; + char attribute_name[MAX_BUFF]; + int min_password_length; + int max_password_length; +}; + +struct wmi_sysman_priv { + char current_admin_password[MAX_BUFF]; + char current_system_password[MAX_BUFF]; + struct wmi_device *password_attr_wdev; + struct wmi_device *bios_attr_wdev; + struct kset *authentication_dir_kset; + struct kset *main_dir_kset; + struct device *class_dev; + struct enumeration_data *enumeration_data; + int enumeration_instances_count; + struct integer_data *integer_data; + int integer_instances_count; + struct str_data *str_data; + int str_instances_count; + struct po_data *po_data; + int po_instances_count; + bool pending_changes; + struct mutex mutex; +}; + +/* global structure used by multiple WMI interfaces */ +extern struct wmi_sysman_priv wmi_priv; + +enum { ENUM, INT, STR, PO }; + +#define ENUM_MIN_ELEMENTS 8 +#define INT_MIN_ELEMENTS 9 +#define STR_MIN_ELEMENTS 8 +#define PO_MIN_ELEMENTS 4 + +enum { + ATTR_NAME, + DISPL_NAME_LANG_CODE, + DISPLAY_NAME, + DEFAULT_VAL, + CURRENT_VAL, + MODIFIER +}; + +#define get_instance_id(type) \ +static int get_##type##_instance_id(struct kobject *kobj) \ +{ \ + int i; \ + for (i = 0; i <= wmi_priv.type##_instances_count; i++) { \ + if (!(strcmp(kobj->name, wmi_priv.type##_data[i].attribute_name)))\ + return i; \ + } \ + return -EIO; \ +} + +#define attribute_s_property_show(name, type) \ +static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \ + char *buf) \ +{ \ + int i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + return sprintf(buf, "%s\n", wmi_priv.type##_data[i].name); \ + return 0; \ +} + +#define attribute_n_property_show(name, type) \ +static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \ + char *buf) \ +{ \ + int i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + return sprintf(buf, "%d\n", wmi_priv.type##_data[i].name); \ + return 0; \ +} + +#define attribute_property_store(curr_val, type) \ +static ssize_t curr_val##_store(struct kobject *kobj, \ + struct kobj_attribute *attr, \ + const char *buf, size_t count) \ +{ \ + char *p, *buf_cp; \ + int i, ret = -EIO; \ + buf_cp = kstrdup(buf, GFP_KERNEL); \ + if (!buf_cp) \ + return -ENOMEM; \ + p = memchr(buf_cp, '\n', count); \ + \ + if (p != NULL) \ + *p = '\0'; \ + i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + ret = validate_##type##_input(i, buf_cp); \ + if (!ret) \ + ret = set_attribute(kobj->name, buf_cp); \ + kfree(buf_cp); \ + return ret ? ret : count; \ +} + +#define check_property_type(attr, prop, valuetype) \ + (attr##_obj[prop].type != valuetype) + +union acpi_object *get_wmiobj_pointer(int instance_id, const char *guid_string); +int get_instance_count(const char *guid_string); +void strlcpy_attr(char *dest, char *src); + +int populate_enum_data(union acpi_object *enumeration_obj, int instance_id, + struct kobject *attr_name_kobj, u32 enum_property_count); +int alloc_enum_data(void); +void exit_enum_attributes(void); + +int populate_int_data(union acpi_object *integer_obj, int instance_id, + struct kobject *attr_name_kobj); +int alloc_int_data(void); +void exit_int_attributes(void); + +int populate_str_data(union acpi_object *str_obj, int instance_id, struct kobject *attr_name_kobj); +int alloc_str_data(void); +void exit_str_attributes(void); + +int populate_po_data(union acpi_object *po_obj, int instance_id, struct kobject *attr_name_kobj); +int alloc_po_data(void); +void exit_po_attributes(void); + +int set_attribute(const char *a_name, const char *a_value); +int set_bios_defaults(u8 defType); + +void exit_bios_attr_set_interface(void); +int init_bios_attr_set_interface(void); +int map_wmi_error(int error_code); +size_t calculate_string_buffer(const char *str); +size_t calculate_security_buffer(const char *authentication); +void populate_security_buffer(u8 *buffer, const char *authentication); +ssize_t populate_string_buffer(u8 *buffer, size_t buffer_len, const char *str); +int set_new_password(const char *password_type, const char *new); +int init_bios_attr_pass_interface(void); +void exit_bios_attr_pass_interface(void); + +#endif diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/enum-attributes.c b/drivers/platform/x86/dell/dell-wmi-sysman/enum-attributes.c new file mode 100644 index 000000000..a85639d8a --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/enum-attributes.c @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to enumeration type attributes under + * BIOS Enumeration GUID for use with dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#include + +#include "dell-wmi-sysman.h" + +get_instance_id(enumeration); + +static int append_enum_string(char *dest, const char *src) +{ + size_t dest_len = strlen(dest); + ssize_t copied; + + if (WARN_ON_ONCE(dest_len >= MAX_BUFF)) + return -EINVAL; + + copied = strscpy(dest + dest_len, src, MAX_BUFF - dest_len); + if (copied < 0) + return -EINVAL; + + dest_len += copied; + copied = strscpy(dest + dest_len, ";", MAX_BUFF - dest_len); + if (copied < 0) + return -EINVAL; + + return 0; +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_enumeration_instance_id(kobj); + union acpi_object *obj; + ssize_t ret; + + if (instance_id < 0) + return instance_id; + + /* need to use specific instance_id and guid combination to get right data */ + obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID); + if (!obj) + return -EIO; + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < ENUM_MIN_ELEMENTS || + obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_STRING) { + kfree(obj); + return -EIO; + } + ret = snprintf(buf, PAGE_SIZE, "%s\n", obj->package.elements[CURRENT_VAL].string.pointer); + kfree(obj); + return ret; +} + +/** + * validate_enumeration_input() - Validate input of current_value against possible values + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_enumeration_input(int instance_id, const char *buf) +{ + char *options, *tmp, *p; + int ret = -EINVAL; + + options = tmp = kstrdup(wmi_priv.enumeration_data[instance_id].possible_values, + GFP_KERNEL); + if (!options) + return -ENOMEM; + + while ((p = strsep(&options, ";")) != NULL) { + if (!*p) + continue; + if (!strcasecmp(p, buf)) { + ret = 0; + break; + } + } + + kfree(tmp); + return ret; +} + +attribute_s_property_show(display_name_language_code, enumeration); +static struct kobj_attribute displ_langcode = + __ATTR_RO(display_name_language_code); + +attribute_s_property_show(display_name, enumeration); +static struct kobj_attribute displ_name = + __ATTR_RO(display_name); + +attribute_s_property_show(default_value, enumeration); +static struct kobj_attribute default_val = + __ATTR_RO(default_value); + +attribute_property_store(current_value, enumeration); +static struct kobj_attribute current_val = + __ATTR_RW_MODE(current_value, 0600); + +attribute_s_property_show(dell_modifier, enumeration); +static struct kobj_attribute modifier = + __ATTR_RO(dell_modifier); + +attribute_s_property_show(dell_value_modifier, enumeration); +static struct kobj_attribute value_modfr = + __ATTR_RO(dell_value_modifier); + +attribute_s_property_show(possible_values, enumeration); +static struct kobj_attribute poss_val = + __ATTR_RO(possible_values); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "enumeration\n"); +} +static struct kobj_attribute type = + __ATTR_RO(type); + +static struct attribute *enumeration_attrs[] = { + &displ_langcode.attr, + &displ_name.attr, + &default_val.attr, + ¤t_val.attr, + &modifier.attr, + &value_modfr.attr, + &poss_val.attr, + &type.attr, + NULL, +}; + +static const struct attribute_group enumeration_attr_group = { + .attrs = enumeration_attrs, +}; + +int alloc_enum_data(void) +{ + int ret = 0; + + wmi_priv.enumeration_instances_count = + get_instance_count(DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID); + wmi_priv.enumeration_data = kzalloc_objs(struct enumeration_data, + wmi_priv.enumeration_instances_count); + if (!wmi_priv.enumeration_data) { + wmi_priv.enumeration_instances_count = 0; + ret = -ENOMEM; + } + return ret; +} + +/** + * populate_enum_data() - Populate all properties of an instance under enumeration attribute + * @enumeration_obj: ACPI object with enumeration data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + * @enum_property_count: Total properties count under enumeration type + */ +int populate_enum_data(union acpi_object *enumeration_obj, int instance_id, + struct kobject *attr_name_kobj, u32 enum_property_count) +{ + int i, next_obj, value_modifier_count, possible_values_count; + + wmi_priv.enumeration_data[instance_id].attr_name_kobj = attr_name_kobj; + if (check_property_type(enumeration, ATTR_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.enumeration_data[instance_id].attribute_name, + enumeration_obj[ATTR_NAME].string.pointer); + if (check_property_type(enumeration, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.enumeration_data[instance_id].display_name_language_code, + enumeration_obj[DISPL_NAME_LANG_CODE].string.pointer); + if (check_property_type(enumeration, DISPLAY_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.enumeration_data[instance_id].display_name, + enumeration_obj[DISPLAY_NAME].string.pointer); + if (check_property_type(enumeration, DEFAULT_VAL, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.enumeration_data[instance_id].default_value, + enumeration_obj[DEFAULT_VAL].string.pointer); + if (check_property_type(enumeration, MODIFIER, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.enumeration_data[instance_id].dell_modifier, + enumeration_obj[MODIFIER].string.pointer); + + next_obj = MODIFIER + 1; + + if (next_obj >= enum_property_count) + return -EINVAL; + + if (check_property_type(enumeration, next_obj, ACPI_TYPE_INTEGER)) + return -EINVAL; + value_modifier_count = (uintptr_t)enumeration_obj[next_obj++].string.pointer; + + for (i = 0; i < value_modifier_count; i++) { + if (next_obj >= enum_property_count) + return -EINVAL; + if (check_property_type(enumeration, next_obj, ACPI_TYPE_STRING)) + return -EINVAL; + if (append_enum_string(wmi_priv.enumeration_data[instance_id].dell_value_modifier, + enumeration_obj[next_obj++].string.pointer)) + return -EINVAL; + } + + if (next_obj >= enum_property_count) + return -EINVAL; + + if (check_property_type(enumeration, next_obj, ACPI_TYPE_INTEGER)) + return -EINVAL; + possible_values_count = (uintptr_t) enumeration_obj[next_obj++].string.pointer; + + for (i = 0; i < possible_values_count; i++) { + if (next_obj >= enum_property_count) + return -EINVAL; + if (check_property_type(enumeration, next_obj, ACPI_TYPE_STRING)) + return -EINVAL; + if (append_enum_string(wmi_priv.enumeration_data[instance_id].possible_values, + enumeration_obj[next_obj++].string.pointer)) + return -EINVAL; + } + + return sysfs_create_group(attr_name_kobj, &enumeration_attr_group); +} + +/** + * exit_enum_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void exit_enum_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < wmi_priv.enumeration_instances_count; instance_id++) { + if (wmi_priv.enumeration_data[instance_id].attr_name_kobj) + sysfs_remove_group(wmi_priv.enumeration_data[instance_id].attr_name_kobj, + &enumeration_attr_group); + } + wmi_priv.enumeration_instances_count = 0; + + kfree(wmi_priv.enumeration_data); + wmi_priv.enumeration_data = NULL; +} diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/int-attributes.c b/drivers/platform/x86/dell/dell-wmi-sysman/int-attributes.c new file mode 100644 index 000000000..ab5c24722 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/int-attributes.c @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to integer type attributes under BIOS Integer GUID for use with + * dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#include "dell-wmi-sysman.h" + +enum int_properties {MIN_VALUE = 6, MAX_VALUE, SCALAR_INCR}; + +get_instance_id(integer); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_integer_instance_id(kobj); + union acpi_object *obj; + ssize_t ret; + + if (instance_id < 0) + return instance_id; + + /* need to use specific instance_id and guid combination to get right data */ + obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID); + if (!obj) + return -EIO; + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < INT_MIN_ELEMENTS || + obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_INTEGER) { + kfree(obj); + return -EIO; + } + ret = snprintf(buf, PAGE_SIZE, "%lld\n", obj->package.elements[CURRENT_VAL].integer.value); + kfree(obj); + return ret; +} + +/** + * validate_integer_input() - Validate input of current_value against lower and upper bound + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_integer_input(int instance_id, char *buf) +{ + int in_val; + int ret; + + ret = kstrtoint(buf, 0, &in_val); + if (ret) + return ret; + if (in_val < wmi_priv.integer_data[instance_id].min_value || + in_val > wmi_priv.integer_data[instance_id].max_value) + return -EINVAL; + + /* workaround for BIOS error. + * validate input to avoid setting 0 when integer input passed with + sign + */ + if (*buf == '+') + memmove(buf, (buf + 1), strlen(buf + 1) + 1); + + return ret; +} + +attribute_s_property_show(display_name_language_code, integer); +static struct kobj_attribute integer_displ_langcode = + __ATTR_RO(display_name_language_code); + +attribute_s_property_show(display_name, integer); +static struct kobj_attribute integer_displ_name = + __ATTR_RO(display_name); + +attribute_n_property_show(default_value, integer); +static struct kobj_attribute integer_default_val = + __ATTR_RO(default_value); + +attribute_property_store(current_value, integer); +static struct kobj_attribute integer_current_val = + __ATTR_RW_MODE(current_value, 0600); + +attribute_s_property_show(dell_modifier, integer); +static struct kobj_attribute integer_modifier = + __ATTR_RO(dell_modifier); + +attribute_n_property_show(min_value, integer); +static struct kobj_attribute integer_lower_bound = + __ATTR_RO(min_value); + +attribute_n_property_show(max_value, integer); +static struct kobj_attribute integer_upper_bound = + __ATTR_RO(max_value); + +attribute_n_property_show(scalar_increment, integer); +static struct kobj_attribute integer_scalar_increment = + __ATTR_RO(scalar_increment); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "integer\n"); +} +static struct kobj_attribute integer_type = + __ATTR_RO(type); + +static struct attribute *integer_attrs[] = { + &integer_displ_langcode.attr, + &integer_displ_name.attr, + &integer_default_val.attr, + &integer_current_val.attr, + &integer_modifier.attr, + &integer_lower_bound.attr, + &integer_upper_bound.attr, + &integer_scalar_increment.attr, + &integer_type.attr, + NULL, +}; + +static const struct attribute_group integer_attr_group = { + .attrs = integer_attrs, +}; + +int alloc_int_data(void) +{ + int ret = 0; + + wmi_priv.integer_instances_count = get_instance_count(DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID); + wmi_priv.integer_data = kzalloc_objs(struct integer_data, + wmi_priv.integer_instances_count); + if (!wmi_priv.integer_data) { + wmi_priv.integer_instances_count = 0; + ret = -ENOMEM; + } + return ret; +} + +/** + * populate_int_data() - Populate all properties of an instance under integer attribute + * @integer_obj: ACPI object with integer data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int populate_int_data(union acpi_object *integer_obj, int instance_id, + struct kobject *attr_name_kobj) +{ + wmi_priv.integer_data[instance_id].attr_name_kobj = attr_name_kobj; + if (check_property_type(integer, ATTR_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.integer_data[instance_id].attribute_name, + integer_obj[ATTR_NAME].string.pointer); + if (check_property_type(integer, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.integer_data[instance_id].display_name_language_code, + integer_obj[DISPL_NAME_LANG_CODE].string.pointer); + if (check_property_type(integer, DISPLAY_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.integer_data[instance_id].display_name, + integer_obj[DISPLAY_NAME].string.pointer); + if (check_property_type(integer, DEFAULT_VAL, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.integer_data[instance_id].default_value = + (uintptr_t)integer_obj[DEFAULT_VAL].string.pointer; + if (check_property_type(integer, MODIFIER, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.integer_data[instance_id].dell_modifier, + integer_obj[MODIFIER].string.pointer); + if (check_property_type(integer, MIN_VALUE, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.integer_data[instance_id].min_value = + (uintptr_t)integer_obj[MIN_VALUE].string.pointer; + if (check_property_type(integer, MAX_VALUE, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.integer_data[instance_id].max_value = + (uintptr_t)integer_obj[MAX_VALUE].string.pointer; + if (check_property_type(integer, SCALAR_INCR, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.integer_data[instance_id].scalar_increment = + (uintptr_t)integer_obj[SCALAR_INCR].string.pointer; + + return sysfs_create_group(attr_name_kobj, &integer_attr_group); +} + +/** + * exit_int_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void exit_int_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < wmi_priv.integer_instances_count; instance_id++) { + if (wmi_priv.integer_data[instance_id].attr_name_kobj) + sysfs_remove_group(wmi_priv.integer_data[instance_id].attr_name_kobj, + &integer_attr_group); + } + wmi_priv.integer_instances_count = 0; + + kfree(wmi_priv.integer_data); + wmi_priv.integer_data = NULL; +} diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/passobj-attributes.c b/drivers/platform/x86/dell/dell-wmi-sysman/passobj-attributes.c new file mode 100644 index 000000000..be939b731 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/passobj-attributes.c @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to password object type attributes under BIOS Password Object GUID for + * use with dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#include "dell-wmi-sysman.h" + +enum po_properties {IS_PASS_SET = 1, MIN_PASS_LEN, MAX_PASS_LEN}; + +get_instance_id(po); + +static ssize_t is_enabled_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + int instance_id = get_po_instance_id(kobj); + union acpi_object *obj; + ssize_t ret; + + if (instance_id < 0) + return instance_id; + + /* need to use specific instance_id and guid combination to get right data */ + obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID); + if (!obj) + return -EIO; + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < PO_MIN_ELEMENTS || + obj->package.elements[IS_PASS_SET].type != ACPI_TYPE_INTEGER) { + kfree(obj); + return -EIO; + } + ret = snprintf(buf, PAGE_SIZE, "%lld\n", obj->package.elements[IS_PASS_SET].integer.value); + kfree(obj); + return ret; +} + +static struct kobj_attribute po_is_pass_set = __ATTR_RO(is_enabled); + +static ssize_t current_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *target = NULL; + int length; + + length = strlen(buf); + if (length && buf[length - 1] == '\n') + length--; + + /* firmware does verifiation of min/max password length, + * hence only check for not exceeding MAX_BUFF here. + */ + if (length >= MAX_BUFF) + return -EINVAL; + + if (strcmp(kobj->name, "Admin") == 0) + target = wmi_priv.current_admin_password; + else if (strcmp(kobj->name, "System") == 0) + target = wmi_priv.current_system_password; + if (!target) + return -EIO; + memcpy(target, buf, length); + target[length] = '\0'; + + return count; +} + +static struct kobj_attribute po_current_password = __ATTR_WO(current_password); + +static ssize_t new_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *p, *buf_cp; + int ret; + + buf_cp = kstrdup(buf, GFP_KERNEL); + if (!buf_cp) + return -ENOMEM; + p = memchr(buf_cp, '\n', count); + + if (p != NULL) + *p = '\0'; + if (strlen(buf_cp) > MAX_BUFF) { + ret = -EINVAL; + goto out; + } + + ret = set_new_password(kobj->name, buf_cp); + +out: + kfree(buf_cp); + return ret ? ret : count; +} + +static struct kobj_attribute po_new_password = __ATTR_WO(new_password); + +attribute_n_property_show(min_password_length, po); +static struct kobj_attribute po_min_pass_length = __ATTR_RO(min_password_length); + +attribute_n_property_show(max_password_length, po); +static struct kobj_attribute po_max_pass_length = __ATTR_RO(max_password_length); + +static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "password\n"); +} + +static struct kobj_attribute po_mechanism = __ATTR_RO(mechanism); + +static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + if (strcmp(kobj->name, "Admin") == 0) + return sprintf(buf, "bios-admin\n"); + else if (strcmp(kobj->name, "System") == 0) + return sprintf(buf, "power-on\n"); + return -EIO; +} + +static struct kobj_attribute po_role = __ATTR_RO(role); + +static struct attribute *po_attrs[] = { + &po_is_pass_set.attr, + &po_min_pass_length.attr, + &po_max_pass_length.attr, + &po_current_password.attr, + &po_new_password.attr, + &po_role.attr, + &po_mechanism.attr, + NULL, +}; + +static const struct attribute_group po_attr_group = { + .attrs = po_attrs, +}; + +int alloc_po_data(void) +{ + int ret = 0; + + wmi_priv.po_instances_count = get_instance_count(DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID); + wmi_priv.po_data = kzalloc_objs(struct po_data, + wmi_priv.po_instances_count); + if (!wmi_priv.po_data) { + wmi_priv.po_instances_count = 0; + ret = -ENOMEM; + } + return ret; +} + +/** + * populate_po_data() - Populate all properties of an instance under password object attribute + * @po_obj: ACPI object with password object data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int populate_po_data(union acpi_object *po_obj, int instance_id, struct kobject *attr_name_kobj) +{ + wmi_priv.po_data[instance_id].attr_name_kobj = attr_name_kobj; + if (check_property_type(po, ATTR_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.po_data[instance_id].attribute_name, + po_obj[ATTR_NAME].string.pointer); + if (check_property_type(po, MIN_PASS_LEN, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.po_data[instance_id].min_password_length = + (uintptr_t)po_obj[MIN_PASS_LEN].string.pointer; + if (check_property_type(po, MAX_PASS_LEN, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.po_data[instance_id].max_password_length = + (uintptr_t) po_obj[MAX_PASS_LEN].string.pointer; + + return sysfs_create_group(attr_name_kobj, &po_attr_group); +} + +/** + * exit_po_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void exit_po_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < wmi_priv.po_instances_count; instance_id++) { + if (wmi_priv.po_data[instance_id].attr_name_kobj) + sysfs_remove_group(wmi_priv.po_data[instance_id].attr_name_kobj, + &po_attr_group); + } + wmi_priv.po_instances_count = 0; + + kfree(wmi_priv.po_data); + wmi_priv.po_data = NULL; +} diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/passwordattr-interface.c b/drivers/platform/x86/dell/dell-wmi-sysman/passwordattr-interface.c new file mode 100644 index 000000000..026c134b3 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/passwordattr-interface.c @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to SET password methods under BIOS attributes interface GUID + * + * Copyright (c) 2020 Dell Inc. + */ + +#include +#include "dell-wmi-sysman.h" + +static int call_password_interface(struct wmi_device *wdev, u8 *in_args, size_t size) +{ + struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL}; + struct acpi_buffer input; + union acpi_object *obj; + acpi_status status; + int ret = -EIO; + + input.length = (acpi_size) size; + input.pointer = in_args; + status = wmidev_evaluate_method(wdev, 0, 1, &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + obj = (union acpi_object *)output.pointer; + if (obj->type == ACPI_TYPE_INTEGER) + ret = obj->integer.value; + + kfree(output.pointer); + /* let userland know it may need to check is_password_set again */ + kobject_uevent(&wmi_priv.class_dev->kobj, KOBJ_CHANGE); + return map_wmi_error(ret); +} + +/** + * set_new_password() - Sets a system admin password + * @password_type: The type of password to set + * @new: The new password + * + * Sets the password using plaintext interface + */ +int set_new_password(const char *password_type, const char *new) +{ + size_t password_type_size, current_password_size, new_size; + size_t security_area_size, buffer_size; + u8 *buffer = NULL, *start; + char *current_password; + int ret; + + mutex_lock(&wmi_priv.mutex); + if (!wmi_priv.password_attr_wdev) { + ret = -ENODEV; + goto out; + } + if (strcmp(password_type, "Admin") == 0) { + current_password = wmi_priv.current_admin_password; + } else if (strcmp(password_type, "System") == 0) { + current_password = wmi_priv.current_system_password; + } else { + ret = -EINVAL; + dev_err(&wmi_priv.password_attr_wdev->dev, "unknown password type %s\n", + password_type); + goto out; + } + + /* build/calculate buffer */ + security_area_size = calculate_security_buffer(wmi_priv.current_admin_password); + password_type_size = calculate_string_buffer(password_type); + current_password_size = calculate_string_buffer(current_password); + new_size = calculate_string_buffer(new); + buffer_size = security_area_size + password_type_size + current_password_size + new_size; + buffer = kzalloc(buffer_size, GFP_KERNEL); + if (!buffer) { + ret = -ENOMEM; + goto out; + } + + /* build security area */ + populate_security_buffer(buffer, wmi_priv.current_admin_password); + + /* build variables to set */ + start = buffer + security_area_size; + ret = populate_string_buffer(start, password_type_size, password_type); + if (ret < 0) + goto out; + + start += ret; + ret = populate_string_buffer(start, current_password_size, current_password); + if (ret < 0) + goto out; + + start += ret; + ret = populate_string_buffer(start, new_size, new); + if (ret < 0) + goto out; + + ret = call_password_interface(wmi_priv.password_attr_wdev, buffer, buffer_size); + /* on success copy the new password to current password */ + if (!ret) + strscpy(current_password, new, MAX_BUFF); + /* explain to user the detailed failure reason */ + else if (ret == -EOPNOTSUPP) + dev_err(&wmi_priv.password_attr_wdev->dev, "admin password must be configured\n"); + else if (ret == -EACCES) + dev_err(&wmi_priv.password_attr_wdev->dev, "invalid password\n"); + +out: + kfree(buffer); + mutex_unlock(&wmi_priv.mutex); + + return ret; +} + +static int bios_attr_pass_interface_probe(struct wmi_device *wdev, const void *context) +{ + mutex_lock(&wmi_priv.mutex); + wmi_priv.password_attr_wdev = wdev; + mutex_unlock(&wmi_priv.mutex); + return 0; +} + +static void bios_attr_pass_interface_remove(struct wmi_device *wdev) +{ + mutex_lock(&wmi_priv.mutex); + wmi_priv.password_attr_wdev = NULL; + mutex_unlock(&wmi_priv.mutex); +} + +static const struct wmi_device_id bios_attr_pass_interface_id_table[] = { + { .guid_string = DELL_WMI_BIOS_PASSWORD_INTERFACE_GUID }, + { }, +}; +static struct wmi_driver bios_attr_pass_interface_driver = { + .driver = { + .name = DRIVER_NAME"-password" + }, + .probe = bios_attr_pass_interface_probe, + .remove = bios_attr_pass_interface_remove, + .id_table = bios_attr_pass_interface_id_table, +}; + +int init_bios_attr_pass_interface(void) +{ + return wmi_driver_register(&bios_attr_pass_interface_driver); +} + +void exit_bios_attr_pass_interface(void) +{ + wmi_driver_unregister(&bios_attr_pass_interface_driver); +} + +MODULE_DEVICE_TABLE(wmi, bios_attr_pass_interface_id_table); diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/string-attributes.c b/drivers/platform/x86/dell/dell-wmi-sysman/string-attributes.c new file mode 100644 index 000000000..41e19f039 --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/string-attributes.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to string type attributes under BIOS String GUID for use with + * dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#include "dell-wmi-sysman.h" + +enum string_properties {MIN_LEN = 6, MAX_LEN}; + +get_instance_id(str); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_str_instance_id(kobj); + union acpi_object *obj; + ssize_t ret; + + if (instance_id < 0) + return -EIO; + + /* need to use specific instance_id and guid combination to get right data */ + obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID); + if (!obj) + return -EIO; + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < STR_MIN_ELEMENTS || + obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_STRING) { + kfree(obj); + return -EIO; + } + ret = snprintf(buf, PAGE_SIZE, "%s\n", obj->package.elements[CURRENT_VAL].string.pointer); + kfree(obj); + return ret; +} + +/** + * validate_str_input() - Validate input of current_value against min and max lengths + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_str_input(int instance_id, const char *buf) +{ + int in_len = strlen(buf); + + if ((in_len < wmi_priv.str_data[instance_id].min_length) || + (in_len > wmi_priv.str_data[instance_id].max_length)) + return -EINVAL; + + return 0; +} + +attribute_s_property_show(display_name_language_code, str); +static struct kobj_attribute str_displ_langcode = + __ATTR_RO(display_name_language_code); + +attribute_s_property_show(display_name, str); +static struct kobj_attribute str_displ_name = + __ATTR_RO(display_name); + +attribute_s_property_show(default_value, str); +static struct kobj_attribute str_default_val = + __ATTR_RO(default_value); + +attribute_property_store(current_value, str); +static struct kobj_attribute str_current_val = + __ATTR_RW_MODE(current_value, 0600); + +attribute_s_property_show(dell_modifier, str); +static struct kobj_attribute str_modifier = + __ATTR_RO(dell_modifier); + +attribute_n_property_show(min_length, str); +static struct kobj_attribute str_min_length = + __ATTR_RO(min_length); + +attribute_n_property_show(max_length, str); +static struct kobj_attribute str_max_length = + __ATTR_RO(max_length); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "string\n"); +} +static struct kobj_attribute str_type = + __ATTR_RO(type); + +static struct attribute *str_attrs[] = { + &str_displ_langcode.attr, + &str_displ_name.attr, + &str_default_val.attr, + &str_current_val.attr, + &str_modifier.attr, + &str_min_length.attr, + &str_max_length.attr, + &str_type.attr, + NULL, +}; + +static const struct attribute_group str_attr_group = { + .attrs = str_attrs, +}; + +int alloc_str_data(void) +{ + int ret = 0; + + wmi_priv.str_instances_count = get_instance_count(DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID); + wmi_priv.str_data = kzalloc_objs(struct str_data, + wmi_priv.str_instances_count); + if (!wmi_priv.str_data) { + wmi_priv.str_instances_count = 0; + ret = -ENOMEM; + } + return ret; +} + +/** + * populate_str_data() - Populate all properties of an instance under string attribute + * @str_obj: ACPI object with string data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int populate_str_data(union acpi_object *str_obj, int instance_id, struct kobject *attr_name_kobj) +{ + wmi_priv.str_data[instance_id].attr_name_kobj = attr_name_kobj; + if (check_property_type(str, ATTR_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.str_data[instance_id].attribute_name, + str_obj[ATTR_NAME].string.pointer); + if (check_property_type(str, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.str_data[instance_id].display_name_language_code, + str_obj[DISPL_NAME_LANG_CODE].string.pointer); + if (check_property_type(str, DISPLAY_NAME, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.str_data[instance_id].display_name, + str_obj[DISPLAY_NAME].string.pointer); + if (check_property_type(str, DEFAULT_VAL, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.str_data[instance_id].default_value, + str_obj[DEFAULT_VAL].string.pointer); + if (check_property_type(str, MODIFIER, ACPI_TYPE_STRING)) + return -EINVAL; + strlcpy_attr(wmi_priv.str_data[instance_id].dell_modifier, + str_obj[MODIFIER].string.pointer); + if (check_property_type(str, MIN_LEN, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.str_data[instance_id].min_length = (uintptr_t)str_obj[MIN_LEN].string.pointer; + if (check_property_type(str, MAX_LEN, ACPI_TYPE_INTEGER)) + return -EINVAL; + wmi_priv.str_data[instance_id].max_length = (uintptr_t) str_obj[MAX_LEN].string.pointer; + + return sysfs_create_group(attr_name_kobj, &str_attr_group); +} + +/** + * exit_str_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void exit_str_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < wmi_priv.str_instances_count; instance_id++) { + if (wmi_priv.str_data[instance_id].attr_name_kobj) + sysfs_remove_group(wmi_priv.str_data[instance_id].attr_name_kobj, + &str_attr_group); + } + wmi_priv.str_instances_count = 0; + + kfree(wmi_priv.str_data); + wmi_priv.str_data = NULL; +} diff --git a/drivers/platform/x86/dell/dell-wmi-sysman/sysman.c b/drivers/platform/x86/dell/dell-wmi-sysman/sysman.c new file mode 100644 index 000000000..ab46a023c --- /dev/null +++ b/drivers/platform/x86/dell/dell-wmi-sysman/sysman.c @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Common methods for use with dell-wmi-sysman + * + * Copyright (c) 2020 Dell Inc. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include "dell-wmi-sysman.h" +#include "../../firmware_attributes_class.h" + +#define MAX_TYPES 4 +#include + +struct wmi_sysman_priv wmi_priv = { + .mutex = __MUTEX_INITIALIZER(wmi_priv.mutex), +}; + +/* reset bios to defaults */ +static const char * const reset_types[] = {"builtinsafe", "lastknowngood", "factory", "custom"}; +static int reset_option = -1; + + +/** + * populate_string_buffer() - populates a string buffer + * @buffer: the start of the destination buffer + * @buffer_len: length of the destination buffer + * @str: the string to insert into buffer + */ +ssize_t populate_string_buffer(u8 *buffer, size_t buffer_len, const char *str) +{ + u16 *length = (u16 *)buffer; + u16 *target = length + 1; + int ret; + + ret = utf8s_to_utf16s(str, strlen(str), UTF16_HOST_ENDIAN, + target, buffer_len - sizeof(u16)); + if (ret < 0) { + dev_err(wmi_priv.class_dev, "UTF16 conversion failed\n"); + return ret; + } + + if ((ret * sizeof(u16)) > U16_MAX) { + dev_err(wmi_priv.class_dev, "Error string too long\n"); + return -ERANGE; + } + + *length = ret * sizeof(u16); + return sizeof(u16) + *length; +} + +/** + * calculate_string_buffer() - determines size of string buffer for use with BIOS communication + * @str: the string to calculate based upon + * + */ +size_t calculate_string_buffer(const char *str) +{ + /* u16 length field + one UTF16 char for each input char */ + return sizeof(u16) + strlen(str) * sizeof(u16); +} + +/** + * calculate_security_buffer() - determines size of security buffer for authentication scheme + * @authentication: the authentication content + * + * Currently only supported type is Admin password + */ +size_t calculate_security_buffer(const char *authentication) +{ + return sizeof(u32) * 2 + ALIGN(strlen(authentication), 2); +} + +/** + * populate_security_buffer() - builds a security buffer for authentication scheme + * @buffer: the buffer to populate + * @authentication: the authentication content + * + * Currently only supported type is PLAIN TEXT + */ +void populate_security_buffer(u8 *buffer, const char *authentication) +{ + size_t seclen = strlen(authentication); + u8 *auth = buffer + sizeof(u32) * 2; + u32 *sectype = (u32 *) buffer; + u32 *seclenp = sectype + 1; + + *sectype = !!seclen; + *seclenp = seclen; + + /* plain text */ + memcpy(auth, authentication, seclen); +} + +/** + * map_wmi_error() - map errors from WMI methods to kernel error codes + * @error_code: integer error code returned from Dell's firmware + */ +int map_wmi_error(int error_code) +{ + switch (error_code) { + case 0: + /* success */ + return 0; + case 1: + /* failed */ + return -EIO; + case 2: + /* invalid parameter */ + return -EINVAL; + case 3: + /* access denied */ + return -EACCES; + case 4: + /* not supported */ + return -EOPNOTSUPP; + case 5: + /* memory error */ + return -ENOMEM; + case 6: + /* protocol error */ + return -EPROTO; + } + /* unspecified error */ + return -EIO; +} + +/** + * reset_bios_show() - sysfs implementaton for read reset_bios + * @kobj: Kernel object for this attribute + * @attr: Kernel object attribute + * @buf: The buffer to display to userspace + */ +static ssize_t reset_bios_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + ssize_t len = 0; + int i; + + for (i = 0; i < MAX_TYPES; i++) { + if (i == reset_option) + len += sysfs_emit_at(buf, len, "[%s] ", reset_types[i]); + else + len += sysfs_emit_at(buf, len, "%s ", reset_types[i]); + } + len += sysfs_emit_at(buf, len, "\n"); + return len; +} + +/** + * reset_bios_store() - sysfs implementaton for write reset_bios + * @kobj: Kernel object for this attribute + * @attr: Kernel object attribute + * @buf: The buffer from userspace + * @count: the size of the buffer from userspace + */ +static ssize_t reset_bios_store(struct kobject *kobj, + struct kobj_attribute *attr, const char *buf, size_t count) +{ + int type = sysfs_match_string(reset_types, buf); + int ret; + + if (type < 0) + return type; + + ret = set_bios_defaults(type); + pr_debug("reset all attributes request type %d: %d\n", type, ret); + if (!ret) { + reset_option = type; + ret = count; + } + + return ret; +} + +/** + * pending_reboot_show() - sysfs implementaton for read pending_reboot + * @kobj: Kernel object for this attribute + * @attr: Kernel object attribute + * @buf: The buffer to display to userspace + * + * Stores default value as 0 + * When current_value is changed this attribute is set to 1 to notify reboot may be required + */ +static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", wmi_priv.pending_changes); +} + +static struct kobj_attribute reset_bios = __ATTR_RW(reset_bios); +static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); + + +/** + * create_attributes_level_sysfs_files() - Creates reset_bios and + * pending_reboot attributes + */ +static int create_attributes_level_sysfs_files(void) +{ + int ret; + + ret = sysfs_create_file(&wmi_priv.main_dir_kset->kobj, &reset_bios.attr); + if (ret) + return ret; + + ret = sysfs_create_file(&wmi_priv.main_dir_kset->kobj, &pending_reboot.attr); + if (ret) + return ret; + + return 0; +} + +static void attr_name_release(struct kobject *kobj) +{ + kfree(kobj); +} + +static const struct kobj_type attr_name_ktype = { + .release = attr_name_release, + .sysfs_ops = &kobj_sysfs_ops, +}; + +/** + * strlcpy_attr - Copy a length-limited, NULL-terminated string with bound checks + * @dest: Where to copy the string to + * @src: Where to copy the string from + */ +void strlcpy_attr(char *dest, char *src) +{ + size_t len = strlen(src) + 1; + + if (len > 1 && len <= MAX_BUFF) + strscpy(dest, src, len); + + /*len can be zero because any property not-applicable to attribute can + * be empty so check only for too long buffers and log error + */ + if (len > MAX_BUFF) + pr_err("Source string returned from BIOS is out of bound!\n"); +} + +/** + * get_wmiobj_pointer() - Get Content of WMI block for particular instance + * @instance_id: WMI instance ID + * @guid_string: WMI GUID (in str form) + * + * Fetches the content for WMI block (instance_id) under GUID (guid_string) + * Caller must kfree the return + */ +union acpi_object *get_wmiobj_pointer(int instance_id, const char *guid_string) +{ + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + status = wmi_query_block(guid_string, instance_id, &out); + + return ACPI_SUCCESS(status) ? (union acpi_object *)out.pointer : NULL; +} + +/** + * get_instance_count() - Compute total number of instances under guid_string + * @guid_string: WMI GUID (in string form) + */ +int get_instance_count(const char *guid_string) +{ + int ret; + + ret = wmi_instance_count(guid_string); + if (ret < 0) + return 0; + + return ret; +} + +/** + * alloc_attributes_data() - Allocate attributes data for a particular type + * @attr_type: Attribute type to allocate + */ +static int alloc_attributes_data(int attr_type) +{ + int retval = 0; + + switch (attr_type) { + case ENUM: + retval = alloc_enum_data(); + break; + case INT: + retval = alloc_int_data(); + break; + case STR: + retval = alloc_str_data(); + break; + case PO: + retval = alloc_po_data(); + break; + default: + break; + } + + return retval; +} + +/** + * destroy_attribute_objs() - Free a kset of kobjects + * @kset: The kset to destroy + * + * Frees kobjects created for each attribute_name under attribute type kset. + */ +static void destroy_attribute_objs(struct kset *kset) +{ + struct kobject *pos, *next; + + list_for_each_entry_safe(pos, next, &kset->list, entry) { + kobject_put(pos); + } +} + +/** + * release_attributes_data() - Clean-up all sysfs directories and files created + */ +static void release_attributes_data(void) +{ + mutex_lock(&wmi_priv.mutex); + exit_enum_attributes(); + exit_int_attributes(); + exit_str_attributes(); + exit_po_attributes(); + if (wmi_priv.authentication_dir_kset) { + destroy_attribute_objs(wmi_priv.authentication_dir_kset); + kset_unregister(wmi_priv.authentication_dir_kset); + wmi_priv.authentication_dir_kset = NULL; + } + if (wmi_priv.main_dir_kset) { + sysfs_remove_file(&wmi_priv.main_dir_kset->kobj, &reset_bios.attr); + sysfs_remove_file(&wmi_priv.main_dir_kset->kobj, &pending_reboot.attr); + destroy_attribute_objs(wmi_priv.main_dir_kset); + kset_unregister(wmi_priv.main_dir_kset); + wmi_priv.main_dir_kset = NULL; + } + mutex_unlock(&wmi_priv.mutex); +} + +/** + * init_bios_attributes() - Initialize all attributes for a type + * @attr_type: The attribute type to initialize + * @guid: The WMI GUID associated with this type to initialize + * + * Initialiaze all 4 types of attributes enumeration, integer, string and password object. + * Populates each attrbute typ's respective properties under sysfs files + */ +static int init_bios_attributes(int attr_type, const char *guid) +{ + struct kobject *attr_name_kobj; //individual attribute names + union acpi_object *obj = NULL; + union acpi_object *elements; + struct kobject *duplicate; + struct kset *tmp_set; + int min_elements; + + /* instance_id needs to be reset for each type GUID + * also, instance IDs are unique within GUID but not across + */ + int instance_id = 0; + int retval = 0; + + retval = alloc_attributes_data(attr_type); + if (retval) + return retval; + + switch (attr_type) { + case ENUM: min_elements = ENUM_MIN_ELEMENTS; break; + case INT: min_elements = INT_MIN_ELEMENTS; break; + case STR: min_elements = STR_MIN_ELEMENTS; break; + case PO: min_elements = PO_MIN_ELEMENTS; break; + default: + pr_err("Error: Unknown attr_type: %d\n", attr_type); + return -EINVAL; + } + + /* need to use specific instance_id and guid combination to get right data */ + obj = get_wmiobj_pointer(instance_id, guid); + if (!obj) + return -ENODEV; + + mutex_lock(&wmi_priv.mutex); + while (obj) { + if (obj->type != ACPI_TYPE_PACKAGE) { + pr_err("Error: Expected ACPI-package type, got: %d\n", obj->type); + retval = -EIO; + goto err_attr_init; + } + + if (obj->package.count < min_elements) { + pr_err("Error: ACPI-package does not have enough elements: %d < %d\n", + obj->package.count, min_elements); + goto nextobj; + } + + elements = obj->package.elements; + + /* sanity checking */ + if (elements[ATTR_NAME].type != ACPI_TYPE_STRING) { + pr_debug("incorrect element type\n"); + goto nextobj; + } + if (strlen(elements[ATTR_NAME].string.pointer) == 0) { + pr_debug("empty attribute found\n"); + goto nextobj; + } + if (attr_type == PO) + tmp_set = wmi_priv.authentication_dir_kset; + else + tmp_set = wmi_priv.main_dir_kset; + + duplicate = kset_find_obj(tmp_set, elements[ATTR_NAME].string.pointer); + if (duplicate) { + pr_debug("Duplicate attribute name found - %s\n", + elements[ATTR_NAME].string.pointer); + kobject_put(duplicate); + goto nextobj; + } + + /* build attribute */ + attr_name_kobj = kzalloc_obj(*attr_name_kobj); + if (!attr_name_kobj) { + retval = -ENOMEM; + goto err_attr_init; + } + + attr_name_kobj->kset = tmp_set; + + retval = kobject_init_and_add(attr_name_kobj, &attr_name_ktype, NULL, "%s", + elements[ATTR_NAME].string.pointer); + if (retval) { + kobject_put(attr_name_kobj); + goto err_attr_init; + } + + /* enumerate all of this attribute */ + switch (attr_type) { + case ENUM: + retval = populate_enum_data(elements, instance_id, attr_name_kobj, + obj->package.count); + break; + case INT: + retval = populate_int_data(elements, instance_id, attr_name_kobj); + break; + case STR: + retval = populate_str_data(elements, instance_id, attr_name_kobj); + break; + case PO: + retval = populate_po_data(elements, instance_id, attr_name_kobj); + break; + default: + break; + } + + if (retval) { + pr_debug("failed to populate %s\n", + elements[ATTR_NAME].string.pointer); + goto err_attr_init; + } + +nextobj: + kfree(obj); + instance_id++; + obj = get_wmiobj_pointer(instance_id, guid); + } + + mutex_unlock(&wmi_priv.mutex); + return 0; + +err_attr_init: + mutex_unlock(&wmi_priv.mutex); + kfree(obj); + return retval; +} + +static int __init sysman_init(void) +{ + int ret = 0; + + if (!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Dell System", NULL) && + !dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Alienware", NULL) && + !dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "www.dell.com", NULL)) { + pr_err("Unable to run on non-Dell system\n"); + return -ENODEV; + } + + ret = init_bios_attr_set_interface(); + if (ret) + return ret; + + ret = init_bios_attr_pass_interface(); + if (ret) + goto err_exit_bios_attr_set_interface; + + if (!wmi_priv.bios_attr_wdev || !wmi_priv.password_attr_wdev) { + pr_debug("failed to find set or pass interface\n"); + ret = -ENODEV; + goto err_exit_bios_attr_pass_interface; + } + + wmi_priv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(wmi_priv.class_dev)) { + ret = PTR_ERR(wmi_priv.class_dev); + goto err_exit_bios_attr_pass_interface; + } + + wmi_priv.main_dir_kset = kset_create_and_add("attributes", NULL, + &wmi_priv.class_dev->kobj); + if (!wmi_priv.main_dir_kset) { + ret = -ENOMEM; + goto err_destroy_classdev; + } + + wmi_priv.authentication_dir_kset = kset_create_and_add("authentication", NULL, + &wmi_priv.class_dev->kobj); + if (!wmi_priv.authentication_dir_kset) { + ret = -ENOMEM; + goto err_release_attributes_data; + } + + ret = create_attributes_level_sysfs_files(); + if (ret) { + pr_debug("could not create reset BIOS attribute\n"); + goto err_release_attributes_data; + } + + ret = init_bios_attributes(ENUM, DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID); + if (ret) { + pr_debug("failed to populate enumeration type attributes\n"); + goto err_release_attributes_data; + } + + ret = init_bios_attributes(INT, DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID); + if (ret) { + pr_debug("failed to populate integer type attributes\n"); + goto err_release_attributes_data; + } + + ret = init_bios_attributes(STR, DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID); + if (ret) { + pr_debug("failed to populate string type attributes\n"); + goto err_release_attributes_data; + } + + ret = init_bios_attributes(PO, DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID); + if (ret) { + pr_debug("failed to populate pass object type attributes\n"); + goto err_release_attributes_data; + } + + return 0; + +err_release_attributes_data: + release_attributes_data(); + +err_destroy_classdev: + device_unregister(wmi_priv.class_dev); + +err_exit_bios_attr_pass_interface: + exit_bios_attr_pass_interface(); + +err_exit_bios_attr_set_interface: + exit_bios_attr_set_interface(); + + return ret; +} + +static void __exit sysman_exit(void) +{ + release_attributes_data(); + device_unregister(wmi_priv.class_dev); + exit_bios_attr_set_interface(); + exit_bios_attr_pass_interface(); +} + +module_init(sysman_init); +module_exit(sysman_exit); + +MODULE_AUTHOR("Mario Limonciello "); +MODULE_AUTHOR("Prasanth Ksr "); +MODULE_AUTHOR("Divya Bharathi "); +MODULE_DESCRIPTION("Dell platform setting control interface"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/dell/dell_rbu.c b/drivers/platform/x86/dell/dell_rbu.c new file mode 100644 index 000000000..768b15f40 --- /dev/null +++ b/drivers/platform/x86/dell/dell_rbu.c @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * dell_rbu.c + * Bios Update driver for Dell systems + * Author: Dell Inc + * Abhay Salunke + * + * Copyright (C) 2005 Dell Inc. + * + * Remote BIOS Update (rbu) driver is used for updating DELL BIOS by + * creating entries in the /sys file systems on Linux 2.6 and higher + * kernels. The driver supports two mechanism to update the BIOS namely + * contiguous and packetized. Both these methods still require having some + * application to set the CMOS bit indicating the BIOS to update itself + * after a reboot. + * + * Contiguous method: + * This driver writes the incoming data in a monolithic image by allocating + * contiguous physical pages large enough to accommodate the incoming BIOS + * image size. + * + * Packetized method: + * The driver writes the incoming packet image by allocating a new packet + * on every time the packet data is written. This driver requires an + * application to break the BIOS image in to fixed sized packet chunks. + * + * See Documentation/admin-guide/dell_rbu.rst for more info. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Abhay Salunke "); +MODULE_DESCRIPTION("Driver for updating BIOS image on DELL systems"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("3.3"); + +#define BIOS_SCAN_LIMIT 0xffffffff +#define MAX_IMAGE_LENGTH 16 +static struct _rbu_data { + void *image_update_buffer; + unsigned long image_update_buffer_size; + unsigned long bios_image_size; + int image_update_ordernum; + spinlock_t lock; + unsigned long packet_read_count; + unsigned long num_packets; + unsigned long packetsize; + unsigned long imagesize; + int entry_created; +} rbu_data; + +static char image_type[MAX_IMAGE_LENGTH + 1] = "mono"; +module_param_string(image_type, image_type, sizeof (image_type), 0); +MODULE_PARM_DESC(image_type, "BIOS image type. choose- mono or packet or init"); + +static unsigned long allocation_floor = 0x100000; +module_param(allocation_floor, ulong, 0644); +MODULE_PARM_DESC(allocation_floor, "Minimum address for allocations when using Packet mode"); + +struct packet_data { + struct list_head list; + size_t length; + void *data; + int ordernum; +}; + +static struct list_head packet_data_list; + +static struct platform_device *rbu_device; +static int context; + +static void init_packet_head(void) +{ + INIT_LIST_HEAD(&packet_data_list); + rbu_data.packet_read_count = 0; + rbu_data.num_packets = 0; + rbu_data.packetsize = 0; + rbu_data.imagesize = 0; +} + +static int create_packet(void *data, size_t length) __must_hold(&rbu_data.lock) +{ + struct packet_data *newpacket; + int ordernum = 0; + int retval = 0; + unsigned int packet_array_size = 0; + void **invalid_addr_packet_array = NULL; + void *packet_data_temp_buf = NULL; + unsigned int idx = 0; + + pr_debug("entry\n"); + + if (!rbu_data.packetsize) { + pr_debug("packetsize not specified\n"); + retval = -EINVAL; + goto out_noalloc; + } + + spin_unlock(&rbu_data.lock); + + newpacket = kzalloc_obj(struct packet_data); + + if (!newpacket) { + pr_warn("failed to allocate new packet\n"); + retval = -ENOMEM; + spin_lock(&rbu_data.lock); + goto out_noalloc; + } + + ordernum = get_order(length); + + /* + * BIOS errata mean we cannot allocate packets below 1MB or they will + * be overwritten by BIOS. + * + * array to temporarily hold packets + * that are below the allocation floor + * + * NOTE: very simplistic because we only need the floor to be at 1MB + * due to BIOS errata. This shouldn't be used for higher floors + * or you will run out of mem trying to allocate the array. + */ + packet_array_size = max_t(unsigned int, allocation_floor / rbu_data.packetsize, 1); + invalid_addr_packet_array = kcalloc(packet_array_size, sizeof(void *), + GFP_KERNEL); + + if (!invalid_addr_packet_array) { + pr_warn("failed to allocate invalid_addr_packet_array\n"); + retval = -ENOMEM; + spin_lock(&rbu_data.lock); + goto out_alloc_packet; + } + + while (!packet_data_temp_buf) { + packet_data_temp_buf = (unsigned char *) + __get_free_pages(GFP_KERNEL, ordernum); + if (!packet_data_temp_buf) { + pr_warn("failed to allocate new packet\n"); + retval = -ENOMEM; + spin_lock(&rbu_data.lock); + goto out_alloc_packet_array; + } + + if ((unsigned long)virt_to_phys(packet_data_temp_buf) + < allocation_floor) { + pr_debug("packet 0x%lx below floor at 0x%lx\n", + (unsigned long)virt_to_phys( + packet_data_temp_buf), + allocation_floor); + invalid_addr_packet_array[idx++] = packet_data_temp_buf; + packet_data_temp_buf = NULL; + } + } + /* + * set to uncachable or it may never get written back before reboot + */ + set_memory_uc((unsigned long)packet_data_temp_buf, 1 << ordernum); + + spin_lock(&rbu_data.lock); + + newpacket->data = packet_data_temp_buf; + + pr_debug("newpacket at physical addr %lx\n", + (unsigned long)virt_to_phys(newpacket->data)); + + /* packets may not have fixed size */ + newpacket->length = length; + newpacket->ordernum = ordernum; + ++rbu_data.num_packets; + + /* initialize the newly created packet headers */ + INIT_LIST_HEAD(&newpacket->list); + list_add_tail(&newpacket->list, &packet_data_list); + + memcpy(newpacket->data, data, length); + + pr_debug("exit\n"); + +out_alloc_packet_array: + /* always free packet array */ + while (idx--) { + pr_debug("freeing unused packet below floor 0x%lx\n", + (unsigned long)virt_to_phys(invalid_addr_packet_array[idx])); + free_pages((unsigned long)invalid_addr_packet_array[idx], ordernum); + } + kfree(invalid_addr_packet_array); + +out_alloc_packet: + /* if error, free data */ + if (retval) + kfree(newpacket); + +out_noalloc: + return retval; +} + +static int packetize_data(const u8 *data, size_t length) +{ + int rc = 0; + int done = 0; + int packet_length; + u8 *temp; + u8 *end = (u8 *) data + length; + pr_debug("data length %zd\n", length); + if (!rbu_data.packetsize) { + pr_warn("packetsize not specified\n"); + return -EIO; + } + + temp = (u8 *) data; + + /* packetize the hunk */ + while (!done) { + if ((temp + rbu_data.packetsize) < end) + packet_length = rbu_data.packetsize; + else { + /* this is the last packet */ + packet_length = end - temp; + done = 1; + } + + rc = create_packet(temp, packet_length); + if (rc) + return rc; + + pr_debug("%p:%td\n", temp, (end - temp)); + temp += packet_length; + } + + rbu_data.imagesize = length; + + return rc; +} + +static int do_packet_read(char *data, struct packet_data *newpacket, + int length, int bytes_read, int *list_read_count) +{ + void *ptemp_buf; + int bytes_copied = 0; + int j = 0; + + *list_read_count += newpacket->length; + + if (*list_read_count > bytes_read) { + /* point to the start of unread data */ + j = newpacket->length - (*list_read_count - bytes_read); + /* point to the offset in the packet buffer */ + ptemp_buf = (u8 *) newpacket->data + j; + /* + * check if there is enough room in + * * the incoming buffer + */ + if (length > (*list_read_count - bytes_read)) + /* + * copy what ever is there in this + * packet and move on + */ + bytes_copied = (*list_read_count - bytes_read); + else + /* copy the remaining */ + bytes_copied = length; + memcpy(data, ptemp_buf, bytes_copied); + } + return bytes_copied; +} + +static int packet_read_list(char *data, size_t *pread_length) +{ + struct packet_data *newpacket; + int temp_count = 0; + int bytes_copied = 0; + int bytes_read = 0; + int remaining_bytes = 0; + char *pdest = data; + + /* check if we have any packets */ + if (0 == rbu_data.num_packets) + return -ENOMEM; + + remaining_bytes = *pread_length; + bytes_read = rbu_data.packet_read_count; + + list_for_each_entry(newpacket, &packet_data_list, list) { + bytes_copied = do_packet_read(pdest, newpacket, + remaining_bytes, bytes_read, &temp_count); + remaining_bytes -= bytes_copied; + bytes_read += bytes_copied; + pdest += bytes_copied; + /* + * check if we reached end of buffer before reaching the + * last packet + */ + if (remaining_bytes == 0) + break; + } + /*finally set the bytes read */ + *pread_length = bytes_read - rbu_data.packet_read_count; + rbu_data.packet_read_count = bytes_read; + return 0; +} + +static void packet_empty_list(void) +{ + struct packet_data *newpacket, *tmp; + + list_for_each_entry_safe(newpacket, tmp, &packet_data_list, list) { + list_del(&newpacket->list); + + /* + * zero out the RBU packet memory before freeing + * to make sure there are no stale RBU packets left in memory + */ + memset(newpacket->data, 0, newpacket->length); + set_memory_wb((unsigned long)newpacket->data, + 1 << newpacket->ordernum); + free_pages((unsigned long) newpacket->data, + newpacket->ordernum); + kfree(newpacket); + } + rbu_data.packet_read_count = 0; + rbu_data.num_packets = 0; + rbu_data.imagesize = 0; +} + +/* + * img_update_free: Frees the buffer allocated for storing BIOS image + * Always called with lock held and returned with lock held + */ +static void img_update_free(void) +{ + if (!rbu_data.image_update_buffer) + return; + /* + * zero out this buffer before freeing it to get rid of any stale + * BIOS image copied in memory. + */ + memset(rbu_data.image_update_buffer, 0, + rbu_data.image_update_buffer_size); + free_pages((unsigned long) rbu_data.image_update_buffer, + rbu_data.image_update_ordernum); + + /* + * Re-initialize the rbu_data variables after a free + */ + rbu_data.image_update_ordernum = -1; + rbu_data.image_update_buffer = NULL; + rbu_data.image_update_buffer_size = 0; + rbu_data.bios_image_size = 0; +} + +/* + * img_update_realloc: This function allocates the contiguous pages to + * accommodate the requested size of data. The memory address and size + * values are stored globally and on every call to this function the new + * size is checked to see if more data is required than the existing size. + * If true the previous memory is freed and new allocation is done to + * accommodate the new size. If the incoming size is less then than the + * already allocated size, then that memory is reused. This function is + * called with lock held and returns with lock held. + */ +static int img_update_realloc(unsigned long size) +{ + unsigned char *image_update_buffer = NULL; + unsigned long img_buf_phys_addr; + int ordernum; + + /* + * check if the buffer of sufficient size has been + * already allocated + */ + if (rbu_data.image_update_buffer_size >= size) { + /* + * check for corruption + */ + if ((size != 0) && (rbu_data.image_update_buffer == NULL)) { + pr_err("corruption check failed\n"); + return -EINVAL; + } + /* + * we have a valid pre-allocated buffer with + * sufficient size + */ + return 0; + } + + /* + * free any previously allocated buffer + */ + img_update_free(); + + spin_unlock(&rbu_data.lock); + + ordernum = get_order(size); + image_update_buffer = + (unsigned char *)__get_free_pages(GFP_DMA32, ordernum); + spin_lock(&rbu_data.lock); + if (!image_update_buffer) { + pr_debug("Not enough memory for image update: size = %ld\n", size); + return -ENOMEM; + } + + img_buf_phys_addr = (unsigned long)virt_to_phys(image_update_buffer); + if (WARN_ON_ONCE(img_buf_phys_addr > BIOS_SCAN_LIMIT)) + return -EINVAL; /* can't happen per definition */ + + rbu_data.image_update_buffer = image_update_buffer; + rbu_data.image_update_buffer_size = size; + rbu_data.bios_image_size = rbu_data.image_update_buffer_size; + rbu_data.image_update_ordernum = ordernum; + return 0; +} + +static ssize_t read_packet_data(char *buffer, loff_t pos, size_t count) +{ + int retval; + size_t bytes_left; + size_t data_length; + char *ptempBuf = buffer; + + /* check to see if we have something to return */ + if (rbu_data.num_packets == 0) { + pr_debug("no packets written\n"); + retval = -ENOMEM; + goto read_rbu_data_exit; + } + + if (pos > rbu_data.imagesize) { + retval = 0; + pr_warn("data underrun\n"); + goto read_rbu_data_exit; + } + + bytes_left = rbu_data.imagesize - pos; + data_length = min(bytes_left, count); + + retval = packet_read_list(ptempBuf, &data_length); + if (retval < 0) + goto read_rbu_data_exit; + + if ((pos + count) > rbu_data.imagesize) { + rbu_data.packet_read_count = 0; + /* this was the last copy */ + retval = bytes_left; + } else + retval = count; + + read_rbu_data_exit: + return retval; +} + +static ssize_t read_rbu_mono_data(char *buffer, loff_t pos, size_t count) +{ + /* check to see if we have something to return */ + if ((rbu_data.image_update_buffer == NULL) || + (rbu_data.bios_image_size == 0)) { + pr_debug("image_update_buffer %p, bios_image_size %lu\n", + rbu_data.image_update_buffer, + rbu_data.bios_image_size); + return -ENOMEM; + } + + return memory_read_from_buffer(buffer, count, &pos, + rbu_data.image_update_buffer, rbu_data.bios_image_size); +} + +static ssize_t data_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buffer, loff_t pos, size_t count) +{ + ssize_t ret_count = 0; + + spin_lock(&rbu_data.lock); + + if (!strcmp(image_type, "mono")) + ret_count = read_rbu_mono_data(buffer, pos, count); + else if (!strcmp(image_type, "packet")) + ret_count = read_packet_data(buffer, pos, count); + else + pr_debug("invalid image type specified\n"); + + spin_unlock(&rbu_data.lock); + return ret_count; +} +static const BIN_ATTR_RO(data, 0); + +static void callbackfn_rbu(const struct firmware *fw, void *context) +{ + rbu_data.entry_created = 0; + + if (!fw) + return; + + if (!fw->size) + goto out; + + spin_lock(&rbu_data.lock); + if (!strcmp(image_type, "mono")) { + if (!img_update_realloc(fw->size)) + memcpy(rbu_data.image_update_buffer, + fw->data, fw->size); + } else if (!strcmp(image_type, "packet")) { + /* + * we need to free previous packets if a + * new hunk of packets needs to be downloaded + */ + packet_empty_list(); + if (packetize_data(fw->data, fw->size)) + /* Incase something goes wrong when we are + * in middle of packetizing the data, we + * need to free up whatever packets might + * have been created before we quit. + */ + packet_empty_list(); + } else + pr_debug("invalid image type specified\n"); + spin_unlock(&rbu_data.lock); + out: + release_firmware(fw); +} + +static ssize_t image_type_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buffer, loff_t pos, size_t count) +{ + int size = 0; + if (!pos) + size = scnprintf(buffer, count, "%s\n", image_type); + return size; +} + +static ssize_t image_type_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buffer, loff_t pos, size_t count) +{ + int rc = count; + int req_firm_rc = 0; + int i; + spin_lock(&rbu_data.lock); + /* + * Find the first newline or space + */ + for (i = 0; i < count; ++i) + if (buffer[i] == '\n' || buffer[i] == ' ') { + buffer[i] = '\0'; + break; + } + if (i == count) + buffer[count] = '\0'; + + if (strstr(buffer, "mono")) + strscpy(image_type, "mono"); + else if (strstr(buffer, "packet")) + strscpy(image_type, "packet"); + else if (strstr(buffer, "init")) { + /* + * If due to the user error the driver gets in a bad + * state where even though it is loaded , the + * /sys/class/firmware/dell_rbu entries are missing. + * to cover this situation the user can recreate entries + * by writing init to image_type. + */ + if (!rbu_data.entry_created) { + spin_unlock(&rbu_data.lock); + req_firm_rc = request_firmware_nowait(THIS_MODULE, + FW_ACTION_NOUEVENT, "dell_rbu", + &rbu_device->dev, GFP_KERNEL, &context, + callbackfn_rbu); + if (req_firm_rc) { + pr_err("request_firmware_nowait failed %d\n", rc); + rc = -EIO; + } else + rbu_data.entry_created = 1; + + spin_lock(&rbu_data.lock); + } + } else { + pr_warn("image_type is invalid\n"); + spin_unlock(&rbu_data.lock); + return -EINVAL; + } + + /* we must free all previous allocations */ + packet_empty_list(); + img_update_free(); + spin_unlock(&rbu_data.lock); + + return rc; +} +static const BIN_ATTR_RW(image_type, 0); + +static ssize_t packet_size_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buffer, loff_t pos, size_t count) +{ + int size = 0; + if (!pos) { + spin_lock(&rbu_data.lock); + size = scnprintf(buffer, count, "%lu\n", rbu_data.packetsize); + spin_unlock(&rbu_data.lock); + } + return size; +} + +static ssize_t packet_size_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buffer, loff_t pos, size_t count) +{ + unsigned long temp; + + if (kstrtoul(buffer, 10, &temp)) + return -EINVAL; + + spin_lock(&rbu_data.lock); + packet_empty_list(); + if (temp < 0xffffffff) + rbu_data.packetsize = temp; + + spin_unlock(&rbu_data.lock); + return count; +} +static const BIN_ATTR_RW(packet_size, 0); + +static const struct bin_attribute *const rbu_bin_attrs[] = { + &bin_attr_data, + &bin_attr_image_type, + &bin_attr_packet_size, + NULL +}; + +static const struct attribute_group rbu_group = { + .bin_attrs = rbu_bin_attrs, +}; + +static int __init dcdrbu_init(void) +{ + int rc; + spin_lock_init(&rbu_data.lock); + + init_packet_head(); + rbu_device = platform_device_register_simple("dell_rbu", PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(rbu_device)) { + pr_err("platform_device_register_simple failed\n"); + return PTR_ERR(rbu_device); + } + + rc = sysfs_create_group(&rbu_device->dev.kobj, &rbu_group); + if (rc) + goto out_devreg; + + rbu_data.entry_created = 0; + return 0; + +out_devreg: + platform_device_unregister(rbu_device); + return rc; +} + +static __exit void dcdrbu_exit(void) +{ + spin_lock(&rbu_data.lock); + packet_empty_list(); + img_update_free(); + spin_unlock(&rbu_data.lock); + sysfs_remove_group(&rbu_device->dev.kobj, &rbu_group); + platform_device_unregister(rbu_device); +} + +module_exit(dcdrbu_exit); +module_init(dcdrbu_init); diff --git a/drivers/platform/x86/dual_accel_detect.h b/drivers/platform/x86/dual_accel_detect.h new file mode 100644 index 000000000..72e962433 --- /dev/null +++ b/drivers/platform/x86/dual_accel_detect.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Helper code to detect 360 degree hinges (yoga) style 2-in-1 devices using 2 accelerometers + * to allow the OS to determine the angle between the display and the base of the device. + * + * On Windows these are read by a special HingeAngleService process which calls undocumented + * ACPI methods, to let the firmware know if the 2-in-1 is in tablet- or laptop-mode. + * The firmware may use this to disable the kbd and touchpad to avoid spurious input in + * tablet-mode as well as to report SW_TABLET_MODE info to the OS. + * + * Since Linux does not call these undocumented methods, the SW_TABLET_MODE info reported + * by various drivers/platform/x86 drivers is incorrect. These drivers use the detection + * code in this file to disable SW_TABLET_MODE reporting to avoid reporting broken info + * (instead userspace can derive the status itself by directly reading the 2 accels). + */ + +#include +#include + +static bool dual_accel_detect_bosc0200(void) +{ + struct acpi_device *adev; + int count; + + adev = acpi_dev_get_first_match_dev("BOSC0200", NULL, -1); + if (!adev) + return false; + + count = i2c_acpi_client_count(adev); + + acpi_dev_put(adev); + + return count == 2; +} + +static bool dual_accel_detect(void) +{ + /* Systems which use a pair of accels with KIOX010A / KIOX020A ACPI ids */ + if (acpi_dev_present("KIOX010A", NULL, -1) && + acpi_dev_present("KIOX020A", NULL, -1)) + return true; + + /* Systems which use a single DUAL250E ACPI device to model 2 accels */ + if (acpi_dev_present("DUAL250E", NULL, -1)) + return true; + + /* Systems which use a single BOSC0200 ACPI device to model 2 accels */ + if (dual_accel_detect_bosc0200()) + return true; + + return false; +} diff --git a/drivers/platform/x86/eeepc-laptop.c b/drivers/platform/x86/eeepc-laptop.c new file mode 100644 index 000000000..d18a80907 --- /dev/null +++ b/drivers/platform/x86/eeepc-laptop.c @@ -0,0 +1,1522 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * eeepc-laptop.c - Asus Eee PC extras + * + * Based on asus_acpi.c as patched for the Eee PC by Asus: + * ftp://ftp.asus.com/pub/ASUS/EeePC/701/ASUS_ACPI_071126.rar + * Based on eee.c from eeepc-linux + */ + +#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 + +#define EEEPC_LAPTOP_VERSION "0.1" +#define EEEPC_LAPTOP_NAME "Eee PC Hotkey Driver" +#define EEEPC_LAPTOP_FILE "eeepc" + +#define EEEPC_ACPI_CLASS "hotkey" +#define EEEPC_ACPI_DEVICE_NAME "Hotkey" +#define EEEPC_ACPI_HID "ASUS010" + +MODULE_AUTHOR("Corentin Chary, Eric Cooper"); +MODULE_DESCRIPTION(EEEPC_LAPTOP_NAME); +MODULE_LICENSE("GPL"); + +static bool hotplug_disabled; + +module_param(hotplug_disabled, bool, 0444); +MODULE_PARM_DESC(hotplug_disabled, + "Disable hotplug for wireless device. " + "If your laptop need that, please report to " + "acpi4asus-user@lists.sourceforge.net."); + +/* + * Definitions for Asus EeePC + */ +#define NOTIFY_BRN_MIN 0x20 +#define NOTIFY_BRN_MAX 0x2f + +enum { + DISABLE_ASL_WLAN = 0x0001, + DISABLE_ASL_BLUETOOTH = 0x0002, + DISABLE_ASL_IRDA = 0x0004, + DISABLE_ASL_CAMERA = 0x0008, + DISABLE_ASL_TV = 0x0010, + DISABLE_ASL_GPS = 0x0020, + DISABLE_ASL_DISPLAYSWITCH = 0x0040, + DISABLE_ASL_MODEM = 0x0080, + DISABLE_ASL_CARDREADER = 0x0100, + DISABLE_ASL_3G = 0x0200, + DISABLE_ASL_WIMAX = 0x0400, + DISABLE_ASL_HWCF = 0x0800 +}; + +enum { + CM_ASL_WLAN = 0, + CM_ASL_BLUETOOTH, + CM_ASL_IRDA, + CM_ASL_1394, + CM_ASL_CAMERA, + CM_ASL_TV, + CM_ASL_GPS, + CM_ASL_DVDROM, + CM_ASL_DISPLAYSWITCH, + CM_ASL_PANELBRIGHT, + CM_ASL_BIOSFLASH, + CM_ASL_ACPIFLASH, + CM_ASL_CPUFV, + CM_ASL_CPUTEMPERATURE, + CM_ASL_FANCPU, + CM_ASL_FANCHASSIS, + CM_ASL_USBPORT1, + CM_ASL_USBPORT2, + CM_ASL_USBPORT3, + CM_ASL_MODEM, + CM_ASL_CARDREADER, + CM_ASL_3G, + CM_ASL_WIMAX, + CM_ASL_HWCF, + CM_ASL_LID, + CM_ASL_TYPE, + CM_ASL_PANELPOWER, /*P901*/ + CM_ASL_TPD +}; + +static const char *cm_getv[] = { + "WLDG", "BTHG", NULL, NULL, + "CAMG", NULL, NULL, NULL, + NULL, "PBLG", NULL, NULL, + "CFVG", NULL, NULL, NULL, + "USBG", NULL, NULL, "MODG", + "CRDG", "M3GG", "WIMG", "HWCF", + "LIDG", "TYPE", "PBPG", "TPDG" +}; + +static const char *cm_setv[] = { + "WLDS", "BTHS", NULL, NULL, + "CAMS", NULL, NULL, NULL, + "SDSP", "PBLS", "HDPS", NULL, + "CFVS", NULL, NULL, NULL, + "USBG", NULL, NULL, "MODS", + "CRDS", "M3GS", "WIMS", NULL, + NULL, NULL, "PBPS", "TPDS" +}; + +static const struct key_entry eeepc_keymap[] = { + { KE_KEY, 0x10, { KEY_WLAN } }, + { KE_KEY, 0x11, { KEY_WLAN } }, + { KE_KEY, 0x12, { KEY_PROG1 } }, + { KE_KEY, 0x13, { KEY_MUTE } }, + { KE_KEY, 0x14, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0x15, { KEY_VOLUMEUP } }, + { KE_KEY, 0x16, { KEY_DISPLAY_OFF } }, + { KE_KEY, 0x1a, { KEY_COFFEE } }, + { KE_KEY, 0x1b, { KEY_ZOOM } }, + { KE_KEY, 0x1c, { KEY_PROG2 } }, + { KE_KEY, 0x1d, { KEY_PROG3 } }, + { KE_KEY, NOTIFY_BRN_MIN, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, NOTIFY_BRN_MAX, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x30, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x31, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x32, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x37, { KEY_F13 } }, /* Disable Touchpad */ + { KE_KEY, 0x38, { KEY_F14 } }, + { KE_IGNORE, 0x50, { KEY_RESERVED } }, /* AC plugged */ + { KE_IGNORE, 0x51, { KEY_RESERVED } }, /* AC unplugged */ + { KE_END, 0 }, +}; + +/* + * This is the main structure, we can use it to store useful information + */ +struct eeepc_laptop { + acpi_handle handle; /* the handle of the acpi device */ + u32 cm_supported; /* the control methods supported + by this BIOS */ + bool cpufv_disabled; + bool hotplug_disabled; + u16 event_count[128]; /* count for each event */ + + struct platform_device *platform_device; + struct acpi_device *device; /* the device we are in */ + struct backlight_device *backlight_device; + + struct input_dev *inputdev; + + struct rfkill *wlan_rfkill; + struct rfkill *bluetooth_rfkill; + struct rfkill *wwan3g_rfkill; + struct rfkill *wimax_rfkill; + + struct hotplug_slot hotplug_slot; + struct mutex hotplug_lock; + + struct led_classdev tpd_led; + int tpd_led_wk; + struct workqueue_struct *led_workqueue; + struct work_struct tpd_led_work; +}; + +/* + * ACPI Helpers + */ +static int write_acpi_int(acpi_handle handle, const char *method, int val) +{ + acpi_status status; + + status = acpi_execute_simple_method(handle, (char *)method, val); + + return (status == AE_OK ? 0 : -1); +} + +static int read_acpi_int(acpi_handle handle, const char *method, int *val) +{ + acpi_status status; + unsigned long long result; + + status = acpi_evaluate_integer(handle, (char *)method, NULL, &result); + if (ACPI_FAILURE(status)) { + *val = -1; + return -1; + } else { + *val = result; + return 0; + } +} + +static int set_acpi(struct eeepc_laptop *eeepc, int cm, int value) +{ + const char *method = cm_setv[cm]; + + if (method == NULL) + return -ENODEV; + if ((eeepc->cm_supported & (0x1 << cm)) == 0) + return -ENODEV; + + if (write_acpi_int(eeepc->handle, method, value)) + pr_warn("Error writing %s\n", method); + return 0; +} + +static int get_acpi(struct eeepc_laptop *eeepc, int cm) +{ + const char *method = cm_getv[cm]; + int value; + + if (method == NULL) + return -ENODEV; + if ((eeepc->cm_supported & (0x1 << cm)) == 0) + return -ENODEV; + + if (read_acpi_int(eeepc->handle, method, &value)) + pr_warn("Error reading %s\n", method); + return value; +} + +static int acpi_setter_handle(struct eeepc_laptop *eeepc, int cm, + acpi_handle *handle) +{ + const char *method = cm_setv[cm]; + acpi_status status; + + if (method == NULL) + return -ENODEV; + if ((eeepc->cm_supported & (0x1 << cm)) == 0) + return -ENODEV; + + status = acpi_get_handle(eeepc->handle, (char *)method, + handle); + if (status != AE_OK) { + pr_warn("Error finding %s\n", method); + return -ENODEV; + } + return 0; +} + + +/* + * Sys helpers + */ +static int parse_arg(const char *buf, int *val) +{ + if (sscanf(buf, "%i", val) != 1) + return -EINVAL; + return 0; +} + +static ssize_t store_sys_acpi(struct device *dev, int cm, + const char *buf, size_t count) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + int rv, value; + + rv = parse_arg(buf, &value); + if (rv < 0) + return rv; + rv = set_acpi(eeepc, cm, value); + if (rv < 0) + return -EIO; + return count; +} + +static ssize_t show_sys_acpi(struct device *dev, int cm, char *buf) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + int value = get_acpi(eeepc, cm); + + if (value < 0) + return -EIO; + return sysfs_emit(buf, "%d\n", value); +} + +#define EEEPC_ACPI_SHOW_FUNC(_name, _cm) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return show_sys_acpi(dev, _cm, buf); \ + } + +#define EEEPC_ACPI_STORE_FUNC(_name, _cm) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return store_sys_acpi(dev, _cm, buf, count); \ + } + +#define EEEPC_CREATE_DEVICE_ATTR_RW(_name, _cm) \ + EEEPC_ACPI_SHOW_FUNC(_name, _cm) \ + EEEPC_ACPI_STORE_FUNC(_name, _cm) \ + static DEVICE_ATTR_RW(_name) + +#define EEEPC_CREATE_DEVICE_ATTR_WO(_name, _cm) \ + EEEPC_ACPI_STORE_FUNC(_name, _cm) \ + static DEVICE_ATTR_WO(_name) + +EEEPC_CREATE_DEVICE_ATTR_RW(camera, CM_ASL_CAMERA); +EEEPC_CREATE_DEVICE_ATTR_RW(cardr, CM_ASL_CARDREADER); +EEEPC_CREATE_DEVICE_ATTR_WO(disp, CM_ASL_DISPLAYSWITCH); + +struct eeepc_cpufv { + int num; + int cur; +}; + +static int get_cpufv(struct eeepc_laptop *eeepc, struct eeepc_cpufv *c) +{ + c->cur = get_acpi(eeepc, CM_ASL_CPUFV); + if (c->cur < 0) + return -ENODEV; + + c->num = (c->cur >> 8) & 0xff; + c->cur &= 0xff; + if (c->num == 0 || c->num > 12) + return -ENODEV; + return 0; +} + +static ssize_t available_cpufv_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + struct eeepc_cpufv c; + int i; + ssize_t len = 0; + + if (get_cpufv(eeepc, &c)) + return -ENODEV; + for (i = 0; i < c.num; i++) + len += sprintf(buf + len, "%d ", i); + len += sprintf(buf + len, "\n"); + return len; +} + +static ssize_t cpufv_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + struct eeepc_cpufv c; + + if (get_cpufv(eeepc, &c)) + return -ENODEV; + return sysfs_emit(buf, "%#x\n", (c.num << 8) | c.cur); +} + +static ssize_t cpufv_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + struct eeepc_cpufv c; + int rv, value; + + if (eeepc->cpufv_disabled) + return -EPERM; + if (get_cpufv(eeepc, &c)) + return -ENODEV; + rv = parse_arg(buf, &value); + if (rv < 0) + return rv; + if (value < 0 || value >= c.num) + return -EINVAL; + rv = set_acpi(eeepc, CM_ASL_CPUFV, value); + if (rv) + return rv; + return count; +} + +static ssize_t cpufv_disabled_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", eeepc->cpufv_disabled); +} + +static ssize_t cpufv_disabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(dev); + int rv, value; + + rv = parse_arg(buf, &value); + if (rv < 0) + return rv; + + switch (value) { + case 0: + if (eeepc->cpufv_disabled) + pr_warn("cpufv enabled (not officially supported on this model)\n"); + eeepc->cpufv_disabled = false; + return count; + case 1: + return -EPERM; + default: + return -EINVAL; + } +} + + +static DEVICE_ATTR_RW(cpufv); +static DEVICE_ATTR_RO(available_cpufv); +static DEVICE_ATTR_RW(cpufv_disabled); + +static struct attribute *platform_attributes[] = { + &dev_attr_camera.attr, + &dev_attr_cardr.attr, + &dev_attr_disp.attr, + &dev_attr_cpufv.attr, + &dev_attr_available_cpufv.attr, + &dev_attr_cpufv_disabled.attr, + NULL +}; + +static const struct attribute_group platform_attribute_group = { + .attrs = platform_attributes +}; + +static int eeepc_platform_init(struct eeepc_laptop *eeepc) +{ + int result; + + eeepc->platform_device = platform_device_alloc(EEEPC_LAPTOP_FILE, PLATFORM_DEVID_NONE); + if (!eeepc->platform_device) + return -ENOMEM; + platform_set_drvdata(eeepc->platform_device, eeepc); + + result = platform_device_add(eeepc->platform_device); + if (result) + goto fail_platform_device; + + result = sysfs_create_group(&eeepc->platform_device->dev.kobj, + &platform_attribute_group); + if (result) + goto fail_sysfs; + return 0; + +fail_sysfs: + platform_device_del(eeepc->platform_device); +fail_platform_device: + platform_device_put(eeepc->platform_device); + return result; +} + +static void eeepc_platform_exit(struct eeepc_laptop *eeepc) +{ + sysfs_remove_group(&eeepc->platform_device->dev.kobj, + &platform_attribute_group); + platform_device_unregister(eeepc->platform_device); +} + +/* + * LEDs + */ +/* + * These functions actually update the LED's, and are called from a + * workqueue. By doing this as separate work rather than when the LED + * subsystem asks, we avoid messing with the Asus ACPI stuff during a + * potentially bad time, such as a timer interrupt. + */ +static void tpd_led_update(struct work_struct *work) +{ + struct eeepc_laptop *eeepc; + + eeepc = container_of(work, struct eeepc_laptop, tpd_led_work); + + set_acpi(eeepc, CM_ASL_TPD, eeepc->tpd_led_wk); +} + +static void tpd_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct eeepc_laptop *eeepc; + + eeepc = container_of(led_cdev, struct eeepc_laptop, tpd_led); + + eeepc->tpd_led_wk = (value > 0) ? 1 : 0; + queue_work(eeepc->led_workqueue, &eeepc->tpd_led_work); +} + +static enum led_brightness tpd_led_get(struct led_classdev *led_cdev) +{ + struct eeepc_laptop *eeepc; + + eeepc = container_of(led_cdev, struct eeepc_laptop, tpd_led); + + return get_acpi(eeepc, CM_ASL_TPD); +} + +static int eeepc_led_init(struct eeepc_laptop *eeepc) +{ + int rv; + + if (get_acpi(eeepc, CM_ASL_TPD) == -ENODEV) + return 0; + + eeepc->led_workqueue = create_singlethread_workqueue("led_workqueue"); + if (!eeepc->led_workqueue) + return -ENOMEM; + INIT_WORK(&eeepc->tpd_led_work, tpd_led_update); + + eeepc->tpd_led.name = "eeepc::touchpad"; + eeepc->tpd_led.brightness_set = tpd_led_set; + if (get_acpi(eeepc, CM_ASL_TPD) >= 0) /* if method is available */ + eeepc->tpd_led.brightness_get = tpd_led_get; + eeepc->tpd_led.max_brightness = 1; + + rv = led_classdev_register(&eeepc->platform_device->dev, + &eeepc->tpd_led); + if (rv) { + destroy_workqueue(eeepc->led_workqueue); + return rv; + } + + return 0; +} + +static void eeepc_led_exit(struct eeepc_laptop *eeepc) +{ + led_classdev_unregister(&eeepc->tpd_led); + if (eeepc->led_workqueue) + destroy_workqueue(eeepc->led_workqueue); +} + +/* + * PCI hotplug (for wlan rfkill) + */ +static bool eeepc_wlan_rfkill_blocked(struct eeepc_laptop *eeepc) +{ + if (get_acpi(eeepc, CM_ASL_WLAN) == 1) + return false; + return true; +} + +static void eeepc_rfkill_hotplug(struct eeepc_laptop *eeepc, acpi_handle handle) +{ + struct pci_dev *port; + struct pci_dev *dev; + struct pci_bus *bus; + bool blocked = eeepc_wlan_rfkill_blocked(eeepc); + bool absent; + u32 l; + + if (eeepc->wlan_rfkill) + rfkill_set_sw_state(eeepc->wlan_rfkill, blocked); + + mutex_lock(&eeepc->hotplug_lock); + pci_lock_rescan_remove(); + + if (!eeepc->hotplug_slot.ops) + goto out_unlock; + + port = acpi_get_pci_dev(handle); + if (!port) { + pr_warn("Unable to find port\n"); + goto out_unlock; + } + + bus = port->subordinate; + + if (!bus) { + pr_warn("Unable to find PCI bus 1?\n"); + goto out_put_dev; + } + + if (pci_bus_read_config_dword(bus, 0, PCI_VENDOR_ID, &l)) { + pr_err("Unable to read PCI config space?\n"); + goto out_put_dev; + } + + absent = (l == 0xffffffff); + + if (blocked != absent) { + pr_warn("BIOS says wireless lan is %s, but the pci device is %s\n", + blocked ? "blocked" : "unblocked", + absent ? "absent" : "present"); + pr_warn("skipped wireless hotplug as probably inappropriate for this model\n"); + goto out_put_dev; + } + + if (!blocked) { + dev = pci_get_slot(bus, 0); + if (dev) { + /* Device already present */ + pci_dev_put(dev); + goto out_put_dev; + } + dev = pci_scan_single_device(bus, 0); + if (dev) { + pci_bus_assign_resources(bus); + pci_bus_add_device(dev); + } + } else { + dev = pci_get_slot(bus, 0); + if (dev) { + pci_stop_and_remove_bus_device(dev); + pci_dev_put(dev); + } + } +out_put_dev: + pci_dev_put(port); + +out_unlock: + pci_unlock_rescan_remove(); + mutex_unlock(&eeepc->hotplug_lock); +} + +static void eeepc_rfkill_hotplug_update(struct eeepc_laptop *eeepc, char *node) +{ + acpi_status status = AE_OK; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + + if (ACPI_SUCCESS(status)) + eeepc_rfkill_hotplug(eeepc, handle); +} + +static void eeepc_rfkill_notify(acpi_handle handle, u32 event, void *data) +{ + struct eeepc_laptop *eeepc = data; + + if (event != ACPI_NOTIFY_BUS_CHECK) + return; + + eeepc_rfkill_hotplug(eeepc, handle); +} + +static int eeepc_register_rfkill_notifier(struct eeepc_laptop *eeepc, + char *node) +{ + acpi_status status; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + + if (ACPI_FAILURE(status)) + return -ENODEV; + + status = acpi_install_notify_handler(handle, + ACPI_SYSTEM_NOTIFY, + eeepc_rfkill_notify, + eeepc); + if (ACPI_FAILURE(status)) + pr_warn("Failed to register notify on %s\n", node); + + /* + * Refresh pci hotplug in case the rfkill state was + * changed during setup. + */ + eeepc_rfkill_hotplug(eeepc, handle); + return 0; +} + +static void eeepc_unregister_rfkill_notifier(struct eeepc_laptop *eeepc, + char *node) +{ + acpi_status status = AE_OK; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + + if (ACPI_FAILURE(status)) + return; + + status = acpi_remove_notify_handler(handle, + ACPI_SYSTEM_NOTIFY, + eeepc_rfkill_notify); + if (ACPI_FAILURE(status)) + pr_err("Error removing rfkill notify handler %s\n", + node); + /* + * Refresh pci hotplug in case the rfkill + * state was changed after + * eeepc_unregister_rfkill_notifier() + */ + eeepc_rfkill_hotplug(eeepc, handle); +} + +static int eeepc_get_adapter_status(struct hotplug_slot *hotplug_slot, + u8 *value) +{ + struct eeepc_laptop *eeepc; + int val; + + eeepc = container_of(hotplug_slot, struct eeepc_laptop, hotplug_slot); + val = get_acpi(eeepc, CM_ASL_WLAN); + + if (val == 1 || val == 0) + *value = val; + else + return -EINVAL; + + return 0; +} + +static const struct hotplug_slot_ops eeepc_hotplug_slot_ops = { + .get_adapter_status = eeepc_get_adapter_status, + .get_power_status = eeepc_get_adapter_status, +}; + +static int eeepc_setup_pci_hotplug(struct eeepc_laptop *eeepc) +{ + int ret = -ENOMEM; + struct pci_bus *bus = pci_find_bus(0, 1); + + if (!bus) { + pr_err("Unable to find wifi PCI bus\n"); + return -ENODEV; + } + + eeepc->hotplug_slot.ops = &eeepc_hotplug_slot_ops; + + ret = pci_hp_register(&eeepc->hotplug_slot, bus, 0, "eeepc-wifi"); + if (ret) { + pr_err("Unable to register hotplug slot - %d\n", ret); + goto error_register; + } + + return 0; + +error_register: + eeepc->hotplug_slot.ops = NULL; + return ret; +} + +/* + * Rfkill devices + */ +static int eeepc_rfkill_set(void *data, bool blocked) +{ + acpi_handle handle = data; + + return write_acpi_int(handle, NULL, !blocked); +} + +static const struct rfkill_ops eeepc_rfkill_ops = { + .set_block = eeepc_rfkill_set, +}; + +static int eeepc_new_rfkill(struct eeepc_laptop *eeepc, + struct rfkill **rfkill, + const char *name, + enum rfkill_type type, int cm) +{ + acpi_handle handle; + int result; + + result = acpi_setter_handle(eeepc, cm, &handle); + if (result < 0) + return result; + + *rfkill = rfkill_alloc(name, &eeepc->platform_device->dev, type, + &eeepc_rfkill_ops, handle); + + if (!*rfkill) + return -EINVAL; + + rfkill_init_sw_state(*rfkill, get_acpi(eeepc, cm) != 1); + result = rfkill_register(*rfkill); + if (result) { + rfkill_destroy(*rfkill); + *rfkill = NULL; + return result; + } + return 0; +} + +static char EEEPC_RFKILL_NODE_1[] = "\\_SB.PCI0.P0P5"; +static char EEEPC_RFKILL_NODE_2[] = "\\_SB.PCI0.P0P6"; +static char EEEPC_RFKILL_NODE_3[] = "\\_SB.PCI0.P0P7"; + +static void eeepc_rfkill_exit(struct eeepc_laptop *eeepc) +{ + eeepc_unregister_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_1); + eeepc_unregister_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_2); + eeepc_unregister_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_3); + if (eeepc->wlan_rfkill) { + rfkill_unregister(eeepc->wlan_rfkill); + rfkill_destroy(eeepc->wlan_rfkill); + eeepc->wlan_rfkill = NULL; + } + + if (eeepc->hotplug_slot.ops) + pci_hp_deregister(&eeepc->hotplug_slot); + + if (eeepc->bluetooth_rfkill) { + rfkill_unregister(eeepc->bluetooth_rfkill); + rfkill_destroy(eeepc->bluetooth_rfkill); + eeepc->bluetooth_rfkill = NULL; + } + if (eeepc->wwan3g_rfkill) { + rfkill_unregister(eeepc->wwan3g_rfkill); + rfkill_destroy(eeepc->wwan3g_rfkill); + eeepc->wwan3g_rfkill = NULL; + } + if (eeepc->wimax_rfkill) { + rfkill_unregister(eeepc->wimax_rfkill); + rfkill_destroy(eeepc->wimax_rfkill); + eeepc->wimax_rfkill = NULL; + } +} + +static int eeepc_rfkill_init(struct eeepc_laptop *eeepc) +{ + int result = 0; + + mutex_init(&eeepc->hotplug_lock); + + result = eeepc_new_rfkill(eeepc, &eeepc->wlan_rfkill, + "eeepc-wlan", RFKILL_TYPE_WLAN, + CM_ASL_WLAN); + + if (result && result != -ENODEV) + goto exit; + + result = eeepc_new_rfkill(eeepc, &eeepc->bluetooth_rfkill, + "eeepc-bluetooth", RFKILL_TYPE_BLUETOOTH, + CM_ASL_BLUETOOTH); + + if (result && result != -ENODEV) + goto exit; + + result = eeepc_new_rfkill(eeepc, &eeepc->wwan3g_rfkill, + "eeepc-wwan3g", RFKILL_TYPE_WWAN, + CM_ASL_3G); + + if (result && result != -ENODEV) + goto exit; + + result = eeepc_new_rfkill(eeepc, &eeepc->wimax_rfkill, + "eeepc-wimax", RFKILL_TYPE_WIMAX, + CM_ASL_WIMAX); + + if (result && result != -ENODEV) + goto exit; + + if (eeepc->hotplug_disabled) + return 0; + + result = eeepc_setup_pci_hotplug(eeepc); + /* + * If we get -EBUSY then something else is handling the PCI hotplug - + * don't fail in this case + */ + if (result == -EBUSY) + result = 0; + + eeepc_register_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_1); + eeepc_register_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_2); + eeepc_register_rfkill_notifier(eeepc, EEEPC_RFKILL_NODE_3); + +exit: + if (result && result != -ENODEV) + eeepc_rfkill_exit(eeepc); + return result; +} + +/* + * Platform driver - hibernate/resume callbacks + */ +static int eeepc_hotk_thaw(struct device *device) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(device); + + if (eeepc->wlan_rfkill) { + int wlan; + + /* + * Work around bios bug - acpi _PTS turns off the wireless led + * during suspend. Normally it restores it on resume, but + * we should kick it ourselves in case hibernation is aborted. + */ + wlan = get_acpi(eeepc, CM_ASL_WLAN); + if (wlan >= 0) + set_acpi(eeepc, CM_ASL_WLAN, wlan); + } + + return 0; +} + +static int eeepc_hotk_restore(struct device *device) +{ + struct eeepc_laptop *eeepc = dev_get_drvdata(device); + + /* Refresh both wlan rfkill state and pci hotplug */ + if (eeepc->wlan_rfkill) { + eeepc_rfkill_hotplug_update(eeepc, EEEPC_RFKILL_NODE_1); + eeepc_rfkill_hotplug_update(eeepc, EEEPC_RFKILL_NODE_2); + eeepc_rfkill_hotplug_update(eeepc, EEEPC_RFKILL_NODE_3); + } + + if (eeepc->bluetooth_rfkill) + rfkill_set_sw_state(eeepc->bluetooth_rfkill, + get_acpi(eeepc, CM_ASL_BLUETOOTH) != 1); + if (eeepc->wwan3g_rfkill) + rfkill_set_sw_state(eeepc->wwan3g_rfkill, + get_acpi(eeepc, CM_ASL_3G) != 1); + if (eeepc->wimax_rfkill) + rfkill_set_sw_state(eeepc->wimax_rfkill, + get_acpi(eeepc, CM_ASL_WIMAX) != 1); + + return 0; +} + +static const struct dev_pm_ops eeepc_pm_ops = { + .thaw = eeepc_hotk_thaw, + .restore = eeepc_hotk_restore, +}; + +static struct platform_driver platform_driver = { + .driver = { + .name = EEEPC_LAPTOP_FILE, + .pm = &eeepc_pm_ops, + } +}; + +/* + * Hwmon device + */ + +#define EEEPC_EC_SC00 0x61 +#define EEEPC_EC_FAN_PWM (EEEPC_EC_SC00 + 2) /* Fan PWM duty cycle (%) */ +#define EEEPC_EC_FAN_HRPM (EEEPC_EC_SC00 + 5) /* High byte, fan speed (RPM) */ +#define EEEPC_EC_FAN_LRPM (EEEPC_EC_SC00 + 6) /* Low byte, fan speed (RPM) */ + +#define EEEPC_EC_SFB0 0xD0 +#define EEEPC_EC_FAN_CTRL (EEEPC_EC_SFB0 + 3) /* Byte containing SF25 */ + +static inline int eeepc_pwm_to_lmsensors(int value) +{ + return value * 255 / 100; +} + +static inline int eeepc_lmsensors_to_pwm(int value) +{ + value = clamp_val(value, 0, 255); + return value * 100 / 255; +} + +static int eeepc_get_fan_pwm(void) +{ + u8 value = 0; + + ec_read(EEEPC_EC_FAN_PWM, &value); + return eeepc_pwm_to_lmsensors(value); +} + +static void eeepc_set_fan_pwm(int value) +{ + value = eeepc_lmsensors_to_pwm(value); + ec_write(EEEPC_EC_FAN_PWM, value); +} + +static int eeepc_get_fan_rpm(void) +{ + u8 high = 0; + u8 low = 0; + + ec_read(EEEPC_EC_FAN_HRPM, &high); + ec_read(EEEPC_EC_FAN_LRPM, &low); + return high << 8 | low; +} + +#define EEEPC_EC_FAN_CTRL_BIT 0x02 +#define EEEPC_FAN_CTRL_MANUAL 1 +#define EEEPC_FAN_CTRL_AUTO 2 + +static int eeepc_get_fan_ctrl(void) +{ + u8 value = 0; + + ec_read(EEEPC_EC_FAN_CTRL, &value); + if (value & EEEPC_EC_FAN_CTRL_BIT) + return EEEPC_FAN_CTRL_MANUAL; + else + return EEEPC_FAN_CTRL_AUTO; +} + +static void eeepc_set_fan_ctrl(int manual) +{ + u8 value = 0; + + ec_read(EEEPC_EC_FAN_CTRL, &value); + if (manual == EEEPC_FAN_CTRL_MANUAL) + value |= EEEPC_EC_FAN_CTRL_BIT; + else + value &= ~EEEPC_EC_FAN_CTRL_BIT; + ec_write(EEEPC_EC_FAN_CTRL, value); +} + +static ssize_t store_sys_hwmon(void (*set)(int), const char *buf, size_t count) +{ + int rv, value; + + rv = parse_arg(buf, &value); + if (rv < 0) + return rv; + set(value); + return count; +} + +static ssize_t show_sys_hwmon(int (*get)(void), char *buf) +{ + return sysfs_emit(buf, "%d\n", get()); +} + +#define EEEPC_SENSOR_SHOW_FUNC(_name, _get) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return show_sys_hwmon(_get, buf); \ + } + +#define EEEPC_SENSOR_STORE_FUNC(_name, _set) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return store_sys_hwmon(_set, buf, count); \ + } + +#define EEEPC_CREATE_SENSOR_ATTR_RW(_name, _get, _set) \ + EEEPC_SENSOR_SHOW_FUNC(_name, _get) \ + EEEPC_SENSOR_STORE_FUNC(_name, _set) \ + static DEVICE_ATTR_RW(_name) + +#define EEEPC_CREATE_SENSOR_ATTR_RO(_name, _get) \ + EEEPC_SENSOR_SHOW_FUNC(_name, _get) \ + static DEVICE_ATTR_RO(_name) + +EEEPC_CREATE_SENSOR_ATTR_RO(fan1_input, eeepc_get_fan_rpm); +EEEPC_CREATE_SENSOR_ATTR_RW(pwm1, eeepc_get_fan_pwm, + eeepc_set_fan_pwm); +EEEPC_CREATE_SENSOR_ATTR_RW(pwm1_enable, eeepc_get_fan_ctrl, + eeepc_set_fan_ctrl); + +static struct attribute *hwmon_attrs[] = { + &dev_attr_pwm1.attr, + &dev_attr_fan1_input.attr, + &dev_attr_pwm1_enable.attr, + NULL +}; +ATTRIBUTE_GROUPS(hwmon); + +static int eeepc_hwmon_init(struct eeepc_laptop *eeepc) +{ + struct device *dev = &eeepc->platform_device->dev; + struct device *hwmon; + + hwmon = devm_hwmon_device_register_with_groups(dev, "eeepc", NULL, + hwmon_groups); + if (IS_ERR(hwmon)) { + pr_err("Could not register eeepc hwmon device\n"); + return PTR_ERR(hwmon); + } + return 0; +} + +/* + * Backlight device + */ +static int read_brightness(struct backlight_device *bd) +{ + struct eeepc_laptop *eeepc = bl_get_data(bd); + + return get_acpi(eeepc, CM_ASL_PANELBRIGHT); +} + +static int set_brightness(struct backlight_device *bd, int value) +{ + struct eeepc_laptop *eeepc = bl_get_data(bd); + + return set_acpi(eeepc, CM_ASL_PANELBRIGHT, value); +} + +static int update_bl_status(struct backlight_device *bd) +{ + return set_brightness(bd, bd->props.brightness); +} + +static const struct backlight_ops eeepcbl_ops = { + .get_brightness = read_brightness, + .update_status = update_bl_status, +}; + +static int eeepc_backlight_notify(struct eeepc_laptop *eeepc) +{ + struct backlight_device *bd = eeepc->backlight_device; + int old = bd->props.brightness; + + backlight_force_update(bd, BACKLIGHT_UPDATE_HOTKEY); + + return old; +} + +static int eeepc_backlight_init(struct eeepc_laptop *eeepc) +{ + struct backlight_properties props; + struct backlight_device *bd; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = 15; + bd = backlight_device_register(EEEPC_LAPTOP_FILE, + &eeepc->platform_device->dev, eeepc, + &eeepcbl_ops, &props); + if (IS_ERR(bd)) { + pr_err("Could not register eeepc backlight device\n"); + eeepc->backlight_device = NULL; + return PTR_ERR(bd); + } + eeepc->backlight_device = bd; + bd->props.brightness = read_brightness(bd); + bd->props.power = BACKLIGHT_POWER_ON; + backlight_update_status(bd); + return 0; +} + +static void eeepc_backlight_exit(struct eeepc_laptop *eeepc) +{ + backlight_device_unregister(eeepc->backlight_device); + eeepc->backlight_device = NULL; +} + + +/* + * Input device (i.e. hotkeys) + */ +static int eeepc_input_init(struct eeepc_laptop *eeepc) +{ + struct input_dev *input; + int error; + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input->name = "Asus EeePC extra buttons"; + input->phys = EEEPC_LAPTOP_FILE "/input0"; + input->id.bustype = BUS_HOST; + input->dev.parent = &eeepc->platform_device->dev; + + error = sparse_keymap_setup(input, eeepc_keymap, NULL); + if (error) { + pr_err("Unable to setup input device keymap\n"); + goto err_free_dev; + } + + error = input_register_device(input); + if (error) { + pr_err("Unable to register input device\n"); + goto err_free_dev; + } + + eeepc->inputdev = input; + return 0; + +err_free_dev: + input_free_device(input); + return error; +} + +static void eeepc_input_exit(struct eeepc_laptop *eeepc) +{ + if (eeepc->inputdev) + input_unregister_device(eeepc->inputdev); + eeepc->inputdev = NULL; +} + +/* + * ACPI driver + */ +static void eeepc_input_notify(struct eeepc_laptop *eeepc, int event) +{ + if (!eeepc->inputdev) + return; + if (!sparse_keymap_report_event(eeepc->inputdev, event, 1, true)) + pr_info("Unknown key %x pressed\n", event); +} + +static void eeepc_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct eeepc_laptop *eeepc = data; + struct acpi_device *device = eeepc->device; + int old_brightness, new_brightness; + u16 count; + + if (event > ACPI_MAX_SYS_NOTIFY) + return; + count = eeepc->event_count[event % 128]++; + acpi_bus_generate_netlink_event(device->pnp.device_class, + dev_name(&device->dev), event, + count); + + /* Brightness events are special */ + if (event < NOTIFY_BRN_MIN || event > NOTIFY_BRN_MAX) { + eeepc_input_notify(eeepc, event); + return; + } + + /* Ignore them completely if the acpi video driver is used */ + if (!eeepc->backlight_device) + return; + + /* Update the backlight device. */ + old_brightness = eeepc_backlight_notify(eeepc); + + /* Convert event to keypress (obsolescent hack) */ + new_brightness = event - NOTIFY_BRN_MIN; + + if (new_brightness < old_brightness) { + event = NOTIFY_BRN_MIN; /* brightness down */ + } else if (new_brightness > old_brightness) { + event = NOTIFY_BRN_MAX; /* brightness up */ + } else { + /* + * no change in brightness - already at min/max, + * event will be desired value (or else ignored) + */ + } + eeepc_input_notify(eeepc, event); +} + +static void eeepc_dmi_check(struct eeepc_laptop *eeepc) +{ + const char *model; + + model = dmi_get_system_info(DMI_PRODUCT_NAME); + if (!model) + return; + + /* + * Blacklist for setting cpufv (cpu speed). + * + * EeePC 4G ("701") implements CFVS, but it is not supported + * by the pre-installed OS, and the original option to change it + * in the BIOS setup screen was removed in later versions. + * + * Judging by the lack of "Super Hybrid Engine" on Asus product pages, + * this applies to all "701" models (4G/4G Surf/2G Surf). + * + * So Asus made a deliberate decision not to support it on this model. + * We have several reports that using it can cause the system to hang + * + * The hang has also been reported on a "702" (Model name "8G"?). + * + * We avoid dmi_check_system() / dmi_match(), because they use + * substring matching. We don't want to affect the "701SD" + * and "701SDX" models, because they do support S.H.E. + */ + if (strcmp(model, "701") == 0 || strcmp(model, "702") == 0) { + eeepc->cpufv_disabled = true; + pr_info("model %s does not officially support setting cpu speed\n", + model); + pr_info("cpufv disabled to avoid instability\n"); + } + + /* + * Blacklist for wlan hotplug + * + * Eeepc 1005HA doesn't work like others models and don't need the + * hotplug code. In fact, current hotplug code seems to unplug another + * device... + */ + if (strcmp(model, "1005HA") == 0 || strcmp(model, "1201N") == 0 || + strcmp(model, "1005PE") == 0) { + eeepc->hotplug_disabled = true; + pr_info("wlan hotplug disabled\n"); + } +} + +static void cmsg_quirk(struct eeepc_laptop *eeepc, int cm, const char *name) +{ + int dummy; + + /* Some BIOSes do not report cm although it is available. + Check if cm_getv[cm] works and, if yes, assume cm should be set. */ + if (!(eeepc->cm_supported & (1 << cm)) + && !read_acpi_int(eeepc->handle, cm_getv[cm], &dummy)) { + pr_info("%s (%x) not reported by BIOS, enabling anyway\n", + name, 1 << cm); + eeepc->cm_supported |= 1 << cm; + } +} + +static void cmsg_quirks(struct eeepc_laptop *eeepc) +{ + cmsg_quirk(eeepc, CM_ASL_LID, "LID"); + cmsg_quirk(eeepc, CM_ASL_TYPE, "TYPE"); + cmsg_quirk(eeepc, CM_ASL_PANELPOWER, "PANELPOWER"); + cmsg_quirk(eeepc, CM_ASL_TPD, "TPD"); +} + +static int eeepc_acpi_init(struct eeepc_laptop *eeepc) +{ + unsigned int init_flags; + int result; + + result = acpi_bus_get_status(eeepc->device); + if (result) + return result; + if (!eeepc->device->status.present) { + pr_err("Hotkey device not present, aborting\n"); + return -ENODEV; + } + + init_flags = DISABLE_ASL_WLAN | DISABLE_ASL_DISPLAYSWITCH; + pr_notice("Hotkey init flags 0x%x\n", init_flags); + + if (write_acpi_int(eeepc->handle, "INIT", init_flags)) { + pr_err("Hotkey initialization failed\n"); + return -ENODEV; + } + + /* get control methods supported */ + if (read_acpi_int(eeepc->handle, "CMSG", &eeepc->cm_supported)) { + pr_err("Get control methods supported failed\n"); + return -ENODEV; + } + cmsg_quirks(eeepc); + pr_info("Get control methods supported: 0x%x\n", eeepc->cm_supported); + + return 0; +} + +static void eeepc_enable_camera(struct eeepc_laptop *eeepc) +{ + /* + * If the following call to set_acpi() fails, it's because there's no + * camera so we can ignore the error. + */ + if (get_acpi(eeepc, CM_ASL_CAMERA) == 0) + set_acpi(eeepc, CM_ASL_CAMERA, 1); +} + +static bool eeepc_device_present; + +static int eeepc_acpi_probe(struct platform_device *pdev) +{ + struct acpi_device *device; + struct eeepc_laptop *eeepc; + int result; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + pr_notice(EEEPC_LAPTOP_NAME "\n"); + eeepc = kzalloc_obj(struct eeepc_laptop); + if (!eeepc) + return -ENOMEM; + eeepc->handle = device->handle; + strscpy(acpi_device_name(device), EEEPC_ACPI_DEVICE_NAME); + strscpy(acpi_device_class(device), EEEPC_ACPI_CLASS); + eeepc->device = device; + + platform_set_drvdata(pdev, eeepc); + + eeepc->hotplug_disabled = hotplug_disabled; + + eeepc_dmi_check(eeepc); + + result = eeepc_acpi_init(eeepc); + if (result) + goto fail_platform; + eeepc_enable_camera(eeepc); + + /* + * Register the platform device first. It is used as a parent for the + * sub-devices below. + * + * Note that if there are multiple instances of this ACPI device it + * will bail out, because the platform device is registered with a + * fixed name. Of course it doesn't make sense to have more than one, + * and machine-specific scripts find the fixed name convenient. But + * It's also good for us to exclude multiple instances because both + * our hwmon and our wlan rfkill subdevice use global ACPI objects + * (the EC and the PCI wlan slot respectively). + */ + result = eeepc_platform_init(eeepc); + if (result) + goto fail_platform; + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + result = eeepc_backlight_init(eeepc); + if (result) + goto fail_backlight; + } + + result = eeepc_input_init(eeepc); + if (result) + goto fail_input; + + result = eeepc_hwmon_init(eeepc); + if (result) + goto fail_hwmon; + + result = eeepc_led_init(eeepc); + if (result) + goto fail_led; + + result = eeepc_rfkill_init(eeepc); + if (result) + goto fail_rfkill; + + result = acpi_dev_install_notify_handler(device, ACPI_ALL_NOTIFY, + eeepc_acpi_notify, eeepc); + if (result) + goto fail_acpi_notifier; + + eeepc_device_present = true; + return 0; + +fail_acpi_notifier: + eeepc_rfkill_exit(eeepc); +fail_rfkill: + eeepc_led_exit(eeepc); +fail_led: +fail_hwmon: + eeepc_input_exit(eeepc); +fail_input: + eeepc_backlight_exit(eeepc); +fail_backlight: + eeepc_platform_exit(eeepc); +fail_platform: + kfree(eeepc); + + return result; +} + +static void eeepc_acpi_remove(struct platform_device *pdev) +{ + struct eeepc_laptop *eeepc = platform_get_drvdata(pdev); + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_ALL_NOTIFY, eeepc_acpi_notify); + eeepc_backlight_exit(eeepc); + eeepc_rfkill_exit(eeepc); + eeepc_input_exit(eeepc); + eeepc_led_exit(eeepc); + eeepc_platform_exit(eeepc); + + kfree(eeepc); +} + + +static const struct acpi_device_id eeepc_device_ids[] = { + {EEEPC_ACPI_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, eeepc_device_ids); + +static struct platform_driver eeepc_acpi_driver = { + .probe = eeepc_acpi_probe, + .remove = eeepc_acpi_remove, + .driver = { + .name = EEEPC_LAPTOP_NAME, + .acpi_match_table = eeepc_device_ids, + }, +}; + + +static int __init eeepc_laptop_init(void) +{ + int result; + + result = platform_driver_register(&platform_driver); + if (result < 0) + return result; + + result = platform_driver_register(&eeepc_acpi_driver); + if (result < 0) + goto fail_acpi_driver; + + if (!eeepc_device_present) { + result = -ENODEV; + goto fail_no_device; + } + + return 0; + +fail_no_device: + platform_driver_unregister(&eeepc_acpi_driver); +fail_acpi_driver: + platform_driver_unregister(&platform_driver); + return result; +} + +static void __exit eeepc_laptop_exit(void) +{ + platform_driver_unregister(&eeepc_acpi_driver); + platform_driver_unregister(&platform_driver); +} + +module_init(eeepc_laptop_init); +module_exit(eeepc_laptop_exit); diff --git a/drivers/platform/x86/eeepc-wmi.c b/drivers/platform/x86/eeepc-wmi.c new file mode 100644 index 000000000..37edb9ae6 --- /dev/null +++ b/drivers/platform/x86/eeepc-wmi.c @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Eee PC WMI hotkey driver + * + * Copyright(C) 2010 Intel Corporation. + * Copyright(C) 2010-2011 Corentin Chary + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac + * Copyright (C) 2005 Bernhard Rosenkraenzer + * Copyright (C) 2005 Dmitry Torokhov + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "asus-wmi.h" + +#define EEEPC_WMI_FILE "eeepc-wmi" + +MODULE_AUTHOR("Corentin Chary "); +MODULE_DESCRIPTION("Eee PC WMI Hotkey Driver"); +MODULE_LICENSE("GPL"); + +#define EEEPC_ACPI_HID "ASUS010" /* old _HID used in eeepc-laptop */ + +#define EEEPC_WMI_EVENT_GUID "ABBC0F72-8EA1-11D1-00A0-C90629100000" + +MODULE_ALIAS("wmi:"EEEPC_WMI_EVENT_GUID); + +static bool hotplug_wireless; + +module_param(hotplug_wireless, bool, 0444); +MODULE_PARM_DESC(hotplug_wireless, + "Enable hotplug for wireless device. " + "If your laptop needs that, please report to " + "acpi4asus-user@lists.sourceforge.net."); + +/* Values for T101MT "Home" key */ +#define HOME_PRESS 0xe4 +#define HOME_HOLD 0xea +#define HOME_RELEASE 0xe5 + +static const struct key_entry eeepc_wmi_keymap[] = { + { KE_KEY, ASUS_WMI_BRN_DOWN, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, ASUS_WMI_BRN_UP, { KEY_BRIGHTNESSUP } }, + /* Sleep already handled via generic ACPI code */ + { KE_KEY, 0x30, { KEY_VOLUMEUP } }, + { KE_KEY, 0x31, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0x32, { KEY_MUTE } }, + { KE_KEY, 0x5c, { KEY_F15 } }, /* Power Gear key */ + { KE_KEY, 0x5d, { KEY_WLAN } }, + { KE_KEY, 0x6b, { KEY_TOUCHPAD_TOGGLE } }, /* Toggle Touchpad */ + { KE_KEY, 0x82, { KEY_CAMERA } }, + { KE_KEY, 0x83, { KEY_CAMERA_ZOOMIN } }, + { KE_KEY, 0x88, { KEY_WLAN } }, + { KE_KEY, 0xbd, { KEY_CAMERA } }, + { KE_KEY, 0xcc, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0xe0, { KEY_PROG1 } }, /* Task Manager */ + { KE_KEY, 0xe1, { KEY_F14 } }, /* Change Resolution */ + { KE_KEY, HOME_PRESS, { KEY_CONFIG } }, /* Home/Express gate key */ + { KE_KEY, 0xe8, { KEY_SCREENLOCK } }, + { KE_KEY, 0xe9, { KEY_DISPLAYTOGGLE } }, + { KE_KEY, 0xeb, { KEY_CAMERA_ZOOMOUT } }, + { KE_KEY, 0xec, { KEY_CAMERA_UP } }, + { KE_KEY, 0xed, { KEY_CAMERA_DOWN } }, + { KE_KEY, 0xee, { KEY_CAMERA_LEFT } }, + { KE_KEY, 0xef, { KEY_CAMERA_RIGHT } }, + { KE_KEY, 0xf3, { KEY_MENU } }, + { KE_KEY, 0xf5, { KEY_HOMEPAGE } }, + { KE_KEY, 0xf6, { KEY_ESC } }, + { KE_END, 0}, +}; + +static struct quirk_entry quirk_asus_unknown = { +}; + +static struct quirk_entry quirk_asus_1000h = { + .hotplug_wireless = true, +}; + +static struct quirk_entry quirk_asus_et2012_type1 = { + .store_backlight_power = true, +}; + +static struct quirk_entry quirk_asus_et2012_type3 = { + .scalar_panel_brightness = true, + .store_backlight_power = true, +}; + +static struct quirk_entry *quirks; + +static void et2012_quirks(void) +{ + const struct dmi_device *dev = NULL; + char oemstring[30]; + + while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) { + if (sscanf(dev->name, "AEMS%24c", oemstring) == 1) { + if (oemstring[18] == '1') + quirks = &quirk_asus_et2012_type1; + else if (oemstring[18] == '3') + quirks = &quirk_asus_et2012_type3; + break; + } + } +} + +static int dmi_matched(const struct dmi_system_id *dmi) +{ + char *model; + + quirks = dmi->driver_data; + + model = (char *)dmi->matches[1].substr; + if (unlikely(strncmp(model, "ET2012", 6) == 0)) + et2012_quirks(); + + return 1; +} + +static const struct dmi_system_id asus_quirks[] = { + { + .callback = dmi_matched, + .ident = "ASUSTeK Computer INC. 1000H", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK Computer INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "1000H"), + }, + .driver_data = &quirk_asus_1000h, + }, + { + .callback = dmi_matched, + .ident = "ASUSTeK Computer INC. ET2012E/I", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK Computer INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "ET2012"), + }, + .driver_data = &quirk_asus_unknown, + }, + {} +}; + +static void eeepc_wmi_key_filter(struct asus_wmi_driver *asus_wmi, int *code, + unsigned int *value, bool *autorelease) +{ + switch (*code) { + case HOME_PRESS: + *value = 1; + *autorelease = 0; + break; + case HOME_HOLD: + *code = ASUS_WMI_KEY_IGNORE; + break; + case HOME_RELEASE: + *code = HOME_PRESS; + *value = 0; + *autorelease = 0; + break; + } +} + +static int eeepc_wmi_probe(struct platform_device *pdev) +{ + if (acpi_dev_found(EEEPC_ACPI_HID)) { + pr_warn("Found legacy ATKD device (%s)\n", EEEPC_ACPI_HID); + pr_warn("WMI device present, but legacy ATKD device is also " + "present and enabled\n"); + pr_warn("You probably booted with acpi_osi=\"Linux\" or " + "acpi_osi=\"!Windows 2009\"\n"); + pr_warn("Can't load eeepc-wmi, use default acpi_osi " + "(preferred) or eeepc-laptop\n"); + return -EBUSY; + } + return 0; +} + +static void eeepc_wmi_quirks(struct asus_wmi_driver *driver) +{ + quirks = &quirk_asus_unknown; + quirks->hotplug_wireless = hotplug_wireless; + + dmi_check_system(asus_quirks); + + driver->quirks = quirks; + driver->quirks->wapf = -1; + driver->panel_power = BACKLIGHT_POWER_ON; +} + +static struct asus_wmi_driver asus_wmi_driver = { + .name = EEEPC_WMI_FILE, + .owner = THIS_MODULE, + .event_guid = EEEPC_WMI_EVENT_GUID, + .keymap = eeepc_wmi_keymap, + .input_name = "Eee PC WMI hotkeys", + .input_phys = EEEPC_WMI_FILE "/input0", + .key_filter = eeepc_wmi_key_filter, + .probe = eeepc_wmi_probe, + .detect_quirks = eeepc_wmi_quirks, +}; + + +static int __init eeepc_wmi_init(void) +{ + return asus_wmi_register_driver(&asus_wmi_driver); +} + +static void __exit eeepc_wmi_exit(void) +{ + asus_wmi_unregister_driver(&asus_wmi_driver); +} + +module_init(eeepc_wmi_init); +module_exit(eeepc_wmi_exit); diff --git a/drivers/platform/x86/firmware_attributes_class.c b/drivers/platform/x86/firmware_attributes_class.c new file mode 100644 index 000000000..736e96c18 --- /dev/null +++ b/drivers/platform/x86/firmware_attributes_class.c @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* Firmware attributes class helper module */ + +#include +#include "firmware_attributes_class.h" + +const struct class firmware_attributes_class = { + .name = "firmware-attributes", +}; +EXPORT_SYMBOL_GPL(firmware_attributes_class); + +static __init int fw_attributes_class_init(void) +{ + return class_register(&firmware_attributes_class); +} +module_init(fw_attributes_class_init); + +static __exit void fw_attributes_class_exit(void) +{ + class_unregister(&firmware_attributes_class); +} +module_exit(fw_attributes_class_exit); + +MODULE_AUTHOR("Mark Pearson "); +MODULE_DESCRIPTION("Firmware attributes class helper module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/firmware_attributes_class.h b/drivers/platform/x86/firmware_attributes_class.h new file mode 100644 index 000000000..d27abe54f --- /dev/null +++ b/drivers/platform/x86/firmware_attributes_class.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +/* Firmware attributes class helper module */ + +#ifndef FW_ATTR_CLASS_H +#define FW_ATTR_CLASS_H + +#include + +extern const struct class firmware_attributes_class; + +#endif /* FW_ATTR_CLASS_H */ diff --git a/drivers/platform/x86/fujitsu-laptop.c b/drivers/platform/x86/fujitsu-laptop.c new file mode 100644 index 000000000..54d0b9cec --- /dev/null +++ b/drivers/platform/x86/fujitsu-laptop.c @@ -0,0 +1,1197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2007,2008 Jonathan Woithe + Copyright (C) 2008 Peter Gruber + Copyright (C) 2008 Tony Vroon + Based on earlier work: + Copyright (C) 2003 Shane Spencer + Adrian Yee + + Templated from msi-laptop.c and thinkpad_acpi.c which is copyright + by its respective authors. + + */ + +/* + * fujitsu-laptop.c - Fujitsu laptop support, providing access to additional + * features made available on a range of Fujitsu laptops including the + * P2xxx/P5xxx/S2xxx/S6xxx/S7xxx series. + * + * This driver implements a vendor-specific backlight control interface for + * Fujitsu laptops and provides support for hotkeys present on certain Fujitsu + * laptops. + * + * This driver has been tested on a Fujitsu Lifebook S2110, S6410, S7020 and + * P8010. It should work on most P-series and S-series Lifebooks, but + * YMMV. + * + * The module parameter use_alt_lcd_levels switches between different ACPI + * brightness controls which are used by different Fujitsu laptops. In most + * cases the correct method is automatically detected. "use_alt_lcd_levels=1" + * is applicable for a Fujitsu Lifebook S6410 if autodetection fails. + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUJITSU_DRIVER_VERSION "0.6.0" + +#define FUJITSU_LCD_N_LEVELS 8 + +#define ACPI_FUJITSU_CLASS "fujitsu" +#define ACPI_FUJITSU_BL_HID "FUJ02B1" +#define ACPI_FUJITSU_BL_DRIVER_NAME "Fujitsu laptop FUJ02B1 ACPI brightness driver" +#define ACPI_FUJITSU_BL_DEVICE_NAME "Fujitsu FUJ02B1" +#define ACPI_FUJITSU_LAPTOP_HID "FUJ02E3" +#define ACPI_FUJITSU_LAPTOP_DRIVER_NAME "Fujitsu laptop FUJ02E3 ACPI hotkeys driver" +#define ACPI_FUJITSU_LAPTOP_DEVICE_NAME "Fujitsu FUJ02E3" + +#define ACPI_FUJITSU_NOTIFY_CODE 0x80 + +/* FUNC interface - command values */ +#define FUNC_FLAGS BIT(12) +#define FUNC_LEDS (BIT(12) | BIT(0)) +#define FUNC_BUTTONS (BIT(12) | BIT(1)) +#define FUNC_BACKLIGHT (BIT(12) | BIT(2)) + +/* FUNC interface - responses */ +#define UNSUPPORTED_CMD 0x80000000 + +/* FUNC interface - status flags */ +#define FLAG_RFKILL BIT(5) +#define FLAG_LID BIT(8) +#define FLAG_DOCK BIT(9) +#define FLAG_TOUCHPAD_TOGGLE BIT(26) +#define FLAG_MICMUTE BIT(29) +#define FLAG_SOFTKEYS (FLAG_RFKILL | FLAG_TOUCHPAD_TOGGLE | FLAG_MICMUTE) + +/* FUNC interface - LED control */ +#define FUNC_LED_OFF BIT(0) +#define FUNC_LED_ON (BIT(0) | BIT(16) | BIT(17)) +#define LOGOLAMP_POWERON BIT(13) +#define LOGOLAMP_ALWAYS BIT(14) +#define KEYBOARD_LAMPS BIT(8) +#define RADIO_LED_ON BIT(5) +#define ECO_LED BIT(16) +#define ECO_LED_ON BIT(19) + +/* FUNC interface - backlight power control */ +#define BACKLIGHT_PARAM_POWER BIT(2) +#define BACKLIGHT_OFF (BIT(0) | BIT(1)) +#define BACKLIGHT_ON 0 + +/* FUNC interface - battery control interface */ +#define FUNC_S006_METHOD 0x1006 +#define CHARGE_CONTROL_RW 0x21 + +/* Scancodes read from the GIRB register */ +#define KEY1_CODE 0x410 +#define KEY2_CODE 0x411 +#define KEY3_CODE 0x412 +#define KEY4_CODE 0x413 +#define KEY5_CODE 0x414 +#define KEY6_CODE 0x415 +#define KEY7_CODE 0x416 +#define KEY8_CODE 0x417 +#define KEY9_CODE 0x420 + +/* Hotkey ringbuffer limits */ +#define MAX_HOTKEY_RINGBUFFER_SIZE 100 +#define RINGBUFFERSIZE 40 + +/* Module parameters */ +static int use_alt_lcd_levels = -1; +static bool disable_brightness_adjust; + +/* Device controlling the backlight and associated keys */ +struct fujitsu_bl { + struct input_dev *input; + char phys[32]; + struct backlight_device *bl_device; + unsigned int max_brightness; + unsigned int brightness_level; +}; + +static struct fujitsu_bl *fujitsu_bl; + +/* Device used to access hotkeys and other features on the laptop */ +struct fujitsu_laptop { + struct input_dev *input; + char phys[32]; + struct platform_device *pf_device; + struct kfifo fifo; + spinlock_t fifo_lock; + int flags_supported; + int flags_state; + bool charge_control_supported; +}; + +static struct device *fext; + +/* Fujitsu ACPI interface function */ + +static int call_fext_func(struct device *dev, + int func, int op, int feature, int state) +{ + union acpi_object params[4] = { + { .integer.type = ACPI_TYPE_INTEGER, .integer.value = func }, + { .integer.type = ACPI_TYPE_INTEGER, .integer.value = op }, + { .integer.type = ACPI_TYPE_INTEGER, .integer.value = feature }, + { .integer.type = ACPI_TYPE_INTEGER, .integer.value = state } + }; + struct acpi_object_list arg_list = { 4, params }; + acpi_handle handle = ACPI_HANDLE(dev); + unsigned long long value; + acpi_status status; + + status = acpi_evaluate_integer(handle, "FUNC", &arg_list, &value); + if (ACPI_FAILURE(status)) { + acpi_handle_err(handle, "Failed to evaluate FUNC\n"); + return -ENODEV; + } + + acpi_handle_debug(handle, "FUNC 0x%x (args 0x%x, 0x%x, 0x%x) returned 0x%x\n", + func, op, feature, state, (int)value); + return value; +} + +/* Battery charge control code */ +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int cc_end_value, s006_cc_return; + unsigned int value; + int ret; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value > 100) + return -EINVAL; + + if (value < 50) + value = 50; + + cc_end_value = value * 0x100 + 0x20; + s006_cc_return = call_fext_func(fext, FUNC_S006_METHOD, + CHARGE_CONTROL_RW, cc_end_value, 0x0); + if (s006_cc_return < 0) + return s006_cc_return; + /* + * The S006 0x21 method returns 0x00 in case the provided value + * is invalid. + */ + if (s006_cc_return == 0x00) + return -EINVAL; + + return count; +} + +static ssize_t charge_control_end_threshold_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int status; + + status = call_fext_func(fext, FUNC_S006_METHOD, + CHARGE_CONTROL_RW, 0x21, 0x0); + if (status < 0) + return status; + + return sysfs_emit(buf, "%d\n", status); +} + +static DEVICE_ATTR_RW(charge_control_end_threshold); + +/* ACPI battery hook */ +static int fujitsu_battery_add_hook(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + return device_create_file(&battery->dev, + &dev_attr_charge_control_end_threshold); +} + +static int fujitsu_battery_remove_hook(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + device_remove_file(&battery->dev, + &dev_attr_charge_control_end_threshold); + + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = fujitsu_battery_add_hook, + .remove_battery = fujitsu_battery_remove_hook, + .name = "Fujitsu Battery Extension", +}; + +/* + * These functions are intended to be called from acpi_fujitsu_laptop_add and + * acpi_fujitsu_laptop_remove. + */ +static int fujitsu_battery_charge_control_add(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + int s006_cc_return; + + priv->charge_control_supported = false; + /* + * Check if the S006 0x21 method exists by trying to get the current + * battery charge limit. + */ + s006_cc_return = call_fext_func(fext, FUNC_S006_METHOD, + CHARGE_CONTROL_RW, 0x21, 0x0); + if (s006_cc_return < 0) + return s006_cc_return; + if (s006_cc_return == UNSUPPORTED_CMD) + return -ENODEV; + + priv->charge_control_supported = true; + battery_hook_register(&battery_hook); + + return 0; +} + +static void fujitsu_battery_charge_control_remove(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + + if (priv->charge_control_supported) + battery_hook_unregister(&battery_hook); +} + +/* Hardware access for LCD brightness control */ + +static int set_lcd_level(struct device *dev, int level) +{ + struct fujitsu_bl *priv = dev_get_drvdata(dev); + acpi_handle handle = ACPI_HANDLE(dev); + acpi_status status; + char *method; + + switch (use_alt_lcd_levels) { + case -1: + if (acpi_has_method(handle, "SBL2")) + method = "SBL2"; + else + method = "SBLL"; + break; + case 1: + method = "SBL2"; + break; + default: + method = "SBLL"; + break; + } + + acpi_handle_debug(handle, "set lcd level via %s [%d]\n", method, level); + + if (level < 0 || level >= priv->max_brightness) + return -EINVAL; + + status = acpi_execute_simple_method(handle, method, level); + if (ACPI_FAILURE(status)) { + acpi_handle_err(handle, "Failed to evaluate %s\n", method); + return -ENODEV; + } + + priv->brightness_level = level; + + return 0; +} + +static int get_lcd_level(struct device *dev) +{ + struct fujitsu_bl *priv = dev_get_drvdata(dev); + acpi_handle handle = ACPI_HANDLE(dev); + unsigned long long state = 0; + acpi_status status = AE_OK; + + acpi_handle_debug(handle, "get lcd level via GBLL\n"); + + status = acpi_evaluate_integer(handle, "GBLL", NULL, &state); + if (ACPI_FAILURE(status)) + return 0; + + priv->brightness_level = state & 0x0fffffff; + + return priv->brightness_level; +} + +static int get_max_brightness(struct device *dev) +{ + struct fujitsu_bl *priv = dev_get_drvdata(dev); + acpi_handle handle = ACPI_HANDLE(dev); + unsigned long long state = 0; + acpi_status status = AE_OK; + + acpi_handle_debug(handle, "get max lcd level via RBLL\n"); + + status = acpi_evaluate_integer(handle, "RBLL", NULL, &state); + if (ACPI_FAILURE(status)) + return -1; + + priv->max_brightness = state; + + return priv->max_brightness; +} + +/* Backlight device stuff */ + +static int bl_get_brightness(struct backlight_device *b) +{ + struct device *dev = bl_get_data(b); + + return b->props.power == BACKLIGHT_POWER_OFF ? 0 : get_lcd_level(dev); +} + +static int bl_update_status(struct backlight_device *b) +{ + if (fext) { + if (b->props.power == BACKLIGHT_POWER_OFF) + call_fext_func(fext, FUNC_BACKLIGHT, 0x1, + BACKLIGHT_PARAM_POWER, BACKLIGHT_OFF); + else + call_fext_func(fext, FUNC_BACKLIGHT, 0x1, + BACKLIGHT_PARAM_POWER, BACKLIGHT_ON); + } + + return set_lcd_level(bl_get_data(b), b->props.brightness); +} + +static const struct backlight_ops fujitsu_bl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + +static ssize_t lid_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + + if (!(priv->flags_supported & FLAG_LID)) + return sysfs_emit(buf, "unknown\n"); + if (priv->flags_state & FLAG_LID) + return sysfs_emit(buf, "open\n"); + else + return sysfs_emit(buf, "closed\n"); +} + +static ssize_t dock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + + if (!(priv->flags_supported & FLAG_DOCK)) + return sysfs_emit(buf, "unknown\n"); + if (priv->flags_state & FLAG_DOCK) + return sysfs_emit(buf, "docked\n"); + else + return sysfs_emit(buf, "undocked\n"); +} + +static ssize_t radios_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + + if (!(priv->flags_supported & FLAG_RFKILL)) + return sysfs_emit(buf, "unknown\n"); + if (priv->flags_state & FLAG_RFKILL) + return sysfs_emit(buf, "on\n"); + else + return sysfs_emit(buf, "killed\n"); +} + +static DEVICE_ATTR_RO(lid); +static DEVICE_ATTR_RO(dock); +static DEVICE_ATTR_RO(radios); + +static struct attribute *fujitsu_pf_attributes[] = { + &dev_attr_lid.attr, + &dev_attr_dock.attr, + &dev_attr_radios.attr, + NULL +}; + +static const struct attribute_group fujitsu_pf_attribute_group = { + .attrs = fujitsu_pf_attributes +}; + +static struct platform_driver fujitsu_pf_driver = { + .driver = { + .name = "fujitsu-laptop", + } +}; + +/* ACPI device for LCD brightness control */ + +static const struct key_entry keymap_backlight[] = { + { KE_KEY, true, { KEY_BRIGHTNESSUP } }, + { KE_KEY, false, { KEY_BRIGHTNESSDOWN } }, + { KE_END, 0 } +}; + +static int acpi_fujitsu_bl_input_setup(struct device *dev) +{ + struct fujitsu_bl *priv = dev_get_drvdata(dev); + struct acpi_device *device = ACPI_COMPANION(dev); + int ret; + + priv->input = devm_input_allocate_device(dev); + if (!priv->input) + return -ENOMEM; + + snprintf(priv->phys, sizeof(priv->phys), "%s/video/input0", + acpi_device_hid(device)); + + priv->input->name = acpi_device_name(device); + priv->input->phys = priv->phys; + priv->input->id.bustype = BUS_HOST; + priv->input->id.product = 0x06; + + ret = sparse_keymap_setup(priv->input, keymap_backlight, NULL); + if (ret) + return ret; + + return input_register_device(priv->input); +} + +static int fujitsu_backlight_register(struct device *dev) +{ + struct fujitsu_bl *priv = dev_get_drvdata(dev); + const struct backlight_properties props = { + .brightness = priv->brightness_level, + .max_brightness = priv->max_brightness - 1, + .type = BACKLIGHT_PLATFORM + }; + struct backlight_device *bd; + + bd = devm_backlight_device_register(dev, "fujitsu-laptop", + dev, dev, &fujitsu_bl_ops, &props); + if (IS_ERR(bd)) + return PTR_ERR(bd); + + priv->bl_device = bd; + + return 0; +} + +/* Brightness notify */ + +static void acpi_fujitsu_bl_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct fujitsu_bl *priv = dev_get_drvdata(dev); + int oldb, newb; + + if (event != ACPI_FUJITSU_NOTIFY_CODE) { + acpi_handle_info(handle, "unsupported event [0x%x]\n", event); + sparse_keymap_report_event(priv->input, -1, 1, true); + return; + } + + oldb = priv->brightness_level; + get_lcd_level(dev); + newb = priv->brightness_level; + + acpi_handle_debug(handle, "brightness button event [%i -> %i]\n", + oldb, newb); + + if (oldb == newb) + return; + + if (!disable_brightness_adjust) + set_lcd_level(dev, newb); + + sparse_keymap_report_event(priv->input, oldb < newb, 1, true); +} + +static int acpi_fujitsu_bl_probe(struct platform_device *pdev) +{ + struct acpi_device *device; + struct fujitsu_bl *priv; + int ret; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + fujitsu_bl = priv; + strscpy(acpi_device_name(device), ACPI_FUJITSU_BL_DEVICE_NAME); + strscpy(acpi_device_class(device), ACPI_FUJITSU_CLASS); + + platform_set_drvdata(pdev, priv); + + pr_info("ACPI: %s [%s]\n", + acpi_device_name(device), acpi_device_bid(device)); + + if (get_max_brightness(&pdev->dev) <= 0) + priv->max_brightness = FUJITSU_LCD_N_LEVELS; + get_lcd_level(&pdev->dev); + + ret = acpi_fujitsu_bl_input_setup(&pdev->dev); + if (ret) + return ret; + + ret = fujitsu_backlight_register(&pdev->dev); + if (ret) + return ret; + + return acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + acpi_fujitsu_bl_notify, &pdev->dev); +} + +static void acpi_fujitsu_bl_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, acpi_fujitsu_bl_notify); +} + +/* ACPI device for hotkey handling */ + +static const struct key_entry keymap_default[] = { + { KE_KEY, KEY1_CODE, { KEY_PROG1 } }, + { KE_KEY, KEY2_CODE, { KEY_PROG2 } }, + { KE_KEY, KEY3_CODE, { KEY_PROG3 } }, + { KE_KEY, KEY4_CODE, { KEY_PROG4 } }, + { KE_KEY, KEY9_CODE, { KEY_RFKILL } }, + /* Soft keys read from status flags */ + { KE_KEY, FLAG_RFKILL, { KEY_RFKILL } }, + { KE_KEY, FLAG_TOUCHPAD_TOGGLE, { KEY_TOUCHPAD_TOGGLE } }, + { KE_KEY, FLAG_MICMUTE, { KEY_MICMUTE } }, + { KE_END, 0 } +}; + +static const struct key_entry keymap_s64x0[] = { + { KE_KEY, KEY1_CODE, { KEY_SCREENLOCK } }, /* "Lock" */ + { KE_KEY, KEY2_CODE, { KEY_HELP } }, /* "Mobility Center */ + { KE_KEY, KEY3_CODE, { KEY_PROG3 } }, + { KE_KEY, KEY4_CODE, { KEY_PROG4 } }, + { KE_END, 0 } +}; + +static const struct key_entry keymap_p8010[] = { + { KE_KEY, KEY1_CODE, { KEY_HELP } }, /* "Support" */ + { KE_KEY, KEY2_CODE, { KEY_PROG2 } }, + { KE_KEY, KEY3_CODE, { KEY_SWITCHVIDEOMODE } }, /* "Presentation" */ + { KE_KEY, KEY4_CODE, { KEY_WWW } }, /* "WWW" */ + { KE_END, 0 } +}; + +static const struct key_entry keymap_s2110[] = { + { KE_KEY, KEY1_CODE, { KEY_PROG1 } }, /* "A" */ + { KE_KEY, KEY2_CODE, { KEY_PROG2 } }, /* "B" */ + { KE_KEY, KEY3_CODE, { KEY_WWW } }, /* "Internet" */ + { KE_KEY, KEY4_CODE, { KEY_EMAIL } }, /* "E-mail" */ + { KE_KEY, KEY5_CODE, { KEY_STOPCD } }, + { KE_KEY, KEY6_CODE, { KEY_PLAYPAUSE } }, + { KE_KEY, KEY7_CODE, { KEY_PREVIOUSSONG } }, + { KE_KEY, KEY8_CODE, { KEY_NEXTSONG } }, + { KE_END, 0 } +}; + +static const struct key_entry *keymap = keymap_default; + +static int fujitsu_laptop_dmi_keymap_override(const struct dmi_system_id *id) +{ + pr_info("Identified laptop model '%s'\n", id->ident); + keymap = id->driver_data; + return 1; +} + +static const struct dmi_system_id fujitsu_laptop_dmi_table[] = { + { + .callback = fujitsu_laptop_dmi_keymap_override, + .ident = "Fujitsu Siemens S6410", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6410"), + }, + .driver_data = (void *)keymap_s64x0 + }, + { + .callback = fujitsu_laptop_dmi_keymap_override, + .ident = "Fujitsu Siemens S6420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6420"), + }, + .driver_data = (void *)keymap_s64x0 + }, + { + .callback = fujitsu_laptop_dmi_keymap_override, + .ident = "Fujitsu LifeBook P8010", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook P8010"), + }, + .driver_data = (void *)keymap_p8010 + }, + { + .callback = fujitsu_laptop_dmi_keymap_override, + .ident = "Fujitsu LifeBook S2110", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S2110"), + }, + .driver_data = (void *)keymap_s2110 + }, + {} +}; + +static int acpi_fujitsu_laptop_input_setup(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + struct acpi_device *device = ACPI_COMPANION(dev); + int ret; + + priv->input = devm_input_allocate_device(dev); + if (!priv->input) + return -ENOMEM; + + snprintf(priv->phys, sizeof(priv->phys), "%s/input0", + acpi_device_hid(device)); + + priv->input->name = acpi_device_name(device); + priv->input->phys = priv->phys; + priv->input->id.bustype = BUS_HOST; + + dmi_check_system(fujitsu_laptop_dmi_table); + ret = sparse_keymap_setup(priv->input, keymap, NULL); + if (ret) + return ret; + + return input_register_device(priv->input); +} + +static int fujitsu_laptop_platform_add(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + int ret; + + priv->pf_device = platform_device_alloc("fujitsu-laptop", PLATFORM_DEVID_NONE); + if (!priv->pf_device) + return -ENOMEM; + + platform_set_drvdata(priv->pf_device, priv); + + ret = platform_device_add(priv->pf_device); + if (ret) + goto err_put_platform_device; + + ret = sysfs_create_group(&priv->pf_device->dev.kobj, + &fujitsu_pf_attribute_group); + if (ret) + goto err_del_platform_device; + + return 0; + +err_del_platform_device: + platform_device_del(priv->pf_device); +err_put_platform_device: + platform_device_put(priv->pf_device); + + return ret; +} + +static void fujitsu_laptop_platform_remove(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + + sysfs_remove_group(&priv->pf_device->dev.kobj, + &fujitsu_pf_attribute_group); + platform_device_unregister(priv->pf_device); +} + +static int logolamp_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct device *parent = cdev->dev->parent; + int poweron = FUNC_LED_ON, always = FUNC_LED_ON; + int ret; + + if (brightness < LED_HALF) + poweron = FUNC_LED_OFF; + + if (brightness < LED_FULL) + always = FUNC_LED_OFF; + + ret = call_fext_func(parent, FUNC_LEDS, 0x1, LOGOLAMP_POWERON, poweron); + if (ret < 0) + return ret; + + return call_fext_func(parent, FUNC_LEDS, 0x1, LOGOLAMP_ALWAYS, always); +} + +static enum led_brightness logolamp_get(struct led_classdev *cdev) +{ + struct device *parent = cdev->dev->parent; + int ret; + + ret = call_fext_func(parent, FUNC_LEDS, 0x2, LOGOLAMP_ALWAYS, 0x0); + if (ret == FUNC_LED_ON) + return LED_FULL; + + ret = call_fext_func(parent, FUNC_LEDS, 0x2, LOGOLAMP_POWERON, 0x0); + if (ret == FUNC_LED_ON) + return LED_HALF; + + return LED_OFF; +} + +static int kblamps_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct device *parent = cdev->dev->parent; + + if (brightness >= LED_FULL) + return call_fext_func(parent, FUNC_LEDS, 0x1, KEYBOARD_LAMPS, + FUNC_LED_ON); + else + return call_fext_func(parent, FUNC_LEDS, 0x1, KEYBOARD_LAMPS, + FUNC_LED_OFF); +} + +static enum led_brightness kblamps_get(struct led_classdev *cdev) +{ + enum led_brightness brightness = LED_OFF; + + if (call_fext_func(cdev->dev->parent, + FUNC_LEDS, 0x2, KEYBOARD_LAMPS, 0x0) == FUNC_LED_ON) + brightness = LED_FULL; + + return brightness; +} + +static int radio_led_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct device *parent = cdev->dev->parent; + + if (brightness >= LED_FULL) + return call_fext_func(parent, FUNC_FLAGS, 0x5, RADIO_LED_ON, + RADIO_LED_ON); + else + return call_fext_func(parent, FUNC_FLAGS, 0x5, RADIO_LED_ON, + 0x0); +} + +static enum led_brightness radio_led_get(struct led_classdev *cdev) +{ + struct device *parent = cdev->dev->parent; + enum led_brightness brightness = LED_OFF; + + if (call_fext_func(parent, FUNC_FLAGS, 0x4, 0x0, 0x0) & RADIO_LED_ON) + brightness = LED_FULL; + + return brightness; +} + +static int eco_led_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct device *parent = cdev->dev->parent; + int curr; + + curr = call_fext_func(parent, FUNC_LEDS, 0x2, ECO_LED, 0x0); + if (brightness >= LED_FULL) + return call_fext_func(parent, FUNC_LEDS, 0x1, ECO_LED, + curr | ECO_LED_ON); + else + return call_fext_func(parent, FUNC_LEDS, 0x1, ECO_LED, + curr & ~ECO_LED_ON); +} + +static enum led_brightness eco_led_get(struct led_classdev *cdev) +{ + struct device *parent = cdev->dev->parent; + enum led_brightness brightness = LED_OFF; + + if (call_fext_func(parent, FUNC_LEDS, 0x2, ECO_LED, 0x0) & ECO_LED_ON) + brightness = LED_FULL; + + return brightness; +} + +static int acpi_fujitsu_laptop_leds_register(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + struct led_classdev *led; + int ret; + + if (call_fext_func(dev, FUNC_LEDS, 0x0, 0x0, 0x0) & LOGOLAMP_POWERON) { + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->name = "fujitsu::logolamp"; + led->brightness_set_blocking = logolamp_set; + led->brightness_get = logolamp_get; + ret = devm_led_classdev_register(dev, led); + if (ret) + return ret; + } + + if ((call_fext_func(dev, FUNC_LEDS, 0x0, 0x0, 0x0) & KEYBOARD_LAMPS) && + (call_fext_func(dev, FUNC_BUTTONS, 0x0, 0x0, 0x0) == 0x0)) { + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->name = "fujitsu::kblamps"; + led->brightness_set_blocking = kblamps_set; + led->brightness_get = kblamps_get; + ret = devm_led_classdev_register(dev, led); + if (ret) + return ret; + } + + /* + * Some Fujitsu laptops have a radio toggle button in place of a slide + * switch and all such machines appear to also have an RF LED. Based on + * comparing DSDT tables of four Fujitsu Lifebook models (E744, E751, + * S7110, S8420; the first one has a radio toggle button, the other + * three have slide switches), bit 17 of flags_supported (the value + * returned by method S000 of ACPI device FUJ02E3) seems to indicate + * whether given model has a radio toggle button. + */ + if (priv->flags_supported & BIT(17)) { + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->name = "fujitsu::radio_led"; + led->brightness_set_blocking = radio_led_set; + led->brightness_get = radio_led_get; + led->default_trigger = "rfkill-any"; + ret = devm_led_classdev_register(dev, led); + if (ret) + return ret; + } + + /* Support for eco led is not always signaled in bit corresponding + * to the bit used to control the led. According to the DSDT table, + * bit 14 seems to indicate presence of said led as well. + * Confirm by testing the status. + */ + if ((call_fext_func(dev, FUNC_LEDS, 0x0, 0x0, 0x0) & BIT(14)) && + (call_fext_func(dev, FUNC_LEDS, 0x2, ECO_LED, 0x0) != UNSUPPORTED_CMD)) { + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->name = "fujitsu::eco_led"; + led->brightness_set_blocking = eco_led_set; + led->brightness_get = eco_led_get; + ret = devm_led_classdev_register(dev, led); + if (ret) + return ret; + } + + return 0; +} + +static void acpi_fujitsu_laptop_press(struct device *dev, int scancode) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + int ret; + + ret = kfifo_in_locked(&priv->fifo, (unsigned char *)&scancode, + sizeof(scancode), &priv->fifo_lock); + if (ret != sizeof(scancode)) { + dev_info(&priv->input->dev, "Could not push scancode [0x%x]\n", + scancode); + return; + } + sparse_keymap_report_event(priv->input, scancode, 1, false); + dev_dbg(&priv->input->dev, "Push scancode into ringbuffer [0x%x]\n", + scancode); +} + +static void acpi_fujitsu_laptop_release(struct device *dev) +{ + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + int scancode, ret; + + while (true) { + ret = kfifo_out_locked(&priv->fifo, (unsigned char *)&scancode, + sizeof(scancode), &priv->fifo_lock); + if (ret != sizeof(scancode)) + return; + sparse_keymap_report_event(priv->input, scancode, 0, false); + dev_dbg(&priv->input->dev, + "Pop scancode from ringbuffer [0x%x]\n", scancode); + } +} + +static void acpi_fujitsu_laptop_notify(acpi_handle handle, u32 event, void *data) +{ + struct device *dev = data; + struct fujitsu_laptop *priv = dev_get_drvdata(dev); + unsigned long flags; + int scancode, i = 0; + unsigned int irb; + + if (event != ACPI_FUJITSU_NOTIFY_CODE) { + acpi_handle_info(handle, "Unsupported event [0x%x]\n", event); + sparse_keymap_report_event(priv->input, -1, 1, true); + return; + } + + if (priv->flags_supported) + priv->flags_state = call_fext_func(dev, FUNC_FLAGS, 0x4, 0x0, 0x0); + + while ((irb = call_fext_func(dev, FUNC_BUTTONS, 0x1, 0x0, 0x0)) != 0 && + i++ < MAX_HOTKEY_RINGBUFFER_SIZE) { + scancode = irb & 0x4ff; + if (sparse_keymap_entry_from_scancode(priv->input, scancode)) + acpi_fujitsu_laptop_press(dev, scancode); + else if (scancode == 0) + acpi_fujitsu_laptop_release(dev); + else + acpi_handle_info(handle, "Unknown GIRB result [%x]\n", irb); + } + + /* + * First seen on the Skylake-based Lifebook E736/E746/E756), the + * touchpad toggle hotkey (Fn+F4) is handled in software. Other models + * have since added additional "soft keys". These are reported in the + * status flags queried using FUNC_FLAGS. + */ + if (priv->flags_supported & (FLAG_SOFTKEYS)) { + flags = call_fext_func(dev, FUNC_FLAGS, 0x1, 0x0, 0x0); + flags &= (FLAG_SOFTKEYS); + for_each_set_bit(i, &flags, BITS_PER_LONG) + sparse_keymap_report_event(priv->input, BIT(i), 1, true); + } +} + +static int acpi_fujitsu_laptop_probe(struct platform_device *pdev) +{ + struct fujitsu_laptop *priv; + struct acpi_device *device; + int ret, i = 0; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + WARN_ONCE(fext, "More than one FUJ02E3 ACPI device was found. Driver may not work as intended."); + fext = &pdev->dev; + + strscpy(acpi_device_name(device), ACPI_FUJITSU_LAPTOP_DEVICE_NAME); + strscpy(acpi_device_class(device), ACPI_FUJITSU_CLASS); + + platform_set_drvdata(pdev, priv); + + /* kfifo */ + spin_lock_init(&priv->fifo_lock); + ret = kfifo_alloc(&priv->fifo, RINGBUFFERSIZE * sizeof(int), + GFP_KERNEL); + if (ret) + return ret; + + pr_info("ACPI: %s [%s]\n", + acpi_device_name(device), acpi_device_bid(device)); + + while (call_fext_func(fext, FUNC_BUTTONS, 0x1, 0x0, 0x0) != 0 && + i++ < MAX_HOTKEY_RINGBUFFER_SIZE) + ; /* No action, result is discarded */ + acpi_handle_debug(device->handle, "Discarded %i ringbuffer entries\n", + i); + + priv->flags_supported = call_fext_func(fext, FUNC_FLAGS, 0x0, 0x0, 0x0); + + /* Make sure our bitmask of supported functions is cleared if the + RFKILL function block is not implemented, like on the S7020. */ + if (priv->flags_supported == UNSUPPORTED_CMD) + priv->flags_supported = 0; + + if (priv->flags_supported) + priv->flags_state = call_fext_func(fext, FUNC_FLAGS, 0x4, 0x0, + 0x0); + + /* Suspect this is a keymap of the application panel, print it */ + acpi_handle_info(device->handle, "BTNI: [0x%x]\n", + call_fext_func(fext, FUNC_BUTTONS, 0x0, 0x0, 0x0)); + + /* Sync backlight power status */ + if (fujitsu_bl && fujitsu_bl->bl_device && + acpi_video_get_backlight_type() == acpi_backlight_vendor) { + if (call_fext_func(fext, FUNC_BACKLIGHT, 0x2, + BACKLIGHT_PARAM_POWER, 0x0) == BACKLIGHT_OFF) + fujitsu_bl->bl_device->props.power = BACKLIGHT_POWER_OFF; + else + fujitsu_bl->bl_device->props.power = BACKLIGHT_POWER_ON; + } + + ret = acpi_fujitsu_laptop_input_setup(fext); + if (ret) + goto err_free_fifo; + + ret = acpi_fujitsu_laptop_leds_register(fext); + if (ret) + goto err_free_fifo; + + ret = fujitsu_laptop_platform_add(fext); + if (ret) + goto err_free_fifo; + + ret = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + acpi_fujitsu_laptop_notify, fext); + if (ret) + goto err_platform_remove; + + ret = fujitsu_battery_charge_control_add(fext); + if (ret < 0) + pr_warn("Unable to register battery charge control: %d\n", ret); + + return 0; + +err_platform_remove: + fujitsu_laptop_platform_remove(fext); +err_free_fifo: + kfifo_free(&priv->fifo); + + return ret; +} + +static void acpi_fujitsu_laptop_remove(struct platform_device *pdev) +{ + struct fujitsu_laptop *priv = platform_get_drvdata(pdev); + + fujitsu_battery_charge_control_remove(&pdev->dev); + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), ACPI_DEVICE_NOTIFY, + acpi_fujitsu_laptop_notify); + + fujitsu_laptop_platform_remove(&pdev->dev); + + kfifo_free(&priv->fifo); +} + +/* Initialization */ + +static const struct acpi_device_id fujitsu_bl_device_ids[] = { + {ACPI_FUJITSU_BL_HID, 0}, + {"", 0}, +}; + +static struct platform_driver acpi_fujitsu_bl_driver = { + .probe = acpi_fujitsu_bl_probe, + .remove = acpi_fujitsu_bl_remove, + .driver = { + .name = ACPI_FUJITSU_BL_DRIVER_NAME, + .acpi_match_table = fujitsu_bl_device_ids, + }, +}; + +static const struct acpi_device_id fujitsu_laptop_device_ids[] = { + {ACPI_FUJITSU_LAPTOP_HID, 0}, + {"", 0}, +}; + +static struct platform_driver acpi_fujitsu_laptop_driver = { + .probe = acpi_fujitsu_laptop_probe, + .remove = acpi_fujitsu_laptop_remove, + .driver = { + .name = ACPI_FUJITSU_LAPTOP_DRIVER_NAME, + .acpi_match_table = fujitsu_laptop_device_ids, + }, +}; + +static const struct acpi_device_id fujitsu_ids[] __used = { + {ACPI_FUJITSU_BL_HID, 0}, + {ACPI_FUJITSU_LAPTOP_HID, 0}, + {"", 0} +}; +MODULE_DEVICE_TABLE(acpi, fujitsu_ids); + +static int __init fujitsu_init(void) +{ + int ret; + + ret = platform_driver_register(&acpi_fujitsu_bl_driver); + if (ret) + return ret; + + /* Register platform stuff */ + + ret = platform_driver_register(&fujitsu_pf_driver); + if (ret) + goto err_unregister_acpi; + + /* Register laptop driver */ + + ret = platform_driver_register(&acpi_fujitsu_laptop_driver); + if (ret) + goto err_unregister_platform_driver; + + pr_info("driver " FUJITSU_DRIVER_VERSION " successfully loaded\n"); + + return 0; + +err_unregister_platform_driver: + platform_driver_unregister(&fujitsu_pf_driver); +err_unregister_acpi: + platform_driver_unregister(&acpi_fujitsu_bl_driver); + + return ret; +} + +static void __exit fujitsu_cleanup(void) +{ + platform_driver_unregister(&acpi_fujitsu_laptop_driver); + + platform_driver_unregister(&fujitsu_pf_driver); + + platform_driver_unregister(&acpi_fujitsu_bl_driver); + + pr_info("driver unloaded\n"); +} + +module_init(fujitsu_init); +module_exit(fujitsu_cleanup); + +module_param(use_alt_lcd_levels, int, 0644); +MODULE_PARM_DESC(use_alt_lcd_levels, "Interface used for setting LCD brightness level (-1 = auto, 0 = force SBLL, 1 = force SBL2)"); +module_param(disable_brightness_adjust, bool, 0644); +MODULE_PARM_DESC(disable_brightness_adjust, "Disable LCD brightness adjustment"); + +MODULE_AUTHOR("Jonathan Woithe, Peter Gruber, Tony Vroon"); +MODULE_DESCRIPTION("Fujitsu laptop extras support"); +MODULE_VERSION(FUJITSU_DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/fujitsu-tablet.c b/drivers/platform/x86/fujitsu-tablet.c new file mode 100644 index 000000000..2f8c1b89c --- /dev/null +++ b/drivers/platform/x86/fujitsu-tablet.c @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2006-2012 Robert Gerlach + * Copyright (C) 2005-2006 Jan Rychter + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MODULENAME "fujitsu-tablet" + +#define ACPI_FUJITSU_CLASS "fujitsu" + +#define INVERT_TABLET_MODE_BIT 0x01 +#define INVERT_DOCK_STATE_BIT 0x02 +#define FORCE_TABLET_MODE_IF_UNDOCK 0x04 + +#define KEYMAP_LEN 16 + +static const struct acpi_device_id fujitsu_ids[] = { + { .id = "FUJ02BD" }, + { .id = "FUJ02BF" }, + { .id = "" } +}; + +struct fujitsu_config { + unsigned short keymap[KEYMAP_LEN]; + unsigned int quirks; +}; + +static unsigned short keymap_Lifebook_Tseries[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_SCROLLDOWN, + KEY_SCROLLUP, + KEY_ROTATE_DISPLAY, + KEY_LEFTCTRL, + KEY_BRIGHTNESSUP, + KEY_BRIGHTNESSDOWN, + KEY_BRIGHTNESS_ZERO, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_LEFTALT +}; + +static unsigned short keymap_Lifebook_T901[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_SCROLLDOWN, + KEY_SCROLLUP, + KEY_CYCLEWINDOWS, + KEY_LEFTCTRL, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_LEFTMETA +}; + +static unsigned short keymap_Lifebook_T902[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_VOLUMEDOWN, + KEY_VOLUMEUP, + KEY_CYCLEWINDOWS, + KEY_PROG1, + KEY_PROG2, + KEY_LEFTMETA, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, +}; + +static unsigned short keymap_Lifebook_U810[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_PROG1, + KEY_PROG2, + KEY_ROTATE_DISPLAY, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_UP, + KEY_DOWN, + KEY_RESERVED, + KEY_RESERVED, + KEY_LEFTCTRL, + KEY_LEFTALT +}; + +static unsigned short keymap_Stylistic_Tseries[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_PRINT, + KEY_BACKSPACE, + KEY_SPACE, + KEY_ENTER, + KEY_BRIGHTNESSUP, + KEY_BRIGHTNESSDOWN, + KEY_DOWN, + KEY_UP, + KEY_SCROLLUP, + KEY_SCROLLDOWN, + KEY_LEFTCTRL, + KEY_LEFTALT +}; + +static unsigned short keymap_Stylistic_ST5xxx[KEYMAP_LEN] __initdata = { + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_RESERVED, + KEY_MAIL, + KEY_ROTATE_DISPLAY, + KEY_ESC, + KEY_ENTER, + KEY_BRIGHTNESSUP, + KEY_BRIGHTNESSDOWN, + KEY_DOWN, + KEY_UP, + KEY_SCROLLUP, + KEY_SCROLLDOWN, + KEY_LEFTCTRL, + KEY_LEFTALT +}; + +static struct { + struct input_dev *idev; + struct fujitsu_config config; + unsigned long prev_keymask; + + char phys[21]; + + int irq; + int io_base; + int io_length; +} fujitsu; + +static u8 fujitsu_ack(void) +{ + return inb(fujitsu.io_base + 2); +} + +static u8 fujitsu_status(void) +{ + return inb(fujitsu.io_base + 6); +} + +static u8 fujitsu_read_register(const u8 addr) +{ + outb(addr, fujitsu.io_base); + return inb(fujitsu.io_base + 4); +} + +static void fujitsu_send_state(void) +{ + int state; + int dock, tablet_mode; + + state = fujitsu_read_register(0xdd); + + dock = state & 0x02; + if (fujitsu.config.quirks & INVERT_DOCK_STATE_BIT) + dock = !dock; + + if ((fujitsu.config.quirks & FORCE_TABLET_MODE_IF_UNDOCK) && (!dock)) { + tablet_mode = 1; + } else{ + tablet_mode = state & 0x01; + if (fujitsu.config.quirks & INVERT_TABLET_MODE_BIT) + tablet_mode = !tablet_mode; + } + + input_report_switch(fujitsu.idev, SW_DOCK, dock); + input_report_switch(fujitsu.idev, SW_TABLET_MODE, tablet_mode); + input_sync(fujitsu.idev); +} + +static void fujitsu_reset(void) +{ + int timeout = 50; + + fujitsu_ack(); + + while ((fujitsu_status() & 0x02) && (--timeout)) + msleep(20); + + fujitsu_send_state(); +} + +static int input_fujitsu_setup(struct device *parent, const char *name, + const char *phys) +{ + struct input_dev *idev; + int error; + int i; + + idev = input_allocate_device(); + if (!idev) + return -ENOMEM; + + idev->dev.parent = parent; + idev->phys = phys; + idev->name = name; + idev->id.bustype = BUS_HOST; + idev->id.vendor = 0x1734; /* Fujitsu Siemens Computer GmbH */ + idev->id.product = 0x0001; + idev->id.version = 0x0101; + + idev->keycode = fujitsu.config.keymap; + idev->keycodesize = sizeof(fujitsu.config.keymap[0]); + idev->keycodemax = ARRAY_SIZE(fujitsu.config.keymap); + + __set_bit(EV_REP, idev->evbit); + + for (i = 0; i < ARRAY_SIZE(fujitsu.config.keymap); i++) + if (fujitsu.config.keymap[i]) + input_set_capability(idev, EV_KEY, fujitsu.config.keymap[i]); + + input_set_capability(idev, EV_MSC, MSC_SCAN); + + input_set_capability(idev, EV_SW, SW_DOCK); + input_set_capability(idev, EV_SW, SW_TABLET_MODE); + + error = input_register_device(idev); + if (error) { + input_free_device(idev); + return error; + } + + fujitsu.idev = idev; + return 0; +} + +static void input_fujitsu_remove(void) +{ + input_unregister_device(fujitsu.idev); +} + +static irqreturn_t fujitsu_interrupt(int irq, void *dev_id) +{ + unsigned long keymask, changed; + unsigned int keycode; + int pressed; + int i; + + if (unlikely(!(fujitsu_status() & 0x01))) + return IRQ_NONE; + + fujitsu_send_state(); + + keymask = fujitsu_read_register(0xde); + keymask |= fujitsu_read_register(0xdf) << 8; + keymask ^= 0xffff; + + changed = keymask ^ fujitsu.prev_keymask; + if (changed) { + fujitsu.prev_keymask = keymask; + + for_each_set_bit(i, &changed, KEYMAP_LEN) { + keycode = fujitsu.config.keymap[i]; + pressed = keymask & changed & BIT(i); + + if (pressed) + input_event(fujitsu.idev, EV_MSC, MSC_SCAN, i); + + input_report_key(fujitsu.idev, keycode, pressed); + input_sync(fujitsu.idev); + } + } + + fujitsu_ack(); + return IRQ_HANDLED; +} + +static void __init fujitsu_dmi_common(const struct dmi_system_id *dmi) +{ + pr_info("%s\n", dmi->ident); + memcpy(fujitsu.config.keymap, dmi->driver_data, + sizeof(fujitsu.config.keymap)); +} + +static int __init fujitsu_dmi_lifebook(const struct dmi_system_id *dmi) +{ + fujitsu_dmi_common(dmi); + fujitsu.config.quirks |= INVERT_TABLET_MODE_BIT; + return 1; +} + +static int __init fujitsu_dmi_stylistic(const struct dmi_system_id *dmi) +{ + fujitsu_dmi_common(dmi); + fujitsu.config.quirks |= FORCE_TABLET_MODE_IF_UNDOCK; + fujitsu.config.quirks |= INVERT_DOCK_STATE_BIT; + return 1; +} + +static const struct dmi_system_id dmi_ids[] __initconst = { + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu Lifebook T901", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook T901") + }, + .driver_data = keymap_Lifebook_T901 + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu Lifebook T901", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK T901") + }, + .driver_data = keymap_Lifebook_T901 + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu Lifebook T902", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK T902") + }, + .driver_data = keymap_Lifebook_T902 + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu Siemens P/T Series", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK") + }, + .driver_data = keymap_Lifebook_Tseries + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu Lifebook T Series", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook T") + }, + .driver_data = keymap_Lifebook_Tseries + }, + { + .callback = fujitsu_dmi_stylistic, + .ident = "Fujitsu Siemens Stylistic T Series", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "Stylistic T") + }, + .driver_data = keymap_Stylistic_Tseries + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Fujitsu LifeBook U810", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook U810") + }, + .driver_data = keymap_Lifebook_U810 + }, + { + .callback = fujitsu_dmi_stylistic, + .ident = "Fujitsu Siemens Stylistic ST5xxx Series", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "STYLISTIC ST5") + }, + .driver_data = keymap_Stylistic_ST5xxx + }, + { + .callback = fujitsu_dmi_stylistic, + .ident = "Fujitsu Siemens Stylistic ST5xxx Series", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"), + DMI_MATCH(DMI_PRODUCT_NAME, "Stylistic ST5") + }, + .driver_data = keymap_Stylistic_ST5xxx + }, + { + .callback = fujitsu_dmi_lifebook, + .ident = "Unknown (using defaults)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, ""), + DMI_MATCH(DMI_PRODUCT_NAME, "") + }, + .driver_data = keymap_Lifebook_Tseries + }, + { NULL } +}; + +static acpi_status fujitsu_walk_resources(struct acpi_resource *res, void *data) +{ + switch (res->type) { + case ACPI_RESOURCE_TYPE_IRQ: + fujitsu.irq = res->data.irq.interrupts[0]; + return AE_OK; + + case ACPI_RESOURCE_TYPE_IO: + fujitsu.io_base = res->data.io.minimum; + fujitsu.io_length = res->data.io.address_length; + return AE_OK; + + case ACPI_RESOURCE_TYPE_END_TAG: + if (fujitsu.irq && fujitsu.io_base) + return AE_OK; + else + return AE_NOT_FOUND; + + default: + return AE_ERROR; + } +} + +static int acpi_fujitsu_probe(struct platform_device *pdev) +{ + struct acpi_device *adev; + acpi_status status; + int error; + + adev = ACPI_COMPANION(&pdev->dev); + if (!adev) + return -ENODEV; + + status = acpi_walk_resources(adev->handle, METHOD_NAME__CRS, + fujitsu_walk_resources, NULL); + if (ACPI_FAILURE(status) || !fujitsu.irq || !fujitsu.io_base) + return -ENODEV; + + sprintf(acpi_device_name(adev), "Fujitsu %s", acpi_device_hid(adev)); + sprintf(acpi_device_class(adev), "%s", ACPI_FUJITSU_CLASS); + + snprintf(fujitsu.phys, sizeof(fujitsu.phys), + "%s/input0", acpi_device_hid(adev)); + + error = input_fujitsu_setup(&pdev->dev, + acpi_device_name(adev), fujitsu.phys); + if (error) + return error; + + if (!request_region(fujitsu.io_base, fujitsu.io_length, MODULENAME)) { + input_fujitsu_remove(); + return -EBUSY; + } + + fujitsu_reset(); + + error = request_irq(fujitsu.irq, fujitsu_interrupt, + IRQF_SHARED, MODULENAME, fujitsu_interrupt); + if (error) { + release_region(fujitsu.io_base, fujitsu.io_length); + input_fujitsu_remove(); + return error; + } + + return 0; +} + +static void acpi_fujitsu_remove(struct platform_device *pdev) +{ + free_irq(fujitsu.irq, fujitsu_interrupt); + release_region(fujitsu.io_base, fujitsu.io_length); + input_fujitsu_remove(); +} + +#ifdef CONFIG_PM_SLEEP +static int acpi_fujitsu_resume(struct device *dev) +{ + fujitsu_reset(); + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(acpi_fujitsu_pm, NULL, acpi_fujitsu_resume); + +static struct platform_driver acpi_fujitsu_driver = { + .probe = acpi_fujitsu_probe, + .remove = acpi_fujitsu_remove, + .driver = { + .name = MODULENAME, + .acpi_match_table = fujitsu_ids, + .pm = &acpi_fujitsu_pm, + }, +}; + +static int __init fujitsu_module_init(void) +{ + int error; + + dmi_check_system(dmi_ids); + + error = platform_driver_register(&acpi_fujitsu_driver); + if (error) + return error; + + return 0; +} + +static void __exit fujitsu_module_exit(void) +{ + platform_driver_unregister(&acpi_fujitsu_driver); +} + +module_init(fujitsu_module_init); +module_exit(fujitsu_module_exit); + +MODULE_AUTHOR("Robert Gerlach "); +MODULE_DESCRIPTION("Fujitsu tablet pc extras driver"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("2.5"); + +MODULE_DEVICE_TABLE(acpi, fujitsu_ids); diff --git a/drivers/platform/x86/gigabyte-wmi.c b/drivers/platform/x86/gigabyte-wmi.c new file mode 100644 index 000000000..f42c85607 --- /dev/null +++ b/drivers/platform/x86/gigabyte-wmi.c @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021 Thomas Weißschuh + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include + +#define GIGABYTE_WMI_GUID "DEADBEEF-2001-0000-00A0-C90629100000" +#define NUM_TEMPERATURE_SENSORS 6 + +static u8 usable_sensors_mask; + +enum gigabyte_wmi_commandtype { + GIGABYTE_WMI_BUILD_DATE_QUERY = 0x1, + GIGABYTE_WMI_MAINBOARD_TYPE_QUERY = 0x2, + GIGABYTE_WMI_FIRMWARE_VERSION_QUERY = 0x4, + GIGABYTE_WMI_MAINBOARD_NAME_QUERY = 0x5, + GIGABYTE_WMI_TEMPERATURE_QUERY = 0x125, +}; + +struct gigabyte_wmi_args { + u32 arg1; +}; + +static int gigabyte_wmi_perform_query(struct wmi_device *wdev, + enum gigabyte_wmi_commandtype command, + struct gigabyte_wmi_args *args, struct acpi_buffer *out) +{ + const struct acpi_buffer in = { + .length = sizeof(*args), + .pointer = args, + }; + + acpi_status ret = wmidev_evaluate_method(wdev, 0x0, command, &in, out); + + if (ACPI_FAILURE(ret)) + return -EIO; + + return 0; +} + +static int gigabyte_wmi_query_integer(struct wmi_device *wdev, + enum gigabyte_wmi_commandtype command, + struct gigabyte_wmi_args *args, u64 *res) +{ + union acpi_object *obj; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + int ret; + + ret = gigabyte_wmi_perform_query(wdev, command, args, &result); + if (ret) + return ret; + obj = result.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + *res = obj->integer.value; + else + ret = -EIO; + kfree(result.pointer); + return ret; +} + +static int gigabyte_wmi_temperature(struct wmi_device *wdev, u8 sensor, long *res) +{ + struct gigabyte_wmi_args args = { + .arg1 = sensor, + }; + u64 temp; + acpi_status ret; + + ret = gigabyte_wmi_query_integer(wdev, GIGABYTE_WMI_TEMPERATURE_QUERY, &args, &temp); + if (ret == 0) { + if (temp == 0) + return -ENODEV; + *res = (s8)temp * 1000; // value is a signed 8-bit integer + } + return ret; +} + +static int gigabyte_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct wmi_device *wdev = dev_get_drvdata(dev); + + return gigabyte_wmi_temperature(wdev, channel, val); +} + +static umode_t gigabyte_wmi_hwmon_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return usable_sensors_mask & BIT(channel) ? 0444 : 0; +} + +static const struct hwmon_channel_info * const gigabyte_wmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT), + NULL +}; + +static const struct hwmon_ops gigabyte_wmi_hwmon_ops = { + .read = gigabyte_wmi_hwmon_read, + .is_visible = gigabyte_wmi_hwmon_is_visible, +}; + +static const struct hwmon_chip_info gigabyte_wmi_hwmon_chip_info = { + .ops = &gigabyte_wmi_hwmon_ops, + .info = gigabyte_wmi_hwmon_info, +}; + +static u8 gigabyte_wmi_detect_sensor_usability(struct wmi_device *wdev) +{ + int i; + long temp; + u8 r = 0; + + for (i = 0; i < NUM_TEMPERATURE_SENSORS; i++) { + if (!gigabyte_wmi_temperature(wdev, i, &temp)) + r |= BIT(i); + } + return r; +} + +static int gigabyte_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct device *hwmon_dev; + + usable_sensors_mask = gigabyte_wmi_detect_sensor_usability(wdev); + if (!usable_sensors_mask) { + dev_info(&wdev->dev, "No temperature sensors usable"); + return -ENODEV; + } + + hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, "gigabyte_wmi", wdev, + &gigabyte_wmi_hwmon_chip_info, NULL); + + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static const struct wmi_device_id gigabyte_wmi_id_table[] = { + { GIGABYTE_WMI_GUID, NULL }, + { } +}; + +static struct wmi_driver gigabyte_wmi_driver = { + .driver = { + .name = "gigabyte-wmi", + }, + .id_table = gigabyte_wmi_id_table, + .probe = gigabyte_wmi_probe, +}; +module_wmi_driver(gigabyte_wmi_driver); + +MODULE_DEVICE_TABLE(wmi, gigabyte_wmi_id_table); +MODULE_AUTHOR("Thomas Weißschuh "); +MODULE_DESCRIPTION("Gigabyte WMI temperature driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/gpd-pocket-fan.c b/drivers/platform/x86/gpd-pocket-fan.c new file mode 100644 index 000000000..c9236738f --- /dev/null +++ b/drivers/platform/x86/gpd-pocket-fan.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * GPD Pocket fan controller driver + * + * Copyright (C) 2017 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_SPEED 3 + +#define TEMP_LIMIT0_DEFAULT 55000 +#define TEMP_LIMIT1_DEFAULT 60000 +#define TEMP_LIMIT2_DEFAULT 65000 + +#define HYSTERESIS_DEFAULT 3000 + +#define SPEED_ON_AC_DEFAULT 2 + +static int temp_limits[3] = { + TEMP_LIMIT0_DEFAULT, TEMP_LIMIT1_DEFAULT, TEMP_LIMIT2_DEFAULT, +}; +module_param_array(temp_limits, int, NULL, 0444); +MODULE_PARM_DESC(temp_limits, + "Millicelsius values above which the fan speed increases"); + +static int hysteresis = HYSTERESIS_DEFAULT; +module_param(hysteresis, int, 0444); +MODULE_PARM_DESC(hysteresis, + "Hysteresis in millicelsius before lowering the fan speed"); + +static int speed_on_ac = SPEED_ON_AC_DEFAULT; +module_param(speed_on_ac, int, 0444); +MODULE_PARM_DESC(speed_on_ac, + "minimum fan speed to allow when system is powered by AC"); + +struct gpd_pocket_fan_data { + struct device *dev; + struct thermal_zone_device *dts0; + struct thermal_zone_device *dts1; + struct gpio_desc *gpio0; + struct gpio_desc *gpio1; + struct delayed_work work; + int last_speed; +}; + +static void gpd_pocket_fan_set_speed(struct gpd_pocket_fan_data *fan, int speed) +{ + if (speed == fan->last_speed) + return; + + gpiod_direction_output(fan->gpio0, !!(speed & 1)); + gpiod_direction_output(fan->gpio1, !!(speed & 2)); + + fan->last_speed = speed; +} + +static int gpd_pocket_fan_min_speed(void) +{ + if (power_supply_is_system_supplied()) + return speed_on_ac; + else + return 0; +} + +static void gpd_pocket_fan_worker(struct work_struct *work) +{ + struct gpd_pocket_fan_data *fan = + container_of(work, struct gpd_pocket_fan_data, work.work); + int t0, t1, temp, speed, min_speed, i; + + if (thermal_zone_get_temp(fan->dts0, &t0) || + thermal_zone_get_temp(fan->dts1, &t1)) { + dev_warn(fan->dev, "Error getting temperature\n"); + speed = MAX_SPEED; + goto set_speed; + } + + temp = max(t0, t1); + + speed = fan->last_speed; + min_speed = gpd_pocket_fan_min_speed(); + + /* Determine minimum speed */ + for (i = min_speed; i < ARRAY_SIZE(temp_limits); i++) { + if (temp < temp_limits[i]) + break; + } + if (speed < i) + speed = i; + + /* Use hysteresis before lowering speed again */ + for (i = min_speed; i < ARRAY_SIZE(temp_limits); i++) { + if (temp <= (temp_limits[i] - hysteresis)) + break; + } + if (speed > i) + speed = i; + + if (fan->last_speed <= 0 && speed) + speed = MAX_SPEED; /* kick start motor */ + +set_speed: + gpd_pocket_fan_set_speed(fan, speed); + + /* When mostly idle (low temp/speed), slow down the poll interval. */ + queue_delayed_work(system_percpu_wq, &fan->work, + msecs_to_jiffies(4000 / (speed + 1))); +} + +static void gpd_pocket_fan_force_update(struct gpd_pocket_fan_data *fan) +{ + fan->last_speed = -1; + mod_delayed_work(system_percpu_wq, &fan->work, 0); +} + +static int gpd_pocket_fan_probe(struct platform_device *pdev) +{ + struct gpd_pocket_fan_data *fan; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(temp_limits); i++) { + if (temp_limits[i] < 20000 || temp_limits[i] > 90000) { + dev_err(&pdev->dev, "Invalid temp-limit %d (must be between 20000 and 90000)\n", + temp_limits[i]); + temp_limits[0] = TEMP_LIMIT0_DEFAULT; + temp_limits[1] = TEMP_LIMIT1_DEFAULT; + temp_limits[2] = TEMP_LIMIT2_DEFAULT; + break; + } + } + if (hysteresis < 1000 || hysteresis > 10000) { + dev_err(&pdev->dev, "Invalid hysteresis %d (must be between 1000 and 10000)\n", + hysteresis); + hysteresis = HYSTERESIS_DEFAULT; + } + if (speed_on_ac < 0 || speed_on_ac > MAX_SPEED) { + dev_err(&pdev->dev, "Invalid speed_on_ac %d (must be between 0 and 3)\n", + speed_on_ac); + speed_on_ac = SPEED_ON_AC_DEFAULT; + } + + fan = devm_kzalloc(&pdev->dev, sizeof(*fan), GFP_KERNEL); + if (!fan) + return -ENOMEM; + + fan->dev = &pdev->dev; + ret = devm_delayed_work_autocancel(&pdev->dev, &fan->work, + gpd_pocket_fan_worker); + if (ret) + return ret; + + /* Note this returns a "weak" reference which we don't need to free */ + fan->dts0 = thermal_zone_get_zone_by_name("soc_dts0"); + if (IS_ERR(fan->dts0)) + return -EPROBE_DEFER; + + fan->dts1 = thermal_zone_get_zone_by_name("soc_dts1"); + if (IS_ERR(fan->dts1)) + return -EPROBE_DEFER; + + fan->gpio0 = devm_gpiod_get_index(fan->dev, NULL, 0, GPIOD_ASIS); + if (IS_ERR(fan->gpio0)) + return PTR_ERR(fan->gpio0); + + fan->gpio1 = devm_gpiod_get_index(fan->dev, NULL, 1, GPIOD_ASIS); + if (IS_ERR(fan->gpio1)) + return PTR_ERR(fan->gpio1); + + gpd_pocket_fan_force_update(fan); + + platform_set_drvdata(pdev, fan); + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int gpd_pocket_fan_suspend(struct device *dev) +{ + struct gpd_pocket_fan_data *fan = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&fan->work); + gpd_pocket_fan_set_speed(fan, gpd_pocket_fan_min_speed()); + return 0; +} + +static int gpd_pocket_fan_resume(struct device *dev) +{ + struct gpd_pocket_fan_data *fan = dev_get_drvdata(dev); + + gpd_pocket_fan_force_update(fan); + return 0; +} +#endif +static SIMPLE_DEV_PM_OPS(gpd_pocket_fan_pm_ops, + gpd_pocket_fan_suspend, + gpd_pocket_fan_resume); + +static struct acpi_device_id gpd_pocket_fan_acpi_match[] = { + { "FAN02501" }, + {}, +}; +MODULE_DEVICE_TABLE(acpi, gpd_pocket_fan_acpi_match); + +static struct platform_driver gpd_pocket_fan_driver = { + .probe = gpd_pocket_fan_probe, + .driver = { + .name = "gpd_pocket_fan", + .acpi_match_table = gpd_pocket_fan_acpi_match, + .pm = &gpd_pocket_fan_pm_ops, + }, +}; + +module_platform_driver(gpd_pocket_fan_driver); +MODULE_AUTHOR("Hans de Goede + * Copyright (C) 2005 Jesper Juhl + * + * The HardDisk Active Protection System (hdaps) is present in IBM ThinkPads + * starting with the R40, T41, and X40. It provides a basic two-axis + * accelerometer and other data, such as the device's temperature. + * + * This driver is based on the document by Mark A. Smith available at + * http://www.almaden.ibm.com/cs/people/marksmith/tpaps.html and a lot of trial + * and error. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HDAPS_LOW_PORT 0x1600 /* first port used by hdaps */ +#define HDAPS_NR_PORTS 0x30 /* number of ports: 0x1600 - 0x162f */ + +#define HDAPS_PORT_STATE 0x1611 /* device state */ +#define HDAPS_PORT_YPOS 0x1612 /* y-axis position */ +#define HDAPS_PORT_XPOS 0x1614 /* x-axis position */ +#define HDAPS_PORT_TEMP1 0x1616 /* device temperature, in Celsius */ +#define HDAPS_PORT_YVAR 0x1617 /* y-axis variance (what is this?) */ +#define HDAPS_PORT_XVAR 0x1619 /* x-axis variance (what is this?) */ +#define HDAPS_PORT_TEMP2 0x161b /* device temperature (again?) */ +#define HDAPS_PORT_UNKNOWN 0x161c /* what is this? */ +#define HDAPS_PORT_KMACT 0x161d /* keyboard or mouse activity */ + +#define STATE_FRESH 0x50 /* accelerometer data is fresh */ + +#define KEYBD_MASK 0x20 /* set if keyboard activity */ +#define MOUSE_MASK 0x40 /* set if mouse activity */ +#define KEYBD_ISSET(n) (!! (n & KEYBD_MASK)) /* keyboard used? */ +#define MOUSE_ISSET(n) (!! (n & MOUSE_MASK)) /* mouse used? */ + +#define INIT_TIMEOUT_MSECS 4000 /* wait up to 4s for device init ... */ +#define INIT_WAIT_MSECS 200 /* ... in 200ms increments */ + +#define HDAPS_POLL_INTERVAL 50 /* poll for input every 1/20s (50 ms)*/ +#define HDAPS_INPUT_FUZZ 4 /* input event threshold */ +#define HDAPS_INPUT_FLAT 4 + +#define HDAPS_X_AXIS (1 << 0) +#define HDAPS_Y_AXIS (1 << 1) +#define HDAPS_BOTH_AXES (HDAPS_X_AXIS | HDAPS_Y_AXIS) + +static struct platform_device *pdev; +static struct input_dev *hdaps_idev; +static unsigned int hdaps_invert; +static u8 km_activity; +static int rest_x; +static int rest_y; + +static DEFINE_MUTEX(hdaps_mtx); + +/* + * __get_latch - Get the value from a given port. Callers must hold hdaps_mtx. + */ +static inline u8 __get_latch(u16 port) +{ + return inb(port) & 0xff; +} + +/* + * __check_latch - Check a port latch for a given value. Returns zero if the + * port contains the given value. Callers must hold hdaps_mtx. + */ +static inline int __check_latch(u16 port, u8 val) +{ + if (__get_latch(port) == val) + return 0; + return -EINVAL; +} + +/* + * __wait_latch - Wait up to 100us for a port latch to get a certain value, + * returning zero if the value is obtained. Callers must hold hdaps_mtx. + */ +static int __wait_latch(u16 port, u8 val) +{ + unsigned int i; + + for (i = 0; i < 20; i++) { + if (!__check_latch(port, val)) + return 0; + udelay(5); + } + + return -EIO; +} + +/* + * __device_refresh - request a refresh from the accelerometer. Does not wait + * for refresh to complete. Callers must hold hdaps_mtx. + */ +static void __device_refresh(void) +{ + udelay(200); + if (inb(0x1604) != STATE_FRESH) { + outb(0x11, 0x1610); + outb(0x01, 0x161f); + } +} + +/* + * __device_refresh_sync - request a synchronous refresh from the + * accelerometer. We wait for the refresh to complete. Returns zero if + * successful and nonzero on error. Callers must hold hdaps_mtx. + */ +static int __device_refresh_sync(void) +{ + __device_refresh(); + return __wait_latch(0x1604, STATE_FRESH); +} + +/* + * __device_complete - indicate to the accelerometer that we are done reading + * data, and then initiate an async refresh. Callers must hold hdaps_mtx. + */ +static inline void __device_complete(void) +{ + inb(0x161f); + inb(0x1604); + __device_refresh(); +} + +/* + * hdaps_readb_one - reads a byte from a single I/O port, placing the value in + * the given pointer. Returns zero on success or a negative error on failure. + * Can sleep. + */ +static int hdaps_readb_one(unsigned int port, u8 *val) +{ + int ret; + + mutex_lock(&hdaps_mtx); + + /* do a sync refresh -- we need to be sure that we read fresh data */ + ret = __device_refresh_sync(); + if (ret) + goto out; + + *val = inb(port); + __device_complete(); + +out: + mutex_unlock(&hdaps_mtx); + return ret; +} + +/* __hdaps_read_pair - internal lockless helper for hdaps_read_pair(). */ +static int __hdaps_read_pair(unsigned int port1, unsigned int port2, + int *x, int *y) +{ + /* do a sync refresh -- we need to be sure that we read fresh data */ + if (__device_refresh_sync()) + return -EIO; + + *y = inw(port2); + *x = inw(port1); + km_activity = inb(HDAPS_PORT_KMACT); + __device_complete(); + + /* hdaps_invert is a bitvector to negate the axes */ + if (hdaps_invert & HDAPS_X_AXIS) + *x = -*x; + if (hdaps_invert & HDAPS_Y_AXIS) + *y = -*y; + + return 0; +} + +/* + * hdaps_read_pair - reads the values from a pair of ports, placing the values + * in the given pointers. Returns zero on success. Can sleep. + */ +static int hdaps_read_pair(unsigned int port1, unsigned int port2, + int *val1, int *val2) +{ + int ret; + + mutex_lock(&hdaps_mtx); + ret = __hdaps_read_pair(port1, port2, val1, val2); + mutex_unlock(&hdaps_mtx); + + return ret; +} + +/* + * hdaps_device_init - initialize the accelerometer. Returns zero on success + * and negative error code on failure. Can sleep. + */ +static int hdaps_device_init(void) +{ + int total, ret = -ENXIO; + + mutex_lock(&hdaps_mtx); + + outb(0x13, 0x1610); + outb(0x01, 0x161f); + if (__wait_latch(0x161f, 0x00)) + goto out; + + /* + * Most ThinkPads return 0x01. + * + * Others--namely the R50p, T41p, and T42p--return 0x03. These laptops + * have "inverted" axises. + * + * The 0x02 value occurs when the chip has been previously initialized. + */ + if (__check_latch(0x1611, 0x03) && + __check_latch(0x1611, 0x02) && + __check_latch(0x1611, 0x01)) + goto out; + + printk(KERN_DEBUG "hdaps: initial latch check good (0x%02x)\n", + __get_latch(0x1611)); + + outb(0x17, 0x1610); + outb(0x81, 0x1611); + outb(0x01, 0x161f); + if (__wait_latch(0x161f, 0x00)) + goto out; + if (__wait_latch(0x1611, 0x00)) + goto out; + if (__wait_latch(0x1612, 0x60)) + goto out; + if (__wait_latch(0x1613, 0x00)) + goto out; + outb(0x14, 0x1610); + outb(0x01, 0x1611); + outb(0x01, 0x161f); + if (__wait_latch(0x161f, 0x00)) + goto out; + outb(0x10, 0x1610); + outb(0xc8, 0x1611); + outb(0x00, 0x1612); + outb(0x02, 0x1613); + outb(0x01, 0x161f); + if (__wait_latch(0x161f, 0x00)) + goto out; + if (__device_refresh_sync()) + goto out; + if (__wait_latch(0x1611, 0x00)) + goto out; + + /* we have done our dance, now let's wait for the applause */ + for (total = INIT_TIMEOUT_MSECS; total > 0; total -= INIT_WAIT_MSECS) { + int x, y; + + /* a read of the device helps push it into action */ + __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y); + if (!__wait_latch(0x1611, 0x02)) { + ret = 0; + break; + } + + msleep(INIT_WAIT_MSECS); + } + +out: + mutex_unlock(&hdaps_mtx); + return ret; +} + + +/* Device model stuff */ + +static int hdaps_probe(struct platform_device *dev) +{ + int ret; + + ret = hdaps_device_init(); + if (ret) + return ret; + + pr_info("device successfully initialized\n"); + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int hdaps_resume(struct device *dev) +{ + return hdaps_device_init(); +} +#endif + +static SIMPLE_DEV_PM_OPS(hdaps_pm, NULL, hdaps_resume); + +static struct platform_driver hdaps_driver = { + .probe = hdaps_probe, + .driver = { + .name = "hdaps", + .pm = &hdaps_pm, + }, +}; + +/* + * hdaps_calibrate - Set our "resting" values. Callers must hold hdaps_mtx. + */ +static void hdaps_calibrate(void) +{ + __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &rest_x, &rest_y); +} + +static void hdaps_mousedev_poll(struct input_dev *input_dev) +{ + int x, y; + + mutex_lock(&hdaps_mtx); + + if (__hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y)) + goto out; + + input_report_abs(input_dev, ABS_X, x - rest_x); + input_report_abs(input_dev, ABS_Y, y - rest_y); + input_sync(input_dev); + +out: + mutex_unlock(&hdaps_mtx); +} + + +/* Sysfs Files */ + +static ssize_t hdaps_position_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, x, y; + + ret = hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y); + if (ret) + return ret; + + return sprintf(buf, "(%d,%d)\n", x, y); +} + +static ssize_t hdaps_variance_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, x, y; + + ret = hdaps_read_pair(HDAPS_PORT_XVAR, HDAPS_PORT_YVAR, &x, &y); + if (ret) + return ret; + + return sprintf(buf, "(%d,%d)\n", x, y); +} + +static ssize_t hdaps_temp1_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u8 temp; + int ret; + + ret = hdaps_readb_one(HDAPS_PORT_TEMP1, &temp); + if (ret) + return ret; + + return sprintf(buf, "%u\n", temp); +} + +static ssize_t hdaps_temp2_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u8 temp; + int ret; + + ret = hdaps_readb_one(HDAPS_PORT_TEMP2, &temp); + if (ret) + return ret; + + return sprintf(buf, "%u\n", temp); +} + +static ssize_t hdaps_keyboard_activity_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%u\n", KEYBD_ISSET(km_activity)); +} + +static ssize_t hdaps_mouse_activity_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%u\n", MOUSE_ISSET(km_activity)); +} + +static ssize_t hdaps_calibrate_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "(%d,%d)\n", rest_x, rest_y); +} + +static ssize_t hdaps_calibrate_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + mutex_lock(&hdaps_mtx); + hdaps_calibrate(); + mutex_unlock(&hdaps_mtx); + + return count; +} + +static ssize_t hdaps_invert_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%u\n", hdaps_invert); +} + +static ssize_t hdaps_invert_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int invert; + + if (sscanf(buf, "%d", &invert) != 1 || + invert < 0 || invert > HDAPS_BOTH_AXES) + return -EINVAL; + + hdaps_invert = invert; + hdaps_calibrate(); + + return count; +} + +static DEVICE_ATTR(position, 0444, hdaps_position_show, NULL); +static DEVICE_ATTR(variance, 0444, hdaps_variance_show, NULL); +static DEVICE_ATTR(temp1, 0444, hdaps_temp1_show, NULL); +static DEVICE_ATTR(temp2, 0444, hdaps_temp2_show, NULL); +static DEVICE_ATTR(keyboard_activity, 0444, hdaps_keyboard_activity_show, NULL); +static DEVICE_ATTR(mouse_activity, 0444, hdaps_mouse_activity_show, NULL); +static DEVICE_ATTR(calibrate, 0644, hdaps_calibrate_show,hdaps_calibrate_store); +static DEVICE_ATTR(invert, 0644, hdaps_invert_show, hdaps_invert_store); + +static struct attribute *hdaps_attributes[] = { + &dev_attr_position.attr, + &dev_attr_variance.attr, + &dev_attr_temp1.attr, + &dev_attr_temp2.attr, + &dev_attr_keyboard_activity.attr, + &dev_attr_mouse_activity.attr, + &dev_attr_calibrate.attr, + &dev_attr_invert.attr, + NULL, +}; + +static const struct attribute_group hdaps_attribute_group = { + .attrs = hdaps_attributes, +}; + + +/* Module stuff */ + +/* hdaps_dmi_match - found a match. return one, short-circuiting the hunt. */ +static int __init hdaps_dmi_match(const struct dmi_system_id *id) +{ + pr_info("%s detected\n", id->ident); + return 1; +} + +/* hdaps_dmi_match_invert - found an inverted match. */ +static int __init hdaps_dmi_match_invert(const struct dmi_system_id *id) +{ + hdaps_invert = (unsigned long)id->driver_data; + pr_info("inverting axis (%u) readings\n", hdaps_invert); + return hdaps_dmi_match(id); +} + +#define HDAPS_DMI_MATCH_INVERT(vendor, model, axes) { \ + .ident = vendor " " model, \ + .callback = hdaps_dmi_match_invert, \ + .driver_data = (void *)axes, \ + .matches = { \ + DMI_MATCH(DMI_BOARD_VENDOR, vendor), \ + DMI_MATCH(DMI_PRODUCT_VERSION, model) \ + } \ +} + +#define HDAPS_DMI_MATCH_NORMAL(vendor, model) \ + HDAPS_DMI_MATCH_INVERT(vendor, model, 0) + +/* Note that HDAPS_DMI_MATCH_NORMAL("ThinkPad T42") would match + "ThinkPad T42p", so the order of the entries matters. + If your ThinkPad is not recognized, please update to latest + BIOS. This is especially the case for some R52 ThinkPads. */ +static const struct dmi_system_id hdaps_whitelist[] __initconst = { + HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad R50p", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R50"), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R51"), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R52"), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad R61i", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad R61", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad T41p", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T41"), + HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad T42p", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T42"), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T43"), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T400", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T60", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T61p", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T61", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad X40"), + HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad X41", HDAPS_Y_AXIS), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X60", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X61s", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X61", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad Z60m"), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad Z61m", HDAPS_BOTH_AXES), + HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad Z61p", HDAPS_BOTH_AXES), + { .ident = NULL } +}; + +static int __init hdaps_init(void) +{ + int ret; + + if (!dmi_check_system(hdaps_whitelist)) { + pr_warn("supported laptop not found!\n"); + ret = -ENODEV; + goto out; + } + + if (!request_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS, "hdaps")) { + ret = -ENXIO; + goto out; + } + + ret = platform_driver_register(&hdaps_driver); + if (ret) + goto out_region; + + pdev = platform_device_register_simple("hdaps", PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(pdev)) { + ret = PTR_ERR(pdev); + goto out_driver; + } + + ret = sysfs_create_group(&pdev->dev.kobj, &hdaps_attribute_group); + if (ret) + goto out_device; + + hdaps_idev = input_allocate_device(); + if (!hdaps_idev) { + ret = -ENOMEM; + goto out_group; + } + + /* initial calibrate for the input device */ + hdaps_calibrate(); + + /* initialize the input class */ + hdaps_idev->name = "hdaps"; + hdaps_idev->phys = "isa1600/input0"; + hdaps_idev->id.bustype = BUS_ISA; + hdaps_idev->dev.parent = &pdev->dev; + input_set_abs_params(hdaps_idev, ABS_X, + -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT); + input_set_abs_params(hdaps_idev, ABS_Y, + -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT); + + ret = input_setup_polling(hdaps_idev, hdaps_mousedev_poll); + if (ret) + goto out_idev; + + input_set_poll_interval(hdaps_idev, HDAPS_POLL_INTERVAL); + + ret = input_register_device(hdaps_idev); + if (ret) + goto out_idev; + + pr_info("driver successfully loaded\n"); + return 0; + +out_idev: + input_free_device(hdaps_idev); +out_group: + sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group); +out_device: + platform_device_unregister(pdev); +out_driver: + platform_driver_unregister(&hdaps_driver); +out_region: + release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS); +out: + pr_warn("driver init failed (ret=%d)!\n", ret); + return ret; +} + +static void __exit hdaps_exit(void) +{ + input_unregister_device(hdaps_idev); + sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group); + platform_device_unregister(pdev); + platform_driver_unregister(&hdaps_driver); + release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS); + + pr_info("driver unloaded\n"); +} + +module_init(hdaps_init); +module_exit(hdaps_exit); + +module_param_named(invert, hdaps_invert, int, 0); +MODULE_PARM_DESC(invert, "invert data along each axis. 1 invert x-axis, " + "2 invert y-axis, 3 invert both axes."); + +MODULE_AUTHOR("Robert Love"); +MODULE_DESCRIPTION("IBM Hard Drive Active Protection System (HDAPS) driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/hp/Kconfig b/drivers/platform/x86/hp/Kconfig new file mode 100644 index 000000000..dd51491b9 --- /dev/null +++ b/drivers/platform/x86/hp/Kconfig @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# X86 Platform Specific Drivers +# +menuconfig X86_PLATFORM_DRIVERS_HP + bool "HP X86 Platform Specific Device Drivers" + depends on X86_PLATFORM_DEVICES + help + Say Y here to get to see options for device drivers for various + HP x86 platforms, including vendor-specific laptop extension drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if X86_PLATFORM_DRIVERS_HP + +config HP_ACCEL + tristate "HP laptop accelerometer" + default m + depends on INPUT && ACPI + depends on SERIO_I8042 + select SENSORS_LIS3LV02D + select NEW_LEDS + select LEDS_CLASS + help + This driver provides support for the "Mobile Data Protection System 3D" + or "3D DriveGuard" feature of HP laptops. On such systems the driver + should load automatically (via ACPI alias). + + Support for a led indicating disk protection will be provided as + hp::hddprotect. For more information on the feature, refer to + Documentation/misc-devices/lis3lv02d.rst. + + To compile this driver as a module, choose M here: the module will + be called hp_accel. + +config HP_WMI + tristate "HP WMI extras" + default m + depends on ACPI_EC + depends on ACPI_WMI + depends on INPUT + depends on RFKILL || RFKILL = n + select POWER_SUPPLY + select INPUT_SPARSEKMAP + select ACPI_PLATFORM_PROFILE + select HWMON + help + Say Y here if you want to support WMI-based hotkeys on HP laptops and + to read data from WMI such as docking or ambient light sensor state. + + To compile this driver as a module, choose M here: the module will + be called hp-wmi. + +config TC1100_WMI + tristate "HP Compaq TC1100 Tablet WMI Extras" + default m + depends on !X86_64 + depends on ACPI + depends on ACPI_WMI + help + This is a driver for the WMI extensions (wireless and bluetooth power + control) of the HP Compaq TC1100 tablet. + +config HP_BIOSCFG + tristate "HP BIOS Configuration Driver" + default m + depends on ACPI_WMI + select NLS + select FW_ATTR_CLASS + help + This driver enables administrators to securely manage BIOS settings + using digital certificates and public-key cryptography that eliminate + the need for passwords for both remote and local management. It supports + changing BIOS settings on many HP machines from 2018 and newer without + the use of any additional software. + + To compile this driver as a module, choose M here: the module will + be called hp-bioscfg. + +endif # X86_PLATFORM_DRIVERS_HP diff --git a/drivers/platform/x86/hp/Makefile b/drivers/platform/x86/hp/Makefile new file mode 100644 index 000000000..e4f908a61 --- /dev/null +++ b/drivers/platform/x86/hp/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/hp +# HP x86 Platform-Specific Drivers +# + +# Hewlett Packard +obj-$(CONFIG_HP_ACCEL) += hp_accel.o +obj-$(CONFIG_HP_WMI) += hp-wmi.o +obj-$(CONFIG_TC1100_WMI) += tc1100-wmi.o +obj-$(CONFIG_HP_BIOSCFG) += hp-bioscfg/ diff --git a/drivers/platform/x86/hp/hp-bioscfg/Makefile b/drivers/platform/x86/hp/hp-bioscfg/Makefile new file mode 100644 index 000000000..7d23649b3 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/Makefile @@ -0,0 +1,11 @@ +obj-$(CONFIG_HP_BIOSCFG) := hp-bioscfg.o + +hp-bioscfg-y := bioscfg.o \ + biosattr-interface.o \ + enum-attributes.o \ + int-attributes.o \ + order-list-attributes.o \ + passwdobj-attributes.o \ + spmobj-attributes.o \ + string-attributes.o \ + surestart-attributes.o diff --git a/drivers/platform/x86/hp/hp-bioscfg/biosattr-interface.c b/drivers/platform/x86/hp/hp-bioscfg/biosattr-interface.c new file mode 100644 index 000000000..4da99cb72 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/biosattr-interface.c @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to methods under BIOS interface GUID + * for use with hp-bioscfg driver. + * + * Copyright (c) 2022 Hewlett-Packard Inc. + */ + +#include +#include "bioscfg.h" + +/* + * struct bios_args buffer is dynamically allocated. New WMI command types + * were introduced that exceeds 128-byte data size. Changes to handle + * the data size allocation scheme were kept in hp_wmi_perform_query function. + */ +struct bios_args { + u32 signature; + u32 command; + u32 commandtype; + u32 datasize; + u8 data[] __counted_by(datasize); +}; + +/** + * hp_set_attribute + * + * @a_name: The attribute name + * @a_value: The attribute value + * + * Sets an attribute to new value + * + * Returns zero on success + * -ENODEV if device is not found + * -EINVAL if the instance of 'Setup Admin' password is not found. + * -ENOMEM unable to allocate memory + */ +int hp_set_attribute(const char *a_name, const char *a_value) +{ + int security_area_size; + int a_name_size, a_value_size; + u16 *buffer = NULL; + u16 *start; + int buffer_size, instance, ret; + char *auth_token_choice; + + mutex_lock(&bioscfg_drv.mutex); + + instance = hp_get_password_instance_for_type(SETUP_PASSWD); + if (instance < 0) { + ret = -EINVAL; + goto out_set_attribute; + } + + /* Select which auth token to use; password or [auth token] */ + if (bioscfg_drv.spm_data.auth_token) + auth_token_choice = bioscfg_drv.spm_data.auth_token; + else + auth_token_choice = bioscfg_drv.password_data[instance].current_password; + + a_name_size = hp_calculate_string_buffer(a_name); + a_value_size = hp_calculate_string_buffer(a_value); + security_area_size = hp_calculate_security_buffer(auth_token_choice); + buffer_size = a_name_size + a_value_size + security_area_size; + + buffer = kmalloc(buffer_size + 1, GFP_KERNEL); + if (!buffer) { + ret = -ENOMEM; + goto out_set_attribute; + } + + /* build variables to set */ + start = buffer; + start = hp_ascii_to_utf16_unicode(start, a_name); + if (!start) { + ret = -EINVAL; + goto out_set_attribute; + } + + start = hp_ascii_to_utf16_unicode(start, a_value); + if (!start) { + ret = -EINVAL; + goto out_set_attribute; + } + + ret = hp_populate_security_buffer(start, auth_token_choice); + if (ret < 0) + goto out_set_attribute; + + ret = hp_wmi_set_bios_setting(buffer, buffer_size); + +out_set_attribute: + kfree(buffer); + mutex_unlock(&bioscfg_drv.mutex); + return ret; +} + +/** + * hp_wmi_perform_query + * + * @query: The commandtype (enum hp_wmi_commandtype) + * @command: The command (enum hp_wmi_command) + * @buffer: Buffer used as input and/or output + * @insize: Size of input buffer + * @outsize: Size of output buffer + * + * returns zero on success + * an HP WMI query specific error code (which is positive) + * -EINVAL if the query was not successful at all + * -EINVAL if the output buffer size exceeds buffersize + * + * Note: The buffersize must at least be the maximum of the input and output + * size. E.g. Battery info query is defined to have 1 byte input + * and 128 byte output. The caller would do: + * buffer = kzalloc(128, GFP_KERNEL); + * ret = hp_wmi_perform_query(HPWMI_BATTERY_QUERY, HPWMI_READ, + * buffer, 1, 128) + */ +int hp_wmi_perform_query(int query, enum hp_wmi_command command, void *buffer, + u32 insize, u32 outsize) +{ + struct acpi_buffer input, output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct bios_return *bios_return; + union acpi_object *obj = NULL; + struct bios_args *args = NULL; + int mid, actual_outsize, ret; + size_t bios_args_size; + + mid = hp_encode_outsize_for_pvsz(outsize); + if (WARN_ON(mid < 0)) + return mid; + + bios_args_size = struct_size(args, data, insize); + args = kmalloc(bios_args_size, GFP_KERNEL); + if (!args) + return -ENOMEM; + + input.length = bios_args_size; + input.pointer = args; + + /* BIOS expects 'SECU' in hex as the signature value*/ + args->signature = 0x55434553; + args->command = command; + args->commandtype = query; + args->datasize = insize; + memcpy(args->data, buffer, flex_array_size(args, data, insize)); + + ret = wmi_evaluate_method(HP_WMI_BIOS_GUID, 0, mid, &input, &output); + if (ret) + goto out_free; + + obj = output.pointer; + if (!obj) { + ret = -EINVAL; + goto out_free; + } + + if (obj->type != ACPI_TYPE_BUFFER || + obj->buffer.length < sizeof(*bios_return)) { + pr_warn("query 0x%x returned wrong type or too small buffer\n", query); + ret = -EINVAL; + goto out_free; + } + + bios_return = (struct bios_return *)obj->buffer.pointer; + ret = bios_return->return_code; + if (ret) { + if (ret != INVALID_CMD_VALUE && ret != INVALID_CMD_TYPE) + pr_warn("query 0x%x returned error 0x%x\n", query, ret); + goto out_free; + } + + /* Ignore output data of zero size */ + if (!outsize) + goto out_free; + + actual_outsize = min_t(u32, outsize, obj->buffer.length - sizeof(*bios_return)); + memcpy_and_pad(buffer, outsize, obj->buffer.pointer + sizeof(*bios_return), + actual_outsize, 0); + +out_free: + ret = hp_wmi_error_and_message(ret); + + kfree(obj); + kfree(args); + return ret; +} + +static void *utf16_empty_string(u16 *p) +{ + *p++ = 2; + *p++ = 0x00; + return p; +} + +/** + * hp_ascii_to_utf16_unicode - Convert ascii string to UTF-16 unicode + * + * BIOS supports UTF-16 characters that are 2 bytes long. No variable + * multi-byte language supported. + * + * @p: Unicode buffer address + * @str: string to convert to unicode + * + * Returns a void pointer to the buffer string + */ +void *hp_ascii_to_utf16_unicode(u16 *p, const u8 *str) +{ + int len = strlen(str); + int ret; + + /* + * Add null character when reading an empty string + * "02 00 00 00" + */ + if (len == 0) + return utf16_empty_string(p); + + /* Move pointer len * 2 number of bytes */ + *p++ = len * 2; + ret = utf8s_to_utf16s(str, strlen(str), UTF16_HOST_ENDIAN, p, len); + if (ret < 0) { + dev_err(bioscfg_drv.class_dev, "UTF16 conversion failed\n"); + return NULL; + } + + if (ret * sizeof(u16) > U16_MAX) { + dev_err(bioscfg_drv.class_dev, "Error string too long\n"); + return NULL; + } + + p += len; + return p; +} + +/** + * hp_wmi_set_bios_setting - Set setting's value in BIOS + * + * @input_buffer: Input buffer address + * @input_size: Input buffer size + * + * Returns: Count of unicode characters written to BIOS if successful, otherwise + * -ENOMEM unable to allocate memory + * -EINVAL buffer not allocated or too small + */ +int hp_wmi_set_bios_setting(u16 *input_buffer, u32 input_size) +{ + union acpi_object *obj; + struct acpi_buffer input = {input_size, input_buffer}; + struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL}; + int ret; + + ret = wmi_evaluate_method(HP_WMI_SET_BIOS_SETTING_GUID, 0, 1, &input, &output); + + obj = output.pointer; + if (!obj) + return -EINVAL; + + if (obj->type != ACPI_TYPE_INTEGER) { + ret = -EINVAL; + goto out_free; + } + + ret = obj->integer.value; + if (ret) { + ret = hp_wmi_error_and_message(ret); + goto out_free; + } + +out_free: + kfree(obj); + return ret; +} + +static int hp_attr_set_interface_probe(struct wmi_device *wdev, const void *context) +{ + mutex_lock(&bioscfg_drv.mutex); + mutex_unlock(&bioscfg_drv.mutex); + return 0; +} + +static void hp_attr_set_interface_remove(struct wmi_device *wdev) +{ + mutex_lock(&bioscfg_drv.mutex); + mutex_unlock(&bioscfg_drv.mutex); +} + +static const struct wmi_device_id hp_attr_set_interface_id_table[] = { + { .guid_string = HP_WMI_BIOS_GUID}, + { } +}; + +static struct wmi_driver hp_attr_set_interface_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = hp_attr_set_interface_probe, + .remove = hp_attr_set_interface_remove, + .id_table = hp_attr_set_interface_id_table, +}; + +int hp_init_attr_set_interface(void) +{ + return wmi_driver_register(&hp_attr_set_interface_driver); +} + +void hp_exit_attr_set_interface(void) +{ + wmi_driver_unregister(&hp_attr_set_interface_driver); +} + +MODULE_DEVICE_TABLE(wmi, hp_attr_set_interface_id_table); diff --git a/drivers/platform/x86/hp/hp-bioscfg/bioscfg.c b/drivers/platform/x86/hp/hp-bioscfg/bioscfg.c new file mode 100644 index 000000000..27fd6cd21 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/bioscfg.c @@ -0,0 +1,1067 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Common methods for use with hp-bioscfg driver + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include "bioscfg.h" +#include "../../firmware_attributes_class.h" +#include +#include + +MODULE_AUTHOR("Jorge Lopez "); +MODULE_DESCRIPTION("HP BIOS Configuration Driver"); +MODULE_LICENSE("GPL"); + +struct bioscfg_priv bioscfg_drv = { + .mutex = __MUTEX_INITIALIZER(bioscfg_drv.mutex), +}; + +ssize_t display_name_language_code_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", LANG_CODE_STR); +} + +struct kobj_attribute common_display_langcode = + __ATTR_RO(display_name_language_code); + +int hp_get_integer_from_buffer(u8 **buffer, u32 *buffer_size, u32 *integer) +{ + int *ptr = PTR_ALIGN((int *)*buffer, sizeof(int)); + + /* Ensure there is enough space remaining to read the integer */ + if (*buffer_size < sizeof(int)) + return -EINVAL; + + *integer = *(ptr++); + *buffer = (u8 *)ptr; + *buffer_size -= sizeof(int); + + return 0; +} + +int hp_get_string_from_buffer(u8 **buffer, u32 *buffer_size, char *dst, u32 dst_size) +{ + u16 *src = (u16 *)*buffer; + u16 src_size; + + u16 size; + int i; + int conv_dst_size; + + if (*buffer_size < sizeof(u16)) + return -EINVAL; + + src_size = *(src++); + /* size value in u16 chars */ + size = src_size / sizeof(u16); + + /* Ensure there is enough space remaining to read and convert + * the string + */ + if (*buffer_size < src_size) + return -EINVAL; + + for (i = 0; i < size; i++) + if (src[i] == '\\' || + src[i] == '\r' || + src[i] == '\n' || + src[i] == '\t') + size++; + + /* + * Conversion is limited to destination string max number of + * bytes. + */ + conv_dst_size = size; + if (size > dst_size) + conv_dst_size = dst_size - 1; + + /* + * convert from UTF-16 unicode to ASCII + */ + utf16s_to_utf8s(src, src_size, UTF16_HOST_ENDIAN, dst, conv_dst_size); + dst[conv_dst_size] = 0; + + for (i = 0; i < conv_dst_size; i++) { + if (*src == '\\' || + *src == '\r' || + *src == '\n' || + *src == '\t') { + dst[i++] = '\\'; + if (i == conv_dst_size) + break; + } + + if (*src == '\r') + dst[i] = 'r'; + else if (*src == '\n') + dst[i] = 'n'; + else if (*src == '\t') + dst[i] = 't'; + else if (*src == '"') + dst[i] = '\''; + else + dst[i] = *src; + src++; + } + + *buffer = (u8 *)src; + *buffer_size -= size * sizeof(u16); + + return size; +} + +int hp_get_common_data_from_buffer(u8 **buffer_ptr, u32 *buffer_size, + struct common_data *common_data) +{ + int ret = 0; + int reqs; + + // PATH: + ret = hp_get_string_from_buffer(buffer_ptr, buffer_size, common_data->path, + sizeof(common_data->path)); + if (ret < 0) + goto common_exit; + + // IS_READONLY: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->is_readonly); + if (ret < 0) + goto common_exit; + + //DISPLAY_IN_UI: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->display_in_ui); + if (ret < 0) + goto common_exit; + + // REQUIRES_PHYSICAL_PRESENCE: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->requires_physical_presence); + if (ret < 0) + goto common_exit; + + // SEQUENCE: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->sequence); + if (ret < 0) + goto common_exit; + + // PREREQUISITES_SIZE: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->prerequisites_size); + if (ret < 0) + goto common_exit; + + if (common_data->prerequisites_size > MAX_PREREQUISITES_SIZE) { + /* Report a message and limit prerequisite size to maximum value */ + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + common_data->prerequisites_size = MAX_PREREQUISITES_SIZE; + } + + // PREREQUISITES: + for (reqs = 0; reqs < common_data->prerequisites_size; reqs++) { + ret = hp_get_string_from_buffer(buffer_ptr, buffer_size, + common_data->prerequisites[reqs], + sizeof(common_data->prerequisites[reqs])); + if (ret < 0) + break; + } + + // SECURITY_LEVEL: + ret = hp_get_integer_from_buffer(buffer_ptr, buffer_size, + &common_data->security_level); + +common_exit: + return ret; +} + +int hp_enforce_single_line_input(char *buf, size_t count) +{ + char *p; + + p = memchr(buf, '\n', count); + + if (p == buf + count - 1) + *p = '\0'; /* strip trailing newline */ + else if (p) + return -EINVAL; /* enforce single line input */ + + return 0; +} + +/* Set pending reboot value and generate KOBJ_NAME event */ +void hp_set_reboot_and_signal_event(void) +{ + bioscfg_drv.pending_reboot = true; + kobject_uevent(&bioscfg_drv.class_dev->kobj, KOBJ_CHANGE); +} + +/** + * hp_calculate_string_buffer() - determines size of string buffer for + * use with BIOS communication + * + * @str: the string to calculate based upon + */ +size_t hp_calculate_string_buffer(const char *str) +{ + size_t length = strlen(str); + + /* BIOS expects 4 bytes when an empty string is found */ + if (length == 0) + return 4; + + /* u16 length field + one UTF16 char for each input char */ + return sizeof(u16) + strlen(str) * sizeof(u16); +} + +int hp_wmi_error_and_message(int error_code) +{ + char *error_msg = NULL; + int ret; + + switch (error_code) { + case SUCCESS: + error_msg = "Success"; + ret = 0; + break; + case CMD_FAILED: + error_msg = "Command failed"; + ret = -EINVAL; + break; + case INVALID_SIGN: + error_msg = "Invalid signature"; + ret = -EINVAL; + break; + case INVALID_CMD_VALUE: + error_msg = "Invalid command value/Feature not supported"; + ret = -EOPNOTSUPP; + break; + case INVALID_CMD_TYPE: + error_msg = "Invalid command type"; + ret = -EINVAL; + break; + case INVALID_DATA_SIZE: + error_msg = "Invalid data size"; + ret = -EINVAL; + break; + case INVALID_CMD_PARAM: + error_msg = "Invalid command parameter"; + ret = -EINVAL; + break; + case ENCRYP_CMD_REQUIRED: + error_msg = "Secure/encrypted command required"; + ret = -EACCES; + break; + case NO_SECURE_SESSION: + error_msg = "No secure session established"; + ret = -EACCES; + break; + case SECURE_SESSION_FOUND: + error_msg = "Secure session already established"; + ret = -EACCES; + break; + case SECURE_SESSION_FAILED: + error_msg = "Secure session failed"; + ret = -EIO; + break; + case AUTH_FAILED: + error_msg = "Other permission/Authentication failed"; + ret = -EACCES; + break; + case INVALID_BIOS_AUTH: + error_msg = "Invalid BIOS administrator password"; + ret = -EINVAL; + break; + case NONCE_DID_NOT_MATCH: + error_msg = "Nonce did not match"; + ret = -EINVAL; + break; + case GENERIC_ERROR: + error_msg = "Generic/Other error"; + ret = -EIO; + break; + case BIOS_ADMIN_POLICY_NOT_MET: + error_msg = "BIOS Admin password does not meet password policy requirements"; + ret = -EINVAL; + break; + case BIOS_ADMIN_NOT_SET: + error_msg = "BIOS Setup password is not set"; + ret = -EPERM; + break; + case P21_NO_PROVISIONED: + error_msg = "P21 is not provisioned"; + ret = -EPERM; + break; + case P21_PROVISION_IN_PROGRESS: + error_msg = "P21 is already provisioned or provisioning is in progress and a signing key has already been sent"; + ret = -EINPROGRESS; + break; + case P21_IN_USE: + error_msg = "P21 in use (cannot deprovision)"; + ret = -EPERM; + break; + case HEP_NOT_ACTIVE: + error_msg = "HEP not activated"; + ret = -EPERM; + break; + case HEP_ALREADY_SET: + error_msg = "HEP Transport already set"; + ret = -EINVAL; + break; + case HEP_CHECK_STATE: + error_msg = "Check the current HEP state"; + ret = -EINVAL; + break; + default: + error_msg = "Generic/Other error"; + ret = -EIO; + break; + } + + if (error_code) + pr_warn_ratelimited("Returned error 0x%x, \"%s\"\n", error_code, error_msg); + + return ret; +} + +static ssize_t pending_reboot_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", bioscfg_drv.pending_reboot); +} + +static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); + +/* + * create_attributes_level_sysfs_files() - Creates pending_reboot attributes + */ +static int create_attributes_level_sysfs_files(void) +{ + return sysfs_create_file(&bioscfg_drv.main_dir_kset->kobj, + &pending_reboot.attr); +} + +static void attr_name_release(struct kobject *kobj) +{ + kfree(kobj); +} + +static const struct kobj_type attr_name_ktype = { + .release = attr_name_release, + .sysfs_ops = &kobj_sysfs_ops, +}; + +/** + * hp_get_wmiobj_pointer() - Get Content of WMI block for particular instance + * + * @instance_id: WMI instance ID + * @guid_string: WMI GUID (in str form) + * + * Fetches the content for WMI block (instance_id) under GUID (guid_string) + * Caller must kfree the return + */ +union acpi_object *hp_get_wmiobj_pointer(int instance_id, const char *guid_string) +{ + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + status = wmi_query_block(guid_string, instance_id, &out); + return ACPI_SUCCESS(status) ? (union acpi_object *)out.pointer : NULL; +} + +/** + * hp_get_instance_count() - Compute total number of instances under guid_string + * + * @guid_string: WMI GUID (in string form) + */ +int hp_get_instance_count(const char *guid_string) +{ + int ret; + + ret = wmi_instance_count(guid_string); + if (ret < 0) + return 0; + + return ret; +} + +/** + * hp_alloc_attributes_data() - Allocate attributes data for a particular type + * + * @attr_type: Attribute type to allocate + */ +static int hp_alloc_attributes_data(int attr_type) +{ + switch (attr_type) { + case HPWMI_STRING_TYPE: + return hp_alloc_string_data(); + + case HPWMI_INTEGER_TYPE: + return hp_alloc_integer_data(); + + case HPWMI_ENUMERATION_TYPE: + return hp_alloc_enumeration_data(); + + case HPWMI_ORDERED_LIST_TYPE: + return hp_alloc_ordered_list_data(); + + case HPWMI_PASSWORD_TYPE: + return hp_alloc_password_data(); + + default: + return 0; + } +} + +int hp_convert_hexstr_to_str(const char *input, u32 input_len, char **str, int *len) +{ + int ret = 0; + int new_len = 0; + char tmp[] = "0x00"; + char *new_str = NULL; + long ch; + int i; + + if (input_len <= 0 || !input || !str || !len) + return -EINVAL; + + *len = 0; + *str = NULL; + + new_str = kmalloc(input_len, GFP_KERNEL); + if (!new_str) + return -ENOMEM; + + for (i = 0; i < input_len; i += 5) { + strscpy(tmp, input + i); + if (kstrtol(tmp, 16, &ch) == 0) { + // escape char + if (ch == '\\' || + ch == '\r' || + ch == '\n' || ch == '\t') { + if (ch == '\r') + ch = 'r'; + else if (ch == '\n') + ch = 'n'; + else if (ch == '\t') + ch = 't'; + new_str[new_len++] = '\\'; + } + new_str[new_len++] = ch; + if (ch == '\0') + break; + } + } + + if (new_len) { + new_str[new_len] = '\0'; + *str = krealloc(new_str, (new_len + 1) * sizeof(char), + GFP_KERNEL); + if (*str) + *len = new_len; + else + ret = -ENOMEM; + } else { + ret = -EFAULT; + } + + if (ret) + kfree(new_str); + return ret; +} + +/* map output size to the corresponding WMI method id */ +int hp_encode_outsize_for_pvsz(int outsize) +{ + if (outsize > 4096) + return -EINVAL; + if (outsize > 1024) + return 5; + if (outsize > 128) + return 4; + if (outsize > 4) + return 3; + if (outsize > 0) + return 2; + return 1; +} + +/* + * Update friendly display name for several attributes associated to + * 'Schedule Power-On' + */ +void hp_friendly_user_name_update(char *path, const char *attr_name, + char *attr_display, int attr_size) +{ + if (strstr(path, SCHEDULE_POWER_ON)) + snprintf(attr_display, attr_size, "%s - %s", SCHEDULE_POWER_ON, attr_name); + else + strscpy(attr_display, attr_name, attr_size); +} + +/** + * hp_update_attribute_permissions() - Update attributes permissions when + * isReadOnly value is 1 + * + * @is_readonly: bool value to indicate if it a readonly attribute. + * @current_val: kobj_attribute corresponding to attribute. + * + */ +void hp_update_attribute_permissions(bool is_readonly, struct kobj_attribute *current_val) +{ + current_val->attr.mode = is_readonly ? 0444 : 0644; +} + +/** + * destroy_attribute_objs() - Free a kset of kobjects + * @kset: The kset to destroy + * + * Fress kobjects created for each attribute_name under attribute type kset + */ +static void destroy_attribute_objs(struct kset *kset) +{ + struct kobject *pos, *next; + + list_for_each_entry_safe(pos, next, &kset->list, entry) + kobject_put(pos); +} + +/** + * release_attributes_data() - Clean-up all sysfs directories and files created + */ +static void release_attributes_data(void) +{ + mutex_lock(&bioscfg_drv.mutex); + + hp_exit_string_attributes(); + hp_exit_integer_attributes(); + hp_exit_enumeration_attributes(); + hp_exit_ordered_list_attributes(); + hp_exit_password_attributes(); + hp_exit_sure_start_attributes(); + hp_exit_secure_platform_attributes(); + + if (bioscfg_drv.authentication_dir_kset) { + destroy_attribute_objs(bioscfg_drv.authentication_dir_kset); + kset_unregister(bioscfg_drv.authentication_dir_kset); + bioscfg_drv.authentication_dir_kset = NULL; + } + if (bioscfg_drv.main_dir_kset) { + sysfs_remove_file(&bioscfg_drv.main_dir_kset->kobj, &pending_reboot.attr); + destroy_attribute_objs(bioscfg_drv.main_dir_kset); + kset_unregister(bioscfg_drv.main_dir_kset); + bioscfg_drv.main_dir_kset = NULL; + } + mutex_unlock(&bioscfg_drv.mutex); +} + +/** + * hp_add_other_attributes() - Initialize HP custom attributes not + * reported by BIOS and required to support Secure Platform and Sure + * Start. + * + * @attr_type: Custom HP attribute not reported by BIOS + * + * Initialize all 2 types of attributes: Platform and Sure Start + * object. Populates each attribute types respective properties + * under sysfs files. + * + * Returns zero(0) if successful. Otherwise, a negative value. + */ +static int hp_add_other_attributes(int attr_type) +{ + struct kobject *attr_name_kobj; + int ret; + char *attr_name; + + attr_name_kobj = kzalloc_obj(*attr_name_kobj); + if (!attr_name_kobj) + return -ENOMEM; + + mutex_lock(&bioscfg_drv.mutex); + + /* Check if attribute type is supported */ + switch (attr_type) { + case HPWMI_SECURE_PLATFORM_TYPE: + attr_name_kobj->kset = bioscfg_drv.authentication_dir_kset; + attr_name = SPM_STR; + break; + + case HPWMI_SURE_START_TYPE: + attr_name_kobj->kset = bioscfg_drv.main_dir_kset; + attr_name = SURE_START_STR; + break; + + default: + pr_err("Error: Unknown attr_type: %d\n", attr_type); + ret = -EINVAL; + kfree(attr_name_kobj); + goto unlock_drv_mutex; + } + + ret = kobject_init_and_add(attr_name_kobj, &attr_name_ktype, + NULL, "%s", attr_name); + if (ret) { + pr_err("Error encountered [%d]\n", ret); + goto err_other_attr_init; + } + + /* Populate attribute data */ + switch (attr_type) { + case HPWMI_SECURE_PLATFORM_TYPE: + ret = hp_populate_secure_platform_data(attr_name_kobj); + break; + + case HPWMI_SURE_START_TYPE: + ret = hp_populate_sure_start_data(attr_name_kobj); + break; + + default: + ret = -EINVAL; + } + + if (ret) + goto err_other_attr_init; + + mutex_unlock(&bioscfg_drv.mutex); + return 0; + +err_other_attr_init: + kobject_put(attr_name_kobj); +unlock_drv_mutex: + mutex_unlock(&bioscfg_drv.mutex); + return ret; +} + +static int hp_init_bios_package_attribute(enum hp_wmi_data_type attr_type, + union acpi_object *obj, + const char *guid, int min_elements, + int instance_id) +{ + struct kobject *attr_name_kobj, *duplicate; + union acpi_object *elements; + struct kset *temp_kset; + + char *str_value = NULL; + int str_len; + int ret = 0; + + /* Take action appropriate to each ACPI TYPE */ + if (obj->package.count < min_elements) { + pr_err("ACPI-package does not have enough elements: %d < %d\n", + obj->package.count, min_elements); + goto pack_attr_exit; + } + + elements = obj->package.elements; + + /* sanity checking */ + if (elements[NAME].type != ACPI_TYPE_STRING) { + pr_debug("incorrect element type\n"); + goto pack_attr_exit; + } + if (strlen(elements[NAME].string.pointer) == 0) { + pr_debug("empty attribute found\n"); + goto pack_attr_exit; + } + + if (attr_type == HPWMI_PASSWORD_TYPE) + temp_kset = bioscfg_drv.authentication_dir_kset; + else + temp_kset = bioscfg_drv.main_dir_kset; + + /* convert attribute name to string */ + ret = hp_convert_hexstr_to_str(elements[NAME].string.pointer, + elements[NAME].string.length, + &str_value, &str_len); + + if (ret) { + pr_debug("Failed to populate integer package data. Error [0%0x]\n", + ret); + kfree(str_value); + return ret; + } + + if (!str_value || !str_value[0]) { + pr_debug("Ignoring attribute with empty name\n"); + goto pack_attr_exit; + } + + /* All duplicate attributes found are ignored */ + duplicate = kset_find_obj(temp_kset, str_value); + if (duplicate) { + pr_debug("Duplicate attribute name found - %s\n", str_value); + /* kset_find_obj() returns a reference */ + kobject_put(duplicate); + goto pack_attr_exit; + } + + /* build attribute */ + attr_name_kobj = kzalloc_obj(*attr_name_kobj); + if (!attr_name_kobj) { + ret = -ENOMEM; + goto pack_attr_exit; + } + + attr_name_kobj->kset = temp_kset; + + ret = kobject_init_and_add(attr_name_kobj, &attr_name_ktype, + NULL, "%s", str_value); + + if (ret) { + kobject_put(attr_name_kobj); + goto pack_attr_exit; + } + + /* enumerate all of these attributes */ + switch (attr_type) { + case HPWMI_STRING_TYPE: + ret = hp_populate_string_package_data(elements, + instance_id, + attr_name_kobj); + break; + case HPWMI_INTEGER_TYPE: + ret = hp_populate_integer_package_data(elements, + instance_id, + attr_name_kobj); + break; + case HPWMI_ENUMERATION_TYPE: + ret = hp_populate_enumeration_package_data(elements, + instance_id, + attr_name_kobj); + break; + case HPWMI_ORDERED_LIST_TYPE: + ret = hp_populate_ordered_list_package_data(elements, + instance_id, + attr_name_kobj); + break; + case HPWMI_PASSWORD_TYPE: + ret = hp_populate_password_package_data(elements, + instance_id, + attr_name_kobj); + break; + default: + pr_debug("Unknown attribute type found: 0x%x\n", attr_type); + break; + } + +pack_attr_exit: + kfree(str_value); + return ret; +} + +static int hp_init_bios_buffer_attribute(enum hp_wmi_data_type attr_type, + union acpi_object *obj, + const char *guid, int min_elements, + int instance_id) +{ + struct kobject *attr_name_kobj, *duplicate; + struct kset *temp_kset; + char str[MAX_BUFF_SIZE]; + + char *temp_str = NULL; + char *str_value = NULL; + u8 *buffer_ptr = NULL; + int buffer_size; + int ret = 0; + + buffer_size = obj->buffer.length; + buffer_ptr = obj->buffer.pointer; + + ret = hp_get_string_from_buffer(&buffer_ptr, + &buffer_size, str, MAX_BUFF_SIZE); + + if (ret < 0) + goto buff_attr_exit; + + if (strlen(str) == 0) { + pr_debug("Ignoring attribute with empty name\n"); + ret = 0; + goto buff_attr_exit; + } + + if (attr_type == HPWMI_PASSWORD_TYPE || + attr_type == HPWMI_SECURE_PLATFORM_TYPE) + temp_kset = bioscfg_drv.authentication_dir_kset; + else + temp_kset = bioscfg_drv.main_dir_kset; + + /* All duplicate attributes found are ignored */ + duplicate = kset_find_obj(temp_kset, str); + if (duplicate) { + pr_debug("Duplicate attribute name found - %s\n", str); + /* kset_find_obj() returns a reference */ + kobject_put(duplicate); + goto buff_attr_exit; + } + + /* build attribute */ + attr_name_kobj = kzalloc_obj(*attr_name_kobj); + if (!attr_name_kobj) { + ret = -ENOMEM; + goto buff_attr_exit; + } + + attr_name_kobj->kset = temp_kset; + + temp_str = str; + if (attr_type == HPWMI_SECURE_PLATFORM_TYPE) + temp_str = "SPM"; + + ret = kobject_init_and_add(attr_name_kobj, + &attr_name_ktype, NULL, "%s", temp_str); + if (ret) { + kobject_put(attr_name_kobj); + goto buff_attr_exit; + } + + /* enumerate all of these attributes */ + switch (attr_type) { + case HPWMI_STRING_TYPE: + ret = hp_populate_string_buffer_data(buffer_ptr, + &buffer_size, + instance_id, + attr_name_kobj); + break; + case HPWMI_INTEGER_TYPE: + ret = hp_populate_integer_buffer_data(buffer_ptr, + &buffer_size, + instance_id, + attr_name_kobj); + break; + case HPWMI_ENUMERATION_TYPE: + ret = hp_populate_enumeration_buffer_data(buffer_ptr, + &buffer_size, + instance_id, + attr_name_kobj); + break; + case HPWMI_ORDERED_LIST_TYPE: + ret = hp_populate_ordered_list_buffer_data(buffer_ptr, + &buffer_size, + instance_id, + attr_name_kobj); + break; + case HPWMI_PASSWORD_TYPE: + ret = hp_populate_password_buffer_data(buffer_ptr, + &buffer_size, + instance_id, + attr_name_kobj); + break; + default: + pr_debug("Unknown attribute type found: 0x%x\n", attr_type); + break; + } + +buff_attr_exit: + kfree(str_value); + return ret; +} + +/** + * hp_init_bios_attributes() - Initialize all attributes for a type + * @attr_type: The attribute type to initialize + * @guid: The WMI GUID associated with this type to initialize + * + * Initialize all 5 types of attributes: enumeration, integer, + * string, password, ordered list object. Populates each attribute types + * respective properties under sysfs files + */ +static int hp_init_bios_attributes(enum hp_wmi_data_type attr_type, const char *guid) +{ + union acpi_object *obj = NULL; + int min_elements; + + /* instance_id needs to be reset for each type GUID + * also, instance IDs are unique within GUID but not across + */ + int instance_id = 0; + int cur_instance_id = instance_id; + int ret = 0; + + ret = hp_alloc_attributes_data(attr_type); + if (ret) + return ret; + + switch (attr_type) { + case HPWMI_STRING_TYPE: + min_elements = STR_ELEM_CNT; + break; + case HPWMI_INTEGER_TYPE: + min_elements = INT_ELEM_CNT; + break; + case HPWMI_ENUMERATION_TYPE: + min_elements = ENUM_ELEM_CNT; + break; + case HPWMI_ORDERED_LIST_TYPE: + min_elements = ORD_ELEM_CNT; + break; + case HPWMI_PASSWORD_TYPE: + min_elements = PSWD_ELEM_CNT; + break; + default: + pr_err("Error: Unknown attr_type: %d\n", attr_type); + return -EINVAL; + } + + /* need to use specific instance_id and guid combination to get right data */ + obj = hp_get_wmiobj_pointer(instance_id, guid); + if (!obj) + return -ENODEV; + + mutex_lock(&bioscfg_drv.mutex); + while (obj) { + /* Take action appropriate to each ACPI TYPE */ + if (obj->type == ACPI_TYPE_PACKAGE) { + ret = hp_init_bios_package_attribute(attr_type, obj, + guid, min_elements, + cur_instance_id); + + } else if (obj->type == ACPI_TYPE_BUFFER) { + ret = hp_init_bios_buffer_attribute(attr_type, obj, + guid, min_elements, + cur_instance_id); + + } else { + pr_err("Expected ACPI-package or buffer type, got: %d\n", + obj->type); + ret = -EIO; + goto err_attr_init; + } + + /* + * Failure reported in one attribute must not + * stop process of the remaining attribute values. + */ + if (ret >= 0) + cur_instance_id++; + + kfree(obj); + instance_id++; + obj = hp_get_wmiobj_pointer(instance_id, guid); + } + +err_attr_init: + mutex_unlock(&bioscfg_drv.mutex); + kfree(obj); + return ret; +} + +static int __init hp_init(void) +{ + int ret; + int hp_bios_capable = wmi_has_guid(HP_WMI_BIOS_GUID); + int set_bios_settings = wmi_has_guid(HP_WMI_SET_BIOS_SETTING_GUID); + + if (!hp_bios_capable) { + pr_err("Unable to run on non-HP system\n"); + return -ENODEV; + } + + if (!set_bios_settings) { + pr_err("Unable to set BIOS settings on HP systems\n"); + return -ENODEV; + } + + ret = hp_init_attr_set_interface(); + if (ret) + return ret; + + bioscfg_drv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(bioscfg_drv.class_dev)) { + ret = PTR_ERR(bioscfg_drv.class_dev); + goto err_unregister_class; + } + + bioscfg_drv.main_dir_kset = kset_create_and_add("attributes", NULL, + &bioscfg_drv.class_dev->kobj); + if (!bioscfg_drv.main_dir_kset) { + ret = -ENOMEM; + pr_debug("Failed to create and add attributes\n"); + goto err_destroy_classdev; + } + + bioscfg_drv.authentication_dir_kset = kset_create_and_add("authentication", NULL, + &bioscfg_drv.class_dev->kobj); + if (!bioscfg_drv.authentication_dir_kset) { + ret = -ENOMEM; + pr_debug("Failed to create and add authentication\n"); + goto err_release_attributes_data; + } + + /* + * sysfs level attributes. + * - pending_reboot + */ + ret = create_attributes_level_sysfs_files(); + if (ret) + pr_debug("Failed to create sysfs level attributes\n"); + + ret = hp_init_bios_attributes(HPWMI_STRING_TYPE, HP_WMI_BIOS_STRING_GUID); + if (ret) + pr_debug("Failed to populate string type attributes\n"); + + ret = hp_init_bios_attributes(HPWMI_INTEGER_TYPE, HP_WMI_BIOS_INTEGER_GUID); + if (ret) + pr_debug("Failed to populate integer type attributes\n"); + + ret = hp_init_bios_attributes(HPWMI_ENUMERATION_TYPE, HP_WMI_BIOS_ENUMERATION_GUID); + if (ret) + pr_debug("Failed to populate enumeration type attributes\n"); + + ret = hp_init_bios_attributes(HPWMI_ORDERED_LIST_TYPE, HP_WMI_BIOS_ORDERED_LIST_GUID); + if (ret) + pr_debug("Failed to populate ordered list object type attributes\n"); + + ret = hp_init_bios_attributes(HPWMI_PASSWORD_TYPE, HP_WMI_BIOS_PASSWORD_GUID); + if (ret) + pr_debug("Failed to populate password object type attributes\n"); + + bioscfg_drv.spm_data.attr_name_kobj = NULL; + ret = hp_add_other_attributes(HPWMI_SECURE_PLATFORM_TYPE); + if (ret) + pr_debug("Failed to populate secure platform object type attribute\n"); + + bioscfg_drv.sure_start_attr_kobj = NULL; + ret = hp_add_other_attributes(HPWMI_SURE_START_TYPE); + if (ret) + pr_debug("Failed to populate sure start object type attribute\n"); + + return 0; + +err_release_attributes_data: + release_attributes_data(); + +err_destroy_classdev: + device_unregister(bioscfg_drv.class_dev); + +err_unregister_class: + hp_exit_attr_set_interface(); + + return ret; +} + +static void __exit hp_exit(void) +{ + release_attributes_data(); + device_unregister(bioscfg_drv.class_dev); + + hp_exit_attr_set_interface(); +} + +module_init(hp_init); +module_exit(hp_exit); diff --git a/drivers/platform/x86/hp/hp-bioscfg/bioscfg.h b/drivers/platform/x86/hp/hp-bioscfg/bioscfg.h new file mode 100644 index 000000000..f1eec0e4b --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/bioscfg.h @@ -0,0 +1,489 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * Definitions for kernel modules using hp_bioscfg driver + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#ifndef _HP_BIOSCFG_H_ +#define _HP_BIOSCFG_H_ + +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "hp-bioscfg" + +#define MAX_BUFF_SIZE 512 +#define MAX_KEY_MOD_SIZE 256 +#define MAX_PASSWD_SIZE 64 +#define MAX_PREREQUISITES_SIZE 20 +#define MAX_REQ_ELEM_SIZE 128 +#define MAX_VALUES_SIZE 16 +#define MAX_ENCODINGS_SIZE 16 +#define MAX_ELEMENTS_SIZE 16 + +#define SPM_STR_DESC "Secure Platform Management" +#define SPM_STR "SPM" +#define SURE_START_DESC "Sure Start" +#define SURE_START_STR "Sure_Start" +#define SETUP_PASSWD "Setup Password" +#define POWER_ON_PASSWD "Power-On Password" + +#define LANG_CODE_STR "en_US.UTF-8" +#define SCHEDULE_POWER_ON "Scheduled Power-On" + +#define COMMA_SEP "," +#define SEMICOLON_SEP ";" + +/* Sure Admin Functions */ + +#define UTF_PREFIX "" +#define BEAM_PREFIX "" + +enum mechanism_values { + PASSWORD = 0x00, + SIGNING_KEY = 0x01, + ENDORSEMENT_KEY = 0x02, +}; + +#define BIOS_ADMIN "bios-admin" +#define POWER_ON "power-on" +#define BIOS_SPM "enhanced-bios-auth" + +#define PASSWD_MECHANISM_TYPES "password" + +#define HP_WMI_BIOS_GUID "5FB7F034-2C63-45E9-BE91-3D44E2C707E4" + +#define HP_WMI_BIOS_STRING_GUID "988D08E3-68F4-4C35-AF3E-6A1B8106F83C" +#define HP_WMI_BIOS_INTEGER_GUID "8232DE3D-663D-4327-A8F4-E293ADB9BF05" +#define HP_WMI_BIOS_ENUMERATION_GUID "2D114B49-2DFB-4130-B8FE-4A3C09E75133" +#define HP_WMI_BIOS_ORDERED_LIST_GUID "14EA9746-CE1F-4098-A0E0-7045CB4DA745" +#define HP_WMI_BIOS_PASSWORD_GUID "322F2028-0F84-4901-988E-015176049E2D" +#define HP_WMI_SET_BIOS_SETTING_GUID "1F4C91EB-DC5C-460B-951D-C7CB9B4B8D5E" + +enum hp_wmi_spm_commandtype { + HPWMI_SECUREPLATFORM_GET_STATE = 0x10, + HPWMI_SECUREPLATFORM_SET_KEK = 0x11, + HPWMI_SECUREPLATFORM_SET_SK = 0x12, +}; + +enum hp_wmi_surestart_commandtype { + HPWMI_SURESTART_GET_LOG_COUNT = 0x01, + HPWMI_SURESTART_GET_LOG = 0x02, +}; + +enum hp_wmi_command { + HPWMI_READ = 0x01, + HPWMI_WRITE = 0x02, + HPWMI_ODM = 0x03, + HPWMI_SURESTART = 0x20006, + HPWMI_GM = 0x20008, + HPWMI_SECUREPLATFORM = 0x20010, +}; + +struct bios_return { + u32 sigpass; + u32 return_code; +}; + +enum wmi_error_values { + SUCCESS = 0x00, + CMD_FAILED = 0x01, + INVALID_SIGN = 0x02, + INVALID_CMD_VALUE = 0x03, + INVALID_CMD_TYPE = 0x04, + INVALID_DATA_SIZE = 0x05, + INVALID_CMD_PARAM = 0x06, + ENCRYP_CMD_REQUIRED = 0x07, + NO_SECURE_SESSION = 0x08, + SECURE_SESSION_FOUND = 0x09, + SECURE_SESSION_FAILED = 0x0A, + AUTH_FAILED = 0x0B, + INVALID_BIOS_AUTH = 0x0E, + NONCE_DID_NOT_MATCH = 0x18, + GENERIC_ERROR = 0x1C, + BIOS_ADMIN_POLICY_NOT_MET = 0x28, + BIOS_ADMIN_NOT_SET = 0x38, + P21_NO_PROVISIONED = 0x1000, + P21_PROVISION_IN_PROGRESS = 0x1001, + P21_IN_USE = 0x1002, + HEP_NOT_ACTIVE = 0x1004, + HEP_ALREADY_SET = 0x1006, + HEP_CHECK_STATE = 0x1007, +}; + +struct common_data { + u8 display_name[MAX_BUFF_SIZE]; + u8 path[MAX_BUFF_SIZE]; + u32 is_readonly; + u32 display_in_ui; + u32 requires_physical_presence; + u32 sequence; + u32 prerequisites_size; + u8 prerequisites[MAX_PREREQUISITES_SIZE][MAX_BUFF_SIZE]; + u32 security_level; +}; + +struct string_data { + struct common_data common; + struct kobject *attr_name_kobj; + u8 current_value[MAX_BUFF_SIZE]; + u8 new_value[MAX_BUFF_SIZE]; + u32 min_length; + u32 max_length; +}; + +struct integer_data { + struct common_data common; + struct kobject *attr_name_kobj; + u32 current_value; + u32 new_value; + u32 lower_bound; + u32 upper_bound; + u32 scalar_increment; +}; + +struct enumeration_data { + struct common_data common; + struct kobject *attr_name_kobj; + u8 current_value[MAX_BUFF_SIZE]; + u8 new_value[MAX_BUFF_SIZE]; + u32 possible_values_size; + u8 possible_values[MAX_VALUES_SIZE][MAX_BUFF_SIZE]; +}; + +struct ordered_list_data { + struct common_data common; + struct kobject *attr_name_kobj; + u8 current_value[MAX_BUFF_SIZE]; + u8 new_value[MAX_BUFF_SIZE]; + u32 elements_size; + u8 elements[MAX_ELEMENTS_SIZE][MAX_BUFF_SIZE]; +}; + +struct password_data { + struct common_data common; + struct kobject *attr_name_kobj; + u8 current_password[MAX_PASSWD_SIZE]; + u8 new_password[MAX_PASSWD_SIZE]; + u32 min_password_length; + u32 max_password_length; + u32 encodings_size; + u8 encodings[MAX_ENCODINGS_SIZE][MAX_BUFF_SIZE]; + bool is_enabled; + + /* + * 'role' identifies the type of authentication. + * Two known types are bios-admin and power-on. + * 'bios-admin' represents BIOS administrator password + * 'power-on' represents a password required to use the system + */ + u32 role; + + /* + * 'mechanism' represents the means of authentication. + * Only supported type currently is "password" + */ + u32 mechanism; +}; + +struct secure_platform_data { + struct kobject *attr_name_kobj; + u8 attribute_name[MAX_BUFF_SIZE]; + u8 *endorsement_key; + u8 *signing_key; + u8 *auth_token; + bool is_enabled; + u32 mechanism; +}; + +struct bioscfg_priv { + struct kset *authentication_dir_kset; + struct kset *main_dir_kset; + struct device *class_dev; + struct string_data *string_data; + u32 string_instances_count; + struct integer_data *integer_data; + u32 integer_instances_count; + struct enumeration_data *enumeration_data; + u32 enumeration_instances_count; + struct ordered_list_data *ordered_list_data; + u32 ordered_list_instances_count; + struct password_data *password_data; + u32 password_instances_count; + + struct kobject *sure_start_attr_kobj; + struct secure_platform_data spm_data; + u8 display_name_language_code[MAX_BUFF_SIZE]; + bool pending_reboot; + struct mutex mutex; +}; + +/* global structure used by multiple WMI interfaces */ +extern struct bioscfg_priv bioscfg_drv; + +enum hp_wmi_data_type { + HPWMI_STRING_TYPE, + HPWMI_INTEGER_TYPE, + HPWMI_ENUMERATION_TYPE, + HPWMI_ORDERED_LIST_TYPE, + HPWMI_PASSWORD_TYPE, + HPWMI_SECURE_PLATFORM_TYPE, + HPWMI_SURE_START_TYPE, +}; + +enum hp_wmi_data_elements { + /* Common elements */ + NAME = 0, + VALUE = 1, + PATH = 2, + IS_READONLY = 3, + DISPLAY_IN_UI = 4, + REQUIRES_PHYSICAL_PRESENCE = 5, + SEQUENCE = 6, + PREREQUISITES_SIZE = 7, + PREREQUISITES = 8, + SECURITY_LEVEL = 9, + + /* String elements */ + STR_MIN_LENGTH = 10, + STR_MAX_LENGTH = 11, + STR_ELEM_CNT = 12, + + /* Integer elements */ + INT_LOWER_BOUND = 10, + INT_UPPER_BOUND = 11, + INT_SCALAR_INCREMENT = 12, + INT_ELEM_CNT = 13, + + /* Enumeration elements */ + ENUM_CURRENT_VALUE = 10, + ENUM_SIZE = 11, + ENUM_POSSIBLE_VALUES = 12, + ENUM_ELEM_CNT = 13, + + /* Ordered list elements */ + ORD_LIST_SIZE = 10, + ORD_LIST_ELEMENTS = 11, + ORD_ELEM_CNT = 12, + + /* Password elements */ + PSWD_MIN_LENGTH = 10, + PSWD_MAX_LENGTH = 11, + PSWD_SIZE = 12, + PSWD_ENCODINGS = 13, + PSWD_IS_SET = 14, + PSWD_ELEM_CNT = 15, +}; + +#define GET_INSTANCE_ID(type) \ + static int get_##type##_instance_id(struct kobject *kobj) \ + { \ + int i; \ + \ + for (i = 0; i < bioscfg_drv.type##_instances_count; i++) { \ + if (bioscfg_drv.type##_data[i].attr_name_kobj && \ + !strcmp(kobj->name, bioscfg_drv.type##_data[i].attr_name_kobj->name)) \ + return i; \ + } \ + return -EIO; \ + } + +#define ATTRIBUTE_S_PROPERTY_SHOW(name, type) \ + static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \ + char *buf) \ + { \ + int i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + return sysfs_emit(buf, "%s\n", bioscfg_drv.type##_data[i].name); \ + return -EIO; \ + } + +#define ATTRIBUTE_N_PROPERTY_SHOW(name, type) \ + static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \ + char *buf) \ + { \ + int i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + return sysfs_emit(buf, "%d\n", bioscfg_drv.type##_data[i].name); \ + return -EIO; \ + } + +#define ATTRIBUTE_PROPERTY_STORE(curr_val, type) \ + static ssize_t curr_val##_store(struct kobject *kobj, \ + struct kobj_attribute *attr, \ + const char *buf, size_t count) \ + { \ + char *attr_value = NULL; \ + int i; \ + int ret = -EIO; \ + \ + attr_value = kstrdup(buf, GFP_KERNEL); \ + if (!attr_value) \ + return -ENOMEM; \ + \ + ret = hp_enforce_single_line_input(attr_value, count); \ + if (!ret) { \ + i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + ret = validate_##type##_input(i, attr_value); \ + } \ + if (!ret) \ + ret = hp_set_attribute(kobj->name, attr_value); \ + if (!ret) { \ + update_##type##_value(i, attr_value); \ + if (bioscfg_drv.type##_data[i].common.requires_physical_presence) \ + hp_set_reboot_and_signal_event(); \ + } \ + hp_clear_all_credentials(); \ + kfree(attr_value); \ + \ + return ret ? ret : count; \ + } + +#define ATTRIBUTE_SPM_N_PROPERTY_SHOW(name, type) \ + static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + return sysfs_emit(buf, "%d\n", bioscfg_drv.type##_data.name); \ + } + +#define ATTRIBUTE_SPM_S_PROPERTY_SHOW(name, type) \ + static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) \ + { \ + return sysfs_emit(buf, "%s\n", bioscfg_drv.type##_data.name); \ + } + +#define ATTRIBUTE_VALUES_PROPERTY_SHOW(name, type, sep) \ + static ssize_t name##_show(struct kobject *kobj, \ + struct kobj_attribute *attr, char *buf) \ + { \ + int i; \ + int len = 0; \ + int instance_id = get_##type##_instance_id(kobj); \ + \ + if (instance_id < 0) \ + return 0; \ + \ + for (i = 0; i < bioscfg_drv.type##_data[instance_id].name##_size; i++) { \ + if (i) \ + len += sysfs_emit_at(buf, len, "%s", sep); \ + \ + len += sysfs_emit_at(buf, len, "%s", \ + bioscfg_drv.type##_data[instance_id].name[i]); \ + } \ + len += sysfs_emit_at(buf, len, "\n"); \ + return len; \ + } + +#define ATTRIBUTE_S_COMMON_PROPERTY_SHOW(name, type) \ + static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \ + char *buf) \ + { \ + int i = get_##type##_instance_id(kobj); \ + if (i >= 0) \ + return sysfs_emit(buf, "%s\n", bioscfg_drv.type##_data[i].common.name); \ + return -EIO; \ + } + +extern struct kobj_attribute common_display_langcode; + +/* Prototypes */ + +/* String attributes */ +int hp_populate_string_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj); +int hp_alloc_string_data(void); +void hp_exit_string_attributes(void); +int hp_populate_string_package_data(union acpi_object *str_obj, + int instance_id, + struct kobject *attr_name_kobj); + +/* Integer attributes */ +int hp_populate_integer_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj); +int hp_alloc_integer_data(void); +void hp_exit_integer_attributes(void); +int hp_populate_integer_package_data(union acpi_object *integer_obj, + int instance_id, + struct kobject *attr_name_kobj); + +/* Enumeration attributes */ +int hp_populate_enumeration_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj); +int hp_alloc_enumeration_data(void); +void hp_exit_enumeration_attributes(void); +int hp_populate_enumeration_package_data(union acpi_object *enum_obj, + int instance_id, + struct kobject *attr_name_kobj); + +/* Ordered list */ +int hp_populate_ordered_list_buffer_data(u8 *buffer_ptr, + u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj); +int hp_alloc_ordered_list_data(void); +void hp_exit_ordered_list_attributes(void); +int hp_populate_ordered_list_package_data(union acpi_object *order_obj, + int instance_id, + struct kobject *attr_name_kobj); + +/* Password authentication attributes */ +int hp_populate_password_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj); +int hp_populate_password_package_data(union acpi_object *password_obj, + int instance_id, + struct kobject *attr_name_kobj); +int hp_alloc_password_data(void); +int hp_get_password_instance_for_type(const char *name); +int hp_clear_all_credentials(void); +int hp_set_attribute(const char *a_name, const char *a_value); + +/* SPM attributes */ +void hp_exit_password_attributes(void); +void hp_exit_secure_platform_attributes(void); +int hp_populate_secure_platform_data(struct kobject *attr_name_kobj); +int hp_populate_security_buffer(u16 *buffer, const char *authentication); + +/* Bios Attributes interface */ +int hp_wmi_set_bios_setting(u16 *input_buffer, u32 input_size); +int hp_wmi_perform_query(int query, enum hp_wmi_command command, + void *buffer, u32 insize, u32 outsize); + +/* Sure Start attributes */ +void hp_exit_sure_start_attributes(void); +int hp_populate_sure_start_data(struct kobject *attr_name_kobj); + +/* Bioscfg */ + +void hp_exit_attr_set_interface(void); +int hp_init_attr_set_interface(void); +size_t hp_calculate_string_buffer(const char *str); +size_t hp_calculate_security_buffer(const char *authentication); +void *hp_ascii_to_utf16_unicode(u16 *p, const u8 *str); +int hp_get_integer_from_buffer(u8 **buffer, u32 *buffer_size, u32 *integer); +int hp_get_string_from_buffer(u8 **buffer, u32 *buffer_size, char *dst, u32 dst_size); +int hp_convert_hexstr_to_str(const char *input, u32 input_len, char **str, int *len); +int hp_encode_outsize_for_pvsz(int outsize); +int hp_enforce_single_line_input(char *buf, size_t count); +void hp_set_reboot_and_signal_event(void); +ssize_t display_name_language_code_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf); +union acpi_object *hp_get_wmiobj_pointer(int instance_id, const char *guid_string); +int hp_get_instance_count(const char *guid_string); +void hp_update_attribute_permissions(bool isreadonly, struct kobj_attribute *current_val); +void hp_friendly_user_name_update(char *path, const char *attr_name, + char *attr_display, int attr_size); +int hp_wmi_error_and_message(int error_code); +int hp_get_common_data_from_buffer(u8 **buffer_ptr, u32 *buffer_size, struct common_data *common); + +#endif diff --git a/drivers/platform/x86/hp/hp-bioscfg/enum-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/enum-attributes.c new file mode 100644 index 000000000..af4d1920d --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/enum-attributes.c @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to enumeration type attributes under + * BIOS Enumeration GUID for use with hp-bioscfg driver. + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" + +GET_INSTANCE_ID(enumeration); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_enumeration_instance_id(kobj); + + if (instance_id < 0) + return -EIO; + + return sysfs_emit(buf, "%s\n", + bioscfg_drv.enumeration_data[instance_id].current_value); +} + +/** + * validate_enumeration_input() - + * Validate input of current_value against possible values + * + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_enumeration_input(int instance_id, const char *buf) +{ + int i; + int found = 0; + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + + /* Is it a read only attribute */ + if (enum_data->common.is_readonly) + return -EIO; + + for (i = 0; i < enum_data->possible_values_size && !found; i++) + if (!strcmp(enum_data->possible_values[i], buf)) + found = 1; + + if (!found) + return -EINVAL; + + return 0; +} + +static void update_enumeration_value(int instance_id, char *attr_value) +{ + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + + strscpy(enum_data->current_value, attr_value); +} + +ATTRIBUTE_S_COMMON_PROPERTY_SHOW(display_name, enumeration); +static struct kobj_attribute enumeration_display_name = + __ATTR_RO(display_name); + +ATTRIBUTE_PROPERTY_STORE(current_value, enumeration); +static struct kobj_attribute enumeration_current_val = + __ATTR_RW(current_value); + +ATTRIBUTE_VALUES_PROPERTY_SHOW(possible_values, enumeration, SEMICOLON_SEP); +static struct kobj_attribute enumeration_poss_val = + __ATTR_RO(possible_values); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "enumeration\n"); +} + +static struct kobj_attribute enumeration_type = + __ATTR_RO(type); + +static struct attribute *enumeration_attrs[] = { + &common_display_langcode.attr, + &enumeration_display_name.attr, + &enumeration_current_val.attr, + &enumeration_poss_val.attr, + &enumeration_type.attr, + NULL +}; + +static const struct attribute_group enumeration_attr_group = { + .attrs = enumeration_attrs, +}; + +int hp_alloc_enumeration_data(void) +{ + bioscfg_drv.enumeration_instances_count = + hp_get_instance_count(HP_WMI_BIOS_ENUMERATION_GUID); + + if (!bioscfg_drv.enumeration_instances_count) + return -EINVAL; + bioscfg_drv.enumeration_data = kvcalloc(bioscfg_drv.enumeration_instances_count, + sizeof(*bioscfg_drv.enumeration_data), GFP_KERNEL); + + if (!bioscfg_drv.enumeration_data) { + bioscfg_drv.enumeration_instances_count = 0; + return -ENOMEM; + } + return 0; +} + +/* Expected Values types associated with each element */ +static const acpi_object_type expected_enum_types[] = { + [NAME] = ACPI_TYPE_STRING, + [VALUE] = ACPI_TYPE_STRING, + [PATH] = ACPI_TYPE_STRING, + [IS_READONLY] = ACPI_TYPE_INTEGER, + [DISPLAY_IN_UI] = ACPI_TYPE_INTEGER, + [REQUIRES_PHYSICAL_PRESENCE] = ACPI_TYPE_INTEGER, + [SEQUENCE] = ACPI_TYPE_INTEGER, + [PREREQUISITES_SIZE] = ACPI_TYPE_INTEGER, + [PREREQUISITES] = ACPI_TYPE_STRING, + [SECURITY_LEVEL] = ACPI_TYPE_INTEGER, + [ENUM_CURRENT_VALUE] = ACPI_TYPE_STRING, + [ENUM_SIZE] = ACPI_TYPE_INTEGER, + [ENUM_POSSIBLE_VALUES] = ACPI_TYPE_STRING, +}; + +static int hp_populate_enumeration_elements_from_package(union acpi_object *enum_obj, + int enum_obj_count, + int instance_id) +{ + char *str_value = NULL; + int value_len; + u32 size = 0; + u32 int_value = 0; + int elem = 0; + int reqs; + int pos_values; + int ret; + int eloc; + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + + for (elem = 1, eloc = 1; elem < enum_obj_count; elem++, eloc++) { + /* ONLY look at the first ENUM_ELEM_CNT elements */ + if (eloc == ENUM_ELEM_CNT) + goto exit_enumeration_package; + + switch (enum_obj[elem].type) { + case ACPI_TYPE_STRING: + if (PREREQUISITES != elem && ENUM_POSSIBLE_VALUES != elem) { + ret = hp_convert_hexstr_to_str(enum_obj[elem].string.pointer, + enum_obj[elem].string.length, + &str_value, &value_len); + if (ret) + return -EINVAL; + } + break; + case ACPI_TYPE_INTEGER: + int_value = (u32)enum_obj[elem].integer.value; + break; + default: + pr_warn("Unsupported object type [%d]\n", enum_obj[elem].type); + continue; + } + + /* Check that both expected and read object type match */ + if (expected_enum_types[eloc] != enum_obj[elem].type) { + pr_err("Error expected type %d for elem %d, but got type %d instead\n", + expected_enum_types[eloc], elem, enum_obj[elem].type); + kfree(str_value); + return -EIO; + } + + /* Assign appropriate element value to corresponding field */ + switch (eloc) { + case NAME: + case VALUE: + break; + case PATH: + strscpy(enum_data->common.path, str_value); + break; + case IS_READONLY: + enum_data->common.is_readonly = int_value; + break; + case DISPLAY_IN_UI: + enum_data->common.display_in_ui = int_value; + break; + case REQUIRES_PHYSICAL_PRESENCE: + enum_data->common.requires_physical_presence = int_value; + break; + case SEQUENCE: + enum_data->common.sequence = int_value; + break; + case PREREQUISITES_SIZE: + if (int_value > MAX_PREREQUISITES_SIZE) { + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_PREREQUISITES_SIZE; + } + enum_data->common.prerequisites_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PREREQUISITES + * object is omitted by BIOS when the size is + * zero. + */ + if (int_value == 0) + eloc++; + break; + + case PREREQUISITES: + size = min_t(u32, enum_data->common.prerequisites_size, MAX_PREREQUISITES_SIZE); + for (reqs = 0; reqs < size; reqs++) { + if (elem + reqs >= enum_obj_count) { + pr_err("Error enum-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(enum_obj[elem + reqs].string.pointer, + enum_obj[elem + reqs].string.length, + &str_value, &value_len); + + if (ret) + return -EINVAL; + + strscpy(enum_data->common.prerequisites[reqs], str_value); + + kfree(str_value); + str_value = NULL; + } + break; + + case SECURITY_LEVEL: + enum_data->common.security_level = int_value; + break; + + case ENUM_CURRENT_VALUE: + strscpy(enum_data->current_value, str_value); + break; + case ENUM_SIZE: + if (int_value > MAX_VALUES_SIZE) { + pr_warn("Possible number values size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_VALUES_SIZE; + } + enum_data->possible_values_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. POSSIBLE_VALUES + * object is omitted by BIOS when the size is zero. + */ + if (int_value == 0) + eloc++; + break; + + case ENUM_POSSIBLE_VALUES: + size = enum_data->possible_values_size; + + for (pos_values = 0; pos_values < size && pos_values < MAX_VALUES_SIZE; + pos_values++) { + if (elem + pos_values >= enum_obj_count) { + pr_err("Error enum-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(enum_obj[elem + pos_values].string.pointer, + enum_obj[elem + pos_values].string.length, + &str_value, &value_len); + + if (ret) + return -EINVAL; + + /* + * ignore strings when possible values size + * is greater than MAX_VALUES_SIZE + */ + if (size < MAX_VALUES_SIZE) + strscpy(enum_data->possible_values[pos_values], str_value); + + kfree(str_value); + str_value = NULL; + } + break; + default: + pr_warn("Invalid element: %d found in Enumeration attribute or data may be malformed\n", elem); + break; + } + + kfree(str_value); + str_value = NULL; + } + +exit_enumeration_package: + kfree(str_value); + return 0; +} + +/** + * hp_populate_enumeration_package_data() - + * Populate all properties of an instance under enumeration attribute + * + * @enum_obj: ACPI object with enumeration data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_enumeration_package_data(union acpi_object *enum_obj, + int instance_id, + struct kobject *attr_name_kobj) +{ + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + + enum_data->attr_name_kobj = attr_name_kobj; + + hp_populate_enumeration_elements_from_package(enum_obj, + enum_obj->package.count, + instance_id); + hp_update_attribute_permissions(enum_data->common.is_readonly, + &enumeration_current_val); + /* + * Several attributes have names such "MONDAY". Friendly + * user nane is generated to make the name more descriptive + */ + hp_friendly_user_name_update(enum_data->common.path, + attr_name_kobj->name, + enum_data->common.display_name, + sizeof(enum_data->common.display_name)); + return sysfs_create_group(attr_name_kobj, &enumeration_attr_group); +} + +static int hp_populate_enumeration_elements_from_buffer(u8 *buffer_ptr, u32 *buffer_size, + int instance_id) +{ + int values; + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + int ret = 0; + + /* + * Only data relevant to this driver and its functionality is + * read. BIOS defines the order in which each * element is + * read. Element 0 data is not relevant to this + * driver hence it is ignored. For clarity, all element names + * (DISPLAY_IN_UI) which defines the order in which is read + * and the name matches the variable where the data is stored. + * + * In earlier implementation, reported errors were ignored + * causing the data to remain uninitialized. It is not + * possible to determine if data read from BIOS is valid or + * not. It is for this reason functions may return a error + * without validating the data itself. + */ + + // VALUE: + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, enum_data->current_value, + sizeof(enum_data->current_value)); + if (ret < 0) + goto buffer_exit; + + // COMMON: + ret = hp_get_common_data_from_buffer(&buffer_ptr, buffer_size, &enum_data->common); + if (ret < 0) + goto buffer_exit; + + // ENUM_CURRENT_VALUE: + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, + enum_data->current_value, + sizeof(enum_data->current_value)); + if (ret < 0) + goto buffer_exit; + + // ENUM_SIZE: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &enum_data->possible_values_size); + + if (enum_data->possible_values_size > MAX_VALUES_SIZE) { + /* Report a message and limit possible values size to maximum value */ + pr_warn("Enum Possible size value exceeded the maximum number of elements supported or data may be malformed\n"); + enum_data->possible_values_size = MAX_VALUES_SIZE; + } + + // ENUM_POSSIBLE_VALUES: + for (values = 0; values < enum_data->possible_values_size; values++) { + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, + enum_data->possible_values[values], + sizeof(enum_data->possible_values[values])); + if (ret < 0) + break; + } + +buffer_exit: + return ret; +} + +/** + * hp_populate_enumeration_buffer_data() - + * Populate all properties of an instance under enumeration attribute + * + * @buffer_ptr: Buffer pointer + * @buffer_size: Buffer size + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_enumeration_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj) +{ + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + int ret = 0; + + enum_data->attr_name_kobj = attr_name_kobj; + + /* Populate enumeration elements */ + ret = hp_populate_enumeration_elements_from_buffer(buffer_ptr, buffer_size, + instance_id); + if (ret < 0) + return ret; + + hp_update_attribute_permissions(enum_data->common.is_readonly, + &enumeration_current_val); + /* + * Several attributes have names such "MONDAY". A Friendlier + * user nane is generated to make the name more descriptive + */ + hp_friendly_user_name_update(enum_data->common.path, + attr_name_kobj->name, + enum_data->common.display_name, + sizeof(enum_data->common.display_name)); + + return sysfs_create_group(attr_name_kobj, &enumeration_attr_group); +} + +/** + * hp_exit_enumeration_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void hp_exit_enumeration_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < bioscfg_drv.enumeration_instances_count; + instance_id++) { + struct enumeration_data *enum_data = &bioscfg_drv.enumeration_data[instance_id]; + struct kobject *attr_name_kobj = enum_data->attr_name_kobj; + + if (attr_name_kobj) + sysfs_remove_group(attr_name_kobj, &enumeration_attr_group); + } + bioscfg_drv.enumeration_instances_count = 0; + + kvfree(bioscfg_drv.enumeration_data); + bioscfg_drv.enumeration_data = NULL; +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/int-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/int-attributes.c new file mode 100644 index 000000000..d96e16095 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/int-attributes.c @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to integer type attributes under + * BIOS Enumeration GUID for use with hp-bioscfg driver. + * + * Copyright (c) 2022 Hewlett-Packard Inc. + */ + +#include "bioscfg.h" + +GET_INSTANCE_ID(integer); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_integer_instance_id(kobj); + + if (instance_id < 0) + return -EIO; + + return sysfs_emit(buf, "%d\n", + bioscfg_drv.integer_data[instance_id].current_value); +} + +/** + * validate_integer_input() - + * Validate input of current_value against lower and upper bound + * + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_integer_input(int instance_id, char *buf) +{ + int in_val; + int ret; + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + + /* BIOS treats it as a read only attribute */ + if (integer_data->common.is_readonly) + return -EIO; + + ret = kstrtoint(buf, 10, &in_val); + if (ret < 0) + return ret; + + if (in_val < integer_data->lower_bound || + in_val > integer_data->upper_bound) + return -ERANGE; + + return 0; +} + +static void update_integer_value(int instance_id, char *attr_value) +{ + int in_val; + int ret; + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + + ret = kstrtoint(attr_value, 10, &in_val); + if (ret == 0) + integer_data->current_value = in_val; + else + pr_warn("Invalid integer value found: %s\n", attr_value); +} + +ATTRIBUTE_S_COMMON_PROPERTY_SHOW(display_name, integer); +static struct kobj_attribute integer_display_name = + __ATTR_RO(display_name); + +ATTRIBUTE_PROPERTY_STORE(current_value, integer); +static struct kobj_attribute integer_current_val = + __ATTR_RW_MODE(current_value, 0644); + +ATTRIBUTE_N_PROPERTY_SHOW(lower_bound, integer); +static struct kobj_attribute integer_lower_bound = + __ATTR_RO(lower_bound); + +ATTRIBUTE_N_PROPERTY_SHOW(upper_bound, integer); +static struct kobj_attribute integer_upper_bound = + __ATTR_RO(upper_bound); + +ATTRIBUTE_N_PROPERTY_SHOW(scalar_increment, integer); +static struct kobj_attribute integer_scalar_increment = + __ATTR_RO(scalar_increment); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +static struct kobj_attribute integer_type = + __ATTR_RO(type); + +static struct attribute *integer_attrs[] = { + &common_display_langcode.attr, + &integer_display_name.attr, + &integer_current_val.attr, + &integer_lower_bound.attr, + &integer_upper_bound.attr, + &integer_scalar_increment.attr, + &integer_type.attr, + NULL +}; + +static const struct attribute_group integer_attr_group = { + .attrs = integer_attrs, +}; + +int hp_alloc_integer_data(void) +{ + bioscfg_drv.integer_instances_count = hp_get_instance_count(HP_WMI_BIOS_INTEGER_GUID); + bioscfg_drv.integer_data = kzalloc_objs(*bioscfg_drv.integer_data, + bioscfg_drv.integer_instances_count); + + if (!bioscfg_drv.integer_data) { + bioscfg_drv.integer_instances_count = 0; + return -ENOMEM; + } + return 0; +} + +/* Expected Values types associated with each element */ +static const acpi_object_type expected_integer_types[] = { + [NAME] = ACPI_TYPE_STRING, + [VALUE] = ACPI_TYPE_STRING, + [PATH] = ACPI_TYPE_STRING, + [IS_READONLY] = ACPI_TYPE_INTEGER, + [DISPLAY_IN_UI] = ACPI_TYPE_INTEGER, + [REQUIRES_PHYSICAL_PRESENCE] = ACPI_TYPE_INTEGER, + [SEQUENCE] = ACPI_TYPE_INTEGER, + [PREREQUISITES_SIZE] = ACPI_TYPE_INTEGER, + [PREREQUISITES] = ACPI_TYPE_STRING, + [SECURITY_LEVEL] = ACPI_TYPE_INTEGER, + [INT_LOWER_BOUND] = ACPI_TYPE_INTEGER, + [INT_UPPER_BOUND] = ACPI_TYPE_INTEGER, + [INT_SCALAR_INCREMENT] = ACPI_TYPE_INTEGER, +}; + +static int hp_populate_integer_elements_from_package(union acpi_object *integer_obj, + int integer_obj_count, + int instance_id) +{ + char *str_value = NULL; + int value_len; + int ret; + u32 int_value = 0; + int elem; + int reqs; + int eloc; + int size; + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + + if (!integer_obj) + return -EINVAL; + + for (elem = 1, eloc = 1; elem < integer_obj_count; elem++, eloc++) { + /* ONLY look at the first INTEGER_ELEM_CNT elements */ + if (eloc == INT_ELEM_CNT) + goto exit_integer_package; + + switch (integer_obj[elem].type) { + case ACPI_TYPE_STRING: + if (elem != PREREQUISITES) { + ret = hp_convert_hexstr_to_str(integer_obj[elem].string.pointer, + integer_obj[elem].string.length, + &str_value, &value_len); + if (ret) + continue; + } + break; + case ACPI_TYPE_INTEGER: + int_value = (u32)integer_obj[elem].integer.value; + break; + default: + pr_warn("Unsupported object type [%d]\n", integer_obj[elem].type); + continue; + } + /* Check that both expected and read object type match */ + if (expected_integer_types[eloc] != integer_obj[elem].type) { + pr_err("Error expected type %d for elem %d, but got type %d instead\n", + expected_integer_types[eloc], elem, integer_obj[elem].type); + kfree(str_value); + return -EIO; + } + /* Assign appropriate element value to corresponding field*/ + switch (eloc) { + case VALUE: + ret = kstrtoint(str_value, 10, &int_value); + if (ret) + continue; + + integer_data->current_value = int_value; + break; + case PATH: + strscpy(integer_data->common.path, str_value); + break; + case IS_READONLY: + integer_data->common.is_readonly = int_value; + break; + case DISPLAY_IN_UI: + integer_data->common.display_in_ui = int_value; + break; + case REQUIRES_PHYSICAL_PRESENCE: + integer_data->common.requires_physical_presence = int_value; + break; + case SEQUENCE: + integer_data->common.sequence = int_value; + break; + case PREREQUISITES_SIZE: + if (int_value > MAX_PREREQUISITES_SIZE) { + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_PREREQUISITES_SIZE; + } + integer_data->common.prerequisites_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PREREQUISITES + * object is omitted by BIOS when the size is + * zero. + */ + if (integer_data->common.prerequisites_size == 0) + eloc++; + break; + case PREREQUISITES: + size = min_t(u32, integer_data->common.prerequisites_size, MAX_PREREQUISITES_SIZE); + + for (reqs = 0; reqs < size; reqs++) { + if (elem + reqs >= integer_obj_count) { + pr_err("Error elem-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(integer_obj[elem + reqs].string.pointer, + integer_obj[elem + reqs].string.length, + &str_value, &value_len); + + if (ret) + continue; + + strscpy(integer_data->common.prerequisites[reqs], str_value); + kfree(str_value); + str_value = NULL; + } + break; + + case SECURITY_LEVEL: + integer_data->common.security_level = int_value; + break; + case INT_LOWER_BOUND: + integer_data->lower_bound = int_value; + break; + case INT_UPPER_BOUND: + integer_data->upper_bound = int_value; + break; + case INT_SCALAR_INCREMENT: + integer_data->scalar_increment = int_value; + break; + default: + pr_warn("Invalid element: %d found in Integer attribute or data may be malformed\n", elem); + break; + } + + kfree(str_value); + str_value = NULL; + } +exit_integer_package: + kfree(str_value); + return 0; +} + +/** + * hp_populate_integer_package_data() - + * Populate all properties of an instance under integer attribute + * + * @integer_obj: ACPI object with integer data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_integer_package_data(union acpi_object *integer_obj, + int instance_id, + struct kobject *attr_name_kobj) +{ + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + + integer_data->attr_name_kobj = attr_name_kobj; + hp_populate_integer_elements_from_package(integer_obj, + integer_obj->package.count, + instance_id); + hp_update_attribute_permissions(integer_data->common.is_readonly, + &integer_current_val); + hp_friendly_user_name_update(integer_data->common.path, + attr_name_kobj->name, + integer_data->common.display_name, + sizeof(integer_data->common.display_name)); + return sysfs_create_group(attr_name_kobj, &integer_attr_group); +} + +static int hp_populate_integer_elements_from_buffer(u8 *buffer_ptr, u32 *buffer_size, + int instance_id) +{ + char *dst = NULL; + int dst_size = *buffer_size / sizeof(u16); + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + int ret = 0; + + dst = kcalloc(dst_size, sizeof(char), GFP_KERNEL); + if (!dst) + return -ENOMEM; + + /* + * Only data relevant to this driver and its functionality is + * read. BIOS defines the order in which each * element is + * read. Element 0 data is not relevant to this + * driver hence it is ignored. For clarity, all element names + * (DISPLAY_IN_UI) which defines the order in which is read + * and the name matches the variable where the data is stored. + * + * In earlier implementation, reported errors were ignored + * causing the data to remain uninitialized. It is not + * possible to determine if data read from BIOS is valid or + * not. It is for this reason functions may return a error + * without validating the data itself. + */ + + // VALUE: + integer_data->current_value = 0; + + hp_get_string_from_buffer(&buffer_ptr, buffer_size, dst, dst_size); + ret = kstrtoint(dst, 10, &integer_data->current_value); + if (ret) + pr_warn("Unable to convert string to integer: %s\n", dst); + kfree(dst); + + // COMMON: + ret = hp_get_common_data_from_buffer(&buffer_ptr, buffer_size, &integer_data->common); + if (ret < 0) + goto buffer_exit; + + // INT_LOWER_BOUND: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &integer_data->lower_bound); + if (ret < 0) + goto buffer_exit; + + // INT_UPPER_BOUND: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &integer_data->upper_bound); + if (ret < 0) + goto buffer_exit; + + // INT_SCALAR_INCREMENT: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &integer_data->scalar_increment); + +buffer_exit: + return ret; +} + +/** + * hp_populate_integer_buffer_data() - + * Populate all properties of an instance under integer attribute + * + * @buffer_ptr: Buffer pointer + * @buffer_size: Buffer size + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_integer_buffer_data(u8 *buffer_ptr, u32 *buffer_size, int instance_id, + struct kobject *attr_name_kobj) +{ + struct integer_data *integer_data = &bioscfg_drv.integer_data[instance_id]; + int ret = 0; + + integer_data->attr_name_kobj = attr_name_kobj; + + /* Populate integer elements */ + ret = hp_populate_integer_elements_from_buffer(buffer_ptr, buffer_size, + instance_id); + if (ret < 0) + return ret; + + hp_update_attribute_permissions(integer_data->common.is_readonly, + &integer_current_val); + hp_friendly_user_name_update(integer_data->common.path, + attr_name_kobj->name, + integer_data->common.display_name, + sizeof(integer_data->common.display_name)); + + return sysfs_create_group(attr_name_kobj, &integer_attr_group); +} + +/** + * hp_exit_integer_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void hp_exit_integer_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < bioscfg_drv.integer_instances_count; + instance_id++) { + struct kobject *attr_name_kobj = + bioscfg_drv.integer_data[instance_id].attr_name_kobj; + + if (attr_name_kobj) + sysfs_remove_group(attr_name_kobj, &integer_attr_group); + } + bioscfg_drv.integer_instances_count = 0; + + kfree(bioscfg_drv.integer_data); + bioscfg_drv.integer_data = NULL; +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/order-list-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/order-list-attributes.c new file mode 100644 index 000000000..f09489a08 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/order-list-attributes.c @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to ordered list type attributes under + * BIOS ORDERED LIST GUID for use with hp-bioscfg driver. + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" + +GET_INSTANCE_ID(ordered_list); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_ordered_list_instance_id(kobj); + + if (instance_id < 0) + return -EIO; + + return sysfs_emit(buf, "%s\n", + bioscfg_drv.ordered_list_data[instance_id].current_value); +} + +static int replace_char_str(u8 *buffer, char *repl_char, char *repl_with) +{ + char *src = buffer; + int buflen = strlen(buffer); + int item; + + if (buflen < 1) + return -EINVAL; + + for (item = 0; item < buflen; item++) + if (src[item] == *repl_char) + src[item] = *repl_with; + + return 0; +} + +/** + * validate_ordered_list_input() - + * Validate input of current_value against possible values + * + * @instance: The instance on which input is validated + * @buf: Input value + */ +static int validate_ordered_list_input(int instance, char *buf) +{ + /* validation is done by BIOS. This validation function will + * convert semicolon to commas. BIOS uses commas as + * separators when reporting ordered-list values. + */ + return replace_char_str(buf, SEMICOLON_SEP, COMMA_SEP); +} + +static void update_ordered_list_value(int instance, char *attr_value) +{ + struct ordered_list_data *ordered_list_data = &bioscfg_drv.ordered_list_data[instance]; + + strscpy(ordered_list_data->current_value, attr_value); +} + +ATTRIBUTE_S_COMMON_PROPERTY_SHOW(display_name, ordered_list); +static struct kobj_attribute ordered_list_display_name = + __ATTR_RO(display_name); + +ATTRIBUTE_PROPERTY_STORE(current_value, ordered_list); +static struct kobj_attribute ordered_list_current_val = + __ATTR_RW_MODE(current_value, 0644); + +ATTRIBUTE_VALUES_PROPERTY_SHOW(elements, ordered_list, SEMICOLON_SEP); +static struct kobj_attribute ordered_list_elements_val = + __ATTR_RO(elements); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "ordered-list\n"); +} + +static struct kobj_attribute ordered_list_type = + __ATTR_RO(type); + +static struct attribute *ordered_list_attrs[] = { + &common_display_langcode.attr, + &ordered_list_display_name.attr, + &ordered_list_current_val.attr, + &ordered_list_elements_val.attr, + &ordered_list_type.attr, + NULL +}; + +static const struct attribute_group ordered_list_attr_group = { + .attrs = ordered_list_attrs, +}; + +int hp_alloc_ordered_list_data(void) +{ + bioscfg_drv.ordered_list_instances_count = + hp_get_instance_count(HP_WMI_BIOS_ORDERED_LIST_GUID); + bioscfg_drv.ordered_list_data = kzalloc_objs(*bioscfg_drv.ordered_list_data, + bioscfg_drv.ordered_list_instances_count); + if (!bioscfg_drv.ordered_list_data) { + bioscfg_drv.ordered_list_instances_count = 0; + return -ENOMEM; + } + return 0; +} + +/* Expected Values types associated with each element */ +static const acpi_object_type expected_order_types[] = { + [NAME] = ACPI_TYPE_STRING, + [VALUE] = ACPI_TYPE_STRING, + [PATH] = ACPI_TYPE_STRING, + [IS_READONLY] = ACPI_TYPE_INTEGER, + [DISPLAY_IN_UI] = ACPI_TYPE_INTEGER, + [REQUIRES_PHYSICAL_PRESENCE] = ACPI_TYPE_INTEGER, + [SEQUENCE] = ACPI_TYPE_INTEGER, + [PREREQUISITES_SIZE] = ACPI_TYPE_INTEGER, + [PREREQUISITES] = ACPI_TYPE_STRING, + [SECURITY_LEVEL] = ACPI_TYPE_INTEGER, + [ORD_LIST_SIZE] = ACPI_TYPE_INTEGER, + [ORD_LIST_ELEMENTS] = ACPI_TYPE_STRING, +}; + +static int hp_populate_ordered_list_elements_from_package(union acpi_object *order_obj, + int order_obj_count, + int instance_id) +{ + char *str_value = NULL; + int value_len = 0; + int ret; + u32 size; + u32 int_value = 0; + int elem; + int olist_elem; + int reqs; + int eloc; + char *tmpstr = NULL; + char *part_tmp = NULL; + int tmp_len = 0; + char *part = NULL; + struct ordered_list_data *ordered_list_data = &bioscfg_drv.ordered_list_data[instance_id]; + + if (!order_obj) + return -EINVAL; + + for (elem = 1, eloc = 1; eloc < ORD_ELEM_CNT; elem++, eloc++) { + + switch (order_obj[elem].type) { + case ACPI_TYPE_STRING: + if (elem != PREREQUISITES && elem != ORD_LIST_ELEMENTS) { + ret = hp_convert_hexstr_to_str(order_obj[elem].string.pointer, + order_obj[elem].string.length, + &str_value, &value_len); + if (ret) + continue; + } + break; + case ACPI_TYPE_INTEGER: + int_value = (u32)order_obj[elem].integer.value; + break; + default: + pr_warn("Unsupported object type [%d]\n", order_obj[elem].type); + continue; + } + + /* Check that both expected and read object type match */ + if (expected_order_types[eloc] != order_obj[elem].type) { + pr_err("Error expected type %d for elem %d, but got type %d instead\n", + expected_order_types[eloc], elem, order_obj[elem].type); + kfree(str_value); + return -EIO; + } + + /* Assign appropriate element value to corresponding field*/ + switch (eloc) { + case VALUE: + strscpy(ordered_list_data->current_value, str_value); + replace_char_str(ordered_list_data->current_value, COMMA_SEP, SEMICOLON_SEP); + break; + case PATH: + strscpy(ordered_list_data->common.path, str_value); + break; + case IS_READONLY: + ordered_list_data->common.is_readonly = int_value; + break; + case DISPLAY_IN_UI: + ordered_list_data->common.display_in_ui = int_value; + break; + case REQUIRES_PHYSICAL_PRESENCE: + ordered_list_data->common.requires_physical_presence = int_value; + break; + case SEQUENCE: + ordered_list_data->common.sequence = int_value; + break; + case PREREQUISITES_SIZE: + if (int_value > MAX_PREREQUISITES_SIZE) { + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_PREREQUISITES_SIZE; + } + ordered_list_data->common.prerequisites_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PREREQUISITES + * object is omitted by BIOS when the size is + * zero. + */ + if (int_value == 0) + eloc++; + break; + case PREREQUISITES: + size = min_t(u32, ordered_list_data->common.prerequisites_size, + MAX_PREREQUISITES_SIZE); + for (reqs = 0; reqs < size; reqs++) { + if (elem + reqs >= order_obj_count) { + pr_err("Error elem-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(order_obj[elem + reqs].string.pointer, + order_obj[elem + reqs].string.length, + &str_value, &value_len); + + if (ret) + continue; + + strscpy(ordered_list_data->common.prerequisites[reqs], str_value); + + kfree(str_value); + str_value = NULL; + } + break; + + case SECURITY_LEVEL: + ordered_list_data->common.security_level = int_value; + break; + + case ORD_LIST_SIZE: + if (int_value > MAX_ELEMENTS_SIZE) { + pr_warn("Order List size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_ELEMENTS_SIZE; + } + ordered_list_data->elements_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. ORD_LIST_ELEMENTS + * object is omitted by BIOS when the size is + * zero. + */ + if (int_value == 0) + eloc++; + break; + case ORD_LIST_ELEMENTS: + + /* + * Ordered list data is stored in hex and comma separated format + * Convert the data and split it to show each element + */ + ret = hp_convert_hexstr_to_str(str_value, value_len, &tmpstr, &tmp_len); + if (ret) + goto exit_list; + + part_tmp = tmpstr; + part = strsep(&part_tmp, COMMA_SEP); + + for (olist_elem = 0; olist_elem < MAX_ELEMENTS_SIZE && part; olist_elem++) { + strscpy(ordered_list_data->elements[olist_elem], part); + part = strsep(&part_tmp, COMMA_SEP); + } + ordered_list_data->elements_size = olist_elem; + + kfree(str_value); + str_value = NULL; + break; + default: + pr_warn("Invalid element: %d found in Ordered_List attribute or data may be malformed\n", elem); + break; + } + kfree(tmpstr); + tmpstr = NULL; + kfree(str_value); + str_value = NULL; + } + +exit_list: + kfree(tmpstr); + kfree(str_value); + return 0; +} + +/** + * hp_populate_ordered_list_package_data() - + * Populate all properties of an instance under ordered_list attribute + * + * @order_obj: ACPI object with ordered_list data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_ordered_list_package_data(union acpi_object *order_obj, int instance_id, + struct kobject *attr_name_kobj) +{ + struct ordered_list_data *ordered_list_data = &bioscfg_drv.ordered_list_data[instance_id]; + + ordered_list_data->attr_name_kobj = attr_name_kobj; + + hp_populate_ordered_list_elements_from_package(order_obj, + order_obj->package.count, + instance_id); + hp_update_attribute_permissions(ordered_list_data->common.is_readonly, + &ordered_list_current_val); + hp_friendly_user_name_update(ordered_list_data->common.path, + attr_name_kobj->name, + ordered_list_data->common.display_name, + sizeof(ordered_list_data->common.display_name)); + return sysfs_create_group(attr_name_kobj, &ordered_list_attr_group); +} + +static int hp_populate_ordered_list_elements_from_buffer(u8 *buffer_ptr, u32 *buffer_size, + int instance_id) +{ + int values; + struct ordered_list_data *ordered_list_data = &bioscfg_drv.ordered_list_data[instance_id]; + int ret = 0; + + /* + * Only data relevant to this driver and its functionality is + * read. BIOS defines the order in which each * element is + * read. Element 0 data is not relevant to this + * driver hence it is ignored. For clarity, all element names + * (DISPLAY_IN_UI) which defines the order in which is read + * and the name matches the variable where the data is stored. + * + * In earlier implementation, reported errors were ignored + * causing the data to remain uninitialized. It is not + * possible to determine if data read from BIOS is valid or + * not. It is for this reason functions may return a error + * without validating the data itself. + */ + + // VALUE: + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, ordered_list_data->current_value, + sizeof(ordered_list_data->current_value)); + if (ret < 0) + goto buffer_exit; + + replace_char_str(ordered_list_data->current_value, COMMA_SEP, SEMICOLON_SEP); + + // COMMON: + ret = hp_get_common_data_from_buffer(&buffer_ptr, buffer_size, + &ordered_list_data->common); + if (ret < 0) + goto buffer_exit; + + // ORD_LIST_SIZE: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &ordered_list_data->elements_size); + + if (ordered_list_data->elements_size > MAX_ELEMENTS_SIZE) { + /* Report a message and limit elements size to maximum value */ + pr_warn("Ordered List size value exceeded the maximum number of elements supported or data may be malformed\n"); + ordered_list_data->elements_size = MAX_ELEMENTS_SIZE; + } + + // ORD_LIST_ELEMENTS: + for (values = 0; values < ordered_list_data->elements_size; values++) { + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, + ordered_list_data->elements[values], + sizeof(ordered_list_data->elements[values])); + if (ret < 0) + break; + } + +buffer_exit: + return ret; +} + +/** + * hp_populate_ordered_list_buffer_data() - Populate all properties of an + * instance under ordered list attribute + * + * @buffer_ptr: Buffer pointer + * @buffer_size: Buffer size + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_ordered_list_buffer_data(u8 *buffer_ptr, u32 *buffer_size, int instance_id, + struct kobject *attr_name_kobj) +{ + struct ordered_list_data *ordered_list_data = &bioscfg_drv.ordered_list_data[instance_id]; + int ret = 0; + + ordered_list_data->attr_name_kobj = attr_name_kobj; + + /* Populate ordered list elements */ + ret = hp_populate_ordered_list_elements_from_buffer(buffer_ptr, buffer_size, + instance_id); + if (ret < 0) + return ret; + + hp_update_attribute_permissions(ordered_list_data->common.is_readonly, + &ordered_list_current_val); + hp_friendly_user_name_update(ordered_list_data->common.path, + attr_name_kobj->name, + ordered_list_data->common.display_name, + sizeof(ordered_list_data->common.display_name)); + + return sysfs_create_group(attr_name_kobj, &ordered_list_attr_group); +} + +/** + * hp_exit_ordered_list_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void hp_exit_ordered_list_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < bioscfg_drv.ordered_list_instances_count; + instance_id++) { + struct kobject *attr_name_kobj = + bioscfg_drv.ordered_list_data[instance_id].attr_name_kobj; + + if (attr_name_kobj) + sysfs_remove_group(attr_name_kobj, + &ordered_list_attr_group); + } + bioscfg_drv.ordered_list_instances_count = 0; + + kfree(bioscfg_drv.ordered_list_data); + bioscfg_drv.ordered_list_data = NULL; +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/passwdobj-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/passwdobj-attributes.c new file mode 100644 index 000000000..4d79eb805 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/passwdobj-attributes.c @@ -0,0 +1,546 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to password object type attributes under + * BIOS PASSWORD for use with hp-bioscfg driver. + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" + +GET_INSTANCE_ID(password); +/* + * Clear all passwords copied to memory for a particular + * authentication instance + */ +static int clear_passwords(const int instance) +{ + struct password_data *password_data = &bioscfg_drv.password_data[instance]; + + if (!password_data->is_enabled) + return 0; + + memset(password_data->current_password, + 0, sizeof(password_data->current_password)); + memset(password_data->new_password, + 0, sizeof(password_data->new_password)); + + return 0; +} + +/* + * Clear all credentials copied to memory for both Power-ON and Setup + * BIOS instances + */ +int hp_clear_all_credentials(void) +{ + int count = bioscfg_drv.password_instances_count; + int instance; + + /* clear all passwords */ + for (instance = 0; instance < count; instance++) + clear_passwords(instance); + + /* clear auth_token */ + kfree(bioscfg_drv.spm_data.auth_token); + bioscfg_drv.spm_data.auth_token = NULL; + + return 0; +} + +int hp_get_password_instance_for_type(const char *name) +{ + int count = bioscfg_drv.password_instances_count; + int instance; + + for (instance = 0; instance < count; instance++) + if (!strcmp(bioscfg_drv.password_data[instance].common.display_name, name)) + return instance; + + return -EINVAL; +} + +static int validate_password_input(int instance_id, const char *buf) +{ + int length; + struct password_data *password_data = &bioscfg_drv.password_data[instance_id]; + + length = strlen(buf); + if (buf[length - 1] == '\n') + length--; + + if (length > MAX_PASSWD_SIZE) + return INVALID_BIOS_AUTH; + + if (password_data->min_password_length > length || + password_data->max_password_length < length) + return INVALID_BIOS_AUTH; + return SUCCESS; +} + +ATTRIBUTE_N_PROPERTY_SHOW(is_enabled, password); +static struct kobj_attribute password_is_password_set = __ATTR_RO(is_enabled); + +static int store_password_instance(struct kobject *kobj, const char *buf, + size_t count, bool is_current) +{ + char *buf_cp; + int id, ret = 0; + + buf_cp = kstrdup(buf, GFP_KERNEL); + if (!buf_cp) + return -ENOMEM; + + ret = hp_enforce_single_line_input(buf_cp, count); + if (!ret) { + id = get_password_instance_id(kobj); + + if (id >= 0) + ret = validate_password_input(id, buf_cp); + } + + if (!ret) { + if (is_current) + strscpy(bioscfg_drv.password_data[id].current_password, buf_cp); + else + strscpy(bioscfg_drv.password_data[id].new_password, buf_cp); + } + + kfree(buf_cp); + return ret < 0 ? ret : count; +} + +static ssize_t current_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + return store_password_instance(kobj, buf, count, true); +} + +static struct kobj_attribute password_current_password = __ATTR_WO(current_password); + +static ssize_t new_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + return store_password_instance(kobj, buf, count, true); +} + +static struct kobj_attribute password_new_password = __ATTR_WO(new_password); + +ATTRIBUTE_N_PROPERTY_SHOW(min_password_length, password); +static struct kobj_attribute password_min_password_length = __ATTR_RO(min_password_length); + +ATTRIBUTE_N_PROPERTY_SHOW(max_password_length, password); +static struct kobj_attribute password_max_password_length = __ATTR_RO(max_password_length); + +static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + if (!strcmp(kobj->name, SETUP_PASSWD)) + return sysfs_emit(buf, "%s\n", BIOS_ADMIN); + + if (!strcmp(kobj->name, POWER_ON_PASSWD)) + return sysfs_emit(buf, "%s\n", POWER_ON); + + return -EIO; +} + +static struct kobj_attribute password_role = __ATTR_RO(role); + +static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + int i = get_password_instance_id(kobj); + + if (i < 0) + return i; + + if (bioscfg_drv.password_data[i].mechanism != PASSWORD) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", PASSWD_MECHANISM_TYPES); +} + +static struct kobj_attribute password_mechanism = __ATTR_RO(mechanism); + +ATTRIBUTE_VALUES_PROPERTY_SHOW(encodings, password, SEMICOLON_SEP); +static struct kobj_attribute password_encodings_val = __ATTR_RO(encodings); + +static struct attribute *password_attrs[] = { + &password_is_password_set.attr, + &password_min_password_length.attr, + &password_max_password_length.attr, + &password_current_password.attr, + &password_new_password.attr, + &password_role.attr, + &password_mechanism.attr, + &password_encodings_val.attr, + NULL +}; + +static const struct attribute_group password_attr_group = { + .attrs = password_attrs +}; + +int hp_alloc_password_data(void) +{ + bioscfg_drv.password_instances_count = hp_get_instance_count(HP_WMI_BIOS_PASSWORD_GUID); + bioscfg_drv.password_data = kzalloc_objs(*bioscfg_drv.password_data, + bioscfg_drv.password_instances_count); + if (!bioscfg_drv.password_data) { + bioscfg_drv.password_instances_count = 0; + return -ENOMEM; + } + + return 0; +} + +/* Expected Values types associated with each element */ +static const acpi_object_type expected_password_types[] = { + [NAME] = ACPI_TYPE_STRING, + [VALUE] = ACPI_TYPE_STRING, + [PATH] = ACPI_TYPE_STRING, + [IS_READONLY] = ACPI_TYPE_INTEGER, + [DISPLAY_IN_UI] = ACPI_TYPE_INTEGER, + [REQUIRES_PHYSICAL_PRESENCE] = ACPI_TYPE_INTEGER, + [SEQUENCE] = ACPI_TYPE_INTEGER, + [PREREQUISITES_SIZE] = ACPI_TYPE_INTEGER, + [PREREQUISITES] = ACPI_TYPE_STRING, + [SECURITY_LEVEL] = ACPI_TYPE_INTEGER, + [PSWD_MIN_LENGTH] = ACPI_TYPE_INTEGER, + [PSWD_MAX_LENGTH] = ACPI_TYPE_INTEGER, + [PSWD_SIZE] = ACPI_TYPE_INTEGER, + [PSWD_ENCODINGS] = ACPI_TYPE_STRING, + [PSWD_IS_SET] = ACPI_TYPE_INTEGER, +}; + +static int hp_populate_password_elements_from_package(union acpi_object *password_obj, + int password_obj_count, + int instance_id) +{ + char *str_value = NULL; + int value_len; + int ret; + u32 size; + u32 int_value = 0; + int elem; + int reqs; + int eloc; + int pos_values; + struct password_data *password_data = &bioscfg_drv.password_data[instance_id]; + + if (!password_obj) + return -EINVAL; + + for (elem = 1, eloc = 1; elem < password_obj_count; elem++, eloc++) { + /* ONLY look at the first PASSWORD_ELEM_CNT elements */ + if (eloc == PSWD_ELEM_CNT) + goto exit_package; + + switch (password_obj[elem].type) { + case ACPI_TYPE_STRING: + if (PREREQUISITES != elem && PSWD_ENCODINGS != elem) { + ret = hp_convert_hexstr_to_str(password_obj[elem].string.pointer, + password_obj[elem].string.length, + &str_value, &value_len); + if (ret) + continue; + } + break; + case ACPI_TYPE_INTEGER: + int_value = (u32)password_obj[elem].integer.value; + break; + default: + pr_warn("Unsupported object type [%d]\n", password_obj[elem].type); + continue; + } + + /* Check that both expected and read object type match */ + if (expected_password_types[eloc] != password_obj[elem].type) { + pr_err("Error expected type %d for elem %d, but got type %d instead\n", + expected_password_types[eloc], elem, password_obj[elem].type); + kfree(str_value); + return -EIO; + } + + /* Assign appropriate element value to corresponding field*/ + switch (eloc) { + case VALUE: + break; + case PATH: + strscpy(password_data->common.path, str_value); + break; + case IS_READONLY: + password_data->common.is_readonly = int_value; + break; + case DISPLAY_IN_UI: + password_data->common.display_in_ui = int_value; + break; + case REQUIRES_PHYSICAL_PRESENCE: + password_data->common.requires_physical_presence = int_value; + break; + case SEQUENCE: + password_data->common.sequence = int_value; + break; + case PREREQUISITES_SIZE: + if (int_value > MAX_PREREQUISITES_SIZE) { + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_PREREQUISITES_SIZE; + } + password_data->common.prerequisites_size = int_value; + + /* This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PREREQUISITES + * object is omitted by BIOS when the size is + * zero. + */ + if (int_value == 0) + eloc++; + break; + case PREREQUISITES: + size = min_t(u32, password_data->common.prerequisites_size, + MAX_PREREQUISITES_SIZE); + + for (reqs = 0; reqs < size; reqs++) { + if (elem + reqs >= password_obj_count) { + pr_err("Error elem-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(password_obj[elem + reqs].string.pointer, + password_obj[elem + reqs].string.length, + &str_value, &value_len); + + if (ret) + break; + + strscpy(password_data->common.prerequisites[reqs], str_value); + + kfree(str_value); + str_value = NULL; + + } + break; + case SECURITY_LEVEL: + password_data->common.security_level = int_value; + break; + case PSWD_MIN_LENGTH: + password_data->min_password_length = int_value; + break; + case PSWD_MAX_LENGTH: + password_data->max_password_length = int_value; + break; + case PSWD_SIZE: + + if (int_value > MAX_ENCODINGS_SIZE) { + pr_warn("Password Encoding size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_ENCODINGS_SIZE; + } + password_data->encodings_size = int_value; + + /* This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PSWD_ENCODINGS + * object is omitted by BIOS when the size is + * zero. + */ + if (int_value == 0) + eloc++; + break; + case PSWD_ENCODINGS: + size = min_t(u32, password_data->encodings_size, MAX_ENCODINGS_SIZE); + for (pos_values = 0; pos_values < size; pos_values++) { + ret = hp_convert_hexstr_to_str(password_obj[elem + pos_values].string.pointer, + password_obj[elem + pos_values].string.length, + &str_value, &value_len); + if (ret) + break; + + strscpy(password_data->encodings[pos_values], str_value); + kfree(str_value); + str_value = NULL; + + } + break; + case PSWD_IS_SET: + password_data->is_enabled = int_value; + break; + default: + pr_warn("Invalid element: %d found in Password attribute or data may be malformed\n", elem); + break; + } + + kfree(str_value); + str_value = NULL; + } + +exit_package: + kfree(str_value); + return 0; +} + +/** + * hp_populate_password_package_data() + * Populate all properties for an instance under password attribute + * + * @password_obj: ACPI object with password data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_password_package_data(union acpi_object *password_obj, int instance_id, + struct kobject *attr_name_kobj) +{ + struct password_data *password_data = &bioscfg_drv.password_data[instance_id]; + + password_data->attr_name_kobj = attr_name_kobj; + + hp_populate_password_elements_from_package(password_obj, + password_obj->package.count, + instance_id); + + hp_friendly_user_name_update(password_data->common.path, + attr_name_kobj->name, + password_data->common.display_name, + sizeof(password_data->common.display_name)); + + if (!strcmp(attr_name_kobj->name, SETUP_PASSWD)) + return sysfs_create_group(attr_name_kobj, &password_attr_group); + + return sysfs_create_group(attr_name_kobj, &password_attr_group); +} + +static int hp_populate_password_elements_from_buffer(u8 *buffer_ptr, u32 *buffer_size, + int instance_id) +{ + int values; + int isreadonly; + struct password_data *password_data = &bioscfg_drv.password_data[instance_id]; + int ret = 0; + + /* + * Only data relevant to this driver and its functionality is + * read. BIOS defines the order in which each * element is + * read. Element 0 data is not relevant to this + * driver hence it is ignored. For clarity, all element names + * (DISPLAY_IN_UI) which defines the order in which is read + * and the name matches the variable where the data is stored. + * + * In earlier implementation, reported errors were ignored + * causing the data to remain uninitialized. It is not + * possible to determine if data read from BIOS is valid or + * not. It is for this reason functions may return a error + * without validating the data itself. + */ + + // VALUE: + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, password_data->current_password, + sizeof(password_data->current_password)); + if (ret < 0) + goto buffer_exit; + + // COMMON: + ret = hp_get_common_data_from_buffer(&buffer_ptr, buffer_size, + &password_data->common); + if (ret < 0) + goto buffer_exit; + + // PSWD_MIN_LENGTH: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &password_data->min_password_length); + if (ret < 0) + goto buffer_exit; + + // PSWD_MAX_LENGTH: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &password_data->max_password_length); + if (ret < 0) + goto buffer_exit; + + // PSWD_SIZE: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &password_data->encodings_size); + if (ret < 0) + goto buffer_exit; + + if (password_data->encodings_size > MAX_ENCODINGS_SIZE) { + /* Report a message and limit possible values size to maximum value */ + pr_warn("Password Encoding size value exceeded the maximum number of elements supported or data may be malformed\n"); + password_data->encodings_size = MAX_ENCODINGS_SIZE; + } + + // PSWD_ENCODINGS: + for (values = 0; values < password_data->encodings_size; values++) { + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, + password_data->encodings[values], + sizeof(password_data->encodings[values])); + if (ret < 0) + break; + } + + // PSWD_IS_SET: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, &isreadonly); + if (ret < 0) + goto buffer_exit; + + password_data->is_enabled = isreadonly ? true : false; + +buffer_exit: + return ret; +} + +/** + * hp_populate_password_buffer_data() + * Populate all properties for an instance under password object attribute + * + * @buffer_ptr: Buffer pointer + * @buffer_size: Buffer size + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_password_buffer_data(u8 *buffer_ptr, u32 *buffer_size, int instance_id, + struct kobject *attr_name_kobj) +{ + struct password_data *password_data = &bioscfg_drv.password_data[instance_id]; + int ret = 0; + + password_data->attr_name_kobj = attr_name_kobj; + + /* Populate Password attributes */ + ret = hp_populate_password_elements_from_buffer(buffer_ptr, buffer_size, + instance_id); + if (ret < 0) + return ret; + + hp_friendly_user_name_update(password_data->common.path, + attr_name_kobj->name, + password_data->common.display_name, + sizeof(password_data->common.display_name)); + if (!strcmp(attr_name_kobj->name, SETUP_PASSWD)) + return sysfs_create_group(attr_name_kobj, &password_attr_group); + + return sysfs_create_group(attr_name_kobj, &password_attr_group); +} + +/** + * hp_exit_password_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void hp_exit_password_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < bioscfg_drv.password_instances_count; + instance_id++) { + struct kobject *attr_name_kobj = + bioscfg_drv.password_data[instance_id].attr_name_kobj; + + if (attr_name_kobj) + sysfs_remove_group(attr_name_kobj, + &password_attr_group); + } + bioscfg_drv.password_instances_count = 0; + kfree(bioscfg_drv.password_data); + bioscfg_drv.password_data = NULL; +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/spmobj-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/spmobj-attributes.c new file mode 100644 index 000000000..2b00a1479 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/spmobj-attributes.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to secure platform management object type + * attributes under BIOS PASSWORD for use with hp-bioscfg driver + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" + +static const char * const spm_state_types[] = { + "not provisioned", + "provisioned", + "provisioning in progress", +}; + +static const char * const spm_mechanism_types[] = { + "not provisioned", + "signing-key", + "endorsement-key", +}; + +struct secureplatform_provisioning_data { + u8 state; + u8 version[2]; + u8 reserved1; + u32 features; + u32 nonce; + u8 reserved2[28]; + u8 sk_mod[MAX_KEY_MOD_SIZE]; + u8 kek_mod[MAX_KEY_MOD_SIZE]; +}; + +/** + * hp_calculate_security_buffer() - determines size of security buffer + * for authentication scheme + * + * @authentication: the authentication content + * + * Currently only supported type is Admin password + */ +size_t hp_calculate_security_buffer(const char *authentication) +{ + size_t size, authlen; + + if (!authentication) + return sizeof(u16) * 2; + + authlen = strlen(authentication); + if (!authlen) + return sizeof(u16) * 2; + + size = sizeof(u16) + authlen * sizeof(u16); + if (!strstarts(authentication, BEAM_PREFIX)) + size += strlen(UTF_PREFIX) * sizeof(u16); + + return size; +} + +/** + * hp_populate_security_buffer() - builds a security buffer for + * authentication scheme + * + * @authbuf: the security buffer + * @authentication: the authentication content + * + * Currently only supported type is PLAIN TEXT + */ +int hp_populate_security_buffer(u16 *authbuf, const char *authentication) +{ + u16 *auth = authbuf; + char *strprefix = NULL; + int ret = 0; + + if (strstarts(authentication, BEAM_PREFIX)) { + /* + * BEAM_PREFIX is append to authbuf when a signature + * is provided and Sure Admin is enabled in BIOS + */ + /* BEAM_PREFIX found, convert part to unicode */ + auth = hp_ascii_to_utf16_unicode(auth, authentication); + if (!auth) + return -EINVAL; + + } else { + /* + * UTF-16 prefix is append to the * authbuf when a BIOS + * admin password is configured in BIOS + */ + + /* append UTF_PREFIX to part and then convert it to unicode */ + strprefix = kasprintf(GFP_KERNEL, "%s%s", UTF_PREFIX, + authentication); + if (!strprefix) + return -ENOMEM; + + auth = hp_ascii_to_utf16_unicode(auth, strprefix); + kfree(strprefix); + + if (!auth) { + ret = -EINVAL; + goto out_buffer; + } + } + +out_buffer: + return ret; +} + +static ssize_t update_spm_state(void) +{ + struct secureplatform_provisioning_data data; + int ret; + + ret = hp_wmi_perform_query(HPWMI_SECUREPLATFORM_GET_STATE, + HPWMI_SECUREPLATFORM, &data, 0, + sizeof(data)); + if (ret < 0) + return ret; + + bioscfg_drv.spm_data.mechanism = data.state; + if (bioscfg_drv.spm_data.mechanism) + bioscfg_drv.spm_data.is_enabled = 1; + + return 0; +} + +static ssize_t statusbin(struct kobject *kobj, + struct kobj_attribute *attr, + struct secureplatform_provisioning_data *buf) +{ + int ret = hp_wmi_perform_query(HPWMI_SECUREPLATFORM_GET_STATE, + HPWMI_SECUREPLATFORM, buf, 0, + sizeof(*buf)); + + if (ret < 0) + return ret; + + return sizeof(struct secureplatform_provisioning_data); +} + +/* + * status_show - Reads SPM status + */ +static ssize_t status_show(struct kobject *kobj, struct kobj_attribute + *attr, char *buf) +{ + int ret, i; + int len = 0; + struct secureplatform_provisioning_data data; + + ret = statusbin(kobj, attr, &data); + if (ret < 0) + return ret; + + /* + * 'status' is a read-only file that returns ASCII text in + * JSON format reporting the status information. + * + * "State": "not provisioned | provisioned | provisioning in progress ", + * "Version": " Major. Minor ", + * "Nonce": <16-bit unsigned number display in base 10>, + * "FeaturesInUse": <16-bit unsigned number display in base 10>, + * "EndorsementKeyMod": "<256 bytes in base64>", + * "SigningKeyMod": "<256 bytes in base64>" + */ + + len += sysfs_emit_at(buf, len, "{\n"); + len += sysfs_emit_at(buf, len, "\t\"State\": \"%s\",\n", + spm_state_types[data.state]); + len += sysfs_emit_at(buf, len, "\t\"Version\": \"%d.%d\"", + data.version[0], data.version[1]); + + /* + * state == 0 means secure platform management + * feature is not configured in BIOS. + */ + if (data.state == 0) { + len += sysfs_emit_at(buf, len, "\n"); + goto status_exit; + } else { + len += sysfs_emit_at(buf, len, ",\n"); + } + + len += sysfs_emit_at(buf, len, "\t\"Nonce\": %d,\n", data.nonce); + len += sysfs_emit_at(buf, len, "\t\"FeaturesInUse\": %d,\n", data.features); + len += sysfs_emit_at(buf, len, "\t\"EndorsementKeyMod\": \""); + + for (i = 255; i >= 0; i--) + len += sysfs_emit_at(buf, len, " %u", data.kek_mod[i]); + + len += sysfs_emit_at(buf, len, " \",\n"); + len += sysfs_emit_at(buf, len, "\t\"SigningKeyMod\": \""); + + for (i = 255; i >= 0; i--) + len += sysfs_emit_at(buf, len, " %u", data.sk_mod[i]); + + /* Return buf contents */ + len += sysfs_emit_at(buf, len, " \"\n"); + +status_exit: + len += sysfs_emit_at(buf, len, "}\n"); + + return len; +} + +static struct kobj_attribute password_spm_status = __ATTR_RO(status); + +ATTRIBUTE_SPM_N_PROPERTY_SHOW(is_enabled, spm); +static struct kobj_attribute password_spm_is_key_enabled = __ATTR_RO(is_enabled); + +static ssize_t key_mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", + spm_mechanism_types[bioscfg_drv.spm_data.mechanism]); +} + +static struct kobj_attribute password_spm_key_mechanism = __ATTR_RO(key_mechanism); + +static ssize_t sk_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int length; + + length = count; + if (buf[length - 1] == '\n') + length--; + + /* allocate space and copy current signing key */ + bioscfg_drv.spm_data.signing_key = kmemdup(buf, length, GFP_KERNEL); + if (!bioscfg_drv.spm_data.signing_key) + return -ENOMEM; + + /* submit signing key payload */ + ret = hp_wmi_perform_query(HPWMI_SECUREPLATFORM_SET_SK, + HPWMI_SECUREPLATFORM, + (void *)bioscfg_drv.spm_data.signing_key, + count, 0); + + if (!ret) { + bioscfg_drv.spm_data.mechanism = SIGNING_KEY; + hp_set_reboot_and_signal_event(); + } + + kfree(bioscfg_drv.spm_data.signing_key); + bioscfg_drv.spm_data.signing_key = NULL; + + return ret ? ret : count; +} + +static struct kobj_attribute password_spm_signing_key = __ATTR_WO(sk); + +static ssize_t kek_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int length; + + length = count; + if (buf[length - 1] == '\n') + length--; + + /* allocate space and copy current signing key */ + bioscfg_drv.spm_data.endorsement_key = kmemdup(buf, length, GFP_KERNEL); + if (!bioscfg_drv.spm_data.endorsement_key) { + ret = -ENOMEM; + goto exit_kek; + } + + ret = hp_wmi_perform_query(HPWMI_SECUREPLATFORM_SET_KEK, + HPWMI_SECUREPLATFORM, + (void *)bioscfg_drv.spm_data.endorsement_key, + count, 0); + + if (!ret) { + bioscfg_drv.spm_data.mechanism = ENDORSEMENT_KEY; + hp_set_reboot_and_signal_event(); + } + +exit_kek: + kfree(bioscfg_drv.spm_data.endorsement_key); + bioscfg_drv.spm_data.endorsement_key = NULL; + + return ret ? ret : count; +} + +static struct kobj_attribute password_spm_endorsement_key = __ATTR_WO(kek); + +static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", BIOS_SPM); +} + +static struct kobj_attribute password_spm_role = __ATTR_RO(role); + +static ssize_t auth_token_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int ret = 0; + int length; + + length = count; + if (buf[length - 1] == '\n') + length--; + + /* allocate space and copy current auth token */ + bioscfg_drv.spm_data.auth_token = kmemdup(buf, length, GFP_KERNEL); + if (!bioscfg_drv.spm_data.auth_token) { + ret = -ENOMEM; + goto exit_token; + } + + return count; + +exit_token: + kfree(bioscfg_drv.spm_data.auth_token); + bioscfg_drv.spm_data.auth_token = NULL; + + return ret; +} + +static struct kobj_attribute password_spm_auth_token = __ATTR_WO(auth_token); + +static struct attribute *secure_platform_attrs[] = { + &password_spm_is_key_enabled.attr, + &password_spm_signing_key.attr, + &password_spm_endorsement_key.attr, + &password_spm_key_mechanism.attr, + &password_spm_status.attr, + &password_spm_role.attr, + &password_spm_auth_token.attr, + NULL, +}; + +static const struct attribute_group secure_platform_attr_group = { + .attrs = secure_platform_attrs, +}; + +void hp_exit_secure_platform_attributes(void) +{ + /* remove secure platform sysfs entry and free key data*/ + + kfree(bioscfg_drv.spm_data.endorsement_key); + bioscfg_drv.spm_data.endorsement_key = NULL; + + kfree(bioscfg_drv.spm_data.signing_key); + bioscfg_drv.spm_data.signing_key = NULL; + + kfree(bioscfg_drv.spm_data.auth_token); + bioscfg_drv.spm_data.auth_token = NULL; + + if (bioscfg_drv.spm_data.attr_name_kobj) + sysfs_remove_group(bioscfg_drv.spm_data.attr_name_kobj, + &secure_platform_attr_group); +} + +int hp_populate_secure_platform_data(struct kobject *attr_name_kobj) +{ + /* Populate data for Secure Platform Management */ + bioscfg_drv.spm_data.attr_name_kobj = attr_name_kobj; + + strscpy(bioscfg_drv.spm_data.attribute_name, SPM_STR); + + bioscfg_drv.spm_data.is_enabled = 0; + bioscfg_drv.spm_data.mechanism = 0; + bioscfg_drv.pending_reboot = false; + update_spm_state(); + + bioscfg_drv.spm_data.endorsement_key = NULL; + bioscfg_drv.spm_data.signing_key = NULL; + bioscfg_drv.spm_data.auth_token = NULL; + + return sysfs_create_group(attr_name_kobj, &secure_platform_attr_group); +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/string-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/string-attributes.c new file mode 100644 index 000000000..fe5a9a3a4 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/string-attributes.c @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to string type attributes under + * HP_WMI_BIOS_STRING_GUID for use with hp-bioscfg driver. + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" + +#define WMI_STRING_TYPE "HPBIOS_BIOSString" + +GET_INSTANCE_ID(string); + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + int instance_id = get_string_instance_id(kobj); + + if (instance_id < 0) + return -EIO; + + return sysfs_emit(buf, "%s\n", + bioscfg_drv.string_data[instance_id].current_value); +} + +/** + * validate_string_input() - + * Validate input of current_value against min and max lengths + * + * @instance_id: The instance on which input is validated + * @buf: Input value + */ +static int validate_string_input(int instance_id, const char *buf) +{ + int in_len = strlen(buf); + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + + /* BIOS treats it as a read only attribute */ + if (string_data->common.is_readonly) + return -EIO; + + if (in_len < string_data->min_length || in_len > string_data->max_length) + return -ERANGE; + + return 0; +} + +static void update_string_value(int instance_id, char *attr_value) +{ + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + + /* Write settings to BIOS */ + strscpy(string_data->current_value, attr_value); +} + +/* + * ATTRIBUTE_S_COMMON_PROPERTY_SHOW(display_name_language_code, string); + * static struct kobj_attribute string_display_langcode = + * __ATTR_RO(display_name_language_code); + */ + +ATTRIBUTE_S_COMMON_PROPERTY_SHOW(display_name, string); +static struct kobj_attribute string_display_name = + __ATTR_RO(display_name); + +ATTRIBUTE_PROPERTY_STORE(current_value, string); +static struct kobj_attribute string_current_val = + __ATTR_RW_MODE(current_value, 0644); + +ATTRIBUTE_N_PROPERTY_SHOW(min_length, string); +static struct kobj_attribute string_min_length = + __ATTR_RO(min_length); + +ATTRIBUTE_N_PROPERTY_SHOW(max_length, string); +static struct kobj_attribute string_max_length = + __ATTR_RO(max_length); + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "string\n"); +} + +static struct kobj_attribute string_type = + __ATTR_RO(type); + +static struct attribute *string_attrs[] = { + &common_display_langcode.attr, + &string_display_name.attr, + &string_current_val.attr, + &string_min_length.attr, + &string_max_length.attr, + &string_type.attr, + NULL +}; + +static const struct attribute_group string_attr_group = { + .attrs = string_attrs, +}; + +int hp_alloc_string_data(void) +{ + bioscfg_drv.string_instances_count = hp_get_instance_count(HP_WMI_BIOS_STRING_GUID); + bioscfg_drv.string_data = kzalloc_objs(*bioscfg_drv.string_data, + bioscfg_drv.string_instances_count); + if (!bioscfg_drv.string_data) { + bioscfg_drv.string_instances_count = 0; + return -ENOMEM; + } + return 0; +} + +/* Expected Values types associated with each element */ +static const acpi_object_type expected_string_types[] = { + [NAME] = ACPI_TYPE_STRING, + [VALUE] = ACPI_TYPE_STRING, + [PATH] = ACPI_TYPE_STRING, + [IS_READONLY] = ACPI_TYPE_INTEGER, + [DISPLAY_IN_UI] = ACPI_TYPE_INTEGER, + [REQUIRES_PHYSICAL_PRESENCE] = ACPI_TYPE_INTEGER, + [SEQUENCE] = ACPI_TYPE_INTEGER, + [PREREQUISITES_SIZE] = ACPI_TYPE_INTEGER, + [PREREQUISITES] = ACPI_TYPE_STRING, + [SECURITY_LEVEL] = ACPI_TYPE_INTEGER, + [STR_MIN_LENGTH] = ACPI_TYPE_INTEGER, + [STR_MAX_LENGTH] = ACPI_TYPE_INTEGER, +}; + +static int hp_populate_string_elements_from_package(union acpi_object *string_obj, + int string_obj_count, + int instance_id) +{ + char *str_value = NULL; + int value_len; + int ret = 0; + u32 int_value = 0; + int elem; + int reqs; + int eloc; + int size; + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + + if (!string_obj) + return -EINVAL; + + for (elem = 1, eloc = 1; elem < string_obj_count; elem++, eloc++) { + /* ONLY look at the first STRING_ELEM_CNT elements */ + if (eloc == STR_ELEM_CNT) + goto exit_string_package; + + switch (string_obj[elem].type) { + case ACPI_TYPE_STRING: + if (elem != PREREQUISITES) { + ret = hp_convert_hexstr_to_str(string_obj[elem].string.pointer, + string_obj[elem].string.length, + &str_value, &value_len); + + if (ret) + continue; + } + break; + case ACPI_TYPE_INTEGER: + int_value = (u32)string_obj[elem].integer.value; + break; + default: + pr_warn("Unsupported object type [%d]\n", string_obj[elem].type); + continue; + } + + /* Check that both expected and read object type match */ + if (expected_string_types[eloc] != string_obj[elem].type) { + pr_err("Error expected type %d for elem %d, but got type %d instead\n", + expected_string_types[eloc], elem, string_obj[elem].type); + kfree(str_value); + return -EIO; + } + + /* Assign appropriate element value to corresponding field*/ + switch (eloc) { + case VALUE: + strscpy(string_data->current_value, str_value); + break; + case PATH: + strscpy(string_data->common.path, str_value); + break; + case IS_READONLY: + string_data->common.is_readonly = int_value; + break; + case DISPLAY_IN_UI: + string_data->common.display_in_ui = int_value; + break; + case REQUIRES_PHYSICAL_PRESENCE: + string_data->common.requires_physical_presence = int_value; + break; + case SEQUENCE: + string_data->common.sequence = int_value; + break; + case PREREQUISITES_SIZE: + if (int_value > MAX_PREREQUISITES_SIZE) { + pr_warn("Prerequisites size value exceeded the maximum number of elements supported or data may be malformed\n"); + int_value = MAX_PREREQUISITES_SIZE; + } + string_data->common.prerequisites_size = int_value; + + /* + * This step is needed to keep the expected + * element list pointing to the right obj[elem].type + * when the size is zero. PREREQUISITES + * object is omitted by BIOS when the size is + * zero. + */ + if (string_data->common.prerequisites_size == 0) + eloc++; + break; + case PREREQUISITES: + size = min_t(u32, string_data->common.prerequisites_size, + MAX_PREREQUISITES_SIZE); + + for (reqs = 0; reqs < size; reqs++) { + if (elem + reqs >= string_obj_count) { + pr_err("Error elem-objects package is too small\n"); + return -EINVAL; + } + + ret = hp_convert_hexstr_to_str(string_obj[elem + reqs].string.pointer, + string_obj[elem + reqs].string.length, + &str_value, &value_len); + + if (ret) + continue; + + strscpy(string_data->common.prerequisites[reqs], str_value); + kfree(str_value); + str_value = NULL; + } + break; + + case SECURITY_LEVEL: + string_data->common.security_level = int_value; + break; + case STR_MIN_LENGTH: + string_data->min_length = int_value; + break; + case STR_MAX_LENGTH: + string_data->max_length = int_value; + break; + default: + pr_warn("Invalid element: %d found in String attribute or data may be malformed\n", elem); + break; + } + + kfree(str_value); + str_value = NULL; + } + +exit_string_package: + kfree(str_value); + return 0; +} + +/** + * hp_populate_string_package_data() - + * Populate all properties of an instance under string attribute + * + * @string_obj: ACPI object with string data + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_string_package_data(union acpi_object *string_obj, + int instance_id, + struct kobject *attr_name_kobj) +{ + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + + string_data->attr_name_kobj = attr_name_kobj; + + hp_populate_string_elements_from_package(string_obj, + string_obj->package.count, + instance_id); + + hp_update_attribute_permissions(string_data->common.is_readonly, + &string_current_val); + hp_friendly_user_name_update(string_data->common.path, + attr_name_kobj->name, + string_data->common.display_name, + sizeof(string_data->common.display_name)); + return sysfs_create_group(attr_name_kobj, &string_attr_group); +} + +static int hp_populate_string_elements_from_buffer(u8 *buffer_ptr, u32 *buffer_size, + int instance_id) +{ + int ret = 0; + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + + /* + * Only data relevant to this driver and its functionality is + * read. BIOS defines the order in which each * element is + * read. Element 0 data is not relevant to this + * driver hence it is ignored. For clarity, all element names + * (DISPLAY_IN_UI) which defines the order in which is read + * and the name matches the variable where the data is stored. + * + * In earlier implementation, reported errors were ignored + * causing the data to remain uninitialized. It is not + * possible to determine if data read from BIOS is valid or + * not. It is for this reason functions may return a error + * without validating the data itself. + */ + + // VALUE: + ret = hp_get_string_from_buffer(&buffer_ptr, buffer_size, string_data->current_value, + sizeof(string_data->current_value)); + if (ret < 0) + goto buffer_exit; + + // COMMON: + ret = hp_get_common_data_from_buffer(&buffer_ptr, buffer_size, &string_data->common); + if (ret < 0) + goto buffer_exit; + + // STR_MIN_LENGTH: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &string_data->min_length); + if (ret < 0) + goto buffer_exit; + + // STR_MAX_LENGTH: + ret = hp_get_integer_from_buffer(&buffer_ptr, buffer_size, + &string_data->max_length); + +buffer_exit: + + return ret; +} + +/** + * hp_populate_string_buffer_data() - + * Populate all properties of an instance under string attribute + * + * @buffer_ptr: Buffer pointer + * @buffer_size: Buffer size + * @instance_id: The instance to enumerate + * @attr_name_kobj: The parent kernel object + */ +int hp_populate_string_buffer_data(u8 *buffer_ptr, u32 *buffer_size, + int instance_id, + struct kobject *attr_name_kobj) +{ + struct string_data *string_data = &bioscfg_drv.string_data[instance_id]; + int ret = 0; + + string_data->attr_name_kobj = attr_name_kobj; + + ret = hp_populate_string_elements_from_buffer(buffer_ptr, buffer_size, + instance_id); + if (ret < 0) + return ret; + + hp_update_attribute_permissions(string_data->common.is_readonly, + &string_current_val); + hp_friendly_user_name_update(string_data->common.path, + attr_name_kobj->name, + string_data->common.display_name, + sizeof(string_data->common.display_name)); + + return sysfs_create_group(attr_name_kobj, &string_attr_group); +} + +/** + * hp_exit_string_attributes() - Clear all attribute data + * + * Clears all data allocated for this group of attributes + */ +void hp_exit_string_attributes(void) +{ + int instance_id; + + for (instance_id = 0; instance_id < bioscfg_drv.string_instances_count; + instance_id++) { + struct kobject *attr_name_kobj = + bioscfg_drv.string_data[instance_id].attr_name_kobj; + + if (attr_name_kobj) + sysfs_remove_group(attr_name_kobj, &string_attr_group); + } + bioscfg_drv.string_instances_count = 0; + + kfree(bioscfg_drv.string_data); + bioscfg_drv.string_data = NULL; +} diff --git a/drivers/platform/x86/hp/hp-bioscfg/surestart-attributes.c b/drivers/platform/x86/hp/hp-bioscfg/surestart-attributes.c new file mode 100644 index 000000000..b57e42f29 --- /dev/null +++ b/drivers/platform/x86/hp/hp-bioscfg/surestart-attributes.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Functions corresponding to sure start object type attributes under + * BIOS for use with hp-bioscfg driver + * + * Copyright (c) 2022 HP Development Company, L.P. + */ + +#include "bioscfg.h" +#include + +/* Maximum number of log entries supported when log entry size is 16 + * bytes. This value is calculated by dividing 4096 (page size) by + * log entry size. + */ +#define LOG_MAX_ENTRIES 254 + +/* + * Current Log entry size. This value size will change in the + * future. The driver reads a total of 128 bytes for each log entry + * provided by BIOS but only the first 16 bytes are used/read. + */ +#define LOG_ENTRY_SIZE 16 + +/* + * audit_log_entry_count_show - Reports the number of + * existing audit log entries available + * to be read + */ +static ssize_t audit_log_entry_count_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + int ret; + u32 count = 0; + + ret = hp_wmi_perform_query(HPWMI_SURESTART_GET_LOG_COUNT, + HPWMI_SURESTART, + &count, 1, sizeof(count)); + + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d,%d,%d\n", count, LOG_ENTRY_SIZE, + LOG_MAX_ENTRIES); +} + +/* + * audit_log_entries_show() - Return all entries found in log file + */ +static ssize_t audit_log_entries_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + int ret; + int i; + u32 count = 0; + u8 audit_log_buffer[128]; + + // Get the number of event logs + ret = hp_wmi_perform_query(HPWMI_SURESTART_GET_LOG_COUNT, + HPWMI_SURESTART, + &count, 1, sizeof(count)); + + if (ret < 0) + return ret; + + /* + * The show() api will not work if the audit logs ever go + * beyond 4KB + */ + if (count * LOG_ENTRY_SIZE > PAGE_SIZE) + return -EIO; + + /* + * We are guaranteed the buffer is 4KB so today all the event + * logs will fit + */ + for (i = 0; i < count; i++) { + audit_log_buffer[0] = i + 1; + + /* + * read audit log entry at a time. 'buf' input value + * provides the audit log entry to be read. On + * input, Byte 0 = Audit Log entry number from + * beginning (1..254) + * Entry number 1 is the newest entry whereas the + * highest entry number (number of entries) is the + * oldest entry. + */ + ret = hp_wmi_perform_query(HPWMI_SURESTART_GET_LOG, + HPWMI_SURESTART, + audit_log_buffer, 1, 128); + + if (ret < 0 || (LOG_ENTRY_SIZE * i) > PAGE_SIZE) { + /* + * Encountered a failure while reading + * individual logs. Only a partial list of + * audit log will be returned. + */ + break; + } else { + memcpy(buf, audit_log_buffer, LOG_ENTRY_SIZE); + buf += LOG_ENTRY_SIZE; + } + } + + return i * LOG_ENTRY_SIZE; +} + +static struct kobj_attribute sure_start_audit_log_entry_count = __ATTR_RO(audit_log_entry_count); +static struct kobj_attribute sure_start_audit_log_entries = __ATTR_RO(audit_log_entries); + +static struct attribute *sure_start_attrs[] = { + &sure_start_audit_log_entry_count.attr, + &sure_start_audit_log_entries.attr, + NULL +}; + +static const struct attribute_group sure_start_attr_group = { + .attrs = sure_start_attrs, +}; + +void hp_exit_sure_start_attributes(void) +{ + sysfs_remove_group(bioscfg_drv.sure_start_attr_kobj, + &sure_start_attr_group); +} + +int hp_populate_sure_start_data(struct kobject *attr_name_kobj) +{ + bioscfg_drv.sure_start_attr_kobj = attr_name_kobj; + return sysfs_create_group(attr_name_kobj, &sure_start_attr_group); +} diff --git a/drivers/platform/x86/hp/hp-wmi.c b/drivers/platform/x86/hp/hp-wmi.c new file mode 100644 index 000000000..8ba286ed8 --- /dev/null +++ b/drivers/platform/x86/hp/hp-wmi.c @@ -0,0 +1,2798 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HP WMI hotkeys + * + * Copyright (C) 2008 Red Hat + * Copyright (C) 2010, 2011 Anssi Hannula + * + * Portions based on wistron_btns.c: + * Copyright (C) 2005 Miloslav Trmac + * Copyright (C) 2005 Bernhard Rosenkraenzer + * Copyright (C) 2005 Dmitry Torokhov + */ + +#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 +#include +#include + +MODULE_AUTHOR("Matthew Garrett "); +MODULE_DESCRIPTION("HP laptop WMI driver"); +MODULE_LICENSE("GPL"); + +MODULE_ALIAS("wmi:95F24279-4D7B-4334-9387-ACCDC67EF61C"); +MODULE_ALIAS("wmi:5FB7F034-2C63-45E9-BE91-3D44E2C707E4"); + +#define HPWMI_EVENT_GUID "95F24279-4D7B-4334-9387-ACCDC67EF61C" +#define HPWMI_BIOS_GUID "5FB7F034-2C63-45E9-BE91-3D44E2C707E4" + +enum hp_ec_offsets { + HP_EC_OFFSET_UNKNOWN = 0x00, + HP_NO_THERMAL_PROFILE_OFFSET = 0x01, + HP_VICTUS_S_EC_THERMAL_PROFILE_OFFSET = 0x59, + HP_OMEN_EC_THERMAL_PROFILE_FLAGS_OFFSET = 0x62, + HP_OMEN_EC_THERMAL_PROFILE_TIMER_OFFSET = 0x63, + HP_OMEN_EC_THERMAL_PROFILE_OFFSET = 0x95, +}; + +#define HP_FAN_SPEED_AUTOMATIC 0x00 +#define HP_POWER_LIMIT_DEFAULT 0x00 +#define HP_POWER_LIMIT_NO_CHANGE 0xFF + +#define zero_if_sup(tmp) (zero_insize_support?0:sizeof(tmp)) // use when zero insize is required + +enum hp_thermal_profile_omen_v0 { + HP_OMEN_V0_THERMAL_PROFILE_DEFAULT = 0x00, + HP_OMEN_V0_THERMAL_PROFILE_PERFORMANCE = 0x01, + HP_OMEN_V0_THERMAL_PROFILE_COOL = 0x02, +}; + +enum hp_thermal_profile_omen_v1 { + HP_OMEN_V1_THERMAL_PROFILE_DEFAULT = 0x30, + HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE = 0x31, + HP_OMEN_V1_THERMAL_PROFILE_COOL = 0x50, +}; + +enum hp_thermal_profile_omen_flags { + HP_OMEN_EC_FLAGS_TURBO = 0x04, + HP_OMEN_EC_FLAGS_NOTIMER = 0x02, + HP_OMEN_EC_FLAGS_JUSTSET = 0x01, +}; + +enum hp_thermal_profile_victus { + HP_VICTUS_THERMAL_PROFILE_DEFAULT = 0x00, + HP_VICTUS_THERMAL_PROFILE_PERFORMANCE = 0x01, + HP_VICTUS_THERMAL_PROFILE_QUIET = 0x03, +}; + +enum hp_thermal_profile_victus_s { + HP_VICTUS_S_THERMAL_PROFILE_DEFAULT = 0x00, + HP_VICTUS_S_THERMAL_PROFILE_PERFORMANCE = 0x01, +}; + +enum hp_thermal_profile { + HP_THERMAL_PROFILE_PERFORMANCE = 0x00, + HP_THERMAL_PROFILE_DEFAULT = 0x01, + HP_THERMAL_PROFILE_COOL = 0x02, + HP_THERMAL_PROFILE_QUIET = 0x03, +}; + + +struct thermal_profile_params { + u8 performance; + u8 balanced; + u8 low_power; + u8 ec_tp_offset; +}; + +static const struct thermal_profile_params victus_s_thermal_params = { + .performance = HP_VICTUS_S_THERMAL_PROFILE_PERFORMANCE, + .balanced = HP_VICTUS_S_THERMAL_PROFILE_DEFAULT, + .low_power = HP_VICTUS_S_THERMAL_PROFILE_DEFAULT, + .ec_tp_offset = HP_EC_OFFSET_UNKNOWN, +}; + +static const struct thermal_profile_params omen_v1_thermal_params = { + .performance = HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE, + .balanced = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .low_power = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .ec_tp_offset = HP_VICTUS_S_EC_THERMAL_PROFILE_OFFSET, +}; + +static const struct thermal_profile_params omen_v1_legacy_thermal_params = { + .performance = HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE, + .balanced = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .low_power = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .ec_tp_offset = HP_OMEN_EC_THERMAL_PROFILE_OFFSET, +}; + +static const struct thermal_profile_params omen_v1_no_ec_thermal_params = { + .performance = HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE, + .balanced = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .low_power = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT, + .ec_tp_offset = HP_NO_THERMAL_PROFILE_OFFSET, +}; + +/* + * A generic pointer for the currently-active board's thermal profile + * parameters. + */ +static struct thermal_profile_params *active_thermal_profile_params; + +/* DMI board names of devices that should use the omen specific path for + * thermal profiles. + * This was obtained by taking a look in the windows omen command center + * app and parsing a json file that they use to figure out what capabilities + * the device should have. + * A device is considered an omen if the DisplayName in that list contains + * "OMEN", and it can use the thermal profile stuff if the "Feature" array + * contains "PerformanceControl". + */ +static const char * const omen_thermal_profile_boards[] = { + "84DA", "84DB", "84DC", + "8572", "8573", "8574", "8575", + "8600", "8601", "8602", "8603", "8604", "8605", "8606", "8607", "860A", + "8746", "8747", "8748", "8749", "874A", "8786", "8787", "8788", "878A", + "878B", "878C", "87B5", + "886B", "886C", "88C8", "88CB", "88D1", "88D2", "88F4", "88F5", "88F6", + "88F7", "88FD", "88FE", "88FF", + "8900", "8901", "8902", "8912", "8917", "8918", "8949", "894A", "89EB", + "8A15", "8A42", + "8BAD", + "8C58", + "8E41", +}; + +/* DMI Board names of Omen laptops that are specifically set to be thermal + * profile version 0 by the Omen Command Center app, regardless of what + * the get system design information WMI call returns + */ +static const char * const omen_thermal_profile_force_v0_boards[] = { + "8607", + "8746", "8747", "8748", "8749", "874A", +}; + +/* DMI board names of Omen laptops that have a thermal profile timer which will + * cause the embedded controller to set the thermal profile back to + * "balanced" when reaching zero. + */ +static const char * const omen_timed_thermal_profile_boards[] = { + "8A15", "8A42", + "8BAD", +}; + +/* DMI Board names of Victus 16-d laptops */ +static const char * const victus_thermal_profile_boards[] = { + "88F8", + "8A25", +}; + +/* DMI Board names of Victus 16-r and Victus 16-s laptops */ +static const struct dmi_system_id victus_s_thermal_profile_boards[] __initconst = { + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8902") }, + .driver_data = (void *)&omen_v1_legacy_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8A44") }, + .driver_data = (void *)&omen_v1_legacy_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8A4D") }, + .driver_data = (void *)&omen_v1_legacy_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BAB") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8B2F") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BBE") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BC2") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BCA") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BCD") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BD4") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8BD5") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8C76") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8C77") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8C78") }, + .driver_data = (void *)&omen_v1_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8C99") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8C9C") }, + .driver_data = (void *)&victus_s_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8D26") }, + .driver_data = (void *)&omen_v1_legacy_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8D41") }, + .driver_data = (void *)&omen_v1_no_ec_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8D87") }, + .driver_data = (void *)&omen_v1_no_ec_thermal_params, + }, + { + .matches = { DMI_MATCH(DMI_BOARD_NAME, "8E35") }, + .driver_data = (void *)&omen_v1_legacy_thermal_params, + }, + {}, +}; + +static bool is_victus_s_board; + +enum hp_wmi_radio { + HPWMI_WIFI = 0x0, + HPWMI_BLUETOOTH = 0x1, + HPWMI_WWAN = 0x2, + HPWMI_GPS = 0x3, +}; + +enum hp_wmi_event_ids { + HPWMI_DOCK_EVENT = 0x01, + HPWMI_PARK_HDD = 0x02, + HPWMI_SMART_ADAPTER = 0x03, + HPWMI_BEZEL_BUTTON = 0x04, + HPWMI_WIRELESS = 0x05, + HPWMI_CPU_BATTERY_THROTTLE = 0x06, + HPWMI_LOCK_SWITCH = 0x07, + HPWMI_LID_SWITCH = 0x08, + HPWMI_SCREEN_ROTATION = 0x09, + HPWMI_COOLSENSE_SYSTEM_MOBILE = 0x0A, + HPWMI_COOLSENSE_SYSTEM_HOT = 0x0B, + HPWMI_PROXIMITY_SENSOR = 0x0C, + HPWMI_BACKLIT_KB_BRIGHTNESS = 0x0D, + HPWMI_PEAKSHIFT_PERIOD = 0x0F, + HPWMI_BATTERY_CHARGE_PERIOD = 0x10, + HPWMI_SANITIZATION_MODE = 0x17, + HPWMI_CAMERA_TOGGLE = 0x1A, + HPWMI_FN_P_HOTKEY = 0x1B, + HPWMI_OMEN_KEY = 0x1D, + HPWMI_SMART_EXPERIENCE_APP = 0x21, +}; + +/* + * struct bios_args buffer is dynamically allocated. New WMI command types + * were introduced that exceeds 128-byte data size. Changes to handle + * the data size allocation scheme were kept in hp_wmi_perform_qurey function. + */ +struct bios_args { + u32 signature; + u32 command; + u32 commandtype; + u32 datasize; + u8 data[]; +}; + +enum hp_wmi_commandtype { + HPWMI_DISPLAY_QUERY = 0x01, + HPWMI_HDDTEMP_QUERY = 0x02, + HPWMI_ALS_QUERY = 0x03, + HPWMI_HARDWARE_QUERY = 0x04, + HPWMI_WIRELESS_QUERY = 0x05, + HPWMI_BATTERY_QUERY = 0x07, + HPWMI_BIOS_QUERY = 0x09, + HPWMI_FEATURE_QUERY = 0x0b, + HPWMI_HOTKEY_QUERY = 0x0c, + HPWMI_FEATURE2_QUERY = 0x0d, + HPWMI_WIRELESS2_QUERY = 0x1b, + HPWMI_POSTCODEERROR_QUERY = 0x2a, + HPWMI_SYSTEM_DEVICE_MODE = 0x40, + HPWMI_THERMAL_PROFILE_QUERY = 0x4c, +}; + +struct victus_power_limits { + u8 pl1; + u8 pl2; + u8 pl4; + u8 cpu_gpu_concurrent_limit; +}; + +struct victus_gpu_power_modes { + u8 ctgp_enable; + u8 ppab_enable; + u8 dstate; + u8 gpu_slowdown_temp; +}; + +enum hp_wmi_gm_commandtype { + HPWMI_FAN_SPEED_GET_QUERY = 0x11, + HPWMI_SET_PERFORMANCE_MODE = 0x1A, + HPWMI_FAN_SPEED_MAX_GET_QUERY = 0x26, + HPWMI_FAN_SPEED_MAX_SET_QUERY = 0x27, + HPWMI_GET_SYSTEM_DESIGN_DATA = 0x28, + HPWMI_FAN_COUNT_GET_QUERY = 0x10, + HPWMI_GET_GPU_THERMAL_MODES_QUERY = 0x21, + HPWMI_SET_GPU_THERMAL_MODES_QUERY = 0x22, + HPWMI_SET_POWER_LIMITS_QUERY = 0x29, + HPWMI_VICTUS_S_FAN_SPEED_GET_QUERY = 0x2D, + HPWMI_VICTUS_S_FAN_SPEED_SET_QUERY = 0x2E, + HPWMI_VICTUS_S_GET_FAN_TABLE_QUERY = 0x2F, +}; + +enum hp_wmi_command { + HPWMI_READ = 0x01, + HPWMI_WRITE = 0x02, + HPWMI_ODM = 0x03, + HPWMI_GM = 0x20008, +}; + +enum hp_wmi_hardware_mask { + HPWMI_DOCK_MASK = 0x01, + HPWMI_TABLET_MASK = 0x04, +}; + +struct bios_return { + u32 sigpass; + u32 return_code; +}; + +enum hp_return_value { + HPWMI_RET_WRONG_SIGNATURE = 0x02, + HPWMI_RET_UNKNOWN_COMMAND = 0x03, + HPWMI_RET_UNKNOWN_CMDTYPE = 0x04, + HPWMI_RET_INVALID_PARAMETERS = 0x05, +}; + +enum hp_wireless2_bits { + HPWMI_POWER_STATE = 0x01, + HPWMI_POWER_SOFT = 0x02, + HPWMI_POWER_BIOS = 0x04, + HPWMI_POWER_HARD = 0x08, + HPWMI_POWER_FW_OR_HW = HPWMI_POWER_BIOS | HPWMI_POWER_HARD, +}; + +#define IS_HWBLOCKED(x) ((x & HPWMI_POWER_FW_OR_HW) != HPWMI_POWER_FW_OR_HW) +#define IS_SWBLOCKED(x) !(x & HPWMI_POWER_SOFT) + +struct bios_rfkill2_device_state { + u8 radio_type; + u8 bus_type; + u16 vendor_id; + u16 product_id; + u16 subsys_vendor_id; + u16 subsys_product_id; + u8 rfkill_id; + u8 power; + u8 unknown[4]; +}; + +/* 7 devices fit into the 128 byte buffer */ +#define HPWMI_MAX_RFKILL2_DEVICES 7 + +struct bios_rfkill2_state { + u8 unknown[7]; + u8 count; + u8 pad[8]; + struct bios_rfkill2_device_state device[HPWMI_MAX_RFKILL2_DEVICES]; +}; + +static const struct key_entry hp_wmi_keymap[] = { + { KE_KEY, 0x02, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x03, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x270, { KEY_MICMUTE } }, + { KE_KEY, 0x20e6, { KEY_PROG1 } }, + { KE_KEY, 0x20e8, { KEY_MEDIA } }, + { KE_KEY, 0x2142, { KEY_MEDIA } }, + { KE_KEY, 0x213b, { KEY_INFO } }, + { KE_KEY, 0x2169, { KEY_ROTATE_DISPLAY } }, + { KE_KEY, 0x216a, { KEY_SETUP } }, + { KE_IGNORE, 0x21a4, }, /* Win Lock On */ + { KE_IGNORE, 0x121a4, }, /* Win Lock Off */ + { KE_KEY, 0x21a5, { KEY_PROG2 } }, /* HP Omen Key */ + { KE_KEY, 0x21a7, { KEY_FN_ESC } }, + { KE_KEY, 0x21a8, { KEY_PROG2 } }, /* HP Envy x360 programmable key */ + { KE_KEY, 0x21a9, { KEY_TOUCHPAD_OFF } }, + { KE_KEY, 0x121a9, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 0x231b, { KEY_HELP } }, + { KE_IGNORE, 0x21ab, }, /* FnLock on */ + { KE_IGNORE, 0x121ab, }, /* FnLock off */ + { KE_IGNORE, 0x30021aa, }, /* kbd backlight: level 2 -> off */ + { KE_IGNORE, 0x33221aa, }, /* kbd backlight: off -> level 1 */ + { KE_IGNORE, 0x36421aa, }, /* kbd backlight: level 1 -> level 2*/ + { KE_END, 0 } +}; + +/* + * Mutex for the active_platform_profile variable, + * see omen_powersource_event. + */ +static DEFINE_MUTEX(active_platform_profile_lock); + +static struct input_dev *hp_wmi_input_dev; +static struct input_dev *camera_shutter_input_dev; +static struct platform_device *hp_wmi_platform_dev; +static struct device *platform_profile_device; +static struct notifier_block platform_power_source_nb; +static enum platform_profile_option active_platform_profile; +static bool platform_profile_support; +static bool zero_insize_support; + +static struct rfkill *wifi_rfkill; +static struct rfkill *bluetooth_rfkill; +static struct rfkill *wwan_rfkill; + +struct rfkill2_device { + u8 id; + int num; + struct rfkill *rfkill; +}; + +static int rfkill2_count; +static struct rfkill2_device rfkill2[HPWMI_MAX_RFKILL2_DEVICES]; + +/* + * Chassis Types values were obtained from SMBIOS reference + * specification version 3.00. A complete list of system enclosures + * and chassis types is available on Table 17. + */ +static const char * const tablet_chassis_types[] = { + "30", /* Tablet*/ + "31", /* Convertible */ + "32" /* Detachable */ +}; + +#define DEVICE_MODE_TABLET 0x06 + +#define CPU_FAN 0 +#define GPU_FAN 1 + +enum pwm_modes { + PWM_MODE_MAX = 0, + PWM_MODE_MANUAL = 1, + PWM_MODE_AUTO = 2, +}; + +struct hp_wmi_hwmon_priv { + struct mutex lock; /* protects mode, pwm */ + u8 min_rpm; + u8 max_rpm; + int gpu_delta; + u8 mode; + u8 pwm; + struct delayed_work keep_alive_dwork; +}; + +struct victus_s_fan_table_header { + u8 num_fans; + u8 unknown; +} __packed; + +struct victus_s_fan_table_entry { + u8 cpu_rpm; + u8 gpu_rpm; + u8 noise_db; +} __packed; + +struct victus_s_fan_table { + struct victus_s_fan_table_header header; + struct victus_s_fan_table_entry entries[]; +} __packed; + +/* + * 90s delay to prevent the firmware from resetting fan mode after fixed + * 120s timeout + */ +#define KEEP_ALIVE_DELAY_SECS 90 + +static inline u8 rpm_to_pwm(u8 rpm, struct hp_wmi_hwmon_priv *priv) +{ + return fixp_linear_interpolate(0, 0, priv->max_rpm, U8_MAX, + clamp_val(rpm, 0, priv->max_rpm)); +} + +static inline u8 pwm_to_rpm(u8 pwm, struct hp_wmi_hwmon_priv *priv) +{ + return fixp_linear_interpolate(0, 0, U8_MAX, priv->max_rpm, + clamp_val(pwm, 0, U8_MAX)); +} + +/* map output size to the corresponding WMI method id */ +static inline int encode_outsize_for_pvsz(int outsize) +{ + if (outsize > 4096) + return -EINVAL; + if (outsize > 1024) + return 5; + if (outsize > 128) + return 4; + if (outsize > 4) + return 3; + if (outsize > 0) + return 2; + return 1; +} + +/* + * hp_wmi_perform_query + * + * query: The commandtype (enum hp_wmi_commandtype) + * write: The command (enum hp_wmi_command) + * buffer: Buffer used as input and/or output + * insize: Size of input buffer + * outsize: Size of output buffer + * + * returns zero on success + * an HP WMI query specific error code (which is positive) + * -EINVAL if the query was not successful at all + * -EINVAL if the output buffer size exceeds buffersize + * + * Note: The buffersize must at least be the maximum of the input and output + * size. E.g. Battery info query is defined to have 1 byte input + * and 128 byte output. The caller would do: + * buffer = kzalloc(128, GFP_KERNEL); + * ret = hp_wmi_perform_query(HPWMI_BATTERY_QUERY, HPWMI_READ, buffer, 1, 128) + */ +static int hp_wmi_perform_query(int query, enum hp_wmi_command command, + void *buffer, int insize, int outsize) +{ + struct acpi_buffer input, output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct bios_return *bios_return; + union acpi_object *obj = NULL; + struct bios_args *args = NULL; + int mid, actual_insize, actual_outsize; + size_t bios_args_size; + int ret; + + mid = encode_outsize_for_pvsz(outsize); + if (WARN_ON(mid < 0)) + return mid; + + actual_insize = max(insize, 128); + bios_args_size = struct_size(args, data, actual_insize); + args = kmalloc(bios_args_size, GFP_KERNEL); + if (!args) + return -ENOMEM; + + input.length = bios_args_size; + input.pointer = args; + + args->signature = 0x55434553; + args->command = command; + args->commandtype = query; + args->datasize = insize; + memcpy(args->data, buffer, flex_array_size(args, data, insize)); + + ret = wmi_evaluate_method(HPWMI_BIOS_GUID, 0, mid, &input, &output); + if (ret) + goto out_free; + + obj = output.pointer; + if (!obj) { + ret = -EINVAL; + goto out_free; + } + + if (obj->type != ACPI_TYPE_BUFFER) { + pr_warn("query 0x%x returned an invalid object 0x%x\n", query, ret); + ret = -EINVAL; + goto out_free; + } + + bios_return = (struct bios_return *)obj->buffer.pointer; + ret = bios_return->return_code; + + if (ret) { + if (ret != HPWMI_RET_UNKNOWN_COMMAND && + ret != HPWMI_RET_UNKNOWN_CMDTYPE) + pr_warn("query 0x%x returned error 0x%x\n", query, ret); + goto out_free; + } + + /* Ignore output data of zero size */ + if (!outsize) + goto out_free; + + actual_outsize = min(outsize, (int)(obj->buffer.length - sizeof(*bios_return))); + memcpy(buffer, obj->buffer.pointer + sizeof(*bios_return), actual_outsize); + memset(buffer + actual_outsize, 0, outsize - actual_outsize); + +out_free: + kfree(obj); + kfree(args); + return ret; +} + +/* + * Calling this hp_wmi_get_fan_count_userdefine_trigger function also enables + * and/or maintains the laptop in user defined thermal and fan states, instead + * of using a fallback state. After a 120 seconds timeout however, the laptop + * goes back to its fallback state. + */ +static int hp_wmi_get_fan_count_userdefine_trigger(void) +{ + u8 fan_data[4] = {}; + int ret; + + ret = hp_wmi_perform_query(HPWMI_FAN_COUNT_GET_QUERY, HPWMI_GM, + &fan_data, sizeof(u8), + sizeof(fan_data)); + if (ret != 0) + return -EINVAL; + + return fan_data[0]; /* Others bytes aren't providing fan count */ +} + +static int hp_wmi_get_fan_speed(int fan) +{ + u8 fsh, fsl; + char fan_data[4] = { fan, 0, 0, 0 }; + + int ret = hp_wmi_perform_query(HPWMI_FAN_SPEED_GET_QUERY, HPWMI_GM, + &fan_data, sizeof(char), + sizeof(fan_data)); + + if (ret != 0) + return -EINVAL; + + fsh = fan_data[2]; + fsl = fan_data[3]; + + return (fsh << 8) | fsl; +} + +static int hp_wmi_get_fan_speed_victus_s(int fan) +{ + u8 fan_data[128] = {}; + int ret; + + if (fan < 0 || fan >= sizeof(fan_data)) + return -EINVAL; + + ret = hp_wmi_perform_query(HPWMI_VICTUS_S_FAN_SPEED_GET_QUERY, + HPWMI_GM, &fan_data, sizeof(u8), + sizeof(fan_data)); + if (ret != 0) + return -EINVAL; + + return fan_data[fan] * 100; +} + +static int hp_wmi_read_int(int query) +{ + int val = 0, ret; + + ret = hp_wmi_perform_query(query, HPWMI_READ, &val, + zero_if_sup(val), sizeof(val)); + + if (ret) + return ret < 0 ? ret : -EINVAL; + + return val; +} + +static int hp_wmi_get_dock_state(void) +{ + int state = hp_wmi_read_int(HPWMI_HARDWARE_QUERY); + + if (state < 0) + return state; + + return !!(state & HPWMI_DOCK_MASK); +} + +static int hp_wmi_get_tablet_mode(void) +{ + char system_device_mode[4] = { 0 }; + const char *chassis_type; + bool tablet_found; + int ret; + + chassis_type = dmi_get_system_info(DMI_CHASSIS_TYPE); + if (!chassis_type) + return -ENODEV; + + tablet_found = match_string(tablet_chassis_types, + ARRAY_SIZE(tablet_chassis_types), + chassis_type) >= 0; + if (!tablet_found) + return -ENODEV; + + ret = hp_wmi_perform_query(HPWMI_SYSTEM_DEVICE_MODE, HPWMI_READ, + system_device_mode, zero_if_sup(system_device_mode), + sizeof(system_device_mode)); + if (ret < 0) + return ret; + + return system_device_mode[0] == DEVICE_MODE_TABLET; +} + +static int omen_thermal_profile_set(int mode) +{ + /* The Omen Control Center actively sets the first byte of the buffer to + * 255, so let's mimic this behaviour to be as close as possible to + * the original software. + */ + char buffer[2] = {-1, mode}; + int ret; + + ret = hp_wmi_perform_query(HPWMI_SET_PERFORMANCE_MODE, HPWMI_GM, + &buffer, sizeof(buffer), 0); + + if (ret) + return ret < 0 ? ret : -EINVAL; + + return mode; +} + +static bool is_omen_thermal_profile(void) +{ + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board_name) + return false; + + return match_string(omen_thermal_profile_boards, + ARRAY_SIZE(omen_thermal_profile_boards), + board_name) >= 0; +} + +static int omen_get_thermal_policy_version(void) +{ + unsigned char buffer[8] = { 0 }; + int ret; + + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + + if (board_name) { + int matches = match_string(omen_thermal_profile_force_v0_boards, + ARRAY_SIZE(omen_thermal_profile_force_v0_boards), + board_name); + if (matches >= 0) + return 0; + } + + ret = hp_wmi_perform_query(HPWMI_GET_SYSTEM_DESIGN_DATA, HPWMI_GM, + &buffer, sizeof(buffer), sizeof(buffer)); + + if (ret) + return ret < 0 ? ret : -EINVAL; + + return buffer[3]; +} + +static int omen_thermal_profile_get(void) +{ + u8 data; + + int ret = ec_read(HP_OMEN_EC_THERMAL_PROFILE_OFFSET, &data); + + if (ret) + return ret; + + return data; +} + +static int hp_wmi_fan_speed_max_set(int enabled) +{ + int ret; + + ret = hp_wmi_perform_query(HPWMI_FAN_SPEED_MAX_SET_QUERY, HPWMI_GM, + &enabled, sizeof(enabled), 0); + + if (ret) + return ret < 0 ? ret : -EINVAL; + + return enabled; +} + +static int hp_wmi_fan_speed_set(struct hp_wmi_hwmon_priv *priv, u8 speed) +{ + u8 fan_speed[2]; + int gpu_speed, ret; + + fan_speed[CPU_FAN] = speed; + fan_speed[GPU_FAN] = speed; + + /* + * GPU fan speed is always a little higher than CPU fan speed, we fetch + * this delta value from the fan table during hwmon init. + * Exception: Speed is set to HP_FAN_SPEED_AUTOMATIC, to revert to + * automatic mode. + */ + if (speed != HP_FAN_SPEED_AUTOMATIC) { + gpu_speed = speed + priv->gpu_delta; + fan_speed[GPU_FAN] = clamp_val(gpu_speed, 0, U8_MAX); + } + + ret = hp_wmi_get_fan_count_userdefine_trigger(); + if (ret < 0) + return ret; + /* Max fans need to be explicitly disabled */ + ret = hp_wmi_fan_speed_max_set(0); + if (ret < 0) + return ret; + ret = hp_wmi_perform_query(HPWMI_VICTUS_S_FAN_SPEED_SET_QUERY, HPWMI_GM, + &fan_speed, sizeof(fan_speed), 0); + + return ret; +} + +static int hp_wmi_fan_speed_reset(struct hp_wmi_hwmon_priv *priv) +{ + return hp_wmi_fan_speed_set(priv, HP_FAN_SPEED_AUTOMATIC); +} + +static int hp_wmi_fan_speed_max_reset(struct hp_wmi_hwmon_priv *priv) +{ + int ret; + + ret = hp_wmi_fan_speed_max_set(0); + if (ret) + return ret; + + /* Disabling max fan speed on Victus s1xxx laptops needs a 2nd step: */ + return hp_wmi_fan_speed_reset(priv); +} + +static int __init hp_wmi_bios_2008_later(void) +{ + int state = 0; + int ret = hp_wmi_perform_query(HPWMI_FEATURE_QUERY, HPWMI_READ, &state, + zero_if_sup(state), sizeof(state)); + if (!ret) + return 1; + + return (ret == HPWMI_RET_UNKNOWN_CMDTYPE) ? 0 : -ENXIO; +} + +static int __init hp_wmi_bios_2009_later(void) +{ + u8 state[128]; + int ret = hp_wmi_perform_query(HPWMI_FEATURE2_QUERY, HPWMI_READ, &state, + zero_if_sup(state), sizeof(state)); + if (!ret) + return 1; + + return (ret == HPWMI_RET_UNKNOWN_CMDTYPE) ? 0 : -ENXIO; +} + +static int __init hp_wmi_enable_hotkeys(void) +{ + int value = 0x6e; + int ret = hp_wmi_perform_query(HPWMI_BIOS_QUERY, HPWMI_WRITE, &value, + sizeof(value), 0); + + return ret <= 0 ? ret : -EINVAL; +} + +static int hp_wmi_set_block(void *data, bool blocked) +{ + enum hp_wmi_radio r = (long)data; + int query = BIT(r + 8) | ((!blocked) << r); + int ret; + + ret = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, HPWMI_WRITE, + &query, sizeof(query), 0); + + return ret <= 0 ? ret : -EINVAL; +} + +static const struct rfkill_ops hp_wmi_rfkill_ops = { + .set_block = hp_wmi_set_block, +}; + +static bool hp_wmi_get_sw_state(enum hp_wmi_radio r) +{ + int mask = 0x200 << (r * 8); + + int wireless = hp_wmi_read_int(HPWMI_WIRELESS_QUERY); + + /* TBD: Pass error */ + WARN_ONCE(wireless < 0, "error executing HPWMI_WIRELESS_QUERY"); + + return !(wireless & mask); +} + +static bool hp_wmi_get_hw_state(enum hp_wmi_radio r) +{ + int mask = 0x800 << (r * 8); + + int wireless = hp_wmi_read_int(HPWMI_WIRELESS_QUERY); + + /* TBD: Pass error */ + WARN_ONCE(wireless < 0, "error executing HPWMI_WIRELESS_QUERY"); + + return !(wireless & mask); +} + +static int hp_wmi_rfkill2_set_block(void *data, bool blocked) +{ + int rfkill_id = (int)(long)data; + char buffer[4] = { 0x01, 0x00, rfkill_id, !blocked }; + int ret; + + ret = hp_wmi_perform_query(HPWMI_WIRELESS2_QUERY, HPWMI_WRITE, + buffer, sizeof(buffer), 0); + + return ret <= 0 ? ret : -EINVAL; +} + +static const struct rfkill_ops hp_wmi_rfkill2_ops = { + .set_block = hp_wmi_rfkill2_set_block, +}; + +static int hp_wmi_rfkill2_refresh(void) +{ + struct bios_rfkill2_state state; + int err, i; + + err = hp_wmi_perform_query(HPWMI_WIRELESS2_QUERY, HPWMI_READ, &state, + zero_if_sup(state), sizeof(state)); + if (err) + return err; + + for (i = 0; i < rfkill2_count; i++) { + int num = rfkill2[i].num; + struct bios_rfkill2_device_state *devstate; + + devstate = &state.device[num]; + + if (num >= state.count || + devstate->rfkill_id != rfkill2[i].id) { + pr_warn("power configuration of the wireless devices unexpectedly changed\n"); + continue; + } + + rfkill_set_states(rfkill2[i].rfkill, + IS_SWBLOCKED(devstate->power), + IS_HWBLOCKED(devstate->power)); + } + + return 0; +} + +static ssize_t display_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_read_int(HPWMI_DISPLAY_QUERY); + + if (value < 0) + return value; + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t hddtemp_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_read_int(HPWMI_HDDTEMP_QUERY); + + if (value < 0) + return value; + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t als_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_read_int(HPWMI_ALS_QUERY); + + if (value < 0) + return value; + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t dock_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_get_dock_state(); + + if (value < 0) + return value; + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t tablet_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int value = hp_wmi_get_tablet_mode(); + + if (value < 0) + return value; + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t postcode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + /* Get the POST error code of previous boot failure. */ + int value = hp_wmi_read_int(HPWMI_POSTCODEERROR_QUERY); + + if (value < 0) + return value; + return sysfs_emit(buf, "0x%x\n", value); +} + +static ssize_t als_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 tmp; + int ret; + + ret = kstrtou32(buf, 10, &tmp); + if (ret) + return ret; + + ret = hp_wmi_perform_query(HPWMI_ALS_QUERY, HPWMI_WRITE, &tmp, + sizeof(tmp), 0); + if (ret) + return ret < 0 ? ret : -EINVAL; + + return count; +} + +static ssize_t postcode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 tmp = 1; + bool clear; + int ret; + + ret = kstrtobool(buf, &clear); + if (ret) + return ret; + + if (clear == false) + return -EINVAL; + + /* Clear the POST error code. It is kept until cleared. */ + ret = hp_wmi_perform_query(HPWMI_POSTCODEERROR_QUERY, HPWMI_WRITE, &tmp, + sizeof(tmp), 0); + if (ret) + return ret < 0 ? ret : -EINVAL; + + return count; +} + +static int camera_shutter_input_setup(void) +{ + int err; + + camera_shutter_input_dev = input_allocate_device(); + if (!camera_shutter_input_dev) + return -ENOMEM; + + camera_shutter_input_dev->name = "HP WMI camera shutter"; + camera_shutter_input_dev->phys = "wmi/input1"; + camera_shutter_input_dev->id.bustype = BUS_HOST; + + __set_bit(EV_SW, camera_shutter_input_dev->evbit); + __set_bit(SW_CAMERA_LENS_COVER, camera_shutter_input_dev->swbit); + + err = input_register_device(camera_shutter_input_dev); + if (err) + goto err_free_dev; + + return 0; + + err_free_dev: + input_free_device(camera_shutter_input_dev); + camera_shutter_input_dev = NULL; + return err; +} + +static DEVICE_ATTR_RO(display); +static DEVICE_ATTR_RO(hddtemp); +static DEVICE_ATTR_RW(als); +static DEVICE_ATTR_RO(dock); +static DEVICE_ATTR_RO(tablet); +static DEVICE_ATTR_RW(postcode); + +static struct attribute *hp_wmi_attrs[] = { + &dev_attr_display.attr, + &dev_attr_hddtemp.attr, + &dev_attr_als.attr, + &dev_attr_dock.attr, + &dev_attr_tablet.attr, + &dev_attr_postcode.attr, + NULL, +}; +ATTRIBUTE_GROUPS(hp_wmi); + +static void hp_wmi_notify(union acpi_object *obj, void *context) +{ + u32 event_id, event_data; + u32 *location; + int key_code; + + if (!obj) + return; + if (obj->type != ACPI_TYPE_BUFFER) { + pr_info("Unknown response received %d\n", obj->type); + return; + } + + /* + * Depending on ACPI version the concatenation of id and event data + * inside _WED function will result in a 8 or 16 byte buffer. + */ + location = (u32 *)obj->buffer.pointer; + if (obj->buffer.length == 8) { + event_id = *location; + event_data = *(location + 1); + } else if (obj->buffer.length == 16) { + event_id = *location; + event_data = *(location + 2); + } else { + pr_info("Unknown buffer length %d\n", obj->buffer.length); + return; + } + + switch (event_id) { + case HPWMI_DOCK_EVENT: + if (test_bit(SW_DOCK, hp_wmi_input_dev->swbit)) + input_report_switch(hp_wmi_input_dev, SW_DOCK, + hp_wmi_get_dock_state()); + if (test_bit(SW_TABLET_MODE, hp_wmi_input_dev->swbit)) + input_report_switch(hp_wmi_input_dev, SW_TABLET_MODE, + hp_wmi_get_tablet_mode()); + input_sync(hp_wmi_input_dev); + break; + case HPWMI_PARK_HDD: + break; + case HPWMI_SMART_ADAPTER: + break; + case HPWMI_BEZEL_BUTTON: + key_code = hp_wmi_read_int(HPWMI_HOTKEY_QUERY); + if (key_code < 0) + break; + + if (!sparse_keymap_report_event(hp_wmi_input_dev, + key_code, 1, true)) + pr_info("Unknown key code - 0x%x\n", key_code); + break; + case HPWMI_FN_P_HOTKEY: + platform_profile_cycle(); + break; + case HPWMI_OMEN_KEY: + if (event_data) /* Only should be true for HP Omen */ + key_code = event_data; + else + key_code = hp_wmi_read_int(HPWMI_HOTKEY_QUERY); + + if (!sparse_keymap_report_event(hp_wmi_input_dev, + key_code, 1, true)) + pr_info("Unknown key code - 0x%x\n", key_code); + break; + case HPWMI_WIRELESS: + if (rfkill2_count) { + hp_wmi_rfkill2_refresh(); + break; + } + + if (wifi_rfkill) + rfkill_set_states(wifi_rfkill, + hp_wmi_get_sw_state(HPWMI_WIFI), + hp_wmi_get_hw_state(HPWMI_WIFI)); + if (bluetooth_rfkill) + rfkill_set_states(bluetooth_rfkill, + hp_wmi_get_sw_state(HPWMI_BLUETOOTH), + hp_wmi_get_hw_state(HPWMI_BLUETOOTH)); + if (wwan_rfkill) + rfkill_set_states(wwan_rfkill, + hp_wmi_get_sw_state(HPWMI_WWAN), + hp_wmi_get_hw_state(HPWMI_WWAN)); + break; + case HPWMI_CPU_BATTERY_THROTTLE: + pr_info("Unimplemented CPU throttle because of 3 Cell battery event detected\n"); + break; + case HPWMI_LOCK_SWITCH: + break; + case HPWMI_LID_SWITCH: + break; + case HPWMI_SCREEN_ROTATION: + break; + case HPWMI_COOLSENSE_SYSTEM_MOBILE: + break; + case HPWMI_COOLSENSE_SYSTEM_HOT: + break; + case HPWMI_PROXIMITY_SENSOR: + break; + case HPWMI_BACKLIT_KB_BRIGHTNESS: + break; + case HPWMI_PEAKSHIFT_PERIOD: + break; + case HPWMI_BATTERY_CHARGE_PERIOD: + break; + case HPWMI_SANITIZATION_MODE: + break; + case HPWMI_CAMERA_TOGGLE: + if (!camera_shutter_input_dev) + if (camera_shutter_input_setup()) { + pr_err("Failed to setup camera shutter input device\n"); + break; + } + if (event_data == 0xff) + input_report_switch(camera_shutter_input_dev, SW_CAMERA_LENS_COVER, 1); + else if (event_data == 0xfe) + input_report_switch(camera_shutter_input_dev, SW_CAMERA_LENS_COVER, 0); + else + pr_warn("Unknown camera shutter state - 0x%x\n", event_data); + input_sync(camera_shutter_input_dev); + break; + case HPWMI_SMART_EXPERIENCE_APP: + break; + default: + pr_info("Unknown event_id - %d - 0x%x\n", event_id, event_data); + break; + } +} + +static int __init hp_wmi_input_setup(void) +{ + acpi_status status; + int err, val; + + hp_wmi_input_dev = input_allocate_device(); + if (!hp_wmi_input_dev) + return -ENOMEM; + + hp_wmi_input_dev->name = "HP WMI hotkeys"; + hp_wmi_input_dev->phys = "wmi/input0"; + hp_wmi_input_dev->id.bustype = BUS_HOST; + + __set_bit(EV_SW, hp_wmi_input_dev->evbit); + + /* Dock */ + val = hp_wmi_get_dock_state(); + if (!(val < 0)) { + __set_bit(SW_DOCK, hp_wmi_input_dev->swbit); + input_report_switch(hp_wmi_input_dev, SW_DOCK, val); + } + + /* Tablet mode */ + val = hp_wmi_get_tablet_mode(); + if (!(val < 0)) { + __set_bit(SW_TABLET_MODE, hp_wmi_input_dev->swbit); + input_report_switch(hp_wmi_input_dev, SW_TABLET_MODE, val); + } + + err = sparse_keymap_setup(hp_wmi_input_dev, hp_wmi_keymap, NULL); + if (err) + goto err_free_dev; + + /* Set initial hardware state */ + input_sync(hp_wmi_input_dev); + + if (!hp_wmi_bios_2009_later() && hp_wmi_bios_2008_later()) + hp_wmi_enable_hotkeys(); + + status = wmi_install_notify_handler(HPWMI_EVENT_GUID, hp_wmi_notify, NULL); + if (ACPI_FAILURE(status)) { + err = -EIO; + goto err_free_dev; + } + + err = input_register_device(hp_wmi_input_dev); + if (err) + goto err_uninstall_notifier; + + return 0; + + err_uninstall_notifier: + wmi_remove_notify_handler(HPWMI_EVENT_GUID); + err_free_dev: + input_free_device(hp_wmi_input_dev); + return err; +} + +static void hp_wmi_input_destroy(void) +{ + wmi_remove_notify_handler(HPWMI_EVENT_GUID); + input_unregister_device(hp_wmi_input_dev); +} + +static int __init hp_wmi_rfkill_setup(struct platform_device *device) +{ + int err, wireless; + + wireless = hp_wmi_read_int(HPWMI_WIRELESS_QUERY); + if (wireless < 0) + return wireless; + + err = hp_wmi_perform_query(HPWMI_WIRELESS_QUERY, HPWMI_WRITE, &wireless, + sizeof(wireless), 0); + if (err) + return err; + + if (wireless & 0x1) { + wifi_rfkill = rfkill_alloc("hp-wifi", &device->dev, + RFKILL_TYPE_WLAN, + &hp_wmi_rfkill_ops, + (void *) HPWMI_WIFI); + if (!wifi_rfkill) + return -ENOMEM; + rfkill_init_sw_state(wifi_rfkill, + hp_wmi_get_sw_state(HPWMI_WIFI)); + rfkill_set_hw_state(wifi_rfkill, + hp_wmi_get_hw_state(HPWMI_WIFI)); + err = rfkill_register(wifi_rfkill); + if (err) + goto register_wifi_error; + } + + if (wireless & 0x2) { + bluetooth_rfkill = rfkill_alloc("hp-bluetooth", &device->dev, + RFKILL_TYPE_BLUETOOTH, + &hp_wmi_rfkill_ops, + (void *) HPWMI_BLUETOOTH); + if (!bluetooth_rfkill) { + err = -ENOMEM; + goto register_bluetooth_error; + } + rfkill_init_sw_state(bluetooth_rfkill, + hp_wmi_get_sw_state(HPWMI_BLUETOOTH)); + rfkill_set_hw_state(bluetooth_rfkill, + hp_wmi_get_hw_state(HPWMI_BLUETOOTH)); + err = rfkill_register(bluetooth_rfkill); + if (err) + goto register_bluetooth_error; + } + + if (wireless & 0x4) { + wwan_rfkill = rfkill_alloc("hp-wwan", &device->dev, + RFKILL_TYPE_WWAN, + &hp_wmi_rfkill_ops, + (void *) HPWMI_WWAN); + if (!wwan_rfkill) { + err = -ENOMEM; + goto register_wwan_error; + } + rfkill_init_sw_state(wwan_rfkill, + hp_wmi_get_sw_state(HPWMI_WWAN)); + rfkill_set_hw_state(wwan_rfkill, + hp_wmi_get_hw_state(HPWMI_WWAN)); + err = rfkill_register(wwan_rfkill); + if (err) + goto register_wwan_error; + } + + return 0; + +register_wwan_error: + rfkill_destroy(wwan_rfkill); + wwan_rfkill = NULL; + if (bluetooth_rfkill) + rfkill_unregister(bluetooth_rfkill); +register_bluetooth_error: + rfkill_destroy(bluetooth_rfkill); + bluetooth_rfkill = NULL; + if (wifi_rfkill) + rfkill_unregister(wifi_rfkill); +register_wifi_error: + rfkill_destroy(wifi_rfkill); + wifi_rfkill = NULL; + return err; +} + +static int __init hp_wmi_rfkill2_setup(struct platform_device *device) +{ + struct bios_rfkill2_state state; + int err, i; + + err = hp_wmi_perform_query(HPWMI_WIRELESS2_QUERY, HPWMI_READ, &state, + zero_if_sup(state), sizeof(state)); + if (err) + return err < 0 ? err : -EINVAL; + + if (state.count > HPWMI_MAX_RFKILL2_DEVICES) { + pr_warn("unable to parse 0x1b query output\n"); + return -EINVAL; + } + + for (i = 0; i < state.count; i++) { + struct rfkill *rfkill; + enum rfkill_type type; + char *name; + + switch (state.device[i].radio_type) { + case HPWMI_WIFI: + type = RFKILL_TYPE_WLAN; + name = "hp-wifi"; + break; + case HPWMI_BLUETOOTH: + type = RFKILL_TYPE_BLUETOOTH; + name = "hp-bluetooth"; + break; + case HPWMI_WWAN: + type = RFKILL_TYPE_WWAN; + name = "hp-wwan"; + break; + case HPWMI_GPS: + type = RFKILL_TYPE_GPS; + name = "hp-gps"; + break; + default: + pr_warn("unknown device type 0x%x\n", + state.device[i].radio_type); + continue; + } + + if (!state.device[i].vendor_id) { + pr_warn("zero device %d while %d reported\n", + i, state.count); + continue; + } + + rfkill = rfkill_alloc(name, &device->dev, type, + &hp_wmi_rfkill2_ops, (void *)(long)i); + if (!rfkill) { + err = -ENOMEM; + goto fail; + } + + rfkill2[rfkill2_count].id = state.device[i].rfkill_id; + rfkill2[rfkill2_count].num = i; + rfkill2[rfkill2_count].rfkill = rfkill; + + rfkill_init_sw_state(rfkill, + IS_SWBLOCKED(state.device[i].power)); + rfkill_set_hw_state(rfkill, + IS_HWBLOCKED(state.device[i].power)); + + if (!(state.device[i].power & HPWMI_POWER_BIOS)) + pr_info("device %s blocked by BIOS\n", name); + + err = rfkill_register(rfkill); + if (err) { + rfkill_destroy(rfkill); + goto fail; + } + + rfkill2_count++; + } + + return 0; +fail: + for (; rfkill2_count > 0; rfkill2_count--) { + rfkill_unregister(rfkill2[rfkill2_count - 1].rfkill); + rfkill_destroy(rfkill2[rfkill2_count - 1].rfkill); + } + return err; +} + +static int platform_profile_omen_get_ec(enum platform_profile_option *profile) +{ + int tp; + + tp = omen_thermal_profile_get(); + if (tp < 0) + return tp; + + switch (tp) { + case HP_OMEN_V0_THERMAL_PROFILE_PERFORMANCE: + case HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case HP_OMEN_V0_THERMAL_PROFILE_DEFAULT: + case HP_OMEN_V1_THERMAL_PROFILE_DEFAULT: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case HP_OMEN_V0_THERMAL_PROFILE_COOL: + case HP_OMEN_V1_THERMAL_PROFILE_COOL: + *profile = PLATFORM_PROFILE_COOL; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int platform_profile_omen_get(struct device *dev, + enum platform_profile_option *profile) +{ + /* + * We directly return the stored platform profile, as the embedded + * controller will not accept switching to the performance option when + * the conditions are not met (e.g. the laptop is not plugged in). + * + * If we directly return what the EC reports, the platform profile will + * immediately "switch back" to normal mode, which is against the + * expected behaviour from a userspace point of view, as described in + * the Platform Profile Section page of the kernel documentation. + * + * See also omen_powersource_event. + */ + guard(mutex)(&active_platform_profile_lock); + *profile = active_platform_profile; + + return 0; +} + +static bool has_omen_thermal_profile_ec_timer(void) +{ + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board_name) + return false; + + return match_string(omen_timed_thermal_profile_boards, + ARRAY_SIZE(omen_timed_thermal_profile_boards), + board_name) >= 0; +} + +inline int omen_thermal_profile_ec_flags_set(enum hp_thermal_profile_omen_flags flags) +{ + return ec_write(HP_OMEN_EC_THERMAL_PROFILE_FLAGS_OFFSET, flags); +} + +inline int omen_thermal_profile_ec_timer_set(u8 value) +{ + return ec_write(HP_OMEN_EC_THERMAL_PROFILE_TIMER_OFFSET, value); +} + +static int platform_profile_omen_set_ec(enum platform_profile_option profile) +{ + int err, tp, tp_version; + enum hp_thermal_profile_omen_flags flags = 0; + + tp_version = omen_get_thermal_policy_version(); + + if (tp_version < 0 || tp_version > 1) + return -EOPNOTSUPP; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + if (tp_version == 0) + tp = HP_OMEN_V0_THERMAL_PROFILE_PERFORMANCE; + else + tp = HP_OMEN_V1_THERMAL_PROFILE_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + if (tp_version == 0) + tp = HP_OMEN_V0_THERMAL_PROFILE_DEFAULT; + else + tp = HP_OMEN_V1_THERMAL_PROFILE_DEFAULT; + break; + case PLATFORM_PROFILE_COOL: + if (tp_version == 0) + tp = HP_OMEN_V0_THERMAL_PROFILE_COOL; + else + tp = HP_OMEN_V1_THERMAL_PROFILE_COOL; + break; + default: + return -EOPNOTSUPP; + } + + err = omen_thermal_profile_set(tp); + if (err < 0) + return err; + + if (has_omen_thermal_profile_ec_timer()) { + err = omen_thermal_profile_ec_timer_set(0); + if (err < 0) + return err; + + if (profile == PLATFORM_PROFILE_PERFORMANCE) + flags = HP_OMEN_EC_FLAGS_NOTIMER | + HP_OMEN_EC_FLAGS_TURBO; + + err = omen_thermal_profile_ec_flags_set(flags); + if (err < 0) + return err; + } + + return 0; +} + +static int platform_profile_omen_set(struct device *dev, + enum platform_profile_option profile) +{ + int err; + + guard(mutex)(&active_platform_profile_lock); + + err = platform_profile_omen_set_ec(profile); + if (err < 0) + return err; + + active_platform_profile = profile; + + return 0; +} + +static int thermal_profile_get(void) +{ + return hp_wmi_read_int(HPWMI_THERMAL_PROFILE_QUERY); +} + +static int thermal_profile_set(int thermal_profile) +{ + return hp_wmi_perform_query(HPWMI_THERMAL_PROFILE_QUERY, HPWMI_WRITE, &thermal_profile, + sizeof(thermal_profile), 0); +} + +static int hp_wmi_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + int tp; + + tp = thermal_profile_get(); + if (tp < 0) + return tp; + + switch (tp) { + case HP_THERMAL_PROFILE_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case HP_THERMAL_PROFILE_DEFAULT: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case HP_THERMAL_PROFILE_COOL: + *profile = PLATFORM_PROFILE_COOL; + break; + case HP_THERMAL_PROFILE_QUIET: + *profile = PLATFORM_PROFILE_QUIET; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int hp_wmi_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + int err, tp; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + tp = HP_THERMAL_PROFILE_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + tp = HP_THERMAL_PROFILE_DEFAULT; + break; + case PLATFORM_PROFILE_COOL: + tp = HP_THERMAL_PROFILE_COOL; + break; + case PLATFORM_PROFILE_QUIET: + tp = HP_THERMAL_PROFILE_QUIET; + break; + default: + return -EOPNOTSUPP; + } + + err = thermal_profile_set(tp); + if (err) + return err; + + return 0; +} + +static bool is_victus_thermal_profile(void) +{ + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board_name) + return false; + + return match_string(victus_thermal_profile_boards, + ARRAY_SIZE(victus_thermal_profile_boards), + board_name) >= 0; +} + +static int platform_profile_victus_get_ec(enum platform_profile_option *profile) +{ + int tp; + + tp = omen_thermal_profile_get(); + if (tp < 0) + return tp; + + switch (tp) { + case HP_VICTUS_THERMAL_PROFILE_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case HP_VICTUS_THERMAL_PROFILE_DEFAULT: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case HP_VICTUS_THERMAL_PROFILE_QUIET: + *profile = PLATFORM_PROFILE_QUIET; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int platform_profile_victus_get(struct device *dev, + enum platform_profile_option *profile) +{ + /* Same behaviour as platform_profile_omen_get */ + return platform_profile_omen_get(dev, profile); +} + +static int platform_profile_victus_set_ec(enum platform_profile_option profile) +{ + int err, tp; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + tp = HP_VICTUS_THERMAL_PROFILE_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + tp = HP_VICTUS_THERMAL_PROFILE_DEFAULT; + break; + case PLATFORM_PROFILE_QUIET: + tp = HP_VICTUS_THERMAL_PROFILE_QUIET; + break; + default: + return -EOPNOTSUPP; + } + + err = omen_thermal_profile_set(tp); + if (err < 0) + return err; + + return 0; +} + +static bool is_victus_s_thermal_profile(void) +{ + /* Initialised in driver init, hence safe to use here */ + return is_victus_s_board; +} + +static int victus_s_gpu_thermal_profile_get(bool *ctgp_enable, + bool *ppab_enable, + u8 *dstate, + u8 *gpu_slowdown_temp) +{ + struct victus_gpu_power_modes gpu_power_modes; + int ret; + + ret = hp_wmi_perform_query(HPWMI_GET_GPU_THERMAL_MODES_QUERY, HPWMI_GM, + &gpu_power_modes, sizeof(gpu_power_modes), + sizeof(gpu_power_modes)); + if (ret == 0) { + *ctgp_enable = gpu_power_modes.ctgp_enable ? true : false; + *ppab_enable = gpu_power_modes.ppab_enable ? true : false; + *dstate = gpu_power_modes.dstate; + *gpu_slowdown_temp = gpu_power_modes.gpu_slowdown_temp; + } + + return ret; +} + +static int victus_s_gpu_thermal_profile_set(bool ctgp_enable, + bool ppab_enable, + u8 dstate) +{ + struct victus_gpu_power_modes gpu_power_modes; + int ret; + + bool current_ctgp_state, current_ppab_state; + u8 current_dstate, current_gpu_slowdown_temp; + + /* Retrieving GPU slowdown temperature, in order to keep it unchanged */ + ret = victus_s_gpu_thermal_profile_get(¤t_ctgp_state, + ¤t_ppab_state, + ¤t_dstate, + ¤t_gpu_slowdown_temp); + if (ret < 0) { + pr_warn("GPU modes not updated, unable to get slowdown temp\n"); + return ret; + } + + gpu_power_modes.ctgp_enable = ctgp_enable ? 0x01 : 0x00; + gpu_power_modes.ppab_enable = ppab_enable ? 0x01 : 0x00; + gpu_power_modes.dstate = dstate; + gpu_power_modes.gpu_slowdown_temp = current_gpu_slowdown_temp; + + + ret = hp_wmi_perform_query(HPWMI_SET_GPU_THERMAL_MODES_QUERY, HPWMI_GM, + &gpu_power_modes, sizeof(gpu_power_modes), 0); + + return ret; +} + +/* Note: HP_POWER_LIMIT_DEFAULT can be used to restore default PL1 and PL2 */ +static int victus_s_set_cpu_pl1_pl2(u8 pl1, u8 pl2) +{ + struct victus_power_limits power_limits; + int ret; + + /* We need to know both PL1 and PL2 values in order to check them */ + if (pl1 == HP_POWER_LIMIT_NO_CHANGE || pl2 == HP_POWER_LIMIT_NO_CHANGE) + return -EINVAL; + + /* PL2 is not supposed to be lower than PL1 */ + if (pl2 < pl1) + return -EINVAL; + + power_limits.pl1 = pl1; + power_limits.pl2 = pl2; + power_limits.pl4 = HP_POWER_LIMIT_NO_CHANGE; + power_limits.cpu_gpu_concurrent_limit = HP_POWER_LIMIT_NO_CHANGE; + + ret = hp_wmi_perform_query(HPWMI_SET_POWER_LIMITS_QUERY, HPWMI_GM, + &power_limits, sizeof(power_limits), 0); + + return ret; +} + +static int platform_profile_victus_s_get_ec(enum platform_profile_option *profile) +{ + int ret = 0; + bool current_ctgp_state, current_ppab_state; + u8 current_dstate, current_gpu_slowdown_temp, tp; + const struct thermal_profile_params *params; + + params = active_thermal_profile_params; + if (params->ec_tp_offset == HP_EC_OFFSET_UNKNOWN || + params->ec_tp_offset == HP_NO_THERMAL_PROFILE_OFFSET) { + *profile = active_platform_profile; + return 0; + } + + ret = ec_read(params->ec_tp_offset, &tp); + if (ret) + return ret; + + /* + * We cannot use active_thermal_profile_params here, because boards + * like 8C78 have tp == 0x0 || tp == 0x1 after cold boot, but logically + * it should have tp == 0x30 || tp == 0x31, as corrected by the Omen + * Gaming Hub on windows. Hence accept both of these values. + */ + if (tp == victus_s_thermal_params.performance || + tp == omen_v1_thermal_params.performance) { + *profile = PLATFORM_PROFILE_PERFORMANCE; + } else if (tp == victus_s_thermal_params.balanced || + tp == omen_v1_thermal_params.balanced) { + /* + * Since both PLATFORM_PROFILE_LOW_POWER and + * PLATFORM_PROFILE_BALANCED share the same thermal profile + * parameter value, hence to differentiate between them, we + * query the GPU CTGP and PPAB states and compare based off of + * that. + */ + ret = victus_s_gpu_thermal_profile_get(¤t_ctgp_state, + ¤t_ppab_state, + ¤t_dstate, + ¤t_gpu_slowdown_temp); + if (ret < 0) + return ret; + if (current_ctgp_state == 0 && current_ppab_state == 0) + *profile = PLATFORM_PROFILE_LOW_POWER; + else if (current_ctgp_state == 0 && current_ppab_state == 1) + *profile = PLATFORM_PROFILE_BALANCED; + else + return -EINVAL; + } else { + return -EINVAL; + } + + return 0; +} + +static int platform_profile_victus_s_set_ec(enum platform_profile_option profile) +{ + struct thermal_profile_params *params; + bool gpu_ctgp_enable, gpu_ppab_enable; + u8 gpu_dstate; /* Test shows 1 = 100%, 2 = 50%, 3 = 25%, 4 = 12.5% */ + int err, tp; + + params = active_thermal_profile_params; + if (!params) + return -ENODEV; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + tp = params->performance; + gpu_ctgp_enable = true; + gpu_ppab_enable = true; + gpu_dstate = 1; + break; + case PLATFORM_PROFILE_BALANCED: + tp = params->balanced; + gpu_ctgp_enable = false; + gpu_ppab_enable = true; + gpu_dstate = 1; + break; + case PLATFORM_PROFILE_LOW_POWER: + tp = params->low_power; + gpu_ctgp_enable = false; + gpu_ppab_enable = false; + gpu_dstate = 1; + break; + default: + return -EOPNOTSUPP; + } + + hp_wmi_get_fan_count_userdefine_trigger(); + + err = omen_thermal_profile_set(tp); + if (err < 0) { + pr_err("Failed to set platform profile %d: %d\n", profile, err); + return err; + } + + err = victus_s_gpu_thermal_profile_set(gpu_ctgp_enable, + gpu_ppab_enable, + gpu_dstate); + if (err < 0) { + pr_err("Failed to set GPU profile %d: %d\n", profile, err); + return err; + } + + return 0; +} + +static int platform_profile_victus_s_set(struct device *dev, + enum platform_profile_option profile) +{ + int err; + + guard(mutex)(&active_platform_profile_lock); + + err = platform_profile_victus_s_set_ec(profile); + if (err < 0) + return err; + + active_platform_profile = profile; + + return 0; +} + +static int platform_profile_victus_set(struct device *dev, + enum platform_profile_option profile) +{ + int err; + + guard(mutex)(&active_platform_profile_lock); + + err = platform_profile_victus_set_ec(profile); + if (err < 0) + return err; + + active_platform_profile = profile; + + return 0; +} + +static int hp_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + if (is_omen_thermal_profile()) { + set_bit(PLATFORM_PROFILE_COOL, choices); + } else if (is_victus_thermal_profile()) { + set_bit(PLATFORM_PROFILE_QUIET, choices); + } else if (is_victus_s_thermal_profile()) { + /* Adding an equivalent to HP Omen software ECO mode: */ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + } else { + set_bit(PLATFORM_PROFILE_QUIET, choices); + set_bit(PLATFORM_PROFILE_COOL, choices); + } + + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + + return 0; +} + +static int omen_powersource_event(struct notifier_block *nb, + unsigned long value, + void *data) +{ + struct acpi_bus_event *event_entry = data; + enum platform_profile_option actual_profile; + int err; + + if (strcmp(event_entry->device_class, ACPI_AC_CLASS) != 0) + return NOTIFY_DONE; + + pr_debug("Received power source device event\n"); + + guard(mutex)(&active_platform_profile_lock); + + /* + * This handler can only be called on Omen and Victus models, so + * there's no need to call is_victus_thermal_profile() here. + */ + if (is_omen_thermal_profile()) + err = platform_profile_omen_get_ec(&actual_profile); + else + err = platform_profile_victus_get_ec(&actual_profile); + + if (err < 0) { + /* + * Although we failed to get the current platform profile, we + * still want the other event consumers to process it. + */ + pr_warn("Failed to read current platform profile (%d)\n", err); + return NOTIFY_DONE; + } + + /* + * If we're back on AC and that the user-chosen power profile is + * different from what the EC reports, we restore the user-chosen + * one. + */ + if (power_supply_is_system_supplied() <= 0 || + active_platform_profile == actual_profile) { + pr_debug("Platform profile update skipped, conditions unmet\n"); + return NOTIFY_DONE; + } + + if (is_omen_thermal_profile()) + err = platform_profile_omen_set_ec(active_platform_profile); + else + err = platform_profile_victus_set_ec(active_platform_profile); + + if (err < 0) { + pr_warn("Failed to restore platform profile (%d)\n", err); + return NOTIFY_DONE; + } + + return NOTIFY_OK; +} + +static int victus_s_powersource_event(struct notifier_block *nb, + unsigned long value, + void *data) +{ + struct acpi_bus_event *event_entry = data; + enum platform_profile_option actual_profile; + int err; + + if (strcmp(event_entry->device_class, ACPI_AC_CLASS) != 0) + return NOTIFY_DONE; + + pr_debug("Received power source device event\n"); + + guard(mutex)(&active_platform_profile_lock); + err = platform_profile_victus_s_get_ec(&actual_profile); + if (err < 0) { + /* + * Although we failed to get the current platform profile, we + * still want the other event consumers to process it. + */ + pr_warn("Failed to read current platform profile (%d)\n", err); + return NOTIFY_DONE; + } + + /* + * Switching to battery power source while Performance mode is active + * needs manual triggering of CPU power limits. Same goes when switching + * to AC power source while Performance mode is active. Other modes + * however are automatically behaving without any manual action. + * Seen on HP 16-s1034nf (board 8C9C) with F.11 and F.13 BIOS versions. + */ + + if (actual_profile == PLATFORM_PROFILE_PERFORMANCE) { + pr_debug("Triggering CPU PL1/PL2 actualization\n"); + err = victus_s_set_cpu_pl1_pl2(HP_POWER_LIMIT_DEFAULT, + HP_POWER_LIMIT_DEFAULT); + if (err) + pr_warn("Failed to actualize power limits: %d\n", err); + + return NOTIFY_DONE; + } + + return NOTIFY_OK; +} + +static int omen_register_powersource_event_handler(void) +{ + int err; + + platform_power_source_nb.notifier_call = omen_powersource_event; + err = register_acpi_notifier(&platform_power_source_nb); + + if (err < 0) { + pr_warn("Failed to install ACPI power source notify handler\n"); + return err; + } + + return 0; +} + +static int victus_s_register_powersource_event_handler(void) +{ + int err; + + platform_power_source_nb.notifier_call = victus_s_powersource_event; + err = register_acpi_notifier(&platform_power_source_nb); + if (err < 0) { + pr_warn("Failed to install ACPI power source notify handler\n"); + return err; + } + + return 0; +} + +static inline void omen_unregister_powersource_event_handler(void) +{ + unregister_acpi_notifier(&platform_power_source_nb); +} + +static inline void victus_s_unregister_powersource_event_handler(void) +{ + unregister_acpi_notifier(&platform_power_source_nb); +} + +static const struct platform_profile_ops platform_profile_omen_ops = { + .probe = hp_wmi_platform_profile_probe, + .profile_get = platform_profile_omen_get, + .profile_set = platform_profile_omen_set, +}; + +static const struct platform_profile_ops platform_profile_victus_ops = { + .probe = hp_wmi_platform_profile_probe, + .profile_get = platform_profile_victus_get, + .profile_set = platform_profile_victus_set, +}; + +static const struct platform_profile_ops platform_profile_victus_s_ops = { + .probe = hp_wmi_platform_profile_probe, + .profile_get = platform_profile_omen_get, + .profile_set = platform_profile_victus_s_set, +}; + +static const struct platform_profile_ops hp_wmi_platform_profile_ops = { + .probe = hp_wmi_platform_profile_probe, + .profile_get = hp_wmi_platform_profile_get, + .profile_set = hp_wmi_platform_profile_set, +}; + +static int thermal_profile_setup(struct platform_device *device) +{ + const struct platform_profile_ops *ops; + int err, tp; + + if (is_omen_thermal_profile()) { + err = platform_profile_omen_get_ec(&active_platform_profile); + if (err < 0) + return err; + + /* + * call thermal profile write command to ensure that the + * firmware correctly sets the OEM variables + */ + err = platform_profile_omen_set_ec(active_platform_profile); + if (err < 0) + return err; + + ops = &platform_profile_omen_ops; + } else if (is_victus_thermal_profile()) { + err = platform_profile_victus_get_ec(&active_platform_profile); + if (err < 0) + return err; + + /* + * call thermal profile write command to ensure that the + * firmware correctly sets the OEM variables + */ + err = platform_profile_victus_set_ec(active_platform_profile); + if (err < 0) + return err; + + ops = &platform_profile_victus_ops; + } else if (is_victus_s_thermal_profile()) { + /* + * For an unknown EC layout board, platform_profile_victus_s_get_ec(), + * behaves like a wrapper around active_platform_profile, to avoid using + * uninitialized data, we default to PLATFORM_PROFILE_BALANCED. + */ + if (active_thermal_profile_params->ec_tp_offset == HP_EC_OFFSET_UNKNOWN || + active_thermal_profile_params->ec_tp_offset == HP_NO_THERMAL_PROFILE_OFFSET) { + active_platform_profile = PLATFORM_PROFILE_BALANCED; + } else { + err = platform_profile_victus_s_get_ec(&active_platform_profile); + if (err < 0) + return err; + } + + /* + * call thermal profile write command to ensure that the + * firmware correctly sets the OEM variables + */ + err = platform_profile_victus_s_set_ec(active_platform_profile); + if (err < 0) + return err; + + ops = &platform_profile_victus_s_ops; + } else { + tp = thermal_profile_get(); + + if (tp < 0) + return tp; + + /* + * call thermal profile write command to ensure that the + * firmware correctly sets the OEM variables for the DPTF + */ + err = thermal_profile_set(tp); + if (err) + return err; + + ops = &hp_wmi_platform_profile_ops; + } + + platform_profile_device = devm_platform_profile_register(&device->dev, "hp-wmi", + NULL, ops); + if (IS_ERR(platform_profile_device)) + return PTR_ERR(platform_profile_device); + + pr_info("Registered as platform profile handler\n"); + platform_profile_support = true; + + return 0; +} + +static int hp_wmi_hwmon_init(void); + +static int __init hp_wmi_bios_setup(struct platform_device *device) +{ + int err; + /* clear detected rfkill devices */ + wifi_rfkill = NULL; + bluetooth_rfkill = NULL; + wwan_rfkill = NULL; + rfkill2_count = 0; + + /* + * In pre-2009 BIOS, command 1Bh return 0x4 to indicate that + * BIOS no longer controls the power for the wireless + * devices. All features supported by this command will no + * longer be supported. + */ + if (!hp_wmi_bios_2009_later()) { + if (hp_wmi_rfkill_setup(device)) + hp_wmi_rfkill2_setup(device); + } + + err = hp_wmi_hwmon_init(); + + if (err < 0) + return err; + + thermal_profile_setup(device); + + return 0; +} + +static void __exit hp_wmi_bios_remove(struct platform_device *device) +{ + int i; + struct hp_wmi_hwmon_priv *priv; + + for (i = 0; i < rfkill2_count; i++) { + rfkill_unregister(rfkill2[i].rfkill); + rfkill_destroy(rfkill2[i].rfkill); + } + + if (wifi_rfkill) { + rfkill_unregister(wifi_rfkill); + rfkill_destroy(wifi_rfkill); + } + if (bluetooth_rfkill) { + rfkill_unregister(bluetooth_rfkill); + rfkill_destroy(bluetooth_rfkill); + } + if (wwan_rfkill) { + rfkill_unregister(wwan_rfkill); + rfkill_destroy(wwan_rfkill); + } + + priv = platform_get_drvdata(device); + if (priv) + cancel_delayed_work_sync(&priv->keep_alive_dwork); +} + +static int hp_wmi_resume_handler(struct device *device) +{ + /* + * Hardware state may have changed while suspended, so trigger + * input events for the current state. As this is a switch, + * the input layer will only actually pass it on if the state + * changed. + */ + if (hp_wmi_input_dev) { + if (test_bit(SW_DOCK, hp_wmi_input_dev->swbit)) + input_report_switch(hp_wmi_input_dev, SW_DOCK, + hp_wmi_get_dock_state()); + if (test_bit(SW_TABLET_MODE, hp_wmi_input_dev->swbit)) + input_report_switch(hp_wmi_input_dev, SW_TABLET_MODE, + hp_wmi_get_tablet_mode()); + input_sync(hp_wmi_input_dev); + } + + if (rfkill2_count) + hp_wmi_rfkill2_refresh(); + + if (wifi_rfkill) + rfkill_set_states(wifi_rfkill, + hp_wmi_get_sw_state(HPWMI_WIFI), + hp_wmi_get_hw_state(HPWMI_WIFI)); + if (bluetooth_rfkill) + rfkill_set_states(bluetooth_rfkill, + hp_wmi_get_sw_state(HPWMI_BLUETOOTH), + hp_wmi_get_hw_state(HPWMI_BLUETOOTH)); + if (wwan_rfkill) + rfkill_set_states(wwan_rfkill, + hp_wmi_get_sw_state(HPWMI_WWAN), + hp_wmi_get_hw_state(HPWMI_WWAN)); + + return 0; +} + +static const struct dev_pm_ops hp_wmi_pm_ops = { + .resume = hp_wmi_resume_handler, + .restore = hp_wmi_resume_handler, +}; + +/* + * hp_wmi_bios_remove() lives in .exit.text. For drivers registered via + * module_platform_driver_probe() this is ok because they cannot get unbound at + * runtime. So mark the driver struct with __refdata to prevent modpost + * triggering a section mismatch warning. + */ +static struct platform_driver hp_wmi_driver __refdata = { + .driver = { + .name = "hp-wmi", + .pm = &hp_wmi_pm_ops, + .dev_groups = hp_wmi_groups, + }, + .remove = __exit_p(hp_wmi_bios_remove), +}; + +static int hp_wmi_apply_fan_settings(struct hp_wmi_hwmon_priv *priv) +{ + int ret; + + switch (priv->mode) { + case PWM_MODE_MAX: + if (is_victus_s_thermal_profile()) { + ret = hp_wmi_get_fan_count_userdefine_trigger(); + if (ret < 0) + return ret; + } + ret = hp_wmi_fan_speed_max_set(1); + if (ret < 0) + return ret; + mod_delayed_work(system_dfl_wq, &priv->keep_alive_dwork, + secs_to_jiffies(KEEP_ALIVE_DELAY_SECS)); + return 0; + case PWM_MODE_MANUAL: + if (!is_victus_s_thermal_profile()) + return -EOPNOTSUPP; + ret = hp_wmi_fan_speed_set(priv, pwm_to_rpm(priv->pwm, priv)); + if (ret < 0) + return ret; + mod_delayed_work(system_dfl_wq, &priv->keep_alive_dwork, + secs_to_jiffies(KEEP_ALIVE_DELAY_SECS)); + return 0; + case PWM_MODE_AUTO: + if (is_victus_s_thermal_profile()) { + ret = hp_wmi_get_fan_count_userdefine_trigger(); + if (ret < 0) + return ret; + ret = hp_wmi_fan_speed_max_reset(priv); + } else { + ret = hp_wmi_fan_speed_max_set(0); + } + if (ret < 0) + return ret; + cancel_delayed_work(&priv->keep_alive_dwork); + return 0; + default: + /* shouldn't happen */ + return -EINVAL; + } +} + +static umode_t hp_wmi_hwmon_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + switch (type) { + case hwmon_pwm: + if (attr == hwmon_pwm_input && !is_victus_s_thermal_profile()) + return 0; + return 0644; + case hwmon_fan: + if (is_victus_s_thermal_profile()) { + if (hp_wmi_get_fan_speed_victus_s(channel) >= 0) + return 0444; + } else { + if (hp_wmi_get_fan_speed(channel) >= 0) + return 0444; + } + break; + default: + return 0; + } + + return 0; +} + +static int hp_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct hp_wmi_hwmon_priv *priv; + int rpm, ret; + u8 mode; + + priv = dev_get_drvdata(dev); + switch (type) { + case hwmon_fan: + if (is_victus_s_thermal_profile()) + ret = hp_wmi_get_fan_speed_victus_s(channel); + else + ret = hp_wmi_get_fan_speed(channel); + if (ret < 0) + return ret; + *val = ret; + return 0; + case hwmon_pwm: + if (attr == hwmon_pwm_input) { + if (!is_victus_s_thermal_profile()) + return -EOPNOTSUPP; + + rpm = hp_wmi_get_fan_speed_victus_s(channel); + if (rpm < 0) + return rpm; + *val = rpm_to_pwm(rpm / 100, priv); + return 0; + } + scoped_guard(mutex, &priv->lock) + mode = priv->mode; + switch (mode) { + case PWM_MODE_MAX: + case PWM_MODE_MANUAL: + case PWM_MODE_AUTO: + *val = mode; + return 0; + default: + /* shouldn't happen */ + return -ENODATA; + } + default: + return -EINVAL; + } +} + +static int hp_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct hp_wmi_hwmon_priv *priv; + int rpm; + + priv = dev_get_drvdata(dev); + guard(mutex)(&priv->lock); + switch (type) { + case hwmon_pwm: + if (attr == hwmon_pwm_input) { + if (!is_victus_s_thermal_profile()) + return -EOPNOTSUPP; + /* PWM input is invalid when not in manual mode */ + if (priv->mode != PWM_MODE_MANUAL) + return -EINVAL; + + /* ensure PWM input is within valid fan speeds */ + rpm = pwm_to_rpm(val, priv); + rpm = clamp_val(rpm, priv->min_rpm, priv->max_rpm); + priv->pwm = rpm_to_pwm(rpm, priv); + return hp_wmi_apply_fan_settings(priv); + } + switch (val) { + case PWM_MODE_MAX: + priv->mode = PWM_MODE_MAX; + return hp_wmi_apply_fan_settings(priv); + case PWM_MODE_MANUAL: + if (!is_victus_s_thermal_profile()) + return -EOPNOTSUPP; + /* + * When switching to manual mode, set fan speed to + * current RPM values to ensure a smooth transition. + */ + rpm = hp_wmi_get_fan_speed_victus_s(channel); + if (rpm < 0) + return rpm; + priv->pwm = rpm_to_pwm(rpm / 100, priv); + priv->mode = PWM_MODE_MANUAL; + return hp_wmi_apply_fan_settings(priv); + case PWM_MODE_AUTO: + priv->mode = PWM_MODE_AUTO; + return hp_wmi_apply_fan_settings(priv); + default: + return -EINVAL; + } + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_channel_info * const info[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE | HWMON_PWM_INPUT), + NULL +}; + +static const struct hwmon_ops ops = { + .is_visible = hp_wmi_hwmon_is_visible, + .read = hp_wmi_hwmon_read, + .write = hp_wmi_hwmon_write, +}; + +static const struct hwmon_chip_info chip_info = { + .ops = &ops, + .info = info, +}; + +static void hp_wmi_hwmon_keep_alive_handler(struct work_struct *work) +{ + struct delayed_work *dwork; + struct hp_wmi_hwmon_priv *priv; + int ret; + + dwork = to_delayed_work(work); + priv = container_of(dwork, struct hp_wmi_hwmon_priv, keep_alive_dwork); + + guard(mutex)(&priv->lock); + /* + * Re-apply the current hwmon context settings. + * NOTE: hp_wmi_apply_fan_settings will handle the re-scheduling. + */ + ret = hp_wmi_apply_fan_settings(priv); + if (ret) + pr_warn_ratelimited("keep-alive failed to refresh fan settings: %d\n", + ret); +} + +static int hp_wmi_setup_fan_settings(struct hp_wmi_hwmon_priv *priv) +{ + u8 fan_data[128] = { 0 }; + struct victus_s_fan_table *fan_table; + u8 min_rpm, max_rpm; + u8 cpu_rpm, gpu_rpm, noise_db; + int gpu_delta, i, num_entries, ret; + size_t header_size, entry_size; + + /* Default behaviour on hwmon init is automatic mode */ + priv->mode = PWM_MODE_AUTO; + + /* Bypass all non-Victus S devices */ + if (!is_victus_s_thermal_profile()) + return 0; + + ret = hp_wmi_perform_query(HPWMI_VICTUS_S_GET_FAN_TABLE_QUERY, + HPWMI_GM, &fan_data, 4, sizeof(fan_data)); + if (ret) + return ret; + + fan_table = (struct victus_s_fan_table *)fan_data; + if (fan_table->header.num_fans == 0) + return -EINVAL; + + header_size = sizeof(struct victus_s_fan_table_header); + entry_size = sizeof(struct victus_s_fan_table_entry); + num_entries = (sizeof(fan_data) - header_size) / entry_size; + min_rpm = U8_MAX; + max_rpm = 0; + + for (i = 0 ; i < num_entries ; i++) { + cpu_rpm = fan_table->entries[i].cpu_rpm; + gpu_rpm = fan_table->entries[i].gpu_rpm; + noise_db = fan_table->entries[i].noise_db; + + /* + * On some devices, the fan table is truncated with an all-zero row, + * hence we stop parsing here. + */ + if (cpu_rpm == 0 && gpu_rpm == 0 && noise_db == 0) + break; + + if (cpu_rpm < min_rpm) + min_rpm = cpu_rpm; + if (cpu_rpm > max_rpm) + max_rpm = cpu_rpm; + } + + if (min_rpm == U8_MAX || max_rpm == 0) + return -EINVAL; + + gpu_delta = fan_table->entries[0].gpu_rpm - fan_table->entries[0].cpu_rpm; + priv->min_rpm = min_rpm; + priv->max_rpm = max_rpm; + priv->gpu_delta = gpu_delta; + + return 0; +} + +static int hp_wmi_hwmon_init(void) +{ + struct device *dev = &hp_wmi_platform_dev->dev; + struct hp_wmi_hwmon_priv *priv; + struct device *hwmon; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + ret = devm_mutex_init(dev, &priv->lock); + if (ret) + return ret; + + ret = hp_wmi_setup_fan_settings(priv); + if (ret) + return ret; + hwmon = devm_hwmon_device_register_with_info(dev, "hp", priv, + &chip_info, NULL); + + if (IS_ERR(hwmon)) { + dev_err(dev, "Could not register hp hwmon device\n"); + return PTR_ERR(hwmon); + } + + INIT_DELAYED_WORK(&priv->keep_alive_dwork, hp_wmi_hwmon_keep_alive_handler); + platform_set_drvdata(hp_wmi_platform_dev, priv); + ret = hp_wmi_apply_fan_settings(priv); + if (ret) + dev_warn(dev, "Failed to apply initial fan settings: %d\n", ret); + + return 0; +} + +static void __init setup_active_thermal_profile_params(void) +{ + const struct dmi_system_id *id; + + /* + * Currently only victus_s devices use the + * active_thermal_profile_params + */ + id = dmi_first_match(victus_s_thermal_profile_boards); + if (id) { + /* + * Marking this boolean is required to ensure that + * is_victus_s_thermal_profile() behaves like a valid + * wrapper. + */ + is_victus_s_board = true; + active_thermal_profile_params = id->driver_data; + if (active_thermal_profile_params->ec_tp_offset == HP_EC_OFFSET_UNKNOWN) { + pr_warn("Unknown EC layout for board %s. Thermal profile readback will be disabled. Please report this to platform-driver-x86@vger.kernel.org\n", + dmi_get_system_info(DMI_BOARD_NAME)); + } + } +} + +static int __init hp_wmi_init(void) +{ + int event_capable = wmi_has_guid(HPWMI_EVENT_GUID); + int bios_capable = wmi_has_guid(HPWMI_BIOS_GUID); + int err, tmp = 0; + + if (!bios_capable && !event_capable) + return -ENODEV; + + if (hp_wmi_perform_query(HPWMI_HARDWARE_QUERY, HPWMI_READ, &tmp, + sizeof(tmp), sizeof(tmp)) == HPWMI_RET_INVALID_PARAMETERS) + zero_insize_support = true; + + if (event_capable) { + err = hp_wmi_input_setup(); + if (err) + return err; + } + + if (bios_capable) { + hp_wmi_platform_dev = + platform_device_register_simple("hp-wmi", PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(hp_wmi_platform_dev)) { + err = PTR_ERR(hp_wmi_platform_dev); + goto err_destroy_input; + } + + /* + * Setup active board's thermal profile parameters before + * starting platform driver probe. + */ + setup_active_thermal_profile_params(); + err = platform_driver_probe(&hp_wmi_driver, hp_wmi_bios_setup); + if (err) + goto err_unregister_device; + } + + if (is_omen_thermal_profile() || is_victus_thermal_profile()) { + err = omen_register_powersource_event_handler(); + if (err) + goto err_unregister_device; + } else if (is_victus_s_thermal_profile()) { + err = victus_s_register_powersource_event_handler(); + if (err) + goto err_unregister_device; + } + + return 0; + +err_unregister_device: + platform_device_unregister(hp_wmi_platform_dev); +err_destroy_input: + if (event_capable) + hp_wmi_input_destroy(); + + return err; +} +module_init(hp_wmi_init); + +static void __exit hp_wmi_exit(void) +{ + if (is_omen_thermal_profile() || is_victus_thermal_profile()) + omen_unregister_powersource_event_handler(); + + if (is_victus_s_thermal_profile()) + victus_s_unregister_powersource_event_handler(); + + if (wmi_has_guid(HPWMI_EVENT_GUID)) + hp_wmi_input_destroy(); + + if (camera_shutter_input_dev) + input_unregister_device(camera_shutter_input_dev); + + if (hp_wmi_platform_dev) { + platform_device_unregister(hp_wmi_platform_dev); + platform_driver_unregister(&hp_wmi_driver); + } +} +module_exit(hp_wmi_exit); diff --git a/drivers/platform/x86/hp/hp_accel.c b/drivers/platform/x86/hp/hp_accel.c new file mode 100644 index 000000000..39b73dc47 --- /dev/null +++ b/drivers/platform/x86/hp/hp_accel.c @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * hp_accel.c - Interface between LIS3LV02DL driver and HP ACPI BIOS + * + * Copyright (C) 2007-2008 Yan Burman + * Copyright (C) 2008 Eric Piel + * Copyright (C) 2008-2009 Pavel Machek + */ + +#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 "../../../misc/lis3lv02d/lis3lv02d.h" + +/* Delayed LEDs infrastructure ------------------------------------ */ + +/* Special LED class that can defer work */ +struct delayed_led_classdev { + struct led_classdev led_classdev; + struct work_struct work; + enum led_brightness new_brightness; + + unsigned int led; /* For driver */ + void (*set_brightness)(struct delayed_led_classdev *data, enum led_brightness value); +}; + +static inline void delayed_set_status_worker(struct work_struct *work) +{ + struct delayed_led_classdev *data = + container_of(work, struct delayed_led_classdev, work); + + data->set_brightness(data, data->new_brightness); +} + +static inline void delayed_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct delayed_led_classdev *data = container_of(led_cdev, + struct delayed_led_classdev, led_classdev); + data->new_brightness = brightness; + schedule_work(&data->work); +} + +/* HP-specific accelerometer driver ------------------------------------ */ + +/* e0 25, e0 26, e0 27, e0 28 are scan codes that the accelerometer with acpi id + * HPQ6000 sends through the keyboard bus */ +#define ACCEL_1 0x25 +#define ACCEL_2 0x26 +#define ACCEL_3 0x27 +#define ACCEL_4 0x28 + +/* For automatic insertion of the module */ +static const struct acpi_device_id lis3lv02d_device_ids[] = { + {"HPQ0004", 0}, /* HP Mobile Data Protection System PNP */ + {"HPQ6000", 0}, /* HP Mobile Data Protection System PNP */ + {"HPQ6007", 0}, /* HP Mobile Data Protection System PNP */ + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, lis3lv02d_device_ids); + +/** + * lis3lv02d_acpi_init - initialize the device for ACPI + * @lis3: pointer to the device struct + * + * Returns 0 on success. + */ +static int lis3lv02d_acpi_init(struct lis3lv02d *lis3) +{ + return 0; +} + +/** + * lis3lv02d_acpi_read - ACPI ALRD method: read a register + * @lis3: pointer to the device struct + * @reg: the register to read + * @ret: result of the operation + * + * Returns 0 on success. + */ +static int lis3lv02d_acpi_read(struct lis3lv02d *lis3, int reg, u8 *ret) +{ + struct acpi_device *dev = lis3->bus_priv; + union acpi_object arg0 = { ACPI_TYPE_INTEGER }; + struct acpi_object_list args = { 1, &arg0 }; + unsigned long long lret; + acpi_status status; + + arg0.integer.value = reg; + + status = acpi_evaluate_integer(dev->handle, "ALRD", &args, &lret); + if (ACPI_FAILURE(status)) + return -EINVAL; + *ret = lret; + return 0; +} + +/** + * lis3lv02d_acpi_write - ACPI ALWR method: write to a register + * @lis3: pointer to the device struct + * @reg: the register to write to + * @val: the value to write + * + * Returns 0 on success. + */ +static int lis3lv02d_acpi_write(struct lis3lv02d *lis3, int reg, u8 val) +{ + struct acpi_device *dev = lis3->bus_priv; + unsigned long long ret; /* Not used when writting */ + union acpi_object in_obj[2]; + struct acpi_object_list args = { 2, in_obj }; + + in_obj[0].type = ACPI_TYPE_INTEGER; + in_obj[0].integer.value = reg; + in_obj[1].type = ACPI_TYPE_INTEGER; + in_obj[1].integer.value = val; + + if (acpi_evaluate_integer(dev->handle, "ALWR", &args, &ret) != AE_OK) + return -EINVAL; + + return 0; +} + +static int lis3lv02d_dmi_matched(const struct dmi_system_id *dmi) +{ + lis3_dev.ac = *((union axis_conversion *)dmi->driver_data); + pr_info("hardware type %s found\n", dmi->ident); + + return 1; +} + +/* Represents, for each axis seen by userspace, the corresponding hw axis (+1). + * If the value is negative, the opposite of the hw value is used. */ +#define DEFINE_CONV(name, x, y, z) \ + static union axis_conversion lis3lv02d_axis_##name = \ + { .as_array = { x, y, z } } +DEFINE_CONV(normal, 1, 2, 3); +DEFINE_CONV(y_inverted, 1, -2, 3); +DEFINE_CONV(x_inverted, -1, 2, 3); +DEFINE_CONV(x_inverted_usd, -1, 2, -3); +DEFINE_CONV(z_inverted, 1, 2, -3); +DEFINE_CONV(xy_swap, 2, 1, 3); +DEFINE_CONV(xy_rotated_left, -2, 1, 3); +DEFINE_CONV(xy_rotated_left_usd, -2, 1, -3); +DEFINE_CONV(xy_swap_inverted, -2, -1, 3); +DEFINE_CONV(xy_rotated_right, 2, -1, 3); +DEFINE_CONV(xy_swap_yz_inverted, 2, -1, -3); + +#define AXIS_DMI_MATCH(_ident, _name, _axis) { \ + .ident = _ident, \ + .callback = lis3lv02d_dmi_matched, \ + .matches = { \ + DMI_MATCH(DMI_PRODUCT_NAME, _name) \ + }, \ + .driver_data = &lis3lv02d_axis_##_axis \ +} + +#define AXIS_DMI_MATCH2(_ident, _class1, _name1, \ + _class2, _name2, \ + _axis) { \ + .ident = _ident, \ + .callback = lis3lv02d_dmi_matched, \ + .matches = { \ + DMI_MATCH(DMI_##_class1, _name1), \ + DMI_MATCH(DMI_##_class2, _name2), \ + }, \ + .driver_data = &lis3lv02d_axis_##_axis \ +} +static const struct dmi_system_id lis3lv02d_dmi_ids[] = { + /* product names are truncated to match all kinds of a same model */ + AXIS_DMI_MATCH("NC64x0", "HP Compaq nc64", x_inverted), + AXIS_DMI_MATCH("NC84x0", "HP Compaq nc84", z_inverted), + AXIS_DMI_MATCH("NX9420", "HP Compaq nx9420", x_inverted), + AXIS_DMI_MATCH("NW9440", "HP Compaq nw9440", x_inverted), + AXIS_DMI_MATCH("NC2510", "HP Compaq 2510", y_inverted), + AXIS_DMI_MATCH("NC2710", "HP Compaq 2710", xy_swap), + AXIS_DMI_MATCH("NC8510", "HP Compaq 8510", xy_swap_inverted), + AXIS_DMI_MATCH("HP2133", "HP 2133", xy_rotated_left), + AXIS_DMI_MATCH("HP2140", "HP 2140", xy_swap_inverted), + AXIS_DMI_MATCH("NC653x", "HP Compaq 653", xy_rotated_left_usd), + AXIS_DMI_MATCH("NC6730b", "HP Compaq 6730b", xy_rotated_left_usd), + AXIS_DMI_MATCH("NC6730s", "HP Compaq 6730s", xy_swap), + AXIS_DMI_MATCH("NC651xx", "HP Compaq 651", xy_rotated_right), + AXIS_DMI_MATCH("NC6710x", "HP Compaq 6710", xy_swap_yz_inverted), + AXIS_DMI_MATCH("NC6715x", "HP Compaq 6715", y_inverted), + AXIS_DMI_MATCH("NC693xx", "HP EliteBook 693", xy_rotated_right), + AXIS_DMI_MATCH("NC693xx", "HP EliteBook 853", xy_swap), + AXIS_DMI_MATCH("NC854xx", "HP EliteBook 854", y_inverted), + AXIS_DMI_MATCH("NC273xx", "HP EliteBook 273", y_inverted), + /* Intel-based HP Pavilion dv5 */ + AXIS_DMI_MATCH2("HPDV5_I", + PRODUCT_NAME, "HP Pavilion dv5", + BOARD_NAME, "3603", + x_inverted), + /* AMD-based HP Pavilion dv5 */ + AXIS_DMI_MATCH2("HPDV5_A", + PRODUCT_NAME, "HP Pavilion dv5", + BOARD_NAME, "3600", + y_inverted), + AXIS_DMI_MATCH("DV7", "HP Pavilion dv7", x_inverted), + AXIS_DMI_MATCH("HP8710", "HP Compaq 8710", y_inverted), + AXIS_DMI_MATCH("HDX18", "HP HDX 18", x_inverted), + AXIS_DMI_MATCH("HPB432x", "HP ProBook 432", xy_rotated_left), + AXIS_DMI_MATCH("HPB440G3", "HP ProBook 440 G3", x_inverted_usd), + AXIS_DMI_MATCH("HPB440G4", "HP ProBook 440 G4", x_inverted), + AXIS_DMI_MATCH("HPB442x", "HP ProBook 442", xy_rotated_left), + AXIS_DMI_MATCH("HPB450G0", "HP ProBook 450 G0", x_inverted), + AXIS_DMI_MATCH("HPB452x", "HP ProBook 452", y_inverted), + AXIS_DMI_MATCH("HPB522x", "HP ProBook 522", xy_swap), + AXIS_DMI_MATCH("HPB532x", "HP ProBook 532", y_inverted), + AXIS_DMI_MATCH("HPB655x", "HP ProBook 655", xy_swap_inverted), + AXIS_DMI_MATCH("Mini510x", "HP Mini 510", xy_rotated_left_usd), + AXIS_DMI_MATCH("HPB63xx", "HP ProBook 63", xy_swap), + AXIS_DMI_MATCH("HPB64xx", "HP ProBook 64", xy_swap), + AXIS_DMI_MATCH("HPB64xx", "HP EliteBook 84", xy_swap), + AXIS_DMI_MATCH("HPB65xx", "HP ProBook 65", x_inverted), + AXIS_DMI_MATCH("HPZBook15", "HP ZBook 15", x_inverted), + AXIS_DMI_MATCH("HPZBook17G5", "HP ZBook 17 G5", x_inverted), + AXIS_DMI_MATCH("HPZBook17", "HP ZBook 17", xy_swap_yz_inverted), + { NULL, } +/* Laptop models without axis info (yet): + * "NC6910" "HP Compaq 6910" + * "NC2400" "HP Compaq nc2400" + * "NX74x0" "HP Compaq nx74" + * "NX6325" "HP Compaq nx6325" + * "NC4400" "HP Compaq nc4400" + */ +}; + +static void hpled_set(struct delayed_led_classdev *led_cdev, enum led_brightness value) +{ + struct acpi_device *dev = lis3_dev.bus_priv; + unsigned long long ret; /* Not used when writing */ + union acpi_object in_obj[1]; + struct acpi_object_list args = { 1, in_obj }; + + in_obj[0].type = ACPI_TYPE_INTEGER; + in_obj[0].integer.value = !!value; + + acpi_evaluate_integer(dev->handle, "ALED", &args, &ret); +} + +static struct delayed_led_classdev hpled_led = { + .led_classdev = { + .name = "hp::hddprotect", + .default_trigger = "none", + .brightness_set = delayed_sysfs_set, + .flags = LED_CORE_SUSPENDRESUME, + }, + .set_brightness = hpled_set, +}; + +static bool hp_accel_i8042_filter(unsigned char data, unsigned char str, + struct serio *port, void *context) +{ + static bool extended; + + if (str & I8042_STR_AUXDATA) + return false; + + if (data == 0xe0) { + extended = true; + return true; + } else if (unlikely(extended)) { + extended = false; + + switch (data) { + case ACCEL_1: + case ACCEL_2: + case ACCEL_3: + case ACCEL_4: + return true; + default: + serio_interrupt(port, 0xe0, 0); + return false; + } + } + + return false; +} + +static int lis3lv02d_probe(struct platform_device *device) +{ + int ret; + + lis3_dev.bus_priv = ACPI_COMPANION(&device->dev); + if (!lis3_dev.bus_priv) + return -ENODEV; + + lis3_dev.init = lis3lv02d_acpi_init; + lis3_dev.read = lis3lv02d_acpi_read; + lis3_dev.write = lis3lv02d_acpi_write; + + /* obtain IRQ number of our device from ACPI */ + ret = platform_get_irq_optional(device, 0); + if (ret > 0) + lis3_dev.irq = ret; + + /* If possible use a "standard" axes order */ + if (lis3_dev.ac.x && lis3_dev.ac.y && lis3_dev.ac.z) { + pr_info("Using custom axes %d,%d,%d\n", + lis3_dev.ac.x, lis3_dev.ac.y, lis3_dev.ac.z); + } else if (dmi_check_system(lis3lv02d_dmi_ids) == 0) { + pr_info("laptop model unknown, using default axes configuration\n"); + lis3_dev.ac = lis3lv02d_axis_normal; + } + + /* call the core layer do its init */ + ret = lis3lv02d_init_device(&lis3_dev); + if (ret) + return ret; + + /* filter to remove HPQ6000 accelerometer data + * from keyboard bus stream */ + if (strstr(dev_name(&device->dev), "HPQ6000")) + i8042_install_filter(hp_accel_i8042_filter, NULL); + + INIT_WORK(&hpled_led.work, delayed_set_status_worker); + ret = led_classdev_register(NULL, &hpled_led.led_classdev); + if (ret) { + i8042_remove_filter(hp_accel_i8042_filter); + lis3lv02d_joystick_disable(&lis3_dev); + lis3lv02d_poweroff(&lis3_dev); + flush_work(&hpled_led.work); + lis3lv02d_remove_fs(&lis3_dev); + return ret; + } + + return ret; +} + +static void lis3lv02d_remove(struct platform_device *device) +{ + i8042_remove_filter(hp_accel_i8042_filter); + lis3lv02d_joystick_disable(&lis3_dev); + lis3lv02d_poweroff(&lis3_dev); + + led_classdev_unregister(&hpled_led.led_classdev); + flush_work(&hpled_led.work); + + lis3lv02d_remove_fs(&lis3_dev); +} + +static int __maybe_unused lis3lv02d_suspend(struct device *dev) +{ + /* make sure the device is off when we suspend */ + lis3lv02d_poweroff(&lis3_dev); + return 0; +} + +static int __maybe_unused lis3lv02d_resume(struct device *dev) +{ + lis3lv02d_poweron(&lis3_dev); + return 0; +} + +static SIMPLE_DEV_PM_OPS(hp_accel_pm, lis3lv02d_suspend, lis3lv02d_resume); + +/* For the HP MDPS aka 3D Driveguard */ +static struct platform_driver lis3lv02d_driver = { + .probe = lis3lv02d_probe, + .remove = lis3lv02d_remove, + .driver = { + .name = "hp_accel", + .pm = &hp_accel_pm, + .acpi_match_table = lis3lv02d_device_ids, + }, +}; +module_platform_driver(lis3lv02d_driver); + +MODULE_DESCRIPTION("Glue between LIS3LV02Dx and HP ACPI BIOS and support for disk protection LED."); +MODULE_AUTHOR("Yan Burman, Eric Piel, Pavel Machek"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/hp/tc1100-wmi.c b/drivers/platform/x86/hp/tc1100-wmi.c new file mode 100644 index 000000000..146716d81 --- /dev/null +++ b/drivers/platform/x86/hp/tc1100-wmi.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HP Compaq TC1100 Tablet WMI Extras Driver + * + * Copyright (C) 2007 Carlos Corbacho + * Copyright (C) 2004 Jamey Hicks + * Copyright (C) 2001, 2002 Andy Grover + * Copyright (C) 2001, 2002 Paul Diefenbaugh + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include + +#define GUID "C364AC71-36DB-495A-8494-B439D472A505" + +#define TC1100_INSTANCE_WIRELESS 1 +#define TC1100_INSTANCE_JOGDIAL 2 + +MODULE_AUTHOR("Jamey Hicks, Carlos Corbacho"); +MODULE_DESCRIPTION("HP Compaq TC1100 Tablet WMI Extras"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("wmi:C364AC71-36DB-495A-8494-B439D472A505"); + +static struct platform_device *tc1100_device; + +struct tc1100_data { + u32 wireless; + u32 jogdial; +}; + +#ifdef CONFIG_PM +static struct tc1100_data suspend_data; +#endif + +/* -------------------------------------------------------------------------- + Device Management + -------------------------------------------------------------------------- */ + +static int get_state(u32 *out, u8 instance) +{ + u32 tmp; + acpi_status status; + struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + if (!out) + return -EINVAL; + + if (instance > 2) + return -ENODEV; + + status = wmi_query_block(GUID, instance, &result); + if (ACPI_FAILURE(status)) + return -ENODEV; + + obj = (union acpi_object *) result.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) { + tmp = obj->integer.value; + } else { + tmp = 0; + } + + if (result.length > 0) + kfree(result.pointer); + + switch (instance) { + case TC1100_INSTANCE_WIRELESS: + *out = (tmp == 3) ? 1 : 0; + return 0; + case TC1100_INSTANCE_JOGDIAL: + *out = (tmp == 1) ? 0 : 1; + return 0; + default: + return -ENODEV; + } +} + +static int set_state(u32 *in, u8 instance) +{ + u32 value; + acpi_status status; + struct acpi_buffer input; + + if (!in) + return -EINVAL; + + if (instance > 2) + return -ENODEV; + + switch (instance) { + case TC1100_INSTANCE_WIRELESS: + value = (*in) ? 1 : 2; + break; + case TC1100_INSTANCE_JOGDIAL: + value = (*in) ? 0 : 1; + break; + default: + return -ENODEV; + } + + input.length = sizeof(u32); + input.pointer = &value; + + status = wmi_set_block(GUID, instance, &input); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; +} + +/* -------------------------------------------------------------------------- + FS Interface (/sys) + -------------------------------------------------------------------------- */ + +/* + * Read/ write bool sysfs macro + */ +#define show_set_bool(value, instance) \ +static ssize_t \ +show_bool_##value(struct device *dev, struct device_attribute *attr, \ + char *buf) \ +{ \ + u32 result; \ + acpi_status status = get_state(&result, instance); \ + if (ACPI_SUCCESS(status)) \ + return sprintf(buf, "%d\n", result); \ + return sprintf(buf, "Read error\n"); \ +} \ +\ +static ssize_t \ +set_bool_##value(struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ +{ \ + u32 tmp = simple_strtoul(buf, NULL, 10); \ + acpi_status status = set_state(&tmp, instance); \ + if (ACPI_FAILURE(status)) \ + return -EINVAL; \ + return count; \ +} \ +static DEVICE_ATTR(value, S_IRUGO | S_IWUSR, \ + show_bool_##value, set_bool_##value); + +show_set_bool(wireless, TC1100_INSTANCE_WIRELESS); +show_set_bool(jogdial, TC1100_INSTANCE_JOGDIAL); + +static struct attribute *tc1100_attributes[] = { + &dev_attr_wireless.attr, + &dev_attr_jogdial.attr, + NULL +}; + +static const struct attribute_group tc1100_attribute_group = { + .attrs = tc1100_attributes, +}; + +/* -------------------------------------------------------------------------- + Driver Model + -------------------------------------------------------------------------- */ + +static int __init tc1100_probe(struct platform_device *device) +{ + return sysfs_create_group(&device->dev.kobj, &tc1100_attribute_group); +} + + +static void tc1100_remove(struct platform_device *device) +{ + sysfs_remove_group(&device->dev.kobj, &tc1100_attribute_group); +} + +#ifdef CONFIG_PM +static int tc1100_suspend(struct device *dev) +{ + int ret; + + ret = get_state(&suspend_data.wireless, TC1100_INSTANCE_WIRELESS); + if (ret) + return ret; + + ret = get_state(&suspend_data.jogdial, TC1100_INSTANCE_JOGDIAL); + if (ret) + return ret; + + return 0; +} + +static int tc1100_resume(struct device *dev) +{ + int ret; + + ret = set_state(&suspend_data.wireless, TC1100_INSTANCE_WIRELESS); + if (ret) + return ret; + + ret = set_state(&suspend_data.jogdial, TC1100_INSTANCE_JOGDIAL); + if (ret) + return ret; + + return 0; +} + +static const struct dev_pm_ops tc1100_pm_ops = { + .suspend = tc1100_suspend, + .resume = tc1100_resume, + .freeze = tc1100_suspend, + .restore = tc1100_resume, +}; +#endif + +static struct platform_driver tc1100_driver = { + .driver = { + .name = "tc1100-wmi", +#ifdef CONFIG_PM + .pm = &tc1100_pm_ops, +#endif + }, + .remove = tc1100_remove, +}; + +static int __init tc1100_init(void) +{ + int error; + + if (!wmi_has_guid(GUID)) + return -ENODEV; + + tc1100_device = platform_device_alloc("tc1100-wmi", PLATFORM_DEVID_NONE); + if (!tc1100_device) + return -ENOMEM; + + error = platform_device_add(tc1100_device); + if (error) + goto err_device_put; + + error = platform_driver_probe(&tc1100_driver, tc1100_probe); + if (error) + goto err_device_del; + + pr_info("HP Compaq TC1100 Tablet WMI Extras loaded\n"); + return 0; + + err_device_del: + platform_device_del(tc1100_device); + err_device_put: + platform_device_put(tc1100_device); + return error; +} + +static void __exit tc1100_exit(void) +{ + platform_device_unregister(tc1100_device); + platform_driver_unregister(&tc1100_driver); +} + +module_init(tc1100_init); +module_exit(tc1100_exit); diff --git a/drivers/platform/x86/huawei-wmi.c b/drivers/platform/x86/huawei-wmi.c new file mode 100644 index 000000000..93cca17fd --- /dev/null +++ b/drivers/platform/x86/huawei-wmi.c @@ -0,0 +1,904 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Huawei WMI laptop extras driver + * + * Copyright (C) 2018 Ayman Bagabas + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Huawei WMI GUIDs + */ +#define HWMI_METHOD_GUID "ABBC0F5B-8EA1-11D1-A000-C90629100000" +#define HWMI_EVENT_GUID "ABBC0F5C-8EA1-11D1-A000-C90629100000" + +/* Legacy GUIDs */ +#define WMI0_EXPENSIVE_GUID "39142400-C6A3-40fa-BADB-8A2652834100" +#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" + +/* HWMI commands */ + +enum { + BATTERY_THRESH_GET = 0x00001103, /* \GBTT */ + BATTERY_THRESH_SET = 0x00001003, /* \SBTT */ + FN_LOCK_GET = 0x00000604, /* \GFRS */ + FN_LOCK_SET = 0x00000704, /* \SFRS */ + MICMUTE_LED_SET = 0x00000b04, /* \SMLS */ +}; + +union hwmi_arg { + u64 cmd; + u8 args[8]; +}; + +struct quirk_entry { + bool battery_reset; + bool ec_micmute; + bool report_brightness; +}; + +static struct quirk_entry *quirks; + +struct huawei_wmi_debug { + struct dentry *root; + u64 arg; +}; + +struct huawei_wmi { + bool battery_available; + bool fn_lock_available; + + struct huawei_wmi_debug debug; + struct led_classdev cdev; + struct device *dev; + + struct mutex wmi_lock; +}; + +static struct huawei_wmi *huawei_wmi; + +static const struct key_entry huawei_wmi_keymap[] = { + { KE_KEY, 0x281, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x282, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x284, { KEY_MUTE } }, + { KE_KEY, 0x285, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0x286, { KEY_VOLUMEUP } }, + { KE_KEY, 0x287, { KEY_MICMUTE } }, + { KE_KEY, 0x289, { KEY_WLAN } }, + // Huawei |M| key + { KE_KEY, 0x28a, { KEY_CONFIG } }, + // HONOR YOYO key + { KE_KEY, 0x28b, { KEY_NOTIFICATION_CENTER } }, + // HONOR print screen + { KE_KEY, 0x28e, { KEY_PRINT } }, + // Keyboard backlit + { KE_IGNORE, 0x293, { KEY_KBDILLUMTOGGLE } }, + { KE_IGNORE, 0x294, { KEY_KBDILLUMUP } }, + { KE_IGNORE, 0x295, { KEY_KBDILLUMUP } }, + // Ignore Ambient Light Sensoring + { KE_KEY, 0x2c1, { KEY_RESERVED } }, + { KE_END, 0 } +}; + +static int battery_reset = -1; +static int report_brightness = -1; + +module_param(battery_reset, bint, 0444); +MODULE_PARM_DESC(battery_reset, + "Reset battery charge values to (0-0) before disabling it using (0-100)"); +module_param(report_brightness, bint, 0444); +MODULE_PARM_DESC(report_brightness, + "Report brightness keys."); + +/* Quirks */ + +static int __init dmi_matched(const struct dmi_system_id *dmi) +{ + quirks = dmi->driver_data; + return 1; +} + +static struct quirk_entry quirk_unknown = { +}; + +static struct quirk_entry quirk_battery_reset = { + .battery_reset = true, +}; + +static struct quirk_entry quirk_matebook_x = { + .ec_micmute = true, + .report_brightness = true, +}; + +static const struct dmi_system_id huawei_quirks[] = { + { + .callback = dmi_matched, + .ident = "Huawei MACH-WX9", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "MACH-WX9"), + }, + .driver_data = &quirk_battery_reset + }, + { + .callback = dmi_matched, + .ident = "Huawei MateBook X", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "HUAWEI MateBook X") + }, + .driver_data = &quirk_matebook_x + }, + { } +}; + +/* Utils */ + +static int huawei_wmi_call(struct huawei_wmi *huawei, + struct acpi_buffer *in, struct acpi_buffer *out) +{ + acpi_status status; + + mutex_lock(&huawei->wmi_lock); + status = wmi_evaluate_method(HWMI_METHOD_GUID, 0, 1, in, out); + mutex_unlock(&huawei->wmi_lock); + if (ACPI_FAILURE(status)) { + dev_err(huawei->dev, "Failed to evaluate wmi method\n"); + return -ENODEV; + } + + return 0; +} + +/* HWMI takes a 64 bit input and returns either a package with 2 buffers, one of + * 4 bytes and the other of 256 bytes, or one buffer of size 0x104 (260) bytes. + * The first 4 bytes are ignored, we ignore the first 4 bytes buffer if we got a + * package, or skip the first 4 if a buffer of 0x104 is used. The first byte of + * the remaining 0x100 sized buffer has the return status of every call. In case + * the return status is non-zero, we return -ENODEV but still copy the returned + * buffer to the given buffer parameter (buf). + */ +static int huawei_wmi_cmd(u64 arg, u8 *buf, size_t buflen) +{ + struct huawei_wmi *huawei = huawei_wmi; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer in; + union acpi_object *obj; + size_t len; + int err, i; + + in.length = sizeof(arg); + in.pointer = &arg; + + /* Some models require calling HWMI twice to execute a command. We evaluate + * HWMI and if we get a non-zero return status we evaluate it again. + */ + for (i = 0; i < 2; i++) { + err = huawei_wmi_call(huawei, &in, &out); + if (err) + goto fail_cmd; + + obj = out.pointer; + if (!obj) { + err = -EIO; + goto fail_cmd; + } + + switch (obj->type) { + /* Models that implement both "legacy" and HWMI tend to return a 0x104 + * sized buffer instead of a package of 0x4 and 0x100 buffers. + */ + case ACPI_TYPE_BUFFER: + if (obj->buffer.length == 0x104) { + // Skip the first 4 bytes. + obj->buffer.pointer += 4; + len = 0x100; + } else { + dev_err(huawei->dev, "Bad buffer length, got %d\n", obj->buffer.length); + err = -EIO; + goto fail_cmd; + } + + break; + /* HWMI returns a package with 2 buffer elements, one of 4 bytes and the + * other is 256 bytes. + */ + case ACPI_TYPE_PACKAGE: + if (obj->package.count != 2) { + dev_err(huawei->dev, "Bad package count, got %d\n", obj->package.count); + err = -EIO; + goto fail_cmd; + } + + obj = &obj->package.elements[1]; + if (obj->type != ACPI_TYPE_BUFFER) { + dev_err(huawei->dev, "Bad package element type, got %d\n", obj->type); + err = -EIO; + goto fail_cmd; + } + len = obj->buffer.length; + + break; + /* Shouldn't get here! */ + default: + dev_err(huawei->dev, "Unexpected obj type, got: %d\n", obj->type); + err = -EIO; + goto fail_cmd; + } + + if (!*obj->buffer.pointer) + break; + } + + err = (*obj->buffer.pointer) ? -ENODEV : 0; + + if (buf) { + len = min(buflen, len); + memcpy(buf, obj->buffer.pointer, len); + } + +fail_cmd: + kfree(out.pointer); + return err; +} + +/* LEDs */ + +static int huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + /* This is a workaround until the "legacy" interface is implemented. */ + if (quirks && quirks->ec_micmute) { + char *acpi_method; + acpi_handle handle; + acpi_status status; + union acpi_object args[3]; + struct acpi_object_list arg_list = { + .pointer = args, + .count = ARRAY_SIZE(args), + }; + + handle = ec_get_handle(); + if (!handle) + return -ENODEV; + + args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; + args[1].integer.value = 0x04; + + if (acpi_has_method(handle, "SPIN")) { + acpi_method = "SPIN"; + args[0].integer.value = 0; + args[2].integer.value = brightness ? 1 : 0; + } else if (acpi_has_method(handle, "WPIN")) { + acpi_method = "WPIN"; + args[0].integer.value = 1; + args[2].integer.value = brightness ? 0 : 1; + } else { + return -ENODEV; + } + + status = acpi_evaluate_object(handle, acpi_method, &arg_list, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + return 0; + } else { + union hwmi_arg arg; + + arg.cmd = MICMUTE_LED_SET; + arg.args[2] = brightness; + + return huawei_wmi_cmd(arg.cmd, NULL, 0); + } +} + +static void huawei_wmi_leds_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + huawei->cdev.name = "platform::micmute"; + huawei->cdev.max_brightness = 1; + huawei->cdev.brightness_set_blocking = &huawei_wmi_micmute_led_set; + huawei->cdev.default_trigger = "audio-micmute"; + huawei->cdev.dev = dev; + huawei->cdev.flags = LED_CORE_SUSPENDRESUME; + + devm_led_classdev_register(dev, &huawei->cdev); +} + +/* Battery protection */ + +static int huawei_wmi_battery_get(int *start, int *end) +{ + u8 ret[0x100]; + int err, i; + + err = huawei_wmi_cmd(BATTERY_THRESH_GET, ret, sizeof(ret)); + if (err) + return err; + + /* Find the last two non-zero values. Return status is ignored. */ + i = ARRAY_SIZE(ret) - 1; + do { + if (start) + *start = ret[i-1]; + if (end) + *end = ret[i]; + } while (i > 2 && !ret[i--]); + + return 0; +} + +static int huawei_wmi_battery_set(int start, int end) +{ + union hwmi_arg arg; + int err; + + if (start < 0 || end < 0 || start > 100 || end > 100) + return -EINVAL; + + arg.cmd = BATTERY_THRESH_SET; + arg.args[2] = start; + arg.args[3] = end; + + /* This is an edge case were some models turn battery protection + * off without changing their thresholds values. We clear the + * values before turning off protection. Sometimes we need a sleep delay to + * make sure these values make their way to EC memory. + */ + if (quirks && quirks->battery_reset && start == 0 && end == 100) { + err = huawei_wmi_battery_set(0, 0); + if (err) + return err; + + msleep(1000); + } + + err = huawei_wmi_cmd(arg.cmd, NULL, 0); + + return err; +} + +static ssize_t charge_control_start_threshold_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, start; + + err = huawei_wmi_battery_get(&start, NULL); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", start); +} + +static ssize_t charge_control_end_threshold_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, end; + + err = huawei_wmi_battery_get(NULL, &end); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", end); +} + +static ssize_t charge_control_thresholds_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, start, end; + + err = huawei_wmi_battery_get(&start, &end); + if (err) + return err; + + return sysfs_emit(buf, "%d %d\n", start, end); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int err, start, end; + + err = huawei_wmi_battery_get(NULL, &end); + if (err) + return err; + + if (sscanf(buf, "%d", &start) != 1) + return -EINVAL; + + err = huawei_wmi_battery_set(start, end); + if (err) + return err; + + return size; +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int err, start, end; + + err = huawei_wmi_battery_get(&start, NULL); + if (err) + return err; + + if (sscanf(buf, "%d", &end) != 1) + return -EINVAL; + + err = huawei_wmi_battery_set(start, end); + if (err) + return err; + + return size; +} + +static ssize_t charge_control_thresholds_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int err, start, end; + + if (sscanf(buf, "%d %d", &start, &end) != 2) + return -EINVAL; + + err = huawei_wmi_battery_set(start, end); + if (err) + return err; + + return size; +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); +static DEVICE_ATTR_RW(charge_control_end_threshold); +static DEVICE_ATTR_RW(charge_control_thresholds); + +static int huawei_wmi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + int err = 0; + + err = device_create_file(&battery->dev, &dev_attr_charge_control_start_threshold); + if (err) + return err; + + err = device_create_file(&battery->dev, &dev_attr_charge_control_end_threshold); + if (err) + device_remove_file(&battery->dev, &dev_attr_charge_control_start_threshold); + + return err; +} + +static int huawei_wmi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_file(&battery->dev, &dev_attr_charge_control_start_threshold); + device_remove_file(&battery->dev, &dev_attr_charge_control_end_threshold); + + return 0; +} + +static struct acpi_battery_hook huawei_wmi_battery_hook = { + .add_battery = huawei_wmi_battery_add, + .remove_battery = huawei_wmi_battery_remove, + .name = "Huawei Battery Extension" +}; + +static void huawei_wmi_battery_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + huawei->battery_available = true; + if (huawei_wmi_battery_get(NULL, NULL)) { + huawei->battery_available = false; + return; + } + + battery_hook_register(&huawei_wmi_battery_hook); + device_create_file(dev, &dev_attr_charge_control_thresholds); +} + +static void huawei_wmi_battery_exit(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + if (huawei->battery_available) { + battery_hook_unregister(&huawei_wmi_battery_hook); + device_remove_file(dev, &dev_attr_charge_control_thresholds); + } +} + +/* Fn lock */ + +static int huawei_wmi_fn_lock_get(int *on) +{ + u8 ret[0x100] = { 0 }; + int err, i; + + err = huawei_wmi_cmd(FN_LOCK_GET, ret, 0x100); + if (err) + return err; + + /* Find the first non-zero value. Return status is ignored. */ + i = 1; + do { + if (on) + *on = ret[i] - 1; // -1 undefined, 0 off, 1 on. + } while (i < 0xff && !ret[i++]); + + return 0; +} + +static int huawei_wmi_fn_lock_set(int on) +{ + union hwmi_arg arg; + + arg.cmd = FN_LOCK_SET; + arg.args[2] = on + 1; // 0 undefined, 1 off, 2 on. + + return huawei_wmi_cmd(arg.cmd, NULL, 0); +} + +static ssize_t fn_lock_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, on; + + err = huawei_wmi_fn_lock_get(&on); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", on); +} + +static ssize_t fn_lock_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int on, err; + + if (kstrtoint(buf, 10, &on) || + on < 0 || on > 1) + return -EINVAL; + + err = huawei_wmi_fn_lock_set(on); + if (err) + return err; + + return size; +} + +static DEVICE_ATTR_RW(fn_lock_state); + +static void huawei_wmi_fn_lock_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + huawei->fn_lock_available = true; + if (huawei_wmi_fn_lock_get(NULL)) { + huawei->fn_lock_available = false; + return; + } + + device_create_file(dev, &dev_attr_fn_lock_state); +} + +static void huawei_wmi_fn_lock_exit(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + if (huawei->fn_lock_available) + device_remove_file(dev, &dev_attr_fn_lock_state); +} + +/* debugfs */ + +static void huawei_wmi_debugfs_call_dump(struct seq_file *m, void *data, + union acpi_object *obj) +{ + struct huawei_wmi *huawei = m->private; + int i; + + switch (obj->type) { + case ACPI_TYPE_INTEGER: + seq_printf(m, "0x%llx", obj->integer.value); + break; + case ACPI_TYPE_STRING: + seq_printf(m, "\"%.*s\"", obj->string.length, obj->string.pointer); + break; + case ACPI_TYPE_BUFFER: + seq_puts(m, "{"); + for (i = 0; i < obj->buffer.length; i++) { + seq_printf(m, "0x%02x", obj->buffer.pointer[i]); + if (i < obj->buffer.length - 1) + seq_puts(m, ","); + } + seq_puts(m, "}"); + break; + case ACPI_TYPE_PACKAGE: + seq_puts(m, "["); + for (i = 0; i < obj->package.count; i++) { + huawei_wmi_debugfs_call_dump(m, huawei, &obj->package.elements[i]); + if (i < obj->package.count - 1) + seq_puts(m, ","); + } + seq_puts(m, "]"); + break; + default: + dev_err(huawei->dev, "Unexpected obj type, got %d\n", obj->type); + return; + } +} + +static int huawei_wmi_debugfs_call_show(struct seq_file *m, void *data) +{ + struct huawei_wmi *huawei = m->private; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer in; + union acpi_object *obj; + int err; + + in.length = sizeof(u64); + in.pointer = &huawei->debug.arg; + + err = huawei_wmi_call(huawei, &in, &out); + if (err) + return err; + + obj = out.pointer; + if (!obj) { + err = -EIO; + goto fail_debugfs_call; + } + + huawei_wmi_debugfs_call_dump(m, huawei, obj); + +fail_debugfs_call: + kfree(out.pointer); + return err; +} + +DEFINE_SHOW_ATTRIBUTE(huawei_wmi_debugfs_call); + +static void huawei_wmi_debugfs_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + huawei->debug.root = debugfs_create_dir("huawei-wmi", NULL); + + debugfs_create_x64("arg", 0644, huawei->debug.root, + &huawei->debug.arg); + debugfs_create_file("call", 0400, + huawei->debug.root, huawei, &huawei_wmi_debugfs_call_fops); +} + +static void huawei_wmi_debugfs_exit(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + debugfs_remove_recursive(huawei->debug.root); +} + +/* Input */ + +static void huawei_wmi_process_key(struct input_dev *idev, int code) +{ + const struct key_entry *key; + + /* + * WMI0 uses code 0x80 to indicate a hotkey event. + * The actual key is fetched from the method WQ00 + * using WMI0_EXPENSIVE_GUID. + */ + if (code == 0x80) { + struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + + status = wmi_query_block(WMI0_EXPENSIVE_GUID, 0, &response); + if (ACPI_FAILURE(status)) + return; + + obj = (union acpi_object *)response.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + code = obj->integer.value; + + kfree(response.pointer); + } + + key = sparse_keymap_entry_from_scancode(idev, code); + if (!key) { + dev_info(&idev->dev, "Unknown key pressed, code: 0x%04x\n", code); + return; + } + + if (quirks && !quirks->report_brightness && + (key->sw.code == KEY_BRIGHTNESSDOWN || + key->sw.code == KEY_BRIGHTNESSUP)) + return; + + sparse_keymap_report_entry(idev, key, 1, true); +} + +static void huawei_wmi_input_notify(union acpi_object *obj, void *context) +{ + struct input_dev *idev = (struct input_dev *)context; + + if (obj && obj->type == ACPI_TYPE_INTEGER) + huawei_wmi_process_key(idev, obj->integer.value); + else + dev_err(&idev->dev, "Bad response type\n"); +} + +static int huawei_wmi_input_setup(struct device *dev, const char *guid) +{ + struct input_dev *idev; + acpi_status status; + int err; + + idev = devm_input_allocate_device(dev); + if (!idev) + return -ENOMEM; + + idev->name = "Huawei WMI hotkeys"; + idev->phys = "wmi/input0"; + idev->id.bustype = BUS_HOST; + idev->dev.parent = dev; + + err = sparse_keymap_setup(idev, huawei_wmi_keymap, NULL); + if (err) + return err; + + err = input_register_device(idev); + if (err) + return err; + + status = wmi_install_notify_handler(guid, huawei_wmi_input_notify, idev); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +static void huawei_wmi_input_exit(struct device *dev, const char *guid) +{ + wmi_remove_notify_handler(guid); +} + +/* Huawei driver */ + +static const struct wmi_device_id huawei_wmi_events_id_table[] = { + { .guid_string = WMI0_EVENT_GUID }, + { .guid_string = HWMI_EVENT_GUID }, + { } +}; + +static int huawei_wmi_probe(struct platform_device *pdev) +{ + const struct wmi_device_id *guid = huawei_wmi_events_id_table; + int err; + + platform_set_drvdata(pdev, huawei_wmi); + huawei_wmi->dev = &pdev->dev; + + while (*guid->guid_string) { + if (wmi_has_guid(guid->guid_string)) { + err = huawei_wmi_input_setup(&pdev->dev, guid->guid_string); + if (err) { + dev_err(&pdev->dev, "Failed to setup input on %s\n", guid->guid_string); + return err; + } + } + + guid++; + } + + if (wmi_has_guid(HWMI_METHOD_GUID)) { + mutex_init(&huawei_wmi->wmi_lock); + + huawei_wmi_leds_setup(&pdev->dev); + huawei_wmi_fn_lock_setup(&pdev->dev); + huawei_wmi_battery_setup(&pdev->dev); + huawei_wmi_debugfs_setup(&pdev->dev); + } + + return 0; +} + +static void huawei_wmi_remove(struct platform_device *pdev) +{ + const struct wmi_device_id *guid = huawei_wmi_events_id_table; + + while (*guid->guid_string) { + if (wmi_has_guid(guid->guid_string)) + huawei_wmi_input_exit(&pdev->dev, guid->guid_string); + + guid++; + } + + if (wmi_has_guid(HWMI_METHOD_GUID)) { + huawei_wmi_debugfs_exit(&pdev->dev); + huawei_wmi_battery_exit(&pdev->dev); + huawei_wmi_fn_lock_exit(&pdev->dev); + } +} + +static struct platform_driver huawei_wmi_driver = { + .driver = { + .name = "huawei-wmi", + }, + .probe = huawei_wmi_probe, + .remove = huawei_wmi_remove, +}; + +static __init int huawei_wmi_init(void) +{ + struct platform_device *pdev; + int err; + + huawei_wmi = kzalloc_obj(struct huawei_wmi); + if (!huawei_wmi) + return -ENOMEM; + + quirks = &quirk_unknown; + dmi_check_system(huawei_quirks); + if (battery_reset != -1) + quirks->battery_reset = battery_reset; + if (report_brightness != -1) + quirks->report_brightness = report_brightness; + + err = platform_driver_register(&huawei_wmi_driver); + if (err) + goto pdrv_err; + + pdev = platform_device_register_simple("huawei-wmi", PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(pdev)) { + err = PTR_ERR(pdev); + goto pdev_err; + } + + return 0; + +pdev_err: + platform_driver_unregister(&huawei_wmi_driver); +pdrv_err: + kfree(huawei_wmi); + return err; +} + +static __exit void huawei_wmi_exit(void) +{ + struct platform_device *pdev = to_platform_device(huawei_wmi->dev); + + platform_device_unregister(pdev); + platform_driver_unregister(&huawei_wmi_driver); + + kfree(huawei_wmi); +} + +module_init(huawei_wmi_init); +module_exit(huawei_wmi_exit); + +MODULE_ALIAS("wmi:"HWMI_METHOD_GUID); +MODULE_DEVICE_TABLE(wmi, huawei_wmi_events_id_table); +MODULE_AUTHOR("Ayman Bagabas "); +MODULE_DESCRIPTION("Huawei WMI laptop extras driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/ibm_rtl.c b/drivers/platform/x86/ibm_rtl.c new file mode 100644 index 000000000..139956168 --- /dev/null +++ b/drivers/platform/x86/ibm_rtl.c @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * IBM Real-Time Linux driver + * + * Copyright (C) IBM Corporation, 2010 + * + * Author: Keith Mannthey + * Vernon Mauery + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +static bool debug; +module_param(debug, bool, 0644); +MODULE_PARM_DESC(debug, "Show debug output"); + +MODULE_DESCRIPTION("IBM Premium Real Time Mode (PRTM) driver"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Keith Mannthey "); +MODULE_AUTHOR("Vernon Mauery "); + +#define RTL_ADDR_TYPE_IO 1 +#define RTL_ADDR_TYPE_MMIO 2 + +#define RTL_CMD_ENTER_PRTM 1 +#define RTL_CMD_EXIT_PRTM 2 + +/* The RTL table as presented by the EBDA: */ +struct ibm_rtl_table { + char signature[5]; /* signature should be "_RTL_" */ + u8 version; + u8 rt_status; + u8 command; + u8 command_status; + u8 cmd_address_type; + u8 cmd_granularity; + u8 cmd_offset; + u16 reserve1; + u32 cmd_port_address; /* platform dependent address */ + u32 cmd_port_value; /* platform dependent value */ +} __attribute__((packed)); + +/* to locate "_RTL_" signature do a masked 5-byte integer compare */ +#define RTL_SIGNATURE 0x0000005f4c54525fULL +#define RTL_MASK 0x000000ffffffffffULL + +#define RTL_DEBUG(fmt, ...) \ +do { \ + if (debug) \ + pr_info(fmt, ##__VA_ARGS__); \ +} while (0) + +static DEFINE_MUTEX(rtl_lock); +static struct ibm_rtl_table __iomem *rtl_table; +static void __iomem *ebda_map; +static void __iomem *rtl_cmd_addr; +static u8 rtl_cmd_type; +static u8 rtl_cmd_width; + +static void __iomem *rtl_port_map(phys_addr_t addr, unsigned long len) +{ + if (rtl_cmd_type == RTL_ADDR_TYPE_MMIO) + return ioremap(addr, len); + return ioport_map(addr, len); +} + +static void rtl_port_unmap(void __iomem *addr) +{ + if (addr && rtl_cmd_type == RTL_ADDR_TYPE_MMIO) + iounmap(addr); + else + ioport_unmap(addr); +} + +static int ibm_rtl_write(u8 value) +{ + int ret = 0, count = 0; + u32 cmd_port_val; + + RTL_DEBUG("%s(%d)\n", __func__, value); + + value = value == 1 ? RTL_CMD_ENTER_PRTM : RTL_CMD_EXIT_PRTM; + + mutex_lock(&rtl_lock); + + if (ioread8(&rtl_table->rt_status) != value) { + iowrite8(value, &rtl_table->command); + + switch (rtl_cmd_width) { + case 8: + cmd_port_val = ioread8(&rtl_table->cmd_port_value); + RTL_DEBUG("cmd_port_val = %u\n", cmd_port_val); + iowrite8((u8)cmd_port_val, rtl_cmd_addr); + break; + case 16: + cmd_port_val = ioread16(&rtl_table->cmd_port_value); + RTL_DEBUG("cmd_port_val = %u\n", cmd_port_val); + iowrite16((u16)cmd_port_val, rtl_cmd_addr); + break; + case 32: + cmd_port_val = ioread32(&rtl_table->cmd_port_value); + RTL_DEBUG("cmd_port_val = %u\n", cmd_port_val); + iowrite32(cmd_port_val, rtl_cmd_addr); + break; + } + + while (ioread8(&rtl_table->command)) { + msleep(10); + if (count++ > 500) { + pr_err("Hardware not responding to " + "mode switch request\n"); + ret = -EIO; + break; + } + + } + + if (ioread8(&rtl_table->command_status)) { + RTL_DEBUG("command_status reports failed command\n"); + ret = -EIO; + } + } + + mutex_unlock(&rtl_lock); + return ret; +} + +static ssize_t rtl_show_version(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%d\n", (int)ioread8(&rtl_table->version)); +} + +static ssize_t rtl_show_state(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%d\n", ioread8(&rtl_table->rt_status)); +} + +static ssize_t rtl_set_state(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + ssize_t ret; + + if (count < 1 || count > 2) + return -EINVAL; + + switch (buf[0]) { + case '0': + ret = ibm_rtl_write(0); + break; + case '1': + ret = ibm_rtl_write(1); + break; + default: + ret = -EINVAL; + } + if (ret >= 0) + ret = count; + + return ret; +} + +static const struct bus_type rtl_subsys = { + .name = "ibm_rtl", + .dev_name = "ibm_rtl", +}; + +static DEVICE_ATTR(version, S_IRUGO, rtl_show_version, NULL); +static DEVICE_ATTR(state, 0600, rtl_show_state, rtl_set_state); + +static struct device_attribute *rtl_attributes[] = { + &dev_attr_version, + &dev_attr_state, + NULL +}; + + +static int rtl_setup_sysfs(void) { + int ret, i; + + ret = subsys_system_register(&rtl_subsys, NULL); + if (!ret) { + struct device *dev_root = bus_get_dev_root(&rtl_subsys); + + if (dev_root) { + for (i = 0; rtl_attributes[i]; i ++) + device_create_file(dev_root, rtl_attributes[i]); + put_device(dev_root); + } + } + return ret; +} + +static void rtl_teardown_sysfs(void) { + struct device *dev_root = bus_get_dev_root(&rtl_subsys); + int i; + + if (dev_root) { + for (i = 0; rtl_attributes[i]; i ++) + device_remove_file(dev_root, rtl_attributes[i]); + put_device(dev_root); + } + bus_unregister(&rtl_subsys); +} + + +static const struct dmi_system_id ibm_rtl_dmi_table[] __initconst = { + { \ + .matches = { \ + DMI_MATCH(DMI_SYS_VENDOR, "IBM"), \ + }, \ + }, + { } +}; + +static int __init ibm_rtl_init(void) { + unsigned long ebda_addr, ebda_size; + unsigned int ebda_kb; + int ret = -ENODEV, i; + + if (force) + pr_warn("module loaded by force\n"); + /* first ensure that we are running on IBM HW */ + else if (efi_enabled(EFI_BOOT) || !dmi_check_system(ibm_rtl_dmi_table)) + return -ENODEV; + + /* Get the address for the Extended BIOS Data Area */ + ebda_addr = get_bios_ebda(); + if (!ebda_addr) { + RTL_DEBUG("no BIOS EBDA found\n"); + return -ENODEV; + } + + ebda_map = ioremap(ebda_addr, 4); + if (!ebda_map) + return -ENOMEM; + + /* First word in the EDBA is the Size in KB */ + ebda_kb = ioread16(ebda_map); + RTL_DEBUG("EBDA is %d kB\n", ebda_kb); + + if (ebda_kb == 0) + goto out; + + iounmap(ebda_map); + ebda_size = ebda_kb*1024; + + /* Remap the whole table */ + ebda_map = ioremap(ebda_addr, ebda_size); + if (!ebda_map) + return -ENOMEM; + + /* search for the _RTL_ signature at the start of the table */ + for (i = 0 ; i < ebda_size/sizeof(unsigned int); i++) { + struct ibm_rtl_table __iomem * tmp; + tmp = (struct ibm_rtl_table __iomem *) (ebda_map + i*sizeof(unsigned int)); + if ((readq(&tmp->signature) & RTL_MASK) == RTL_SIGNATURE) { + phys_addr_t addr; + unsigned int plen; + RTL_DEBUG("found RTL_SIGNATURE at %p\n", tmp); + rtl_table = tmp; + /* The address, value, width and offset are platform + * dependent and found in the ibm_rtl_table */ + rtl_cmd_width = ioread8(&rtl_table->cmd_granularity); + rtl_cmd_type = ioread8(&rtl_table->cmd_address_type); + RTL_DEBUG("rtl_cmd_width = %u, rtl_cmd_type = %u\n", + rtl_cmd_width, rtl_cmd_type); + addr = ioread32(&rtl_table->cmd_port_address); + RTL_DEBUG("addr = %#llx\n", (unsigned long long)addr); + plen = rtl_cmd_width/sizeof(char); + rtl_cmd_addr = rtl_port_map(addr, plen); + RTL_DEBUG("rtl_cmd_addr = %p\n", rtl_cmd_addr); + if (!rtl_cmd_addr) { + ret = -ENOMEM; + break; + } + ret = rtl_setup_sysfs(); + break; + } + } + +out: + if (ret) { + iounmap(ebda_map); + rtl_port_unmap(rtl_cmd_addr); + } + + return ret; +} + +static void __exit ibm_rtl_exit(void) +{ + if (rtl_table) { + RTL_DEBUG("cleaning up"); + /* do not leave the machine in SMI-free mode */ + ibm_rtl_write(0); + /* unmap, unlink and remove all traces */ + rtl_teardown_sysfs(); + iounmap(ebda_map); + rtl_port_unmap(rtl_cmd_addr); + } +} + +module_init(ibm_rtl_init); +module_exit(ibm_rtl_exit); diff --git a/drivers/platform/x86/inspur_platform_profile.c b/drivers/platform/x86/inspur_platform_profile.c new file mode 100644 index 000000000..e02f5a55a --- /dev/null +++ b/drivers/platform/x86/inspur_platform_profile.c @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Inspur WMI Platform Profile + * + * Copyright (C) 2018 Ai Chao + */ + +#include +#include +#include +#include +#include + +#define WMI_INSPUR_POWERMODE_BIOS_GUID "596C31E3-332D-43C9-AEE9-585493284F5D" + +enum inspur_wmi_method_ids { + INSPUR_WMI_GET_POWERMODE = 0x02, + INSPUR_WMI_SET_POWERMODE = 0x03, +}; + +/* + * Power Mode: + * 0x0: Balance Mode + * 0x1: Performance Mode + * 0x2: Power Saver Mode + */ +enum inspur_tmp_profile { + INSPUR_TMP_PROFILE_BALANCE = 0, + INSPUR_TMP_PROFILE_PERFORMANCE = 1, + INSPUR_TMP_PROFILE_POWERSAVE = 2, +}; + +struct inspur_wmi_priv { + struct wmi_device *wdev; + struct device *ppdev; +}; + +static int inspur_wmi_perform_query(struct wmi_device *wdev, + enum inspur_wmi_method_ids query_id, + void *buffer, size_t insize, + size_t outsize) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer input = { insize, buffer}; + union acpi_object *obj; + acpi_status status; + int ret = 0; + + status = wmidev_evaluate_method(wdev, 0, query_id, &input, &output); + if (ACPI_FAILURE(status)) { + dev_err(&wdev->dev, "EC Powermode control failed: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + obj = output.pointer; + if (!obj) + return -EINVAL; + + if (obj->type != ACPI_TYPE_BUFFER || + obj->buffer.length != outsize) { + ret = -EINVAL; + goto out_free; + } + + memcpy(buffer, obj->buffer.pointer, obj->buffer.length); + +out_free: + kfree(obj); + return ret; +} + +/* + * Set Power Mode to EC RAM. If Power Mode value greater than 0x3, + * return error + * Method ID: 0x3 + * Arg: 4 Bytes + * Byte [0]: Power Mode: + * 0x0: Balance Mode + * 0x1: Performance Mode + * 0x2: Power Saver Mode + * Return Value: 4 Bytes + * Byte [0]: Return Code + * 0x0: No Error + * 0x1: Error + */ +static int inspur_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct inspur_wmi_priv *priv = dev_get_drvdata(dev); + u8 ret_code[4] = {0, 0, 0, 0}; + int ret; + + switch (profile) { + case PLATFORM_PROFILE_BALANCED: + ret_code[0] = INSPUR_TMP_PROFILE_BALANCE; + break; + case PLATFORM_PROFILE_PERFORMANCE: + ret_code[0] = INSPUR_TMP_PROFILE_PERFORMANCE; + break; + case PLATFORM_PROFILE_LOW_POWER: + ret_code[0] = INSPUR_TMP_PROFILE_POWERSAVE; + break; + default: + return -EOPNOTSUPP; + } + + ret = inspur_wmi_perform_query(priv->wdev, INSPUR_WMI_SET_POWERMODE, + ret_code, sizeof(ret_code), + sizeof(ret_code)); + + if (ret < 0) + return ret; + + if (ret_code[0]) + return -EBADRQC; + + return 0; +} + +/* + * Get Power Mode from EC RAM, If Power Mode value greater than 0x3, + * return error + * Method ID: 0x2 + * Return Value: 4 Bytes + * Byte [0]: Return Code + * 0x0: No Error + * 0x1: Error + * Byte [1]: Power Mode + * 0x0: Balance Mode + * 0x1: Performance Mode + * 0x2: Power Saver Mode + */ +static int inspur_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct inspur_wmi_priv *priv = dev_get_drvdata(dev); + u8 ret_code[4] = {0, 0, 0, 0}; + int ret; + + ret = inspur_wmi_perform_query(priv->wdev, INSPUR_WMI_GET_POWERMODE, + &ret_code, sizeof(ret_code), + sizeof(ret_code)); + if (ret < 0) + return ret; + + if (ret_code[0]) + return -EBADRQC; + + switch (ret_code[1]) { + case INSPUR_TMP_PROFILE_BALANCE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case INSPUR_TMP_PROFILE_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case INSPUR_TMP_PROFILE_POWERSAVE: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int inspur_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); + + return 0; +} + +static const struct platform_profile_ops inspur_platform_profile_ops = { + .probe = inspur_platform_profile_probe, + .profile_get = inspur_platform_profile_get, + .profile_set = inspur_platform_profile_set, +}; + +static int inspur_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct inspur_wmi_priv *priv; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + priv->ppdev = devm_platform_profile_register(&wdev->dev, "inspur-wmi", priv, + &inspur_platform_profile_ops); + + return PTR_ERR_OR_ZERO(priv->ppdev); +} + +static const struct wmi_device_id inspur_wmi_id_table[] = { + { .guid_string = WMI_INSPUR_POWERMODE_BIOS_GUID }, + { } +}; + +MODULE_DEVICE_TABLE(wmi, inspur_wmi_id_table); + +static struct wmi_driver inspur_wmi_driver = { + .driver = { + .name = "inspur-wmi-platform-profile", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = inspur_wmi_id_table, + .probe = inspur_wmi_probe, + .no_singleton = true, +}; + +module_wmi_driver(inspur_wmi_driver); + +MODULE_AUTHOR("Ai Chao "); +MODULE_DESCRIPTION("Platform Profile Support for Inspur"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/Kconfig b/drivers/platform/x86/intel/Kconfig new file mode 100644 index 000000000..2900407d6 --- /dev/null +++ b/drivers/platform/x86/intel/Kconfig @@ -0,0 +1,253 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +source "drivers/platform/x86/intel/atomisp2/Kconfig" +source "drivers/platform/x86/intel/ifs/Kconfig" +source "drivers/platform/x86/intel/int1092/Kconfig" +source "drivers/platform/x86/intel/int3472/Kconfig" +source "drivers/platform/x86/intel/pmc/Kconfig" +source "drivers/platform/x86/intel/pmt/Kconfig" +source "drivers/platform/x86/intel/speed_select_if/Kconfig" +source "drivers/platform/x86/intel/telemetry/Kconfig" +source "drivers/platform/x86/intel/wmi/Kconfig" +source "drivers/platform/x86/intel/uncore-frequency/Kconfig" + + +config INTEL_HID_EVENT + tristate "Intel HID Event" + depends on ACPI + depends on INPUT + depends on I2C + select INPUT_SPARSEKMAP + help + This driver provides support for the Intel HID Event hotkey interface. + Some laptops require this driver for hotkey support. + + To compile this driver as a module, choose M here: the module will + be called intel_hid. + +config INTEL_VBTN + tristate "Intel Virtual Button" + depends on ACPI + depends on INPUT + depends on I2C + select INPUT_SPARSEKMAP + help + This driver provides support for the Intel Virtual Button interface. + Some laptops require this driver for power button support. + + To compile this driver as a module, choose M here: the module will + be called intel_vbtn. + +config INTEL_EHL_PSE_IO + tristate "Intel Elkhart Lake PSE I/O driver" + depends on PCI + select AUXILIARY_BUS + help + Select this option to enable Intel Elkhart Lake PSE GPIO and Timed + I/O support. This driver enumerates the PCI parent device and + creates auxiliary child devices for these capabilities. The actual + functionalities are provided by their respective auxiliary drivers. + + To compile this driver as a module, choose M here: the module will + be called intel_ehl_pse_io. + +config INTEL_INT0002_VGPIO + tristate "Intel ACPI INT0002 Virtual GPIO driver" + depends on GPIOLIB && ACPI && PM_SLEEP + select GPIOLIB_IRQCHIP + help + Some peripherals on Bay Trail and Cherry Trail platforms signal a + Power Management Event (PME) to the Power Management Controller (PMC) + to wakeup the system. When this happens software needs to explicitly + clear the PME bus 0 status bit in the GPE0a_STS register to avoid an + IRQ storm on IRQ 9. + + This is modelled in ACPI through the INT0002 ACPI device, which is + called a "Virtual GPIO controller" in ACPI because it defines the + event handler to call when the PME triggers through _AEI and _L02 + methods as would be done for a real GPIO interrupt in ACPI. + + To compile this driver as a module, choose M here: the module will + be called intel_int0002_vgpio. + +config INTEL_OAKTRAIL + tristate "Intel Oaktrail Platform Extras" + depends on ACPI_EC + depends on ACPI_VIDEO || ACPI_VIDEO=n + depends on RFKILL && BACKLIGHT_CLASS_DEVICE && ACPI + help + Intel Oaktrail platform need this driver to provide interfaces to + enable/disable the Camera, WiFi, BT etc. devices. If in doubt, say Y + here; it will only load on supported platforms. + +config INTEL_BXTWC_PMIC_TMU + tristate "Intel Broxton Whiskey Cove TMU Driver" + depends on INTEL_SOC_PMIC_BXTWC + depends on MFD_INTEL_PMC_BXT + select REGMAP + help + Select this driver to use Intel Broxton Whiskey Cove PMIC TMU feature. + This driver enables the alarm wakeup functionality in the TMU unit of + Whiskey Cove PMIC. + +config INTEL_BYTCRC_PWRSRC + tristate "Intel Bay Trail Crystal Cove power source driver" + depends on INTEL_SOC_PMIC + depends on POWER_SUPPLY + help + This option adds a power source driver for Crystal Cove PMICs + on Intel Bay Trail devices. + + To compile this driver as a module, choose M here: the module + will be called intel_bytcrc_pwrsrc. + +config INTEL_CHTDC_TI_PWRBTN + tristate "Intel Cherry Trail Dollar Cove TI power button driver" + depends on INTEL_SOC_PMIC_CHTDC_TI + depends on INPUT + help + This option adds a power button driver for Dollar Cove TI + PMIC on Intel Cherry Trail devices. + + To compile this driver as a module, choose M here: the module + will be called intel_chtdc_ti_pwrbtn. + +config INTEL_CHTWC_INT33FE + tristate "Intel Cherry Trail Whiskey Cove ACPI INT33FE Driver" + depends on X86 && ACPI && I2C && REGULATOR + depends on CHARGER_BQ24190=y || (CHARGER_BQ24190=m && m) + depends on USB_ROLES_INTEL_XHCI=y || (USB_ROLES_INTEL_XHCI=m && m) + depends on TYPEC_MUX_PI3USB30532=y || (TYPEC_MUX_PI3USB30532=m && m) + help + This driver add support for the Intel Cherry Trail Whiskey Cove + INT33FE ACPI device found on the GPD win and the GPD pocket. + + The INT33FE ACPI device on these mini laptops contains I2cSerialBusV2 + resources for a MAX17042 Fuel Gauge, FUSB302 USB Type-C Controller + and PI3USB30532 USB switch. + This driver instantiates i2c-clients for these, so that standard + i2c drivers for these chips can bind to the them. + + If you enable this driver it is advised to also select + CONFIG_TYPEC_FUSB302=m, CONFIG_TYPEC_MUX_PI3USB30532=m and + CONFIG_BATTERY_MAX17042=m. + +config INTEL_ISHTP_ECLITE + tristate "Intel ISHTP eclite controller Driver" + depends on INTEL_ISH_HID + depends on ACPI + help + This driver is for accessing the PSE (Programmable Service Engine) - + an Embedded Controller like IP - using ISHTP (Integrated Sensor Hub + Transport Protocol) to get battery, thermal and UCSI (USB Type-C + Connector System Software Interface) related data from the platform. + Users who don't want to use discrete Embedded Controller on Intel's + Elkhartlake platform can leverage this integrated solution of + ECLite which is part of PSE subsystem. + + To compile this driver as a module, choose M here: the module + will be called intel_ishtp_eclite. + +config INTEL_MRFLD_PWRBTN + tristate "Intel Merrifield Basin Cove power button driver" + depends on INTEL_SOC_PMIC_MRFLD + depends on INPUT + help + This option adds a power button driver for Basin Cove PMIC + on Intel Merrifield devices. + + To compile this driver as a module, choose M here: the module + will be called intel_mrfld_pwrbtn. + +config INTEL_PUNIT_IPC + tristate "Intel P-Unit IPC Driver" + help + This driver provides support for Intel P-Unit Mailbox IPC mechanism, + which is used to bridge the communications between kernel and P-Unit. + +config INTEL_RST + tristate "Intel Rapid Start Technology Driver" + depends on ACPI + help + This driver provides support for modifying parameters on systems + equipped with Intel's Rapid Start Technology. When put in an ACPI + sleep state, these devices will wake after either a configured + timeout or when the system battery reaches a critical state, + automatically copying memory contents to disk. On resume, the + firmware will copy the memory contents back to RAM and resume the OS + as usual. + +config INTEL_SDSI + tristate "Intel On Demand (Software Defined Silicon) Driver" + depends on INTEL_VSEC + depends on X86_64 + help + This driver enables access to the Intel On Demand (formerly Software + Defined Silicon) interface used to provision silicon features with an + authentication certificate and capability license. + + To compile this driver as a module, choose M here: the module will + be called intel_sdsi. + +config INTEL_SMARTCONNECT + tristate "Intel Smart Connect disabling driver" + depends on ACPI + help + Intel Smart Connect is a technology intended to permit devices to + update state by resuming for a short period of time at regular + intervals. If a user enables this functionality under Windows and + then reboots into Linux, the system may remain configured to resume + on suspend. In the absence of any userspace to support it, the system + will then remain awake until something triggers another suspend. + + This driver checks to determine whether the device has Intel Smart + Connect enabled, and if so disables it. + +config INTEL_TPMI_POWER_DOMAINS + tristate + +config INTEL_TPMI + tristate "Intel Topology Aware Register and PM Capsule Interface (TPMI)" + depends on INTEL_VSEC + depends on X86_64 + select INTEL_TPMI_POWER_DOMAINS + help + The Intel Topology Aware Register and PM Capsule Interface (TPMI), + provides enumerable MMIO interface for power management features. + This driver creates devices, so that other PM feature driver can + be loaded for PM specific feature operation. + + To compile this driver as a module, choose M here: the module will + be called intel_vsec_tpmi. + +config INTEL_PLR_TPMI + tristate "Intel SoC TPMI Power Limit Reasons driver" + depends on INTEL_TPMI + help + This driver provides the TPMI power limit reasons status information + via debugfs files. + +config INTEL_TURBO_MAX_3 + bool "Intel Turbo Boost Max Technology 3.0 enumeration driver" + depends on X86_64 && SCHED_MC_PRIO + help + This driver reads maximum performance ratio of each CPU and set up + the scheduler priority metrics. In this way scheduler can prefer + CPU with higher performance to schedule tasks. + + This driver is only required when the system is not using Hardware + P-States (HWP). In HWP mode, priority can be read from ACPI tables. + +config INTEL_VSEC + tristate "Intel Vendor Specific Extended Capabilities Driver" + depends on PCI + select AUXILIARY_BUS + help + Adds support for feature drivers exposed using Intel PCIe VSEC and + DVSEC. + + To compile this driver as a module, choose M here: the module will + be called intel_vsec. diff --git a/drivers/platform/x86/intel/Makefile b/drivers/platform/x86/intel/Makefile new file mode 100644 index 000000000..138b13756 --- /dev/null +++ b/drivers/platform/x86/intel/Makefile @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for drivers/platform/x86/intel +# Intel x86 Platform-Specific Drivers +# + +obj-$(CONFIG_INTEL_ATOMISP2_PDX86) += atomisp2/ +obj-$(CONFIG_INTEL_IFS) += ifs/ +obj-$(CONFIG_INTEL_SAR_INT1092) += int1092/ +obj-$(CONFIG_INTEL_SKL_INT3472) += int3472/ +obj-$(CONFIG_INTEL_PMC_CORE) += pmc/ +obj-$(CONFIG_INTEL_PMT_CLASS) += pmt/ +obj-$(CONFIG_INTEL_SPEED_SELECT_INTERFACE) += speed_select_if/ +obj-$(CONFIG_INTEL_TELEMETRY) += telemetry/ +obj-$(CONFIG_INTEL_WMI) += wmi/ +obj-$(CONFIG_INTEL_UNCORE_FREQ_CONTROL) += uncore-frequency/ + + +# Intel input drivers +intel-target-$(CONFIG_INTEL_HID_EVENT) += hid.o +intel-target-$(CONFIG_INTEL_VBTN) += vbtn.o + +# Intel miscellaneous drivers +intel-target-$(CONFIG_INTEL_EHL_PSE_IO) += ehl_pse_io.o +intel-target-$(CONFIG_INTEL_INT0002_VGPIO) += int0002_vgpio.o +intel-target-$(CONFIG_INTEL_ISHTP_ECLITE) += ishtp_eclite.o +intel-target-$(CONFIG_INTEL_OAKTRAIL) += oaktrail.o +intel-target-$(CONFIG_INTEL_SDSI) += sdsi.o +intel-target-$(CONFIG_INTEL_VSEC) += vsec.o + +# Intel PMIC / PMC / P-Unit drivers +intel-target-$(CONFIG_INTEL_BYTCRC_PWRSRC) += bytcrc_pwrsrc.o +intel-target-$(CONFIG_INTEL_BXTWC_PMIC_TMU) += bxtwc_tmu.o +intel-target-$(CONFIG_INTEL_CHTDC_TI_PWRBTN) += chtdc_ti_pwrbtn.o +intel-target-$(CONFIG_INTEL_CHTWC_INT33FE) += chtwc_int33fe.o +intel-target-$(CONFIG_X86_ANDROID_TABLETS) += crystal_cove_charger.o +intel-target-$(CONFIG_INTEL_MRFLD_PWRBTN) += mrfld_pwrbtn.o +intel-target-$(CONFIG_INTEL_PUNIT_IPC) += punit_ipc.o + +# TPMI drivers +intel-target-$(CONFIG_INTEL_PLR_TPMI) += plr_tpmi.o +intel-target-$(CONFIG_INTEL_TPMI_POWER_DOMAINS) += tpmi_power_domains.o +intel-target-$(CONFIG_INTEL_TPMI) += vsec_tpmi.o + +# Intel Uncore drivers +intel-target-$(CONFIG_INTEL_RST) += rst.o +intel-target-$(CONFIG_INTEL_SMARTCONNECT) += smartconnect.o +intel-target-$(CONFIG_INTEL_TURBO_MAX_3) += turbo_max_3.o + +# Add 'intel' prefix to each module listed in intel-target-* +define INTEL_OBJ_TARGET +intel-$(1)-y := $(1).o +obj-$(2) += intel-$(1).o +endef + +$(foreach target, $(basename $(intel-target-y)), $(eval $(call INTEL_OBJ_TARGET,$(target),y))) +$(foreach target, $(basename $(intel-target-m)), $(eval $(call INTEL_OBJ_TARGET,$(target),m))) diff --git a/drivers/platform/x86/intel/atomisp2/Kconfig b/drivers/platform/x86/intel/atomisp2/Kconfig new file mode 100644 index 000000000..35dd2be9d --- /dev/null +++ b/drivers/platform/x86/intel/atomisp2/Kconfig @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +config INTEL_ATOMISP2_PDX86 + bool + +config INTEL_ATOMISP2_LED + tristate "Intel AtomISP v2 camera LED driver" + depends on GPIOLIB && LEDS_GPIO + select INTEL_ATOMISP2_PDX86 + help + Many Bay Trail and Cherry Trail devices come with a camera attached + to Intel's Image Signal Processor. Linux currently does not have a + driver for these, so they do not work as a camera. Some of these + camera's have a LED which is controlled through a GPIO. + + Some of these devices have a firmware issue where the LED gets turned + on at boot. This driver will turn the LED off at boot and also allows + controlling the LED (repurposing it) through the sysfs LED interface. + + Which GPIO is attached to the LED is usually not described in the + ACPI tables, so this driver contains per-system info about the GPIO + inside the driver, this means that this driver only works on systems + the driver knows about. + + To compile this driver as a module, choose M here: the module + will be called intel_atomisp2_led. + +config INTEL_ATOMISP2_PM + tristate "Intel AtomISP v2 dummy / power-management driver" + depends on PCI && IOSF_MBI && PM + depends on !INTEL_ATOMISP + select INTEL_ATOMISP2_PDX86 + help + Power-management driver for Intel's Image Signal Processor found on + Bay Trail and Cherry Trail devices. This dummy driver's sole purpose + is to turn the ISP off (put it in D3) to save power and to allow + entering of S0ix modes. + + To compile this driver as a module, choose M here: the module + will be called intel_atomisp2_pm. diff --git a/drivers/platform/x86/intel/atomisp2/Makefile b/drivers/platform/x86/intel/atomisp2/Makefile new file mode 100644 index 000000000..96b1e877d --- /dev/null +++ b/drivers/platform/x86/intel/atomisp2/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +intel_atomisp2_led-y := led.o +obj-$(CONFIG_INTEL_ATOMISP2_LED) += intel_atomisp2_led.o +intel_atomisp2_pm-y += pm.o +obj-$(CONFIG_INTEL_ATOMISP2_PM) += intel_atomisp2_pm.o diff --git a/drivers/platform/x86/intel/atomisp2/led.c b/drivers/platform/x86/intel/atomisp2/led.c new file mode 100644 index 000000000..344ea57d9 --- /dev/null +++ b/drivers/platform/x86/intel/atomisp2/led.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for controlling LEDs for cameras connected to the Intel atomisp2 + * The main purpose of this driver is to turn off LEDs which are on at boot. + * + * Copyright (C) 2020 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include + +/* This must be leds-gpio as the leds-gpio driver binds to the name */ +#define DEV_NAME "leds-gpio" + +static const struct gpio_led atomisp2_leds[] = { + { + .name = "atomisp2::camera", + .default_state = LEDS_GPIO_DEFSTATE_OFF, + }, +}; + +static const struct gpio_led_platform_data atomisp2_leds_pdata = { + .num_leds = ARRAY_SIZE(atomisp2_leds), + .leds = atomisp2_leds, +}; + +static struct gpiod_lookup_table asus_t100ta_lookup = { + .dev_id = DEV_NAME, + .table = { + GPIO_LOOKUP_IDX("INT33FC:02", 8, NULL, 0, GPIO_ACTIVE_HIGH), + { } + } +}; + +static struct gpiod_lookup_table asus_t100chi_lookup = { + .dev_id = DEV_NAME, + .table = { + GPIO_LOOKUP_IDX("INT33FC:01", 24, NULL, 0, GPIO_ACTIVE_HIGH), + { } + } +}; + +static const struct dmi_system_id atomisp2_led_systems[] __initconst = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + /* Non exact match to also match T100TAF */ + DMI_MATCH(DMI_PRODUCT_NAME, "T100TA"), + }, + .driver_data = &asus_t100ta_lookup, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "T200TA"), + }, + .driver_data = &asus_t100ta_lookup, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "T100CHI"), + }, + .driver_data = &asus_t100chi_lookup, + }, + {} /* Terminating entry */ +}; +MODULE_DEVICE_TABLE(dmi, atomisp2_led_systems); + +static struct gpiod_lookup_table *gpio_lookup; +static struct platform_device *pdev; + +static int __init atomisp2_led_init(void) +{ + const struct dmi_system_id *system; + + system = dmi_first_match(atomisp2_led_systems); + if (!system) + return -ENODEV; + + gpio_lookup = system->driver_data; + gpiod_add_lookup_table(gpio_lookup); + + pdev = platform_device_register_resndata(NULL, + DEV_NAME, PLATFORM_DEVID_NONE, + NULL, 0, &atomisp2_leds_pdata, + sizeof(atomisp2_leds_pdata)); + if (IS_ERR(pdev)) + gpiod_remove_lookup_table(gpio_lookup); + + return PTR_ERR_OR_ZERO(pdev); +} + +static void __exit atomisp2_led_cleanup(void) +{ + platform_device_unregister(pdev); + gpiod_remove_lookup_table(gpio_lookup); +} + +module_init(atomisp2_led_init); +module_exit(atomisp2_led_cleanup); + +/* + * The ACPI INIT method from Asus WMI's code on the T100TA and T200TA turns the + * LED on (without the WMI interface allowing further control over the LED). + * Ensure we are loaded after asus-nb-wmi so that we turn the LED off again. + */ +MODULE_SOFTDEP("pre: asus_nb_wmi"); +MODULE_AUTHOR("Hans de Goede + * + * Based on various non upstream patches for ISP support: + * Copyright (C) 2010-2017 Intel Corporation. All rights reserved. + * Copyright (c) 2010 Silicon Hive www.siliconhive.com. + */ + +#include +#include +#include +#include +#include + +/* PCI configuration regs */ +#define PCI_INTERRUPT_CTRL 0x9c + +#define PCI_CSI_CONTROL 0xe8 +#define PCI_CSI_CONTROL_PORTS_OFF_MASK 0x7 + +/* IOSF BT_MBI_UNIT_PMC regs */ +#define ISPSSPM0 0x39 +#define ISPSSPM0_ISPSSC_OFFSET 0 +#define ISPSSPM0_ISPSSC_MASK 0x00000003 +#define ISPSSPM0_ISPSSS_OFFSET 24 +#define ISPSSPM0_ISPSSS_MASK 0x03000000 +#define ISPSSPM0_IUNIT_POWER_ON 0x0 +#define ISPSSPM0_IUNIT_POWER_OFF 0x3 + +static int isp_set_power(struct pci_dev *dev, bool enable) +{ + unsigned long timeout; + u32 val = enable ? ISPSSPM0_IUNIT_POWER_ON : ISPSSPM0_IUNIT_POWER_OFF; + + /* Write to ISPSSPM0 bit[1:0] to power on/off the IUNIT */ + iosf_mbi_modify(BT_MBI_UNIT_PMC, MBI_REG_READ, ISPSSPM0, + val, ISPSSPM0_ISPSSC_MASK); + + /* + * There should be no IUNIT access while power-down is + * in progress. HW sighting: 4567865. + * Wait up to 50 ms for the IUNIT to shut down. + * And we do the same for power on. + */ + timeout = jiffies + msecs_to_jiffies(50); + do { + u32 tmp; + + /* Wait until ISPSSPM0 bit[25:24] shows the right value */ + iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, ISPSSPM0, &tmp); + tmp = (tmp & ISPSSPM0_ISPSSS_MASK) >> ISPSSPM0_ISPSSS_OFFSET; + if (tmp == val) + return 0; + + usleep_range(1000, 2000); + } while (time_before(jiffies, timeout)); + + dev_err(&dev->dev, "IUNIT power-%s timeout.\n", enable ? "on" : "off"); + return -EBUSY; +} + +static int isp_probe(struct pci_dev *dev, const struct pci_device_id *id) +{ + pm_runtime_allow(&dev->dev); + pm_runtime_put_sync_suspend(&dev->dev); + + return 0; +} + +static void isp_remove(struct pci_dev *dev) +{ + pm_runtime_get_sync(&dev->dev); + pm_runtime_forbid(&dev->dev); +} + +static int isp_pci_suspend(struct device *dev) +{ + struct pci_dev *pdev = to_pci_dev(dev); + u32 val; + + pci_write_config_dword(pdev, PCI_INTERRUPT_CTRL, 0); + + /* + * MRFLD IUNIT DPHY is located in an always-power-on island + * MRFLD HW design need all CSI ports are disabled before + * powering down the IUNIT. + */ + pci_read_config_dword(pdev, PCI_CSI_CONTROL, &val); + val |= PCI_CSI_CONTROL_PORTS_OFF_MASK; + pci_write_config_dword(pdev, PCI_CSI_CONTROL, val); + + /* + * We lose config space access when punit power gates + * the ISP. Can't use pci_set_power_state() because + * pmcsr won't actually change when we write to it. + */ + pci_save_state(pdev); + pdev->current_state = PCI_D3cold; + isp_set_power(pdev, false); + + return 0; +} + +static int isp_pci_resume(struct device *dev) +{ + struct pci_dev *pdev = to_pci_dev(dev); + + isp_set_power(pdev, true); + pdev->current_state = PCI_D0; + pci_restore_state(pdev); + + return 0; +} + +static UNIVERSAL_DEV_PM_OPS(isp_pm_ops, isp_pci_suspend, + isp_pci_resume, NULL); + +static const struct pci_device_id isp_id_table[] = { + { PCI_VDEVICE(INTEL, 0x0f38), }, + { PCI_VDEVICE(INTEL, 0x22b8), }, + { 0, } +}; +MODULE_DEVICE_TABLE(pci, isp_id_table); + +static struct pci_driver isp_pci_driver = { + .name = "intel_atomisp2_pm", + .id_table = isp_id_table, + .probe = isp_probe, + .remove = isp_remove, + .driver.pm = &isp_pm_ops, +}; + +module_pci_driver(isp_pci_driver); + +MODULE_DESCRIPTION("Intel AtomISP2 dummy / power-management drv (for suspend)"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/bxtwc_tmu.c b/drivers/platform/x86/intel/bxtwc_tmu.c new file mode 100644 index 000000000..b3666704d --- /dev/null +++ b/drivers/platform/x86/intel/bxtwc_tmu.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel BXT Whiskey Cove PMIC TMU driver + * + * Copyright (C) 2016 Intel Corporation. All rights reserved. + * + * This driver adds TMU (Time Management Unit) support for Intel BXT platform. + * It enables the alarm wake-up functionality in the TMU unit of Whiskey Cove + * PMIC. + */ + +#include +#include +#include +#include + +#define BXTWC_TMUIRQ 0x4fb6 +#define BXTWC_MIRQLVL1 0x4e0e +#define BXTWC_MTMUIRQ_REG 0x4fb7 +#define BXTWC_MIRQLVL1_MTMU BIT(1) +#define BXTWC_TMU_WK_ALRM BIT(1) +#define BXTWC_TMU_SYS_ALRM BIT(2) +#define BXTWC_TMU_ALRM_MASK (BXTWC_TMU_WK_ALRM | BXTWC_TMU_SYS_ALRM) +#define BXTWC_TMU_ALRM_IRQ (BXTWC_TMU_WK_ALRM | BXTWC_TMU_SYS_ALRM) + +struct wcove_tmu { + int irq; + struct device *dev; + struct regmap *regmap; +}; + +static irqreturn_t bxt_wcove_tmu_irq_handler(int irq, void *data) +{ + struct wcove_tmu *wctmu = data; + unsigned int tmu_irq; + + /* Read TMU interrupt reg */ + regmap_read(wctmu->regmap, BXTWC_TMUIRQ, &tmu_irq); + if (tmu_irq & BXTWC_TMU_ALRM_IRQ) { + /* clear TMU irq */ + regmap_write(wctmu->regmap, BXTWC_TMUIRQ, tmu_irq); + return IRQ_HANDLED; + } + return IRQ_NONE; +} + +static int bxt_wcove_tmu_probe(struct platform_device *pdev) +{ + struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent); + struct wcove_tmu *wctmu; + int ret; + + wctmu = devm_kzalloc(&pdev->dev, sizeof(*wctmu), GFP_KERNEL); + if (!wctmu) + return -ENOMEM; + + wctmu->dev = &pdev->dev; + wctmu->regmap = pmic->regmap; + + wctmu->irq = platform_get_irq(pdev, 0); + if (wctmu->irq < 0) + return wctmu->irq; + + ret = devm_request_threaded_irq(&pdev->dev, wctmu->irq, + NULL, bxt_wcove_tmu_irq_handler, + IRQF_ONESHOT, "bxt_wcove_tmu", wctmu); + if (ret) { + dev_err(&pdev->dev, "request irq failed: %d,virq: %d\n", + ret, wctmu->irq); + return ret; + } + + /* Unmask TMU second level Wake & System alarm */ + regmap_update_bits(wctmu->regmap, BXTWC_MTMUIRQ_REG, + BXTWC_TMU_ALRM_MASK, 0); + + platform_set_drvdata(pdev, wctmu); + return 0; +} + +static void bxt_wcove_tmu_remove(struct platform_device *pdev) +{ + struct wcove_tmu *wctmu = platform_get_drvdata(pdev); + unsigned int val; + + /* Mask TMU interrupts */ + regmap_read(wctmu->regmap, BXTWC_MIRQLVL1, &val); + regmap_write(wctmu->regmap, BXTWC_MIRQLVL1, + val | BXTWC_MIRQLVL1_MTMU); + regmap_read(wctmu->regmap, BXTWC_MTMUIRQ_REG, &val); + regmap_write(wctmu->regmap, BXTWC_MTMUIRQ_REG, + val | BXTWC_TMU_ALRM_MASK); +} + +#ifdef CONFIG_PM_SLEEP +static int bxtwc_tmu_suspend(struct device *dev) +{ + struct wcove_tmu *wctmu = dev_get_drvdata(dev); + + enable_irq_wake(wctmu->irq); + return 0; +} + +static int bxtwc_tmu_resume(struct device *dev) +{ + struct wcove_tmu *wctmu = dev_get_drvdata(dev); + + disable_irq_wake(wctmu->irq); + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(bxtwc_tmu_pm_ops, bxtwc_tmu_suspend, bxtwc_tmu_resume); + +static const struct platform_device_id bxt_wcove_tmu_id_table[] = { + { .name = "bxt_wcove_tmu" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, bxt_wcove_tmu_id_table); + +static struct platform_driver bxt_wcove_tmu_driver = { + .probe = bxt_wcove_tmu_probe, + .remove = bxt_wcove_tmu_remove, + .driver = { + .name = "bxt_wcove_tmu", + .pm = &bxtwc_tmu_pm_ops, + }, + .id_table = bxt_wcove_tmu_id_table, +}; + +module_platform_driver(bxt_wcove_tmu_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Nilesh Bacchewar "); +MODULE_DESCRIPTION("BXT Whiskey Cove TMU Driver"); diff --git a/drivers/platform/x86/intel/bytcrc_pwrsrc.c b/drivers/platform/x86/intel/bytcrc_pwrsrc.c new file mode 100644 index 000000000..68ac04008 --- /dev/null +++ b/drivers/platform/x86/intel/bytcrc_pwrsrc.c @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Power-source driver for Bay Trail Crystal Cove PMIC + * + * Copyright (c) 2023 Hans de Goede + * + * Based on intel_crystalcove_pwrsrc.c from Android kernel sources, which is: + * Copyright (C) 2013 Intel Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CRYSTALCOVE_PWRSRC_IRQ 0x03 +#define CRYSTALCOVE_SPWRSRC_REG 0x1E +#define CRYSTALCOVE_SPWRSRC_USB BIT(0) +#define CRYSTALCOVE_SPWRSRC_DC BIT(1) +#define CRYSTALCOVE_SPWRSRC_BATTERY BIT(2) +#define CRYSTALCOVE_RESETSRC0_REG 0x20 +#define CRYSTALCOVE_RESETSRC1_REG 0x21 +#define CRYSTALCOVE_WAKESRC_REG 0x22 + +struct crc_pwrsrc_data { + struct regmap *regmap; + struct dentry *debug_dentry; + struct power_supply *psy; + unsigned int resetsrc0; + unsigned int resetsrc1; + unsigned int wakesrc; +}; + +static const char * const pwrsrc_pwrsrc_info[] = { + /* bit 0 */ "USB", + /* bit 1 */ "DC in", + /* bit 2 */ "Battery", + NULL, +}; + +static const char * const pwrsrc_resetsrc0_info[] = { + /* bit 0 */ "SOC reporting a thermal event", + /* bit 1 */ "critical PMIC temperature", + /* bit 2 */ "critical system temperature", + /* bit 3 */ "critical battery temperature", + /* bit 4 */ "VSYS under voltage", + /* bit 5 */ "VSYS over voltage", + /* bit 6 */ "battery removal", + NULL, +}; + +static const char * const pwrsrc_resetsrc1_info[] = { + /* bit 0 */ "VCRIT threshold", + /* bit 1 */ "BATID reporting battery removal", + /* bit 2 */ "user pressing the power button", + NULL, +}; + +static const char * const pwrsrc_wakesrc_info[] = { + /* bit 0 */ "user pressing the power button", + /* bit 1 */ "a battery insertion", + /* bit 2 */ "a USB charger insertion", + /* bit 3 */ "an adapter insertion", + NULL, +}; + +static void crc_pwrsrc_log(struct seq_file *seq, const char *prefix, + const char * const *info, unsigned int reg_val) +{ + int i; + + for (i = 0; info[i]; i++) { + if (reg_val & BIT(i)) + seq_printf(seq, "%s by %s\n", prefix, info[i]); + } +} + +static int pwrsrc_show(struct seq_file *seq, void *unused) +{ + struct crc_pwrsrc_data *data = seq->private; + unsigned int reg_val; + int ret; + + ret = regmap_read(data->regmap, CRYSTALCOVE_SPWRSRC_REG, ®_val); + if (ret) + return ret; + + crc_pwrsrc_log(seq, "System powered", pwrsrc_pwrsrc_info, reg_val); + return 0; +} + +static int resetsrc_show(struct seq_file *seq, void *unused) +{ + struct crc_pwrsrc_data *data = seq->private; + + crc_pwrsrc_log(seq, "Last shutdown caused", pwrsrc_resetsrc0_info, data->resetsrc0); + crc_pwrsrc_log(seq, "Last shutdown caused", pwrsrc_resetsrc1_info, data->resetsrc1); + return 0; +} + +static int wakesrc_show(struct seq_file *seq, void *unused) +{ + struct crc_pwrsrc_data *data = seq->private; + + crc_pwrsrc_log(seq, "Last wake caused", pwrsrc_wakesrc_info, data->wakesrc); + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(pwrsrc); +DEFINE_SHOW_ATTRIBUTE(resetsrc); +DEFINE_SHOW_ATTRIBUTE(wakesrc); + +static int crc_pwrsrc_read_and_clear(struct crc_pwrsrc_data *data, + unsigned int reg, unsigned int *val) +{ + int ret; + + ret = regmap_read(data->regmap, reg, val); + if (ret) + return ret; + + return regmap_write(data->regmap, reg, *val); +} + +static irqreturn_t crc_pwrsrc_irq_handler(int irq, void *_data) +{ + struct crc_pwrsrc_data *data = _data; + unsigned int irq_mask; + + if (regmap_read(data->regmap, CRYSTALCOVE_PWRSRC_IRQ, &irq_mask)) + return IRQ_NONE; + + regmap_write(data->regmap, CRYSTALCOVE_PWRSRC_IRQ, irq_mask); + + power_supply_changed(data->psy); + return IRQ_HANDLED; +} + +static int crc_pwrsrc_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct crc_pwrsrc_data *data = power_supply_get_drvdata(psy); + unsigned int pwrsrc; + int ret; + + if (psp != POWER_SUPPLY_PROP_ONLINE) + return -EINVAL; + + ret = regmap_read(data->regmap, CRYSTALCOVE_SPWRSRC_REG, &pwrsrc); + if (ret) + return ret; + + val->intval = !!(pwrsrc & (CRYSTALCOVE_SPWRSRC_USB | + CRYSTALCOVE_SPWRSRC_DC)); + return 0; +} + +static const enum power_supply_property crc_pwrsrc_psy_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc crc_pwrsrc_psy_desc = { + .name = "crystal_cove_pwrsrc", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = crc_pwrsrc_psy_props, + .num_properties = ARRAY_SIZE(crc_pwrsrc_psy_props), + .get_property = crc_pwrsrc_psy_get_property, +}; + +static int crc_pwrsrc_probe(struct platform_device *pdev) +{ + struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct crc_pwrsrc_data *data; + int irq, ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->regmap = pmic->regmap; + + /* + * Read + clear resetsrc0/1 and wakesrc now, so that they get + * cleared even if the debugfs interface is never used. + * + * Properly clearing the wakesrc is important, leaving bit 0 of it + * set turns reboot into poweroff on some tablets. + */ + ret = crc_pwrsrc_read_and_clear(data, CRYSTALCOVE_RESETSRC0_REG, &data->resetsrc0); + if (ret) + return ret; + + ret = crc_pwrsrc_read_and_clear(data, CRYSTALCOVE_RESETSRC1_REG, &data->resetsrc1); + if (ret) + return ret; + + ret = crc_pwrsrc_read_and_clear(data, CRYSTALCOVE_WAKESRC_REG, &data->wakesrc); + if (ret) + return ret; + + if (device_property_read_bool(dev->parent, "linux,register-pwrsrc-power_supply")) { + struct power_supply_config psy_cfg = { .drv_data = data }; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + data->psy = devm_power_supply_register(dev, &crc_pwrsrc_psy_desc, &psy_cfg); + if (IS_ERR(data->psy)) + return dev_err_probe(dev, PTR_ERR(data->psy), "registering power-supply\n"); + + ret = devm_request_threaded_irq(dev, irq, NULL, + crc_pwrsrc_irq_handler, + IRQF_ONESHOT, KBUILD_MODNAME, data); + if (ret) + return dev_err_probe(dev, ret, "requesting IRQ\n"); + } + + data->debug_dentry = debugfs_create_dir(KBUILD_MODNAME, NULL); + debugfs_create_file("pwrsrc", 0444, data->debug_dentry, data, &pwrsrc_fops); + debugfs_create_file("resetsrc", 0444, data->debug_dentry, data, &resetsrc_fops); + debugfs_create_file("wakesrc", 0444, data->debug_dentry, data, &wakesrc_fops); + + platform_set_drvdata(pdev, data); + return 0; +} + +static void crc_pwrsrc_remove(struct platform_device *pdev) +{ + struct crc_pwrsrc_data *data = platform_get_drvdata(pdev); + + debugfs_remove_recursive(data->debug_dentry); +} + +static struct platform_driver crc_pwrsrc_driver = { + .probe = crc_pwrsrc_probe, + .remove = crc_pwrsrc_remove, + .driver = { + .name = "crystal_cove_pwrsrc", + }, +}; +module_platform_driver(crc_pwrsrc_driver); + +MODULE_ALIAS("platform:crystal_cove_pwrsrc"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_DESCRIPTION("Power-source driver for Bay Trail Crystal Cove PMIC"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/chtdc_ti_pwrbtn.c b/drivers/platform/x86/intel/chtdc_ti_pwrbtn.c new file mode 100644 index 000000000..53f01e198 --- /dev/null +++ b/drivers/platform/x86/intel/chtdc_ti_pwrbtn.c @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Power-button driver for Dollar Cove TI PMIC + * Copyright (C) 2014 Intel Corp + * Copyright (c) 2017 Takashi Iwai + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHTDC_TI_SIRQ_REG 0x3 +#define SIRQ_PWRBTN_REL BIT(0) + +static irqreturn_t chtdc_ti_pwrbtn_interrupt(int irq, void *dev_id) +{ + struct input_dev *input = dev_id; + struct device *dev = input->dev.parent; + struct regmap *regmap = dev_get_drvdata(dev); + int state; + + if (!regmap_read(regmap, CHTDC_TI_SIRQ_REG, &state)) { + dev_dbg(dev, "SIRQ_REG=0x%x\n", state); + input_report_key(input, KEY_POWER, !(state & SIRQ_PWRBTN_REL)); + input_sync(input); + } + + return IRQ_HANDLED; +} + +static int chtdc_ti_pwrbtn_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct intel_soc_pmic *pmic = dev_get_drvdata(dev->parent); + struct input_dev *input; + int irq, err; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + input = devm_input_allocate_device(dev); + if (!input) + return -ENOMEM; + input->name = pdev->name; + input->phys = "power-button/input0"; + input->id.bustype = BUS_HOST; + input_set_capability(input, EV_KEY, KEY_POWER); + err = input_register_device(input); + if (err) + return err; + + dev_set_drvdata(dev, pmic->regmap); + + err = devm_request_threaded_irq(dev, irq, NULL, + chtdc_ti_pwrbtn_interrupt, + IRQF_ONESHOT, KBUILD_MODNAME, input); + if (err) + return err; + + device_init_wakeup(dev, true); + dev_pm_set_wake_irq(dev, irq); + return 0; +} + +static void chtdc_ti_pwrbtn_remove(struct platform_device *pdev) +{ + dev_pm_clear_wake_irq(&pdev->dev); + device_init_wakeup(&pdev->dev, false); +} + +static const struct platform_device_id chtdc_ti_pwrbtn_id_table[] = { + { .name = "chtdc_ti_pwrbtn" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, chtdc_ti_pwrbtn_id_table); + +static struct platform_driver chtdc_ti_pwrbtn_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .probe = chtdc_ti_pwrbtn_probe, + .remove = chtdc_ti_pwrbtn_remove, + .id_table = chtdc_ti_pwrbtn_id_table, +}; +module_platform_driver(chtdc_ti_pwrbtn_driver); + +MODULE_DESCRIPTION("Power-button driver for Dollar Cove TI PMIC"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/chtwc_int33fe.c b/drivers/platform/x86/intel/chtwc_int33fe.c new file mode 100644 index 000000000..d183aa53c --- /dev/null +++ b/drivers/platform/x86/intel/chtwc_int33fe.c @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Cherry Trail ACPI INT33FE pseudo device driver + * + * Copyright (C) 2017 Hans de Goede + * + * Some Intel Cherry Trail based device which ship with Windows 10, have + * this weird INT33FE ACPI device with a CRS table with 4 I2cSerialBusV2 + * resources, for 4 different chips attached to various I²C buses: + * 1. The Whiskey Cove PMIC, which is also described by the INT34D3 ACPI device + * 2. Maxim MAX17047 Fuel Gauge Controller + * 3. FUSB302 USB Type-C Controller + * 4. PI3USB30532 USB switch + * + * So this driver is a stub / pseudo driver whose only purpose is to + * instantiate I²C clients for chips 2 - 4, so that standard I²C drivers + * for these chips can bind to the them. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct cht_int33fe_data { + struct i2c_client *battery_fg; + struct i2c_client *fusb302; + struct i2c_client *pi3usb30532; + struct fwnode_handle *dp; +}; + +/* + * Grrr, I severely dislike buggy BIOS-es. At least one BIOS enumerates + * the max17047 both through the INT33FE ACPI device (it is right there + * in the resources table) as well as through a separate MAX17047 device. + * + * These helpers are used to work around this by checking if an I²C client + * for the max17047 has already been registered. + */ +static int cht_int33fe_check_for_max17047(struct device *dev, void *data) +{ + struct i2c_client **max17047 = data; + struct acpi_device *adev; + + adev = ACPI_COMPANION(dev); + if (!adev) + return 0; + + /* The MAX17047 ACPI node doesn't have an UID, so we don't check that */ + if (!acpi_dev_hid_uid_match(adev, "MAX17047", NULL)) + return 0; + + *max17047 = to_i2c_client(dev); + return 1; +} + +static const char * const max17047_suppliers[] = { "bq24190-charger" }; + +static const struct property_entry max17047_properties[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", max17047_suppliers), + { } +}; + +static const struct software_node max17047_node = { + .name = "max17047", + .properties = max17047_properties, +}; + +/* + * We are not using inline property here because those are constant, + * and we need to adjust this one at runtime to point to real + * software node. + */ +static struct software_node_ref_args fusb302_mux_refs[] = { + SOFTWARE_NODE_REFERENCE(NULL), +}; + +static const struct property_entry fusb302_properties[] = { + PROPERTY_ENTRY_STRING("linux,extcon-name", "cht_wcove_pwrsrc"), + PROPERTY_ENTRY_REF_ARRAY("usb-role-switch", fusb302_mux_refs), + { } +}; + +static const struct software_node fusb302_node = { + .name = "fusb302", + .properties = fusb302_properties, +}; + +#define PDO_FIXED_FLAGS \ + (PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_USB_COMM) + +static const u32 src_pdo[] = { + PDO_FIXED(5000, 1500, PDO_FIXED_FLAGS), +}; + +static const u32 snk_pdo[] = { + PDO_FIXED(5000, 400, PDO_FIXED_FLAGS), + PDO_VAR(5000, 12000, 3000), +}; + +static const struct software_node pi3usb30532_node = { + .name = "pi3usb30532", +}; + +static const struct software_node displayport_node = { + .name = "displayport", +}; + +static const struct property_entry usb_connector_properties[] = { + PROPERTY_ENTRY_STRING("data-role", "dual"), + PROPERTY_ENTRY_STRING("power-role", "dual"), + PROPERTY_ENTRY_STRING("try-power-role", "sink"), + PROPERTY_ENTRY_U32_ARRAY("source-pdos", src_pdo), + PROPERTY_ENTRY_U32_ARRAY("sink-pdos", snk_pdo), + PROPERTY_ENTRY_U32("op-sink-microwatt", 2500000), + PROPERTY_ENTRY_REF("orientation-switch", &pi3usb30532_node), + PROPERTY_ENTRY_REF("mode-switch", &pi3usb30532_node), + PROPERTY_ENTRY_REF("displayport", &displayport_node), + { } +}; + +static const struct software_node usb_connector_node = { + .name = "connector", + .parent = &fusb302_node, + .properties = usb_connector_properties, +}; + +static const struct software_node altmodes_node = { + .name = "altmodes", + .parent = &usb_connector_node, +}; + +static const struct property_entry dp_altmode_properties[] = { + PROPERTY_ENTRY_U16("svid", 0xff01), + PROPERTY_ENTRY_U32("vdo", 0x0c0086), + { } +}; + +static const struct software_node dp_altmode_node = { + .name = "displayport-altmode", + .parent = &altmodes_node, + .properties = dp_altmode_properties, +}; + +static const struct software_node *node_group[] = { + &fusb302_node, + &max17047_node, + &pi3usb30532_node, + &displayport_node, + &usb_connector_node, + &altmodes_node, + &dp_altmode_node, + NULL +}; + +static int cht_int33fe_setup_dp(struct cht_int33fe_data *data) +{ + struct fwnode_handle *fwnode; + struct pci_dev *pdev; + + fwnode = software_node_fwnode(&displayport_node); + if (!fwnode) + return -ENODEV; + + /* First let's find the GPU PCI device */ + pdev = pci_get_class(PCI_CLASS_DISPLAY_VGA << 8, NULL); + if (!pdev || pdev->vendor != PCI_VENDOR_ID_INTEL) { + pci_dev_put(pdev); + return -ENODEV; + } + + /* Then the DP-2 child device node */ + data->dp = device_get_named_child_node(&pdev->dev, "DD04"); + pci_dev_put(pdev); + if (!data->dp) + return -ENODEV; + + fwnode->secondary = ERR_PTR(-ENODEV); + data->dp->secondary = fwnode; + + return 0; +} + +static void cht_int33fe_remove_nodes(struct cht_int33fe_data *data) +{ + software_node_unregister_node_group(node_group); + + if (data->dp) { + data->dp->secondary = NULL; + fwnode_handle_put(data->dp); + data->dp = NULL; + } +} + +static void cht_int33fe_put_swnode(void *data) +{ + struct fwnode_handle *fwnode = data; + + fwnode_handle_put(fwnode); + fusb302_mux_refs[0] = SOFTWARE_NODE_REFERENCE(NULL); +} + +static int cht_int33fe_add_nodes(struct device *dev, struct cht_int33fe_data *data) +{ + const struct software_node *mux_ref_node; + int ret; + + /* + * There is no ACPI device node for the USB role mux, so we need to wait + * until the mux driver has created software node for the mux device. + * It means we depend on the mux driver. This function will return + * -EPROBE_DEFER until the mux device is registered. + * + * FIXME: the relevant software node exists in intel-xhci-usb-role-switch + * and - if exported - could be used to set up a static reference. + */ + mux_ref_node = software_node_find_by_name(NULL, "intel-xhci-usb-sw"); + if (!mux_ref_node) + return -EPROBE_DEFER; + + ret = devm_add_action_or_reset(dev, cht_int33fe_put_swnode, + software_node_fwnode(mux_ref_node)); + if (ret) + return ret; + + /* + * Update node used in "usb-role-switch" property. Note that we + * rely on software_node_register_node_group() to use the original + * instance of properties instead of copying them. + */ + fusb302_mux_refs[0] = SOFTWARE_NODE_REFERENCE(mux_ref_node); + + ret = software_node_register_node_group(node_group); + if (ret) + return ret; + + /* The devices that are not created in this driver need extra steps. */ + + /* + * The DP connector does have ACPI device node. In this case we can just + * find that ACPI node and assign our node as the secondary node to it. + */ + ret = cht_int33fe_setup_dp(data); + if (ret) + goto err_remove_nodes; + + return 0; + +err_remove_nodes: + cht_int33fe_remove_nodes(data); + + return ret; +} + +static int +cht_int33fe_register_max17047(struct device *dev, struct cht_int33fe_data *data) +{ + struct i2c_client *max17047 = NULL; + struct i2c_board_info board_info; + struct fwnode_handle *fwnode; + int ret; + + fwnode = software_node_fwnode(&max17047_node); + if (!fwnode) + return -ENODEV; + + i2c_for_each_dev(&max17047, cht_int33fe_check_for_max17047); + if (max17047) { + /* Pre-existing I²C client for the max17047, add device properties */ + set_secondary_fwnode(&max17047->dev, fwnode); + /* And re-probe to get the new device properties applied */ + ret = device_reprobe(&max17047->dev); + if (ret) + dev_warn(dev, "Reprobing max17047 error: %d\n", ret); + return 0; + } + + memset(&board_info, 0, sizeof(board_info)); + strscpy(board_info.type, "max17047"); + board_info.dev_name = "max17047"; + board_info.fwnode = fwnode; + data->battery_fg = i2c_acpi_new_device(dev, 1, &board_info); + + return PTR_ERR_OR_ZERO(data->battery_fg); +} + +static const struct dmi_system_id cht_int33fe_typec_ids[] = { + { + /* + * GPD win / GPD pocket mini laptops + * + * This DMI match may not seem unique, but it is. In the 67000+ + * DMI decode dumps from linux-hardware.org only 116 have + * board_vendor set to "AMI Corporation" and of those 116 only + * the GPD win's and pocket's board_name is "Default string". + */ + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Default string"), + DMI_EXACT_MATCH(DMI_BOARD_SERIAL, "Default string"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Default string"), + }, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, cht_int33fe_typec_ids); + +static int cht_int33fe_typec_probe(struct platform_device *pdev) +{ + struct i2c_board_info board_info; + struct device *dev = &pdev->dev; + struct cht_int33fe_data *data; + struct fwnode_handle *fwnode; + struct regulator *regulator; + int fusb302_irq; + int ret; + + if (!dmi_check_system(cht_int33fe_typec_ids)) + return -ENODEV; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + /* + * We expect the WC PMIC to be paired with a TI bq24292i charger-IC. + * We check for the bq24292i vbus regulator here, this has 2 purposes: + * 1) The bq24292i allows charging with up to 12V, setting the fusb302's + * max-snk voltage to 12V with another charger-IC is not good. + * 2) For the fusb302 driver to get the bq24292i vbus regulator, the + * regulator-map, which is part of the bq24292i regulator_init_data, + * must be registered before the fusb302 is instantiated, otherwise + * it will end up with a dummy-regulator. + * Note "cht_wc_usb_typec_vbus" comes from the regulator_init_data + * which is defined in i2c-cht-wc.c from where the bq24292i I²C client + * gets instantiated. We use regulator_get_optional here so that we + * don't end up getting a dummy-regulator ourselves. + */ + regulator = regulator_get_optional(dev, "cht_wc_usb_typec_vbus"); + if (IS_ERR(regulator)) { + ret = PTR_ERR(regulator); + return (ret == -ENODEV) ? -EPROBE_DEFER : ret; + } + regulator_put(regulator); + + /* The FUSB302 uses the IRQ at index 1 and is the only IRQ user */ + fusb302_irq = acpi_dev_gpio_irq_get(ACPI_COMPANION(dev), 1); + if (fusb302_irq < 0) { + if (fusb302_irq != -EPROBE_DEFER) + dev_err(dev, "Error getting FUSB302 irq\n"); + return fusb302_irq; + } + + ret = cht_int33fe_add_nodes(dev, data); + if (ret) + return ret; + + /* Work around BIOS bug, see comment on cht_int33fe_check_for_max17047() */ + ret = cht_int33fe_register_max17047(dev, data); + if (ret) + goto out_remove_nodes; + + fwnode = software_node_fwnode(&fusb302_node); + if (!fwnode) { + ret = -ENODEV; + goto out_unregister_max17047; + } + + memset(&board_info, 0, sizeof(board_info)); + strscpy(board_info.type, "typec_fusb302"); + board_info.dev_name = "fusb302"; + board_info.fwnode = fwnode; + board_info.irq = fusb302_irq; + + data->fusb302 = i2c_acpi_new_device(dev, 2, &board_info); + if (IS_ERR(data->fusb302)) { + ret = PTR_ERR(data->fusb302); + goto out_unregister_max17047; + } + + fwnode = software_node_fwnode(&pi3usb30532_node); + if (!fwnode) { + ret = -ENODEV; + goto out_unregister_fusb302; + } + + memset(&board_info, 0, sizeof(board_info)); + board_info.dev_name = "pi3usb30532"; + board_info.fwnode = fwnode; + strscpy(board_info.type, "pi3usb30532"); + + data->pi3usb30532 = i2c_acpi_new_device(dev, 3, &board_info); + if (IS_ERR(data->pi3usb30532)) { + ret = PTR_ERR(data->pi3usb30532); + goto out_unregister_fusb302; + } + + platform_set_drvdata(pdev, data); + + return 0; + +out_unregister_fusb302: + i2c_unregister_device(data->fusb302); + +out_unregister_max17047: + i2c_unregister_device(data->battery_fg); + +out_remove_nodes: + cht_int33fe_remove_nodes(data); + + return ret; +} + +static void cht_int33fe_typec_remove(struct platform_device *pdev) +{ + struct cht_int33fe_data *data = platform_get_drvdata(pdev); + + i2c_unregister_device(data->pi3usb30532); + i2c_unregister_device(data->fusb302); + i2c_unregister_device(data->battery_fg); + + cht_int33fe_remove_nodes(data); +} + +static const struct acpi_device_id cht_int33fe_acpi_ids[] = { + { "INT33FE", }, + { } +}; + +static struct platform_driver cht_int33fe_typec_driver = { + .driver = { + .name = "Intel Cherry Trail ACPI INT33FE Type-C driver", + .acpi_match_table = ACPI_PTR(cht_int33fe_acpi_ids), + }, + .probe = cht_int33fe_typec_probe, + .remove = cht_int33fe_typec_remove, +}; + +module_platform_driver(cht_int33fe_typec_driver); + +MODULE_DESCRIPTION("Intel Cherry Trail ACPI INT33FE Type-C pseudo device driver"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/crystal_cove_charger.c b/drivers/platform/x86/intel/crystal_cove_charger.c new file mode 100644 index 000000000..e4299cfa2 --- /dev/null +++ b/drivers/platform/x86/intel/crystal_cove_charger.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for the external-charger IRQ pass-through function of the + * Intel Bay Trail Crystal Cove PMIC. + * + * Note this is NOT a power_supply class driver, it just deals with IRQ + * pass-through, this requires a separate driver because the PMIC's + * level 2 interrupt for this must be explicitly acked. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define CHGRIRQ_REG 0x0a +#define MCHGRIRQ_REG 0x17 + +struct crystal_cove_charger_data { + struct mutex buslock; /* irq_bus_lock */ + struct irq_chip irqchip; + struct regmap *regmap; + struct irq_domain *irq_domain; + int irq; + int charger_irq; + u8 mask; + u8 new_mask; +}; + +static irqreturn_t crystal_cove_charger_irq(int irq, void *data) +{ + struct crystal_cove_charger_data *charger = data; + + /* No need to read CHGRIRQ_REG as there is only 1 IRQ */ + handle_nested_irq(charger->charger_irq); + + /* Ack CHGRIRQ 0 */ + regmap_write(charger->regmap, CHGRIRQ_REG, BIT(0)); + + return IRQ_HANDLED; +} + +static void crystal_cove_charger_irq_bus_lock(struct irq_data *data) +{ + struct crystal_cove_charger_data *charger = irq_data_get_irq_chip_data(data); + + mutex_lock(&charger->buslock); +} + +static void crystal_cove_charger_irq_bus_sync_unlock(struct irq_data *data) +{ + struct crystal_cove_charger_data *charger = irq_data_get_irq_chip_data(data); + + if (charger->mask != charger->new_mask) { + regmap_write(charger->regmap, MCHGRIRQ_REG, charger->new_mask); + charger->mask = charger->new_mask; + } + + mutex_unlock(&charger->buslock); +} + +static void crystal_cove_charger_irq_unmask(struct irq_data *data) +{ + struct crystal_cove_charger_data *charger = irq_data_get_irq_chip_data(data); + + charger->new_mask &= ~BIT(data->hwirq); +} + +static void crystal_cove_charger_irq_mask(struct irq_data *data) +{ + struct crystal_cove_charger_data *charger = irq_data_get_irq_chip_data(data); + + charger->new_mask |= BIT(data->hwirq); +} + +static void crystal_cove_charger_rm_irq_domain(void *data) +{ + struct crystal_cove_charger_data *charger = data; + + irq_domain_remove(charger->irq_domain); +} + +static int crystal_cove_charger_probe(struct platform_device *pdev) +{ + struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent); + struct crystal_cove_charger_data *charger; + int ret; + + charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + charger->regmap = pmic->regmap; + mutex_init(&charger->buslock); + + charger->irq = platform_get_irq(pdev, 0); + if (charger->irq < 0) + return charger->irq; + + charger->irq_domain = irq_domain_create_linear(dev_fwnode(pdev->dev.parent), 1, + &irq_domain_simple_ops, NULL); + if (!charger->irq_domain) + return -ENOMEM; + + /* Distuingish IRQ domain from others sharing (MFD) the same fwnode */ + irq_domain_update_bus_token(charger->irq_domain, DOMAIN_BUS_WAKEUP); + + ret = devm_add_action_or_reset(&pdev->dev, crystal_cove_charger_rm_irq_domain, charger); + if (ret) + return ret; + + charger->charger_irq = irq_create_mapping(charger->irq_domain, 0); + if (!charger->charger_irq) + return -ENOMEM; + + charger->irqchip.name = KBUILD_MODNAME; + charger->irqchip.irq_unmask = crystal_cove_charger_irq_unmask; + charger->irqchip.irq_mask = crystal_cove_charger_irq_mask; + charger->irqchip.irq_bus_lock = crystal_cove_charger_irq_bus_lock; + charger->irqchip.irq_bus_sync_unlock = crystal_cove_charger_irq_bus_sync_unlock; + + irq_set_chip_data(charger->charger_irq, charger); + irq_set_chip_and_handler(charger->charger_irq, &charger->irqchip, handle_simple_irq); + irq_set_nested_thread(charger->charger_irq, true); + irq_set_noprobe(charger->charger_irq); + + /* Mask the single 2nd level IRQ before enabling the 1st level IRQ */ + charger->mask = charger->new_mask = BIT(0); + regmap_write(charger->regmap, MCHGRIRQ_REG, charger->mask); + + ret = devm_request_threaded_irq(&pdev->dev, charger->irq, NULL, + crystal_cove_charger_irq, + IRQF_ONESHOT, KBUILD_MODNAME, charger); + if (ret) + return dev_err_probe(&pdev->dev, ret, "requesting irq\n"); + + return 0; +} + +static struct platform_driver crystal_cove_charger_driver = { + .probe = crystal_cove_charger_probe, + .driver = { + .name = "crystal_cove_charger", + }, +}; +module_platform_driver(crystal_cove_charger_driver); + +MODULE_AUTHOR("Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define EHL_PSE_IO_DEV_SIZE SZ_4K + +static int ehl_pse_io_dev_create(struct pci_dev *pci, const char *name, int idx) +{ + struct device *dev = &pci->dev; + struct auxiliary_device *adev; + struct ehl_pse_io_data *data; + resource_size_t start, offset; + u32 id; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + id = (pci_domain_nr(pci->bus) << 16) | pci_dev_id(pci); + start = pci_resource_start(pci, 0); + offset = EHL_PSE_IO_DEV_SIZE * idx; + + data->mem = DEFINE_RES_MEM(start + offset, EHL_PSE_IO_DEV_SIZE); + data->irq = pci_irq_vector(pci, idx); + + adev = __devm_auxiliary_device_create(dev, EHL_PSE_IO_NAME, name, data, id); + + return adev ? 0 : -ENODEV; +} + +static int ehl_pse_io_probe(struct pci_dev *pci, const struct pci_device_id *id) +{ + int ret; + + ret = pcim_enable_device(pci); + if (ret) + return ret; + + pci_set_master(pci); + + ret = pci_alloc_irq_vectors(pci, 2, 2, PCI_IRQ_MSI); + if (ret < 0) + return ret; + + ret = ehl_pse_io_dev_create(pci, EHL_PSE_GPIO_NAME, 0); + if (ret) + return ret; + + return ehl_pse_io_dev_create(pci, EHL_PSE_TIO_NAME, 1); +} + +static const struct pci_device_id ehl_pse_io_ids[] = { + { PCI_VDEVICE(INTEL, 0x4b88) }, + { PCI_VDEVICE(INTEL, 0x4b89) }, + { } +}; +MODULE_DEVICE_TABLE(pci, ehl_pse_io_ids); + +static struct pci_driver ehl_pse_io_driver = { + .name = EHL_PSE_IO_NAME, + .id_table = ehl_pse_io_ids, + .probe = ehl_pse_io_probe, +}; +module_pci_driver(ehl_pse_io_driver); + +MODULE_AUTHOR("Raag Jadav "); +MODULE_DESCRIPTION("Intel Elkhart Lake PSE I/O driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/hid.c b/drivers/platform/x86/intel/hid.c new file mode 100644 index 000000000..acd42e639 --- /dev/null +++ b/drivers/platform/x86/intel/hid.c @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Intel HID event & 5 button array driver + * + * Copyright (C) 2015 Alex Hung + * Copyright (C) 2015 Andrew Lutomirski + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../dual_accel_detect.h" + +enum intel_hid_tablet_sw_mode { + TABLET_SW_AUTO = -1, + TABLET_SW_OFF = 0, + TABLET_SW_AT_EVENT, + TABLET_SW_AT_PROBE, +}; + +static bool enable_5_button_array; +module_param(enable_5_button_array, bool, 0444); +MODULE_PARM_DESC(enable_5_button_array, + "Enable 5 Button Array support. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static int enable_sw_tablet_mode = TABLET_SW_AUTO; +module_param(enable_sw_tablet_mode, int, 0444); +MODULE_PARM_DESC(enable_sw_tablet_mode, + "Enable SW_TABLET_MODE reporting -1:auto 0:off 1:at-first-event 2:at-probe. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +/* When NOT in tablet mode, VGBS returns with the flag 0x40 */ +#define TABLET_MODE_FLAG BIT(6) + +MODULE_DESCRIPTION("Intel HID Event hotkey driver"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Alex Hung"); + +static const struct acpi_device_id intel_hid_ids[] = { + { "INT33D5" }, + { "INTC1051" }, + { "INTC1054" }, + { "INTC1070" }, + { "INTC1076" }, + { "INTC1077" }, + { "INTC1078" }, + { "INTC107B" }, + { "INTC10CB" }, + { "INTC10CC" }, + { "INTC10F1" }, + { "INTC10F2" }, + { } +}; +MODULE_DEVICE_TABLE(acpi, intel_hid_ids); + +/* In theory, these are HID usages. */ +static const struct key_entry intel_hid_keymap[] = { + /* 1: LSuper (Page 0x07, usage 0xE3) -- unclear what to do */ + /* 2: Toggle SW_ROTATE_LOCK -- easy to implement if seen in wild */ + { KE_KEY, 3, { KEY_NUMLOCK } }, + { KE_KEY, 4, { KEY_HOME } }, + { KE_KEY, 5, { KEY_END } }, + { KE_KEY, 6, { KEY_PAGEUP } }, + { KE_KEY, 7, { KEY_PAGEDOWN } }, + { KE_KEY, 8, { KEY_RFKILL } }, + { KE_KEY, 9, { KEY_POWER } }, + { KE_KEY, 11, { KEY_SLEEP } }, + /* 13 has two different meanings in the spec -- ignore it. */ + { KE_KEY, 14, { KEY_STOPCD } }, + { KE_KEY, 15, { KEY_PLAYPAUSE } }, + { KE_KEY, 16, { KEY_MUTE } }, + { KE_KEY, 17, { KEY_VOLUMEUP } }, + { KE_KEY, 18, { KEY_VOLUMEDOWN } }, + { KE_KEY, 19, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 20, { KEY_BRIGHTNESSDOWN } }, + /* 27: wake -- needs special handling */ + { KE_END }, +}; + +/* 5 button array notification value. */ +static const struct key_entry intel_array_keymap[] = { + { KE_KEY, 0xC2, { KEY_LEFTMETA } }, /* Press */ + { KE_IGNORE, 0xC3, { KEY_LEFTMETA } }, /* Release */ + { KE_KEY, 0xC4, { KEY_VOLUMEUP } }, /* Press */ + { KE_IGNORE, 0xC5, { KEY_VOLUMEUP } }, /* Release */ + { KE_KEY, 0xC6, { KEY_VOLUMEDOWN } }, /* Press */ + { KE_IGNORE, 0xC7, { KEY_VOLUMEDOWN } }, /* Release */ + { KE_KEY, 0xC8, { KEY_ROTATE_LOCK_TOGGLE } }, /* Press */ + { KE_IGNORE, 0xC9, { KEY_ROTATE_LOCK_TOGGLE } }, /* Release */ + { KE_KEY, 0xCE, { KEY_POWER } }, /* Press */ + { KE_IGNORE, 0xCF, { KEY_POWER } }, /* Release */ + { KE_END }, +}; + +static const struct dmi_system_id button_array_table[] = { + { + .ident = "Wacom MobileStudio Pro 13", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Wacom Co.,Ltd"), + DMI_MATCH(DMI_PRODUCT_NAME, "Wacom MobileStudio Pro 13"), + }, + }, + { + .ident = "Wacom MobileStudio Pro 16", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Wacom Co.,Ltd"), + DMI_MATCH(DMI_PRODUCT_NAME, "Wacom MobileStudio Pro 16"), + }, + }, + { + .ident = "HP Spectre x2 (2015)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Spectre x2 Detachable"), + }, + }, + { + .ident = "Lenovo ThinkPad X1 Tablet Gen 1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "ThinkPad X12 Detachable Gen 1"), + }, + }, + { + .ident = "Lenovo ThinkPad X1 Tablet Gen 2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "ThinkPad X1 Tablet Gen 2"), + }, + }, + { + .ident = "Lenovo ThinkPad X1 Fold 16 Gen 1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "ThinkPad X1 Fold 16 Gen 1"), + }, + }, + { + .ident = "Microsoft Surface Go 3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "Surface Go 3"), + }, + }, + { + .ident = "Microsoft Surface Go 4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "Surface Go 4"), + }, + }, + { + .ident = "HP ProBook x360 440 G1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP ProBook x360 440 G1"), + }, + }, + { } +}; + +/* + * Some convertible use the intel-hid ACPI interface to report SW_TABLET_MODE, + * these need to be compared via a DMI based authorization list because some + * models have unreliable VGBS return which could cause incorrect + * SW_TABLET_MODE report. + */ +static const struct dmi_system_id dmi_vgbs_allow_list[] = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Spectre x360 Convertible 15-df0xxx"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "Surface Go"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Elite Dragonfly G2 Notebook PC"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell Pro Rugged 10 Tablet RA00260"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell Pro Rugged 12 Tablet RA02260"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell 14 Plus 2-in-1 DB04250"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Dell 16 Plus 2-in-1 DB06250"), + }, + }, + { } +}; + +/* + * Some devices, even non convertible ones, can send incorrect SW_TABLET_MODE + * reports. Accept such reports only from devices in this list. + */ +static const struct dmi_system_id dmi_auto_add_switch[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */), + }, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */), + }, + }, + {} /* Array terminator */ +}; + +struct intel_hid_priv { + struct mutex mutex; /* Avoid notify_handler() racing with itself */ + struct input_dev *input_dev; + struct input_dev *array; + struct input_dev *switches; + bool wakeup_mode; +}; + +#define HID_EVENT_FILTER_UUID "eeec56b3-4442-408f-a792-4edd4d758054" + +enum intel_hid_dsm_fn_codes { + INTEL_HID_DSM_FN_INVALID, + INTEL_HID_DSM_BTNL_FN, + INTEL_HID_DSM_HDMM_FN, + INTEL_HID_DSM_HDSM_FN, + INTEL_HID_DSM_HDEM_FN, + INTEL_HID_DSM_BTNS_FN, + INTEL_HID_DSM_BTNE_FN, + INTEL_HID_DSM_HEBC_V1_FN, + INTEL_HID_DSM_VGBS_FN, + INTEL_HID_DSM_HEBC_V2_FN, + INTEL_HID_DSM_FN_MAX +}; + +static const char *intel_hid_dsm_fn_to_method[INTEL_HID_DSM_FN_MAX] = { + NULL, + "BTNL", + "HDMM", + "HDSM", + "HDEM", + "BTNS", + "BTNE", + "HEBC", + "VGBS", + "HEBC" +}; + +static unsigned long long intel_hid_dsm_fn_mask; +static guid_t intel_dsm_guid; + +static bool intel_hid_execute_method(acpi_handle handle, + enum intel_hid_dsm_fn_codes fn_index, + unsigned long long arg) +{ + union acpi_object *obj, argv4, req; + acpi_status status; + char *method_name; + + if (fn_index <= INTEL_HID_DSM_FN_INVALID || + fn_index >= INTEL_HID_DSM_FN_MAX) + return false; + + method_name = (char *)intel_hid_dsm_fn_to_method[fn_index]; + + if (!(intel_hid_dsm_fn_mask & BIT(fn_index))) + goto skip_dsm_exec; + + /* All methods expects a package with one integer element */ + req.type = ACPI_TYPE_INTEGER; + req.integer.value = arg; + + argv4.type = ACPI_TYPE_PACKAGE; + argv4.package.count = 1; + argv4.package.elements = &req; + + obj = acpi_evaluate_dsm(handle, &intel_dsm_guid, 1, fn_index, &argv4); + if (obj) { + acpi_handle_debug(handle, "Exec DSM Fn code: %d[%s] success\n", + fn_index, method_name); + ACPI_FREE(obj); + return true; + } + +skip_dsm_exec: + status = acpi_execute_simple_method(handle, method_name, arg); + if (ACPI_SUCCESS(status)) + return true; + + return false; +} + +static bool intel_hid_evaluate_method(acpi_handle handle, + enum intel_hid_dsm_fn_codes fn_index, + unsigned long long *result) +{ + union acpi_object *obj; + acpi_status status; + char *method_name; + + if (fn_index <= INTEL_HID_DSM_FN_INVALID || + fn_index >= INTEL_HID_DSM_FN_MAX) + return false; + + method_name = (char *)intel_hid_dsm_fn_to_method[fn_index]; + + if (!(intel_hid_dsm_fn_mask & BIT(fn_index))) + goto skip_dsm_eval; + + obj = acpi_evaluate_dsm_typed(handle, &intel_dsm_guid, + 1, fn_index, + NULL, ACPI_TYPE_INTEGER); + if (obj) { + *result = obj->integer.value; + acpi_handle_debug(handle, + "Eval DSM Fn code: %d[%s] results: 0x%llx\n", + fn_index, method_name, *result); + ACPI_FREE(obj); + return true; + } + +skip_dsm_eval: + status = acpi_evaluate_integer(handle, method_name, NULL, result); + if (ACPI_SUCCESS(status)) + return true; + + return false; +} + +static void intel_hid_init_dsm(acpi_handle handle) +{ + union acpi_object *obj; + + guid_parse(HID_EVENT_FILTER_UUID, &intel_dsm_guid); + + obj = acpi_evaluate_dsm_typed(handle, &intel_dsm_guid, 1, 0, NULL, + ACPI_TYPE_BUFFER); + if (obj) { + switch (obj->buffer.length) { + default: + case 2: + intel_hid_dsm_fn_mask = *(u16 *)obj->buffer.pointer; + break; + case 1: + intel_hid_dsm_fn_mask = *obj->buffer.pointer; + break; + case 0: + acpi_handle_warn(handle, "intel_hid_dsm_fn_mask length is zero\n"); + intel_hid_dsm_fn_mask = 0; + break; + } + ACPI_FREE(obj); + } + + acpi_handle_debug(handle, "intel_hid_dsm_fn_mask = %llx\n", + intel_hid_dsm_fn_mask); +} + +static int intel_hid_set_enable(struct device *device, bool enable) +{ + acpi_handle handle = ACPI_HANDLE(device); + + /* Enable|disable features - power button is always enabled */ + if (!intel_hid_execute_method(handle, INTEL_HID_DSM_HDSM_FN, enable)) { + dev_warn(device, "failed to %s hotkeys\n", str_enable_disable(enable)); + return -EIO; + } + + return 0; +} + +static void intel_button_array_enable(struct device *device, bool enable) +{ + struct intel_hid_priv *priv = dev_get_drvdata(device); + acpi_handle handle = ACPI_HANDLE(device); + unsigned long long button_cap; + acpi_status status; + + if (!priv->array) + return; + + /* Query supported platform features */ + status = acpi_evaluate_integer(handle, "BTNC", NULL, &button_cap); + if (ACPI_FAILURE(status)) { + dev_warn(device, "failed to get button capability\n"); + return; + } + + /* Enable|disable features - power button is always enabled */ + if (!intel_hid_execute_method(handle, INTEL_HID_DSM_BTNE_FN, + enable ? button_cap : 1)) + dev_warn(device, "failed to set button capability\n"); +} + +static int intel_hid_pm_prepare(struct device *device) +{ + if (device_may_wakeup(device)) { + struct intel_hid_priv *priv = dev_get_drvdata(device); + + priv->wakeup_mode = true; + } + return 0; +} + +static void intel_hid_pm_complete(struct device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(device); + + priv->wakeup_mode = false; +} + +static int intel_hid_pl_suspend_handler(struct device *device) +{ + intel_button_array_enable(device, false); + + if (!pm_suspend_no_platform()) + intel_hid_set_enable(device, false); + + return 0; +} + +static int intel_hid_pl_freeze_handler(struct device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(device); + + priv->wakeup_mode = false; + return intel_hid_pl_suspend_handler(device); +} + +static int intel_hid_pl_resume_handler(struct device *device) +{ + intel_hid_pm_complete(device); + + if (!pm_suspend_no_platform()) + intel_hid_set_enable(device, true); + + intel_button_array_enable(device, true); + return 0; +} + +static const struct dev_pm_ops intel_hid_pl_pm_ops = { + .prepare = intel_hid_pm_prepare, + .complete = intel_hid_pm_complete, + .freeze = intel_hid_pl_freeze_handler, + .thaw = intel_hid_pl_resume_handler, + .restore = intel_hid_pl_resume_handler, + .suspend = intel_hid_pl_suspend_handler, + .resume = intel_hid_pl_resume_handler, +}; + +static int intel_hid_input_setup(struct platform_device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(&device->dev); + int ret; + + priv->input_dev = devm_input_allocate_device(&device->dev); + if (!priv->input_dev) + return -ENOMEM; + + ret = sparse_keymap_setup(priv->input_dev, intel_hid_keymap, NULL); + if (ret) + return ret; + + priv->input_dev->name = "Intel HID events"; + priv->input_dev->id.bustype = BUS_HOST; + + return input_register_device(priv->input_dev); +} + +static int intel_button_array_input_setup(struct platform_device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(&device->dev); + int ret; + + /* Setup input device for 5 button array */ + priv->array = devm_input_allocate_device(&device->dev); + if (!priv->array) + return -ENOMEM; + + ret = sparse_keymap_setup(priv->array, intel_array_keymap, NULL); + if (ret) + return ret; + + priv->array->name = "Intel HID 5 button array"; + priv->array->id.bustype = BUS_HOST; + + return input_register_device(priv->array); +} + +static int intel_hid_switches_setup(struct platform_device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(&device->dev); + + /* Setup input device for switches */ + priv->switches = devm_input_allocate_device(&device->dev); + if (!priv->switches) + return -ENOMEM; + + __set_bit(EV_SW, priv->switches->evbit); + __set_bit(SW_TABLET_MODE, priv->switches->swbit); + + priv->switches->name = "Intel HID switches"; + priv->switches->id.bustype = BUS_HOST; + return input_register_device(priv->switches); +} + +static void report_tablet_mode_state(struct platform_device *device) +{ + struct intel_hid_priv *priv = dev_get_drvdata(&device->dev); + acpi_handle handle = ACPI_HANDLE(&device->dev); + unsigned long long vgbs; + int m; + + if (!intel_hid_evaluate_method(handle, INTEL_HID_DSM_VGBS_FN, &vgbs)) + return; + + m = !(vgbs & TABLET_MODE_FLAG); + input_report_switch(priv->switches, SW_TABLET_MODE, m); + input_sync(priv->switches); +} + +static bool report_tablet_mode_event(struct input_dev *input_dev, u32 event) +{ + if (!input_dev) + return false; + + switch (event) { + case 0xcc: + input_report_switch(input_dev, SW_TABLET_MODE, 1); + input_sync(input_dev); + return true; + case 0xcd: + input_report_switch(input_dev, SW_TABLET_MODE, 0); + input_sync(input_dev); + return true; + default: + return false; + } +} + +static void notify_handler(acpi_handle handle, u32 event, void *context) +{ + struct platform_device *device = context; + struct intel_hid_priv *priv = dev_get_drvdata(&device->dev); + unsigned long long ev_index; + struct key_entry *ke; + int err; + + guard(mutex)(&priv->mutex); + + /* + * Some convertible have unreliable VGBS return which could cause incorrect + * SW_TABLET_MODE report, in these cases we enable support when receiving + * the first event instead of during driver setup. + */ + if (!priv->switches && enable_sw_tablet_mode == TABLET_SW_AT_EVENT && + (event == 0xcc || event == 0xcd)) { + dev_info(&device->dev, "switch event received, enable switches supports\n"); + err = intel_hid_switches_setup(device); + if (err) + pr_err("Failed to setup Intel HID switches\n"); + } + + if (priv->wakeup_mode) { + /* + * Needed for wakeup from suspend-to-idle to work on some + * platforms that don't expose the 5-button array, but still + * send notifies with the power button event code to this + * device object on power button actions while suspended. + */ + if (event == 0xce) + goto wakeup; + + /* + * Some devices send (duplicate) tablet-mode events when moved + * around even though the mode has not changed; and they do this + * even when suspended. + * Update the switch state in case it changed and then return + * without waking up to avoid spurious wakeups. + */ + if (event == 0xcc || event == 0xcd) { + report_tablet_mode_event(priv->switches, event); + return; + } + + /* Wake up on 5-button array events only. */ + if (event == 0xc0 || !priv->array) + return; + + ke = sparse_keymap_entry_from_scancode(priv->array, event); + if (!ke) { + dev_info(&device->dev, "unknown event 0x%x\n", event); + return; + } + + if (ke->type == KE_IGNORE) + return; + +wakeup: + pm_wakeup_hard_event(&device->dev); + + return; + } + + /* + * Needed for suspend to work on some platforms that don't expose + * the 5-button array, but still send notifies with power button + * event code to this device object on power button actions. + * + * Report the power button press and release. + */ + if (!priv->array) { + if (event == 0xce) { + input_report_key(priv->input_dev, KEY_POWER, 1); + input_sync(priv->input_dev); + return; + } + + if (event == 0xcf) { + input_report_key(priv->input_dev, KEY_POWER, 0); + input_sync(priv->input_dev); + return; + } + } + + if (report_tablet_mode_event(priv->switches, event)) + return; + + /* 0xC0 is for HID events, other values are for 5 button array */ + if (event != 0xc0) { + if (!priv->array || + !sparse_keymap_report_event(priv->array, event, 1, true)) + dev_dbg(&device->dev, "unknown event 0x%x\n", event); + return; + } + + if (!intel_hid_evaluate_method(handle, INTEL_HID_DSM_HDEM_FN, + &ev_index)) { + dev_warn(&device->dev, "failed to get event index\n"); + return; + } + + if (!sparse_keymap_report_event(priv->input_dev, ev_index, 1, true)) + dev_dbg(&device->dev, "unknown event index 0x%llx\n", + ev_index); +} + +static bool button_array_present(struct platform_device *device) +{ + acpi_handle handle = ACPI_HANDLE(&device->dev); + unsigned long long event_cap; + + if (intel_hid_evaluate_method(handle, INTEL_HID_DSM_HEBC_V2_FN, + &event_cap)) { + /* Check presence of 5 button array or v2 power button */ + if (event_cap & 0x60000) + return true; + } + + if (intel_hid_evaluate_method(handle, INTEL_HID_DSM_HEBC_V1_FN, + &event_cap)) { + if (event_cap & 0x20000) + return true; + } + + if (enable_5_button_array || dmi_check_system(button_array_table)) + return true; + + return false; +} + +static int intel_hid_probe(struct platform_device *device) +{ + unsigned long long mode, dummy; + struct intel_hid_priv *priv; + acpi_handle handle; + acpi_status status; + int err; + + handle = ACPI_HANDLE(&device->dev); + if (!handle) + return -ENODEV; + + intel_hid_init_dsm(handle); + + if (!intel_hid_evaluate_method(handle, INTEL_HID_DSM_HDMM_FN, &mode)) { + dev_warn(&device->dev, "failed to read mode\n"); + return -ENODEV; + } + + if (mode != 0) { + /* + * This driver only implements "simple" mode. There appear + * to be no other modes, but we should be paranoid and check + * for compatibility. + */ + dev_info(&device->dev, "platform is not in simple mode\n"); + return -ENODEV; + } + + priv = devm_kzalloc(&device->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + dev_set_drvdata(&device->dev, priv); + + err = devm_mutex_init(&device->dev, &priv->mutex); + if (err) + return err; + + /* See dual_accel_detect.h for more info on the dual_accel check. */ + if (enable_sw_tablet_mode == TABLET_SW_AUTO) { + if (dmi_check_system(dmi_vgbs_allow_list)) + enable_sw_tablet_mode = TABLET_SW_AT_PROBE; + else if (dmi_check_system(dmi_auto_add_switch) && !dual_accel_detect()) + enable_sw_tablet_mode = TABLET_SW_AT_EVENT; + else + enable_sw_tablet_mode = TABLET_SW_OFF; + } + + err = intel_hid_input_setup(device); + if (err) { + pr_err("Failed to setup Intel HID hotkeys\n"); + return err; + } + + /* Setup 5 button array */ + if (button_array_present(device)) { + dev_info(&device->dev, "platform supports 5 button array\n"); + err = intel_button_array_input_setup(device); + if (err) + pr_err("Failed to setup Intel 5 button array hotkeys\n"); + } + + /* Setup switches for devices that we know VGBS return correctly */ + if (enable_sw_tablet_mode == TABLET_SW_AT_PROBE) { + dev_info(&device->dev, "platform supports switches\n"); + err = intel_hid_switches_setup(device); + if (err) + pr_err("Failed to setup Intel HID switches\n"); + else + report_tablet_mode_state(device); + } + + status = acpi_install_notify_handler(handle, + ACPI_DEVICE_NOTIFY, + notify_handler, + device); + if (ACPI_FAILURE(status)) + return -EBUSY; + + err = intel_hid_set_enable(&device->dev, true); + if (err) + goto err_remove_notify; + + intel_button_array_enable(&device->dev, true); + + /* + * Call button load method to enable HID power button + * Always do this since it activates events on some devices without + * a button array too. + */ + if (!intel_hid_evaluate_method(handle, INTEL_HID_DSM_BTNL_FN, &dummy)) + dev_warn(&device->dev, "failed to enable HID power button\n"); + + device_init_wakeup(&device->dev, true); + /* + * In order for system wakeup to work, the EC GPE has to be marked as + * a wakeup one, so do that here (this setting will persist, but it has + * no effect until the wakeup mask is set for the EC GPE). + */ + acpi_ec_mark_gpe_for_wake(); + return 0; + +err_remove_notify: + acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, notify_handler); + + return err; +} + +static void intel_hid_remove(struct platform_device *device) +{ + acpi_handle handle = ACPI_HANDLE(&device->dev); + + device_init_wakeup(&device->dev, false); + acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, notify_handler); + intel_hid_set_enable(&device->dev, false); + intel_button_array_enable(&device->dev, false); +} + +static struct platform_driver intel_hid_pl_driver = { + .driver = { + .name = "intel-hid", + .acpi_match_table = intel_hid_ids, + .pm = &intel_hid_pl_pm_ops, + }, + .probe = intel_hid_probe, + .remove = intel_hid_remove, +}; + +module_platform_driver(intel_hid_pl_driver); diff --git a/drivers/platform/x86/intel/ifs/Kconfig b/drivers/platform/x86/intel/ifs/Kconfig new file mode 100644 index 000000000..3eded9667 --- /dev/null +++ b/drivers/platform/x86/intel/ifs/Kconfig @@ -0,0 +1,12 @@ +config INTEL_IFS + tristate "Intel In Field Scan" + depends on X86 && CPU_SUP_INTEL && 64BIT && SMP + help + Enable support for the In Field Scan capability in select + CPUs. The capability allows for running low level tests via + a scan image distributed by Intel via Github to validate CPU + operation beyond baseline RAS capabilities. To compile this + support as a module, choose M here. The module will be called + intel_ifs. + + If unsure, say N. diff --git a/drivers/platform/x86/intel/ifs/Makefile b/drivers/platform/x86/intel/ifs/Makefile new file mode 100644 index 000000000..c3e417bce --- /dev/null +++ b/drivers/platform/x86/intel/ifs/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_INTEL_IFS) += intel_ifs.o + +intel_ifs-y := core.o load.o runtest.o sysfs.o diff --git a/drivers/platform/x86/intel/ifs/core.c b/drivers/platform/x86/intel/ifs/core.c new file mode 100644 index 000000000..3dafa28a1 --- /dev/null +++ b/drivers/platform/x86/intel/ifs/core.c @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2022 Intel Corporation. */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "ifs.h" + +#define X86_MATCH(vfm, array_gen) \ + X86_MATCH_VFM_FEATURE(vfm, X86_FEATURE_CORE_CAPABILITIES, array_gen) + +static const struct x86_cpu_id ifs_cpu_ids[] __initconst = { + X86_MATCH(INTEL_SAPPHIRERAPIDS_X, ARRAY_GEN0), + X86_MATCH(INTEL_EMERALDRAPIDS_X, ARRAY_GEN0), + X86_MATCH(INTEL_GRANITERAPIDS_X, ARRAY_GEN0), + X86_MATCH(INTEL_GRANITERAPIDS_D, ARRAY_GEN0), + X86_MATCH(INTEL_ATOM_CRESTMONT_X, ARRAY_GEN1), + X86_MATCH(INTEL_ATOM_DARKMONT_X, ARRAY_GEN1), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, ifs_cpu_ids); + +ATTRIBUTE_GROUPS(plat_ifs); +ATTRIBUTE_GROUPS(plat_ifs_array); + +bool *ifs_pkg_auth; + +static const struct ifs_test_caps scan_test = { + .integrity_cap_bit = MSR_INTEGRITY_CAPS_PERIODIC_BIST_BIT, + .test_num = IFS_TYPE_SAF, + .image_suffix = "scan", +}; + +static const struct ifs_test_caps array_test = { + .integrity_cap_bit = MSR_INTEGRITY_CAPS_ARRAY_BIST_BIT, + .test_num = IFS_TYPE_ARRAY_BIST, +}; + +static const struct ifs_test_msrs scan_msrs = { + .copy_hashes = MSR_COPY_SCAN_HASHES, + .copy_hashes_status = MSR_SCAN_HASHES_STATUS, + .copy_chunks = MSR_AUTHENTICATE_AND_COPY_CHUNK, + .copy_chunks_status = MSR_CHUNKS_AUTHENTICATION_STATUS, + .test_ctrl = MSR_SAF_CTRL, +}; + +static const struct ifs_test_msrs sbaf_msrs = { + .copy_hashes = MSR_COPY_SBAF_HASHES, + .copy_hashes_status = MSR_SBAF_HASHES_STATUS, + .copy_chunks = MSR_AUTHENTICATE_AND_COPY_SBAF_CHUNK, + .copy_chunks_status = MSR_SBAF_CHUNKS_AUTHENTICATION_STATUS, + .test_ctrl = MSR_SBAF_CTRL, +}; + +static const struct ifs_test_caps sbaf_test = { + .integrity_cap_bit = MSR_INTEGRITY_CAPS_SBAF_BIT, + .test_num = IFS_TYPE_SBAF, + .image_suffix = "sbft", +}; + +static struct ifs_device ifs_devices[] = { + [IFS_TYPE_SAF] = { + .test_caps = &scan_test, + .test_msrs = &scan_msrs, + .misc = { + .name = "intel_ifs_0", + .minor = MISC_DYNAMIC_MINOR, + .groups = plat_ifs_groups, + }, + }, + [IFS_TYPE_ARRAY_BIST] = { + .test_caps = &array_test, + .misc = { + .name = "intel_ifs_1", + .minor = MISC_DYNAMIC_MINOR, + .groups = plat_ifs_array_groups, + }, + }, + [IFS_TYPE_SBAF] = { + .test_caps = &sbaf_test, + .test_msrs = &sbaf_msrs, + .misc = { + .name = "intel_ifs_2", + .minor = MISC_DYNAMIC_MINOR, + .groups = plat_ifs_groups, + }, + }, +}; + +#define IFS_NUMTESTS ARRAY_SIZE(ifs_devices) + +static void ifs_cleanup(void) +{ + int i; + + for (i = 0; i < IFS_NUMTESTS; i++) { + if (ifs_devices[i].misc.this_device) + misc_deregister(&ifs_devices[i].misc); + } + kfree(ifs_pkg_auth); +} + +static int __init ifs_init(void) +{ + const struct x86_cpu_id *m; + u64 msrval; + int i, ret; + + m = x86_match_cpu(ifs_cpu_ids); + if (!m) + return -ENODEV; + + if (rdmsrq_safe(MSR_IA32_CORE_CAPS, &msrval)) + return -ENODEV; + + if (!(msrval & MSR_IA32_CORE_CAPS_INTEGRITY_CAPS)) + return -ENODEV; + + if (rdmsrq_safe(MSR_INTEGRITY_CAPS, &msrval)) + return -ENODEV; + + ifs_pkg_auth = kmalloc_objs(bool, topology_max_packages()); + if (!ifs_pkg_auth) + return -ENOMEM; + + for (i = 0; i < IFS_NUMTESTS; i++) { + if (!(msrval & BIT(ifs_devices[i].test_caps->integrity_cap_bit))) + continue; + ifs_devices[i].rw_data.generation = FIELD_GET(MSR_INTEGRITY_CAPS_SAF_GEN_MASK, + msrval); + ifs_devices[i].rw_data.array_gen = (u32)m->driver_data; + ret = misc_register(&ifs_devices[i].misc); + if (ret) + goto err_exit; + } + return 0; + +err_exit: + ifs_cleanup(); + return ret; +} + +static void __exit ifs_exit(void) +{ + ifs_cleanup(); +} + +module_init(ifs_init); +module_exit(ifs_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Intel In Field Scan (IFS) device"); diff --git a/drivers/platform/x86/intel/ifs/ifs.h b/drivers/platform/x86/intel/ifs/ifs.h new file mode 100644 index 000000000..f369fb0d3 --- /dev/null +++ b/drivers/platform/x86/intel/ifs/ifs.h @@ -0,0 +1,417 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright(c) 2022 Intel Corporation. */ + +#ifndef _IFS_H_ +#define _IFS_H_ + +/** + * DOC: In-Field Scan + * + * ============= + * In-Field Scan + * ============= + * + * Introduction + * ------------ + * + * In Field Scan (IFS) is a hardware feature to run circuit level tests on + * a CPU core to detect problems that are not caught by parity or ECC checks. + * Future CPUs will support more than one type of test which will show up + * with a new platform-device instance-id. + * + * + * IFS Image + * --------- + * + * Intel provides firmware files containing the scan tests via the webpage [#f1]_. + * Look under "In-Field Scan Test Images Download" section towards the + * end of the page. Similar to microcode, there are separate files for each + * family-model-stepping. IFS Images are not applicable for some test types. + * Wherever applicable the sysfs directory would provide a "current_batch" file + * (see below) for loading the image. + * + * .. [#f1] https://intel.com/InFieldScan + * + * IFS Image Loading + * ----------------- + * + * The driver loads the tests into memory reserved BIOS local to each CPU + * socket in a two step process using writes to MSRs to first load the + * SHA hashes for the test. Then the tests themselves. Status MSRs provide + * feedback on the success/failure of these steps. + * + * The test files are kept in a fixed location: /lib/firmware/intel/ifs_/ + * For e.g if there are 3 test files, they would be named in the following + * fashion: + * ff-mm-ss-01.scan + * ff-mm-ss-02.scan + * ff-mm-ss-03.scan + * (where ff refers to family, mm indicates model and ss indicates stepping) + * + * A different test file can be loaded by writing the numerical portion + * (e.g 1, 2 or 3 in the above scenario) into the curent_batch file. + * To load ff-mm-ss-02.scan, the following command can be used:: + * + * # echo 2 > /sys/devices/virtual/misc/intel_ifs_/current_batch + * + * The above file can also be read to know the currently loaded image. + * + * Running tests + * ------------- + * + * Tests are run by the driver synchronizing execution of all threads on a + * core and then writing to the ACTIVATE_SCAN MSR on all threads. Instruction + * execution continues when: + * + * 1) All tests have completed. + * 2) Execution was interrupted. + * 3) A test detected a problem. + * + * Note that ALL THREADS ON THE CORE ARE EFFECTIVELY OFFLINE FOR THE + * DURATION OF THE TEST. This can be up to 200 milliseconds. If the system + * is running latency sensitive applications that cannot tolerate an + * interruption of this magnitude, the system administrator must arrange + * to migrate those applications to other cores before running a core test. + * It may also be necessary to redirect interrupts to other CPUs. + * + * In all cases reading the corresponding test's STATUS MSR provides details on what + * happened. The driver makes the value of this MSR visible to applications + * via the "details" file (see below). Interrupted tests may be restarted. + * + * The IFS driver provides sysfs interfaces via /sys/devices/virtual/misc/intel_ifs_/ + * to control execution: + * + * Test a specific core:: + * + * # echo > /sys/devices/virtual/misc/intel_ifs_/run_test + * + * when HT is enabled any of the sibling cpu# can be specified to test + * its corresponding physical core. Since the tests are per physical core, + * the result of testing any thread is same. All siblings must be online + * to run a core test. It is only necessary to test one thread. + * + * For e.g. to test core corresponding to cpu5 + * + * # echo 5 > /sys/devices/virtual/misc/intel_ifs_/run_test + * + * Results of the last test is provided in /sys:: + * + * $ cat /sys/devices/virtual/misc/intel_ifs_/status + * pass + * + * Status can be one of pass, fail, untested + * + * Additional details of the last test is provided by the details file:: + * + * $ cat /sys/devices/virtual/misc/intel_ifs_/details + * 0x8081 + * + * The details file reports the hex value of the test specific status MSR. + * Hardware defined error codes are documented in volume 4 of the Intel + * Software Developer's Manual but the error_code field may contain one of + * the following driver defined software codes: + * + * +------+--------------------+ + * | 0xFD | Software timeout | + * +------+--------------------+ + * | 0xFE | Partial completion | + * +------+--------------------+ + * + * Driver design choices + * --------------------- + * + * 1) The ACTIVATE_SCAN MSR allows for running any consecutive subrange of + * available tests. But the driver always tries to run all tests and only + * uses the subrange feature to restart an interrupted test. + * + * 2) Hardware allows for some number of cores to be tested in parallel. + * The driver does not make use of this, it only tests one core at a time. + * + * Structural Based Functional Test at Field (SBAF): + * ------------------------------------------------- + * + * SBAF is a new type of testing that provides comprehensive core test + * coverage complementing Scan at Field (SAF) testing. SBAF mimics the + * manufacturing screening environment and leverages the same test suite. + * It makes use of Design For Test (DFT) observation sites and features + * to maximize coverage in minimum time. + * + * Similar to the SAF test, SBAF isolates the core under test from the + * rest of the system during execution. Upon completion, the core + * seamlessly resets to its pre-test state and resumes normal operation. + * Any machine checks or hangs encountered during the test are confined to + * the isolated core, preventing disruption to the overall system. + * + * Like the SAF test, the SBAF test is also divided into multiple batches, + * and each batch test can take hundreds of milliseconds (100-200 ms) to + * complete. If such a lengthy interruption is undesirable, it is + * recommended to relocate the time-sensitive applications to other cores. + */ +#include +#include + +#define MSR_ARRAY_BIST 0x00000105 + +#define MSR_COPY_SBAF_HASHES 0x000002b8 +#define MSR_SBAF_HASHES_STATUS 0x000002b9 +#define MSR_AUTHENTICATE_AND_COPY_SBAF_CHUNK 0x000002ba +#define MSR_SBAF_CHUNKS_AUTHENTICATION_STATUS 0x000002bb +#define MSR_ACTIVATE_SBAF 0x000002bc +#define MSR_SBAF_STATUS 0x000002bd + +#define MSR_COPY_SCAN_HASHES 0x000002c2 +#define MSR_SCAN_HASHES_STATUS 0x000002c3 +#define MSR_AUTHENTICATE_AND_COPY_CHUNK 0x000002c4 +#define MSR_CHUNKS_AUTHENTICATION_STATUS 0x000002c5 +#define MSR_ACTIVATE_SCAN 0x000002c6 +#define MSR_SCAN_STATUS 0x000002c7 +#define MSR_ARRAY_TRIGGER 0x000002d6 +#define MSR_ARRAY_STATUS 0x000002d7 +#define MSR_SAF_CTRL 0x000004f0 +#define MSR_SBAF_CTRL 0x000004f8 + +#define SCAN_NOT_TESTED 0 +#define SCAN_TEST_PASS 1 +#define SCAN_TEST_FAIL 2 + +#define IFS_TYPE_SAF 0 +#define IFS_TYPE_ARRAY_BIST 1 +#define IFS_TYPE_SBAF 2 + +#define ARRAY_GEN0 0 +#define ARRAY_GEN1 1 + +/* MSR_SCAN_HASHES_STATUS bit fields */ +union ifs_scan_hashes_status { + u64 data; + struct { + u32 chunk_size :16; + u32 num_chunks :8; + u32 rsvd1 :8; + u32 error_code :8; + u32 rsvd2 :11; + u32 max_core_limit :12; + u32 valid :1; + }; +}; + +union ifs_scan_hashes_status_gen2 { + u64 data; + struct { + u16 chunk_size; + u16 num_chunks; + u32 error_code :8; + u32 chunks_in_stride :9; + u32 rsvd :2; + u32 max_core_limit :12; + u32 valid :1; + }; +}; + +/* MSR_CHUNKS_AUTH_STATUS bit fields */ +union ifs_chunks_auth_status { + u64 data; + struct { + u32 valid_chunks :8; + u32 total_chunks :8; + u32 rsvd1 :16; + u32 error_code :8; + u32 rsvd2 :24; + }; +}; + +union ifs_chunks_auth_status_gen2 { + u64 data; + struct { + u16 valid_chunks; + u16 total_chunks; + u32 error_code :8; + u32 rsvd2 :8; + u32 max_bundle :16; + }; +}; + +/* MSR_ACTIVATE_SCAN bit fields */ +union ifs_scan { + u64 data; + struct { + union { + struct { + u8 start; + u8 stop; + u16 rsvd; + } gen0; + struct { + u16 start; + u16 stop; + } gen2; + }; + u32 delay :31; + u32 sigmce :1; + }; +}; + +/* MSR_SCAN_STATUS bit fields */ +union ifs_status { + u64 data; + struct { + union { + struct { + u8 chunk_num; + u8 chunk_stop_index; + u16 rsvd1; + } gen0; + struct { + u16 chunk_num; + u16 chunk_stop_index; + } gen2; + }; + u32 error_code :8; + u32 rsvd2 :22; + u32 control_error :1; + u32 signature_error :1; + }; +}; + +/* MSR_ARRAY_BIST bit fields */ +union ifs_array { + u64 data; + struct { + u32 array_bitmask; + u16 array_bank; + u16 rsvd :15; + u16 ctrl_result :1; + }; +}; + +/* MSR_ACTIVATE_SBAF bit fields */ +union ifs_sbaf { + u64 data; + struct { + u32 bundle_idx :9; + u32 rsvd1 :5; + u32 pgm_idx :2; + u32 rsvd2 :16; + u32 delay :31; + u32 sigmce :1; + }; +}; + +/* MSR_SBAF_STATUS bit fields */ +union ifs_sbaf_status { + u64 data; + struct { + u32 bundle_idx :9; + u32 rsvd1 :5; + u32 pgm_idx :2; + u32 rsvd2 :16; + u32 error_code :8; + u32 rsvd3 :21; + u32 test_fail :1; + u32 sbaf_status :2; + }; +}; + +/* + * Driver populated error-codes + * 0xFD: Test timed out before completing all the chunks. + * 0xFE: not all scan chunks were executed. Maximum forward progress retries exceeded. + */ +#define IFS_SW_TIMEOUT 0xFD +#define IFS_SW_PARTIAL_COMPLETION 0xFE + +#define IFS_SUFFIX_SZ 5 + +struct ifs_test_caps { + int integrity_cap_bit; + int test_num; + char image_suffix[IFS_SUFFIX_SZ]; +}; + +/** + * struct ifs_test_msrs - MSRs used in IFS tests + * @copy_hashes: Copy test hash data + * @copy_hashes_status: Status of copied test hash data + * @copy_chunks: Copy chunks of the test data + * @copy_chunks_status: Status of the copied test data chunks + * @test_ctrl: Control the test attributes + */ +struct ifs_test_msrs { + u32 copy_hashes; + u32 copy_hashes_status; + u32 copy_chunks; + u32 copy_chunks_status; + u32 test_ctrl; +}; + +/** + * struct ifs_data - attributes related to intel IFS driver + * @loaded_version: stores the currently loaded ifs image version. + * @loaded: If a valid test binary has been loaded into the memory + * @loading_error: Error occurred on another CPU while loading image + * @valid_chunks: number of chunks which could be validated. + * @status: it holds simple status pass/fail/untested + * @scan_details: opaque scan status code from h/w + * @cur_batch: number indicating the currently loaded test file + * @generation: IFS test generation enumerated by hardware + * @chunk_size: size of a test chunk + * @array_gen: test generation of array test + * @max_bundle: maximum bundle index + */ +struct ifs_data { + int loaded_version; + bool loaded; + bool loading_error; + int valid_chunks; + int status; + u64 scan_details; + u32 cur_batch; + u32 generation; + u32 chunk_size; + u32 array_gen; + u32 max_bundle; +}; + +struct ifs_work { + struct work_struct w; + struct device *dev; +}; + +struct ifs_device { + const struct ifs_test_caps *test_caps; + const struct ifs_test_msrs *test_msrs; + struct ifs_data rw_data; + struct miscdevice misc; +}; + +static inline struct ifs_data *ifs_get_data(struct device *dev) +{ + struct miscdevice *m = dev_get_drvdata(dev); + struct ifs_device *d = container_of(m, struct ifs_device, misc); + + return &d->rw_data; +} + +static inline const struct ifs_test_caps *ifs_get_test_caps(struct device *dev) +{ + struct miscdevice *m = dev_get_drvdata(dev); + struct ifs_device *d = container_of(m, struct ifs_device, misc); + + return d->test_caps; +} + +static inline const struct ifs_test_msrs *ifs_get_test_msrs(struct device *dev) +{ + struct miscdevice *m = dev_get_drvdata(dev); + struct ifs_device *d = container_of(m, struct ifs_device, misc); + + return d->test_msrs; +} + +extern bool *ifs_pkg_auth; +int ifs_load_firmware(struct device *dev); +int do_core_test(int cpu, struct device *dev); +extern struct attribute *plat_ifs_attrs[]; +extern struct attribute *plat_ifs_array_attrs[]; + +#endif diff --git a/drivers/platform/x86/intel/ifs/load.c b/drivers/platform/x86/intel/ifs/load.c new file mode 100644 index 000000000..50f1fdf7d --- /dev/null +++ b/drivers/platform/x86/intel/ifs/load.c @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2022 Intel Corporation. */ + +#include +#include +#include +#include +#include + +#include "ifs.h" + +#define IFS_CHUNK_ALIGNMENT 256 +union meta_data { + struct { + u32 meta_type; // metadata type + u32 meta_size; // size of this entire struct including hdrs. + u32 test_type; // IFS test type + u32 fusa_info; // Fusa info + u32 total_images; // Total number of images + u32 current_image; // Current Image # + u32 total_chunks; // Total number of chunks in this image + u32 starting_chunk; // Starting chunk number in this image + u32 size_per_chunk; // size of each chunk + u32 chunks_per_stride; // number of chunks in a stride + }; + u8 padding[IFS_CHUNK_ALIGNMENT]; +}; + +#define IFS_HEADER_SIZE (sizeof(struct microcode_header_intel)) +#define META_TYPE_IFS 1 +#define INVALIDATE_STRIDE 0x1UL +#define IFS_GEN_STRIDE_AWARE 2 +#define AUTH_INTERRUPTED_ERROR 5 +#define IFS_AUTH_RETRY_CT 10 + +static struct microcode_header_intel *ifs_header_ptr; /* pointer to the ifs image header */ +static u64 ifs_hash_ptr; /* Address of ifs metadata (hash) */ +static u64 ifs_test_image_ptr; /* 256B aligned address of test pattern */ +static DECLARE_COMPLETION(ifs_done); + +static const char * const scan_hash_status[] = { + [0] = "No error reported", + [1] = "Attempt to copy scan hashes when copy already in progress", + [2] = "Secure Memory not set up correctly", + [3] = "FuSaInfo.ProgramID does not match or ff-mm-ss does not match", + [4] = "Reserved", + [5] = "Integrity check failed", + [6] = "Scan reload or test is in progress" +}; + +static const char * const scan_authentication_status[] = { + [0] = "No error reported", + [1] = "Attempt to authenticate a chunk which is already marked as authentic", + [2] = "Chunk authentication error. The hash of chunk did not match expected value", + [3] = "Reserved", + [4] = "Chunk outside the current stride", + [5] = "Authentication flow interrupted", +}; + +#define MC_HEADER_META_TYPE_END (0) + +struct metadata_header { + unsigned int type; + unsigned int blk_size; +}; + +static struct metadata_header *find_meta_data(void *ucode, unsigned int meta_type) +{ + struct microcode_header_intel *hdr = &((struct microcode_intel *)ucode)->hdr; + struct metadata_header *meta_header; + unsigned long data_size, total_meta; + unsigned long meta_size = 0; + + data_size = intel_microcode_get_datasize(hdr); + total_meta = hdr->metasize; + if (!total_meta) + return NULL; + + meta_header = (ucode + MC_HEADER_SIZE + data_size) - total_meta; + + while (meta_header->type != MC_HEADER_META_TYPE_END && + meta_header->blk_size && + meta_size < total_meta) { + meta_size += meta_header->blk_size; + if (meta_header->type == meta_type) + return meta_header; + + meta_header = (void *)meta_header + meta_header->blk_size; + } + return NULL; +} + +static void hashcopy_err_message(struct device *dev, u32 err_code) +{ + if (err_code >= ARRAY_SIZE(scan_hash_status)) + dev_err(dev, "invalid error code 0x%x for hash copy\n", err_code); + else + dev_err(dev, "Hash copy error : %s\n", scan_hash_status[err_code]); +} + +static void auth_err_message(struct device *dev, u32 err_code) +{ + if (err_code >= ARRAY_SIZE(scan_authentication_status)) + dev_err(dev, "invalid error code 0x%x for authentication\n", err_code); + else + dev_err(dev, "Chunk authentication error : %s\n", + scan_authentication_status[err_code]); +} + +/* + * To copy scan hashes and authenticate test chunks, the initiating cpu must point + * to the EDX:EAX to the test image in linear address. + * Run wrmsr(MSR_COPY_SCAN_HASHES) for scan hash copy and run wrmsr(MSR_AUTHENTICATE_AND_COPY_CHUNK) + * for scan hash copy and test chunk authentication. + */ +static void copy_hashes_authenticate_chunks(struct work_struct *work) +{ + struct ifs_work *local_work = container_of(work, struct ifs_work, w); + union ifs_scan_hashes_status hashes_status; + union ifs_chunks_auth_status chunk_status; + struct device *dev = local_work->dev; + const struct ifs_test_msrs *msrs; + int i, num_chunks, chunk_size; + struct ifs_data *ifsd; + u64 linear_addr, base; + u32 err_code; + + ifsd = ifs_get_data(dev); + msrs = ifs_get_test_msrs(dev); + /* run scan hash copy */ + wrmsrq(msrs->copy_hashes, ifs_hash_ptr); + rdmsrq(msrs->copy_hashes_status, hashes_status.data); + + /* enumerate the scan image information */ + num_chunks = hashes_status.num_chunks; + chunk_size = hashes_status.chunk_size * 1024; + err_code = hashes_status.error_code; + + if (!hashes_status.valid) { + ifsd->loading_error = true; + hashcopy_err_message(dev, err_code); + goto done; + } + + /* base linear address to the scan data */ + base = ifs_test_image_ptr; + + /* scan data authentication and copy chunks to secured memory */ + for (i = 0; i < num_chunks; i++) { + linear_addr = base + i * chunk_size; + linear_addr |= i; + + wrmsrq(msrs->copy_chunks, linear_addr); + rdmsrq(msrs->copy_chunks_status, chunk_status.data); + + ifsd->valid_chunks = chunk_status.valid_chunks; + err_code = chunk_status.error_code; + + if (err_code) { + ifsd->loading_error = true; + auth_err_message(dev, err_code); + goto done; + } + } +done: + complete(&ifs_done); +} + +static int get_num_chunks(int gen, union ifs_scan_hashes_status_gen2 status) +{ + return gen >= IFS_GEN_STRIDE_AWARE ? status.chunks_in_stride : status.num_chunks; +} + +static bool need_copy_scan_hashes(struct ifs_data *ifsd) +{ + return !ifsd->loaded || + ifsd->generation < IFS_GEN_STRIDE_AWARE || + ifsd->loaded_version != ifs_header_ptr->rev; +} + +static int copy_hashes_authenticate_chunks_gen2(struct device *dev) +{ + union ifs_scan_hashes_status_gen2 hashes_status; + union ifs_chunks_auth_status_gen2 chunk_status; + u32 err_code, valid_chunks, total_chunks; + const struct ifs_test_msrs *msrs; + int i, num_chunks, chunk_size; + union meta_data *ifs_meta; + int starting_chunk_nr; + struct ifs_data *ifsd; + u64 linear_addr, base; + u64 chunk_table[2]; + int retry_count; + + ifsd = ifs_get_data(dev); + msrs = ifs_get_test_msrs(dev); + + if (need_copy_scan_hashes(ifsd)) { + wrmsrq(msrs->copy_hashes, ifs_hash_ptr); + rdmsrq(msrs->copy_hashes_status, hashes_status.data); + + /* enumerate the scan image information */ + chunk_size = hashes_status.chunk_size * SZ_1K; + err_code = hashes_status.error_code; + + num_chunks = get_num_chunks(ifsd->generation, hashes_status); + + if (!hashes_status.valid) { + hashcopy_err_message(dev, err_code); + return -EIO; + } + ifsd->loaded_version = ifs_header_ptr->rev; + ifsd->chunk_size = chunk_size; + } else { + num_chunks = ifsd->valid_chunks; + chunk_size = ifsd->chunk_size; + } + + if (ifsd->generation >= IFS_GEN_STRIDE_AWARE) { + wrmsrq(msrs->test_ctrl, INVALIDATE_STRIDE); + rdmsrq(msrs->copy_chunks_status, chunk_status.data); + if (chunk_status.valid_chunks != 0) { + dev_err(dev, "Couldn't invalidate installed stride - %d\n", + chunk_status.valid_chunks); + return -EIO; + } + } + + base = ifs_test_image_ptr; + ifs_meta = (union meta_data *)find_meta_data(ifs_header_ptr, META_TYPE_IFS); + starting_chunk_nr = ifs_meta->starting_chunk; + + /* scan data authentication and copy chunks to secured memory */ + for (i = 0; i < num_chunks; i++) { + retry_count = IFS_AUTH_RETRY_CT; + linear_addr = base + i * chunk_size; + + chunk_table[0] = starting_chunk_nr + i; + chunk_table[1] = linear_addr; + do { + local_irq_disable(); + wrmsrq(msrs->copy_chunks, (u64)chunk_table); + local_irq_enable(); + rdmsrq(msrs->copy_chunks_status, chunk_status.data); + err_code = chunk_status.error_code; + } while (err_code == AUTH_INTERRUPTED_ERROR && --retry_count); + + if (err_code) { + ifsd->loading_error = true; + auth_err_message(dev, err_code); + return -EIO; + } + } + + valid_chunks = chunk_status.valid_chunks; + total_chunks = chunk_status.total_chunks; + + if (valid_chunks != total_chunks) { + ifsd->loading_error = true; + dev_err(dev, "Couldn't authenticate all the chunks. Authenticated %d total %d.\n", + valid_chunks, total_chunks); + return -EIO; + } + ifsd->valid_chunks = valid_chunks; + ifsd->max_bundle = chunk_status.max_bundle; + + return 0; +} + +static int validate_ifs_metadata(struct device *dev) +{ + const struct ifs_test_caps *test = ifs_get_test_caps(dev); + struct ifs_data *ifsd = ifs_get_data(dev); + union meta_data *ifs_meta; + char test_file[64]; + int ret = -EINVAL; + + snprintf(test_file, sizeof(test_file), "%02x-%02x-%02x-%02x.%s", + boot_cpu_data.x86, boot_cpu_data.x86_model, + boot_cpu_data.x86_stepping, ifsd->cur_batch, test->image_suffix); + + ifs_meta = (union meta_data *)find_meta_data(ifs_header_ptr, META_TYPE_IFS); + if (!ifs_meta) { + dev_err(dev, "IFS Metadata missing in file %s\n", test_file); + return ret; + } + + ifs_test_image_ptr = (u64)ifs_meta + sizeof(union meta_data); + + /* Scan chunk start must be 256 byte aligned */ + if (!IS_ALIGNED(ifs_test_image_ptr, IFS_CHUNK_ALIGNMENT)) { + dev_err(dev, "Scan pattern is not aligned on %d bytes aligned in %s\n", + IFS_CHUNK_ALIGNMENT, test_file); + return ret; + } + + if (ifs_meta->current_image != ifsd->cur_batch) { + dev_warn(dev, "Mismatch between filename %s and batch metadata 0x%02x\n", + test_file, ifs_meta->current_image); + return ret; + } + + if (ifs_meta->chunks_per_stride && + (ifs_meta->starting_chunk % ifs_meta->chunks_per_stride != 0)) { + dev_warn(dev, "Starting chunk num %u not a multiple of chunks_per_stride %u\n", + ifs_meta->starting_chunk, ifs_meta->chunks_per_stride); + return ret; + } + + if (ifs_meta->test_type != test->test_num) { + dev_warn(dev, "Metadata test_type %d mismatches with device type\n", + ifs_meta->test_type); + return ret; + } + + return 0; +} + +/* + * IFS requires scan chunks authenticated per each socket in the platform. + * Once the test chunk is authenticated, it is automatically copied to secured memory + * and proceed the authentication for the next chunk. + */ +static int scan_chunks_sanity_check(struct device *dev) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + struct ifs_work local_work; + int curr_pkg, cpu, ret; + + memset(ifs_pkg_auth, 0, (topology_max_packages() * sizeof(bool))); + ret = validate_ifs_metadata(dev); + if (ret) + return ret; + + ifsd->loading_error = false; + + if (ifsd->generation > 0) + return copy_hashes_authenticate_chunks_gen2(dev); + + /* copy the scan hash and authenticate per package */ + cpus_read_lock(); + for_each_online_cpu(cpu) { + curr_pkg = topology_physical_package_id(cpu); + if (ifs_pkg_auth[curr_pkg]) + continue; + reinit_completion(&ifs_done); + local_work.dev = dev; + INIT_WORK_ONSTACK(&local_work.w, copy_hashes_authenticate_chunks); + schedule_work_on(cpu, &local_work.w); + wait_for_completion(&ifs_done); + if (ifsd->loading_error) { + ret = -EIO; + goto out; + } + ifs_pkg_auth[curr_pkg] = 1; + } + ret = 0; + ifsd->loaded_version = ifs_header_ptr->rev; +out: + cpus_read_unlock(); + + return ret; +} + +static int image_sanity_check(struct device *dev, const struct microcode_header_intel *data) +{ + struct cpu_signature sig; + + /* Provide a specific error message when loading an older/unsupported image */ + if (data->hdrver != MC_HEADER_TYPE_IFS) { + dev_err(dev, "Header version %d not supported\n", data->hdrver); + return -EINVAL; + } + + if (intel_microcode_sanity_check((void *)data, true, MC_HEADER_TYPE_IFS)) { + dev_err(dev, "sanity check failed\n"); + return -EINVAL; + } + + intel_collect_cpu_info(&sig); + + if (!intel_find_matching_signature((void *)data, &sig)) { + dev_err(dev, "cpu signature, processor flags not matching\n"); + return -EINVAL; + } + + return 0; +} + +/* + * Load ifs image. Before loading ifs module, the ifs image must be located + * in /lib/firmware/intel/ifs_x/ and named as family-model-stepping-02x.{testname}. + */ +int ifs_load_firmware(struct device *dev) +{ + const struct ifs_test_caps *test = ifs_get_test_caps(dev); + struct ifs_data *ifsd = ifs_get_data(dev); + unsigned int expected_size; + const struct firmware *fw; + char scan_path[64]; + int ret; + + snprintf(scan_path, sizeof(scan_path), "intel/ifs_%d/%02x-%02x-%02x-%02x.%s", + test->test_num, boot_cpu_data.x86, boot_cpu_data.x86_model, + boot_cpu_data.x86_stepping, ifsd->cur_batch, test->image_suffix); + + ret = request_firmware_direct(&fw, scan_path, dev); + if (ret) { + dev_err(dev, "ifs file %s load failed\n", scan_path); + goto done; + } + + expected_size = ((struct microcode_header_intel *)fw->data)->totalsize; + if (fw->size != expected_size) { + dev_err(dev, "File size mismatch (expected %u, actual %zu). Corrupted IFS image.\n", + expected_size, fw->size); + ret = -EINVAL; + goto release; + } + + ret = image_sanity_check(dev, (struct microcode_header_intel *)fw->data); + if (ret) + goto release; + + ifs_header_ptr = (struct microcode_header_intel *)fw->data; + ifs_hash_ptr = (u64)(ifs_header_ptr + 1); + + ret = scan_chunks_sanity_check(dev); + if (ret) + dev_err(dev, "Load failure for batch: %02x\n", ifsd->cur_batch); + +release: + release_firmware(fw); +done: + ifsd->loaded = (ret == 0); + + return ret; +} diff --git a/drivers/platform/x86/intel/ifs/runtest.c b/drivers/platform/x86/intel/ifs/runtest.c new file mode 100644 index 000000000..dfc119d73 --- /dev/null +++ b/drivers/platform/x86/intel/ifs/runtest.c @@ -0,0 +1,663 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2022 Intel Corporation. */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ifs.h" + +/* + * Note all code and data in this file is protected by + * ifs_sem. On HT systems all threads on a core will + * execute together, but only the first thread on the + * core will update results of the test. + */ + +#define CREATE_TRACE_POINTS +#include + +/* Max retries on the same chunk */ +#define MAX_IFS_RETRIES 5 + +struct run_params { + struct ifs_data *ifsd; + union ifs_scan *activate; + union ifs_status status; +}; + +struct sbaf_run_params { + struct ifs_data *ifsd; + int *retry_cnt; + union ifs_sbaf *activate; + union ifs_sbaf_status status; +}; + +/* + * Number of TSC cycles that a logical CPU will wait for the other + * logical CPU on the core in the WRMSR(ACTIVATE_SCAN). + */ +#define IFS_THREAD_WAIT 100000 + +enum ifs_status_err_code { + IFS_NO_ERROR = 0, + IFS_OTHER_THREAD_COULD_NOT_JOIN = 1, + IFS_INTERRUPTED_BEFORE_RENDEZVOUS = 2, + IFS_POWER_MGMT_INADEQUATE_FOR_SCAN = 3, + IFS_INVALID_CHUNK_RANGE = 4, + IFS_MISMATCH_ARGUMENTS_BETWEEN_THREADS = 5, + IFS_CORE_NOT_CAPABLE_CURRENTLY = 6, + IFS_UNASSIGNED_ERROR_CODE = 7, + IFS_EXCEED_NUMBER_OF_THREADS_CONCURRENT = 8, + IFS_INTERRUPTED_DURING_EXECUTION = 9, + IFS_UNASSIGNED_ERROR_CODE_0xA = 0xA, + IFS_CORRUPTED_CHUNK = 0xB, +}; + +static const char * const scan_test_status[] = { + [IFS_NO_ERROR] = "SCAN no error", + [IFS_OTHER_THREAD_COULD_NOT_JOIN] = "Other thread could not join.", + [IFS_INTERRUPTED_BEFORE_RENDEZVOUS] = "Interrupt occurred prior to SCAN coordination.", + [IFS_POWER_MGMT_INADEQUATE_FOR_SCAN] = + "Core Abort SCAN Response due to power management condition.", + [IFS_INVALID_CHUNK_RANGE] = "Non valid chunks in the range", + [IFS_MISMATCH_ARGUMENTS_BETWEEN_THREADS] = "Mismatch in arguments between threads T0/T1.", + [IFS_CORE_NOT_CAPABLE_CURRENTLY] = "Core not capable of performing SCAN currently", + [IFS_UNASSIGNED_ERROR_CODE] = "Unassigned error code 0x7", + [IFS_EXCEED_NUMBER_OF_THREADS_CONCURRENT] = + "Exceeded number of Logical Processors (LP) allowed to run Scan-At-Field concurrently", + [IFS_INTERRUPTED_DURING_EXECUTION] = "Interrupt occurred prior to SCAN start", + [IFS_UNASSIGNED_ERROR_CODE_0xA] = "Unassigned error code 0xA", + [IFS_CORRUPTED_CHUNK] = "Scan operation aborted due to corrupted image. Try reloading", +}; + +static void message_not_tested(struct device *dev, int cpu, union ifs_status status) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + /* + * control_error is set when the microcode runs into a problem + * loading the image from the reserved BIOS memory, or it has + * been corrupted. Reloading the image may fix this issue. + */ + if (status.control_error) { + dev_warn(dev, "CPU(s) %*pbl: Scan controller error. Batch: %02x version: 0x%x\n", + cpumask_pr_args(cpu_smt_mask(cpu)), ifsd->cur_batch, ifsd->loaded_version); + return; + } + + if (status.error_code < ARRAY_SIZE(scan_test_status)) { + dev_info(dev, "CPU(s) %*pbl: SCAN operation did not start. %s\n", + cpumask_pr_args(cpu_smt_mask(cpu)), + scan_test_status[status.error_code]); + } else if (status.error_code == IFS_SW_TIMEOUT) { + dev_info(dev, "CPU(s) %*pbl: software timeout during scan\n", + cpumask_pr_args(cpu_smt_mask(cpu))); + } else if (status.error_code == IFS_SW_PARTIAL_COMPLETION) { + dev_info(dev, "CPU(s) %*pbl: %s\n", + cpumask_pr_args(cpu_smt_mask(cpu)), + "Not all scan chunks were executed. Maximum forward progress retries exceeded"); + } else { + dev_info(dev, "CPU(s) %*pbl: SCAN unknown status %llx\n", + cpumask_pr_args(cpu_smt_mask(cpu)), status.data); + } +} + +static void message_fail(struct device *dev, int cpu, union ifs_status status) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + /* + * signature_error is set when the output from the scan chains does not + * match the expected signature. This might be a transient problem (e.g. + * due to a bit flip from an alpha particle or neutron). If the problem + * repeats on a subsequent test, then it indicates an actual problem in + * the core being tested. + */ + if (status.signature_error) { + dev_err(dev, "CPU(s) %*pbl: test signature incorrect. Batch: %02x version: 0x%x\n", + cpumask_pr_args(cpu_smt_mask(cpu)), ifsd->cur_batch, ifsd->loaded_version); + } +} + +static bool can_restart(union ifs_status status) +{ + enum ifs_status_err_code err_code = status.error_code; + + /* Signature for chunk is bad, or scan test failed */ + if (status.signature_error || status.control_error) + return false; + + switch (err_code) { + case IFS_NO_ERROR: + case IFS_OTHER_THREAD_COULD_NOT_JOIN: + case IFS_INTERRUPTED_BEFORE_RENDEZVOUS: + case IFS_POWER_MGMT_INADEQUATE_FOR_SCAN: + case IFS_EXCEED_NUMBER_OF_THREADS_CONCURRENT: + case IFS_INTERRUPTED_DURING_EXECUTION: + return true; + case IFS_INVALID_CHUNK_RANGE: + case IFS_MISMATCH_ARGUMENTS_BETWEEN_THREADS: + case IFS_CORE_NOT_CAPABLE_CURRENTLY: + case IFS_UNASSIGNED_ERROR_CODE: + case IFS_UNASSIGNED_ERROR_CODE_0xA: + case IFS_CORRUPTED_CHUNK: + break; + } + return false; +} + +#define SPINUNIT 100 /* 100 nsec */ +static atomic_t array_cpus_in; +static atomic_t scan_cpus_in; +static atomic_t sbaf_cpus_in; + +/* + * Simplified cpu sibling rendezvous loop based on microcode loader __wait_for_cpus() + */ +static void wait_for_sibling_cpu(atomic_t *t, long long timeout) +{ + int cpu = smp_processor_id(); + const struct cpumask *smt_mask = cpu_smt_mask(cpu); + int all_cpus = cpumask_weight(smt_mask); + + atomic_inc(t); + while (atomic_read(t) < all_cpus) { + if (timeout < SPINUNIT) + return; + ndelay(SPINUNIT); + timeout -= SPINUNIT; + touch_nmi_watchdog(); + } +} + +/* + * Execute the scan. Called "simultaneously" on all threads of a core + * at high priority using the stop_cpus mechanism. + */ +static int doscan(void *data) +{ + int cpu = smp_processor_id(), start, stop; + struct run_params *params = data; + union ifs_status status; + struct ifs_data *ifsd; + int first; + + ifsd = params->ifsd; + + if (ifsd->generation) { + start = params->activate->gen2.start; + stop = params->activate->gen2.stop; + } else { + start = params->activate->gen0.start; + stop = params->activate->gen0.stop; + } + + /* Only the first logical CPU on a core reports result */ + first = cpumask_first(cpu_smt_mask(cpu)); + + wait_for_sibling_cpu(&scan_cpus_in, NSEC_PER_SEC); + + /* + * This WRMSR will wait for other HT threads to also write + * to this MSR (at most for activate.delay cycles). Then it + * starts scan of each requested chunk. The core scan happens + * during the "execution" of the WRMSR. This instruction can + * take up to 200 milliseconds (in the case where all chunks + * are processed in a single pass) before it retires. + */ + wrmsrq(MSR_ACTIVATE_SCAN, params->activate->data); + rdmsrq(MSR_SCAN_STATUS, status.data); + + trace_ifs_status(ifsd->cur_batch, start, stop, status.data); + + /* Pass back the result of the scan */ + if (cpu == first) + params->status = status; + + return 0; +} + +/* + * Use stop_core_cpuslocked() to synchronize writing to MSR_ACTIVATE_SCAN + * on all threads of the core to be tested. Loop if necessary to complete + * run of all chunks. Include some defensive tests to make sure forward + * progress is made, and that the whole test completes in a reasonable time. + */ +static void ifs_test_core(int cpu, struct device *dev) +{ + union ifs_status status = {}; + union ifs_scan activate; + unsigned long timeout; + struct ifs_data *ifsd; + int to_start, to_stop; + int status_chunk; + struct run_params params; + int retries; + + ifsd = ifs_get_data(dev); + + activate.gen0.rsvd = 0; + activate.delay = IFS_THREAD_WAIT; + activate.sigmce = 0; + to_start = 0; + to_stop = ifsd->valid_chunks - 1; + + params.ifsd = ifs_get_data(dev); + + if (ifsd->generation) { + activate.gen2.start = to_start; + activate.gen2.stop = to_stop; + } else { + activate.gen0.start = to_start; + activate.gen0.stop = to_stop; + } + + timeout = jiffies + HZ / 2; + retries = MAX_IFS_RETRIES; + + while (to_start <= to_stop) { + if (time_after(jiffies, timeout)) { + status.error_code = IFS_SW_TIMEOUT; + break; + } + + params.activate = &activate; + atomic_set(&scan_cpus_in, 0); + stop_core_cpuslocked(cpu, doscan, ¶ms); + + status = params.status; + + /* Some cases can be retried, give up for others */ + if (!can_restart(status)) + break; + + status_chunk = ifsd->generation ? status.gen2.chunk_num : status.gen0.chunk_num; + if (status_chunk == to_start) { + /* Check for forward progress */ + if (--retries == 0) { + if (status.error_code == IFS_NO_ERROR) + status.error_code = IFS_SW_PARTIAL_COMPLETION; + break; + } + } else { + retries = MAX_IFS_RETRIES; + if (ifsd->generation) + activate.gen2.start = status_chunk; + else + activate.gen0.start = status_chunk; + to_start = status_chunk; + } + } + + /* Update status for this core */ + ifsd->scan_details = status.data; + + if (status.signature_error) { + ifsd->status = SCAN_TEST_FAIL; + message_fail(dev, cpu, status); + } else if (status.control_error || status.error_code) { + ifsd->status = SCAN_NOT_TESTED; + message_not_tested(dev, cpu, status); + } else { + ifsd->status = SCAN_TEST_PASS; + } +} + +static int do_array_test(void *data) +{ + union ifs_array *command = data; + int cpu = smp_processor_id(); + int first; + + wait_for_sibling_cpu(&array_cpus_in, NSEC_PER_SEC); + + /* + * Only one logical CPU on a core needs to trigger the Array test via MSR write. + */ + first = cpumask_first(cpu_smt_mask(cpu)); + + if (cpu == first) { + wrmsrq(MSR_ARRAY_BIST, command->data); + /* Pass back the result of the test */ + rdmsrq(MSR_ARRAY_BIST, command->data); + } + + return 0; +} + +static void ifs_array_test_core(int cpu, struct device *dev) +{ + union ifs_array command = {}; + bool timed_out = false; + struct ifs_data *ifsd; + unsigned long timeout; + + ifsd = ifs_get_data(dev); + + command.array_bitmask = ~0U; + timeout = jiffies + HZ / 2; + + do { + if (time_after(jiffies, timeout)) { + timed_out = true; + break; + } + atomic_set(&array_cpus_in, 0); + stop_core_cpuslocked(cpu, do_array_test, &command); + + if (command.ctrl_result) + break; + } while (command.array_bitmask); + + ifsd->scan_details = command.data; + + if (command.ctrl_result) + ifsd->status = SCAN_TEST_FAIL; + else if (timed_out || command.array_bitmask) + ifsd->status = SCAN_NOT_TESTED; + else + ifsd->status = SCAN_TEST_PASS; +} + +#define ARRAY_GEN1_TEST_ALL_ARRAYS 0x0ULL +#define ARRAY_GEN1_STATUS_FAIL 0x1ULL + +static int do_array_test_gen1(void *status) +{ + int cpu = smp_processor_id(); + int first; + + first = cpumask_first(cpu_smt_mask(cpu)); + + if (cpu == first) { + wrmsrq(MSR_ARRAY_TRIGGER, ARRAY_GEN1_TEST_ALL_ARRAYS); + rdmsrq(MSR_ARRAY_STATUS, *((u64 *)status)); + } + + return 0; +} + +static void ifs_array_test_gen1(int cpu, struct device *dev) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + u64 status = 0; + + stop_core_cpuslocked(cpu, do_array_test_gen1, &status); + ifsd->scan_details = status; + + if (status & ARRAY_GEN1_STATUS_FAIL) + ifsd->status = SCAN_TEST_FAIL; + else + ifsd->status = SCAN_TEST_PASS; +} + +#define SBAF_STATUS_PASS 0 +#define SBAF_STATUS_SIGN_FAIL 1 +#define SBAF_STATUS_INTR 2 +#define SBAF_STATUS_TEST_FAIL 3 + +enum sbaf_status_err_code { + IFS_SBAF_NO_ERROR = 0, + IFS_SBAF_OTHER_THREAD_COULD_NOT_JOIN = 1, + IFS_SBAF_INTERRUPTED_BEFORE_RENDEZVOUS = 2, + IFS_SBAF_UNASSIGNED_ERROR_CODE3 = 3, + IFS_SBAF_INVALID_BUNDLE_INDEX = 4, + IFS_SBAF_MISMATCH_ARGS_BETWEEN_THREADS = 5, + IFS_SBAF_CORE_NOT_CAPABLE_CURRENTLY = 6, + IFS_SBAF_UNASSIGNED_ERROR_CODE7 = 7, + IFS_SBAF_EXCEED_NUMBER_OF_THREADS_CONCURRENT = 8, + IFS_SBAF_INTERRUPTED_DURING_EXECUTION = 9, + IFS_SBAF_INVALID_PROGRAM_INDEX = 0xA, + IFS_SBAF_CORRUPTED_CHUNK = 0xB, + IFS_SBAF_DID_NOT_START = 0xC, +}; + +static const char * const sbaf_test_status[] = { + [IFS_SBAF_NO_ERROR] = "SBAF no error", + [IFS_SBAF_OTHER_THREAD_COULD_NOT_JOIN] = "Other thread could not join.", + [IFS_SBAF_INTERRUPTED_BEFORE_RENDEZVOUS] = "Interrupt occurred prior to SBAF coordination.", + [IFS_SBAF_UNASSIGNED_ERROR_CODE3] = "Unassigned error code 0x3", + [IFS_SBAF_INVALID_BUNDLE_INDEX] = "Non-valid sbaf bundles. Reload test image", + [IFS_SBAF_MISMATCH_ARGS_BETWEEN_THREADS] = "Mismatch in arguments between threads T0/T1.", + [IFS_SBAF_CORE_NOT_CAPABLE_CURRENTLY] = "Core not capable of performing SBAF currently", + [IFS_SBAF_UNASSIGNED_ERROR_CODE7] = "Unassigned error code 0x7", + [IFS_SBAF_EXCEED_NUMBER_OF_THREADS_CONCURRENT] = "Exceeded number of Logical Processors (LP) allowed to run Scan-At-Field concurrently", + [IFS_SBAF_INTERRUPTED_DURING_EXECUTION] = "Interrupt occurred prior to SBAF start", + [IFS_SBAF_INVALID_PROGRAM_INDEX] = "SBAF program index not valid", + [IFS_SBAF_CORRUPTED_CHUNK] = "SBAF operation aborted due to corrupted chunk", + [IFS_SBAF_DID_NOT_START] = "SBAF operation did not start", +}; + +static void sbaf_message_not_tested(struct device *dev, int cpu, u64 status_data) +{ + union ifs_sbaf_status status = (union ifs_sbaf_status)status_data; + + if (status.error_code < ARRAY_SIZE(sbaf_test_status)) { + dev_info(dev, "CPU(s) %*pbl: SBAF operation did not start. %s\n", + cpumask_pr_args(cpu_smt_mask(cpu)), + sbaf_test_status[status.error_code]); + } else if (status.error_code == IFS_SW_TIMEOUT) { + dev_info(dev, "CPU(s) %*pbl: software timeout during scan\n", + cpumask_pr_args(cpu_smt_mask(cpu))); + } else if (status.error_code == IFS_SW_PARTIAL_COMPLETION) { + dev_info(dev, "CPU(s) %*pbl: %s\n", + cpumask_pr_args(cpu_smt_mask(cpu)), + "Not all SBAF bundles executed. Maximum forward progress retries exceeded"); + } else { + dev_info(dev, "CPU(s) %*pbl: SBAF unknown status %llx\n", + cpumask_pr_args(cpu_smt_mask(cpu)), status.data); + } +} + +static void sbaf_message_fail(struct device *dev, int cpu, union ifs_sbaf_status status) +{ + /* Failed signature check is set when SBAF signature did not match the expected value */ + if (status.sbaf_status == SBAF_STATUS_SIGN_FAIL) { + dev_err(dev, "CPU(s) %*pbl: Failed signature check\n", + cpumask_pr_args(cpu_smt_mask(cpu))); + } + + /* Failed to reach end of test */ + if (status.sbaf_status == SBAF_STATUS_TEST_FAIL) { + dev_err(dev, "CPU(s) %*pbl: Failed to complete test\n", + cpumask_pr_args(cpu_smt_mask(cpu))); + } +} + +static bool sbaf_bundle_completed(union ifs_sbaf_status status) +{ + return !(status.sbaf_status || status.error_code); +} + +static bool sbaf_can_restart(union ifs_sbaf_status status) +{ + enum sbaf_status_err_code err_code = status.error_code; + + /* Signature for chunk is bad, or scan test failed */ + if (status.sbaf_status == SBAF_STATUS_SIGN_FAIL || + status.sbaf_status == SBAF_STATUS_TEST_FAIL) + return false; + + switch (err_code) { + case IFS_SBAF_NO_ERROR: + case IFS_SBAF_OTHER_THREAD_COULD_NOT_JOIN: + case IFS_SBAF_INTERRUPTED_BEFORE_RENDEZVOUS: + case IFS_SBAF_EXCEED_NUMBER_OF_THREADS_CONCURRENT: + case IFS_SBAF_INTERRUPTED_DURING_EXECUTION: + return true; + case IFS_SBAF_UNASSIGNED_ERROR_CODE3: + case IFS_SBAF_INVALID_BUNDLE_INDEX: + case IFS_SBAF_MISMATCH_ARGS_BETWEEN_THREADS: + case IFS_SBAF_CORE_NOT_CAPABLE_CURRENTLY: + case IFS_SBAF_UNASSIGNED_ERROR_CODE7: + case IFS_SBAF_INVALID_PROGRAM_INDEX: + case IFS_SBAF_CORRUPTED_CHUNK: + case IFS_SBAF_DID_NOT_START: + break; + } + return false; +} + +/* + * Execute the SBAF test. Called "simultaneously" on all threads of a core + * at high priority using the stop_cpus mechanism. + */ +static int dosbaf(void *data) +{ + struct sbaf_run_params *run_params = data; + int cpu = smp_processor_id(); + union ifs_sbaf_status status; + struct ifs_data *ifsd; + int first; + + ifsd = run_params->ifsd; + + /* Only the first logical CPU on a core reports result */ + first = cpumask_first(cpu_smt_mask(cpu)); + wait_for_sibling_cpu(&sbaf_cpus_in, NSEC_PER_SEC); + + /* + * This WRMSR will wait for other HT threads to also write + * to this MSR (at most for activate.delay cycles). Then it + * starts scan of each requested bundle. The core test happens + * during the "execution" of the WRMSR. + */ + wrmsrq(MSR_ACTIVATE_SBAF, run_params->activate->data); + rdmsrq(MSR_SBAF_STATUS, status.data); + trace_ifs_sbaf(ifsd->cur_batch, *run_params->activate, status); + + /* Pass back the result of the test */ + if (cpu == first) + run_params->status = status; + + return 0; +} + +static void ifs_sbaf_test_core(int cpu, struct device *dev) +{ + struct sbaf_run_params run_params; + union ifs_sbaf_status status = {}; + union ifs_sbaf activate; + unsigned long timeout; + struct ifs_data *ifsd; + int stop_bundle; + int retries; + + ifsd = ifs_get_data(dev); + + activate.data = 0; + activate.delay = IFS_THREAD_WAIT; + + timeout = jiffies + 2 * HZ; + retries = MAX_IFS_RETRIES; + activate.bundle_idx = 0; + stop_bundle = ifsd->max_bundle; + + while (activate.bundle_idx <= stop_bundle) { + if (time_after(jiffies, timeout)) { + status.error_code = IFS_SW_TIMEOUT; + break; + } + + atomic_set(&sbaf_cpus_in, 0); + + run_params.ifsd = ifsd; + run_params.activate = &activate; + run_params.retry_cnt = &retries; + stop_core_cpuslocked(cpu, dosbaf, &run_params); + + status = run_params.status; + + if (sbaf_bundle_completed(status)) { + activate.bundle_idx = status.bundle_idx + 1; + activate.pgm_idx = 0; + retries = MAX_IFS_RETRIES; + continue; + } + + /* Some cases can be retried, give up for others */ + if (!sbaf_can_restart(status)) + break; + + if (status.pgm_idx == activate.pgm_idx) { + /* If no progress retry */ + if (--retries == 0) { + if (status.error_code == IFS_NO_ERROR) + status.error_code = IFS_SW_PARTIAL_COMPLETION; + break; + } + } else { + /* if some progress, more pgms remaining in bundle, reset retries */ + retries = MAX_IFS_RETRIES; + activate.bundle_idx = status.bundle_idx; + activate.pgm_idx = status.pgm_idx; + } + } + + /* Update status for this core */ + ifsd->scan_details = status.data; + + if (status.sbaf_status == SBAF_STATUS_SIGN_FAIL || + status.sbaf_status == SBAF_STATUS_TEST_FAIL) { + ifsd->status = SCAN_TEST_FAIL; + sbaf_message_fail(dev, cpu, status); + } else if (status.error_code || status.sbaf_status == SBAF_STATUS_INTR || + (activate.bundle_idx < stop_bundle)) { + ifsd->status = SCAN_NOT_TESTED; + sbaf_message_not_tested(dev, cpu, status.data); + } else { + ifsd->status = SCAN_TEST_PASS; + } +} + +/* + * Initiate per core test. It wakes up work queue threads on the target cpu and + * its sibling cpu. Once all sibling threads wake up, the scan test gets executed and + * wait for all sibling threads to finish the scan test. + */ +int do_core_test(int cpu, struct device *dev) +{ + const struct ifs_test_caps *test = ifs_get_test_caps(dev); + struct ifs_data *ifsd = ifs_get_data(dev); + int ret = 0; + + /* Prevent CPUs from being taken offline during the scan test */ + cpus_read_lock(); + + if (!cpu_online(cpu)) { + dev_info(dev, "cannot test on the offline cpu %d\n", cpu); + ret = -EINVAL; + goto out; + } + + switch (test->test_num) { + case IFS_TYPE_SAF: + if (!ifsd->loaded) + ret = -EPERM; + else + ifs_test_core(cpu, dev); + break; + case IFS_TYPE_ARRAY_BIST: + if (ifsd->array_gen == ARRAY_GEN0) + ifs_array_test_core(cpu, dev); + else + ifs_array_test_gen1(cpu, dev); + break; + case IFS_TYPE_SBAF: + if (!ifsd->loaded) + ret = -EPERM; + else + ifs_sbaf_test_core(cpu, dev); + break; + default: + ret = -EINVAL; + } +out: + cpus_read_unlock(); + return ret; +} diff --git a/drivers/platform/x86/intel/ifs/sysfs.c b/drivers/platform/x86/intel/ifs/sysfs.c new file mode 100644 index 000000000..01b7502f4 --- /dev/null +++ b/drivers/platform/x86/intel/ifs/sysfs.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2022 Intel Corporation. */ + +#include +#include +#include +#include +#include + +#include "ifs.h" + +/* + * Protects against simultaneous tests on multiple cores, or + * reloading can file while a test is in progress + */ +static DEFINE_SEMAPHORE(ifs_sem, 1); + +/* + * The sysfs interface to check additional details of last test + * cat /sys/devices/system/platform/ifs/details + */ +static ssize_t details_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + return sysfs_emit(buf, "%#llx\n", ifsd->scan_details); +} + +static DEVICE_ATTR_RO(details); + +static const char * const status_msg[] = { + [SCAN_NOT_TESTED] = "untested", + [SCAN_TEST_PASS] = "pass", + [SCAN_TEST_FAIL] = "fail" +}; + +/* + * The sysfs interface to check the test status: + * To check the status of last test + * cat /sys/devices/platform/ifs/status + */ +static ssize_t status_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + return sysfs_emit(buf, "%s\n", status_msg[ifsd->status]); +} + +static DEVICE_ATTR_RO(status); + +/* + * The sysfs interface for single core testing + * To start test, for example, cpu5 + * echo 5 > /sys/devices/platform/ifs/run_test + * To check the result: + * cat /sys/devices/platform/ifs/result + * The sibling core gets tested at the same time. + */ +static ssize_t run_test_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int cpu; + int rc; + + rc = kstrtouint(buf, 0, &cpu); + if (rc < 0 || cpu >= nr_cpu_ids) + return -EINVAL; + + if (down_interruptible(&ifs_sem)) + return -EINTR; + + rc = do_core_test(cpu, dev); + + up(&ifs_sem); + + return rc ? rc : count; +} + +static DEVICE_ATTR_WO(run_test); + +static ssize_t current_batch_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + unsigned int cur_batch; + int rc; + + rc = kstrtouint(buf, 0, &cur_batch); + if (rc < 0 || cur_batch > 0xff) + return -EINVAL; + + if (down_interruptible(&ifs_sem)) + return -EINTR; + + ifsd->cur_batch = cur_batch; + + rc = ifs_load_firmware(dev); + + up(&ifs_sem); + + return (rc == 0) ? count : rc; +} + +static ssize_t current_batch_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + if (!ifsd->loaded) + return sysfs_emit(buf, "none\n"); + else + return sysfs_emit(buf, "0x%02x\n", ifsd->cur_batch); +} + +static DEVICE_ATTR_RW(current_batch); + +/* + * Display currently loaded IFS image version. + */ +static ssize_t image_version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ifs_data *ifsd = ifs_get_data(dev); + + if (!ifsd->loaded) + return sysfs_emit(buf, "%s\n", "none"); + else + return sysfs_emit(buf, "%#x\n", ifsd->loaded_version); +} + +static DEVICE_ATTR_RO(image_version); + +/* global scan sysfs attributes */ +struct attribute *plat_ifs_attrs[] = { + &dev_attr_details.attr, + &dev_attr_status.attr, + &dev_attr_run_test.attr, + &dev_attr_current_batch.attr, + &dev_attr_image_version.attr, + NULL +}; + +/* global array sysfs attributes */ +struct attribute *plat_ifs_array_attrs[] = { + &dev_attr_details.attr, + &dev_attr_status.attr, + &dev_attr_run_test.attr, + NULL +}; diff --git a/drivers/platform/x86/intel/int0002_vgpio.c b/drivers/platform/x86/intel/int0002_vgpio.c new file mode 100644 index 000000000..562e88025 --- /dev/null +++ b/drivers/platform/x86/intel/int0002_vgpio.c @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel INT0002 "Virtual GPIO" driver + * + * Copyright (C) 2017 Hans de Goede + * + * Loosely based on android x86 kernel code which is: + * + * Copyright (c) 2014, Intel Corporation. + * + * Author: Dyut Kumar Sil + * + * Some peripherals on Bay Trail and Cherry Trail platforms signal a Power + * Management Event (PME) to the Power Management Controller (PMC) to wakeup + * the system. When this happens software needs to clear the PME bus 0 status + * bit in the GPE0a_STS register to avoid an IRQ storm on IRQ 9. + * + * This is modelled in ACPI through the INT0002 ACPI device, which is + * called a "Virtual GPIO controller" in ACPI because it defines the event + * handler to call when the PME triggers through _AEI and _L02 / _E02 + * methods as would be done for a real GPIO interrupt in ACPI. Note this + * is a hack to define an AML event handler for the PME while using existing + * ACPI mechanisms, this is not a real GPIO at all. + * + * This driver will bind to the INT0002 device, and register as a GPIO + * controller, letting gpiolib-acpi call the _L02 handler as it would + * for a real GPIO controller. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "INT0002 Virtual GPIO" + +/* For some reason the virtual GPIO pin tied to the GPE is numbered pin 2 */ +#define GPE0A_PME_B0_VIRT_GPIO_PIN 2 + +#define GPE0A_PME_B0_STS_BIT BIT(13) +#define GPE0A_PME_B0_EN_BIT BIT(13) +#define GPE0A_STS_PORT 0x420 +#define GPE0A_EN_PORT 0x428 + +struct int0002_data { + struct gpio_chip chip; + int parent_irq; + int wake_enable_count; +}; + +/* + * As this is not a real GPIO at all, but just a hack to model an event in + * ACPI the get / set functions are dummy functions. + */ + +static int int0002_gpio_get(struct gpio_chip *chip, unsigned int offset) +{ + return 0; +} + +static int int0002_gpio_set(struct gpio_chip *chip, unsigned int offset, + int value) +{ + return 0; +} + +static int int0002_gpio_direction_output(struct gpio_chip *chip, + unsigned int offset, int value) +{ + return 0; +} + +static void int0002_irq_ack(struct irq_data *data) +{ + outl(GPE0A_PME_B0_STS_BIT, GPE0A_STS_PORT); +} + +static void int0002_irq_unmask(struct irq_data *data) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + irq_hw_number_t hwirq = irqd_to_hwirq(data); + u32 gpe_en_reg; + + gpiochip_enable_irq(gc, hwirq); + + gpe_en_reg = inl(GPE0A_EN_PORT); + gpe_en_reg |= GPE0A_PME_B0_EN_BIT; + outl(gpe_en_reg, GPE0A_EN_PORT); +} + +static void int0002_irq_mask(struct irq_data *data) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(data); + irq_hw_number_t hwirq = irqd_to_hwirq(data); + u32 gpe_en_reg; + + gpe_en_reg = inl(GPE0A_EN_PORT); + gpe_en_reg &= ~GPE0A_PME_B0_EN_BIT; + outl(gpe_en_reg, GPE0A_EN_PORT); + + gpiochip_disable_irq(gc, hwirq); +} + +static int int0002_irq_set_wake(struct irq_data *data, unsigned int on) +{ + struct gpio_chip *chip = irq_data_get_irq_chip_data(data); + struct int0002_data *int0002 = container_of(chip, struct int0002_data, chip); + + /* + * Applying of the wakeup flag to our parent IRQ is delayed till system + * suspend, because we only want to do this when using s2idle. + */ + if (on) + int0002->wake_enable_count++; + else + int0002->wake_enable_count--; + + return 0; +} + +static irqreturn_t int0002_irq(int irq, void *data) +{ + struct gpio_chip *chip = data; + u32 gpe_sts_reg; + + gpe_sts_reg = inl(GPE0A_STS_PORT); + if (!(gpe_sts_reg & GPE0A_PME_B0_STS_BIT)) + return IRQ_NONE; + + generic_handle_domain_irq_safe(chip->irq.domain, GPE0A_PME_B0_VIRT_GPIO_PIN); + + pm_wakeup_hard_event(chip->parent); + + return IRQ_HANDLED; +} + +static bool int0002_check_wake(void *data) +{ + u32 gpe_sts_reg; + + gpe_sts_reg = inl(GPE0A_STS_PORT); + return (gpe_sts_reg & GPE0A_PME_B0_STS_BIT); +} + +static const struct irq_chip int0002_irqchip = { + .name = DRV_NAME, + .irq_ack = int0002_irq_ack, + .irq_mask = int0002_irq_mask, + .irq_unmask = int0002_irq_unmask, + .irq_set_wake = int0002_irq_set_wake, + .flags = IRQCHIP_IMMUTABLE, + GPIOCHIP_IRQ_RESOURCE_HELPERS, +}; + +static void int0002_init_irq_valid_mask(struct gpio_chip *chip, + unsigned long *valid_mask, + unsigned int ngpios) +{ + bitmap_clear(valid_mask, 0, GPE0A_PME_B0_VIRT_GPIO_PIN); +} + +static int int0002_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct int0002_data *int0002; + struct gpio_irq_chip *girq; + struct gpio_chip *chip; + int irq, ret; + + /* Menlow has a different INT0002 device? */ + if (!soc_intel_is_byt() && !soc_intel_is_cht()) + return -ENODEV; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + int0002 = devm_kzalloc(dev, sizeof(*int0002), GFP_KERNEL); + if (!int0002) + return -ENOMEM; + + int0002->parent_irq = irq; + + chip = &int0002->chip; + chip->label = DRV_NAME; + chip->parent = dev; + chip->owner = THIS_MODULE; + chip->get = int0002_gpio_get; + chip->set = int0002_gpio_set; + chip->direction_input = int0002_gpio_get; + chip->direction_output = int0002_gpio_direction_output; + chip->base = -1; + chip->ngpio = GPE0A_PME_B0_VIRT_GPIO_PIN + 1; + chip->irq.init_valid_mask = int0002_init_irq_valid_mask; + + /* + * We directly request the irq here instead of passing a flow-handler + * to gpiochip_set_chained_irqchip, because the irq is shared. + * FIXME: augment this if we managed to pull handling of shared + * IRQs into gpiolib. + */ + ret = devm_request_irq(dev, irq, int0002_irq, IRQF_SHARED, "INT0002", + chip); + if (ret) { + dev_err(dev, "Error requesting IRQ %d: %d\n", irq, ret); + return ret; + } + + girq = &chip->irq; + gpio_irq_chip_set_chip(girq, &int0002_irqchip); + /* This let us handle the parent IRQ in the driver */ + girq->parent_handler = NULL; + girq->num_parents = 0; + girq->parents = NULL; + girq->default_type = IRQ_TYPE_NONE; + girq->handler = handle_edge_irq; + + ret = devm_gpiochip_add_data(dev, chip, NULL); + if (ret) { + dev_err(dev, "Error adding gpio chip: %d\n", ret); + return ret; + } + + acpi_register_wakeup_handler(irq, int0002_check_wake, NULL); + device_init_wakeup(dev, true); + dev_set_drvdata(dev, int0002); + return 0; +} + +static void int0002_remove(struct platform_device *pdev) +{ + device_init_wakeup(&pdev->dev, false); + acpi_unregister_wakeup_handler(int0002_check_wake, NULL); +} + +static int int0002_suspend(struct device *dev) +{ + struct int0002_data *int0002 = dev_get_drvdata(dev); + + /* + * The INT0002 parent IRQ is often shared with the ACPI GPE IRQ, don't + * muck with it when firmware based suspend is used, otherwise we may + * cause spurious wakeups from firmware managed suspend. + */ + if (!pm_suspend_via_firmware() && int0002->wake_enable_count) + enable_irq_wake(int0002->parent_irq); + + return 0; +} + +static int int0002_resume(struct device *dev) +{ + struct int0002_data *int0002 = dev_get_drvdata(dev); + + if (!pm_suspend_via_firmware() && int0002->wake_enable_count) + disable_irq_wake(int0002->parent_irq); + + return 0; +} + +static const struct dev_pm_ops int0002_pm_ops = { + .suspend = int0002_suspend, + .resume = int0002_resume, +}; + +static const struct acpi_device_id int0002_acpi_ids[] = { + { "INT0002", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, int0002_acpi_ids); + +static struct platform_driver int0002_driver = { + .driver = { + .name = DRV_NAME, + .acpi_match_table = int0002_acpi_ids, + .pm = &int0002_pm_ops, + }, + .probe = int0002_probe, + .remove = int0002_remove, +}; + +module_platform_driver(int0002_driver); + +MODULE_AUTHOR("Hans de Goede "); +MODULE_DESCRIPTION("Intel INT0002 Virtual GPIO driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/int1092/Kconfig b/drivers/platform/x86/intel/int1092/Kconfig new file mode 100644 index 000000000..2e9a17724 --- /dev/null +++ b/drivers/platform/x86/intel/int1092/Kconfig @@ -0,0 +1,14 @@ +config INTEL_SAR_INT1092 + tristate "Intel Specific Absorption Rate Driver" + depends on ACPI + help + This driver helps to limit the exposure of human body to RF frequency by + providing information to userspace application that will inform the Intel + M.2 modem to regulate the RF power based on SAR data obtained from the + sensors captured in the BIOS. ACPI interface exposes this data from the BIOS + to SAR driver. The front end application in userspace will interact with SAR + driver to obtain information like the device mode, Antenna index, baseband index, + SAR table index and use available communication like MBIM interface to enable + data communication to modem for RF power regulation. Enable this config when + given platform needs to support "Dynamic SAR" configuration for a modem available + on the platform. diff --git a/drivers/platform/x86/intel/int1092/Makefile b/drivers/platform/x86/intel/int1092/Makefile new file mode 100644 index 000000000..4ab94e541 --- /dev/null +++ b/drivers/platform/x86/intel/int1092/Makefile @@ -0,0 +1 @@ +obj-$(CONFIG_INTEL_SAR_INT1092) += intel_sar.o diff --git a/drivers/platform/x86/intel/int1092/intel_sar.c b/drivers/platform/x86/intel/int1092/intel_sar.c new file mode 100644 index 000000000..849f7b415 --- /dev/null +++ b/drivers/platform/x86/intel/int1092/intel_sar.c @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2021, Intel Corporation. + */ + +#include +#include +#include +#include +#include "intel_sar.h" + +/** + * get_int_value: Retrieve integer values from ACPI Object + * @obj: acpi_object pointer which has the integer value + * @out: output pointer will get integer value + * + * Function is used to retrieve integer value from acpi object. + * + * Return: + * * 0 on success + * * -EIO if there is an issue in acpi_object passed. + */ +static int get_int_value(union acpi_object *obj, int *out) +{ + if (!obj || obj->type != ACPI_TYPE_INTEGER) + return -EIO; + *out = (int)obj->integer.value; + return 0; +} + +/** + * update_sar_data: sar data is updated based on regulatory mode + * @context: pointer to driver context structure + * + * sar_data is updated based on regulatory value + * context->reg_value will never exceed MAX_REGULATORY + */ +static void update_sar_data(struct wwan_sar_context *context) +{ + struct wwan_device_mode_configuration *config = + &context->config_data[context->reg_value]; + + if (config->device_mode_info && + context->sar_data.device_mode < config->total_dev_mode) { + int itr = 0; + + for (itr = 0; itr < config->total_dev_mode; itr++) { + if (context->sar_data.device_mode == + config->device_mode_info[itr].device_mode) { + struct wwan_device_mode_info *dev_mode = + &config->device_mode_info[itr]; + + context->sar_data.antennatable_index = dev_mode->antennatable_index; + context->sar_data.bandtable_index = dev_mode->bandtable_index; + context->sar_data.sartable_index = dev_mode->sartable_index; + break; + } + } + } +} + +/** + * parse_package: parse acpi package for retrieving SAR information + * @context: pointer to driver context structure + * @item : acpi_object pointer + * + * Given acpi_object is iterated to retrieve information for each device mode. + * If a given package corresponding to a specific device mode is faulty, it is + * skipped and the specific entry in context structure will have the default value + * of zero. Decoding of subsequent device modes is realized by having "continue" + * statements in the for loop on encountering error in parsing given device mode. + * + * Return: + * AE_OK if success + * AE_ERROR on error + */ +static acpi_status parse_package(struct wwan_sar_context *context, union acpi_object *item) +{ + struct wwan_device_mode_configuration *data; + int value, itr, reg; + union acpi_object *num; + + num = &item->package.elements[0]; + if (get_int_value(num, &value) || value < 0 || value >= MAX_REGULATORY) + return AE_ERROR; + + reg = value; + + data = &context->config_data[reg]; + if (data->total_dev_mode > MAX_DEV_MODES || data->total_dev_mode == 0 || + item->package.count <= data->total_dev_mode) + return AE_ERROR; + + data->device_mode_info = kmalloc_objs(struct wwan_device_mode_info, + data->total_dev_mode); + if (!data->device_mode_info) + return AE_ERROR; + + for (itr = 0; itr < data->total_dev_mode; itr++) { + struct wwan_device_mode_info temp = { 0 }; + + num = &item->package.elements[itr + 1]; + if (num->type != ACPI_TYPE_PACKAGE || num->package.count < TOTAL_DATA) + continue; + if (get_int_value(&num->package.elements[0], &temp.device_mode)) + continue; + if (get_int_value(&num->package.elements[1], &temp.bandtable_index)) + continue; + if (get_int_value(&num->package.elements[2], &temp.antennatable_index)) + continue; + if (get_int_value(&num->package.elements[3], &temp.sartable_index)) + continue; + data->device_mode_info[itr] = temp; + } + return AE_OK; +} + +/** + * sar_get_device_mode: Extraction of information from BIOS via DSM calls + * @device: ACPI device for which to retrieve the data + * + * Retrieve the current device mode information from the BIOS. + * + * Return: + * AE_OK on success + * AE_ERROR on error + */ +static acpi_status sar_get_device_mode(struct platform_device *device) +{ + struct wwan_sar_context *context = dev_get_drvdata(&device->dev); + acpi_status status = AE_OK; + union acpi_object *out; + u32 rev = 0; + + out = acpi_evaluate_dsm_typed(context->handle, &context->guid, rev, + COMMAND_ID_DEV_MODE, NULL, ACPI_TYPE_INTEGER); + if (!out) { + dev_err(&device->dev, "DSM cmd:%d Failed to retrieve value\n", COMMAND_ID_DEV_MODE); + status = AE_ERROR; + goto dev_mode_error; + } + context->sar_data.device_mode = out->integer.value; + update_sar_data(context); + sysfs_notify(&device->dev.kobj, NULL, SYSFS_DATANAME); + +dev_mode_error: + ACPI_FREE(out); + return status; +} + +static const struct acpi_device_id sar_device_ids[] = { + { "INTC1092", 0}, + {} +}; +MODULE_DEVICE_TABLE(acpi, sar_device_ids); + +static ssize_t intc_data_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct wwan_sar_context *context = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d %d %d %d\n", context->sar_data.device_mode, + context->sar_data.bandtable_index, + context->sar_data.antennatable_index, + context->sar_data.sartable_index); +} +static DEVICE_ATTR_RO(intc_data); + +static ssize_t intc_reg_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct wwan_sar_context *context = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", context->reg_value); +} + +static ssize_t intc_reg_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct wwan_sar_context *context = dev_get_drvdata(dev); + unsigned int value; + int read; + + if (!count) + return -EINVAL; + read = kstrtouint(buf, 10, &value); + if (read < 0) + return read; + if (value >= MAX_REGULATORY) + return -EOVERFLOW; + context->reg_value = value; + update_sar_data(context); + sysfs_notify(&dev->kobj, NULL, SYSFS_DATANAME); + return count; +} +static DEVICE_ATTR_RW(intc_reg); + +static struct attribute *intcsar_attrs[] = { + &dev_attr_intc_data.attr, + &dev_attr_intc_reg.attr, + NULL +}; + +static struct attribute_group intcsar_group = { + .attrs = intcsar_attrs, +}; + +static void sar_notify(acpi_handle handle, u32 event, void *data) +{ + struct platform_device *device = data; + + if (event == SAR_EVENT) { + if (sar_get_device_mode(device) != AE_OK) + dev_err(&device->dev, "sar_get_device_mode error"); + } +} + +static void sar_get_data(int reg, struct wwan_sar_context *context) +{ + union acpi_object *out, req; + u32 rev = 0; + + req.type = ACPI_TYPE_INTEGER; + req.integer.value = reg; + out = acpi_evaluate_dsm_typed(context->handle, &context->guid, rev, + COMMAND_ID_CONFIG_TABLE, &req, ACPI_TYPE_PACKAGE); + if (!out) + return; + if (out->package.count >= 3 && + out->package.elements[0].type == ACPI_TYPE_INTEGER && + out->package.elements[1].type == ACPI_TYPE_INTEGER && + out->package.elements[2].type == ACPI_TYPE_PACKAGE && + out->package.elements[2].package.count > 0) { + context->config_data[reg].version = out->package.elements[0].integer.value; + context->config_data[reg].total_dev_mode = + out->package.elements[1].integer.value; + if (context->config_data[reg].total_dev_mode <= 0 || + context->config_data[reg].total_dev_mode > MAX_DEV_MODES) { + ACPI_FREE(out); + return; + } + parse_package(context, &out->package.elements[2]); + } + ACPI_FREE(out); +} + +static int sar_probe(struct platform_device *device) +{ + struct wwan_sar_context *context; + acpi_handle handle; + int reg; + int result; + + handle = ACPI_HANDLE(&device->dev); + if (!handle) + return -ENODEV; + + context = kzalloc_obj(*context); + if (!context) + return -ENOMEM; + + context->sar_device = device; + context->handle = handle; + dev_set_drvdata(&device->dev, context); + + result = guid_parse(SAR_DSM_UUID, &context->guid); + if (result) { + dev_err(&device->dev, "SAR UUID parse error: %d\n", result); + goto r_free; + } + + for (reg = 0; reg < MAX_REGULATORY; reg++) + sar_get_data(reg, context); + + if (sar_get_device_mode(device) != AE_OK) { + dev_err(&device->dev, "Failed to get device mode\n"); + result = -EIO; + goto r_free; + } + + result = sysfs_create_group(&device->dev.kobj, &intcsar_group); + if (result) { + dev_err(&device->dev, "sysfs creation failed\n"); + goto r_free; + } + + if (acpi_install_notify_handler(ACPI_HANDLE(&device->dev), ACPI_DEVICE_NOTIFY, + sar_notify, (void *)device) != AE_OK) { + dev_err(&device->dev, "Failed acpi_install_notify_handler\n"); + result = -EIO; + goto r_sys; + } + return 0; + +r_sys: + sysfs_remove_group(&device->dev.kobj, &intcsar_group); +r_free: + kfree(context); + return result; +} + +static void sar_remove(struct platform_device *device) +{ + struct wwan_sar_context *context = dev_get_drvdata(&device->dev); + int reg; + + acpi_remove_notify_handler(ACPI_HANDLE(&device->dev), + ACPI_DEVICE_NOTIFY, sar_notify); + sysfs_remove_group(&device->dev.kobj, &intcsar_group); + for (reg = 0; reg < MAX_REGULATORY; reg++) + kfree(context->config_data[reg].device_mode_info); + + kfree(context); +} + +static struct platform_driver sar_driver = { + .probe = sar_probe, + .remove = sar_remove, + .driver = { + .name = DRVNAME, + .acpi_match_table = ACPI_PTR(sar_device_ids) + } +}; +module_platform_driver(sar_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Platform device driver for INTEL MODEM BIOS SAR"); +MODULE_AUTHOR("Shravan Sudhakar "); diff --git a/drivers/platform/x86/intel/int1092/intel_sar.h b/drivers/platform/x86/intel/int1092/intel_sar.h new file mode 100644 index 000000000..b5310510b --- /dev/null +++ b/drivers/platform/x86/intel/int1092/intel_sar.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2021, Intel Corporation. + */ +#ifndef INTEL_SAR_H +#define INTEL_SAR_H + +#define COMMAND_ID_DEV_MODE 1 +#define COMMAND_ID_CONFIG_TABLE 2 +#define DRVNAME "intc_sar" +#define MAX_DEV_MODES 50 +#define MAX_REGULATORY 3 +#define SAR_DSM_UUID "82737E72-3A33-4C45-A9C7-57C0411A5F13" +#define SAR_EVENT 0x80 +#define SYSFS_DATANAME "intc_data" +#define TOTAL_DATA 4 + +/** + * Structure wwan_device_mode_info - device mode information + * Holds the data that needs to be passed to userspace. + * The data is updated from the BIOS sensor information. + * @device_mode: Specific mode of the device + * @bandtable_index: Index of RF band + * @antennatable_index: Index of antenna + * @sartable_index: Index of SAR + */ +struct wwan_device_mode_info { + int device_mode; + int bandtable_index; + int antennatable_index; + int sartable_index; +}; + +/** + * Structure wwan_device_mode_configuration - device configuration + * Holds the data that is configured and obtained on probe event. + * The data is updated from the BIOS sensor information. + * @version: Mode configuration version + * @total_dev_mode: Total number of device modes + * @device_mode_info: pointer to structure wwan_device_mode_info + */ +struct wwan_device_mode_configuration { + int version; + int total_dev_mode; + struct wwan_device_mode_info *device_mode_info; +}; + +/** + * Structure wwan_supported_info - userspace datastore + * Holds the data that is obtained from userspace + * The data is updated from the userspace and send value back in the + * structure format that is mentioned here. + * @reg_mode_needed: regulatory mode set by user for tests + * @bios_table_revision: Version of SAR table + * @num_supported_modes: Total supported modes based on reg_mode + */ +struct wwan_supported_info { + int reg_mode_needed; + int bios_table_revision; + int num_supported_modes; +}; + +/** + * Structure wwan_sar_context - context of SAR + * Holds the complete context as long as the driver is in existence + * The context holds instance of the data used for different cases. + * @guid: Group id + * @handle: store acpi handle + * @reg_value: regulatory value + * Regulatory 0: FCC, 1: CE, 2: ISED + * @sar_device: platform_device type + * @sar_kobject: kobject for sysfs + * @supported_data: wwan_supported_info struct + * @sar_data: wwan_device_mode_info struct + * @config_data: wwan_device_mode_configuration array struct + */ +struct wwan_sar_context { + guid_t guid; + acpi_handle handle; + int reg_value; + struct platform_device *sar_device; + struct wwan_supported_info supported_data; + struct wwan_device_mode_info sar_data; + struct wwan_device_mode_configuration config_data[MAX_REGULATORY]; +}; +#endif /* INTEL_SAR_H */ diff --git a/drivers/platform/x86/intel/int3472/Kconfig b/drivers/platform/x86/intel/int3472/Kconfig new file mode 100644 index 000000000..17ae997f9 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/Kconfig @@ -0,0 +1,31 @@ +config INTEL_SKL_INT3472 + tristate "Intel SkyLake ACPI INT3472 Driver" + depends on ACPI + depends on COMMON_CLK + depends on I2C + depends on GPIOLIB + depends on LEDS_CLASS + depends on REGULATOR + select MFD_CORE + select REGMAP_I2C + help + This driver adds power controller support for the Intel SkyCam + devices found on the Intel SkyLake platforms. + + The INT3472 is a camera power controller, a logical device found on + Intel Skylake-based systems that can map to different hardware + devices depending on the platform. On machines designed for Chrome OS + it maps to a TPS68470 camera PMIC. On machines designed for Windows, + it maps to either a TP68470 camera PMIC, a uP6641Q sensor PMIC, or a + set of discrete GPIOs and power gates. + + If your device was designed for Chrome OS, this driver will provide + an ACPI OpRegion, which must be available before any of the devices + using it are probed. For this reason, you should select Y if your + device was designed for ChromeOS. For the same reason the + I2C_DESIGNWARE_PLATFORM option must be set to Y too. + + Say Y or M here if you have a SkyLake device designed for use + with Windows or ChromeOS. Say N here if you are not sure. + + The module will be named "intel-skl-int3472". diff --git a/drivers/platform/x86/intel/int3472/Makefile b/drivers/platform/x86/intel/int3472/Makefile new file mode 100644 index 000000000..103661e66 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/Makefile @@ -0,0 +1,8 @@ +obj-$(CONFIG_INTEL_SKL_INT3472) += intel_skl_int3472_discrete.o \ + intel_skl_int3472_tps68470.o \ + intel_skl_int3472_common.o +intel_skl_int3472_discrete-y := discrete.o discrete_quirks.o \ + clk_and_regulator.o led.o +intel_skl_int3472_tps68470-y := tps68470.o tps68470_board_data.o + +intel_skl_int3472_common-y += common.o diff --git a/drivers/platform/x86/intel/int3472/clk_and_regulator.c b/drivers/platform/x86/intel/int3472/clk_and_regulator.c new file mode 100644 index 000000000..9e052b164 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/clk_and_regulator.c @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Dan Scally */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 82c0d13a-78c5-4244-9bb1-eb8b539a8d11 + * This _DSM GUID allows controlling the sensor clk when it is not controlled + * through a GPIO. + */ +static const guid_t img_clk_guid = + GUID_INIT(0x82c0d13a, 0x78c5, 0x4244, + 0x9b, 0xb1, 0xeb, 0x8b, 0x53, 0x9a, 0x8d, 0x11); + +static void skl_int3472_enable_clk(struct int3472_clock *clk, int enable) +{ + struct int3472_discrete_device *int3472 = to_int3472_device(clk); + union acpi_object args[3]; + union acpi_object argv4; + + if (clk->ena_gpio) { + gpiod_set_value_cansleep(clk->ena_gpio, enable); + return; + } + + args[0].integer.type = ACPI_TYPE_INTEGER; + args[0].integer.value = clk->imgclk_index; + args[1].integer.type = ACPI_TYPE_INTEGER; + args[1].integer.value = enable; + args[2].integer.type = ACPI_TYPE_INTEGER; + args[2].integer.value = 1; + + argv4.type = ACPI_TYPE_PACKAGE; + argv4.package.count = 3; + argv4.package.elements = args; + + acpi_evaluate_dsm(acpi_device_handle(int3472->adev), &img_clk_guid, + 0, 1, &argv4); +} + +/* + * The regulators have to have .ops to be valid, but the only ops we actually + * support are .enable and .disable which are handled via .ena_gpiod. Pass an + * empty struct to clear the check without lying about capabilities. + */ +static const struct regulator_ops int3472_gpio_regulator_ops; + +static int skl_int3472_clk_prepare(struct clk_hw *hw) +{ + skl_int3472_enable_clk(to_int3472_clk(hw), 1); + return 0; +} + +static void skl_int3472_clk_unprepare(struct clk_hw *hw) +{ + skl_int3472_enable_clk(to_int3472_clk(hw), 0); +} + +static int skl_int3472_clk_enable(struct clk_hw *hw) +{ + /* + * We're just turning a GPIO on to enable the clock, which operation + * has the potential to sleep. Given .enable() cannot sleep, but + * .prepare() can, we toggle the GPIO in .prepare() instead. Thus, + * nothing to do here. + */ + return 0; +} + +static void skl_int3472_clk_disable(struct clk_hw *hw) +{ + /* Likewise, nothing to do here... */ +} + +static unsigned int skl_int3472_get_clk_frequency(struct int3472_discrete_device *int3472) +{ + union acpi_object *obj; + unsigned int freq; + + obj = skl_int3472_get_acpi_buffer(int3472->sensor, "SSDB"); + if (IS_ERR(obj)) + return 0; /* report rate as 0 on error */ + + if (obj->buffer.length < CIO2_SENSOR_SSDB_MCLKSPEED_OFFSET + sizeof(u32)) { + dev_err(int3472->dev, "The buffer is too small\n"); + kfree(obj); + return 0; + } + + freq = *(u32 *)(obj->buffer.pointer + CIO2_SENSOR_SSDB_MCLKSPEED_OFFSET); + + kfree(obj); + return freq; +} + +static unsigned long skl_int3472_clk_recalc_rate(struct clk_hw *hw, + unsigned long parent_rate) +{ + struct int3472_clock *clk = to_int3472_clk(hw); + + return clk->frequency; +} + +static const struct clk_ops skl_int3472_clock_ops = { + .prepare = skl_int3472_clk_prepare, + .unprepare = skl_int3472_clk_unprepare, + .enable = skl_int3472_clk_enable, + .disable = skl_int3472_clk_disable, + .recalc_rate = skl_int3472_clk_recalc_rate, +}; + +static int skl_int3472_register_clock(struct int3472_discrete_device *int3472) +{ + struct acpi_device *adev = int3472->adev; + struct clk_init_data init = { + .ops = &skl_int3472_clock_ops, + .flags = CLK_GET_RATE_NOCACHE, + }; + int ret; + + init.name = kasprintf(GFP_KERNEL, "%s-clk", acpi_dev_name(adev)); + if (!init.name) + return -ENOMEM; + + int3472->clock.frequency = skl_int3472_get_clk_frequency(int3472); + int3472->clock.clk_hw.init = &init; + int3472->clock.clk = clk_register(&adev->dev, &int3472->clock.clk_hw); + if (IS_ERR(int3472->clock.clk)) { + ret = PTR_ERR(int3472->clock.clk); + goto out_free_init_name; + } + + int3472->clock.cl = clkdev_create(int3472->clock.clk, NULL, int3472->sensor_name); + if (!int3472->clock.cl) { + ret = -ENOMEM; + goto err_unregister_clk; + } + + kfree(init.name); + return 0; + +err_unregister_clk: + clk_unregister(int3472->clock.clk); +out_free_init_name: + kfree(init.name); + return ret; +} + +int skl_int3472_register_dsm_clock(struct int3472_discrete_device *int3472) +{ + if (int3472->clock.cl) + return 0; /* A GPIO controlled clk has already been registered */ + + if (!acpi_check_dsm(int3472->adev->handle, &img_clk_guid, 0, BIT(1))) + return 0; /* DSM clock control is not available */ + + return skl_int3472_register_clock(int3472); +} + +int skl_int3472_register_gpio_clock(struct int3472_discrete_device *int3472, + struct gpio_desc *gpio) +{ + if (int3472->clock.cl) + return -EBUSY; + + int3472->clock.ena_gpio = gpio; + + return skl_int3472_register_clock(int3472); +} + +void skl_int3472_unregister_clock(struct int3472_discrete_device *int3472) +{ + if (!int3472->clock.cl) + return; + + clkdev_drop(int3472->clock.cl); + clk_unregister(int3472->clock.clk); + gpiod_put(int3472->clock.ena_gpio); +} + +int skl_int3472_register_regulator(struct int3472_discrete_device *int3472, + struct gpio_desc *gpio, + unsigned int enable_time, + const char *supply_name, + const char *second_sensor) +{ + struct regulator_init_data init_data = { }; + struct int3472_gpio_regulator *regulator; + struct regulator_config cfg = { }; + int i, j; + + if (int3472->n_regulator_gpios >= INT3472_MAX_REGULATORS) { + dev_err(int3472->dev, "Too many regulators mapped\n"); + return -EINVAL; + } + + if (strlen(supply_name) >= GPIO_SUPPLY_NAME_LENGTH) { + dev_err(int3472->dev, "supply-name '%s' length too long\n", supply_name); + return -E2BIG; + } + + regulator = &int3472->regulators[int3472->n_regulator_gpios]; + string_upper(regulator->supply_name_upper, supply_name); + + /* The below code assume that map-count is 2 (upper- and lower-case) */ + static_assert(GPIO_REGULATOR_SUPPLY_MAP_COUNT == 2); + + for (i = 0, j = 0; i < GPIO_REGULATOR_SUPPLY_MAP_COUNT; i++) { + const char *supply = i ? regulator->supply_name_upper : supply_name; + + regulator->supply_map[j].supply = supply; + regulator->supply_map[j].dev_name = int3472->sensor_name; + j++; + + if (second_sensor) { + regulator->supply_map[j].supply = supply; + regulator->supply_map[j].dev_name = second_sensor; + j++; + } + } + + init_data.constraints.valid_ops_mask = REGULATOR_CHANGE_STATUS; + init_data.consumer_supplies = regulator->supply_map; + init_data.num_consumer_supplies = j; + + snprintf(regulator->regulator_name, sizeof(regulator->regulator_name), "%s-%s", + acpi_dev_name(int3472->adev), supply_name); + + regulator->rdesc = INT3472_REGULATOR(regulator->regulator_name, + &int3472_gpio_regulator_ops, + enable_time, GPIO_REGULATOR_OFF_ON_DELAY); + + cfg.dev = &int3472->adev->dev; + cfg.init_data = &init_data; + cfg.ena_gpiod = gpio; + + regulator->rdev = regulator_register(int3472->dev, ®ulator->rdesc, &cfg); + if (IS_ERR(regulator->rdev)) + return PTR_ERR(regulator->rdev); + + int3472->n_regulator_gpios++; + return 0; +} + +void skl_int3472_unregister_regulator(struct int3472_discrete_device *int3472) +{ + for (int i = 0; i < int3472->n_regulator_gpios; i++) + regulator_unregister(int3472->regulators[i].rdev); +} diff --git a/drivers/platform/x86/intel/int3472/common.c b/drivers/platform/x86/intel/int3472/common.c new file mode 100644 index 000000000..6dc38d5cb --- /dev/null +++ b/drivers/platform/x86/intel/int3472/common.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Dan Scally */ + +#include +#include +#include + +union acpi_object *skl_int3472_get_acpi_buffer(struct acpi_device *adev, char *id) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_handle handle = adev->handle; + union acpi_object *obj; + acpi_status status; + + status = acpi_evaluate_object(handle, id, NULL, &buffer); + if (ACPI_FAILURE(status)) + return ERR_PTR(-ENODEV); + + obj = buffer.pointer; + if (!obj) + return ERR_PTR(-ENODEV); + + if (obj->type != ACPI_TYPE_BUFFER) { + acpi_handle_err(handle, "%s object is not an ACPI buffer\n", id); + kfree(obj); + return ERR_PTR(-EINVAL); + } + + return obj; +} +EXPORT_SYMBOL_NS_GPL(skl_int3472_get_acpi_buffer, "INTEL_INT3472"); + +int skl_int3472_fill_cldb(struct acpi_device *adev, struct int3472_cldb *cldb) +{ + union acpi_object *obj; + int ret; + + obj = skl_int3472_get_acpi_buffer(adev, "CLDB"); + if (IS_ERR(obj)) + return PTR_ERR(obj); + + if (obj->buffer.length > sizeof(*cldb)) { + acpi_handle_err(adev->handle, "The CLDB buffer is too large\n"); + ret = -EINVAL; + goto out_free_obj; + } + + memcpy(cldb, obj->buffer.pointer, obj->buffer.length); + ret = 0; + +out_free_obj: + kfree(obj); + return ret; +} +EXPORT_SYMBOL_NS_GPL(skl_int3472_fill_cldb, "INTEL_INT3472"); + +/* sensor_adev_ret may be NULL, name_ret must not be NULL */ +int skl_int3472_get_sensor_adev_and_name(struct device *dev, + struct acpi_device **sensor_adev_ret, + const char **name_ret) +{ + struct acpi_device *adev = ACPI_COMPANION(dev); + struct acpi_device *sensor; + int ret = 0; + + sensor = acpi_dev_get_next_consumer_dev(adev, NULL); + if (!sensor) { + dev_err(dev, "INT3472 seems to have no dependents.\n"); + return -ENODEV; + } + + dev_dbg(dev, "Sensor name %s\n", acpi_dev_name(sensor)); + + *name_ret = devm_kasprintf(dev, GFP_KERNEL, I2C_DEV_NAME_FORMAT, + acpi_dev_name(sensor)); + if (!*name_ret) + ret = -ENOMEM; + + if (ret == 0 && sensor_adev_ret) + *sensor_adev_ret = sensor; + else + acpi_dev_put(sensor); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(skl_int3472_get_sensor_adev_and_name, "INTEL_INT3472"); + +MODULE_DESCRIPTION("Intel SkyLake INT3472 ACPI Device Driver library"); +MODULE_AUTHOR("Daniel Scally "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/int3472/discrete.c b/drivers/platform/x86/intel/int3472/discrete.c new file mode 100644 index 000000000..6c729fcfc --- /dev/null +++ b/drivers/platform/x86/intel/int3472/discrete.c @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Dan Scally */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 79234640-9e10-4fea-a5c1-b5aa8b19756f + * This _DSM GUID returns information about the GPIO lines mapped to a + * discrete INT3472 device. Function number 1 returns a count of the GPIO + * lines that are mapped. Subsequent functions return 32 bit ints encoding + * information about the GPIO line, including its purpose. + */ +static const guid_t int3472_gpio_guid = + GUID_INIT(0x79234640, 0x9e10, 0x4fea, + 0xa5, 0xc1, 0xb5, 0xaa, 0x8b, 0x19, 0x75, 0x6f); + +#define INT3472_GPIO_DSM_TYPE GENMASK(7, 0) +#define INT3472_GPIO_DSM_PIN GENMASK(15, 8) +#define INT3472_GPIO_DSM_SENSOR_ON_VAL GENMASK(31, 24) + +/* + * 822ace8f-2814-4174-a56b-5f029fe079ee + * This _DSM GUID returns a string from the sensor device, which acts as a + * module identifier. + */ +static const guid_t cio2_sensor_module_guid = + GUID_INIT(0x822ace8f, 0x2814, 0x4174, + 0xa5, 0x6b, 0x5f, 0x02, 0x9f, 0xe0, 0x79, 0xee); + +static void skl_int3472_log_sensor_module_name(struct int3472_discrete_device *int3472) +{ + union acpi_object *obj; + + obj = acpi_evaluate_dsm_typed(int3472->sensor->handle, + &cio2_sensor_module_guid, 0x00, + 0x01, NULL, ACPI_TYPE_STRING); + if (obj) { + dev_dbg(int3472->dev, "Sensor module id: '%s'\n", obj->string.pointer); + ACPI_FREE(obj); + } +} + +static int skl_int3472_fill_gpiod_lookup(struct gpiod_lookup *table_entry, + struct acpi_resource_gpio *agpio, + const char *con_id, unsigned long gpio_flags) +{ + char *path = agpio->resource_source.string_ptr; + struct acpi_device *adev; + acpi_handle handle; + acpi_status status; + + status = acpi_get_handle(NULL, path, &handle); + if (ACPI_FAILURE(status)) + return -EINVAL; + + adev = acpi_fetch_acpi_dev(handle); + if (!adev) + return -ENODEV; + + *table_entry = GPIO_LOOKUP(acpi_dev_name(adev), agpio->pin_table[0], con_id, gpio_flags); + + return 0; +} + +static int skl_int3472_map_gpio_to_sensor(struct int3472_discrete_device *int3472, + struct acpi_resource_gpio *agpio, + const char *con_id, unsigned long gpio_flags) +{ + int ret; + + if (int3472->n_sensor_gpios >= INT3472_MAX_SENSOR_GPIOS) { + dev_warn(int3472->dev, "Too many GPIOs mapped\n"); + return -EINVAL; + } + + ret = skl_int3472_fill_gpiod_lookup(&int3472->gpios.table[int3472->n_sensor_gpios], + agpio, con_id, gpio_flags); + if (ret) + return ret; + + int3472->n_sensor_gpios++; + + return 0; +} + +/* This should *really* only be used when there's no other way... */ +static struct gpio_desc * +skl_int3472_gpiod_get_from_temp_lookup(struct int3472_discrete_device *int3472, + struct acpi_resource_gpio *agpio, + const char *con_id, unsigned long gpio_flags) +{ + struct gpio_desc *desc; + int ret; + + struct gpiod_lookup_table *lookup __free(kfree) = + kzalloc_flex(*lookup, table, 2); + if (!lookup) + return ERR_PTR(-ENOMEM); + + lookup->dev_id = dev_name(int3472->dev); + ret = skl_int3472_fill_gpiod_lookup(&lookup->table[0], agpio, con_id, gpio_flags); + if (ret) + return ERR_PTR(ret); + + gpiod_add_lookup_table(lookup); + desc = gpiod_get(int3472->dev, con_id, GPIOD_OUT_LOW); + gpiod_remove_lookup_table(lookup); + + return desc; +} + +/* + * Other vana-supply users (e.g. ST, Toshiba, Sony sensors) can be added to + * this array instead of adding new quirk table entries. + */ +static const char * const power_enable_hids_vana[] = { + "SONY471A", /* imx471 on Lenovo X9-14 and X9-15 */ + "TBE20A0", /* imx471 on Lenovo X1 Carbon G14 */ + NULL +}; + +static const char * const power_enable_hids_vdd[] = { + "INT33F0", /* mt9m114 */ + NULL +}; + +static const char * const power_enable_hids_enable[] = { + "INT347E", /* ov7251 */ + NULL +}; + +/** + * struct int3472_gpio_map - Map GPIOs to whatever is expected by the + * sensor driver (as in DT bindings) + * @hids: NULL-terminated array of ACPI HIDs of the devices without the + * instance number e.g. INT347E + * @type_from: The GPIO type from ACPI ?SDT + * @type_to: The assigned GPIO type, typically same as @type_from + * @enable_time_us: Enable time in usec for GPIOs mapped to regulators + * @con_id: The name of the GPIO for the device + * @polarity_low: GPIO_ACTIVE_LOW true if the @polarity_low is true, + * GPIO_ACTIVE_HIGH otherwise + */ +struct int3472_gpio_map { + const char * const *hids; + u8 type_from; + u8 type_to; + bool polarity_low; + unsigned int enable_time_us; + const char *con_id; +}; + +static const struct int3472_gpio_map int3472_gpio_map[] = { + { /* mt9m114 designs declare a powerdown pin which controls the regulators */ + .hids = power_enable_hids_vdd, + .type_from = INT3472_GPIO_TYPE_POWERDOWN, + .type_to = INT3472_GPIO_TYPE_POWER_ENABLE, + .con_id = "vdd", + .enable_time_us = GPIO_REGULATOR_ENABLE_TIME, + }, + { /* ov7251 driver / DT-bindings expect "enable" as con_id for reset */ + .hids = power_enable_hids_enable, + .type_from = INT3472_GPIO_TYPE_RESET, + .type_to = INT3472_GPIO_TYPE_RESET, + .con_id = "enable", + }, + { /* ov08x40's handshake pin needs a 45 ms delay on some HP laptops */ + .hids = (const char * const[]) { "OVTI08F4", NULL }, + .type_from = INT3472_GPIO_TYPE_HANDSHAKE, + .type_to = INT3472_GPIO_TYPE_HANDSHAKE, + .con_id = "dvdd", + .enable_time_us = 45 * USEC_PER_MSEC, + }, + { /* Sensors which expect "vana" as con_id for power enable */ + .hids = power_enable_hids_vana, + .type_from = INT3472_GPIO_TYPE_POWER_ENABLE, + .type_to = INT3472_GPIO_TYPE_POWER_ENABLE, + .con_id = "vana", + .enable_time_us = GPIO_REGULATOR_ENABLE_TIME, + }, +}; + +static bool int3472_gpio_map_hids_match(struct acpi_device *adev, + const char * const *hids) +{ + for (unsigned int i = 0; hids[i]; i++) { + if (acpi_dev_hid_uid_match(adev, hids[i], NULL)) + return true; + } + + return false; +} + +static void int3472_get_con_id_and_polarity(struct int3472_discrete_device *int3472, u8 *type, + const char **con_id, unsigned long *gpio_flags, + unsigned int *enable_time_us) +{ + struct acpi_device *adev = int3472->sensor; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(int3472_gpio_map); i++) { + /* + * Map the firmware-provided GPIO to whatever a driver expects + * (as in DT bindings). First check if the type matches with the + * GPIO map, then further check that the device _HID matches. + */ + if (*type != int3472_gpio_map[i].type_from) + continue; + + if (!int3472_gpio_map_hids_match(adev, int3472_gpio_map[i].hids)) + continue; + + dev_dbg(int3472->dev, "mapping type 0x%02x pin to 0x%02x %s\n", + *type, int3472_gpio_map[i].type_to, int3472_gpio_map[i].con_id); + + *type = int3472_gpio_map[i].type_to; + *gpio_flags = int3472_gpio_map[i].polarity_low ? + GPIO_ACTIVE_LOW : GPIO_ACTIVE_HIGH; + *con_id = int3472_gpio_map[i].con_id; + *enable_time_us = int3472_gpio_map[i].enable_time_us; + return; + } + + *enable_time_us = GPIO_REGULATOR_ENABLE_TIME; + + switch (*type) { + case INT3472_GPIO_TYPE_RESET: + *con_id = "reset"; + *gpio_flags = GPIO_ACTIVE_LOW; + break; + case INT3472_GPIO_TYPE_POWERDOWN: + *con_id = "powerdown"; + *gpio_flags = GPIO_ACTIVE_LOW; + break; + case INT3472_GPIO_TYPE_CLK_ENABLE: + *con_id = "clk-enable"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_PRIVACY_LED: + *con_id = "privacy"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_STROBE: + *con_id = "ir_flood"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_HOTPLUG_DETECT: + *con_id = "hpd"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_POWER_ENABLE: + *con_id = "avdd"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_DOVDD: + *con_id = "dovdd"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + case INT3472_GPIO_TYPE_HANDSHAKE: + *con_id = "dvdd"; + *gpio_flags = GPIO_ACTIVE_HIGH; + /* Setups using a handshake pin need 25 ms enable delay */ + *enable_time_us = 25 * USEC_PER_MSEC; + break; + default: + *con_id = "unknown"; + *gpio_flags = GPIO_ACTIVE_HIGH; + break; + } +} + +/** + * skl_int3472_handle_gpio_resources: Map PMIC resources to consuming sensor + * @ares: A pointer to a &struct acpi_resource + * @data: A pointer to a &struct int3472_discrete_device + * + * This function handles GPIO resources that are against an INT3472 + * ACPI device, by checking the value of the corresponding _DSM entry. + * This will return a 32bit int, where the lowest byte represents the + * function of the GPIO pin: + * + * 0x00 Reset + * 0x01 Power down + * 0x02 Strobe + * 0x0b Power enable + * 0x0c Clock enable + * 0x0d Privacy LED + * 0x10 DOVDD (digital I/O voltage) + * 0x13 Hotplug detect + * + * There are some known platform specific quirks where that does not quite + * hold up; for example where a pin with type 0x01 (Power down) is mapped to + * a sensor pin that performs a reset function or entries in _CRS and _DSM that + * do not actually correspond to a physical connection. These will be handled + * by the mapping sub-functions. + * + * GPIOs will either be mapped directly to the sensor device or else used + * to create clocks and regulators via the usual frameworks. + * + * Return: + * * 1 - Continue the loop without adding a copy of the resource to + * * the list passed to acpi_dev_get_resources() + * * 0 - Continue the loop after adding a copy of the resource to + * * the list passed to acpi_dev_get_resources() + * * -errno - Error, break loop + */ +static int skl_int3472_handle_gpio_resources(struct acpi_resource *ares, + void *data) +{ + struct int3472_discrete_device *int3472 = data; + const char *second_sensor = NULL; + struct acpi_resource_gpio *agpio; + unsigned int enable_time_us; + u8 active_value, pin, type; + unsigned long gpio_flags; + union acpi_object *obj; + struct gpio_desc *gpio; + const char *err_msg; + const char *con_id; + int ret; + + if (!acpi_gpio_get_io_resource(ares, &agpio)) + return 1; + + /* + * ngpios + 2 because the index of this _DSM function is 1-based and + * the first function is just a count. + */ + obj = acpi_evaluate_dsm_typed(int3472->adev->handle, + &int3472_gpio_guid, 0x00, + int3472->ngpios + 2, + NULL, ACPI_TYPE_INTEGER); + + if (!obj) { + dev_warn(int3472->dev, "No _DSM entry for GPIO pin %u\n", + agpio->pin_table[0]); + return 1; + } + + type = FIELD_GET(INT3472_GPIO_DSM_TYPE, obj->integer.value); + + int3472_get_con_id_and_polarity(int3472, &type, &con_id, &gpio_flags, &enable_time_us); + + pin = FIELD_GET(INT3472_GPIO_DSM_PIN, obj->integer.value); + /* Pin field is not really used under Windows and wraps around at 8 bits */ + if (pin != (agpio->pin_table[0] & 0xff)) + dev_dbg(int3472->dev, FW_BUG "%s %s pin number mismatch _DSM %d resource %d\n", + con_id, agpio->resource_source.string_ptr, pin, agpio->pin_table[0]); + + active_value = FIELD_GET(INT3472_GPIO_DSM_SENSOR_ON_VAL, obj->integer.value); + if (!active_value) + gpio_flags ^= GPIO_ACTIVE_LOW; + + dev_dbg(int3472->dev, "%s %s pin %d active-%s\n", con_id, + agpio->resource_source.string_ptr, agpio->pin_table[0], + str_high_low(gpio_flags == GPIO_ACTIVE_HIGH)); + + switch (type) { + case INT3472_GPIO_TYPE_RESET: + case INT3472_GPIO_TYPE_POWERDOWN: + case INT3472_GPIO_TYPE_HOTPLUG_DETECT: + ret = skl_int3472_map_gpio_to_sensor(int3472, agpio, con_id, gpio_flags); + if (ret) + err_msg = "Failed to map GPIO pin to sensor\n"; + + break; + case INT3472_GPIO_TYPE_CLK_ENABLE: + case INT3472_GPIO_TYPE_PRIVACY_LED: + case INT3472_GPIO_TYPE_STROBE: + case INT3472_GPIO_TYPE_POWER_ENABLE: + case INT3472_GPIO_TYPE_DOVDD: + case INT3472_GPIO_TYPE_HANDSHAKE: + gpio = skl_int3472_gpiod_get_from_temp_lookup(int3472, agpio, con_id, gpio_flags); + if (IS_ERR(gpio)) { + ret = PTR_ERR(gpio); + err_msg = "Failed to get GPIO\n"; + break; + } + + switch (type) { + case INT3472_GPIO_TYPE_CLK_ENABLE: + ret = skl_int3472_register_gpio_clock(int3472, gpio); + if (ret) + err_msg = "Failed to register clock\n"; + + break; + case INT3472_GPIO_TYPE_PRIVACY_LED: + case INT3472_GPIO_TYPE_STROBE: + ret = skl_int3472_register_led(int3472, gpio, con_id); + if (ret) + err_msg = "Failed to register LED\n"; + + break; + case INT3472_GPIO_TYPE_POWER_ENABLE: + second_sensor = int3472->quirks.avdd_second_sensor; + fallthrough; + case INT3472_GPIO_TYPE_DOVDD: + case INT3472_GPIO_TYPE_HANDSHAKE: + ret = skl_int3472_register_regulator(int3472, gpio, enable_time_us, + con_id, second_sensor); + if (ret) + err_msg = "Failed to register regulator\n"; + + break; + default: /* Never reached */ + ret = -EINVAL; + break; + } + + if (ret) + gpiod_put(gpio); + + break; + default: + dev_warn(int3472->dev, + "GPIO type 0x%02x unknown; the sensor may not work\n", + type); + ret = 1; + break; + } + + int3472->ngpios++; + ACPI_FREE(obj); + + if (ret < 0) + return dev_err_probe(int3472->dev, ret, err_msg); + + /* Tell acpi_dev_get_resources() to not make a copy of the resource */ + return 1; +} + +int int3472_discrete_parse_crs(struct int3472_discrete_device *int3472) +{ + LIST_HEAD(resource_list); + int ret; + + skl_int3472_log_sensor_module_name(int3472); + + ret = acpi_dev_get_resources(int3472->adev, &resource_list, + skl_int3472_handle_gpio_resources, + int3472); + if (ret < 0) + return ret; + + acpi_dev_free_resource_list(&resource_list); + + /* Register _DSM based clock (no-op if a GPIO clock was already registered) */ + ret = skl_int3472_register_dsm_clock(int3472); + if (ret < 0) + return ret; + + int3472->gpios.dev_id = int3472->sensor_name; + gpiod_add_lookup_table(&int3472->gpios); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(int3472_discrete_parse_crs, "INTEL_INT3472_DISCRETE"); + +void int3472_discrete_cleanup(struct int3472_discrete_device *int3472) +{ + gpiod_remove_lookup_table(&int3472->gpios); + + skl_int3472_unregister_clock(int3472); + skl_int3472_unregister_leds(int3472); + skl_int3472_unregister_regulator(int3472); +} +EXPORT_SYMBOL_NS_GPL(int3472_discrete_cleanup, "INTEL_INT3472_DISCRETE"); + +static void skl_int3472_discrete_remove(struct platform_device *pdev) +{ + int3472_discrete_cleanup(platform_get_drvdata(pdev)); +} + +static int skl_int3472_discrete_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + const struct int3472_discrete_quirks *quirks = NULL; + struct int3472_discrete_device *int3472; + const struct dmi_system_id *id; + struct int3472_cldb cldb; + int ret; + + if (!adev) + return -ENODEV; + + id = dmi_first_match(skl_int3472_discrete_quirks); + if (id) + quirks = id->driver_data; + + ret = skl_int3472_fill_cldb(adev, &cldb); + if (ret) { + dev_err(&pdev->dev, "Couldn't fill CLDB structure\n"); + return ret; + } + + if (cldb.control_logic_type != 1) { + dev_err(&pdev->dev, "Unsupported control logic type %u\n", + cldb.control_logic_type); + return -EINVAL; + } + + /* Max num GPIOs we've seen plus a terminator */ + int3472 = devm_kzalloc(&pdev->dev, struct_size(int3472, gpios.table, + INT3472_MAX_SENSOR_GPIOS + 1), GFP_KERNEL); + if (!int3472) + return -ENOMEM; + + int3472->adev = adev; + int3472->dev = &pdev->dev; + platform_set_drvdata(pdev, int3472); + int3472->clock.imgclk_index = cldb.clock_source; + + if (quirks) + int3472->quirks = *quirks; + + ret = skl_int3472_get_sensor_adev_and_name(&pdev->dev, &int3472->sensor, + &int3472->sensor_name); + if (ret) + return ret; + + /* + * Initialising this list means we can call gpiod_remove_lookup_table() + * in failure paths without issue. + */ + INIT_LIST_HEAD(&int3472->gpios.list); + + ret = int3472_discrete_parse_crs(int3472); + if (ret) { + skl_int3472_discrete_remove(pdev); + return ret; + } + + acpi_dev_clear_dependencies(adev); + return 0; +} + +static const struct acpi_device_id int3472_device_id[] = { + { "INT3472", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, int3472_device_id); + +static struct platform_driver int3472_discrete = { + .driver = { + .name = "int3472-discrete", + .acpi_match_table = int3472_device_id, + }, + .probe = skl_int3472_discrete_probe, + .remove = skl_int3472_discrete_remove, +}; +module_platform_driver(int3472_discrete); + +MODULE_DESCRIPTION("Intel SkyLake INT3472 ACPI Discrete Device Driver"); +MODULE_AUTHOR("Daniel Scally "); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("INTEL_INT3472"); diff --git a/drivers/platform/x86/intel/int3472/discrete_quirks.c b/drivers/platform/x86/intel/int3472/discrete_quirks.c new file mode 100644 index 000000000..552869ef9 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/discrete_quirks.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Hans de Goede */ + +#include +#include + +static const struct int3472_discrete_quirks lenovo_miix_510_quirks = { + .avdd_second_sensor = "i2c-OVTI2680:00", +}; + +const struct dmi_system_id skl_int3472_discrete_quirks[] = { + { + /* Lenovo Miix 510-12IKB */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "MIIX 510-12IKB"), + }, + .driver_data = (void *)&lenovo_miix_510_quirks, + }, + { } +}; diff --git a/drivers/platform/x86/intel/int3472/led.c b/drivers/platform/x86/intel/int3472/led.c new file mode 100644 index 000000000..9b2573cc3 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/led.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Hans de Goede */ + +#include +#include +#include +#include + +static int int3472_led_set(struct led_classdev *led_cdev, enum led_brightness brightness) +{ + struct int3472_led *led = container_of(led_cdev, struct int3472_led, classdev); + + gpiod_set_value_cansleep(led->gpio, brightness); + return 0; +} + +int skl_int3472_register_led(struct int3472_discrete_device *int3472, struct gpio_desc *gpio, + const char *con_id) +{ + struct int3472_led *led; + char *p; + int ret; + + if (int3472->n_leds >= INT3472_MAX_LEDS) + return -ENOSPC; + + led = &int3472->leds[int3472->n_leds]; + led->gpio = gpio; + + /* Generate the name, replacing the ':' in the ACPI devname with '_' */ + snprintf(led->name, sizeof(led->name), + "%s::%s_led", acpi_dev_name(int3472->sensor), con_id); + p = strchr(led->name, ':'); + if (p) + *p = '_'; + + led->classdev.name = led->name; + led->classdev.max_brightness = 1; + led->classdev.brightness_set_blocking = int3472_led_set; + + ret = led_classdev_register(int3472->dev, &led->classdev); + if (ret) + return ret; + + led->lookup.provider = led->name; + led->lookup.dev_id = int3472->sensor_name; + led->lookup.con_id = con_id; + led_add_lookup(&led->lookup); + + int3472->n_leds++; + return 0; +} + +void skl_int3472_unregister_leds(struct int3472_discrete_device *int3472) +{ + for (unsigned int i = 0; i < int3472->n_leds; i++) { + struct int3472_led *led = &int3472->leds[i]; + + led_remove_lookup(&led->lookup); + led_classdev_unregister(&led->classdev); + gpiod_put(led->gpio); + } +} diff --git a/drivers/platform/x86/intel/int3472/tps68470.c b/drivers/platform/x86/intel/int3472/tps68470.c new file mode 100644 index 000000000..a77ed32ab --- /dev/null +++ b/drivers/platform/x86/intel/int3472/tps68470.c @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Author: Dan Scally */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tps68470.h" + +#define DESIGNED_FOR_CHROMEOS 1 +#define DESIGNED_FOR_WINDOWS 2 + +#define TPS68470_WIN_MFD_CELL_COUNT 3 + +static const struct mfd_cell tps68470_cros[] = { + { .name = "tps68470-gpio" }, + { .name = "tps68470_pmic_opregion" }, +}; + +static const struct regmap_config tps68470_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = TPS68470_REG_MAX, +}; + +static int tps68470_chip_init(struct device *dev, struct regmap *regmap) +{ + unsigned int version; + int ret; + + /* Force software reset */ + ret = regmap_write(regmap, TPS68470_REG_RESET, TPS68470_REG_RESET_MASK); + if (ret) + return ret; + + ret = regmap_read(regmap, TPS68470_REG_REVID, &version); + if (ret) { + dev_err(dev, "Failed to read revision register: %d\n", ret); + return ret; + } + + dev_info(dev, "TPS68470 REVID: 0x%02x\n", version); + + return 0; +} + +/** skl_int3472_tps68470_calc_type: Check what platform a device is designed for + * @adev: A pointer to a &struct acpi_device + * + * Check CLDB buffer against the PMIC's adev. If present, then we check + * the value of control_logic_type field and follow one of the + * following scenarios: + * + * 1. No CLDB - likely ACPI tables designed for ChromeOS. We + * create platform devices for the GPIOs and OpRegion drivers. + * + * 2. CLDB, with control_logic_type = 2 - probably ACPI tables + * made for Windows 2-in-1 platforms. Register pdevs for GPIO, + * Clock and Regulator drivers to bind to. + * + * 3. Any other value in control_logic_type, we should never have + * gotten to this point; fail probe and return. + * + * Return: + * * 1 Device intended for ChromeOS + * * 2 Device intended for Windows + * * -EINVAL Where @adev has an object named CLDB but it does not conform to + * our expectations + */ +static int skl_int3472_tps68470_calc_type(struct acpi_device *adev) +{ + struct int3472_cldb cldb = { 0 }; + int ret; + + /* + * A CLDB buffer that exists, but which does not match our expectations + * should trigger an error so we don't blindly continue. + */ + ret = skl_int3472_fill_cldb(adev, &cldb); + if (ret && ret != -ENODEV) + return ret; + + if (ret) + return DESIGNED_FOR_CHROMEOS; + + if (cldb.control_logic_type != 2) + return -EINVAL; + + return DESIGNED_FOR_WINDOWS; +} + +/* + * Return the size of the flexible array member, because we'll need that later + * on to pass .pdata_size to cells. + */ +static int +skl_int3472_fill_clk_pdata(struct device *dev, struct tps68470_clk_platform_data **clk_pdata) +{ + struct acpi_device *adev = ACPI_COMPANION(dev); + struct acpi_device *consumer; + unsigned int n_consumers = 0; + const char *sensor_name; + unsigned int i = 0; + + for_each_acpi_consumer_dev(adev, consumer) + n_consumers++; + + if (!n_consumers) { + dev_err(dev, "INT3472 seems to have no dependents\n"); + return -ENODEV; + } + + *clk_pdata = devm_kzalloc(dev, struct_size(*clk_pdata, consumers, n_consumers), + GFP_KERNEL); + if (!*clk_pdata) + return -ENOMEM; + + (*clk_pdata)->n_consumers = n_consumers; + i = 0; + + for_each_acpi_consumer_dev(adev, consumer) { + sensor_name = devm_kasprintf(dev, GFP_KERNEL, I2C_DEV_NAME_FORMAT, + acpi_dev_name(consumer)); + if (!sensor_name) { + acpi_dev_put(consumer); + return -ENOMEM; + } + + (*clk_pdata)->consumers[i].consumer_dev_name = sensor_name; + i++; + } + + return n_consumers; +} + +static int skl_int3472_tps68470_probe(struct i2c_client *client) +{ + struct acpi_device *adev = ACPI_COMPANION(&client->dev); + const struct int3472_tps68470_board_data *board_data; + struct tps68470_clk_platform_data *clk_pdata; + struct mfd_cell *cells; + struct regmap *regmap; + int n_consumers; + int device_type; + int ret; + int i; + + if (!adev) + return -ENODEV; + + n_consumers = skl_int3472_fill_clk_pdata(&client->dev, &clk_pdata); + if (n_consumers < 0) + return n_consumers; + + regmap = devm_regmap_init_i2c(client, &tps68470_regmap_config); + if (IS_ERR(regmap)) { + dev_err(&client->dev, "Failed to create regmap: %ld\n", PTR_ERR(regmap)); + return PTR_ERR(regmap); + } + + i2c_set_clientdata(client, regmap); + + ret = tps68470_chip_init(&client->dev, regmap); + if (ret < 0) { + dev_err(&client->dev, "TPS68470 init error %d\n", ret); + return ret; + } + + device_type = skl_int3472_tps68470_calc_type(adev); + switch (device_type) { + case DESIGNED_FOR_WINDOWS: + board_data = int3472_tps68470_get_board_data(dev_name(&client->dev)); + if (!board_data) + return dev_err_probe(&client->dev, -ENODEV, "No board-data found for this model\n"); + + cells = kzalloc_objs(*cells, TPS68470_WIN_MFD_CELL_COUNT); + if (!cells) + return -ENOMEM; + + /* + * The order of the cells matters here! The clk must be first + * because the regulator depends on it. The gpios must be last, + * acpi_gpiochip_add() calls acpi_dev_clear_dependencies() and + * the clk + regulators must be ready when this happens. + */ + cells[0].name = "tps68470-clk"; + cells[0].platform_data = clk_pdata; + cells[0].pdata_size = struct_size(clk_pdata, consumers, n_consumers); + cells[1].name = "tps68470-regulator"; + cells[1].platform_data = (void *)board_data->tps68470_regulator_pdata; + cells[1].pdata_size = sizeof(struct tps68470_regulator_platform_data); + cells[2].name = "tps68470-gpio"; + cells[2].swnode = board_data->tps68470_gpio_swnode; + + for (i = 0; i < board_data->n_gpiod_lookups; i++) + gpiod_add_lookup_table(board_data->tps68470_gpio_lookup_tables[i]); + + ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE, + cells, TPS68470_WIN_MFD_CELL_COUNT, + NULL, 0, NULL); + kfree(cells); + + if (ret) { + for (i = 0; i < board_data->n_gpiod_lookups; i++) + gpiod_remove_lookup_table(board_data->tps68470_gpio_lookup_tables[i]); + } + + break; + case DESIGNED_FOR_CHROMEOS: + ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE, + tps68470_cros, ARRAY_SIZE(tps68470_cros), + NULL, 0, NULL); + break; + default: + dev_err(&client->dev, "Failed to add MFD devices\n"); + return device_type; + } + + /* + * No acpi_dev_clear_dependencies() here, since the acpi_gpiochip_add() + * for the GPIO cell already does this. + */ + + return ret; +} + +static void skl_int3472_tps68470_remove(struct i2c_client *client) +{ + const struct int3472_tps68470_board_data *board_data; + int i; + + board_data = int3472_tps68470_get_board_data(dev_name(&client->dev)); + if (board_data) { + for (i = 0; i < board_data->n_gpiod_lookups; i++) + gpiod_remove_lookup_table(board_data->tps68470_gpio_lookup_tables[i]); + } +} + +static const struct acpi_device_id int3472_device_id[] = { + { "INT3472", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, int3472_device_id); + +static struct i2c_driver int3472_tps68470 = { + .driver = { + .name = "int3472-tps68470", + .acpi_match_table = int3472_device_id, + }, + .probe = skl_int3472_tps68470_probe, + .remove = skl_int3472_tps68470_remove, +}; +module_i2c_driver(int3472_tps68470); + +MODULE_DESCRIPTION("Intel SkyLake INT3472 ACPI TPS68470 Device Driver"); +MODULE_AUTHOR("Daniel Scally "); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("INTEL_INT3472"); +MODULE_SOFTDEP("pre: clk-tps68470 tps68470-regulator"); diff --git a/drivers/platform/x86/intel/int3472/tps68470.h b/drivers/platform/x86/intel/int3472/tps68470.h new file mode 100644 index 000000000..3bbaade96 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/tps68470.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * TI TPS68470 PMIC platform data definition. + * + * Copyright (c) 2021 Red Hat Inc. + * + * Red Hat authors: + * Hans de Goede + */ + +#ifndef _INTEL_SKL_INT3472_TPS68470_H +#define _INTEL_SKL_INT3472_TPS68470_H + +struct gpiod_lookup_table; +struct tps68470_regulator_platform_data; + +struct int3472_tps68470_board_data { + const char *dev_name; + const struct tps68470_regulator_platform_data *tps68470_regulator_pdata; + const struct software_node *tps68470_gpio_swnode; + unsigned int n_gpiod_lookups; + struct gpiod_lookup_table *tps68470_gpio_lookup_tables[]; +}; + +const struct int3472_tps68470_board_data *int3472_tps68470_get_board_data(const char *dev_name); + +#endif diff --git a/drivers/platform/x86/intel/int3472/tps68470_board_data.c b/drivers/platform/x86/intel/int3472/tps68470_board_data.c new file mode 100644 index 000000000..c53542465 --- /dev/null +++ b/drivers/platform/x86/intel/int3472/tps68470_board_data.c @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * TI TPS68470 PMIC platform data definition. + * + * Copyright (c) 2021 Dan Scally + * Copyright (c) 2021 Red Hat Inc. + * + * Red Hat authors: + * Hans de Goede + */ + +#include +#include +#include +#include +#include +#include "tps68470.h" + +static struct regulator_consumer_supply int347a_core_consumer_supplies[] = { + REGULATOR_SUPPLY("dvdd", "i2c-INT347A:00"), +}; + +static struct regulator_consumer_supply int347a_ana_consumer_supplies[] = { + REGULATOR_SUPPLY("avdd", "i2c-INT347A:00"), +}; + +static struct regulator_consumer_supply int347a_vcm_consumer_supplies[] = { + REGULATOR_SUPPLY("vdd", "i2c-INT347A:00-VCM"), +}; + +static struct regulator_consumer_supply int347a_vsio_consumer_supplies[] = { + REGULATOR_SUPPLY("dovdd", "i2c-INT347A:00"), + REGULATOR_SUPPLY("vsio", "i2c-INT347A:00-VCM"), + REGULATOR_SUPPLY("vddd", "i2c-INT347E:00"), +}; + +static struct regulator_consumer_supply int347a_aux1_consumer_supplies[] = { + REGULATOR_SUPPLY("vdda", "i2c-INT347E:00"), +}; + +static struct regulator_consumer_supply int347a_aux2_consumer_supplies[] = { + REGULATOR_SUPPLY("vdddo", "i2c-INT347E:00"), +}; + +static const struct regulator_init_data surface_go_tps68470_core_reg_init_data = { + .constraints = { + .min_uV = 1200000, + .max_uV = 1200000, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_core_consumer_supplies), + .consumer_supplies = int347a_core_consumer_supplies, +}; + +static const struct regulator_init_data surface_go_tps68470_ana_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_ana_consumer_supplies), + .consumer_supplies = int347a_ana_consumer_supplies, +}; + +static const struct regulator_init_data surface_go_tps68470_vcm_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_vcm_consumer_supplies), + .consumer_supplies = int347a_vcm_consumer_supplies, +}; + +/* Ensure the always-on VIO regulator has the same voltage as VSIO */ +static const struct regulator_init_data surface_go_tps68470_vio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = true, + .always_on = true, + }, +}; + +static const struct regulator_init_data surface_go_tps68470_vsio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_vsio_consumer_supplies), + .consumer_supplies = int347a_vsio_consumer_supplies, +}; + +static const struct regulator_init_data surface_go_tps68470_aux1_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_aux1_consumer_supplies), + .consumer_supplies = int347a_aux1_consumer_supplies, +}; + +static const struct regulator_init_data surface_go_tps68470_aux2_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int347a_aux2_consumer_supplies), + .consumer_supplies = int347a_aux2_consumer_supplies, +}; + +static const struct tps68470_regulator_platform_data surface_go_tps68470_pdata = { + .reg_init_data = { + [TPS68470_CORE] = &surface_go_tps68470_core_reg_init_data, + [TPS68470_ANA] = &surface_go_tps68470_ana_reg_init_data, + [TPS68470_VCM] = &surface_go_tps68470_vcm_reg_init_data, + [TPS68470_VIO] = &surface_go_tps68470_vio_reg_init_data, + [TPS68470_VSIO] = &surface_go_tps68470_vsio_reg_init_data, + [TPS68470_AUX1] = &surface_go_tps68470_aux1_reg_init_data, + [TPS68470_AUX2] = &surface_go_tps68470_aux2_reg_init_data, + }, +}; + +/* Settings for Dell 7212 Tablet */ + +static struct regulator_consumer_supply int3479_vsio_consumer_supplies[] = { + REGULATOR_SUPPLY("avdd", "i2c-INT3479:00"), +}; + +static struct regulator_consumer_supply int3479_aux1_consumer_supplies[] = { + REGULATOR_SUPPLY("dvdd", "i2c-INT3479:00"), +}; + +static struct regulator_consumer_supply int3479_aux2_consumer_supplies[] = { + REGULATOR_SUPPLY("dovdd", "i2c-INT3479:00"), +}; + +static const struct regulator_init_data dell_7212_tps68470_core_reg_init_data = { + .constraints = { + .min_uV = 1200000, + .max_uV = 1200000, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static const struct regulator_init_data dell_7212_tps68470_ana_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static const struct regulator_init_data dell_7212_tps68470_vcm_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static const struct regulator_init_data dell_7212_tps68470_vio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static const struct regulator_init_data dell_7212_tps68470_vsio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int3479_vsio_consumer_supplies), + .consumer_supplies = int3479_vsio_consumer_supplies, +}; + +static const struct regulator_init_data dell_7212_tps68470_aux1_reg_init_data = { + .constraints = { + .min_uV = 1213200, + .max_uV = 1213200, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int3479_aux1_consumer_supplies), + .consumer_supplies = int3479_aux1_consumer_supplies, +}; + +static const struct regulator_init_data dell_7212_tps68470_aux2_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(int3479_aux2_consumer_supplies), + .consumer_supplies = int3479_aux2_consumer_supplies, +}; + +static const struct tps68470_regulator_platform_data dell_7212_tps68470_pdata = { + .reg_init_data = { + [TPS68470_CORE] = &dell_7212_tps68470_core_reg_init_data, + [TPS68470_ANA] = &dell_7212_tps68470_ana_reg_init_data, + [TPS68470_VCM] = &dell_7212_tps68470_vcm_reg_init_data, + [TPS68470_VIO] = &dell_7212_tps68470_vio_reg_init_data, + [TPS68470_VSIO] = &dell_7212_tps68470_vsio_reg_init_data, + [TPS68470_AUX1] = &dell_7212_tps68470_aux1_reg_init_data, + [TPS68470_AUX2] = &dell_7212_tps68470_aux2_reg_init_data, + }, +}; + +/* Settings for MSI Prestige 14 AI+ Evo C2VMG laptop. */ +static struct regulator_consumer_supply ovti5675_avdd_consumer_supplies[] = { + REGULATOR_SUPPLY("avdd", "i2c-OVTI5675:00"), +}; + +static struct regulator_consumer_supply ovti5675_dovdd_consumer_supplies[] = { + REGULATOR_SUPPLY("dovdd", "i2c-OVTI5675:00"), +}; + +static struct regulator_consumer_supply ovti5675_dvdd_consumer_supplies[] = { + REGULATOR_SUPPLY("dvdd", "i2c-OVTI5675:00"), +}; + +static const struct regulator_init_data msi_prestige_ai_evo_tps68470_core_reg_init_data = { + .constraints = { + .min_uV = 1200000, + .max_uV = 1200000, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti5675_dvdd_consumer_supplies), + .consumer_supplies = ovti5675_dvdd_consumer_supplies, +}; + +static const struct regulator_init_data msi_prestige_ai_evo_tps68470_ana_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti5675_avdd_consumer_supplies), + .consumer_supplies = ovti5675_avdd_consumer_supplies, +}; + +static const struct regulator_init_data msi_prestige_ai_evo_tps68470_vio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static const struct regulator_init_data msi_prestige_ai_evo_tps68470_vsio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = 1, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti5675_dovdd_consumer_supplies), + .consumer_supplies = ovti5675_dovdd_consumer_supplies, +}; + +static const struct tps68470_regulator_platform_data msi_prestige_ai_evo_tps68470_pdata = { + .reg_init_data = { + [TPS68470_CORE] = &msi_prestige_ai_evo_tps68470_core_reg_init_data, + [TPS68470_ANA] = &msi_prestige_ai_evo_tps68470_ana_reg_init_data, + [TPS68470_VIO] = &msi_prestige_ai_evo_tps68470_vio_reg_init_data, + [TPS68470_VSIO] = &msi_prestige_ai_evo_tps68470_vsio_reg_init_data, + }, +}; + +/* Settings for Intel NVL platform */ + +static struct regulator_consumer_supply ovti13b1_core_consumer_supplies[] = { + REGULATOR_SUPPLY("dvdd", "i2c-OVTI13B1:01"), +}; + +static struct regulator_consumer_supply ovti13b1_ana_consumer_supplies[] = { + REGULATOR_SUPPLY("avdd", "i2c-OVTI13B1:01"), +}; + +static struct regulator_consumer_supply ovti13b1_vcm_consumer_supplies[] = { + REGULATOR_SUPPLY("vcc", "i2c-OVTI13B1:01-VCM"), +}; + +static struct regulator_consumer_supply ovti13b1_vsio_consumer_supplies[] = { + REGULATOR_SUPPLY("dovdd", "i2c-OVTI13B1:01"), +}; + +static const struct regulator_init_data intel_nvl_tps68470_core_reg_init_data = { + .constraints = { + .min_uV = 1200000, + .max_uV = 1200000, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti13b1_core_consumer_supplies), + .consumer_supplies = ovti13b1_core_consumer_supplies, +}; + +static const struct regulator_init_data intel_nvl_tps68470_ana_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti13b1_ana_consumer_supplies), + .consumer_supplies = ovti13b1_ana_consumer_supplies, +}; +static const struct regulator_init_data intel_nvl_tps68470_vcm_reg_init_data = { + .constraints = { + .min_uV = 2815200, + .max_uV = 2815200, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti13b1_vcm_consumer_supplies), + .consumer_supplies = ovti13b1_vcm_consumer_supplies, +}; + +/* Ensure the always-on VIO regulator has the same voltage as VSIO */ +static const struct regulator_init_data intel_nvl_tps68470_vio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = true, + .always_on = true, + }, +}; +static const struct regulator_init_data intel_nvl_tps68470_vsio_reg_init_data = { + .constraints = { + .min_uV = 1800600, + .max_uV = 1800600, + .apply_uV = true, + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .num_consumer_supplies = ARRAY_SIZE(ovti13b1_vsio_consumer_supplies), + .consumer_supplies = ovti13b1_vsio_consumer_supplies, +}; + +static const struct tps68470_regulator_platform_data intel_nvl_tps68470_pdata = { + .reg_init_data = { + [TPS68470_CORE] = &intel_nvl_tps68470_core_reg_init_data, + [TPS68470_ANA] = &intel_nvl_tps68470_ana_reg_init_data, + [TPS68470_VCM] = &intel_nvl_tps68470_vcm_reg_init_data, + [TPS68470_VIO] = &intel_nvl_tps68470_vio_reg_init_data, + [TPS68470_VSIO] = &intel_nvl_tps68470_vsio_reg_init_data, + }, +}; + +static struct gpiod_lookup_table surface_go_int347a_gpios = { + .dev_id = "i2c-INT347A:00", + .table = { + GPIO_LOOKUP("tps68470-gpio", 9, "reset", GPIO_ACTIVE_LOW), + GPIO_LOOKUP("tps68470-gpio", 7, "powerdown", GPIO_ACTIVE_LOW), + { } + } +}; + +static struct gpiod_lookup_table surface_go_int347e_gpios = { + .dev_id = "i2c-INT347E:00", + .table = { + GPIO_LOOKUP("tps68470-gpio", 5, "enable", GPIO_ACTIVE_HIGH), + { } + } +}; + +static struct gpiod_lookup_table dell_7212_int3479_gpios = { + .dev_id = "i2c-INT3479:00", + .table = { + GPIO_LOOKUP("tps68470-gpio", 3, "reset", GPIO_ACTIVE_LOW), + GPIO_LOOKUP("tps68470-gpio", 4, "powerdown", GPIO_ACTIVE_LOW), + { } + } +}; + +static struct gpiod_lookup_table msi_prestige_ai_evo_ovti5675_gpios = { + .dev_id = "i2c-OVTI5675:00", + .table = { + GPIO_LOOKUP("tps68470-gpio", 9, "reset", GPIO_ACTIVE_LOW), + { } + } +}; + +static struct gpiod_lookup_table intel_nvl_tps68470_gpios = { + .dev_id = "i2c-OVTI13B1:01", + .table = { + GPIO_LOOKUP("tps68470-gpio", 9, "reset", GPIO_ACTIVE_LOW), + { } + } +}; + +static const struct property_entry int3472_tps68470_daisy_chain_gpio_props[] = { + PROPERTY_ENTRY_BOOL("daisy-chain-enable"), + { } +}; + +static const struct software_node int3472_tps68470_daisy_chain_gpio_swnode = { + .properties = int3472_tps68470_daisy_chain_gpio_props, +}; + +static const struct int3472_tps68470_board_data surface_go_tps68470_board_data = { + .dev_name = "i2c-INT3472:05", + .tps68470_regulator_pdata = &surface_go_tps68470_pdata, + .n_gpiod_lookups = 2, + .tps68470_gpio_lookup_tables = { + &surface_go_int347a_gpios, + &surface_go_int347e_gpios, + }, +}; + +static const struct int3472_tps68470_board_data surface_go3_tps68470_board_data = { + .dev_name = "i2c-INT3472:01", + .tps68470_regulator_pdata = &surface_go_tps68470_pdata, + .n_gpiod_lookups = 2, + .tps68470_gpio_lookup_tables = { + &surface_go_int347a_gpios, + &surface_go_int347e_gpios, + }, +}; + +static const struct int3472_tps68470_board_data dell_7212_tps68470_board_data = { + .dev_name = "i2c-INT3472:05", + .tps68470_regulator_pdata = &dell_7212_tps68470_pdata, + .n_gpiod_lookups = 1, + .tps68470_gpio_lookup_tables = { + &dell_7212_int3479_gpios, + }, +}; + +static const struct int3472_tps68470_board_data msi_prestige_ai_evo_tps68470_board_data = { + .dev_name = "i2c-INT3472:06", + .tps68470_regulator_pdata = &msi_prestige_ai_evo_tps68470_pdata, + .tps68470_gpio_swnode = &int3472_tps68470_daisy_chain_gpio_swnode, + .n_gpiod_lookups = 1, + .tps68470_gpio_lookup_tables = { + &msi_prestige_ai_evo_ovti5675_gpios, + }, +}; + +static const struct int3472_tps68470_board_data intel_nvl_tps68470_board_data = { + .dev_name = "i2c-INT3472:04", + .tps68470_regulator_pdata = &intel_nvl_tps68470_pdata, + .tps68470_gpio_swnode = &int3472_tps68470_daisy_chain_gpio_swnode, + .n_gpiod_lookups = 1, + .tps68470_gpio_lookup_tables = { + &intel_nvl_tps68470_gpios, + }, +}; + +static const struct dmi_system_id int3472_tps68470_board_data_table[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Go"), + }, + .driver_data = (void *)&surface_go_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Go 2"), + }, + .driver_data = (void *)&surface_go_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Go 3"), + }, + .driver_data = (void *)&surface_go3_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Latitude 7212 Rugged Extreme Tablet"), + }, + .driver_data = (void *)&dell_7212_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Prestige 13 AI+ Evo A2VMG"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "MS-13Q3"), + }, + .driver_data = (void *)&msi_prestige_ai_evo_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Prestige 14 AI+ Evo C2VMG"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "MS-14N3"), + }, + .driver_data = (void *)&msi_prestige_ai_evo_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Prestige 16 AI+ Evo B2VMG"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "MS-15A3"), + }, + .driver_data = (void *)&msi_prestige_ai_evo_tps68470_board_data, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Nova Lake Client Platform"), + }, + .driver_data = (void *)&intel_nvl_tps68470_board_data, + }, + { } +}; + +const struct int3472_tps68470_board_data *int3472_tps68470_get_board_data(const char *dev_name) +{ + const struct int3472_tps68470_board_data *board_data; + const struct dmi_system_id *match; + + for (match = dmi_first_match(int3472_tps68470_board_data_table); + match; + match = dmi_first_match(match + 1)) { + board_data = match->driver_data; + if (strcmp(board_data->dev_name, dev_name) == 0) + return board_data; + } + + return NULL; +} diff --git a/drivers/platform/x86/intel/ishtp_eclite.c b/drivers/platform/x86/intel/ishtp_eclite.c new file mode 100644 index 000000000..93ac8b2db --- /dev/null +++ b/drivers/platform/x86/intel/ishtp_eclite.c @@ -0,0 +1,703 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Intel ECLite opregion driver for talking to ECLite firmware running on + * Intel Integrated Sensor Hub (ISH) using ISH Transport Protocol (ISHTP) + * + * Copyright (c) 2021, Intel Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ECLITE_DATA_OPREGION_ID 0x9E +#define ECLITE_CMD_OPREGION_ID 0x9F + +#define ECL_MSG_DATA 0x1 +#define ECL_MSG_EVENT 0x2 + +#define ECL_ISH_READ 0x1 +#define ECL_ISH_WRITE 0x2 +#define ECL_ISH_HEADER_VERSION 0 + +#define ECL_CL_RX_RING_SIZE 16 +#define ECL_CL_TX_RING_SIZE 8 + +#define ECL_DATA_OPR_BUFLEN 384 +#define ECL_EVENTS_NOTIFY 333 + +#define cmd_opr_offsetof(element) offsetof(struct opregion_cmd, element) +#define cl_data_to_dev(opr_dev) ishtp_device((opr_dev)->cl_device) + +#ifndef BITS_TO_BYTES +#define BITS_TO_BYTES(x) ((x) / 8) +#endif + +struct opregion_cmd { + unsigned int command; + unsigned int offset; + unsigned int length; + unsigned int event_id; +}; + +struct opregion_data { + char data[ECL_DATA_OPR_BUFLEN]; +}; + +struct opregion_context { + struct opregion_cmd cmd_area; + struct opregion_data data_area; +}; + +struct ecl_message_header { + unsigned int version:2; + unsigned int data_type:2; + unsigned int request_type:2; + unsigned int offset:9; + unsigned int data_len:9; + unsigned int event:8; +}; + +struct ecl_message { + struct ecl_message_header header; + char payload[ECL_DATA_OPR_BUFLEN]; +}; + +struct ishtp_opregion_dev { + struct opregion_context opr_context; + struct ishtp_cl *ecl_ishtp_cl; + struct ishtp_cl_device *cl_device; + struct ishtp_fw_client *fw_client; + struct ishtp_cl_rb *rb; + struct acpi_device *adev; + unsigned int dsm_event_id; + unsigned int ish_link_ready; + unsigned int ish_read_done; + unsigned int acpi_init_done; + wait_queue_head_t read_wait; + struct work_struct event_work; + struct work_struct reset_work; + /* lock for opregion context */ + struct mutex lock; + +}; + +/* eclite ishtp client UUID: 6a19cc4b-d760-4de3-b14d-f25ebd0fbcd9 */ +static const struct ishtp_device_id ecl_ishtp_id_table[] = { + { .guid = GUID_INIT(0x6a19cc4b, 0xd760, 0x4de3, + 0xb1, 0x4d, 0xf2, 0x5e, 0xbd, 0xf, 0xbc, 0xd9), }, + { } +}; +MODULE_DEVICE_TABLE(ishtp, ecl_ishtp_id_table); + +/* ACPI DSM UUID: 91d936a7-1f01-49c6-a6b4-72f00ad8d8a5 */ +static const guid_t ecl_acpi_guid = + GUID_INIT(0x91d936a7, 0x1f01, 0x49c6, 0xa6, + 0xb4, 0x72, 0xf0, 0x0a, 0xd8, 0xd8, 0xa5); + +/** + * ecl_ish_cl_read() - Read data from eclite FW + * + * @opr_dev: pointer to opregion device + * + * This function issues a read request to eclite FW and waits until it + * receives a response. When response is received the read data is copied to + * opregion buffer. + */ +static int ecl_ish_cl_read(struct ishtp_opregion_dev *opr_dev) +{ + struct ecl_message_header header; + int len, rv; + + if (!opr_dev->ish_link_ready) + return -EIO; + + if ((opr_dev->opr_context.cmd_area.offset + + opr_dev->opr_context.cmd_area.length) > ECL_DATA_OPR_BUFLEN) { + return -EINVAL; + } + + header.version = ECL_ISH_HEADER_VERSION; + header.data_type = ECL_MSG_DATA; + header.request_type = ECL_ISH_READ; + header.offset = opr_dev->opr_context.cmd_area.offset; + header.data_len = opr_dev->opr_context.cmd_area.length; + header.event = opr_dev->opr_context.cmd_area.event_id; + len = sizeof(header); + + opr_dev->ish_read_done = false; + rv = ishtp_cl_send(opr_dev->ecl_ishtp_cl, (uint8_t *)&header, len); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "ish-read : send failed\n"); + return -EIO; + } + + dev_dbg(cl_data_to_dev(opr_dev), + "[ish_rd] Req: off : %x, len : %x\n", + header.offset, + header.data_len); + + rv = wait_event_interruptible_timeout(opr_dev->read_wait, + opr_dev->ish_read_done, + 2 * HZ); + if (!rv) { + dev_err(cl_data_to_dev(opr_dev), + "[ish_rd] No response from firmware\n"); + return -EIO; + } + + return 0; +} + +/** + * ecl_ish_cl_write() - This function writes data to eclite FW. + * + * @opr_dev: pointer to opregion device + * + * This function writes data to eclite FW. + */ +static int ecl_ish_cl_write(struct ishtp_opregion_dev *opr_dev) +{ + struct ecl_message message; + int len; + + if (!opr_dev->ish_link_ready) + return -EIO; + + if ((opr_dev->opr_context.cmd_area.offset + + opr_dev->opr_context.cmd_area.length) > ECL_DATA_OPR_BUFLEN) { + return -EINVAL; + } + + message.header.version = ECL_ISH_HEADER_VERSION; + message.header.data_type = ECL_MSG_DATA; + message.header.request_type = ECL_ISH_WRITE; + message.header.offset = opr_dev->opr_context.cmd_area.offset; + message.header.data_len = opr_dev->opr_context.cmd_area.length; + message.header.event = opr_dev->opr_context.cmd_area.event_id; + len = sizeof(struct ecl_message_header) + message.header.data_len; + + memcpy(message.payload, + opr_dev->opr_context.data_area.data + message.header.offset, + message.header.data_len); + + dev_dbg(cl_data_to_dev(opr_dev), + "[ish_wr] off : %x, len : %x\n", + message.header.offset, + message.header.data_len); + + return ishtp_cl_send(opr_dev->ecl_ishtp_cl, (uint8_t *)&message, len); +} + +static acpi_status +ecl_opregion_cmd_handler(u32 function, acpi_physical_address address, + u32 bits, u64 *value64, + void *handler_context, void *region_context) +{ + struct ishtp_opregion_dev *opr_dev; + struct opregion_cmd *cmd; + acpi_status status = AE_OK; + + if (!region_context || !value64) + return AE_BAD_PARAMETER; + + if (function == ACPI_READ) + return AE_ERROR; + + opr_dev = (struct ishtp_opregion_dev *)region_context; + + mutex_lock(&opr_dev->lock); + + cmd = &opr_dev->opr_context.cmd_area; + + switch (address) { + case cmd_opr_offsetof(command): + cmd->command = (u32)*value64; + + if (cmd->command == ECL_ISH_READ) + status = ecl_ish_cl_read(opr_dev); + else if (cmd->command == ECL_ISH_WRITE) + status = ecl_ish_cl_write(opr_dev); + else + status = AE_ERROR; + break; + case cmd_opr_offsetof(offset): + cmd->offset = (u32)*value64; + break; + case cmd_opr_offsetof(length): + cmd->length = (u32)*value64; + break; + case cmd_opr_offsetof(event_id): + cmd->event_id = (u32)*value64; + break; + default: + status = AE_ERROR; + } + + mutex_unlock(&opr_dev->lock); + + return status; +} + +static acpi_status +ecl_opregion_data_handler(u32 function, acpi_physical_address address, + u32 bits, u64 *value64, + void *handler_context, void *region_context) +{ + struct ishtp_opregion_dev *opr_dev; + unsigned int bytes = BITS_TO_BYTES(bits); + void *data_addr; + + if (!region_context || !value64) + return AE_BAD_PARAMETER; + + if (address + bytes > ECL_DATA_OPR_BUFLEN) + return AE_BAD_PARAMETER; + + opr_dev = (struct ishtp_opregion_dev *)region_context; + + mutex_lock(&opr_dev->lock); + + data_addr = &opr_dev->opr_context.data_area.data[address]; + + if (function == ACPI_READ) { + memcpy(value64, data_addr, bytes); + } else if (function == ACPI_WRITE) { + memcpy(data_addr, value64, bytes); + } else { + mutex_unlock(&opr_dev->lock); + return AE_BAD_PARAMETER; + } + + mutex_unlock(&opr_dev->lock); + + return AE_OK; +} + +static int acpi_find_eclite_device(struct ishtp_opregion_dev *opr_dev) +{ + struct acpi_device *adev; + + /* Find ECLite device and save reference */ + adev = acpi_dev_get_first_match_dev("INTC1035", NULL, -1); + if (!adev) { + dev_err(cl_data_to_dev(opr_dev), "eclite ACPI device not found\n"); + return -ENODEV; + } + + opr_dev->adev = adev; + + return 0; +} + +static int acpi_opregion_init(struct ishtp_opregion_dev *opr_dev) +{ + acpi_status status; + + status = acpi_install_address_space_handler(opr_dev->adev->handle, + ECLITE_CMD_OPREGION_ID, + ecl_opregion_cmd_handler, + NULL, opr_dev); + if (ACPI_FAILURE(status)) { + dev_err(cl_data_to_dev(opr_dev), + "cmd space handler install failed\n"); + return -ENODEV; + } + + status = acpi_install_address_space_handler(opr_dev->adev->handle, + ECLITE_DATA_OPREGION_ID, + ecl_opregion_data_handler, + NULL, opr_dev); + if (ACPI_FAILURE(status)) { + dev_err(cl_data_to_dev(opr_dev), + "data space handler install failed\n"); + + acpi_remove_address_space_handler(opr_dev->adev->handle, + ECLITE_CMD_OPREGION_ID, + ecl_opregion_cmd_handler); + return -ENODEV; + } + opr_dev->acpi_init_done = true; + + dev_dbg(cl_data_to_dev(opr_dev), "Opregion handlers are installed\n"); + + return 0; +} + +static void acpi_opregion_deinit(struct ishtp_opregion_dev *opr_dev) +{ + acpi_remove_address_space_handler(opr_dev->adev->handle, + ECLITE_CMD_OPREGION_ID, + ecl_opregion_cmd_handler); + + acpi_remove_address_space_handler(opr_dev->adev->handle, + ECLITE_DATA_OPREGION_ID, + ecl_opregion_data_handler); + opr_dev->acpi_init_done = false; +} + +static void ecl_acpi_invoke_dsm(struct work_struct *work) +{ + struct ishtp_opregion_dev *opr_dev; + union acpi_object *obj; + + opr_dev = container_of(work, struct ishtp_opregion_dev, event_work); + if (!opr_dev->acpi_init_done) + return; + + obj = acpi_evaluate_dsm(opr_dev->adev->handle, &ecl_acpi_guid, 0, + opr_dev->dsm_event_id, NULL); + if (!obj) { + dev_warn(cl_data_to_dev(opr_dev), "_DSM fn call failed\n"); + return; + } + + dev_dbg(cl_data_to_dev(opr_dev), "Exec DSM function code: %d success\n", + opr_dev->dsm_event_id); + + ACPI_FREE(obj); +} + +static void ecl_ish_process_rx_data(struct ishtp_opregion_dev *opr_dev) +{ + struct ecl_message *message = + (struct ecl_message *)opr_dev->rb->buffer.data; + + dev_dbg(cl_data_to_dev(opr_dev), + "[ish_rd] Resp: off : %x, len : %x\n", + message->header.offset, + message->header.data_len); + + if ((message->header.offset + message->header.data_len) > + ECL_DATA_OPR_BUFLEN) { + return; + } + + memcpy(opr_dev->opr_context.data_area.data + message->header.offset, + message->payload, message->header.data_len); + + opr_dev->ish_read_done = true; + wake_up_interruptible(&opr_dev->read_wait); +} + +static void ecl_ish_process_rx_event(struct ishtp_opregion_dev *opr_dev) +{ + struct ecl_message_header *header = + (struct ecl_message_header *)opr_dev->rb->buffer.data; + + dev_dbg(cl_data_to_dev(opr_dev), + "[ish_ev] Evt received: %8x\n", header->event); + + opr_dev->dsm_event_id = header->event; + schedule_work(&opr_dev->event_work); +} + +static int ecl_ish_cl_enable_events(struct ishtp_opregion_dev *opr_dev, + bool config_enable) +{ + struct ecl_message message; + int len; + + message.header.version = ECL_ISH_HEADER_VERSION; + message.header.data_type = ECL_MSG_DATA; + message.header.request_type = ECL_ISH_WRITE; + message.header.offset = ECL_EVENTS_NOTIFY; + message.header.data_len = 1; + message.payload[0] = config_enable; + + len = sizeof(struct ecl_message_header) + message.header.data_len; + + return ishtp_cl_send(opr_dev->ecl_ishtp_cl, (uint8_t *)&message, len); +} + +static void ecl_ishtp_cl_event_cb(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *ecl_ishtp_cl = ishtp_get_drvdata(cl_device); + struct ishtp_opregion_dev *opr_dev; + struct ecl_message_header *header; + struct ishtp_cl_rb *rb; + + opr_dev = ishtp_get_client_data(ecl_ishtp_cl); + while ((rb = ishtp_cl_rx_get_rb(opr_dev->ecl_ishtp_cl)) != NULL) { + opr_dev->rb = rb; + header = (struct ecl_message_header *)rb->buffer.data; + + if (header->data_type == ECL_MSG_DATA) + ecl_ish_process_rx_data(opr_dev); + else if (header->data_type == ECL_MSG_EVENT) + ecl_ish_process_rx_event(opr_dev); + else + /* Got an event with wrong data_type, ignore it */ + dev_err(cl_data_to_dev(opr_dev), + "[ish_cb] Received wrong data_type\n"); + + ishtp_cl_io_rb_recycle(rb); + } +} + +static int ecl_ishtp_cl_init(struct ishtp_cl *ecl_ishtp_cl) +{ + struct ishtp_opregion_dev *opr_dev = + ishtp_get_client_data(ecl_ishtp_cl); + struct ishtp_fw_client *fw_client; + struct ishtp_device *dev; + int rv; + + rv = ishtp_cl_link(ecl_ishtp_cl); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "ishtp_cl_link failed\n"); + return rv; + } + + dev = ishtp_get_ishtp_device(ecl_ishtp_cl); + + /* Connect to FW client */ + ishtp_set_tx_ring_size(ecl_ishtp_cl, ECL_CL_TX_RING_SIZE); + ishtp_set_rx_ring_size(ecl_ishtp_cl, ECL_CL_RX_RING_SIZE); + + fw_client = ishtp_fw_cl_get_client(dev, &ecl_ishtp_id_table[0].guid); + if (!fw_client) { + dev_err(cl_data_to_dev(opr_dev), "fw client not found\n"); + return -ENOENT; + } + + ishtp_cl_set_fw_client_id(ecl_ishtp_cl, + ishtp_get_fw_client_id(fw_client)); + + ishtp_set_connection_state(ecl_ishtp_cl, ISHTP_CL_CONNECTING); + + rv = ishtp_cl_connect(ecl_ishtp_cl); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "client connect failed\n"); + + ishtp_cl_unlink(ecl_ishtp_cl); + return rv; + } + + dev_dbg(cl_data_to_dev(opr_dev), "Host connected to fw client\n"); + + return 0; +} + +static void ecl_ishtp_cl_deinit(struct ishtp_cl *ecl_ishtp_cl) +{ + ishtp_cl_unlink(ecl_ishtp_cl); + ishtp_cl_flush_queues(ecl_ishtp_cl); + ishtp_cl_free(ecl_ishtp_cl); +} + +static void ecl_ishtp_cl_reset_handler(struct work_struct *work) +{ + struct ishtp_opregion_dev *opr_dev; + struct ishtp_cl_device *cl_device; + struct ishtp_cl *ecl_ishtp_cl; + int rv; + int retry; + + opr_dev = container_of(work, struct ishtp_opregion_dev, reset_work); + + opr_dev->ish_link_ready = false; + + cl_device = opr_dev->cl_device; + ecl_ishtp_cl = opr_dev->ecl_ishtp_cl; + + ecl_ishtp_cl_deinit(ecl_ishtp_cl); + + ecl_ishtp_cl = ishtp_cl_allocate(cl_device); + if (!ecl_ishtp_cl) + return; + + ishtp_set_drvdata(cl_device, ecl_ishtp_cl); + ishtp_set_client_data(ecl_ishtp_cl, opr_dev); + + opr_dev->ecl_ishtp_cl = ecl_ishtp_cl; + + for (retry = 0; retry < 3; ++retry) { + rv = ecl_ishtp_cl_init(ecl_ishtp_cl); + if (!rv) + break; + } + if (rv) { + ishtp_cl_free(ecl_ishtp_cl); + opr_dev->ecl_ishtp_cl = NULL; + dev_err(cl_data_to_dev(opr_dev), + "[ish_rst] Reset failed. Link not ready.\n"); + return; + } + + ishtp_register_event_cb(cl_device, ecl_ishtp_cl_event_cb); + dev_info(cl_data_to_dev(opr_dev), + "[ish_rst] Reset Success. Link ready.\n"); + + opr_dev->ish_link_ready = true; + + if (opr_dev->acpi_init_done) + return; + + rv = acpi_opregion_init(opr_dev); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), + "ACPI opregion init failed\n"); + } +} + +static int ecl_ishtp_cl_probe(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *ecl_ishtp_cl; + struct ishtp_opregion_dev *opr_dev; + int rv; + + opr_dev = devm_kzalloc(ishtp_device(cl_device), sizeof(*opr_dev), + GFP_KERNEL); + if (!opr_dev) + return -ENOMEM; + + ecl_ishtp_cl = ishtp_cl_allocate(cl_device); + if (!ecl_ishtp_cl) + return -ENOMEM; + + ishtp_set_drvdata(cl_device, ecl_ishtp_cl); + ishtp_set_client_data(ecl_ishtp_cl, opr_dev); + opr_dev->ecl_ishtp_cl = ecl_ishtp_cl; + opr_dev->cl_device = cl_device; + + init_waitqueue_head(&opr_dev->read_wait); + INIT_WORK(&opr_dev->event_work, ecl_acpi_invoke_dsm); + INIT_WORK(&opr_dev->reset_work, ecl_ishtp_cl_reset_handler); + + /* Initialize ish client device */ + rv = ecl_ishtp_cl_init(ecl_ishtp_cl); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "Client init failed\n"); + goto err_exit; + } + + dev_dbg(cl_data_to_dev(opr_dev), "eclite-ishtp client initialised\n"); + + opr_dev->ish_link_ready = true; + mutex_init(&opr_dev->lock); + + rv = acpi_find_eclite_device(opr_dev); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "ECLite ACPI ID not found\n"); + goto err_exit; + } + + /* Register a handler for eclite fw events */ + ishtp_register_event_cb(cl_device, ecl_ishtp_cl_event_cb); + + /* Now init opregion handlers */ + rv = acpi_opregion_init(opr_dev); + if (rv) { + dev_err(cl_data_to_dev(opr_dev), "ACPI opregion init failed\n"); + goto err_exit; + } + + /* Reprobe devices depending on ECLite - battery, fan, etc. */ + acpi_dev_clear_dependencies(opr_dev->adev); + + return 0; +err_exit: + ishtp_set_connection_state(ecl_ishtp_cl, ISHTP_CL_DISCONNECTING); + ishtp_cl_disconnect(ecl_ishtp_cl); + ecl_ishtp_cl_deinit(ecl_ishtp_cl); + + return rv; +} + +static void ecl_ishtp_cl_remove(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *ecl_ishtp_cl = ishtp_get_drvdata(cl_device); + struct ishtp_opregion_dev *opr_dev = + ishtp_get_client_data(ecl_ishtp_cl); + + if (opr_dev->acpi_init_done) + acpi_opregion_deinit(opr_dev); + + acpi_dev_put(opr_dev->adev); + + ishtp_set_connection_state(ecl_ishtp_cl, ISHTP_CL_DISCONNECTING); + ishtp_cl_disconnect(ecl_ishtp_cl); + ecl_ishtp_cl_deinit(ecl_ishtp_cl); + + cancel_work_sync(&opr_dev->reset_work); + cancel_work_sync(&opr_dev->event_work); +} + +static int ecl_ishtp_cl_reset(struct ishtp_cl_device *cl_device) +{ + struct ishtp_cl *ecl_ishtp_cl = ishtp_get_drvdata(cl_device); + struct ishtp_opregion_dev *opr_dev = + ishtp_get_client_data(ecl_ishtp_cl); + + schedule_work(&opr_dev->reset_work); + + return 0; +} + +static int ecl_ishtp_cl_suspend(struct device *device) +{ + struct ishtp_cl_device *cl_device = ishtp_dev_to_cl_device(device); + struct ishtp_cl *ecl_ishtp_cl = ishtp_get_drvdata(cl_device); + struct ishtp_opregion_dev *opr_dev = + ishtp_get_client_data(ecl_ishtp_cl); + + if (acpi_target_system_state() == ACPI_STATE_S0) + return 0; + + acpi_opregion_deinit(opr_dev); + ecl_ish_cl_enable_events(opr_dev, false); + + return 0; +} + +static int ecl_ishtp_cl_resume(struct device *device) +{ + /* A reset is expected to call after an Sx. At this point + * we are not sure if the link is up or not to restore anything, + * so do nothing in resume path + */ + return 0; +} + +static const struct dev_pm_ops ecl_ishtp_pm_ops = { + .suspend = ecl_ishtp_cl_suspend, + .resume = ecl_ishtp_cl_resume, +}; + +static struct ishtp_cl_driver ecl_ishtp_cl_driver = { + .name = "ishtp-eclite", + .id = ecl_ishtp_id_table, + .probe = ecl_ishtp_cl_probe, + .remove = ecl_ishtp_cl_remove, + .reset = ecl_ishtp_cl_reset, + .driver.pm = &ecl_ishtp_pm_ops, +}; + +static int __init ecl_ishtp_init(void) +{ + return ishtp_cl_driver_register(&ecl_ishtp_cl_driver, THIS_MODULE); +} + +static void __exit ecl_ishtp_exit(void) +{ + return ishtp_cl_driver_unregister(&ecl_ishtp_cl_driver); +} + +late_initcall(ecl_ishtp_init); +module_exit(ecl_ishtp_exit); + +MODULE_DESCRIPTION("ISH ISHTP eclite client opregion driver"); +MODULE_AUTHOR("K Naduvalath, Sumesh "); + +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/mrfld_pwrbtn.c b/drivers/platform/x86/intel/mrfld_pwrbtn.c new file mode 100644 index 000000000..6c43f801c --- /dev/null +++ b/drivers/platform/x86/intel/mrfld_pwrbtn.c @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Power-button driver for Basin Cove PMIC + * + * Copyright (c) 2019, Intel Corporation. + * Author: Andy Shevchenko + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BCOVE_PBSTATUS 0x27 +#define BCOVE_PBSTATUS_PBLVL BIT(4) /* 1 - release, 0 - press */ + +static irqreturn_t mrfld_pwrbtn_interrupt(int irq, void *dev_id) +{ + struct input_dev *input = dev_id; + struct device *dev = input->dev.parent; + struct regmap *regmap = dev_get_drvdata(dev); + unsigned int state; + int ret; + + ret = regmap_read(regmap, BCOVE_PBSTATUS, &state); + if (ret) + return IRQ_NONE; + + dev_dbg(dev, "PBSTATUS=0x%x\n", state); + input_report_key(input, KEY_POWER, !(state & BCOVE_PBSTATUS_PBLVL)); + input_sync(input); + + regmap_update_bits(regmap, BCOVE_MIRQLVL1, BCOVE_LVL1_PWRBTN, 0); + return IRQ_HANDLED; +} + +static int mrfld_pwrbtn_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct intel_soc_pmic *pmic = dev_get_drvdata(dev->parent); + struct input_dev *input; + int irq, ret; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + input = devm_input_allocate_device(dev); + if (!input) + return -ENOMEM; + input->name = pdev->name; + input->phys = "power-button/input0"; + input->id.bustype = BUS_HOST; + input->dev.parent = dev; + input_set_capability(input, EV_KEY, KEY_POWER); + ret = input_register_device(input); + if (ret) + return ret; + + dev_set_drvdata(dev, pmic->regmap); + + ret = devm_request_threaded_irq(dev, irq, NULL, mrfld_pwrbtn_interrupt, + IRQF_ONESHOT | IRQF_SHARED, pdev->name, + input); + if (ret) + return ret; + + regmap_update_bits(pmic->regmap, BCOVE_MIRQLVL1, BCOVE_LVL1_PWRBTN, 0); + regmap_update_bits(pmic->regmap, BCOVE_MPBIRQ, BCOVE_PBIRQ_PBTN, 0); + + device_init_wakeup(dev, true); + dev_pm_set_wake_irq(dev, irq); + return 0; +} + +static void mrfld_pwrbtn_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + + dev_pm_clear_wake_irq(dev); + device_init_wakeup(dev, false); +} + +static const struct platform_device_id mrfld_pwrbtn_id_table[] = { + { .name = "mrfld_bcove_pwrbtn" }, + {} +}; +MODULE_DEVICE_TABLE(platform, mrfld_pwrbtn_id_table); + +static struct platform_driver mrfld_pwrbtn_driver = { + .driver = { + .name = "mrfld_bcove_pwrbtn", + }, + .probe = mrfld_pwrbtn_probe, + .remove = mrfld_pwrbtn_remove, + .id_table = mrfld_pwrbtn_id_table, +}; +module_platform_driver(mrfld_pwrbtn_driver); + +MODULE_DESCRIPTION("Power-button driver for Basin Cove PMIC"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/oaktrail.c b/drivers/platform/x86/intel/oaktrail.c new file mode 100644 index 000000000..265cef327 --- /dev/null +++ b/drivers/platform/x86/intel/oaktrail.c @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Intel OakTrail Platform support + * + * Copyright (C) 2010-2011 Intel Corporation + * Author: Yin Kangkai (kangkai.yin@intel.com) + * + * based on Compal driver, Copyright (C) 2008 Cezary Jackiewicz + * , based on MSI driver + * Copyright (C) 2006 Lennart Poettering + * + * This driver does below things: + * 1. registers itself in the Linux backlight control in + * /sys/class/backlight/intel_oaktrail/ + * + * 2. registers in the rfkill subsystem here: /sys/class/rfkill/rfkillX/ + * for these components: wifi, bluetooth, wwan (3g), gps + * + * This driver might work on other products based on Oaktrail. If you + * want to try it you can pass force=1 as argument to the module which + * will force it to load even when the DMI data doesn't identify the + * product as compatible. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DRIVER_NAME "intel_oaktrail" +#define DRIVER_VERSION "0.4ac1" + +/* + * This is the devices status address in EC space, and the control bits + * definition: + * + * (1 << 0): Camera enable/disable, RW. + * (1 << 1): Bluetooth enable/disable, RW. + * (1 << 2): GPS enable/disable, RW. + * (1 << 3): WiFi enable/disable, RW. + * (1 << 4): WWAN (3G) enable/disable, RW. + * (1 << 5): Touchscreen enable/disable, Read Only. + */ +#define OT_EC_DEVICE_STATE_ADDRESS 0xD6 + +#define OT_EC_CAMERA_MASK (1 << 0) +#define OT_EC_BT_MASK (1 << 1) +#define OT_EC_GPS_MASK (1 << 2) +#define OT_EC_WIFI_MASK (1 << 3) +#define OT_EC_WWAN_MASK (1 << 4) +#define OT_EC_TS_MASK (1 << 5) + +/* + * This is the address in EC space and commands used to control LCD backlight: + * + * Two steps needed to change the LCD backlight: + * 1. write the backlight percentage into OT_EC_BL_BRIGHTNESS_ADDRESS; + * 2. write OT_EC_BL_CONTROL_ON_DATA into OT_EC_BL_CONTROL_ADDRESS. + * + * To read the LCD back light, just read out the value from + * OT_EC_BL_BRIGHTNESS_ADDRESS. + * + * LCD backlight brightness range: 0 - 100 (OT_EC_BL_BRIGHTNESS_MAX) + */ +#define OT_EC_BL_BRIGHTNESS_ADDRESS 0x44 +#define OT_EC_BL_BRIGHTNESS_MAX 100 +#define OT_EC_BL_CONTROL_ADDRESS 0x3A +#define OT_EC_BL_CONTROL_ON_DATA 0x1A + + +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +static struct platform_device *oaktrail_device; +static struct backlight_device *oaktrail_bl_device; +static struct rfkill *bt_rfkill; +static struct rfkill *gps_rfkill; +static struct rfkill *wifi_rfkill; +static struct rfkill *wwan_rfkill; + + +/* rfkill */ +static int oaktrail_rfkill_set(void *data, bool blocked) +{ + u8 value; + u8 result; + unsigned long radio = (unsigned long) data; + + ec_read(OT_EC_DEVICE_STATE_ADDRESS, &result); + + if (!blocked) + value = (u8) (result | radio); + else + value = (u8) (result & ~radio); + + ec_write(OT_EC_DEVICE_STATE_ADDRESS, value); + + return 0; +} + +static const struct rfkill_ops oaktrail_rfkill_ops = { + .set_block = oaktrail_rfkill_set, +}; + +static struct rfkill *oaktrail_rfkill_new(char *name, enum rfkill_type type, + unsigned long mask) +{ + struct rfkill *rfkill_dev; + u8 value; + int err; + + rfkill_dev = rfkill_alloc(name, &oaktrail_device->dev, type, + &oaktrail_rfkill_ops, (void *)mask); + if (!rfkill_dev) + return ERR_PTR(-ENOMEM); + + ec_read(OT_EC_DEVICE_STATE_ADDRESS, &value); + rfkill_init_sw_state(rfkill_dev, (value & mask) != 1); + + err = rfkill_register(rfkill_dev); + if (err) { + rfkill_destroy(rfkill_dev); + return ERR_PTR(err); + } + + return rfkill_dev; +} + +static inline void __oaktrail_rfkill_cleanup(struct rfkill *rf) +{ + if (rf) { + rfkill_unregister(rf); + rfkill_destroy(rf); + } +} + +static void oaktrail_rfkill_cleanup(void) +{ + __oaktrail_rfkill_cleanup(wifi_rfkill); + __oaktrail_rfkill_cleanup(bt_rfkill); + __oaktrail_rfkill_cleanup(gps_rfkill); + __oaktrail_rfkill_cleanup(wwan_rfkill); +} + +static int oaktrail_rfkill_init(void) +{ + int ret; + + wifi_rfkill = oaktrail_rfkill_new("oaktrail-wifi", + RFKILL_TYPE_WLAN, + OT_EC_WIFI_MASK); + if (IS_ERR(wifi_rfkill)) { + ret = PTR_ERR(wifi_rfkill); + wifi_rfkill = NULL; + goto cleanup; + } + + bt_rfkill = oaktrail_rfkill_new("oaktrail-bluetooth", + RFKILL_TYPE_BLUETOOTH, + OT_EC_BT_MASK); + if (IS_ERR(bt_rfkill)) { + ret = PTR_ERR(bt_rfkill); + bt_rfkill = NULL; + goto cleanup; + } + + gps_rfkill = oaktrail_rfkill_new("oaktrail-gps", + RFKILL_TYPE_GPS, + OT_EC_GPS_MASK); + if (IS_ERR(gps_rfkill)) { + ret = PTR_ERR(gps_rfkill); + gps_rfkill = NULL; + goto cleanup; + } + + wwan_rfkill = oaktrail_rfkill_new("oaktrail-wwan", + RFKILL_TYPE_WWAN, + OT_EC_WWAN_MASK); + if (IS_ERR(wwan_rfkill)) { + ret = PTR_ERR(wwan_rfkill); + wwan_rfkill = NULL; + goto cleanup; + } + + return 0; + +cleanup: + oaktrail_rfkill_cleanup(); + return ret; +} + + +/* backlight */ +static int get_backlight_brightness(struct backlight_device *b) +{ + u8 value; + ec_read(OT_EC_BL_BRIGHTNESS_ADDRESS, &value); + + return value; +} + +static int set_backlight_brightness(struct backlight_device *b) +{ + u8 percent = (u8) b->props.brightness; + if (percent < 0 || percent > OT_EC_BL_BRIGHTNESS_MAX) + return -EINVAL; + + ec_write(OT_EC_BL_BRIGHTNESS_ADDRESS, percent); + ec_write(OT_EC_BL_CONTROL_ADDRESS, OT_EC_BL_CONTROL_ON_DATA); + + return 0; +} + +static const struct backlight_ops oaktrail_bl_ops = { + .get_brightness = get_backlight_brightness, + .update_status = set_backlight_brightness, +}; + +static int oaktrail_backlight_init(void) +{ + struct backlight_device *bd; + struct backlight_properties props; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = OT_EC_BL_BRIGHTNESS_MAX; + bd = backlight_device_register(DRIVER_NAME, + &oaktrail_device->dev, NULL, + &oaktrail_bl_ops, + &props); + + if (IS_ERR(bd)) { + oaktrail_bl_device = NULL; + pr_warn("Unable to register backlight device\n"); + return PTR_ERR(bd); + } + + oaktrail_bl_device = bd; + + bd->props.brightness = get_backlight_brightness(bd); + bd->props.power = BACKLIGHT_POWER_ON; + backlight_update_status(bd); + + return 0; +} + +static void oaktrail_backlight_exit(void) +{ + backlight_device_unregister(oaktrail_bl_device); +} + +static int oaktrail_probe(struct platform_device *pdev) +{ + return 0; +} + +static struct platform_driver oaktrail_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = oaktrail_probe, +}; + +static int dmi_check_cb(const struct dmi_system_id *id) +{ + pr_info("Identified model '%s'\n", id->ident); + return 0; +} + +static const struct dmi_system_id oaktrail_dmi_table[] __initconst = { + { + .ident = "OakTrail platform", + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "OakTrail platform"), + }, + .callback = dmi_check_cb + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, oaktrail_dmi_table); + +static int __init oaktrail_init(void) +{ + int ret; + + if (acpi_disabled) { + pr_err("ACPI needs to be enabled for this driver to work!\n"); + return -ENODEV; + } + + if (!force && !dmi_check_system(oaktrail_dmi_table)) { + pr_err("Platform not recognized (You could try the module's force-parameter)"); + return -ENODEV; + } + + ret = platform_driver_register(&oaktrail_driver); + if (ret) { + pr_warn("Unable to register platform driver\n"); + goto err_driver_reg; + } + + oaktrail_device = platform_device_alloc(DRIVER_NAME, PLATFORM_DEVID_NONE); + if (!oaktrail_device) { + pr_warn("Unable to allocate platform device\n"); + ret = -ENOMEM; + goto err_device_alloc; + } + + ret = platform_device_add(oaktrail_device); + if (ret) { + pr_warn("Unable to add platform device\n"); + goto err_device_add; + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + ret = oaktrail_backlight_init(); + if (ret) + goto err_backlight; + } + + ret = oaktrail_rfkill_init(); + if (ret) { + pr_warn("Setup rfkill failed\n"); + goto err_rfkill; + } + + pr_info("Driver "DRIVER_VERSION" successfully loaded\n"); + return 0; + +err_rfkill: + oaktrail_backlight_exit(); +err_backlight: + platform_device_del(oaktrail_device); +err_device_add: + platform_device_put(oaktrail_device); +err_device_alloc: + platform_driver_unregister(&oaktrail_driver); +err_driver_reg: + + return ret; +} + +static void __exit oaktrail_cleanup(void) +{ + oaktrail_backlight_exit(); + oaktrail_rfkill_cleanup(); + platform_device_unregister(oaktrail_device); + platform_driver_unregister(&oaktrail_driver); + + pr_info("Driver unloaded\n"); +} + +module_init(oaktrail_init); +module_exit(oaktrail_cleanup); + +MODULE_AUTHOR("Yin Kangkai "); +MODULE_DESCRIPTION("Intel Oaktrail Platform ACPI Extras"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/plr_tpmi.c b/drivers/platform/x86/intel/plr_tpmi.c new file mode 100644 index 000000000..f98c241ed --- /dev/null +++ b/drivers/platform/x86/intel/plr_tpmi.c @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Performance Limit Reasons via TPMI + * + * Copyright (c) 2024, Intel Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tpmi_power_domains.h" + +#define PLR_HEADER 0x00 +#define PLR_MAILBOX_INTERFACE 0x08 +#define PLR_MAILBOX_DATA 0x10 +#define PLR_DIE_LEVEL 0x18 + +#define PLR_MODULE_ID_MASK GENMASK_ULL(19, 12) +#define PLR_RUN_BUSY BIT_ULL(63) + +#define PLR_COMMAND_WRITE 1 + +#define PLR_INVALID GENMASK_ULL(63, 0) + +#define PLR_TIMEOUT_US 5 +#define PLR_TIMEOUT_MAX_US 1000 + +#define PLR_COARSE_REASON_BITS 32 + +struct tpmi_plr; + +struct tpmi_plr_die { + void __iomem *base; + struct mutex lock; /* Protect access to PLR mailbox */ + int package_id; + int die_id; + struct tpmi_plr *plr; +}; + +struct tpmi_plr { + struct dentry *dbgfs_dir; + struct tpmi_plr_die *die_info; + int num_dies; + struct auxiliary_device *auxdev; + struct notifier_block nb; + struct mutex lock; /* Protect access to dbgfs_dir */ +}; + +static const char * const plr_coarse_reasons[] = { + "FREQUENCY", + "CURRENT", + "POWER", + "THERMAL", + "PLATFORM", + "MCP", + "RAS", + "MISC", + "QOS", + "DFC", +}; + +static const char * const plr_fine_reasons[] = { + "FREQUENCY_CDYN0", + "FREQUENCY_CDYN1", + "FREQUENCY_CDYN2", + "FREQUENCY_CDYN3", + "FREQUENCY_CDYN4", + "FREQUENCY_CDYN5", + "FREQUENCY_FCT", + "FREQUENCY_PCS_TRL", + "CURRENT_MTPMAX", + "POWER_FAST_RAPL", + "POWER_PKG_PL1_MSR_TPMI", + "POWER_PKG_PL1_MMIO", + "POWER_PKG_PL1_PCS", + "POWER_PKG_PL2_MSR_TPMI", + "POWER_PKG_PL2_MMIO", + "POWER_PKG_PL2_PCS", + "POWER_PLATFORM_PL1_MSR_TPMI", + "POWER_PLATFORM_PL1_MMIO", + "POWER_PLATFORM_PL1_PCS", + "POWER_PLATFORM_PL2_MSR_TPMI", + "POWER_PLATFORM_PL2_MMIO", + "POWER_PLATFORM_PL2_PCS", + "UNKNOWN(22)", + "THERMAL_PER_CORE", + "DFC_UFS", + "PLATFORM_PROCHOT", + "PLATFORM_HOT_VR", + "UNKNOWN(27)", + "UNKNOWN(28)", + "MISC_PCS_PSTATE", +}; + +static u64 plr_read(struct tpmi_plr_die *plr_die, int offset) +{ + return readq(plr_die->base + offset); +} + +static void plr_write(u64 val, struct tpmi_plr_die *plr_die, int offset) +{ + writeq(val, plr_die->base + offset); +} + +static int plr_read_cpu_status(struct tpmi_plr_die *plr_die, int cpu, + u64 *status) +{ + u64 regval; + int ret; + + lockdep_assert_held(&plr_die->lock); + + regval = FIELD_PREP(PLR_MODULE_ID_MASK, tpmi_get_punit_core_number(cpu)); + regval |= PLR_RUN_BUSY; + + plr_write(regval, plr_die, PLR_MAILBOX_INTERFACE); + + ret = readq_poll_timeout(plr_die->base + PLR_MAILBOX_INTERFACE, regval, + !(regval & PLR_RUN_BUSY), PLR_TIMEOUT_US, + PLR_TIMEOUT_MAX_US); + if (ret) + return ret; + + *status = plr_read(plr_die, PLR_MAILBOX_DATA); + + return 0; +} + +static int plr_clear_cpu_status(struct tpmi_plr_die *plr_die, int cpu) +{ + u64 regval; + + lockdep_assert_held(&plr_die->lock); + + regval = FIELD_PREP(PLR_MODULE_ID_MASK, tpmi_get_punit_core_number(cpu)); + regval |= PLR_RUN_BUSY | PLR_COMMAND_WRITE; + + plr_write(0, plr_die, PLR_MAILBOX_DATA); + + plr_write(regval, plr_die, PLR_MAILBOX_INTERFACE); + + return readq_poll_timeout(plr_die->base + PLR_MAILBOX_INTERFACE, regval, + !(regval & PLR_RUN_BUSY), PLR_TIMEOUT_US, + PLR_TIMEOUT_MAX_US); +} + +static void plr_print_bits(struct seq_file *s, u64 val, int bits) +{ + const unsigned long mask[] = { BITMAP_FROM_U64(val) }; + int bit, index; + + for_each_set_bit(bit, mask, bits) { + const char *str = NULL; + + if (bit < PLR_COARSE_REASON_BITS) { + if (bit < ARRAY_SIZE(plr_coarse_reasons)) + str = plr_coarse_reasons[bit]; + } else { + index = bit - PLR_COARSE_REASON_BITS; + if (index < ARRAY_SIZE(plr_fine_reasons)) + str = plr_fine_reasons[index]; + } + + if (str) + seq_printf(s, " %s", str); + else + seq_printf(s, " UNKNOWN(%d)", bit); + } + + if (!val) + seq_puts(s, " none"); + + seq_putc(s, '\n'); +} + +static int plr_status_show(struct seq_file *s, void *unused) +{ + struct tpmi_plr_die *plr_die = s->private; + int ret; + u64 val; + + val = plr_read(plr_die, PLR_DIE_LEVEL); + seq_puts(s, "cpus"); + plr_print_bits(s, val, 32); + + guard(mutex)(&plr_die->lock); + + for (int cpu = 0; cpu < nr_cpu_ids; cpu++) { + if (plr_die->die_id != tpmi_get_power_domain_id(cpu)) + continue; + + if (plr_die->package_id != topology_physical_package_id(cpu)) + continue; + + seq_printf(s, "cpu%d", cpu); + ret = plr_read_cpu_status(plr_die, cpu, &val); + if (ret) { + dev_err(&plr_die->plr->auxdev->dev, "Failed to read PLR for cpu %d, ret=%d\n", + cpu, ret); + return ret; + } + + plr_print_bits(s, val, 64); + } + + return 0; +} + +static ssize_t plr_status_write(struct file *filp, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = filp->private_data; + struct tpmi_plr_die *plr_die = s->private; + bool val; + int ret; + + ret = kstrtobool_from_user(ubuf, count, &val); + if (ret) + return ret; + + if (val != 0) + return -EINVAL; + + plr_write(0, plr_die, PLR_DIE_LEVEL); + + guard(mutex)(&plr_die->lock); + + for (int cpu = 0; cpu < nr_cpu_ids; cpu++) { + if (plr_die->die_id != tpmi_get_power_domain_id(cpu)) + continue; + + if (plr_die->package_id != topology_physical_package_id(cpu)) + continue; + + plr_clear_cpu_status(plr_die, cpu); + } + + return count; +} +DEFINE_SHOW_STORE_ATTRIBUTE(plr_status); + +static int intel_plr_notify(struct notifier_block *self, unsigned long action, void *data) +{ + struct tpmi_plr *plr = container_of(self, struct tpmi_plr, nb); + + if (action == TPMI_CORE_EXIT) { + guard(mutex)(&plr->lock); + plr->dbgfs_dir = NULL; + } + + return NOTIFY_DONE; +} + +static int intel_plr_register_notifier(struct notifier_block *nb) +{ + nb->notifier_call = intel_plr_notify; + nb->priority = 0; + return tpmi_register_notifier(nb); +} + +static void intel_plr_unregister_notifier(struct notifier_block *nb) +{ + tpmi_unregister_notifier(nb); +} + +static int intel_plr_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct oobmsm_plat_info *plat_info; + struct dentry *dentry; + int i, num_resources; + struct resource *res; + struct tpmi_plr *plr; + void __iomem *base; + char name[17]; + int err; + + plat_info = tpmi_get_platform_data(auxdev); + if (!plat_info) + return dev_err_probe(&auxdev->dev, -EINVAL, "No platform info\n"); + + dentry = tpmi_get_debugfs_dir(auxdev); + if (!dentry) + return dev_err_probe(&auxdev->dev, -ENODEV, "No TPMI debugfs directory.\n"); + + num_resources = tpmi_get_resource_count(auxdev); + if (!num_resources) + return -EINVAL; + + plr = devm_kzalloc(&auxdev->dev, sizeof(*plr), GFP_KERNEL); + if (!plr) + return -ENOMEM; + + err = devm_mutex_init(&auxdev->dev, &plr->lock); + if (err) + return err; + + intel_plr_register_notifier(&plr->nb); + + plr->die_info = devm_kcalloc(&auxdev->dev, num_resources, sizeof(*plr->die_info), + GFP_KERNEL); + if (!plr->die_info) { + err = -ENOMEM; + goto err_notify; + } + + plr->num_dies = num_resources; + plr->dbgfs_dir = debugfs_create_dir("plr", dentry); + plr->auxdev = auxdev; + + for (i = 0; i < num_resources; i++) { + res = tpmi_get_resource_at_index(auxdev, i); + if (!res) { + err = dev_err_probe(&auxdev->dev, -EINVAL, "No resource\n"); + goto err; + } + + base = devm_ioremap_resource(&auxdev->dev, res); + if (IS_ERR(base)) { + err = PTR_ERR(base); + goto err; + } + + plr->die_info[i].base = base; + plr->die_info[i].package_id = plat_info->package_id; + plr->die_info[i].die_id = i; + plr->die_info[i].plr = plr; + mutex_init(&plr->die_info[i].lock); + + if (plr_read(&plr->die_info[i], PLR_HEADER) == PLR_INVALID) + continue; + + snprintf(name, sizeof(name), "domain%d", i); + + dentry = debugfs_create_dir(name, plr->dbgfs_dir); + debugfs_create_file("status", 0644, dentry, &plr->die_info[i], + &plr_status_fops); + } + + auxiliary_set_drvdata(auxdev, plr); + + return 0; + +err: + debugfs_remove_recursive(plr->dbgfs_dir); +err_notify: + intel_plr_unregister_notifier(&plr->nb); + + return err; +} + +static void intel_plr_remove(struct auxiliary_device *auxdev) +{ + struct tpmi_plr *plr = auxiliary_get_drvdata(auxdev); + + intel_plr_unregister_notifier(&plr->nb); + + guard(mutex)(&plr->lock); + debugfs_remove_recursive(plr->dbgfs_dir); +} + +static const struct auxiliary_device_id intel_plr_id_table[] = { + { .name = "intel_vsec.tpmi-plr" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, intel_plr_id_table); + +static struct auxiliary_driver intel_plr_aux_driver = { + .id_table = intel_plr_id_table, + .remove = intel_plr_remove, + .probe = intel_plr_probe, +}; +module_auxiliary_driver(intel_plr_aux_driver); + +MODULE_IMPORT_NS("INTEL_TPMI"); +MODULE_IMPORT_NS("INTEL_TPMI_POWER_DOMAIN"); +MODULE_DESCRIPTION("Intel TPMI PLR Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/pmc/Kconfig b/drivers/platform/x86/intel/pmc/Kconfig new file mode 100644 index 000000000..561d46634 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/Kconfig @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Intel x86 Platform-Specific Drivers +# + +config INTEL_PMC_CORE + tristate "Intel PMC Core driver" + depends on PCI + depends on ACPI + depends on INTEL_PMT_TELEMETRY + select INTEL_PMC_SSRAM_TELEMETRY + select INTEL_PMC_PWRM_TELEMETRY + help + The Intel Platform Controller Hub for Intel Core SoCs provides access + to Power Management Controller registers via various interfaces. This + driver can utilize debugging capabilities and supported features as + exposed by the Power Management Controller. It also may perform some + tasks in the PMC in order to enable transition into the SLPS0 state. + It should be selected on all Intel platforms supported by the driver. + + Supported features: + - SLP_S0_RESIDENCY counter + - PCH IP Power Gating status + - LTR Ignore / LTR Show + - MPHY/PLL gating status (Sunrisepoint PCH only) + - SLPS0 Debug registers (Cannonlake/Icelake PCH) + - Low Power Mode registers (Tigerlake and beyond) + - PMC quirks as needed to enable SLPS0/S0ix + +config INTEL_PMC_SSRAM_TELEMETRY + tristate + help + This driver discovers PMC SSRAM telemetry regions through the PMC's + MMIO interface (PCI) or ACPI _DSD properties and registers them with + the Intel VSEC framework as Intel PMT telemetry devices. + + It probes the PMC SSRAM device, extracts DVSEC information from MMIO, + reads device IDs and base addresses for multiple PMCs (main, IOE, PCH), + and exposes the discovered telemetry through Intel PMT interfaces + (including sysfs). + + This option is selected by INTEL_PMC_CORE. + +config INTEL_PMC_PWRM_TELEMETRY + tristate + help + This driver discovers PMC PWRM telemetry regions described in ACPI + _DSD and registers them with the Intel VSEC framework as Intel PMT + telemetry devices. + + It validates the ACPI discovery data and publishes the discovered + regions so they can be accessed through the Intel PMT telemetry + interfaces (including sysfs). + + This option is selected by INTEL_PMC_CORE. diff --git a/drivers/platform/x86/intel/pmc/Makefile b/drivers/platform/x86/intel/pmc/Makefile new file mode 100644 index 000000000..5b595176f --- /dev/null +++ b/drivers/platform/x86/intel/pmc/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Intel x86 Platform-Specific Drivers +# + +intel_pmc_core-y := core.o spt.o cnp.o icl.o \ + tgl.o adl.o mtl.o arl.o \ + lnl.o ptl.o wcl.o nvl.o +obj-$(CONFIG_INTEL_PMC_CORE) += intel_pmc_core.o +intel_pmc_core_pltdrv-y := pltdrv.o +obj-$(CONFIG_INTEL_PMC_CORE) += intel_pmc_core_pltdrv.o + +# Intel PMC SSRAM driver +intel_pmc_ssram_telemetry-y += ssram_telemetry.o +obj-$(CONFIG_INTEL_PMC_SSRAM_TELEMETRY) += intel_pmc_ssram_telemetry.o +intel_pmc_pwrm_telemetry-y += pwrm_telemetry.o +obj-$(CONFIG_INTEL_PMC_PWRM_TELEMETRY) += intel_pmc_pwrm_telemetry.o diff --git a/drivers/platform/x86/intel/pmc/adl.c b/drivers/platform/x86/intel/pmc/adl.c new file mode 100644 index 000000000..9e7dfd6e3 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/adl.c @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Alder Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include "core.h" + +/* Alder Lake: PGD PFET Enable Ack Status Register(s) bitmap */ +static const struct pmc_bit_map adl_pfear_map[] = { + {"SPI/eSPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SPC", BIT(6)}, + {"GBE", BIT(7)}, + + {"SATA", BIT(0)}, + {"HDA_PGD0", BIT(1)}, + {"HDA_PGD1", BIT(2)}, + {"HDA_PGD2", BIT(3)}, + {"HDA_PGD3", BIT(4)}, + {"SPD", BIT(5)}, + {"LPSS", BIT(6)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"ITH", BIT(3)}, + + {"XDCI", BIT(1)}, + {"DCI", BIT(2)}, + {"CSE", BIT(3)}, + {"CSME_KVM", BIT(4)}, + {"CSME_PMT", BIT(5)}, + {"CSME_CLINK", BIT(6)}, + {"CSME_PTIO", BIT(7)}, + + {"CSME_USBR", BIT(0)}, + {"CSME_SUSRAM", BIT(1)}, + {"CSME_SMT1", BIT(2)}, + {"CSME_SMS2", BIT(4)}, + {"CSME_SMS1", BIT(5)}, + {"CSME_RTC", BIT(6)}, + {"CSME_PSF", BIT(7)}, + + {"CNVI", BIT(3)}, + {"HDA_PGD4", BIT(2)}, + {"HDA_PGD5", BIT(3)}, + {"HDA_PGD6", BIT(4)}, + {} +}; + +static const struct pmc_bit_map *ext_adl_pfear_map[] = { + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + adl_pfear_map, + NULL +}; + +static const struct pmc_bit_map adl_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", CNP_PMC_LTR_RESERVED}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map adl_clocksource_status_map[] = { + {"CLKPART1_OFF_STS", BIT(0)}, + {"CLKPART2_OFF_STS", BIT(1)}, + {"CLKPART3_OFF_STS", BIT(2)}, + {"CLKPART4_OFF_STS", BIT(3)}, + {"CLKPART5_OFF_STS", BIT(4)}, + {"CLKPART6_OFF_STS", BIT(5)}, + {"CLKPART7_OFF_STS", BIT(6)}, + {"CLKPART8_OFF_STS", BIT(7)}, + {"PCIE0PLL_OFF_STS", BIT(10)}, + {"PCIE1PLL_OFF_STS", BIT(11)}, + {"PCIE2PLL_OFF_STS", BIT(12)}, + {"PCIE3PLL_OFF_STS", BIT(13)}, + {"PCIE4PLL_OFF_STS", BIT(14)}, + {"PCIE5PLL_OFF_STS", BIT(15)}, + {"PCIE6PLL_OFF_STS", BIT(16)}, + {"USB2PLL_OFF_STS", BIT(18)}, + {"OCPLL_OFF_STS", BIT(22)}, + {"AUDIOPLL_OFF_STS", BIT(23)}, + {"GBEPLL_OFF_STS", BIT(24)}, + {"Fast_XTAL_Osc_OFF_STS", BIT(25)}, + {"AC_Ring_Osc_OFF_STS", BIT(26)}, + {"MC_Ring_Osc_OFF_STS", BIT(27)}, + {"SATAPLL_OFF_STS", BIT(29)}, + {"USB3PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map adl_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0)}, + {"DMI_PGD0_PG_STS", BIT(1)}, + {"ESPISPI_PGD0_PG_STS", BIT(2)}, + {"XHCI_PGD0_PG_STS", BIT(3)}, + {"SPA_PGD0_PG_STS", BIT(4)}, + {"SPB_PGD0_PG_STS", BIT(5)}, + {"SPC_PGD0_PG_STS", BIT(6)}, + {"GBE_PGD0_PG_STS", BIT(7)}, + {"SATA_PGD0_PG_STS", BIT(8)}, + {"DSP_PGD0_PG_STS", BIT(9)}, + {"DSP_PGD1_PG_STS", BIT(10)}, + {"DSP_PGD2_PG_STS", BIT(11)}, + {"DSP_PGD3_PG_STS", BIT(12)}, + {"SPD_PGD0_PG_STS", BIT(13)}, + {"LPSS_PGD0_PG_STS", BIT(14)}, + {"SMB_PGD0_PG_STS", BIT(16)}, + {"ISH_PGD0_PG_STS", BIT(17)}, + {"NPK_PGD0_PG_STS", BIT(19)}, + {"PECI_PGD0_PG_STS", BIT(21)}, + {"XDCI_PGD0_PG_STS", BIT(25)}, + {"EXI_PGD0_PG_STS", BIT(26)}, + {"CSE_PGD0_PG_STS", BIT(27)}, + {"KVMCC_PGD0_PG_STS", BIT(28)}, + {"PMT_PGD0_PG_STS", BIT(29)}, + {"CLINK_PGD0_PG_STS", BIT(30)}, + {"PTIO_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map adl_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0)}, + {"SMT1_PGD0_PG_STS", BIT(2)}, + {"CSMERTC_PGD0_PG_STS", BIT(6)}, + {"CSMEPSF_PGD0_PG_STS", BIT(7)}, + {"CNVI_PGD0_PG_STS", BIT(19)}, + {"DSP_PGD4_PG_STS", BIT(26)}, + {"SPG_PGD0_PG_STS", BIT(27)}, + {"SPE_PGD0_PG_STS", BIT(28)}, + {} +}; + +static const struct pmc_bit_map adl_power_gating_status_2_map[] = { + {"THC0_PGD0_PG_STS", BIT(7)}, + {"THC1_PGD0_PG_STS", BIT(8)}, + {"SPF_PGD0_PG_STS", BIT(14)}, + {} +}; + +static const struct pmc_bit_map adl_d3_status_0_map[] = { + {"ISH_D3_STS", BIT(2)}, + {"LPSS_D3_STS", BIT(3)}, + {"XDCI_D3_STS", BIT(4)}, + {"XHCI_D3_STS", BIT(5)}, + {"SPA_D3_STS", BIT(12)}, + {"SPB_D3_STS", BIT(13)}, + {"SPC_D3_STS", BIT(14)}, + {"SPD_D3_STS", BIT(15)}, + {"SPE_D3_STS", BIT(16)}, + {"DSP_D3_STS", BIT(19)}, + {"SATA_D3_STS", BIT(20)}, + {"DMI_D3_STS", BIT(22)}, + {} +}; + +static const struct pmc_bit_map adl_d3_status_1_map[] = { + {"GBE_D3_STS", BIT(19)}, + {"CNVI_D3_STS", BIT(27)}, + {} +}; + +static const struct pmc_bit_map adl_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1)}, + {"CSE_D3_STS", BIT(4)}, + {"KVMCC_D3_STS", BIT(5)}, + {"USBR0_D3_STS", BIT(6)}, + {"SMT1_D3_STS", BIT(8)}, + {"PTIO_D3_STS", BIT(16)}, + {"PMT_D3_STS", BIT(17)}, + {} +}; + +static const struct pmc_bit_map adl_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14)}, + {"THC1_D3_STS", BIT(15)}, + {} +}; + +static const struct pmc_bit_map adl_vnn_req_status_0_map[] = { + {"ISH_VNN_REQ_STS", BIT(2)}, + {"ESPISPI_VNN_REQ_STS", BIT(18)}, + {"DSP_VNN_REQ_STS", BIT(19)}, + {} +}; + +static const struct pmc_bit_map adl_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4)}, + {"EXI_VNN_REQ_STS", BIT(9)}, + {"GBE_VNN_REQ_STS", BIT(19)}, + {"SMB_VNN_REQ_STS", BIT(25)}, + {"CNVI_VNN_REQ_STS", BIT(27)}, + {} +}; + +static const struct pmc_bit_map adl_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1)}, + {"CSE_VNN_REQ_STS", BIT(4)}, + {"SMT1_VNN_REQ_STS", BIT(8)}, + {"CLINK_VNN_REQ_STS", BIT(14)}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20)}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21)}, + {"GPIOCOM2_VNN_REQ_STS", BIT(22)}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23)}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24)}, + {} +}; + +static const struct pmc_bit_map adl_vnn_req_status_3_map[] = { + {"GPIOCOM5_VNN_REQ_STS", BIT(11)}, + {} +}; + +static const struct pmc_bit_map adl_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0)}, + {"PCIe_LPM_En_REQ_STS", BIT(3)}, + {"ITH_REQ_STS", BIT(5)}, + {"CNVI_REQ_STS", BIT(6)}, + {"ISH_REQ_STS", BIT(7)}, + {"USB2_SUS_PG_Sys_REQ_STS", BIT(10)}, + {"PCIe_Clk_REQ_STS", BIT(12)}, + {"MPHY_Core_DL_REQ_STS", BIT(16)}, + {"Break-even_En_REQ_STS", BIT(17)}, + {"MPHY_SUS_REQ_STS", BIT(22)}, + {"xDCI_attached_REQ_STS", BIT(24)}, + {} +}; + +static const struct pmc_bit_map *adl_lpm_maps[] = { + adl_clocksource_status_map, + adl_power_gating_status_0_map, + adl_power_gating_status_1_map, + adl_power_gating_status_2_map, + adl_d3_status_0_map, + adl_d3_status_1_map, + adl_d3_status_2_map, + adl_d3_status_3_map, + adl_vnn_req_status_0_map, + adl_vnn_req_status_1_map, + adl_vnn_req_status_2_map, + adl_vnn_req_status_3_map, + adl_vnn_misc_status_map, + tgl_signal_status_map, + NULL +}; + +const struct pmc_reg_map adl_reg_map = { + .pfear_sts = ext_adl_pfear_map, + .slp_s0_offset = ADL_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = adl_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = CNP_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = CNP_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = ADL_NUM_IP_IGN_ALLOWED, + .lpm_num_modes = ADL_LPM_NUM_MODES, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = ADL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = ADL_LPM_PRI_OFFSET, + .lpm_en_offset = ADL_LPM_EN_OFFSET, + .lpm_residency_offset = ADL_LPM_RESIDENCY_OFFSET, + .lpm_sts = adl_lpm_maps, + .lpm_status_offset = ADL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = ADL_LPM_LIVE_STATUS_OFFSET, + .pson_residency_offset = TGL_PSON_RESIDENCY_OFFSET, + .pson_residency_counter_step = TGL_PSON_RES_COUNTER_STEP, +}; + +struct pmc_dev_info adl_pmc_dev = { + .map = &adl_reg_map, + .suspend = cnl_suspend, + .resume = cnl_resume, +}; diff --git a/drivers/platform/x86/intel/pmc/arl.c b/drivers/platform/x86/intel/pmc/arl.c new file mode 100644 index 000000000..11609d593 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/arl.c @@ -0,0 +1,754 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Arrow Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include +#include "core.h" + +/* PMC SSRAM PMT Telemetry GUID */ +#define IOEP_LPM_REQ_GUID 0x5077612 +#define SOCS_LPM_REQ_GUID 0x8478657 +#define PCHS_LPM_REQ_GUID 0x9684572 +#define SOCM_LPM_REQ_GUID 0x2625030 + +static const u8 ARL_LPM_REG_INDEX[] = {0, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20}; + +static const struct pmc_bit_map arl_socs_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"Reserved", ARL_SOCS_PMC_LTR_RESERVED}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map arl_socs_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0)}, + {"AON3_OFF_STS", BIT(1)}, + {"AON4_OFF_STS", BIT(2)}, + {"AON5_OFF_STS", BIT(3)}, + {"AON1_OFF_STS", BIT(4)}, + {"XTAL_LVM_OFF_STS", BIT(5)}, + {"AON3_SPL_OFF_STS", BIT(9)}, + {"DMI3FPW_0_PLL_OFF_STS", BIT(10)}, + {"DMI3FPW_1_PLL_OFF_STS", BIT(11)}, + {"G5X16FPW_0_PLL_OFF_STS", BIT(14)}, + {"G5X16FPW_1_PLL_OFF_STS", BIT(15)}, + {"G5X16FPW_2_PLL_OFF_STS", BIT(16)}, + {"XTAL_AGGR_OFF_STS", BIT(17)}, + {"USB2_PLL_OFF_STS", BIT(18)}, + {"G5X16FPW_3_PLL_OFF_STS", BIT(19)}, + {"BCLK_EXT_INJ_CLK_OFF_STS", BIT(20)}, + {"PHY_OC_EXT_INJ_CLK_OFF_STS", BIT(21)}, + {"FILTER_PLL_OFF_STS", BIT(22)}, + {"FABRIC_PLL_OFF_STS", BIT(25)}, + {"SOC_PLL_OFF_STS", BIT(26)}, + {"PCIEFAB_PLL_OFF_STS", BIT(27)}, + {"REF_PLL_OFF_STS", BIT(28)}, + {"GENLOCK_FILTER_PLL_OFF_STS", BIT(30)}, + {"RTC_PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_socs_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0)}, + {"DMI_PGD0_PG_STS", BIT(1)}, + {"ESPISPI_PGD0_PG_STS", BIT(2)}, + {"XHCI_PGD0_PG_STS", BIT(3)}, + {"SPA_PGD0_PG_STS", BIT(4)}, + {"SPB_PGD0_PG_STS", BIT(5)}, + {"SPC_PGD0_PG_STS", BIT(6)}, + {"GBE_PGD0_PG_STS", BIT(7)}, + {"SATA_PGD0_PG_STS", BIT(8)}, + {"FIACPCB_P5x16_PGD0_PG_STS", BIT(9)}, + {"G5x16FPW_PGD0_PG_STS", BIT(10)}, + {"FIA_D_PGD0_PG_STS", BIT(11)}, + {"MPFPW2_PGD0_PG_STS", BIT(12)}, + {"SPD_PGD0_PG_STS", BIT(13)}, + {"LPSS_PGD0_PG_STS", BIT(14)}, + {"LPC_PGD0_PG_STS", BIT(15)}, + {"SMB_PGD0_PG_STS", BIT(16)}, + {"ISH_PGD0_PG_STS", BIT(17)}, + {"P2S_PGD0_PG_STS", BIT(18)}, + {"NPK_PGD0_PG_STS", BIT(19)}, + {"DMI3FPW_PGD0_PG_STS", BIT(20)}, + {"GBETSN1_PGD0_PG_STS", BIT(21)}, + {"FUSE_PGD0_PG_STS", BIT(22)}, + {"FIACPCB_D_PGD0_PG_STS", BIT(23)}, + {"FUSEGPSB_PGD0_PG_STS", BIT(24)}, + {"XDCI_PGD0_PG_STS", BIT(25)}, + {"EXI_PGD0_PG_STS", BIT(26)}, + {"CSE_PGD0_PG_STS", BIT(27)}, + {"KVMCC_PGD0_PG_STS", BIT(28)}, + {"PMT_PGD0_PG_STS", BIT(29)}, + {"CLINK_PGD0_PG_STS", BIT(30)}, + {"PTIO_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_socs_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0)}, + {"SUSRAM_PGD0_PG_STS", BIT(1)}, + {"SMT1_PGD0_PG_STS", BIT(2)}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3)}, + {"SMS2_PGD0_PG_STS", BIT(4)}, + {"SMS1_PGD0_PG_STS", BIT(5)}, + {"CSMERTC_PGD0_PG_STS", BIT(6)}, + {"CSMEPSF_PGD0_PG_STS", BIT(7)}, + {"SBR0_PGD0_PG_STS", BIT(8)}, + {"SBR1_PGD0_PG_STS", BIT(9)}, + {"SBR2_PGD0_PG_STS", BIT(10)}, + {"SBR3_PGD0_PG_STS", BIT(11)}, + {"MPFPW1_PGD0_PG_STS", BIT(12)}, + {"SBR5_PGD0_PG_STS", BIT(13)}, + {"FIA_X_PGD0_PG_STS", BIT(14)}, + {"FIACPCB_X_PGD0_PG_STS", BIT(15)}, + {"SBRG_PGD0_PG_STS", BIT(16)}, + {"SOC_D2D_PGD1_PG_STS", BIT(17)}, + {"PSF4_PGD0_PG_STS", BIT(18)}, + {"CNVI_PGD0_PG_STS", BIT(19)}, + {"UFSX2_PGD0_PG_STS", BIT(20)}, + {"ENDBG_PGD0_PG_STS", BIT(21)}, + {"DBG_PSF_PGD0_PG_STS", BIT(22)}, + {"SBR6_PGD0_PG_STS", BIT(23)}, + {"SOC_D2D_PGD2_PG_STS", BIT(24)}, + {"NPK_PGD1_PG_STS", BIT(25)}, + {"DMI3_PGD0_PG_STS", BIT(26)}, + {"DBG_SBR_PGD0_PG_STS", BIT(27)}, + {"SOC_D2D_PGD0_PG_STS", BIT(28)}, + {"PSF6_PGD0_PG_STS", BIT(29)}, + {"PSF7_PGD0_PG_STS", BIT(30)}, + {"MPFPW3_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_socs_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0)}, + {"FIA_PGD0_PG_STS", BIT(1)}, + {"SOC_D2D_PGD3_PG_STS", BIT(2)}, + {"FIA_U_PGD0_PG_STS", BIT(3)}, + {"TAM_PGD0_PG_STS", BIT(4)}, + {"GBETSN_PGD0_PG_STS", BIT(5)}, + {"TBTLSX_PGD0_PG_STS", BIT(6)}, + {"THC0_PGD0_PG_STS", BIT(7)}, + {"THC1_PGD0_PG_STS", BIT(8)}, + {"PMC_PGD1_PG_STS", BIT(9)}, + {"FIA_P5x16_PGD0_PG_STS", BIT(10)}, + {"GNA_PGD0_PG_STS", BIT(11)}, + {"ACE_PGD0_PG_STS", BIT(12)}, + {"ACE_PGD1_PG_STS", BIT(13)}, + {"ACE_PGD2_PG_STS", BIT(14)}, + {"ACE_PGD3_PG_STS", BIT(15)}, + {"ACE_PGD4_PG_STS", BIT(16)}, + {"ACE_PGD5_PG_STS", BIT(17)}, + {"ACE_PGD6_PG_STS", BIT(18)}, + {"ACE_PGD7_PG_STS", BIT(19)}, + {"ACE_PGD8_PG_STS", BIT(20)}, + {"FIA_PGS_PGD0_PG_STS", BIT(21)}, + {"FIACPCB_PGS_PGD0_PG_STS", BIT(22)}, + {"FUSEPMSB_PGD0_PG_STS", BIT(23)}, + {} +}; + +static const struct pmc_bit_map arl_socs_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1)}, + {"SUSRAM_D3_STS", BIT(2)}, + {"CSE_D3_STS", BIT(4)}, + {"KVMCC_D3_STS", BIT(5)}, + {"USBR0_D3_STS", BIT(6)}, + {"ISH_D3_STS", BIT(7)}, + {"SMT1_D3_STS", BIT(8)}, + {"SMT2_D3_STS", BIT(9)}, + {"SMT3_D3_STS", BIT(10)}, + {"GNA_D3_STS", BIT(12)}, + {"CLINK_D3_STS", BIT(14)}, + {"PTIO_D3_STS", BIT(16)}, + {"PMT_D3_STS", BIT(17)}, + {"SMS1_D3_STS", BIT(18)}, + {"SMS2_D3_STS", BIT(19)}, + {} +}; + +static const struct pmc_bit_map arl_socs_d3_status_3_map[] = { + {"GBETSN_D3_STS", BIT(13)}, + {"THC0_D3_STS", BIT(14)}, + {"THC1_D3_STS", BIT(15)}, + {"ACE_D3_STS", BIT(23)}, + {} +}; + +static const struct pmc_bit_map arl_socs_vnn_req_status_3_map[] = { + {"DTS0_VNN_REQ_STS", BIT(7)}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11)}, + {} +}; + +static const struct pmc_bit_map *arl_socs_lpm_maps[] = { + arl_socs_clocksource_status_map, + arl_socs_power_gating_status_0_map, + arl_socs_power_gating_status_1_map, + arl_socs_power_gating_status_2_map, + mtl_socm_d3_status_0_map, + mtl_socm_d3_status_1_map, + arl_socs_d3_status_2_map, + arl_socs_d3_status_3_map, + mtl_socm_vnn_req_status_0_map, + mtl_socm_vnn_req_status_1_map, + mtl_socm_vnn_req_status_2_map, + arl_socs_vnn_req_status_3_map, + mtl_socm_vnn_misc_status_map, + mtl_socm_signal_status_map, + NULL +}; + +static const struct pmc_bit_map arl_socs_pfear_map[] = { + {"RSVD64", BIT(0)}, + {"RSVD65", BIT(1)}, + {"RSVD66", BIT(2)}, + {"RSVD67", BIT(3)}, + {"RSVD68", BIT(4)}, + {"GBETSN", BIT(5)}, + {"TBTLSX", BIT(6)}, + {} +}; + +static const struct pmc_bit_map *ext_arl_socs_pfear_map[] = { + mtl_socm_pfear_map, + arl_socs_pfear_map, + NULL +}; + +static const struct pmc_reg_map arl_socs_reg_map = { + .pfear_sts = ext_arl_socs_pfear_map, + .ppfear_buckets = ARL_SOCS_PPFEAR_NUM_ENTRIES, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_sts = arl_socs_lpm_maps, + .ltr_ignore_max = ARL_SOCS_NUM_IP_IGN_ALLOWED, + .ltr_show_sts = arl_socs_ltr_show_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = MTL_SOC_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_reg_index = ARL_LPM_REG_INDEX, + .etr3_offset = ETR3_OFFSET, + .pson_residency_offset = TGL_PSON_RESIDENCY_OFFSET, + .pson_residency_counter_step = TGL_PSON_RES_COUNTER_STEP, + .lpm_req_guid = SOCS_LPM_REQ_GUID, +}; + +static const struct pmc_bit_map arl_pchs_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"ESE", MTL_PMC_LTR_ESE}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map arl_pchs_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0)}, + {"AON3_OFF_STS", BIT(1)}, + {"AON4_OFF_STS", BIT(2)}, + {"AON2_SPL_OFF_STS", BIT(3)}, + {"AONL_OFF_STS", BIT(4)}, + {"XTAL_LVM_OFF_STS", BIT(5)}, + {"AON5_ACRO_OFF_STS", BIT(6)}, + {"AON6_ACRO_OFF_STS", BIT(7)}, + {"USB3_PLL_OFF_STS", BIT(8)}, + {"ACRO_OFF_STS", BIT(9)}, + {"AUDIO_PLL_OFF_STS", BIT(10)}, + {"MAIN_CRO_OFF_STS", BIT(11)}, + {"MAIN_DIVIDER_OFF_STS", BIT(12)}, + {"REF_PLL_NON_OC_OFF_STS", BIT(13)}, + {"DMI_PLL_OFF_STS", BIT(14)}, + {"PHY_EXT_INJ_OFF_STS", BIT(15)}, + {"AON6_MCRO_OFF_STS", BIT(16)}, + {"XTAL_AGGR_OFF_STS", BIT(17)}, + {"USB2_PLL_OFF_STS", BIT(18)}, + {"TSN0_PLL_OFF_STS", BIT(19)}, + {"TSN1_PLL_OFF_STS", BIT(20)}, + {"GBE_PLL_OFF_STS", BIT(21)}, + {"SATA_PLL_OFF_STS", BIT(22)}, + {"PCIE0_PLL_OFF_STS", BIT(23)}, + {"PCIE1_PLL_OFF_STS", BIT(24)}, + {"PCIE2_PLL_OFF_STS", BIT(26)}, + {"PCIE3_PLL_OFF_STS", BIT(27)}, + {"REF_PLL_OFF_STS", BIT(28)}, + {"PCIE4_PLL_OFF_STS", BIT(29)}, + {"PCIE5_PLL_OFF_STS", BIT(30)}, + {"REF38P4_PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0)}, + {"DMI_PGD0_PG_STS", BIT(1)}, + {"ESPISPI_PGD0_PG_STS", BIT(2)}, + {"XHCI_PGD0_PG_STS", BIT(3)}, + {"SPA_PGD0_PG_STS", BIT(4)}, + {"SPB_PGD0_PG_STS", BIT(5)}, + {"SPC_PGD0_PG_STS", BIT(6)}, + {"GBE_PGD0_PG_STS", BIT(7)}, + {"SATA_PGD0_PG_STS", BIT(8)}, + {"FIA_X_PGD0_PG_STS", BIT(9)}, + {"MPFPW4_PGD0_PG_STS", BIT(10)}, + {"EAH_PGD0_PG_STS", BIT(11)}, + {"MPFPW1_PGD0_PG_STS", BIT(12)}, + {"SPD_PGD0_PG_STS", BIT(13)}, + {"LPSS_PGD0_PG_STS", BIT(14)}, + {"LPC_PGD0_PG_STS", BIT(15)}, + {"SMB_PGD0_PG_STS", BIT(16)}, + {"ISH_PGD0_PG_STS", BIT(17)}, + {"P2S_PGD0_PG_STS", BIT(18)}, + {"NPK_PGD0_PG_STS", BIT(19)}, + {"U3FPW1_PGD0_PG_STS", BIT(20)}, + {"PECI_PGD0_PG_STS", BIT(21)}, + {"FUSE_PGD0_PG_STS", BIT(22)}, + {"SBR8_PGD0_PG_STS", BIT(23)}, + {"EXE_PGD0_PG_STS", BIT(24)}, + {"XDCI_PGD0_PG_STS", BIT(25)}, + {"EXI_PGD0_PG_STS", BIT(26)}, + {"CSE_PGD0_PG_STS", BIT(27)}, + {"KVMCC_PGD0_PG_STS", BIT(28)}, + {"PMT_PGD0_PG_STS", BIT(29)}, + {"CLINK_PGD0_PG_STS", BIT(30)}, + {"PTIO_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0)}, + {"SUSRAM_PGD0_PG_STS", BIT(1)}, + {"SMT1_PGD0_PG_STS", BIT(2)}, + {"SMT4_PGD0_PG_STS", BIT(3)}, + {"SMS2_PGD0_PG_STS", BIT(4)}, + {"SMS1_PGD0_PG_STS", BIT(5)}, + {"CSMERTC_PGD0_PG_STS", BIT(6)}, + {"CSMEPSF_PGD0_PG_STS", BIT(7)}, + {"SBR0_PGD0_PG_STS", BIT(8)}, + {"SBR1_PGD0_PG_STS", BIT(9)}, + {"SBR2_PGD0_PG_STS", BIT(10)}, + {"SBR3_PGD0_PG_STS", BIT(11)}, + {"SBR4_PGD0_PG_STS", BIT(12)}, + {"SBR5_PGD0_PG_STS", BIT(13)}, + {"MPFPW3_PGD0_PG_STS", BIT(14)}, + {"PSF1_PGD0_PG_STS", BIT(15)}, + {"PSF2_PGD0_PG_STS", BIT(16)}, + {"PSF3_PGD0_PG_STS", BIT(17)}, + {"PSF4_PGD0_PG_STS", BIT(18)}, + {"CNVI_PGD0_PG_STS", BIT(19)}, + {"DMI3_PGD0_PG_STS", BIT(20)}, + {"ENDBG_PGD0_PG_STS", BIT(21)}, + {"DBG_SBR_PGD0_PG_STS", BIT(22)}, + {"SBR6_PGD0_PG_STS", BIT(23)}, + {"SBR7_PGD0_PG_STS", BIT(24)}, + {"NPK_PGD1_PG_STS", BIT(25)}, + {"U3FPW3_PGD0_PG_STS", BIT(26)}, + {"MPFPW2_PGD0_PG_STS", BIT(27)}, + {"MPFPW7_PGD0_PG_STS", BIT(28)}, + {"GBETSN1_PGD0_PG_STS", BIT(29)}, + {"PSF7_PGD0_PG_STS", BIT(30)}, + {"FIA2_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_power_gating_status_2_map[] = { + {"U3FPW2_PGD0_PG_STS", BIT(0)}, + {"FIA_PGD0_PG_STS", BIT(1)}, + {"FIACPCB_X_PGD0_PG_STS", BIT(2)}, + {"FIA1_PGD0_PG_STS", BIT(3)}, + {"TAM_PGD0_PG_STS", BIT(4)}, + {"GBETSN_PGD0_PG_STS", BIT(5)}, + {"SBR9_PGD0_PG_STS", BIT(6)}, + {"THC0_PGD0_PG_STS", BIT(7)}, + {"THC1_PGD0_PG_STS", BIT(8)}, + {"PMC_PGD1_PG_STS", BIT(9)}, + {"DBC_PGD0_PG_STS", BIT(10)}, + {"DBG_PSF_PGD0_PG_STS", BIT(11)}, + {"SPF_PGD0_PG_STS", BIT(12)}, + {"ACE_PGD0_PG_STS", BIT(13)}, + {"ACE_PGD1_PG_STS", BIT(14)}, + {"ACE_PGD2_PG_STS", BIT(15)}, + {"ACE_PGD3_PG_STS", BIT(16)}, + {"ACE_PGD4_PG_STS", BIT(17)}, + {"ACE_PGD5_PG_STS", BIT(18)}, + {"ACE_PGD6_PG_STS", BIT(19)}, + {"ACE_PGD7_PG_STS", BIT(20)}, + {"SPE_PGD0_PG_STS", BIT(21)}, + {"MPFPW5_PG_STS", BIT(22)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_d3_status_0_map[] = { + {"SPF_D3_STS", BIT(0)}, + {"LPSS_D3_STS", BIT(3)}, + {"XDCI_D3_STS", BIT(4)}, + {"XHCI_D3_STS", BIT(5)}, + {"SPA_D3_STS", BIT(12)}, + {"SPB_D3_STS", BIT(13)}, + {"SPC_D3_STS", BIT(14)}, + {"SPD_D3_STS", BIT(15)}, + {"SPE_D3_STS", BIT(16)}, + {"ESPISPI_D3_STS", BIT(18)}, + {"SATA_D3_STS", BIT(20)}, + {"PSTH_D3_STS", BIT(21)}, + {"DMI_D3_STS", BIT(22)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_d3_status_1_map[] = { + {"GBETSN1_D3_STS", BIT(14)}, + {"GBE_D3_STS", BIT(19)}, + {"ITSS_D3_STS", BIT(23)}, + {"P2S_D3_STS", BIT(24)}, + {"CNVI_D3_STS", BIT(27)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1)}, + {"SUSRAM_D3_STS", BIT(2)}, + {"CSE_D3_STS", BIT(4)}, + {"KVMCC_D3_STS", BIT(5)}, + {"USBR0_D3_STS", BIT(6)}, + {"ISH_D3_STS", BIT(7)}, + {"SMT1_D3_STS", BIT(8)}, + {"SMT2_D3_STS", BIT(9)}, + {"SMT3_D3_STS", BIT(10)}, + {"SMT4_D3_STS", BIT(11)}, + {"SMT5_D3_STS", BIT(12)}, + {"SMT6_D3_STS", BIT(13)}, + {"CLINK_D3_STS", BIT(14)}, + {"PTIO_D3_STS", BIT(16)}, + {"PMT_D3_STS", BIT(17)}, + {"SMS1_D3_STS", BIT(18)}, + {"SMS2_D3_STS", BIT(19)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_d3_status_3_map[] = { + {"ESE_D3_STS", BIT(3)}, + {"GBETSN_D3_STS", BIT(13)}, + {"THC0_D3_STS", BIT(14)}, + {"THC1_D3_STS", BIT(15)}, + {"ACE_D3_STS", BIT(23)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_vnn_req_status_0_map[] = { + {"FIA_VNN_REQ_STS", BIT(17)}, + {"ESPISPI_VNN_REQ_STS", BIT(18)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4)}, + {"DFXAGG_VNN_REQ_STS", BIT(8)}, + {"EXI_VNN_REQ_STS", BIT(9)}, + {"GBE_VNN_REQ_STS", BIT(19)}, + {"SMB_VNN_REQ_STS", BIT(25)}, + {"LPC_VNN_REQ_STS", BIT(26)}, + {"CNVI_VNN_REQ_STS", BIT(27)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_vnn_req_status_2_map[] = { + {"FIA2_VNN_REQ_STS", BIT(0)}, + {"CSMERTC_VNN_REQ_STS", BIT(1)}, + {"CSE_VNN_REQ_STS", BIT(4)}, + {"ISH_VNN_REQ_STS", BIT(7)}, + {"SMT1_VNN_REQ_STS", BIT(8)}, + {"SMT4_VNN_REQ_STS", BIT(11)}, + {"CLINK_VNN_REQ_STS", BIT(14)}, + {"SMS1_VNN_REQ_STS", BIT(18)}, + {"SMS2_VNN_REQ_STS", BIT(19)}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20)}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21)}, + {"GPIOCOM2_VNN_REQ_STS", BIT(22)}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23)}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_vnn_req_status_3_map[] = { + {"ESE_VNN_REQ_STS", BIT(3)}, + {"DTS0_VNN_REQ_STS", BIT(7)}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11)}, + {"FIA1_VNN_REQ_STS", BIT(12)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0)}, + {"TS_OFF_REQ_STS", BIT(1)}, + {"PNDE_MET_REQ_STS", BIT(2)}, + {"PCIE_DEEP_PM_REQ_STS", BIT(3)}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4)}, + {"ISH_VNNAON_REQ_STS", BIT(7)}, + {"IOE_COND_MET_S02I2_0_REQ_STS", BIT(8)}, + {"IOE_COND_MET_S02I2_1_REQ_STS", BIT(9)}, + {"IOE_COND_MET_S02I2_2_REQ_STS", BIT(10)}, + {"PLT_GREATER_REQ_STS", BIT(11)}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13)}, + {"PM_SYNC_STATES_REQ_STS", BIT(14)}, + {"EA_REQ_STS", BIT(15)}, + {"DMI_CLKREQ_B_REQ_STS", BIT(16)}, + {"BRK_EV_EN_REQ_STS", BIT(17)}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18)}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19)}, + {"ARC_IDLE_REQ_STS", BIT(21)}, + {"DMI_IN_REQ_STS", BIT(22)}, + {"FIA_DEEP_PM_REQ_STS", BIT(23)}, + {"XDCI_ATTACHED_REQ_STS", BIT(24)}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25)}, + {"PRE_WAKE0_REQ_STS", BIT(27)}, + {"PRE_WAKE1_REQ_STS", BIT(28)}, + {"PRE_WAKE2_EN_REQ_STS", BIT(29)}, + {"CNVI_V1P05_REQ_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map arl_pchs_signal_status_map[] = { + {"LSX_Wake0_STS", BIT(0)}, + {"LSX_Wake1_STS", BIT(1)}, + {"LSX_Wake2_STS", BIT(2)}, + {"LSX_Wake3_STS", BIT(3)}, + {"LSX_Wake4_STS", BIT(4)}, + {"LSX_Wake5_STS", BIT(5)}, + {"LSX_Wake6_STS", BIT(6)}, + {"LSX_Wake7_STS", BIT(7)}, + {"Int_Timer_SS_Wake0_STS", BIT(8)}, + {"Int_Timer_SS_Wake1_STS", BIT(9)}, + {"Int_Timer_SS_Wake0_STS", BIT(10)}, + {"Int_Timer_SS_Wake1_STS", BIT(11)}, + {"Int_Timer_SS_Wake2_STS", BIT(12)}, + {"Int_Timer_SS_Wake3_STS", BIT(13)}, + {"Int_Timer_SS_Wake4_STS", BIT(14)}, + {"Int_Timer_SS_Wake5_STS", BIT(15)}, + {} +}; + +static const struct pmc_bit_map *arl_pchs_lpm_maps[] = { + arl_pchs_clocksource_status_map, + arl_pchs_power_gating_status_0_map, + arl_pchs_power_gating_status_1_map, + arl_pchs_power_gating_status_2_map, + arl_pchs_d3_status_0_map, + arl_pchs_d3_status_1_map, + arl_pchs_d3_status_2_map, + arl_pchs_d3_status_3_map, + arl_pchs_vnn_req_status_0_map, + arl_pchs_vnn_req_status_1_map, + arl_pchs_vnn_req_status_2_map, + arl_pchs_vnn_req_status_3_map, + arl_pchs_vnn_misc_status_map, + arl_pchs_signal_status_map, + NULL +}; + +static const struct pmc_reg_map arl_pchs_reg_map = { + .pfear_sts = ext_arl_socs_pfear_map, + .ppfear_buckets = ARL_SOCS_PPFEAR_NUM_ENTRIES, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = ARL_SOCS_NUM_IP_IGN_ALLOWED, + .lpm_sts = arl_pchs_lpm_maps, + .ltr_show_sts = arl_pchs_ltr_show_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = ARL_PCH_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_reg_index = ARL_LPM_REG_INDEX, + .etr3_offset = ETR3_OFFSET, + .lpm_req_guid = PCHS_LPM_REQ_GUID, +}; + +static struct pmc_info arl_pmc_info_list[] = { + { + .devid = PMC_DEVID_ARL_IOEP, + .map = &mtl_ioep_reg_map, + }, + { + .devid = PMC_DEVID_ARL_SOCS, + .map = &arl_socs_reg_map, + }, + { + .devid = PMC_DEVID_ARL_PCHS, + .map = &arl_pchs_reg_map, + }, + { + .devid = PMC_DEVID_ARL_SOCM, + .map = &mtl_socm_reg_map, + }, + {} +}; + +static const u8 arl_pmc_list[] = {PMC_IDX_MAIN, PMC_IDX_IOE, PMC_IDX_PCH}; +static const u8 arl_h_pmc_list[] = {PMC_IDX_MAIN, PMC_IDX_IOE}; + +#define ARL_NPU_PCI_DEV 0xad1d +#define ARL_GNA_PCI_DEV 0xae4c +#define ARL_H_NPU_PCI_DEV 0x7d1d +#define ARL_H_GNA_PCI_DEV 0x774c +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void arl_d3_fixup(void) +{ + pmc_core_set_device_d3(ARL_NPU_PCI_DEV); + pmc_core_set_device_d3(ARL_GNA_PCI_DEV); +} + +static void arl_h_d3_fixup(void) +{ + pmc_core_set_device_d3(ARL_H_NPU_PCI_DEV); + pmc_core_set_device_d3(ARL_H_GNA_PCI_DEV); +} + +static int arl_resume(struct pmc_dev *pmcdev) +{ + arl_d3_fixup(); + + return cnl_resume(pmcdev); +} + +static int arl_h_resume(struct pmc_dev *pmcdev) +{ + arl_h_d3_fixup(); + + return cnl_resume(pmcdev); +} + +static int arl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + arl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +static int arl_h_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + arl_h_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +static u32 ARL_PMT_DMU_GUIDS[] = {ARL_PMT_DMU_GUID, 0x0}; +struct pmc_dev_info arl_pmc_dev = { + .dmu_guids = ARL_PMT_DMU_GUIDS, + .num_pmcs = ARRAY_SIZE(arl_pmc_list), + .pmc_list = arl_pmc_list, + .regmap_list = arl_pmc_info_list, + .map = &arl_socs_reg_map, + .sub_req_show = &pmc_core_substate_req_regs_fops, + .suspend = cnl_suspend, + .resume = arl_resume, + .init = arl_core_init, + .sub_req = pmc_core_pmt_get_lpm_req, + .ssram_hidden = true, + .die_c6_offset = MTL_PMT_DMU_DIE_C6_OFFSET, +}; + +static u32 ARL_H_PMT_DMU_GUIDS[] = {ARL_PMT_DMU_GUID, ARL_H_PMT_DMU_GUID, 0x0}; +struct pmc_dev_info arl_h_pmc_dev = { + .dmu_guids = ARL_H_PMT_DMU_GUIDS, + .num_pmcs = ARRAY_SIZE(arl_h_pmc_list), + .pmc_list = arl_h_pmc_list, + .regmap_list = arl_pmc_info_list, + .map = &mtl_socm_reg_map, + .sub_req_show = &pmc_core_substate_req_regs_fops, + .suspend = cnl_suspend, + .resume = arl_h_resume, + .init = arl_h_core_init, + .sub_req = pmc_core_pmt_get_lpm_req, + .ssram_hidden = true, + .die_c6_offset = MTL_PMT_DMU_DIE_C6_OFFSET, +}; diff --git a/drivers/platform/x86/intel/pmc/cnp.c b/drivers/platform/x86/intel/pmc/cnp.c new file mode 100644 index 000000000..efea4e1ba --- /dev/null +++ b/drivers/platform/x86/intel/pmc/cnp.c @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Cannon Lake Point PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include +#include +#include +#include "core.h" + +/* Cannon Lake: PGD PFET Enable Ack Status Register(s) bitmap */ +const struct pmc_bit_map cnp_pfear_map[] = { + {"PMC", BIT(0)}, + {"OPI-DMI", BIT(1)}, + {"SPI/eSPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SPC", BIT(6)}, + {"GBE", BIT(7)}, + + {"SATA", BIT(0)}, + {"HDA_PGD0", BIT(1)}, + {"HDA_PGD1", BIT(2)}, + {"HDA_PGD2", BIT(3)}, + {"HDA_PGD3", BIT(4)}, + {"SPD", BIT(5)}, + {"LPSS", BIT(6)}, + {"LPC", BIT(7)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"P2SB", BIT(2)}, + {"NPK_VNN", BIT(3)}, + {"SDX", BIT(4)}, + {"SPE", BIT(5)}, + {"Fuse", BIT(6)}, + {"SBR8", BIT(7)}, + + {"CSME_FSC", BIT(0)}, + {"USB3_OTG", BIT(1)}, + {"EXI", BIT(2)}, + {"CSE", BIT(3)}, + {"CSME_KVM", BIT(4)}, + {"CSME_PMT", BIT(5)}, + {"CSME_CLINK", BIT(6)}, + {"CSME_PTIO", BIT(7)}, + + {"CSME_USBR", BIT(0)}, + {"CSME_SUSRAM", BIT(1)}, + {"CSME_SMT1", BIT(2)}, + {"CSME_SMT4", BIT(3)}, + {"CSME_SMS2", BIT(4)}, + {"CSME_SMS1", BIT(5)}, + {"CSME_RTC", BIT(6)}, + {"CSME_PSF", BIT(7)}, + + {"SBR0", BIT(0)}, + {"SBR1", BIT(1)}, + {"SBR2", BIT(2)}, + {"SBR3", BIT(3)}, + {"SBR4", BIT(4)}, + {"SBR5", BIT(5)}, + {"CSME_PECI", BIT(6)}, + {"PSF1", BIT(7)}, + + {"PSF2", BIT(0)}, + {"PSF3", BIT(1)}, + {"PSF4", BIT(2)}, + {"CNVI", BIT(3)}, + {"UFS0", BIT(4)}, + {"EMMC", BIT(5)}, + {"SPF", BIT(6)}, + {"SBR6", BIT(7)}, + + {"SBR7", BIT(0)}, + {"NPK_AON", BIT(1)}, + {"HDA_PGD4", BIT(2)}, + {"HDA_PGD5", BIT(3)}, + {"HDA_PGD6", BIT(4)}, + {"PSF6", BIT(5)}, + {"PSF7", BIT(6)}, + {"PSF8", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_cnp_pfear_map[] = { + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + cnp_pfear_map, + NULL +}; + +static const struct pmc_bit_map cnp_slps0_dbg0_map[] = { + {"AUDIO_D3", BIT(0)}, + {"OTG_D3", BIT(1)}, + {"XHCI_D3", BIT(2)}, + {"LPIO_D3", BIT(3)}, + {"SDX_D3", BIT(4)}, + {"SATA_D3", BIT(5)}, + {"UFS0_D3", BIT(6)}, + {"UFS1_D3", BIT(7)}, + {"EMMC_D3", BIT(8)}, + {} +}; + +static const struct pmc_bit_map cnp_slps0_dbg1_map[] = { + {"SDIO_PLL_OFF", BIT(0)}, + {"USB2_PLL_OFF", BIT(1)}, + {"AUDIO_PLL_OFF", BIT(2)}, + {"OC_PLL_OFF", BIT(3)}, + {"MAIN_PLL_OFF", BIT(4)}, + {"XOSC_OFF", BIT(5)}, + {"LPC_CLKS_GATED", BIT(6)}, + {"PCIE_CLKREQS_IDLE", BIT(7)}, + {"AUDIO_ROSC_OFF", BIT(8)}, + {"HPET_XOSC_CLK_REQ", BIT(9)}, + {"PMC_ROSC_SLOW_CLK", BIT(10)}, + {"AON2_ROSC_GATED", BIT(11)}, + {"CLKACKS_DEASSERTED", BIT(12)}, + {} +}; + +static const struct pmc_bit_map cnp_slps0_dbg2_map[] = { + {"MPHY_CORE_GATED", BIT(0)}, + {"CSME_GATED", BIT(1)}, + {"USB2_SUS_GATED", BIT(2)}, + {"DYN_FLEX_IO_IDLE", BIT(3)}, + {"GBE_NO_LINK", BIT(4)}, + {"THERM_SEN_DISABLED", BIT(5)}, + {"PCIE_LOW_POWER", BIT(6)}, + {"ISH_VNNAON_REQ_ACT", BIT(7)}, + {"ISH_VNN_REQ_ACT", BIT(8)}, + {"CNV_VNNAON_REQ_ACT", BIT(9)}, + {"CNV_VNN_REQ_ACT", BIT(10)}, + {"NPK_VNNON_REQ_ACT", BIT(11)}, + {"PMSYNC_STATE_IDLE", BIT(12)}, + {"ALST_GT_THRES", BIT(13)}, + {"PMC_ARC_PG_READY", BIT(14)}, + {} +}; + +const struct pmc_bit_map *cnp_slps0_dbg_maps[] = { + cnp_slps0_dbg0_map, + cnp_slps0_dbg1_map, + cnp_slps0_dbg2_map, + NULL +}; + +const struct pmc_bit_map cnp_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"Reserved", CNP_PMC_LTR_RESERVED}, + {"ME", CNP_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"EVA", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"CAMERA", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +const struct pmc_reg_map cnp_reg_map = { + .pfear_sts = ext_cnp_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = SPT_PMC_SLP_S0_RES_COUNTER_STEP, + .slps0_dbg_maps = cnp_slps0_dbg_maps, + .ltr_show_sts = cnp_ltr_show_map, + .msr_sts = msr_map, + .slps0_dbg_offset = CNP_PMC_SLPS0_DBG_OFFSET, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = CNP_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = CNP_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = CNP_NUM_IP_IGN_ALLOWED, + .etr3_offset = ETR3_OFFSET, +}; + + +/* + * Disable C1 auto-demotion + * + * Aggressive C1 auto-demotion may lead to failure to enter the deepest C-state + * during suspend-to-idle, causing high power consumption. To prevent this, we + * disable C1 auto-demotion during suspend and re-enable on resume. + * + * Note that, although MSR_PKG_CST_CONFIG_CONTROL has 'package' in its name, it + * is actually a per-core MSR on client platforms, affecting only a single CPU. + * Therefore, it must be configured on all online CPUs. The online cpu mask is + * unchanged during the phase of suspend/resume as user space is frozen. + */ + +static DEFINE_PER_CPU(u64, pkg_cst_config); + +static void disable_c1_auto_demote(void *unused) +{ + int cpunum = smp_processor_id(); + u64 val; + + rdmsrq(MSR_PKG_CST_CONFIG_CONTROL, val); + per_cpu(pkg_cst_config, cpunum) = val; + val &= ~NHM_C1_AUTO_DEMOTE; + wrmsrq(MSR_PKG_CST_CONFIG_CONTROL, val); + + pr_debug("%s: cpu:%d cst %llx\n", __func__, cpunum, val); +} + +static void restore_c1_auto_demote(void *unused) +{ + int cpunum = smp_processor_id(); + + wrmsrq(MSR_PKG_CST_CONFIG_CONTROL, per_cpu(pkg_cst_config, cpunum)); + + pr_debug("%s: cpu:%d cst %llx\n", __func__, cpunum, + per_cpu(pkg_cst_config, cpunum)); +} + +static void s2idle_cpu_quirk(smp_call_func_t func) +{ + if (pm_suspend_via_firmware()) + return; + + on_each_cpu(func, NULL, true); +} + +void cnl_suspend(struct pmc_dev *pmcdev) +{ + s2idle_cpu_quirk(disable_c1_auto_demote); + + /* + * Due to a hardware limitation, the GBE LTR blocks PC10 + * when a cable is attached. To unblock PC10 during suspend, + * tell the PMC to ignore it. + */ + pmc_core_send_ltr_ignore(pmcdev, 3, 1); +} + +int cnl_resume(struct pmc_dev *pmcdev) +{ + s2idle_cpu_quirk(restore_c1_auto_demote); + + pmc_core_send_ltr_ignore(pmcdev, 3, 0); + + return pmc_core_resume_common(pmcdev); +} + +struct pmc_dev_info cnp_pmc_dev = { + .map = &cnp_reg_map, + .suspend = cnl_suspend, + .resume = cnl_resume, +}; + diff --git a/drivers/platform/x86/intel/pmc/core.c b/drivers/platform/x86/intel/pmc/core.c new file mode 100644 index 000000000..825ba5fa0 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/core.c @@ -0,0 +1,2161 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Core SoC Power Management Controller Driver + * + * Copyright (c) 2016, Intel Corporation. + * All Rights Reserved. + * + * Authors: Rajneesh Bhardwaj + * Vishwanath Somayaji + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +enum header_type { + HEADER_STATUS, + HEADER_VALUE, +}; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "core.h" +#include "ssram_telemetry.h" +#include "../pmt/telemetry.h" + +/* Maximum number of modes supported by platfoms that has low power mode capability */ +const char *pmc_lpm_modes[] = { + "S0i2.0", + "S0i2.1", + "S0i2.2", + "S0i3.0", + "S0i3.1", + "S0i3.2", + "S0i3.3", + "S0i3.4", + NULL +}; + +/* PKGC MSRs are common across Intel Core SoCs */ +const struct pmc_bit_map msr_map[] = { + {"Package C2", MSR_PKG_C2_RESIDENCY}, + {"Package C3", MSR_PKG_C3_RESIDENCY}, + {"Package C6", MSR_PKG_C6_RESIDENCY}, + {"Package C7", MSR_PKG_C7_RESIDENCY}, + {"Package C8", MSR_PKG_C8_RESIDENCY}, + {"Package C9", MSR_PKG_C9_RESIDENCY}, + {"Package C10", MSR_PKG_C10_RESIDENCY}, + {} +}; + +static inline u32 pmc_core_reg_read(struct pmc *pmc, int reg_offset) +{ + return readl(pmc->regbase + reg_offset); +} + +static inline void pmc_core_reg_write(struct pmc *pmc, int reg_offset, + u32 val) +{ + writel(val, pmc->regbase + reg_offset); +} + +static inline u64 pmc_core_adjust_slp_s0_step(struct pmc *pmc, u32 value) +{ + /* + * ADL PCH does not have the SLP_S0 counter and LPM Residency counters are + * used as a workaround which uses 30.5 usec tick. All other client + * programs have the legacy SLP_S0 residency counter that is using the 122 + * usec tick. + */ + const int lpm_adj_x2 = pmc->map->lpm_res_counter_step_x2; + + if (pmc->map == &adl_reg_map) + return (u64)value * GET_X2_COUNTER((u64)lpm_adj_x2); + else + return (u64)value * pmc->map->slp_s0_res_counter_step; +} + +static int set_etr3(struct pmc_dev *pmcdev) +{ + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_reg_map *map = pmc->map; + u32 reg; + + if (!map->etr3_offset) + return -EOPNOTSUPP; + + guard(mutex)(&pmcdev->lock); + + /* check if CF9 is locked */ + reg = pmc_core_reg_read(pmc, map->etr3_offset); + if (reg & ETR3_CF9LOCK) + return -EACCES; + + /* write CF9 global reset bit */ + reg |= ETR3_CF9GR; + pmc_core_reg_write(pmc, map->etr3_offset, reg); + + reg = pmc_core_reg_read(pmc, map->etr3_offset); + if (!(reg & ETR3_CF9GR)) + return -EIO; + + return 0; +} +static umode_t etr3_is_visible(struct kobject *kobj, + struct attribute *attr, + int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct pmc_dev *pmcdev = dev_get_drvdata(dev); + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_reg_map *map = pmc->map; + u32 reg; + + scoped_guard(mutex, &pmcdev->lock) + reg = pmc_core_reg_read(pmc, map->etr3_offset); + + return reg & ETR3_CF9LOCK ? attr->mode & (SYSFS_PREALLOC | 0444) : attr->mode; +} + +static ssize_t etr3_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct pmc_dev *pmcdev = dev_get_drvdata(dev); + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_reg_map *map = pmc->map; + u32 reg; + + if (!map->etr3_offset) + return -EOPNOTSUPP; + + scoped_guard(mutex, &pmcdev->lock) { + reg = pmc_core_reg_read(pmc, map->etr3_offset); + reg &= ETR3_CF9GR | ETR3_CF9LOCK; + } + + return sysfs_emit(buf, "0x%08x", reg); +} + +static ssize_t etr3_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct pmc_dev *pmcdev = dev_get_drvdata(dev); + int err; + u32 reg; + + err = kstrtouint(buf, 16, ®); + if (err) + return err; + + /* allow only CF9 writes */ + if (reg != ETR3_CF9GR) + return -EINVAL; + + err = set_etr3(pmcdev); + if (err) + return err; + + return len; +} +static DEVICE_ATTR_RW(etr3); + +static struct attribute *pmc_attrs[] = { + &dev_attr_etr3.attr, + NULL +}; + +static const struct attribute_group pmc_attr_group = { + .attrs = pmc_attrs, + .is_visible = etr3_is_visible, +}; + +static const struct attribute_group *pmc_dev_groups[] = { + &pmc_attr_group, + NULL +}; + +static int pmc_core_dev_state_get(void *data, u64 *val) +{ + struct pmc *pmc = data; + const struct pmc_reg_map *map = pmc->map; + u32 value; + + value = pmc_core_reg_read(pmc, map->slp_s0_offset); + *val = pmc_core_adjust_slp_s0_step(pmc, value); + + return 0; +} + +DEFINE_DEBUGFS_ATTRIBUTE(pmc_core_dev_state, pmc_core_dev_state_get, NULL, "%llu\n"); + +static int pmc_core_pson_residency_get(void *data, u64 *val) +{ + struct pmc *pmc = data; + const struct pmc_reg_map *map = pmc->map; + u32 value; + + value = pmc_core_reg_read(pmc, map->pson_residency_offset); + *val = (u64)value * map->pson_residency_counter_step; + + return 0; +} + +DEFINE_DEBUGFS_ATTRIBUTE(pmc_core_pson_residency, pmc_core_pson_residency_get, NULL, "%llu\n"); + +static int pmc_core_check_read_lock_bit(struct pmc *pmc) +{ + u32 value; + + value = pmc_core_reg_read(pmc, pmc->map->pm_cfg_offset); + return value & BIT(pmc->map->pm_read_disable_bit); +} + +static void pmc_core_slps0_display(struct pmc *pmc, struct device *dev, + struct seq_file *s) +{ + const struct pmc_bit_map **maps = pmc->map->slps0_dbg_maps; + const struct pmc_bit_map *map; + int offset = pmc->map->slps0_dbg_offset; + u32 data; + + while (*maps) { + map = *maps; + data = pmc_core_reg_read(pmc, offset); + offset += 4; + while (map->name) { + if (dev) + dev_info(dev, "SLP_S0_DBG: %-32s\tState: %s\n", + map->name, + data & map->bit_mask ? "Yes" : "No"); + if (s) + seq_printf(s, "SLP_S0_DBG: %-32s\tState: %s\n", + map->name, + data & map->bit_mask ? "Yes" : "No"); + ++map; + } + ++maps; + } +} + +static unsigned int pmc_core_lpm_get_arr_size(const struct pmc_bit_map **maps) +{ + unsigned int idx; + + for (idx = 0; maps[idx]; idx++) + ;/* Nothing */ + + return idx; +} + +static void pmc_core_lpm_display(struct pmc *pmc, struct device *dev, + struct seq_file *s, u32 offset, int pmc_index, + const char *str, + const struct pmc_bit_map **maps) +{ + unsigned int index, idx, len = 32, arr_size; + u32 bit_mask, *lpm_regs; + + arr_size = pmc_core_lpm_get_arr_size(maps); + lpm_regs = kmalloc_array(arr_size, sizeof(*lpm_regs), GFP_KERNEL); + if (!lpm_regs) + return; + + for (index = 0; index < arr_size; index++) { + lpm_regs[index] = pmc_core_reg_read(pmc, offset); + offset += 4; + } + + for (idx = 0; idx < arr_size; idx++) { + if (dev) + dev_info(dev, "\nPMC%d:LPM_%s_%d:\t0x%x\n", pmc_index, str, idx, + lpm_regs[idx]); + if (s) + seq_printf(s, "\nPMC%d:LPM_%s_%d:\t0x%x\n", pmc_index, str, idx, + lpm_regs[idx]); + for (index = 0; maps[idx][index].name && index < len; index++) { + bit_mask = maps[idx][index].bit_mask; + if (dev) + dev_info(dev, "PMC%d:%-30s %-30d\n", pmc_index, + maps[idx][index].name, + lpm_regs[idx] & bit_mask ? 1 : 0); + if (s) + seq_printf(s, "PMC%d:%-30s %-30d\n", pmc_index, + maps[idx][index].name, + lpm_regs[idx] & bit_mask ? 1 : 0); + } + } + + kfree(lpm_regs); +} + +static bool slps0_dbg_latch; + +static inline u8 pmc_core_reg_read_byte(struct pmc *pmc, int offset) +{ + return readb(pmc->regbase + offset); +} + +static void pmc_core_display_map(struct seq_file *s, int index, int idx, int ip, + int pmc_idx, u8 pf_reg, const struct pmc_bit_map **pf_map) +{ + seq_printf(s, "PMC%d:PCH IP: %-2d - %-32s\tState: %s\n", + pmc_idx, ip, pf_map[idx][index].name, + pf_map[idx][index].bit_mask & pf_reg ? "Off" : "On"); +} + +static int pmc_core_ppfear_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + const struct pmc_bit_map **maps; + u8 pf_regs[PPFEAR_MAX_NUM_ENTRIES]; + unsigned int index, iter, idx, ip = 0; + + if (!pmc) + continue; + + maps = pmc->map->pfear_sts; + iter = pmc->map->ppfear0_offset; + + for (index = 0; index < pmc->map->ppfear_buckets && + index < PPFEAR_MAX_NUM_ENTRIES; index++, iter++) + pf_regs[index] = pmc_core_reg_read_byte(pmc, iter); + + for (idx = 0; maps[idx]; idx++) { + for (index = 0; maps[idx][index].name && + index < pmc->map->ppfear_buckets * 8; ip++, index++) + pmc_core_display_map(s, index, idx, ip, pmc_idx, + pf_regs[index / 8], maps); + } + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_ppfear); + +/* This function should return link status, 0 means ready */ +static int pmc_core_mtpmc_link_status(struct pmc *pmc) +{ + u32 value; + + value = pmc_core_reg_read(pmc, SPT_PMC_PM_STS_OFFSET); + return value & BIT(SPT_PMC_MSG_FULL_STS_BIT); +} + +static int pmc_core_send_msg(struct pmc *pmc, u32 *addr_xram) +{ + u32 dest; + int timeout; + + for (timeout = NUM_RETRIES; timeout > 0; timeout--) { + if (pmc_core_mtpmc_link_status(pmc) == 0) + break; + msleep(5); + } + + if (timeout <= 0 && pmc_core_mtpmc_link_status(pmc)) + return -EBUSY; + + dest = (*addr_xram & MTPMC_MASK) | (1U << 1); + pmc_core_reg_write(pmc, SPT_PMC_MTPMC_OFFSET, dest); + return 0; +} + +static int pmc_core_mphy_pg_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_bit_map *map = pmc->map->mphy_sts; + u32 mphy_core_reg_low, mphy_core_reg_high; + u32 val_low, val_high; + unsigned int index; + int err = 0; + + if (pmcdev->pmc_xram_read_bit) { + seq_puts(s, "Access denied: please disable PMC_READ_DISABLE setting in BIOS."); + return 0; + } + + mphy_core_reg_low = (SPT_PMC_MPHY_CORE_STS_0 << 16); + mphy_core_reg_high = (SPT_PMC_MPHY_CORE_STS_1 << 16); + + guard(mutex)(&pmcdev->lock); + + err = pmc_core_send_msg(pmc, &mphy_core_reg_low); + if (err) + return err; + + msleep(10); + val_low = pmc_core_reg_read(pmc, SPT_PMC_MFPMC_OFFSET); + + err = pmc_core_send_msg(pmc, &mphy_core_reg_high); + if (err) + return err; + + msleep(10); + val_high = pmc_core_reg_read(pmc, SPT_PMC_MFPMC_OFFSET); + + for (index = 0; index < 8 && map[index].name; index++) { + seq_printf(s, "%-32s\tState: %s\n", + map[index].name, + map[index].bit_mask & val_low ? "Not power gated" : + "Power gated"); + } + + for (index = 8; map[index].name; index++) { + seq_printf(s, "%-32s\tState: %s\n", + map[index].name, + map[index].bit_mask & val_high ? "Not power gated" : + "Power gated"); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_mphy_pg); + +static int pmc_core_pll_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_bit_map *map = pmc->map->pll_sts; + u32 mphy_common_reg, val; + unsigned int index; + int err = 0; + + if (pmcdev->pmc_xram_read_bit) { + seq_puts(s, "Access denied: please disable PMC_READ_DISABLE setting in BIOS."); + return 0; + } + + mphy_common_reg = (SPT_PMC_MPHY_COM_STS_0 << 16); + guard(mutex)(&pmcdev->lock); + + err = pmc_core_send_msg(pmc, &mphy_common_reg); + if (err) + return err; + + /* Observed PMC HW response latency for MTPMC-MFPMC is ~10 ms */ + msleep(10); + val = pmc_core_reg_read(pmc, SPT_PMC_MFPMC_OFFSET); + + for (index = 0; map[index].name ; index++) { + seq_printf(s, "%-32s\tState: %s\n", + map[index].name, + map[index].bit_mask & val ? "Active" : "Idle"); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_pll); + +int pmc_core_send_ltr_ignore(struct pmc_dev *pmcdev, u32 value, int ignore) +{ + struct pmc *pmc; + const struct pmc_reg_map *map; + u32 reg; + unsigned int pmc_idx; + int ltr_index; + + ltr_index = value; + /* For platforms with multiple pmcs, ltr index value given by user + * is based on the contiguous indexes from ltr_show output. + * pmc index and ltr index needs to be calculated from it. + */ + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs) && ltr_index >= 0; pmc_idx++) { + pmc = pmcdev->pmcs[pmc_idx]; + + if (!pmc) + continue; + + map = pmc->map; + if (ltr_index <= map->ltr_ignore_max) + break; + + /* Along with IP names, ltr_show map includes CURRENT_PLATFORM + * and AGGREGATED_SYSTEM values per PMC. Take these two index + * values into account in ltr_index calculation. Also, to start + * ltr index from zero for next pmc, subtract it by 1. + */ + ltr_index = ltr_index - (map->ltr_ignore_max + 2) - 1; + } + + if (pmc_idx >= ARRAY_SIZE(pmcdev->pmcs) || ltr_index < 0) + return -EINVAL; + + pr_debug("ltr_ignore for pmc%d: ltr_index:%d\n", pmc_idx, ltr_index); + + guard(mutex)(&pmcdev->lock); + + reg = pmc_core_reg_read(pmc, map->ltr_ignore_offset); + if (ignore) + reg |= BIT(ltr_index); + else + reg &= ~BIT(ltr_index); + pmc_core_reg_write(pmc, map->ltr_ignore_offset, reg); + + return 0; +} + +static ssize_t pmc_core_ltr_write(struct pmc_dev *pmcdev, + const char __user *userbuf, + size_t count, int ignore) +{ + u32 value; + int err; + + err = kstrtou32_from_user(userbuf, count, 10, &value); + if (err) + return err; + + err = pmc_core_send_ltr_ignore(pmcdev, value, ignore); + + return err ?: count; +} + +static ssize_t pmc_core_ltr_ignore_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct pmc_dev *pmcdev = s->private; + + return pmc_core_ltr_write(pmcdev, userbuf, count, 1); +} + +static int pmc_core_ltr_ignore_show(struct seq_file *s, void *unused) +{ + return 0; +} +DEFINE_SHOW_STORE_ATTRIBUTE(pmc_core_ltr_ignore); + +static ssize_t pmc_core_ltr_restore_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct pmc_dev *pmcdev = s->private; + + return pmc_core_ltr_write(pmcdev, userbuf, count, 0); +} + +static int pmc_core_ltr_restore_show(struct seq_file *s, void *unused) +{ + return 0; +} +DEFINE_SHOW_STORE_ATTRIBUTE(pmc_core_ltr_restore); + +static void pmc_core_slps0_dbg_latch(struct pmc_dev *pmcdev, bool reset) +{ + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_reg_map *map = pmc->map; + u32 fd; + + guard(mutex)(&pmcdev->lock); + + if (!reset && !slps0_dbg_latch) + return; + + fd = pmc_core_reg_read(pmc, map->slps0_dbg_offset); + if (reset) + fd &= ~CNP_PMC_LATCH_SLPS0_EVENTS; + else + fd |= CNP_PMC_LATCH_SLPS0_EVENTS; + pmc_core_reg_write(pmc, map->slps0_dbg_offset, fd); + + slps0_dbg_latch = false; +} + +static int pmc_core_slps0_dbg_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + + pmc_core_slps0_dbg_latch(pmcdev, false); + pmc_core_slps0_display(pmcdev->pmcs[PMC_IDX_MAIN], NULL, s); + pmc_core_slps0_dbg_latch(pmcdev, true); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_slps0_dbg); + +static u32 convert_ltr_scale(u32 val) +{ + /* + * As per PCIE specification supporting document + * ECN_LatencyTolnReporting_14Aug08.pdf the Latency + * Tolerance Reporting data payload is encoded in a + * 3 bit scale and 10 bit value fields. Values are + * multiplied by the indicated scale to yield an absolute time + * value, expressible in a range from 1 nanosecond to + * 2^25*(2^10-1) = 34,326,183,936 nanoseconds. + * + * scale encoding is as follows: + * + * ---------------------------------------------- + * |scale factor | Multiplier (ns) | + * ---------------------------------------------- + * | 0 | 1 | + * | 1 | 32 | + * | 2 | 1024 | + * | 3 | 32768 | + * | 4 | 1048576 | + * | 5 | 33554432 | + * | 6 | Invalid | + * | 7 | Invalid | + * ---------------------------------------------- + */ + if (val > 5) { + pr_warn_once("Invalid LTR scale factor %u (only 0-5 are valid per PCIe spec)\n", + val); + return 0; + } + + return 1U << (5 * val); +} + +static int pmc_core_ltr_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + u64 decoded_snoop_ltr, decoded_non_snoop_ltr, val; + u32 ltr_raw_data, scale; + u16 snoop_ltr, nonsnoop_ltr; + unsigned int pmc_idx, index, ltr_index = 0; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc; + const struct pmc_bit_map *map; + u32 ltr_ign_reg; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + scoped_guard(mutex, &pmcdev->lock) + ltr_ign_reg = pmc_core_reg_read(pmc, pmc->map->ltr_ignore_offset); + + map = pmc->map->ltr_show_sts; + for (index = 0; map[index].name; index++) { + bool ltr_ign_data; + + if (index > pmc->map->ltr_ignore_max) + ltr_ign_data = false; + else + ltr_ign_data = ltr_ign_reg & BIT(index); + + decoded_snoop_ltr = decoded_non_snoop_ltr = 0; + ltr_raw_data = pmc_core_reg_read(pmc, + map[index].bit_mask); + snoop_ltr = ltr_raw_data & ~MTPMC_MASK; + nonsnoop_ltr = (ltr_raw_data >> 0x10) & ~MTPMC_MASK; + + if (FIELD_GET(LTR_REQ_NONSNOOP, ltr_raw_data)) { + scale = FIELD_GET(LTR_DECODED_SCALE, nonsnoop_ltr); + val = FIELD_GET(LTR_DECODED_VAL, nonsnoop_ltr); + decoded_non_snoop_ltr = val * convert_ltr_scale(scale); + } + if (FIELD_GET(LTR_REQ_SNOOP, ltr_raw_data)) { + scale = FIELD_GET(LTR_DECODED_SCALE, snoop_ltr); + val = FIELD_GET(LTR_DECODED_VAL, snoop_ltr); + decoded_snoop_ltr = val * convert_ltr_scale(scale); + } + + seq_printf(s, "%d\tPMC%d:%-32s\tLTR: RAW: 0x%-16x\tNon-Snoop(ns): %-16llu\tSnoop(ns): %-16llu\tLTR_IGNORE: %d\n", + ltr_index, pmc_idx, map[index].name, ltr_raw_data, + decoded_non_snoop_ltr, + decoded_snoop_ltr, ltr_ign_data); + ltr_index++; + } + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_ltr); + +static int pmc_core_s0ix_blocker_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); pmc_idx++) { + const struct pmc_bit_map **maps; + unsigned int arr_size, r_idx; + u32 offset, counter; + struct pmc *pmc; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + maps = pmc->map->s0ix_blocker_maps; + offset = pmc->map->s0ix_blocker_offset; + arr_size = pmc_core_lpm_get_arr_size(maps); + + for (r_idx = 0; r_idx < arr_size; r_idx++) { + const struct pmc_bit_map *map; + + for (map = maps[r_idx]; map->name; map++) { + if (!map->blk) + continue; + counter = pmc_core_reg_read(pmc, offset); + seq_printf(s, "PMC%d:%-30s %-30d\n", pmc_idx, + map->name, counter); + offset += map->blk * S0IX_BLK_SIZE; + } + } + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_s0ix_blocker); + +static void pmc_core_ltr_ignore_all(struct pmc_dev *pmcdev) +{ + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); pmc_idx++) { + struct pmc *pmc; + u32 ltr_ign; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + guard(mutex)(&pmcdev->lock); + pmc->ltr_ign = pmc_core_reg_read(pmc, pmc->map->ltr_ignore_offset); + + /* ltr_ignore_max is the max index value for LTR ignore register */ + ltr_ign = pmc->ltr_ign | GENMASK(pmc->map->ltr_ignore_max, 0); + pmc_core_reg_write(pmc, pmc->map->ltr_ignore_offset, ltr_ign); + } + + /* + * Ignoring ME during suspend is blocking platforms with ADL PCH to get to + * deeper S0ix substate. + */ + pmc_core_send_ltr_ignore(pmcdev, 6, 0); +} + +static void pmc_core_ltr_restore_all(struct pmc_dev *pmcdev) +{ + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); pmc_idx++) { + struct pmc *pmc; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + guard(mutex)(&pmcdev->lock); + pmc_core_reg_write(pmc, pmc->map->ltr_ignore_offset, pmc->ltr_ign); + } +} + +static inline u64 adjust_lpm_residency(struct pmc *pmc, u32 offset, + const int lpm_adj_x2) +{ + u64 lpm_res = pmc_core_reg_read(pmc, offset); + + return GET_X2_COUNTER((u64)lpm_adj_x2 * lpm_res); +} + +static int pmc_core_substate_res_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + int lpm_adj_x2; + struct pmc *pmc; + u32 offset; + u8 mode; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + lpm_adj_x2 = pmc->map->lpm_res_counter_step_x2; + offset = pmc->map->lpm_residency_offset; + + seq_printf(s, "pmc%u %10s %15s\n", pmc_idx, "Substate", "Residency"); + pmc_for_each_mode(mode, pmc) { + seq_printf(s, "%15s %15llu\n", pmc_lpm_modes[mode], + adjust_lpm_residency(pmc, offset + (4 * mode), lpm_adj_x2)); + } + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_substate_res); + +static int pmc_core_substate_sts_regs_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + const struct pmc_bit_map **maps; + u32 offset; + + if (!pmc) + continue; + maps = pmc->map->lpm_sts; + offset = pmc->map->lpm_status_offset; + pmc_core_lpm_display(pmc, NULL, s, offset, pmc_idx, "STATUS", maps); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_substate_sts_regs); + +static int pmc_core_substate_l_sts_regs_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + const struct pmc_bit_map **maps; + u32 offset; + + if (!pmc) + continue; + maps = pmc->map->lpm_sts; + offset = pmc->map->lpm_live_status_offset; + pmc_core_lpm_display(pmc, NULL, s, offset, pmc_idx, "LIVE_STATUS", maps); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_substate_l_sts_regs); + +static void pmc_core_substate_req_header_show(struct seq_file *s, int pmc_index, + enum header_type type) +{ + struct pmc_dev *pmcdev = s->private; + struct pmc *pmc = pmcdev->pmcs[pmc_index]; + u8 mode; + + seq_printf(s, "%40s |", "Element"); + pmc_for_each_mode(mode, pmc) + seq_printf(s, " %9s |", pmc_lpm_modes[mode]); + + if (type == HEADER_STATUS) { + seq_printf(s, " %9s |", "Status"); + seq_printf(s, " %11s |\n", "Live Status"); + } else { + seq_printf(s, " %9s |\n", "Value"); + } +} + +static int pmc_core_substate_blk_req_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); pmc_idx++) { + const struct pmc_bit_map **maps; + unsigned int arr_size, r_idx; + u32 offset, counter; + u32 *lpm_req_regs; + struct pmc *pmc; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc || !pmc->lpm_req_regs) + continue; + + lpm_req_regs = pmc->lpm_req_regs; + maps = pmc->map->s0ix_blocker_maps; + offset = pmc->map->s0ix_blocker_offset; + arr_size = pmc_core_lpm_get_arr_size(maps); + + /* Display the header */ + pmc_core_substate_req_header_show(s, pmc_idx, HEADER_VALUE); + + for (r_idx = 0; r_idx < arr_size; r_idx++) { + const struct pmc_bit_map *map; + + for (map = maps[r_idx]; map->name; map++) { + u8 mode; + + if (!map->blk) + continue; + + counter = pmc_core_reg_read(pmc, offset); + seq_printf(s, "pmc%u: %34s |", pmc_idx, map->name); + pmc_for_each_mode(mode, pmc) { + bool required = *lpm_req_regs & BIT(mode); + + seq_printf(s, " %9s |", required ? "Required" : " "); + } + seq_printf(s, " %9u |\n", counter); + offset += map->blk * S0IX_BLK_SIZE; + lpm_req_regs++; + } + } + } + return 0; +} + +static int pmc_core_substate_blk_req_open(struct inode *inode, struct file *file) +{ + return single_open(file, pmc_core_substate_blk_req_show, inode->i_private); +} + +const struct file_operations pmc_core_substate_blk_req_fops = { + .owner = THIS_MODULE, + .open = pmc_core_substate_blk_req_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int pmc_core_substate_req_regs_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + u32 sts_offset; + u32 sts_offset_live; + u32 *lpm_req_regs; + unsigned int mp, pmc_idx; + int num_maps; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + const struct pmc_bit_map **maps; + + if (!pmc) + continue; + + maps = pmc->map->lpm_sts; + num_maps = pmc->map->lpm_num_maps; + sts_offset = pmc->map->lpm_status_offset; + sts_offset_live = pmc->map->lpm_live_status_offset; + lpm_req_regs = pmc->lpm_req_regs; + + /* + * When there are multiple PMCs, though the PMC may exist, the + * requirement register discovery could have failed so check + * before accessing. + */ + if (!lpm_req_regs) + continue; + + /* Display the header */ + pmc_core_substate_req_header_show(s, pmc_idx, HEADER_STATUS); + + /* Loop over maps */ + for (mp = 0; mp < num_maps; mp++) { + u32 req_mask = 0; + u32 lpm_status; + u32 lpm_status_live; + const struct pmc_bit_map *map; + int i, len = 32; + u8 mode; + + /* + * Capture the requirements and create a mask so that we only + * show an element if it's required for at least one of the + * enabled low power modes + */ + pmc_for_each_mode(mode, pmc) + req_mask |= lpm_req_regs[mp + (mode * num_maps)]; + + /* Get the last latched status for this map */ + lpm_status = pmc_core_reg_read(pmc, sts_offset + (mp * 4)); + + /* Get the runtime status for this map */ + lpm_status_live = pmc_core_reg_read(pmc, sts_offset_live + (mp * 4)); + + /* Loop over elements in this map */ + map = maps[mp]; + for (i = 0; map[i].name && i < len; i++) { + u32 bit_mask = map[i].bit_mask; + + if (!(bit_mask & req_mask)) { + /* + * Not required for any enabled states + * so don't display + */ + continue; + } + + /* Display the element name in the first column */ + seq_printf(s, "pmc%d: %34s |", pmc_idx, map[i].name); + + /* Loop over the enabled states and display if required */ + pmc_for_each_mode(mode, pmc) { + bool required = lpm_req_regs[mp + (mode * num_maps)] & + bit_mask; + seq_printf(s, " %9s |", required ? "Required" : " "); + } + + /* In Status column, show the last captured state of this agent */ + seq_printf(s, " %9s |", lpm_status & bit_mask ? "Yes" : " "); + + /* In Live status column, show the live state of this agent */ + seq_printf(s, " %11s |", lpm_status_live & bit_mask ? "Yes" : " "); + + seq_puts(s, "\n"); + } + } + } + return 0; +} + +static int pmc_core_substate_req_regs_open(struct inode *inode, struct file *file) +{ + return single_open(file, pmc_core_substate_req_regs_show, inode->i_private); +} + +const struct file_operations pmc_core_substate_req_regs_fops = { + .owner = THIS_MODULE, + .open = pmc_core_substate_req_regs_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static unsigned int pmc_core_get_crystal_freq(void) +{ + unsigned int eax_denominator, ebx_numerator, ecx_hz, edx; + + if (boot_cpu_data.cpuid_level < CPUID_LEAF_TSC) + return 0; + + eax_denominator = ebx_numerator = ecx_hz = edx = 0; + + /* TSC/Crystal ratio, plus optionally Crystal Hz */ + cpuid(CPUID_LEAF_TSC, &eax_denominator, &ebx_numerator, &ecx_hz, &edx); + + if (ebx_numerator == 0 || eax_denominator == 0) + return 0; + + return ecx_hz; +} + +static int pmc_core_die_c6_us_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + u64 die_c6_res, count; + int ret; + + if (!pmcdev->crystal_freq) { + dev_warn_once(&pmcdev->pdev->dev, "Crystal frequency unavailable\n"); + return -ENXIO; + } + + ret = pmt_telem_read(pmcdev->punit_ep, pmcdev->die_c6_offset, + &count, 1); + if (ret) + return ret; + + die_c6_res = div64_u64(count * HZ_PER_MHZ, pmcdev->crystal_freq); + seq_printf(s, "%llu\n", die_c6_res); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_die_c6_us); + +static int pmc_core_pkgc_counters_show(struct seq_file *s, + struct telem_endpoint *ep, + u32 offset, const char **counters) +{ + unsigned int i; + u32 counter; + int ret; + + for (i = 0; counters[i]; i++) { + ret = pmt_telem_read32(ep, offset + i, &counter, 1); + if (ret) + return ret; + seq_printf(s, "%-30s %-30u\n", counters[i], counter); + } + + return 0; +} + +static int pmc_core_pkgc_ltr_blocker_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + + return pmc_core_pkgc_counters_show(s, pmcdev->pc_ep, + pmcdev->pkgc_ltr_blocker_offset, + pmcdev->pkgc_ltr_blocker_counters); +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_pkgc_ltr_blocker); + +static int pmc_core_pkgc_blocker_residency_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + + return pmc_core_pkgc_counters_show(s, pmcdev->pc_ep, + pmcdev->pkgc_blocker_offset, + pmcdev->pkgc_blocker_counters); +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_pkgc_blocker_residency); + +static int pmc_core_lpm_latch_mode_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmcdev = s->private; + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + bool c10; + u32 reg; + u8 mode; + + reg = pmc_core_reg_read(pmc, pmc->map->lpm_sts_latch_en_offset); + if (reg & LPM_STS_LATCH_MODE) { + seq_puts(s, "c10"); + c10 = false; + } else { + seq_puts(s, "[c10]"); + c10 = true; + } + + pmc_for_each_mode(mode, pmc) { + if ((BIT(mode) & reg) && !c10) + seq_printf(s, " [%s]", pmc_lpm_modes[mode]); + else + seq_printf(s, " %s", pmc_lpm_modes[mode]); + } + + seq_puts(s, " clear\n"); + + return 0; +} + +static ssize_t pmc_core_lpm_latch_mode_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct pmc_dev *pmcdev = s->private; + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + bool clear = false, c10 = false; + unsigned char buf[8]; + int mode; + u32 reg; + u8 m; + + if (count > sizeof(buf) - 1) + return -EINVAL; + if (copy_from_user(buf, userbuf, count)) + return -EFAULT; + buf[count] = '\0'; + + /* + * Allowed strings are: + * Any enabled substate, e.g. 'S0i2.0' + * 'c10' + * 'clear' + */ + mode = sysfs_match_string(pmc_lpm_modes, buf); + + /* Check string matches enabled mode */ + pmc_for_each_mode(m, pmc) + if (mode == m) + break; + + if (mode != m || mode < 0) { + if (sysfs_streq(buf, "clear")) + clear = true; + else if (sysfs_streq(buf, "c10")) + c10 = true; + else + return -EINVAL; + } + + if (clear) { + guard(mutex)(&pmcdev->lock); + + reg = pmc_core_reg_read(pmc, pmc->map->etr3_offset); + reg |= ETR3_CLEAR_LPM_EVENTS; + pmc_core_reg_write(pmc, pmc->map->etr3_offset, reg); + + return count; + } + + if (c10) { + guard(mutex)(&pmcdev->lock); + + reg = pmc_core_reg_read(pmc, pmc->map->lpm_sts_latch_en_offset); + reg &= ~LPM_STS_LATCH_MODE; + pmc_core_reg_write(pmc, pmc->map->lpm_sts_latch_en_offset, reg); + + return count; + } + + /* + * For LPM mode latching we set the latch enable bit and selected mode + * and clear everything else. + */ + reg = LPM_STS_LATCH_MODE | BIT(mode); + guard(mutex)(&pmcdev->lock); + pmc_core_reg_write(pmc, pmc->map->lpm_sts_latch_en_offset, reg); + + return count; +} +DEFINE_PMC_CORE_ATTR_WRITE(pmc_core_lpm_latch_mode); + +static int pmc_core_pkgc_show(struct seq_file *s, void *unused) +{ + struct pmc *pmc = s->private; + const struct pmc_bit_map *map = pmc->map->msr_sts; + u64 pcstate_count; + unsigned int index; + + for (index = 0; map[index].name ; index++) { + if (rdmsrq_safe(map[index].bit_mask, &pcstate_count)) + continue; + + pcstate_count *= 1000; + do_div(pcstate_count, tsc_khz); + seq_printf(s, "%-8s : %llu\n", map[index].name, + pcstate_count); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(pmc_core_pkgc); + +static bool pmc_core_pri_verify(u32 lpm_pri, u8 *mode_order) +{ + unsigned int i, j; + + if (!lpm_pri) + return false; + /* + * Each byte contains the priority level for 2 modes (7:4 and 3:0). + * In a 32 bit register this allows for describing 8 modes. Store the + * levels and look for values out of range. + */ + for (i = 0; i < 8; i++) { + int level = lpm_pri & GENMASK(3, 0); + + if (level >= LPM_MAX_NUM_MODES) + return false; + + mode_order[i] = level; + lpm_pri >>= 4; + } + + /* Check that we have unique values */ + for (i = 0; i < LPM_MAX_NUM_MODES - 1; i++) + for (j = i + 1; j < LPM_MAX_NUM_MODES; j++) + if (mode_order[i] == mode_order[j]) + return false; + + return true; +} + +static void pmc_core_pmc_get_low_power_modes(struct pmc_dev *pmcdev, struct pmc *pmc) +{ + u8 pri_order[LPM_MAX_NUM_MODES] = LPM_DEFAULT_PRI; + u8 mode_order[LPM_MAX_NUM_MODES]; + u32 lpm_pri; + u32 lpm_en; + u8 mode; + unsigned int i; + int p; + + /* Use LPM Maps to indicate support for substates */ + if (!pmc->map->lpm_num_maps) + return; + + lpm_en = pmc_core_reg_read(pmc, pmc->map->lpm_en_offset); + /* For MTL, BIT 31 is not an lpm mode but a enable bit. + * Lower byte is enough to cover the number of lpm modes for all + * platforms and hence mask the upper 3 bytes. + */ + pmc->num_lpm_modes = hweight32(lpm_en & 0xFF); + + /* Read 32 bit LPM_PRI register */ + lpm_pri = pmc_core_reg_read(pmc, pmc->map->lpm_priority_offset); + + /* + * If lpm_pri value passes verification, then override the default + * modes here. Otherwise stick with the default. + */ + if (pmc_core_pri_verify(lpm_pri, mode_order)) + /* Get list of modes in priority order */ + for (mode = 0; mode < LPM_MAX_NUM_MODES; mode++) + pri_order[mode_order[mode]] = mode; + else + dev_dbg(&pmcdev->pdev->dev, + "Assuming a default substate order for this platform\n"); + + /* + * Loop through all modes from lowest to highest priority, + * and capture all enabled modes in order + */ + i = 0; + for (p = LPM_MAX_NUM_MODES - 1; p >= 0; p--) { + u8 mode = pri_order[p]; + + if (!(BIT(mode) & lpm_en)) + continue; + + pmc->lpm_en_modes[i++] = mode; + } +} + +static void pmc_core_get_low_power_modes(struct pmc_dev *pmcdev) +{ + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); pmc_idx++) { + struct pmc *pmc; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + pmc_core_pmc_get_low_power_modes(pmcdev, pmc); + } +} + +int get_primary_reg_base(struct pmc *pmc) +{ + u64 slp_s0_addr; + + if (lpit_read_residency_count_address(&slp_s0_addr)) { + pmc->base_addr = PMC_BASE_ADDR_DEFAULT; + + if (page_is_ram(PHYS_PFN(pmc->base_addr))) + return -ENODEV; + } else { + pmc->base_addr = slp_s0_addr - pmc->map->slp_s0_offset; + } + + pmc->regbase = ioremap(pmc->base_addr, pmc->map->regmap_length); + if (!pmc->regbase) + return -ENOMEM; + return 0; +} + +static struct telem_endpoint *pmc_core_register_endpoint(struct pci_dev *pcidev, u32 *guids) +{ + struct telem_endpoint *ep; + unsigned int i; + + for (i = 0; guids[i]; i++) { + ep = pmt_telem_find_and_register_endpoint(&pcidev->dev, guids[i], 0); + if (!IS_ERR(ep)) + return ep; + } + return ERR_PTR(-ENODEV); +} + +void pmc_core_punit_pmt_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + struct telem_endpoint *ep; + + struct pci_dev *pcidev __free(pci_dev_put) = pci_get_domain_bus_and_slot(0, 0, + PCI_DEVFN(10, 0)); + if (!pcidev) { + dev_err(&pmcdev->pdev->dev, "PUNIT PMT device not found."); + return; + } + + if (pmc_dev_info->dmu_guids) { + ep = pmc_core_register_endpoint(pcidev, pmc_dev_info->dmu_guids); + if (IS_ERR(ep)) { + dev_err(&pmcdev->pdev->dev, + "pmc_core: couldn't get DMU telem endpoint %ld", + PTR_ERR(ep)); + return; + } + + pmcdev->punit_ep = ep; + pmcdev->die_c6_offset = pmc_dev_info->die_c6_offset; + } + + if (pmc_dev_info->pc_guid) { + ep = pmt_telem_find_and_register_endpoint(&pcidev->dev, pmc_dev_info->pc_guid, 0); + if (IS_ERR(ep)) { + dev_err(&pmcdev->pdev->dev, + "pmc_core: couldn't get Package C-state telem endpoint %ld", + PTR_ERR(ep)); + return; + } + + pmcdev->pc_ep = ep; + pmcdev->pkgc_ltr_blocker_counters = pmc_dev_info->pkgc_ltr_blocker_counters; + pmcdev->pkgc_ltr_blocker_offset = pmc_dev_info->pkgc_ltr_blocker_offset; + pmcdev->pkgc_blocker_counters = pmc_dev_info->pkgc_blocker_counters; + pmcdev->pkgc_blocker_offset = pmc_dev_info->pkgc_blocker_offset; + } +} + +void pmc_core_set_device_d3(unsigned int device) +{ + struct pci_dev *pcidev; + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, device, NULL); + if (pcidev) { + if (!device_trylock(&pcidev->dev)) { + pci_dev_put(pcidev); + return; + } + if (!pcidev->dev.driver) { + dev_info(&pcidev->dev, "Setting to D3hot\n"); + pci_set_power_state(pcidev, PCI_D3hot); + } + device_unlock(&pcidev->dev); + pci_dev_put(pcidev); + } +} + +static bool pmc_core_is_pson_residency_enabled(struct pmc_dev *pmcdev) +{ + struct platform_device *pdev = pmcdev->pdev; + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + u8 val; + + if (!adev) + return false; + + if (fwnode_property_read_u8(acpi_fwnode_handle(adev), + "intel-cec-pson-switching-enabled-in-s0", + &val)) + return false; + + return val == 1; +} + +static void pmc_core_dbgfs_unregister(struct pmc_dev *pmcdev) +{ + debugfs_remove_recursive(pmcdev->dbgfs_dir); +} + +static void pmc_core_dbgfs_register(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + struct pmc *primary_pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + struct dentry *dir; + + dir = debugfs_create_dir("pmc_core", NULL); + pmcdev->dbgfs_dir = dir; + + debugfs_create_file("slp_s0_residency_usec", 0444, dir, primary_pmc, + &pmc_core_dev_state); + + if (primary_pmc->map->pfear_sts) + debugfs_create_file("pch_ip_power_gating_status", 0444, dir, + pmcdev, &pmc_core_ppfear_fops); + + debugfs_create_file("ltr_ignore", 0644, dir, pmcdev, + &pmc_core_ltr_ignore_fops); + + debugfs_create_file("ltr_restore", 0200, dir, pmcdev, &pmc_core_ltr_restore_fops); + + debugfs_create_file("ltr_show", 0444, dir, pmcdev, &pmc_core_ltr_fops); + + if (primary_pmc->map->s0ix_blocker_maps) + debugfs_create_file("s0ix_blocker", 0444, dir, pmcdev, &pmc_core_s0ix_blocker_fops); + + debugfs_create_file("package_cstate_show", 0444, dir, primary_pmc, + &pmc_core_pkgc_fops); + + if (primary_pmc->map->pll_sts) + debugfs_create_file("pll_status", 0444, dir, pmcdev, + &pmc_core_pll_fops); + + if (primary_pmc->map->mphy_sts) + debugfs_create_file("mphy_core_lanes_power_gating_status", + 0444, dir, pmcdev, + &pmc_core_mphy_pg_fops); + + if (primary_pmc->map->slps0_dbg_maps) { + debugfs_create_file("slp_s0_debug_status", 0444, + dir, pmcdev, + &pmc_core_slps0_dbg_fops); + + debugfs_create_bool("slp_s0_dbg_latch", 0644, + dir, &slps0_dbg_latch); + } + + if (primary_pmc->map->lpm_en_offset) { + debugfs_create_file("substate_residencies", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_substate_res_fops); + } + + if (primary_pmc->map->lpm_status_offset) { + debugfs_create_file("substate_status_registers", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_substate_sts_regs_fops); + debugfs_create_file("substate_live_status_registers", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_substate_l_sts_regs_fops); + debugfs_create_file("lpm_latch_mode", 0644, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_lpm_latch_mode_fops); + } + + if (primary_pmc->lpm_req_regs) { + debugfs_create_file("substate_requirements", 0444, + pmcdev->dbgfs_dir, pmcdev, + pmc_dev_info->sub_req_show); + } + + if (primary_pmc->map->pson_residency_offset && pmc_core_is_pson_residency_enabled(pmcdev)) { + debugfs_create_file("pson_residency_usec", 0444, + pmcdev->dbgfs_dir, primary_pmc, &pmc_core_pson_residency); + } + + if (pmcdev->punit_ep) { + debugfs_create_file("die_c6_us_show", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_die_c6_us_fops); + } + + if (pmcdev->pc_ep) { + debugfs_create_file("pkgc_ltr_blocker_show", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_pkgc_ltr_blocker_fops); + debugfs_create_file("pkgc_blocker_residency_show", 0444, + pmcdev->dbgfs_dir, pmcdev, + &pmc_core_pkgc_blocker_residency_fops); + } + +} + +/* + * This function retrieves low power mode requirement data from PMC Low + * Power Mode (LPM) table. + * + * In telemetry space, the LPM table contains a 4 byte header followed + * by 8 consecutive mode blocks (one for each LPM mode). Each block + * has a 4 byte header followed by a set of registers that describe the + * IP state requirements for the given mode. The IP mapping is platform + * specific but the same for each block, making for easy analysis. + * Platforms only use a subset of the space to track the requirements + * for their IPs. Callers provide the requirement registers they use as + * a list of indices. Each requirement register is associated with an + * IP map that's maintained by the caller. + * + * Header + * +----+----------------------------+----------------------------+ + * | 0 | REVISION | ENABLED MODES | + * +----+--------------+-------------+-------------+--------------+ + * + * Low Power Mode 0 Block + * +----+--------------+-------------+-------------+--------------+ + * | 1 | SUB ID | SIZE | MAJOR | MINOR | + * +----+--------------+-------------+-------------+--------------+ + * | 2 | LPM0 Requirements 0 | + * +----+---------------------------------------------------------+ + * | | ... | + * +----+---------------------------------------------------------+ + * | 29 | LPM0 Requirements 27 | + * +----+---------------------------------------------------------+ + * + * ... + * + * Low Power Mode 7 Block + * +----+--------------+-------------+-------------+--------------+ + * | | SUB ID | SIZE | MAJOR | MINOR | + * +----+--------------+-------------+-------------+--------------+ + * | 60 | LPM7 Requirements 0 | + * +----+---------------------------------------------------------+ + * | | ... | + * +----+---------------------------------------------------------+ + * | 87 | LPM7 Requirements 27 | + * +----+---------------------------------------------------------+ + * + */ +int pmc_core_pmt_get_lpm_req(struct pmc_dev *pmcdev, struct pmc *pmc, struct telem_endpoint *ep) +{ + const u8 *lpm_indices; + int num_maps, mode_offset = 0; + int ret, lpm_size; + u8 mode; + + lpm_indices = pmc->map->lpm_reg_index; + num_maps = pmc->map->lpm_num_maps; + lpm_size = LPM_MAX_NUM_MODES * num_maps; + + pmc->lpm_req_regs = devm_kzalloc(&pmcdev->pdev->dev, + lpm_size * sizeof(u32), + GFP_KERNEL); + if (!pmc->lpm_req_regs) + return -ENOMEM; + + mode_offset = LPM_HEADER_OFFSET + LPM_MODE_OFFSET; + pmc_for_each_mode(mode, pmc) { + u32 *req_offset = pmc->lpm_req_regs + (mode * num_maps); + int m; + + for (m = 0; m < num_maps; m++) { + u8 sample_id = lpm_indices[m] + mode_offset; + + ret = pmt_telem_read32(ep, sample_id, req_offset, 1); + if (ret) { + dev_err(&pmcdev->pdev->dev, + "couldn't read Low Power Mode requirements: %d\n", ret); + return ret; + } + ++req_offset; + } + mode_offset += LPM_REG_COUNT + LPM_MODE_OFFSET; + } + return ret; +} + +int pmc_core_pmt_get_blk_sub_req(struct pmc_dev *pmcdev, struct pmc *pmc, + struct telem_endpoint *ep) +{ + u32 num_blocker, sample_offset; + unsigned int index; + u32 *req_offset; + int ret; + + num_blocker = pmc->map->num_s0ix_blocker; + sample_offset = pmc->map->blocker_req_offset; + + pmc->lpm_req_regs = devm_kcalloc(&pmcdev->pdev->dev, num_blocker, + sizeof(u32), GFP_KERNEL); + if (!pmc->lpm_req_regs) + return -ENOMEM; + + req_offset = pmc->lpm_req_regs; + for (index = 0; index < num_blocker; index++, req_offset++) { + ret = pmt_telem_read32(ep, index + sample_offset, req_offset, 1); + if (ret) { + dev_err(&pmcdev->pdev->dev, + "couldn't read Low Power Mode requirements: %d\n", ret); + return ret; + } + } + return 0; +} + +static int pmc_core_get_telem_info(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + struct telem_endpoint *ep; + unsigned int pmc_idx; + int ret; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc; + u16 devid; + + pmc = pmcdev->pmcs[pmc_idx]; + if (!pmc) + continue; + + if (!pmc->map->lpm_req_guid) + return -ENXIO; + + if (pmc_dev_info->ssram_hidden) + devid = pmcdev->pmcs[PMC_IDX_MAIN]->devid; + else + devid = pmc->devid; + + struct pci_dev *pcidev __free(pci_dev_put) = + pci_get_device(PCI_VENDOR_ID_INTEL, devid, NULL); + if (!pcidev) + return -ENODEV; + + ep = pmt_telem_find_and_register_endpoint(&pcidev->dev, pmc->map->lpm_req_guid, 0); + if (IS_ERR(ep)) { + dev_dbg(&pmcdev->pdev->dev, "couldn't get telem endpoint %pe", ep); + return -EPROBE_DEFER; + } + + ret = pmc_dev_info->sub_req(pmcdev, pmc, ep); + pmt_telem_unregister_endpoint(ep); + if (ret) + return ret; + } + + return 0; +} + +static const struct pmc_reg_map *pmc_core_find_regmap(struct pmc_info *list, u16 devid) +{ + for (; list->map; ++list) + if (devid == list->devid) + return list->map; + + return NULL; +} + +static int pmc_core_pmc_add(struct pmc_dev *pmcdev, unsigned int pmc_idx) + +{ + struct pmc_ssram_telemetry pmc_ssram_telemetry; + const struct pmc_reg_map *map; + struct pmc *pmc; + int ret; + + ret = pmc_ssram_telemetry_get_pmc_info(pmc_idx, &pmc_ssram_telemetry); + if (ret) + return ret; + + map = pmc_core_find_regmap(pmcdev->regmap_list, pmc_ssram_telemetry.devid); + if (!map) + return -ENODEV; + + pmc = pmcdev->pmcs[pmc_idx]; + /* Memory for primary PMC has been allocated */ + if (!pmc) { + pmc = devm_kzalloc(&pmcdev->pdev->dev, sizeof(*pmc), GFP_KERNEL); + if (!pmc) + return -ENOMEM; + } + + pmc->map = map; + pmc->base_addr = pmc_ssram_telemetry.base_addr; + pmc->devid = pmc_ssram_telemetry.devid; + pmc->regbase = ioremap(pmc->base_addr, pmc->map->regmap_length); + + if (!pmc->regbase) { + devm_kfree(&pmcdev->pdev->dev, pmc); + return -ENOMEM; + } + + pmcdev->pmcs[pmc_idx] = pmc; + + return 0; +} + +static int pmc_core_ssram_get_reg_base(struct pmc_dev *pmcdev, u8 num_pmcs, const u8 *pmc_list) +{ + unsigned int i; + int ret; + + for (i = 0; i < num_pmcs; ++i) { + /* Non-MAIN PMCs are allowed to fail */ + ret = pmc_core_pmc_add(pmcdev, pmc_list[i]); + if (ret && (pmc_list[i] == PMC_IDX_MAIN)) + return ret; + } + + return 0; +} + +/* + * When supported, ssram init is used to achieve all available PMCs. + * If ssram init fails, this function uses legacy method to at least get the + * primary PMC. + */ +int generic_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + bool ssram; + int ret; + + pmcdev->suspend = pmc_dev_info->suspend; + pmcdev->resume = pmc_dev_info->resume; + + ssram = pmc_dev_info->regmap_list != NULL; + if (ssram) { + pmcdev->regmap_list = pmc_dev_info->regmap_list; + ret = pmc_core_ssram_get_reg_base(pmcdev, + pmc_dev_info->num_pmcs, + pmc_dev_info->pmc_list); + /* + * EAGAIN error code indicates Intel PMC SSRAM Telemetry driver + * has not finished probe and PMC info is not available yet. Try + * again later. + */ + if (ret == -EAGAIN) + return -EPROBE_DEFER; + + if (ret) { + dev_warn(&pmcdev->pdev->dev, + "Failed to get PMC info from SSRAM, %d, using legacy init\n", ret); + ssram = false; + } + } + + if (!ssram) { + pmc->map = pmc_dev_info->map; + ret = get_primary_reg_base(pmc); + if (ret) + return ret; + } + + pmc_core_get_low_power_modes(pmcdev); + if (pmc_dev_info->dmu_guids || pmc_dev_info->pc_guid) + pmc_core_punit_pmt_init(pmcdev, pmc_dev_info); + + if (ssram) { + ret = pmc_core_get_telem_info(pmcdev, pmc_dev_info); + if (ret) + goto unmap_regbase; + } + + return 0; + +unmap_regbase: + for (unsigned int pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + + if (pmc && pmc->regbase) + iounmap(pmc->regbase); + } + + if (pmcdev->punit_ep) + pmt_telem_unregister_endpoint(pmcdev->punit_ep); + + if (pmcdev->pc_ep) + pmt_telem_unregister_endpoint(pmcdev->pc_ep); + + return ret; +} + +static const struct x86_cpu_id intel_pmc_core_ids[] = { + X86_MATCH_VFM(INTEL_SKYLAKE_L, &spt_pmc_dev), + X86_MATCH_VFM(INTEL_SKYLAKE, &spt_pmc_dev), + X86_MATCH_VFM(INTEL_KABYLAKE_L, &spt_pmc_dev), + X86_MATCH_VFM(INTEL_KABYLAKE, &spt_pmc_dev), + X86_MATCH_VFM(INTEL_CANNONLAKE_L, &cnp_pmc_dev), + X86_MATCH_VFM(INTEL_ICELAKE_L, &icl_pmc_dev), + X86_MATCH_VFM(INTEL_ICELAKE_NNPI, &icl_pmc_dev), + X86_MATCH_VFM(INTEL_COMETLAKE, &cnp_pmc_dev), + X86_MATCH_VFM(INTEL_COMETLAKE_L, &cnp_pmc_dev), + X86_MATCH_VFM(INTEL_TIGERLAKE_L, &tgl_l_pmc_dev), + X86_MATCH_VFM(INTEL_TIGERLAKE, &tgl_pmc_dev), + X86_MATCH_VFM(INTEL_ATOM_TREMONT, &tgl_l_pmc_dev), + X86_MATCH_VFM(INTEL_ATOM_TREMONT_L, &icl_pmc_dev), + X86_MATCH_VFM(INTEL_ROCKETLAKE, &tgl_pmc_dev), + X86_MATCH_VFM(INTEL_ALDERLAKE_L, &tgl_l_pmc_dev), + X86_MATCH_VFM(INTEL_ATOM_GRACEMONT, &tgl_l_pmc_dev), + X86_MATCH_VFM(INTEL_ALDERLAKE, &adl_pmc_dev), + X86_MATCH_VFM(INTEL_RAPTORLAKE_P, &tgl_l_pmc_dev), + X86_MATCH_VFM(INTEL_RAPTORLAKE, &adl_pmc_dev), + X86_MATCH_VFM(INTEL_RAPTORLAKE_S, &adl_pmc_dev), + X86_MATCH_VFM(INTEL_BARTLETTLAKE, &adl_pmc_dev), + X86_MATCH_VFM(INTEL_METEORLAKE_L, &mtl_pmc_dev), + X86_MATCH_VFM(INTEL_ARROWLAKE, &arl_pmc_dev), + X86_MATCH_VFM(INTEL_ARROWLAKE_H, &arl_h_pmc_dev), + X86_MATCH_VFM(INTEL_ARROWLAKE_U, &arl_h_pmc_dev), + X86_MATCH_VFM(INTEL_LUNARLAKE_M, &lnl_pmc_dev), + X86_MATCH_VFM(INTEL_PANTHERLAKE_L, &ptl_pmc_dev), + X86_MATCH_VFM(INTEL_WILDCATLAKE_L, &wcl_pmc_dev), + X86_MATCH_VFM(INTEL_NOVALAKE, &nvl_s_pmc_dev), + X86_MATCH_VFM(INTEL_NOVALAKE_L, &nvl_h_pmc_dev), + {} +}; + +MODULE_DEVICE_TABLE(x86cpu, intel_pmc_core_ids); + +/* + * This quirk can be used on those platforms where + * the platform BIOS enforces 24Mhz crystal to shutdown + * before PMC can assert SLP_S0#. + */ +static bool xtal_ignore; +static int quirk_xtal_ignore(const struct dmi_system_id *id) +{ + xtal_ignore = true; + return 0; +} + +static void pmc_core_xtal_ignore(struct pmc *pmc) +{ + u32 value; + + value = pmc_core_reg_read(pmc, pmc->map->pm_vric1_offset); + /* 24MHz Crystal Shutdown Qualification Disable */ + value |= SPT_PMC_VRIC1_XTALSDQDIS; + /* Low Voltage Mode Enable */ + value &= ~SPT_PMC_VRIC1_SLPS0LVEN; + pmc_core_reg_write(pmc, pmc->map->pm_vric1_offset, value); +} + +static const struct dmi_system_id pmc_core_dmi_table[] = { + { + .callback = quirk_xtal_ignore, + .ident = "HP Elite x2 1013 G3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Elite x2 1013 G3"), + }, + }, + {} +}; + +static void pmc_core_do_dmi_quirks(struct pmc *pmc) +{ + dmi_check_system(pmc_core_dmi_table); + + if (xtal_ignore) + pmc_core_xtal_ignore(pmc); +} + +static void pmc_core_clean_structure(struct platform_device *pdev) +{ + struct pmc_dev *pmcdev = platform_get_drvdata(pdev); + unsigned int pmc_idx; + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + + if (pmc && pmc->regbase) + iounmap(pmc->regbase); + } + + if (pmcdev->punit_ep) + pmt_telem_unregister_endpoint(pmcdev->punit_ep); + + if (pmcdev->pc_ep) + pmt_telem_unregister_endpoint(pmcdev->pc_ep); + + platform_set_drvdata(pdev, NULL); +} + +static int pmc_core_probe(struct platform_device *pdev) +{ + static bool device_initialized; + struct pmc_dev *pmcdev; + const struct x86_cpu_id *cpu_id; + struct pmc_dev_info *pmc_dev_info; + struct pmc *primary_pmc; + int ret; + + if (device_initialized) + return -ENODEV; + + pmcdev = devm_kzalloc(&pdev->dev, sizeof(*pmcdev), GFP_KERNEL); + if (!pmcdev) + return -ENOMEM; + + pmcdev->crystal_freq = pmc_core_get_crystal_freq(); + + platform_set_drvdata(pdev, pmcdev); + pmcdev->pdev = pdev; + + cpu_id = x86_match_cpu(intel_pmc_core_ids); + if (!cpu_id) + return -ENODEV; + + pmc_dev_info = (struct pmc_dev_info *)cpu_id->driver_data; + + /* Primary PMC */ + primary_pmc = devm_kzalloc(&pdev->dev, sizeof(*primary_pmc), GFP_KERNEL); + if (!primary_pmc) + return -ENOMEM; + pmcdev->pmcs[PMC_IDX_MAIN] = primary_pmc; + + /* The last element in msr_map is empty */ + pmcdev->num_of_pkgc = ARRAY_SIZE(msr_map) - 1; + pmcdev->pkgc_res_cnt = devm_kcalloc(&pdev->dev, + pmcdev->num_of_pkgc, + sizeof(*pmcdev->pkgc_res_cnt), + GFP_KERNEL); + if (!pmcdev->pkgc_res_cnt) + return -ENOMEM; + + ret = devm_mutex_init(&pdev->dev, &pmcdev->lock); + if (ret) + return ret; + + if (pmc_dev_info->init) + ret = pmc_dev_info->init(pmcdev, pmc_dev_info); + else + ret = generic_core_init(pmcdev, pmc_dev_info); + + if (ret) { + platform_set_drvdata(pdev, NULL); + return ret; + } + + pmcdev->pmc_xram_read_bit = pmc_core_check_read_lock_bit(primary_pmc); + pmc_core_do_dmi_quirks(primary_pmc); + + pmc_core_dbgfs_register(pmcdev, pmc_dev_info); + pm_report_max_hw_sleep(FIELD_MAX(SLP_S0_RES_COUNTER_MASK) * + pmc_core_adjust_slp_s0_step(primary_pmc, 1)); + + device_initialized = true; + dev_info(&pdev->dev, " initialized\n"); + + return 0; +} + +static void pmc_core_remove(struct platform_device *pdev) +{ + struct pmc_dev *pmcdev = platform_get_drvdata(pdev); + pmc_core_dbgfs_unregister(pmcdev); + pmc_core_clean_structure(pdev); +} + +static bool warn_on_s0ix_failures; +module_param(warn_on_s0ix_failures, bool, 0644); +MODULE_PARM_DESC(warn_on_s0ix_failures, "Check and warn for S0ix failures"); + +static bool ltr_ignore_all_suspend = true; +module_param(ltr_ignore_all_suspend, bool, 0644); +MODULE_PARM_DESC(ltr_ignore_all_suspend, "Ignore all LTRs during suspend"); + +static __maybe_unused int pmc_core_suspend(struct device *dev) +{ + struct pmc_dev *pmcdev = dev_get_drvdata(dev); + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + unsigned int i; + + if (pmcdev->suspend) + pmcdev->suspend(pmcdev); + + if (ltr_ignore_all_suspend) + pmc_core_ltr_ignore_all(pmcdev); + + /* Check if the syspend will actually use S0ix */ + if (pm_suspend_via_firmware()) + return 0; + + /* Save PKGC residency for checking later */ + for (i = 0; i < pmcdev->num_of_pkgc; i++) { + if (rdmsrq_safe(msr_map[i].bit_mask, &pmcdev->pkgc_res_cnt[i])) + return -EIO; + } + + /* Save S0ix residency for checking later */ + if (pmc_core_dev_state_get(pmc, &pmcdev->s0ix_counter)) + return -EIO; + + return 0; +} + +static inline bool pmc_core_is_deepest_pkgc_failed(struct pmc_dev *pmcdev) +{ + u32 deepest_pkgc_msr = msr_map[pmcdev->num_of_pkgc - 1].bit_mask; + u64 deepest_pkgc_residency; + + if (rdmsrq_safe(deepest_pkgc_msr, &deepest_pkgc_residency)) + return false; + + if (deepest_pkgc_residency == pmcdev->pkgc_res_cnt[pmcdev->num_of_pkgc - 1]) + return true; + + return false; +} + +static inline bool pmc_core_is_s0ix_failed(struct pmc_dev *pmcdev) +{ + u64 s0ix_counter; + + if (pmc_core_dev_state_get(pmcdev->pmcs[PMC_IDX_MAIN], &s0ix_counter)) + return false; + + pm_report_hw_sleep_time((u32)(s0ix_counter - pmcdev->s0ix_counter)); + + if (s0ix_counter == pmcdev->s0ix_counter) + return true; + + return false; +} + +int pmc_core_resume_common(struct pmc_dev *pmcdev) +{ + struct device *dev = &pmcdev->pdev->dev; + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const struct pmc_bit_map **maps = pmc->map->lpm_sts; + int offset = pmc->map->lpm_status_offset; + unsigned int pmc_idx, i; + + /* Check if the syspend used S0ix */ + if (pm_suspend_via_firmware()) + return 0; + + if (!pmc_core_is_s0ix_failed(pmcdev)) + return 0; + + if (!warn_on_s0ix_failures) + return 0; + + if (pmc_core_is_deepest_pkgc_failed(pmcdev)) { + /* S0ix failed because of deepest PKGC entry failure */ + dev_info(dev, "CPU did not enter %s!!! (%s cnt=0x%llx)\n", + msr_map[pmcdev->num_of_pkgc - 1].name, + msr_map[pmcdev->num_of_pkgc - 1].name, + pmcdev->pkgc_res_cnt[pmcdev->num_of_pkgc - 1]); + + for (i = 0; i < pmcdev->num_of_pkgc; i++) { + u64 pc_cnt; + + if (!rdmsrq_safe(msr_map[i].bit_mask, &pc_cnt)) { + dev_info(dev, "Prev %s cnt = 0x%llx, Current %s cnt = 0x%llx\n", + msr_map[i].name, pmcdev->pkgc_res_cnt[i], + msr_map[i].name, pc_cnt); + } + } + return 0; + } + + /* The real interesting case - S0ix failed - lets ask PMC why. */ + dev_warn(dev, "CPU did not enter SLP_S0!!! (S0ix cnt=%llu)\n", + pmcdev->s0ix_counter); + + if (pmc->map->slps0_dbg_maps) + pmc_core_slps0_display(pmc, dev, NULL); + + for (pmc_idx = 0; pmc_idx < ARRAY_SIZE(pmcdev->pmcs); ++pmc_idx) { + struct pmc *pmc = pmcdev->pmcs[pmc_idx]; + + if (!pmc) + continue; + if (pmc->map->lpm_sts) + pmc_core_lpm_display(pmc, dev, NULL, offset, pmc_idx, "STATUS", maps); + } + + return 0; +} + +static __maybe_unused int pmc_core_resume(struct device *dev) +{ + struct pmc_dev *pmcdev = dev_get_drvdata(dev); + + if (ltr_ignore_all_suspend) + pmc_core_ltr_restore_all(pmcdev); + + if (pmcdev->resume) + return pmcdev->resume(pmcdev); + + return pmc_core_resume_common(pmcdev); +} + +static const struct dev_pm_ops pmc_core_pm_ops = { + SET_LATE_SYSTEM_SLEEP_PM_OPS(pmc_core_suspend, pmc_core_resume) +}; + +static const struct acpi_device_id pmc_core_acpi_ids[] = { + {"INT33A1", 0}, /* _HID for Intel Power Engine, _CID PNP0D80*/ + { } +}; +MODULE_DEVICE_TABLE(acpi, pmc_core_acpi_ids); + +static struct platform_driver pmc_core_driver = { + .driver = { + .name = "intel_pmc_core", + .acpi_match_table = ACPI_PTR(pmc_core_acpi_ids), + .pm = &pmc_core_pm_ops, + .dev_groups = pmc_dev_groups, + }, + .probe = pmc_core_probe, + .remove = pmc_core_remove, +}; + +module_platform_driver(pmc_core_driver); + +MODULE_IMPORT_NS("INTEL_PMT_TELEMETRY"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel PMC Core Driver"); diff --git a/drivers/platform/x86/intel/pmc/core.h b/drivers/platform/x86/intel/pmc/core.h new file mode 100644 index 000000000..b4c7399f8 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/core.h @@ -0,0 +1,662 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Intel Core SoC Power Management Controller Header File + * + * Copyright (c) 2016, Intel Corporation. + * All Rights Reserved. + * + * Authors: Rajneesh Bhardwaj + * Vishwanath Somayaji + */ + +#ifndef PMC_CORE_H +#define PMC_CORE_H + +#include +#include +#include +#include +#include +#include + +struct telem_endpoint; + +DEFINE_FREE(pmc_acpi_free, void *, if (_T) ACPI_FREE(_T)) + +#define SLP_S0_RES_COUNTER_MASK GENMASK(31, 0) + +#define PMC_BASE_ADDR_DEFAULT 0xFE000000 +#define MAX_NUM_PMC 3 +#define S0IX_BLK_SIZE 4 + +/* PCH query */ +#define LPM_HEADER_OFFSET 1 +#define LPM_REG_COUNT 28 +#define LPM_MODE_OFFSET 1 + +/* Sunrise Point Power Management Controller PCI Device ID */ +#define SPT_PMC_PCI_DEVICE_ID 0x9d21 +#define SPT_PMC_BASE_ADDR_OFFSET 0x48 +#define SPT_PMC_SLP_S0_RES_COUNTER_OFFSET 0x13c +#define SPT_PMC_PM_CFG_OFFSET 0x18 +#define SPT_PMC_PM_STS_OFFSET 0x1c +#define SPT_PMC_MTPMC_OFFSET 0x20 +#define SPT_PMC_MFPMC_OFFSET 0x38 +#define SPT_PMC_LTR_IGNORE_OFFSET 0x30C +#define SPT_PMC_VRIC1_OFFSET 0x31c +#define SPT_PMC_MPHY_CORE_STS_0 0x1143 +#define SPT_PMC_MPHY_CORE_STS_1 0x1142 +#define SPT_PMC_MPHY_COM_STS_0 0x1155 +#define SPT_PMC_MMIO_REG_LEN 0x1000 +#define SPT_PMC_SLP_S0_RES_COUNTER_STEP 0x68 +#define PMC_BASE_ADDR_MASK ~(SPT_PMC_MMIO_REG_LEN - 1) +#define MTPMC_MASK 0xffff0000 +#define PPFEAR_MAX_NUM_ENTRIES 13 +#define SPT_PPFEAR_NUM_ENTRIES 5 +#define SPT_PMC_READ_DISABLE_BIT 0x16 +#define SPT_PMC_MSG_FULL_STS_BIT 0x18 +#define NUM_RETRIES 100 +#define SPT_NUM_IP_IGN_ALLOWED 17 + +#define SPT_PMC_LTR_CUR_PLT 0x350 +#define SPT_PMC_LTR_CUR_ASLT 0x354 +#define SPT_PMC_LTR_SPA 0x360 +#define SPT_PMC_LTR_SPB 0x364 +#define SPT_PMC_LTR_SATA 0x368 +#define SPT_PMC_LTR_GBE 0x36C +#define SPT_PMC_LTR_XHCI 0x370 +#define SPT_PMC_LTR_RESERVED 0x374 +#define SPT_PMC_LTR_ME 0x378 +#define SPT_PMC_LTR_EVA 0x37C +#define SPT_PMC_LTR_SPC 0x380 +#define SPT_PMC_LTR_AZ 0x384 +#define SPT_PMC_LTR_LPSS 0x38C +#define SPT_PMC_LTR_CAM 0x390 +#define SPT_PMC_LTR_SPD 0x394 +#define SPT_PMC_LTR_SPE 0x398 +#define SPT_PMC_LTR_ESPI 0x39C +#define SPT_PMC_LTR_SCC 0x3A0 +#define SPT_PMC_LTR_ISH 0x3A4 + +/* Sunrise Point: PGD PFET Enable Ack Status Registers */ +enum ppfear_regs { + SPT_PMC_XRAM_PPFEAR0A = 0x590, + SPT_PMC_XRAM_PPFEAR0B, + SPT_PMC_XRAM_PPFEAR0C, + SPT_PMC_XRAM_PPFEAR0D, + SPT_PMC_XRAM_PPFEAR1A, +}; + +#define SPT_PMC_BIT_PMC BIT(0) +#define SPT_PMC_BIT_OPI BIT(1) +#define SPT_PMC_BIT_SPI BIT(2) +#define SPT_PMC_BIT_XHCI BIT(3) +#define SPT_PMC_BIT_SPA BIT(4) +#define SPT_PMC_BIT_SPB BIT(5) +#define SPT_PMC_BIT_SPC BIT(6) +#define SPT_PMC_BIT_GBE BIT(7) + +#define SPT_PMC_BIT_SATA BIT(0) +#define SPT_PMC_BIT_HDA_PGD0 BIT(1) +#define SPT_PMC_BIT_HDA_PGD1 BIT(2) +#define SPT_PMC_BIT_HDA_PGD2 BIT(3) +#define SPT_PMC_BIT_HDA_PGD3 BIT(4) +#define SPT_PMC_BIT_RSVD_0B BIT(5) +#define SPT_PMC_BIT_LPSS BIT(6) +#define SPT_PMC_BIT_LPC BIT(7) + +#define SPT_PMC_BIT_SMB BIT(0) +#define SPT_PMC_BIT_ISH BIT(1) +#define SPT_PMC_BIT_P2SB BIT(2) +#define SPT_PMC_BIT_DFX BIT(3) +#define SPT_PMC_BIT_SCC BIT(4) +#define SPT_PMC_BIT_RSVD_0C BIT(5) +#define SPT_PMC_BIT_FUSE BIT(6) +#define SPT_PMC_BIT_CAMREA BIT(7) + +#define SPT_PMC_BIT_RSVD_0D BIT(0) +#define SPT_PMC_BIT_USB3_OTG BIT(1) +#define SPT_PMC_BIT_EXI BIT(2) +#define SPT_PMC_BIT_CSE BIT(3) +#define SPT_PMC_BIT_CSME_KVM BIT(4) +#define SPT_PMC_BIT_CSME_PMT BIT(5) +#define SPT_PMC_BIT_CSME_CLINK BIT(6) +#define SPT_PMC_BIT_CSME_PTIO BIT(7) + +#define SPT_PMC_BIT_CSME_USBR BIT(0) +#define SPT_PMC_BIT_CSME_SUSRAM BIT(1) +#define SPT_PMC_BIT_CSME_SMT BIT(2) +#define SPT_PMC_BIT_RSVD_1A BIT(3) +#define SPT_PMC_BIT_CSME_SMS2 BIT(4) +#define SPT_PMC_BIT_CSME_SMS1 BIT(5) +#define SPT_PMC_BIT_CSME_RTC BIT(6) +#define SPT_PMC_BIT_CSME_PSF BIT(7) + +#define SPT_PMC_BIT_MPHY_LANE0 BIT(0) +#define SPT_PMC_BIT_MPHY_LANE1 BIT(1) +#define SPT_PMC_BIT_MPHY_LANE2 BIT(2) +#define SPT_PMC_BIT_MPHY_LANE3 BIT(3) +#define SPT_PMC_BIT_MPHY_LANE4 BIT(4) +#define SPT_PMC_BIT_MPHY_LANE5 BIT(5) +#define SPT_PMC_BIT_MPHY_LANE6 BIT(6) +#define SPT_PMC_BIT_MPHY_LANE7 BIT(7) + +#define SPT_PMC_BIT_MPHY_LANE8 BIT(0) +#define SPT_PMC_BIT_MPHY_LANE9 BIT(1) +#define SPT_PMC_BIT_MPHY_LANE10 BIT(2) +#define SPT_PMC_BIT_MPHY_LANE11 BIT(3) +#define SPT_PMC_BIT_MPHY_LANE12 BIT(4) +#define SPT_PMC_BIT_MPHY_LANE13 BIT(5) +#define SPT_PMC_BIT_MPHY_LANE14 BIT(6) +#define SPT_PMC_BIT_MPHY_LANE15 BIT(7) + +#define SPT_PMC_BIT_MPHY_CMN_LANE0 BIT(0) +#define SPT_PMC_BIT_MPHY_CMN_LANE1 BIT(1) +#define SPT_PMC_BIT_MPHY_CMN_LANE2 BIT(2) +#define SPT_PMC_BIT_MPHY_CMN_LANE3 BIT(3) + +#define SPT_PMC_VRIC1_SLPS0LVEN BIT(13) +#define SPT_PMC_VRIC1_XTALSDQDIS BIT(22) + +/* Cannonlake Power Management Controller register offsets */ +#define CNP_PMC_SLPS0_DBG_OFFSET 0x10B4 +#define CNP_PMC_PM_CFG_OFFSET 0x1818 +#define CNP_PMC_SLP_S0_RES_COUNTER_OFFSET 0x193C +#define CNP_PMC_LTR_IGNORE_OFFSET 0x1B0C +/* Cannonlake: PGD PFET Enable Ack Status Register(s) start */ +#define CNP_PMC_HOST_PPFEAR0A 0x1D90 + +#define CNP_PMC_LATCH_SLPS0_EVENTS BIT(31) + +#define CNP_PMC_MMIO_REG_LEN 0x2000 +#define CNP_PPFEAR_NUM_ENTRIES 8 +#define CNP_PMC_READ_DISABLE_BIT 22 +#define CNP_NUM_IP_IGN_ALLOWED 19 +#define CNP_PMC_LTR_CUR_PLT 0x1B50 +#define CNP_PMC_LTR_CUR_ASLT 0x1B54 +#define CNP_PMC_LTR_SPA 0x1B60 +#define CNP_PMC_LTR_SPB 0x1B64 +#define CNP_PMC_LTR_SATA 0x1B68 +#define CNP_PMC_LTR_GBE 0x1B6C +#define CNP_PMC_LTR_XHCI 0x1B70 +#define CNP_PMC_LTR_RESERVED 0x1B74 +#define CNP_PMC_LTR_ME 0x1B78 +#define CNP_PMC_LTR_EVA 0x1B7C +#define CNP_PMC_LTR_SPC 0x1B80 +#define CNP_PMC_LTR_AZ 0x1B84 +#define CNP_PMC_LTR_LPSS 0x1B8C +#define CNP_PMC_LTR_CAM 0x1B90 +#define CNP_PMC_LTR_SPD 0x1B94 +#define CNP_PMC_LTR_SPE 0x1B98 +#define CNP_PMC_LTR_ESPI 0x1B9C +#define CNP_PMC_LTR_SCC 0x1BA0 +#define CNP_PMC_LTR_ISH 0x1BA4 +#define CNP_PMC_LTR_CNV 0x1BF0 +#define CNP_PMC_LTR_EMMC 0x1BF4 +#define CNP_PMC_LTR_UFSX2 0x1BF8 + +#define LTR_DECODED_VAL GENMASK(9, 0) +#define LTR_DECODED_SCALE GENMASK(12, 10) +#define LTR_REQ_SNOOP BIT(15) +#define LTR_REQ_NONSNOOP BIT(31) + +#define ICL_PPFEAR_NUM_ENTRIES 9 +#define ICL_NUM_IP_IGN_ALLOWED 20 +#define ICL_PMC_LTR_WIGIG 0x1BFC +#define ICL_PMC_SLP_S0_RES_COUNTER_STEP 0x64 + +#define LPM_MAX_NUM_MODES 8 +#define LPM_DEFAULT_PRI { 7, 6, 2, 5, 4, 1, 3, 0 } + +#define GET_X2_COUNTER(v) ((v) >> 1) +#define LPM_STS_LATCH_MODE BIT(31) + +#define TGL_PMC_SLP_S0_RES_COUNTER_STEP 0x7A +#define TGL_PMC_LTR_THC0 0x1C04 +#define TGL_PMC_LTR_THC1 0x1C08 +#define TGL_NUM_IP_IGN_ALLOWED 23 +#define TGL_PMC_LPM_RES_COUNTER_STEP_X2 61 /* 30.5us * 2 */ + +#define ADL_PMC_LTR_SPF 0x1C00 +#define ADL_NUM_IP_IGN_ALLOWED 23 +#define ADL_PMC_SLP_S0_RES_COUNTER_OFFSET 0x1098 + +/* + * Tigerlake Power Management Controller register offsets + */ +#define TGL_LPM_STS_LATCH_EN_OFFSET 0x1C34 +#define TGL_LPM_EN_OFFSET 0x1C78 +#define TGL_LPM_RESIDENCY_OFFSET 0x1C80 + +/* Tigerlake Low Power Mode debug registers */ +#define TGL_LPM_STATUS_OFFSET 0x1C3C +#define TGL_LPM_LIVE_STATUS_OFFSET 0x1C5C +#define TGL_LPM_PRI_OFFSET 0x1C7C +#define TGL_LPM_NUM_MAPS 6 + +/* Tigerlake PSON residency register */ +#define TGL_PSON_RESIDENCY_OFFSET 0x18f8 +#define TGL_PSON_RES_COUNTER_STEP 0x7A + +/* Extended Test Mode Register 3 (CNL and later) */ +#define ETR3_OFFSET 0x1048 +#define ETR3_CF9GR BIT(20) +#define ETR3_CF9LOCK BIT(31) + +/* Extended Test Mode Register LPM bits (TGL and later */ +#define ETR3_CLEAR_LPM_EVENTS BIT(28) + +/* Alder Lake Power Management Controller register offsets */ +#define ADL_LPM_EN_OFFSET 0x179C +#define ADL_LPM_RESIDENCY_OFFSET 0x17A4 +#define ADL_LPM_NUM_MODES 2 +#define ADL_LPM_NUM_MAPS 14 + +/* Alder Lake Low Power Mode debug registers */ +#define ADL_LPM_STATUS_OFFSET 0x170C +#define ADL_LPM_PRI_OFFSET 0x17A0 +#define ADL_LPM_STATUS_LATCH_EN_OFFSET 0x1704 +#define ADL_LPM_LIVE_STATUS_OFFSET 0x1764 + +/* Meteor Lake Power Management Controller register offsets */ +#define MTL_LPM_EN_OFFSET 0x1798 +#define MTL_LPM_RESIDENCY_OFFSET 0x17A0 + +/* Meteor Lake Low Power Mode debug registers */ +#define MTL_LPM_PRI_OFFSET 0x179C +#define MTL_LPM_STATUS_LATCH_EN_OFFSET 0x16F8 +#define MTL_LPM_STATUS_OFFSET 0x1700 +#define MTL_LPM_LIVE_STATUS_OFFSET 0x175C +#define MTL_PMC_LTR_IOE_PMC 0x1C0C +#define MTL_PMC_LTR_ESE 0x1BAC +#define MTL_PMC_LTR_RESERVED 0x1BA4 +#define MTL_IOE_PMC_MMIO_REG_LEN 0x23A4 +#define MTL_SOCM_NUM_IP_IGN_ALLOWED 25 +#define MTL_SOC_PMC_MMIO_REG_LEN 0x2708 +#define MTL_PMC_LTR_SPG 0x1B74 +#define ARL_SOCS_PMC_LTR_RESERVED 0x1B88 +#define ARL_SOCS_NUM_IP_IGN_ALLOWED 26 +#define ARL_PMC_LTR_DMI3 0x1BE4 +#define ARL_PCH_PMC_MMIO_REG_LEN 0x2720 + +/* Meteor Lake PGD PFET Enable Ack Status */ +#define MTL_SOCM_PPFEAR_NUM_ENTRIES 8 +#define MTL_IOE_PPFEAR_NUM_ENTRIES 10 +#define ARL_SOCS_PPFEAR_NUM_ENTRIES 9 + +/* Die C6 from PUNIT telemetry */ +#define MTL_PMT_DMU_DIE_C6_OFFSET 15 +#define MTL_PMT_DMU_GUID 0x1A067102 +#define ARL_PMT_DMU_GUID 0x1A06A102 +#define ARL_H_PMT_DMU_GUID 0x1A06A101 + +#define LNL_PMC_MMIO_REG_LEN 0x2708 +#define LNL_PMC_LTR_OSSE 0x1B88 +#define LNL_NUM_IP_IGN_ALLOWED 27 +#define LNL_PPFEAR_NUM_ENTRIES 12 +#define LNL_S0IX_BLOCKER_OFFSET 0x2004 + +/* Panther Lake Power Management Controller register offsets */ +#define PTL_LPM_NUM_MAPS 14 +#define PTL_PMC_LTR_SATA2 0x1B90 +#define PTL_PMC_LTR_PMC 0x1BA8 +#define PTL_PMC_LTR_CUR_ASLT 0x1C28 +#define PTL_PMC_LTR_CUR_PLT 0x1C2C +#define PTL_PCD_PMC_MMIO_REG_LEN 0x31A8 +#define PTL_NUM_S0IX_BLOCKER 106 +#define PTL_BLK_REQ_OFFSET 55 + +/* Wildcat Lake */ +#define WCL_PMC_LTR_RESERVED 0x1B64 +#define WCL_PCD_PMC_MMIO_REG_LEN 0x3178 +#define WCL_NUM_S0IX_BLOCKER 94 +#define WCL_BLK_REQ_OFFSET 50 + +/* Nova Lake */ +#define NVL_PCDH_PPFEAR_NUM_ENTRIES 13 +#define NVL_PCDH_PMC_MMIO_REG_LEN 0x363c +#define NVL_PCDS_PMC_MMIO_REG_LEN 0x3118 +#define NVL_PCHS_PMC_MMIO_REG_LEN 0x30d8 +#define NVL_LPM_PRI_OFFSET 0x17a4 +#define NVL_LPM_EN_OFFSET 0x17a0 +#define NVL_LPM_RESIDENCY_OFFSET 0x17a8 +#define NVL_LPM_LIVE_STATUS_OFFSET 0x1760 +#define NVL_LPM_NUM_MAPS 15 +#define NVL_PCDH_NUM_S0IX_BLOCKER 107 +#define NVL_PCDS_NUM_S0IX_BLOCKER 71 +#define NVL_PCHS_NUM_S0IX_BLOCKER 54 +#define NVL_PCDS_PMC_LTR_RESERVED 0x1bac +#define NVL_PCDH_BLK_REQ_OFFSET 53 +#define NVL_PCDS_BLK_REQ_OFFSET 18 +#define NVL_PCHS_BLK_REQ_OFFSET 46 +#define NVL_PMT_PC_GUID 0x13000101 +#define NVL_PMT_DMU_GUID 0x1a000101 +#define NVL_LTR_BLK_OFFSET 64 +#define NVL_PKGC_BLK_OFFSET 4 +#define NVL_PMT_DMU_DIE_C6_OFFSET 25 + +/* SSRAM PMC Device ID */ +/* LNL */ +#define PMC_DEVID_LNL_SOCM 0xa87f + +/* PTL */ +#define PMC_DEVID_PTL_PCDH 0xe37f +#define PMC_DEVID_PTL_PCDP 0xe47f + +/* WCL */ +#define PMC_DEVID_WCL_PCDN 0x4d7f + +/* ARL */ +#define PMC_DEVID_ARL_SOCM 0x777f +#define PMC_DEVID_ARL_SOCS 0xae7f +#define PMC_DEVID_ARL_IOEP 0x7ecf +#define PMC_DEVID_ARL_PCHS 0x7f27 + +/* MTL */ +#define PMC_DEVID_MTL_SOCM 0x7e7f +#define PMC_DEVID_MTL_IOEP 0x7ecf +#define PMC_DEVID_MTL_IOEM 0x7ebf + +/* NVL */ +#define PMC_DEVID_NVL_PCDH 0xd37e +#define PMC_DEVID_NVL_PCDS 0xd47e +#define PMC_DEVID_NVL_PCHS 0x6e27 + +extern const char *pmc_lpm_modes[]; + +struct pmc_bit_map { + const char *name; + u32 bit_mask; + u8 blk; +}; + +/** + * struct pmc_reg_map - Structure used to define parameter unique to a + PCH family + * @pfear_sts: Maps name of IP block to PPFEAR* bit + * @mphy_sts: Maps name of MPHY lane to MPHY status lane status bit + * @pll_sts: Maps name of PLL to corresponding bit status + * @slps0_dbg_maps: Array of SLP_S0_DBG* registers containing debug info + * @ltr_show_sts: Maps PCH IP Names to their MMIO register offsets + * @s0ix_blocker_maps: Maps name of IP block to S0ix blocker counter + * @slp_s0_offset: PWRMBASE offset to read SLP_S0 residency + * @ltr_ignore_offset: PWRMBASE offset to read/write LTR ignore bit + * @regmap_length: Length of memory to map from PWRMBASE address to access + * @ppfear0_offset: PWRMBASE offset to read PPFEAR* + * @ppfear_buckets: Number of 8 bits blocks to read all IP blocks from + * PPFEAR + * @pm_cfg_offset: PWRMBASE offset to PM_CFG register + * @pm_read_disable_bit: Bit index to read PMC_READ_DISABLE + * @slps0_dbg_offset: PWRMBASE offset to SLP_S0_DEBUG_REG* + * @s0ix_blocker_offset PWRMBASE offset to S0ix blocker counter + * @num_s0ix_blocker: Number of S0ix blockers + * @blocker_req_offset: Telemetry offset to S0ix blocker low power mode substate requirement table + * @lpm_req_guid: Telemetry GUID to read low power mode substate requirement table + * + * Each PCH has unique set of register offsets and bit indexes. This structure + * captures them to have a common implementation. + */ +struct pmc_reg_map { + const struct pmc_bit_map **pfear_sts; + const struct pmc_bit_map *mphy_sts; + const struct pmc_bit_map *pll_sts; + const struct pmc_bit_map **slps0_dbg_maps; + const struct pmc_bit_map *ltr_show_sts; + const struct pmc_bit_map *msr_sts; + const struct pmc_bit_map **lpm_sts; + const struct pmc_bit_map **s0ix_blocker_maps; + const u32 slp_s0_offset; + const int slp_s0_res_counter_step; + const u32 ltr_ignore_offset; + const int regmap_length; + const u32 ppfear0_offset; + const int ppfear_buckets; + const u32 pm_cfg_offset; + const int pm_read_disable_bit; + const u32 slps0_dbg_offset; + const u32 ltr_ignore_max; + const u32 pm_vric1_offset; + const u32 s0ix_blocker_offset; + const u32 num_s0ix_blocker; + const u32 blocker_req_offset; + /* Low Power Mode registers */ + const int lpm_num_maps; + const int lpm_num_modes; + const int lpm_res_counter_step_x2; + const u32 lpm_sts_latch_en_offset; + const u32 lpm_en_offset; + const u32 lpm_priority_offset; + const u32 lpm_residency_offset; + const u32 lpm_status_offset; + const u32 lpm_live_status_offset; + const u32 etr3_offset; + const u8 *lpm_reg_index; + const u32 pson_residency_offset; + const u32 pson_residency_counter_step; + /* GUID for telemetry regions */ + const u32 lpm_req_guid; +}; + +/** + * struct pmc_info - Structure to keep pmc info + * @devid: device id of the pmc device + * @map: pointer to a pmc_reg_map struct that contains platform + * specific attributes + */ +struct pmc_info { + u16 devid; + const struct pmc_reg_map *map; +}; + +/** + * struct pmc - pmc private info structure + * @base_addr: contains pmc base address + * @regbase: pointer to io-remapped memory location + * @map: pointer to pmc_reg_map struct that contains platform + * specific attributes + * @lpm_req_regs: List of substate requirements + * @ltr_ign: Holds LTR ignore data while suspended + * @num_lpm_modes: Count of enabled modes + * @lpm_en_modes: Array of enabled modes from lowest to highest priority + * @devid: Device ID of the SSRAM device + * + * pmc contains info about one power management controller device. + */ +struct pmc { + u64 base_addr; + void __iomem *regbase; + const struct pmc_reg_map *map; + u32 *lpm_req_regs; + u32 ltr_ign; + u8 num_lpm_modes; + u8 lpm_en_modes[LPM_MAX_NUM_MODES]; + u16 devid; +}; + +/** + * struct pmc_dev - pmc device structure + * @devs: pointer to an array of pmc pointers + * @pdev: pointer to platform_device struct + * @crystal_freq: crystal frequency from cpuid + * @dbgfs_dir: path to debugfs interface + * @pmc_xram_read_bit: flag to indicate whether PMC XRAM shadow registers + * used to read MPHY PG and PLL status are available + * @mutex_lock: mutex to complete one transcation + * @pkgc_res_cnt: Array of PKGC residency counters + * @num_of_pkgc: Number of PKGC + * @s0ix_counter: S0ix residency (step adjusted) + * @suspend: Function to perform platform specific suspend + * @resume: Function to perform platform specific resume + * + * @pkgc_ltr_blocker_counters: Array of PKGC LTR blocker counters + * @pkgc_ltr_blocker_offset: Offset to PKGC LTR blockers in telemetry region + * @pkgc_blocker_counters: Array of PKGC blocker counters + * @pkgc_blocker_offset: Offset to PKGC blocker in telemetry region + * + * pmc_dev contains info about power management controller device. + */ +struct pmc_dev { + struct pmc *pmcs[MAX_NUM_PMC]; + struct dentry *dbgfs_dir; + struct platform_device *pdev; + unsigned int crystal_freq; + int pmc_xram_read_bit; + struct mutex lock; /* generic mutex lock for PMC Core */ + + u64 s0ix_counter; + void (*suspend)(struct pmc_dev *pmcdev); + int (*resume)(struct pmc_dev *pmcdev); + + u64 *pkgc_res_cnt; + u8 num_of_pkgc; + + u32 die_c6_offset; + struct telem_endpoint *pc_ep; + struct telem_endpoint *punit_ep; + struct pmc_info *regmap_list; + + const char **pkgc_ltr_blocker_counters; + u32 pkgc_ltr_blocker_offset; + const char **pkgc_blocker_counters; + u32 pkgc_blocker_offset; +}; + +enum pmc_index { + PMC_IDX_MAIN, + PMC_IDX_IOE, + PMC_IDX_PCH, + PMC_IDX_MAX +}; + +/** + * struct pmc_dev_info - Structure to keep PMC device info + * @dmu_guids: List of Die Management Unit GUID + * @pc_guid: GUID for telemetry region to read PKGC blocker info + * @pkgc_ltr_blocker_offset: Offset to PKGC LTR blockers in telemetry region + * @pkgc_blocker_offset:Offset to PKGC blocker in telemetry region + * @num_pmcs: Number of entries in @pmc_list + * @pmc_list: Index list of available PMC + * @regmap_list: Pointer to a list of pmc_info structure that could be + * available for the platform. When set, this field implies + * SSRAM support. + * @map: Pointer to a pmc_reg_map struct that contains platform + * specific attributes of the primary PMC + * @sub_req_show: File operations to show substate requirements + * @pkgc_ltr_blocker_counters: Array of PKGC LTR blocker counters + * @pkgc_blocker_counters: Array of PKGC blocker counters + * @suspend: Function to perform platform specific suspend + * @resume: Function to perform platform specific resume + * @init: Function to perform platform specific init action + * @sub_req: Function to achieve low power mode substate requirements + * @ssram_hidden: Some SSRAM devices are hidden on this platform + * @die_c6_offset: Telemetry offset to read Die C6 residency + */ +struct pmc_dev_info { + u32 *dmu_guids; + u32 pc_guid; + u32 pkgc_ltr_blocker_offset; + u32 pkgc_blocker_offset; + u8 num_pmcs; + const u8 *pmc_list; + struct pmc_info *regmap_list; + const struct pmc_reg_map *map; + const struct file_operations *sub_req_show; + const char **pkgc_ltr_blocker_counters; + const char **pkgc_blocker_counters; + void (*suspend)(struct pmc_dev *pmcdev); + int (*resume)(struct pmc_dev *pmcdev); + int (*init)(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info); + int (*sub_req)(struct pmc_dev *pmcdev, struct pmc *pmc, struct telem_endpoint *ep); + bool ssram_hidden; + u32 die_c6_offset; +}; + +extern const struct pmc_bit_map msr_map[]; +extern const struct pmc_bit_map cnp_pfear_map[]; +extern const struct pmc_bit_map *cnp_slps0_dbg_maps[]; +extern const struct pmc_bit_map cnp_ltr_show_map[]; +extern const struct pmc_reg_map cnp_reg_map; +extern const struct pmc_bit_map tgl_signal_status_map[]; +extern const struct pmc_reg_map adl_reg_map; +extern const struct pmc_bit_map mtl_socm_pfear_map[]; +extern const struct pmc_bit_map mtl_socm_d3_status_0_map[]; +extern const struct pmc_bit_map mtl_socm_d3_status_1_map[]; +extern const struct pmc_bit_map mtl_socm_vnn_req_status_0_map[]; +extern const struct pmc_bit_map mtl_socm_vnn_req_status_1_map[]; +extern const struct pmc_bit_map mtl_socm_vnn_req_status_2_map[]; +extern const struct pmc_bit_map mtl_socm_vnn_misc_status_map[]; +extern const struct pmc_bit_map mtl_socm_signal_status_map[]; +extern const struct pmc_reg_map mtl_socm_reg_map; +extern const struct pmc_reg_map mtl_ioep_reg_map; +extern const struct pmc_bit_map ptl_pcdp_clocksource_status_map[]; +extern const struct pmc_bit_map ptl_pcdp_vnn_req_status_3_map[]; +extern const struct pmc_bit_map ptl_pcdp_signal_status_map[]; +extern const struct pmc_bit_map ptl_pcdp_ltr_show_map[]; + +void pmc_core_get_tgl_lpm_reqs(struct platform_device *pdev); +int pmc_core_send_ltr_ignore(struct pmc_dev *pmcdev, u32 value, int ignore); + +int pmc_core_resume_common(struct pmc_dev *pmcdev); +int get_primary_reg_base(struct pmc *pmc); +void pmc_core_punit_pmt_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info); +void pmc_core_set_device_d3(unsigned int device); + +int generic_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info); + +extern struct pmc_dev_info spt_pmc_dev; +extern struct pmc_dev_info cnp_pmc_dev; +extern struct pmc_dev_info icl_pmc_dev; +extern struct pmc_dev_info tgl_l_pmc_dev; +extern struct pmc_dev_info tgl_pmc_dev; +extern struct pmc_dev_info adl_pmc_dev; +extern struct pmc_dev_info mtl_pmc_dev; +extern struct pmc_dev_info arl_pmc_dev; +extern struct pmc_dev_info arl_h_pmc_dev; +extern struct pmc_dev_info lnl_pmc_dev; +extern struct pmc_dev_info ptl_pmc_dev; +extern struct pmc_dev_info wcl_pmc_dev; +extern struct pmc_dev_info nvl_s_pmc_dev; +extern struct pmc_dev_info nvl_h_pmc_dev; + +void cnl_suspend(struct pmc_dev *pmcdev); +int cnl_resume(struct pmc_dev *pmcdev); +int pmc_core_pmt_get_lpm_req(struct pmc_dev *pmcdev, struct pmc *pmc, struct telem_endpoint *ep); +int pmc_core_pmt_get_blk_sub_req(struct pmc_dev *pmcdev, struct pmc *pmc, + struct telem_endpoint *ep); + +extern const struct file_operations pmc_core_substate_req_regs_fops; +extern const struct file_operations pmc_core_substate_blk_req_fops; + +extern const guid_t intel_vsec_guid; + +#define pmc_for_each_mode(mode, pmc) \ + for (unsigned int __i = 0, __cond; \ + __cond = __i < (pmc)->num_lpm_modes, \ + __cond && ((mode) = (pmc)->lpm_en_modes[__i]), \ + __cond; \ + __i++) + +#define DEFINE_PMC_CORE_ATTR_WRITE(__name) \ +static int __name ## _open(struct inode *inode, struct file *file) \ +{ \ + return single_open(file, __name ## _show, inode->i_private); \ +} \ + \ +static const struct file_operations __name ## _fops = { \ + .owner = THIS_MODULE, \ + .open = __name ## _open, \ + .read = seq_read, \ + .write = __name ## _write, \ + .release = single_release, \ +} + +struct intel_vsec_header; +union acpi_object; + +/* Avoid checkpatch warning */ +typedef u32 (*acpi_disc_t)[PMT_DISC_DWORDS]; + +acpi_disc_t pmc_parse_telem_dsd(union acpi_object *obj, + struct intel_vsec_header *header); +union acpi_object *pmc_find_telem_guid(union acpi_object *dsd); +#endif /* PMC_CORE_H */ diff --git a/drivers/platform/x86/intel/pmc/icl.c b/drivers/platform/x86/intel/pmc/icl.c new file mode 100644 index 000000000..db7ed15bf --- /dev/null +++ b/drivers/platform/x86/intel/pmc/icl.c @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Ice Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include "core.h" + +static const struct pmc_bit_map icl_pfear_map[] = { + {"RES_65", BIT(0)}, + {"RES_66", BIT(1)}, + {"RES_67", BIT(2)}, + {"TAM", BIT(3)}, + {"GBETSN", BIT(4)}, + {"TBTLSX", BIT(5)}, + {"RES_71", BIT(6)}, + {"RES_72", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_icl_pfear_map[] = { + /* + * Check intel_pmc_core_ids[] users of icl_reg_map for + * a list of core SoCs using this. + */ + cnp_pfear_map, + icl_pfear_map, + NULL +}; + +static const struct pmc_reg_map icl_reg_map = { + .pfear_sts = ext_icl_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = ICL_PMC_SLP_S0_RES_COUNTER_STEP, + .slps0_dbg_maps = cnp_slps0_dbg_maps, + .ltr_show_sts = cnp_ltr_show_map, + .msr_sts = msr_map, + .slps0_dbg_offset = CNP_PMC_SLPS0_DBG_OFFSET, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = CNP_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = ICL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = ICL_NUM_IP_IGN_ALLOWED, + .etr3_offset = ETR3_OFFSET, +}; + +struct pmc_dev_info icl_pmc_dev = { + .map = &icl_reg_map, +}; diff --git a/drivers/platform/x86/intel/pmc/lnl.c b/drivers/platform/x86/intel/pmc/lnl.c new file mode 100644 index 000000000..5ff492282 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/lnl.c @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Lunar Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include +#include + +#include "core.h" + +#define SOCM_LPM_REQ_GUID 0x15099748 + +static const u8 LNL_LPM_REG_INDEX[] = {0, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20}; + +static const struct pmc_bit_map lnl_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + /* + * Check intel_pmc_core_ids[] users of cnp_reg_map for + * a list of core SoCs using this. + */ + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", CNP_PMC_LTR_RESERVED}, + + {"ESE", MTL_PMC_LTR_ESE}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + {"OSSE", LNL_PMC_LTR_OSSE}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map lnl_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FUSE_OSSE_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 1}, + {"SPA_PGD0_PG_STS", BIT(4), 1}, + {"SPB_PGD0_PG_STS", BIT(5), 1}, + {"SPR16B0_PGD0_PG_STS", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 1}, + {"SBR8B7_PGD0_PG_STS", BIT(8), 0}, + {"SBR8B6_PGD0_PG_STS", BIT(9), 0}, + {"SBR16B1_PGD0_PG_STS", BIT(10), 0}, + {"SBR8B8_PGD0_PG_STS", BIT(11), 0}, + {"ESE_PGD3_PG_STS", BIT(12), 1}, + {"D2D_DISP_PGD0_PG_STS", BIT(13), 1}, + {"LPSS_PGD0_PG_STS", BIT(14), 1}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"SBR8B2_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(20), 0}, + {"SAFSS_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"D2D_DISP_PGD1_PG_STS", BIT(23), 1}, + {"MPFPW1_PGD0_PG_STS", BIT(24), 0}, + {"XDCI_PGD0_PG_STS", BIT(25), 1}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 1}, + {"PMT_PGD0_PG_STS", BIT(29), 1}, + {"CLINK_PGD0_PG_STS", BIT(30), 1}, + {"PTIO_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map lnl_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 1}, + {"SUSRAM_PGD0_PG_STS", BIT(1), 1}, + {"SMT1_PGD0_PG_STS", BIT(2), 1}, + {"U3FPW1_PGD0_PG_STS", BIT(3), 0}, + {"SMS2_PGD0_PG_STS", BIT(4), 1}, + {"SMS1_PGD0_PG_STS", BIT(5), 1}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"FIA_PG_PGD0_PG_STS", BIT(8), 0}, + {"SBR16B4_PGD0_PG_STS", BIT(9), 0}, + {"P2SB8B_PGD0_PG_STS", BIT(10), 1}, + {"DBG_SBR_PGD0_PG_STS", BIT(11), 0}, + {"SBR8B9_PGD0_PG_STS", BIT(12), 0}, + {"OSSE_SMT1_PGD0_PG_STS", BIT(13), 1}, + {"SBR8B10_PGD0_PG_STS", BIT(14), 0}, + {"SBR16B3_PGD0_PG_STS", BIT(15), 0}, + {"G5FPW1_PGD0_PG_STS", BIT(16), 0}, + {"SBRG_PGD0_PG_STS", BIT(17), 0}, + {"PSF4_PGD0_PG_STS", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"USFX2_PGD0_PG_STS", BIT(20), 1}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"FIACPCB_P5X4_PGD0_PG_STS", BIT(22), 0}, + {"SBR8B3_PGD0_PG_STS", BIT(23), 0}, + {"SBR8B0_PGD0_PG_STS", BIT(24), 0}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"OSSE_HOTHAM_PGD0_PG_STS", BIT(26), 1}, + {"D2D_NOC_PGD2_PG_STS", BIT(27), 1}, + {"SBR8B1_PGD0_PG_STS", BIT(28), 0}, + {"PSF6_PGD0_PG_STS", BIT(29), 0}, + {"PSF7_PGD0_PG_STS", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map lnl_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0), 0}, + {"SBR16B2_PGD0_PG_STS", BIT(1), 0}, + {"D2D_IPU_PGD0_PG_STS", BIT(2), 1}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 1}, + {"D2D_NOC_PGD1_PG_STS", BIT(5), 1}, + {"TBTLSX_PGD0_PG_STS", BIT(6), 1}, + {"THC0_PGD0_PG_STS", BIT(7), 1}, + {"THC1_PGD0_PG_STS", BIT(8), 1}, + {"PMC_PGD0_PG_STS", BIT(9), 0}, + {"SBR8B5_PGD0_PG_STS", BIT(10), 0}, + {"UFSPW1_PGD0_PG_STS", BIT(11), 0}, + {"DBC_PGD0_PG_STS", BIT(12), 0}, + {"TCSS_PGD0_PG_STS", BIT(13), 0}, + {"FIA_P5X4_PGD0_PG_STS", BIT(14), 0}, + {"DISP_PGA_PGD0_PG_STS", BIT(15), 0}, + {"DISP_PSF_PGD0_PG_STS", BIT(16), 0}, + {"PSF0_PGD0_PG_STS", BIT(17), 0}, + {"P2SB16B_PGD0_PG_STS", BIT(18), 1}, + {"ACE_PGD0_PG_STS", BIT(19), 0}, + {"ACE_PGD1_PG_STS", BIT(20), 0}, + {"ACE_PGD2_PG_STS", BIT(21), 0}, + {"ACE_PGD3_PG_STS", BIT(22), 0}, + {"ACE_PGD4_PG_STS", BIT(23), 0}, + {"ACE_PGD5_PG_STS", BIT(24), 0}, + {"ACE_PGD6_PG_STS", BIT(25), 0}, + {"ACE_PGD7_PG_STS", BIT(26), 0}, + {"ACE_PGD8_PG_STS", BIT(27), 0}, + {"ACE_PGD9_PG_STS", BIT(28), 0}, + {"ACE_PGD10_PG_STS", BIT(29), 0}, + {"FIACPCB_PG_PGD0_PG_STS", BIT(30), 0}, + {"OSSE_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map lnl_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 1}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPB_D3_STS", BIT(13), 0}, + {"OSSE_D3_STS", BIT(15), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"PSTH_D3_STS", BIT(21), 0}, + {} +}; + +static const struct pmc_bit_map lnl_d3_status_1_map[] = { + {"OSSE_SMT1_D3_STS", BIT(7), 0}, + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {"UFSX2_D3_STS", BIT(28), 1}, + {"OSSE_HOTHAM_D3_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map lnl_d3_status_2_map[] = { + {"ESE_D3_STS", BIT(0), 0}, + {"CSMERTC_D3_STS", BIT(1), 0}, + {"SUSRAM_D3_STS", BIT(2), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"OSSE_SMT2_D3_STS", BIT(13), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map lnl_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14), 1}, + {"THC1_D3_STS", BIT(15), 1}, + {"OSSE_SMT3_D3_STS", BIT(21), 0}, + {"ACE_D3_STS", BIT(23), 0}, + {} +}; + +static const struct pmc_bit_map lnl_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3), 1}, + {"OSSE_VNN_REQ_STS", BIT(15), 1}, + {"ESPISPI_VNN_REQ_STS", BIT(18), 1}, + {} +}; + +static const struct pmc_bit_map lnl_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4), 1}, + {"OSSE_SMT1_VNN_REQ_STS", BIT(7), 1}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 1}, + {"P2D_VNN_REQ_STS", BIT(18), 1}, + {"GBE_VNN_REQ_STS", BIT(19), 1}, + {"SMB_VNN_REQ_STS", BIT(25), 1}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {} +}; + +static const struct pmc_bit_map lnl_vnn_req_status_2_map[] = { + {"eSE_VNN_REQ_STS", BIT(0), 1}, + {"CSMERTC_VNN_REQ_STS", BIT(1), 1}, + {"CSE_VNN_REQ_STS", BIT(4), 1}, + {"ISH_VNN_REQ_STS", BIT(7), 1}, + {"SMT1_VNN_REQ_STS", BIT(8), 1}, + {"CLINK_VNN_REQ_STS", BIT(14), 1}, + {"SMS1_VNN_REQ_STS", BIT(18), 1}, + {"SMS2_VNN_REQ_STS", BIT(19), 1}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 1}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 1}, + {"GPIOCOM2_VNN_REQ_STS", BIT(22), 0}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 1}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 1}, + {} +}; + +static const struct pmc_bit_map lnl_vnn_req_status_3_map[] = { + {"DISP_SHIM_VNN_REQ_STS", BIT(2), 0}, + {"DTS0_VNN_REQ_STS", BIT(7), 0}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11), 2}, + {} +}; + +static const struct pmc_bit_map lnl_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"PCIE_DEEP_PM_REQ_STS", BIT(3), 0}, + {"PMC_CLK_THROTTLE_EN_REQ_STS", BIT(4), 0}, + {"NPK_VNNAON_REQ_STS", BIT(5), 0}, + {"VNN_SOC_REQ_STS", BIT(6), 1}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"D2D_NOC_CFI_QACTIVE_REQ_STS", BIT(8), 1}, + {"D2D_NOC_GPSB_QACTIVE_REQ_STS", BIT(9), 1}, + {"D2D_NOC_IPU_QACTIVE_REQ_STS", BIT(10), 1}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"PCIE_CLKREQ_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"LPC_CLK_SRC_REQ_STS", BIT(20), 0}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"MPHY_SUS_REQ_STS", BIT(22), 0}, + {"FIA_DEEP_PM_REQ_STS", BIT(23), 0}, + {"UXD_CONNECTED_REQ_STS", BIT(24), 1}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"D2D_NOC_DISP_DDI_QACTIVE_REQ_STS", BIT(26), 1}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_EN_REQ_STS", BIT(29), 1}, + {"WOV_REQ_STS", BIT(30), 0}, + {"D2D_NOC_DISP_EDP_QACTIVE_REQ_STS_31", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map lnl_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0), 0}, + {"AON3_OFF_STS", BIT(1), 1}, + {"AON4_OFF_STS", BIT(2), 1}, + {"AON5_OFF_STS", BIT(3), 1}, + {"AON1_OFF_STS", BIT(4), 0}, + {"MPFPW1_0_PLL_OFF_STS", BIT(6), 1}, + {"USB3_PLL_OFF_STS", BIT(8), 1}, + {"AON3_SPL_OFF_STS", BIT(9), 1}, + {"G5FPW1_PLL_OFF_STS", BIT(15), 1}, + {"XTAL_AGGR_OFF_STS", BIT(17), 1}, + {"USB2_PLL_OFF_STS", BIT(18), 0}, + {"SAF_PLL_OFF_STS", BIT(19), 1}, + {"SE_TCSS_PLL_OFF_STS", BIT(20), 1}, + {"DDI_PLL_OFF_STS", BIT(21), 1}, + {"FILTER_PLL_OFF_STS", BIT(22), 1}, + {"ACE_PLL_OFF_STS", BIT(24), 0}, + {"FABRIC_PLL_OFF_STS", BIT(25), 1}, + {"SOC_PLL_OFF_STS", BIT(26), 1}, + {"REF_OFF_STS", BIT(28), 1}, + {"IMG_OFF_STS", BIT(29), 1}, + {"RTC_PLL_OFF_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map lnl_signal_status_map[] = { + {"LSX_Wake0_STS", BIT(0), 0}, + {"LSX_Wake1_STS", BIT(1), 0}, + {"LSX_Wake2_STS", BIT(2), 0}, + {"LSX_Wake3_STS", BIT(3), 0}, + {"LSX_Wake4_STS", BIT(4), 0}, + {"LSX_Wake5_STS", BIT(5), 0}, + {"LSX_Wake6_STS", BIT(6), 0}, + {"LSX_Wake7_STS", BIT(7), 0}, + {"LPSS_Wake0_STS", BIT(8), 1}, + {"LPSS_Wake1_STS", BIT(9), 1}, + {"Int_Timer_SS_Wake0_STS", BIT(10), 1}, + {"Int_Timer_SS_Wake1_STS", BIT(11), 1}, + {"Int_Timer_SS_Wake2_STS", BIT(12), 1}, + {"Int_Timer_SS_Wake3_STS", BIT(13), 1}, + {"Int_Timer_SS_Wake4_STS", BIT(14), 1}, + {"Int_Timer_SS_Wake5_STS", BIT(15), 1}, + {} +}; + +static const struct pmc_bit_map lnl_rsc_status_map[] = { + {"Memory", 0, 1}, + {"PSF0", 0, 1}, + {"PSF4", 0, 1}, + {"PSF6", 0, 1}, + {"PSF7", 0, 1}, + {"PSF8", 0, 1}, + {"SAF_CFI_LINK", 0, 1}, + {"SBR", 0, 1}, + {} +}; + +static const struct pmc_bit_map *lnl_lpm_maps[] = { + lnl_clocksource_status_map, + lnl_power_gating_status_0_map, + lnl_power_gating_status_1_map, + lnl_power_gating_status_2_map, + lnl_d3_status_0_map, + lnl_d3_status_1_map, + lnl_d3_status_2_map, + lnl_d3_status_3_map, + lnl_vnn_req_status_0_map, + lnl_vnn_req_status_1_map, + lnl_vnn_req_status_2_map, + lnl_vnn_req_status_3_map, + lnl_vnn_misc_status_map, + lnl_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *lnl_blk_maps[] = { + lnl_power_gating_status_0_map, + lnl_power_gating_status_1_map, + lnl_power_gating_status_2_map, + lnl_rsc_status_map, + lnl_vnn_req_status_0_map, + lnl_vnn_req_status_1_map, + lnl_vnn_req_status_2_map, + lnl_vnn_req_status_3_map, + lnl_d3_status_0_map, + lnl_d3_status_1_map, + lnl_d3_status_2_map, + lnl_d3_status_3_map, + lnl_clocksource_status_map, + lnl_vnn_misc_status_map, + lnl_signal_status_map, + NULL +}; + +static const struct pmc_bit_map lnl_pfear_map[] = { + {"PMC_0", BIT(0)}, + {"FUSE_OSSE", BIT(1)}, + {"ESPISPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SBR16B0", BIT(6)}, + {"GBE", BIT(7)}, + + {"SBR8B7", BIT(0)}, + {"SBR8B6", BIT(1)}, + {"SBR16B1", BIT(1)}, + {"SBR8B8", BIT(2)}, + {"ESE", BIT(3)}, + {"SBR8B10", BIT(4)}, + {"D2D_DISP_0", BIT(5)}, + {"LPSS", BIT(6)}, + {"LPC", BIT(7)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"SBR8B2", BIT(2)}, + {"NPK_0", BIT(3)}, + {"D2D_NOC_0", BIT(4)}, + {"SAFSS", BIT(5)}, + {"FUSE", BIT(6)}, + {"D2D_DISP_1", BIT(7)}, + + {"MPFPW1", BIT(0)}, + {"XDCI", BIT(1)}, + {"EXI", BIT(2)}, + {"CSE", BIT(3)}, + {"KVMCC", BIT(4)}, + {"PMT", BIT(5)}, + {"CLINK", BIT(6)}, + {"PTIO", BIT(7)}, + + {"USBR", BIT(0)}, + {"SUSRAM", BIT(1)}, + {"SMT1", BIT(2)}, + {"U3FPW1", BIT(3)}, + {"SMS2", BIT(4)}, + {"SMS1", BIT(5)}, + {"CSMERTC", BIT(6)}, + {"CSMEPSF", BIT(7)}, + + {"FIA_PG", BIT(0)}, + {"SBR16B4", BIT(1)}, + {"P2SB8B", BIT(2)}, + {"DBG_SBR", BIT(3)}, + {"SBR8B9", BIT(4)}, + {"OSSE_SMT1", BIT(5)}, + {"SBR8B10", BIT(6)}, + {"SBR16B3", BIT(7)}, + + {"G5FPW1", BIT(0)}, + {"SBRG", BIT(1)}, + {"PSF4", BIT(2)}, + {"CNVI", BIT(3)}, + {"UFSX2", BIT(4)}, + {"ENDBG", BIT(5)}, + {"FIACPCB_P5X4", BIT(6)}, + {"SBR8B3", BIT(7)}, + + {"SBR8B0", BIT(0)}, + {"NPK_1", BIT(1)}, + {"OSSE_HOTHAM", BIT(2)}, + {"D2D_NOC_2", BIT(3)}, + {"SBR8B1", BIT(4)}, + {"PSF6", BIT(5)}, + {"PSF7", BIT(6)}, + {"FIA_U", BIT(7)}, + + {"PSF8", BIT(0)}, + {"SBR16B2", BIT(1)}, + {"D2D_IPU", BIT(2)}, + {"FIACPCB_U", BIT(3)}, + {"TAM", BIT(4)}, + {"D2D_NOC_1", BIT(5)}, + {"TBTLSX", BIT(6)}, + {"THC0", BIT(7)}, + + {"THC1", BIT(0)}, + {"PMC_1", BIT(1)}, + {"SBR8B5", BIT(2)}, + {"UFSPW1", BIT(3)}, + {"DBC", BIT(4)}, + {"TCSS", BIT(5)}, + {"FIA_P5X4", BIT(6)}, + {"DISP_PGA", BIT(7)}, + + {"DBG_PSF", BIT(0)}, + {"PSF0", BIT(1)}, + {"P2SB16B", BIT(2)}, + {"ACE0", BIT(3)}, + {"ACE1", BIT(4)}, + {"ACE2", BIT(5)}, + {"ACE3", BIT(6)}, + {"ACE4", BIT(7)}, + + {"ACE5", BIT(0)}, + {"ACE6", BIT(1)}, + {"ACE7", BIT(2)}, + {"ACE8", BIT(3)}, + {"ACE9", BIT(4)}, + {"ACE10", BIT(5)}, + {"FIACPCB", BIT(6)}, + {"OSSE", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_lnl_pfear_map[] = { + lnl_pfear_map, + NULL +}; + +static const struct pmc_reg_map lnl_socm_reg_map = { + .pfear_sts = ext_lnl_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = lnl_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = LNL_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = LNL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = lnl_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = lnl_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .lpm_reg_index = LNL_LPM_REG_INDEX, + .lpm_req_guid = SOCM_LPM_REQ_GUID, +}; + +static struct pmc_info lnl_pmc_info_list[] = { + { + .devid = PMC_DEVID_LNL_SOCM, + .map = &lnl_socm_reg_map, + }, + {} +}; + +static const u8 lnl_pmc_list[] = {PMC_IDX_MAIN}; + +#define LNL_NPU_PCI_DEV 0x643e +#define LNL_IPU_PCI_DEV 0x645d + +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void lnl_d3_fixup(void) +{ + pmc_core_set_device_d3(LNL_IPU_PCI_DEV); + pmc_core_set_device_d3(LNL_NPU_PCI_DEV); +} + +static int lnl_resume(struct pmc_dev *pmcdev) +{ + lnl_d3_fixup(); + + return cnl_resume(pmcdev); +} + +static int lnl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + lnl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +struct pmc_dev_info lnl_pmc_dev = { + .num_pmcs = ARRAY_SIZE(lnl_pmc_list), + .pmc_list = lnl_pmc_list, + .regmap_list = lnl_pmc_info_list, + .map = &lnl_socm_reg_map, + .sub_req_show = &pmc_core_substate_req_regs_fops, + .suspend = cnl_suspend, + .resume = lnl_resume, + .init = lnl_core_init, + .sub_req = pmc_core_pmt_get_lpm_req, + .ssram_hidden = true, +}; diff --git a/drivers/platform/x86/intel/pmc/mtl.c b/drivers/platform/x86/intel/pmc/mtl.c new file mode 100644 index 000000000..9abeabd3d --- /dev/null +++ b/drivers/platform/x86/intel/pmc/mtl.c @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Meteor Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include +#include "core.h" + +/* PMC SSRAM PMT Telemetry GUIDS */ +#define SOCP_LPM_REQ_GUID 0x2625030 +#define IOEM_LPM_REQ_GUID 0x4357464 +#define IOEP_LPM_REQ_GUID 0x5077612 + +static const u8 MTL_LPM_REG_INDEX[] = {0, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20}; + +/* + * Die Mapping to Product. + * Product SOCDie IOEDie PCHDie + * MTL-M SOC-M IOE-M None + * MTL-P SOC-M IOE-P None + * MTL-S SOC-S IOE-P PCH-S + */ + +const struct pmc_bit_map mtl_socm_pfear_map[] = { + {"PMC", BIT(0)}, + {"OPI", BIT(1)}, + {"SPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SPC", BIT(6)}, + {"GBE", BIT(7)}, + + {"SATA", BIT(0)}, + {"DSP0", BIT(1)}, + {"DSP1", BIT(2)}, + {"DSP2", BIT(3)}, + {"DSP3", BIT(4)}, + {"SPD", BIT(5)}, + {"LPSS", BIT(6)}, + {"LPC", BIT(7)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"P2SB", BIT(2)}, + {"NPK_VNN", BIT(3)}, + {"SDX", BIT(4)}, + {"SPE", BIT(5)}, + {"FUSE", BIT(6)}, + {"SBR8", BIT(7)}, + + {"RSVD24", BIT(0)}, + {"OTG", BIT(1)}, + {"EXI", BIT(2)}, + {"CSE", BIT(3)}, + {"CSME_KVM", BIT(4)}, + {"CSME_PMT", BIT(5)}, + {"CSME_CLINK", BIT(6)}, + {"CSME_PTIO", BIT(7)}, + + {"CSME_USBR", BIT(0)}, + {"CSME_SUSRAM", BIT(1)}, + {"CSME_SMT1", BIT(2)}, + {"RSVD35", BIT(3)}, + {"CSME_SMS2", BIT(4)}, + {"CSME_SMS", BIT(5)}, + {"CSME_RTC", BIT(6)}, + {"CSME_PSF", BIT(7)}, + + {"SBR0", BIT(0)}, + {"SBR1", BIT(1)}, + {"SBR2", BIT(2)}, + {"SBR3", BIT(3)}, + {"SBR4", BIT(4)}, + {"SBR5", BIT(5)}, + {"RSVD46", BIT(6)}, + {"PSF1", BIT(7)}, + + {"PSF2", BIT(0)}, + {"PSF3", BIT(1)}, + {"PSF4", BIT(2)}, + {"CNVI", BIT(3)}, + {"UFSX2", BIT(4)}, + {"EMMC", BIT(5)}, + {"SPF", BIT(6)}, + {"SBR6", BIT(7)}, + + {"SBR7", BIT(0)}, + {"NPK_AON", BIT(1)}, + {"HDA4", BIT(2)}, + {"HDA5", BIT(3)}, + {"HDA6", BIT(4)}, + {"PSF6", BIT(5)}, + {"RSVD62", BIT(6)}, + {"RSVD63", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_mtl_socm_pfear_map[] = { + mtl_socm_pfear_map, + NULL +}; + +static const struct pmc_bit_map mtl_socm_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"ESE", MTL_PMC_LTR_ESE}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map mtl_socm_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0)}, + {"AON3_OFF_STS", BIT(1)}, + {"AON4_OFF_STS", BIT(2)}, + {"AON5_OFF_STS", BIT(3)}, + {"AON1_OFF_STS", BIT(4)}, + {"XTAL_LVM_OFF_STS", BIT(5)}, + {"MPFPW1_0_PLL_OFF_STS", BIT(6)}, + {"MPFPW1_1_PLL_OFF_STS", BIT(7)}, + {"USB3_PLL_OFF_STS", BIT(8)}, + {"AON3_SPL_OFF_STS", BIT(9)}, + {"MPFPW2_0_PLL_OFF_STS", BIT(12)}, + {"MPFPW3_0_PLL_OFF_STS", BIT(13)}, + {"XTAL_AGGR_OFF_STS", BIT(17)}, + {"USB2_PLL_OFF_STS", BIT(18)}, + {"FILTER_PLL_OFF_STS", BIT(22)}, + {"ACE_PLL_OFF_STS", BIT(24)}, + {"FABRIC_PLL_OFF_STS", BIT(25)}, + {"SOC_PLL_OFF_STS", BIT(26)}, + {"PCIFAB_PLL_OFF_STS", BIT(27)}, + {"REF_PLL_OFF_STS", BIT(28)}, + {"IMG_PLL_OFF_STS", BIT(29)}, + {"RTC_PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0)}, + {"DMI_PGD0_PG_STS", BIT(1)}, + {"ESPISPI_PGD0_PG_STS", BIT(2)}, + {"XHCI_PGD0_PG_STS", BIT(3)}, + {"SPA_PGD0_PG_STS", BIT(4)}, + {"SPB_PGD0_PG_STS", BIT(5)}, + {"SPC_PGD0_PG_STS", BIT(6)}, + {"GBE_PGD0_PG_STS", BIT(7)}, + {"SATA_PGD0_PG_STS", BIT(8)}, + {"PSF13_PGD0_PG_STS", BIT(9)}, + {"SOC_D2D_PGD3_PG_STS", BIT(10)}, + {"MPFPW3_PGD0_PG_STS", BIT(11)}, + {"ESE_PGD0_PG_STS", BIT(12)}, + {"SPD_PGD0_PG_STS", BIT(13)}, + {"LPSS_PGD0_PG_STS", BIT(14)}, + {"LPC_PGD0_PG_STS", BIT(15)}, + {"SMB_PGD0_PG_STS", BIT(16)}, + {"ISH_PGD0_PG_STS", BIT(17)}, + {"P2S_PGD0_PG_STS", BIT(18)}, + {"NPK_PGD0_PG_STS", BIT(19)}, + {"DBG_SBR_PGD0_PG_STS", BIT(20)}, + {"SBRG_PGD0_PG_STS", BIT(21)}, + {"FUSE_PGD0_PG_STS", BIT(22)}, + {"SBR8_PGD0_PG_STS", BIT(23)}, + {"SOC_D2D_PGD2_PG_STS", BIT(24)}, + {"XDCI_PGD0_PG_STS", BIT(25)}, + {"EXI_PGD0_PG_STS", BIT(26)}, + {"CSE_PGD0_PG_STS", BIT(27)}, + {"KVMCC_PGD0_PG_STS", BIT(28)}, + {"PMT_PGD0_PG_STS", BIT(29)}, + {"CLINK_PGD0_PG_STS", BIT(30)}, + {"PTIO_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0)}, + {"SUSRAM_PGD0_PG_STS", BIT(1)}, + {"SMT1_PGD0_PG_STS", BIT(2)}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3)}, + {"SMS2_PGD0_PG_STS", BIT(4)}, + {"SMS1_PGD0_PG_STS", BIT(5)}, + {"CSMERTC_PGD0_PG_STS", BIT(6)}, + {"CSMEPSF_PGD0_PG_STS", BIT(7)}, + {"SBR0_PGD0_PG_STS", BIT(8)}, + {"SBR1_PGD0_PG_STS", BIT(9)}, + {"SBR2_PGD0_PG_STS", BIT(10)}, + {"SBR3_PGD0_PG_STS", BIT(11)}, + {"U3FPW1_PGD0_PG_STS", BIT(12)}, + {"SBR5_PGD0_PG_STS", BIT(13)}, + {"MPFPW1_PGD0_PG_STS", BIT(14)}, + {"UFSPW1_PGD0_PG_STS", BIT(15)}, + {"FIA_X_PGD0_PG_STS", BIT(16)}, + {"SOC_D2D_PGD0_PG_STS", BIT(17)}, + {"MPFPW2_PGD0_PG_STS", BIT(18)}, + {"CNVI_PGD0_PG_STS", BIT(19)}, + {"UFSX2_PGD0_PG_STS", BIT(20)}, + {"ENDBG_PGD0_PG_STS", BIT(21)}, + {"DBG_PSF_PGD0_PG_STS", BIT(22)}, + {"SBR6_PGD0_PG_STS", BIT(23)}, + {"SBR7_PGD0_PG_STS", BIT(24)}, + {"NPK_PGD1_PG_STS", BIT(25)}, + {"FIACPCB_X_PGD0_PG_STS", BIT(26)}, + {"DBC_PGD0_PG_STS", BIT(27)}, + {"FUSEGPSB_PGD0_PG_STS", BIT(28)}, + {"PSF6_PGD0_PG_STS", BIT(29)}, + {"PSF7_PGD0_PG_STS", BIT(30)}, + {"GBETSN1_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0)}, + {"FIA_PGD0_PG_STS", BIT(1)}, + {"SOC_D2D_PGD1_PG_STS", BIT(2)}, + {"FIA_U_PGD0_PG_STS", BIT(3)}, + {"TAM_PGD0_PG_STS", BIT(4)}, + {"GBETSN_PGD0_PG_STS", BIT(5)}, + {"TBTLSX_PGD0_PG_STS", BIT(6)}, + {"THC0_PGD0_PG_STS", BIT(7)}, + {"THC1_PGD0_PG_STS", BIT(8)}, + {"PMC_PGD1_PG_STS", BIT(9)}, + {"GNA_PGD0_PG_STS", BIT(10)}, + {"ACE_PGD0_PG_STS", BIT(11)}, + {"ACE_PGD1_PG_STS", BIT(12)}, + {"ACE_PGD2_PG_STS", BIT(13)}, + {"ACE_PGD3_PG_STS", BIT(14)}, + {"ACE_PGD4_PG_STS", BIT(15)}, + {"ACE_PGD5_PG_STS", BIT(16)}, + {"ACE_PGD6_PG_STS", BIT(17)}, + {"ACE_PGD7_PG_STS", BIT(18)}, + {"ACE_PGD8_PG_STS", BIT(19)}, + {"FIA_PGS_PGD0_PG_STS", BIT(20)}, + {"FIACPCB_PGS_PGD0_PG_STS", BIT(21)}, + {"FUSEPMSB_PGD0_PG_STS", BIT(22)}, + {} +}; + +const struct pmc_bit_map mtl_socm_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3)}, + {"XDCI_D3_STS", BIT(4)}, + {"XHCI_D3_STS", BIT(5)}, + {"SPA_D3_STS", BIT(12)}, + {"SPB_D3_STS", BIT(13)}, + {"SPC_D3_STS", BIT(14)}, + {"SPD_D3_STS", BIT(15)}, + {"ESPISPI_D3_STS", BIT(18)}, + {"SATA_D3_STS", BIT(20)}, + {"PSTH_D3_STS", BIT(21)}, + {"DMI_D3_STS", BIT(22)}, + {} +}; + +const struct pmc_bit_map mtl_socm_d3_status_1_map[] = { + {"GBETSN1_D3_STS", BIT(14)}, + {"GBE_D3_STS", BIT(19)}, + {"ITSS_D3_STS", BIT(23)}, + {"P2S_D3_STS", BIT(24)}, + {"CNVI_D3_STS", BIT(27)}, + {"UFSX2_D3_STS", BIT(28)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_d3_status_2_map[] = { + {"GNA_D3_STS", BIT(0)}, + {"CSMERTC_D3_STS", BIT(1)}, + {"SUSRAM_D3_STS", BIT(2)}, + {"CSE_D3_STS", BIT(4)}, + {"KVMCC_D3_STS", BIT(5)}, + {"USBR0_D3_STS", BIT(6)}, + {"ISH_D3_STS", BIT(7)}, + {"SMT1_D3_STS", BIT(8)}, + {"SMT2_D3_STS", BIT(9)}, + {"SMT3_D3_STS", BIT(10)}, + {"CLINK_D3_STS", BIT(14)}, + {"PTIO_D3_STS", BIT(16)}, + {"PMT_D3_STS", BIT(17)}, + {"SMS1_D3_STS", BIT(18)}, + {"SMS2_D3_STS", BIT(19)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_d3_status_3_map[] = { + {"ESE_D3_STS", BIT(2)}, + {"GBETSN_D3_STS", BIT(13)}, + {"THC0_D3_STS", BIT(14)}, + {"THC1_D3_STS", BIT(15)}, + {"ACE_D3_STS", BIT(23)}, + {} +}; + +const struct pmc_bit_map mtl_socm_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3)}, + {"FIA_VNN_REQ_STS", BIT(17)}, + {"ESPISPI_VNN_REQ_STS", BIT(18)}, + {} +}; + +const struct pmc_bit_map mtl_socm_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4)}, + {"DFXAGG_VNN_REQ_STS", BIT(8)}, + {"EXI_VNN_REQ_STS", BIT(9)}, + {"P2D_VNN_REQ_STS", BIT(18)}, + {"GBE_VNN_REQ_STS", BIT(19)}, + {"SMB_VNN_REQ_STS", BIT(25)}, + {"LPC_VNN_REQ_STS", BIT(26)}, + {} +}; + +const struct pmc_bit_map mtl_socm_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1)}, + {"CSE_VNN_REQ_STS", BIT(4)}, + {"ISH_VNN_REQ_STS", BIT(7)}, + {"SMT1_VNN_REQ_STS", BIT(8)}, + {"CLINK_VNN_REQ_STS", BIT(14)}, + {"SMS1_VNN_REQ_STS", BIT(18)}, + {"SMS2_VNN_REQ_STS", BIT(19)}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20)}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21)}, + {"GPIOCOM2_VNN_REQ_STS", BIT(22)}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23)}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24)}, + {} +}; + +static const struct pmc_bit_map mtl_socm_vnn_req_status_3_map[] = { + {"ESE_VNN_REQ_STS", BIT(2)}, + {"DTS0_VNN_REQ_STS", BIT(7)}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11)}, + {} +}; + +const struct pmc_bit_map mtl_socm_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0)}, + {"TS_OFF_REQ_STS", BIT(1)}, + {"PNDE_MET_REQ_STS", BIT(2)}, + {"PCIE_DEEP_PM_REQ_STS", BIT(3)}, + {"PMC_CLK_THROTTLE_EN_REQ_STS", BIT(4)}, + {"NPK_VNNAON_REQ_STS", BIT(5)}, + {"VNN_SOC_REQ_STS", BIT(6)}, + {"ISH_VNNAON_REQ_STS", BIT(7)}, + {"IOE_COND_MET_S02I2_0_REQ_STS", BIT(8)}, + {"IOE_COND_MET_S02I2_1_REQ_STS", BIT(9)}, + {"IOE_COND_MET_S02I2_2_REQ_STS", BIT(10)}, + {"PLT_GREATER_REQ_STS", BIT(11)}, + {"PCIE_CLKREQ_REQ_STS", BIT(12)}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13)}, + {"PM_SYNC_STATES_REQ_STS", BIT(14)}, + {"EA_REQ_STS", BIT(15)}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16)}, + {"BRK_EV_EN_REQ_STS", BIT(17)}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18)}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19)}, + {"LPC_CLK_SRC_REQ_STS", BIT(20)}, + {"ARC_IDLE_REQ_STS", BIT(21)}, + {"MPHY_SUS_REQ_STS", BIT(22)}, + {"FIA_DEEP_PM_REQ_STS", BIT(23)}, + {"UXD_CONNECTED_REQ_STS", BIT(24)}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25)}, + {"USB2_VNNAON_ACT_REQ_STS", BIT(26)}, + {"PRE_WAKE0_REQ_STS", BIT(27)}, + {"PRE_WAKE1_REQ_STS", BIT(28)}, + {"PRE_WAKE2_EN_REQ_STS", BIT(29)}, + {"WOV_REQ_STS", BIT(30)}, + {"CNVI_V1P05_REQ_STS", BIT(31)}, + {} +}; + +const struct pmc_bit_map mtl_socm_signal_status_map[] = { + {"LSX_Wake0_En_STS", BIT(0)}, + {"LSX_Wake0_Pol_STS", BIT(1)}, + {"LSX_Wake1_En_STS", BIT(2)}, + {"LSX_Wake1_Pol_STS", BIT(3)}, + {"LSX_Wake2_En_STS", BIT(4)}, + {"LSX_Wake2_Pol_STS", BIT(5)}, + {"LSX_Wake3_En_STS", BIT(6)}, + {"LSX_Wake3_Pol_STS", BIT(7)}, + {"LSX_Wake4_En_STS", BIT(8)}, + {"LSX_Wake4_Pol_STS", BIT(9)}, + {"LSX_Wake5_En_STS", BIT(10)}, + {"LSX_Wake5_Pol_STS", BIT(11)}, + {"LSX_Wake6_En_STS", BIT(12)}, + {"LSX_Wake6_Pol_STS", BIT(13)}, + {"LSX_Wake7_En_STS", BIT(14)}, + {"LSX_Wake7_Pol_STS", BIT(15)}, + {"LPSS_Wake0_En_STS", BIT(16)}, + {"LPSS_Wake0_Pol_STS", BIT(17)}, + {"LPSS_Wake1_En_STS", BIT(18)}, + {"LPSS_Wake1_Pol_STS", BIT(19)}, + {"Int_Timer_SS_Wake0_En_STS", BIT(20)}, + {"Int_Timer_SS_Wake0_Pol_STS", BIT(21)}, + {"Int_Timer_SS_Wake1_En_STS", BIT(22)}, + {"Int_Timer_SS_Wake1_Pol_STS", BIT(23)}, + {"Int_Timer_SS_Wake2_En_STS", BIT(24)}, + {"Int_Timer_SS_Wake2_Pol_STS", BIT(25)}, + {"Int_Timer_SS_Wake3_En_STS", BIT(26)}, + {"Int_Timer_SS_Wake3_Pol_STS", BIT(27)}, + {"Int_Timer_SS_Wake4_En_STS", BIT(28)}, + {"Int_Timer_SS_Wake4_Pol_STS", BIT(29)}, + {"Int_Timer_SS_Wake5_En_STS", BIT(30)}, + {"Int_Timer_SS_Wake5_Pol_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map *mtl_socm_lpm_maps[] = { + mtl_socm_clocksource_status_map, + mtl_socm_power_gating_status_0_map, + mtl_socm_power_gating_status_1_map, + mtl_socm_power_gating_status_2_map, + mtl_socm_d3_status_0_map, + mtl_socm_d3_status_1_map, + mtl_socm_d3_status_2_map, + mtl_socm_d3_status_3_map, + mtl_socm_vnn_req_status_0_map, + mtl_socm_vnn_req_status_1_map, + mtl_socm_vnn_req_status_2_map, + mtl_socm_vnn_req_status_3_map, + mtl_socm_vnn_misc_status_map, + mtl_socm_signal_status_map, + NULL +}; + +const struct pmc_reg_map mtl_socm_reg_map = { + .pfear_sts = ext_mtl_socm_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = mtl_socm_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = MTL_SOC_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = MTL_SOCM_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .ltr_ignore_max = MTL_SOCM_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = mtl_socm_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .lpm_reg_index = MTL_LPM_REG_INDEX, + .lpm_req_guid = SOCP_LPM_REQ_GUID, +}; + +static const struct pmc_bit_map mtl_ioep_pfear_map[] = { + {"PMC_0", BIT(0)}, + {"OPI", BIT(1)}, + {"TCSS", BIT(2)}, + {"RSVD3", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SPC", BIT(6)}, + {"IOE_D2D_3", BIT(7)}, + + {"RSVD8", BIT(0)}, + {"RSVD9", BIT(1)}, + {"SPE", BIT(2)}, + {"RSVD11", BIT(3)}, + {"RSVD12", BIT(4)}, + {"SPD", BIT(5)}, + {"ACE_7", BIT(6)}, + {"RSVD15", BIT(7)}, + + {"ACE_0", BIT(0)}, + {"FIACPCB_P", BIT(1)}, + {"P2S", BIT(2)}, + {"RSVD19", BIT(3)}, + {"ACE_8", BIT(4)}, + {"IOE_D2D_0", BIT(5)}, + {"FUSE", BIT(6)}, + {"RSVD23", BIT(7)}, + + {"FIACPCB_P5", BIT(0)}, + {"ACE_3", BIT(1)}, + {"RSF5", BIT(2)}, + {"ACE_2", BIT(3)}, + {"ACE_4", BIT(4)}, + {"RSVD29", BIT(5)}, + {"RSF10", BIT(6)}, + {"MPFPW5", BIT(7)}, + + {"PSF9", BIT(0)}, + {"MPFPW4", BIT(1)}, + {"RSVD34", BIT(2)}, + {"RSVD35", BIT(3)}, + {"RSVD36", BIT(4)}, + {"RSVD37", BIT(5)}, + {"RSVD38", BIT(6)}, + {"RSVD39", BIT(7)}, + + {"SBR0", BIT(0)}, + {"SBR1", BIT(1)}, + {"SBR2", BIT(2)}, + {"SBR3", BIT(3)}, + {"SBR4", BIT(4)}, + {"SBR5", BIT(5)}, + {"RSVD46", BIT(6)}, + {"RSVD47", BIT(7)}, + + {"RSVD48", BIT(0)}, + {"FIA_P5", BIT(1)}, + {"RSVD50", BIT(2)}, + {"RSVD51", BIT(3)}, + {"RSVD52", BIT(4)}, + {"RSVD53", BIT(5)}, + {"RSVD54", BIT(6)}, + {"ACE_1", BIT(7)}, + + {"RSVD56", BIT(0)}, + {"ACE_5", BIT(1)}, + {"RSVD58", BIT(2)}, + {"G5FPW1", BIT(3)}, + {"RSVD60", BIT(4)}, + {"ACE_6", BIT(5)}, + {"RSVD62", BIT(6)}, + {"GBETSN1", BIT(7)}, + + {"RSVD64", BIT(0)}, + {"FIA", BIT(1)}, + {"RSVD66", BIT(2)}, + {"FIA_P", BIT(3)}, + {"TAM", BIT(4)}, + {"GBETSN", BIT(5)}, + {"IOE_D2D_2", BIT(6)}, + {"IOE_D2D_1", BIT(7)}, + + {"SPF", BIT(0)}, + {"PMC_1", BIT(1)}, + {} +}; + +static const struct pmc_bit_map *ext_mtl_ioep_pfear_map[] = { + mtl_ioep_pfear_map, + NULL +}; + +static const struct pmc_bit_map mtl_ioep_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", CNP_PMC_LTR_CAM}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"Reserved", MTL_PMC_LTR_RESERVED}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", CNP_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", CNP_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0)}, + {"AON3_OFF_STS", BIT(1)}, + {"AON4_OFF_STS", BIT(2)}, + {"AON5_OFF_STS", BIT(3)}, + {"AON1_OFF_STS", BIT(4)}, + {"TBT_PLL_OFF_STS", BIT(5)}, + {"TMU_PLL_OFF_STS", BIT(6)}, + {"BCLK_PLL_OFF_STS", BIT(7)}, + {"D2D_PLL_OFF_STS", BIT(8)}, + {"AON3_SPL_OFF_STS", BIT(9)}, + {"MPFPW4_0_PLL_OFF_STS", BIT(12)}, + {"MPFPW5_0_PLL_OFF_STS", BIT(13)}, + {"G5FPW_0_PLL_OFF_STS", BIT(14)}, + {"G5FPW_1_PLL_OFF_STS", BIT(15)}, + {"XTAL_AGGR_OFF_STS", BIT(17)}, + {"FABRIC_PLL_OFF_STS", BIT(25)}, + {"SOC_PLL_OFF_STS", BIT(26)}, + {"REF_PLL_OFF_STS", BIT(28)}, + {"RTC_PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0)}, + {"DMI_PGD0_PG_STS", BIT(1)}, + {"TCSS_PGD0_PG_STS", BIT(2)}, + {"SPA_PGD0_PG_STS", BIT(4)}, + {"SPB_PGD0_PG_STS", BIT(5)}, + {"SPC_PGD0_PG_STS", BIT(6)}, + {"IOE_D2D_PGD3_PG_STS", BIT(7)}, + {"SPE_PGD0_PG_STS", BIT(10)}, + {"SPD_PGD0_PG_STS", BIT(13)}, + {"ACE_PGD7_PG_STS", BIT(14)}, + {"ACE_PGD0_PG_STS", BIT(16)}, + {"FIACPCB_P_PGD0_PG_STS", BIT(17)}, + {"P2S_PGD0_PG_STS", BIT(18)}, + {"ACE_PGD8_PG_STS", BIT(20)}, + {"IOE_D2D_PGD0_PG_STS", BIT(21)}, + {"FUSE_PGD0_PG_STS", BIT(22)}, + {"FIACPCB_P5_PGD0_PG_STS", BIT(24)}, + {"ACE_PGD3_PG_STS", BIT(25)}, + {"PSF5_PGD0_PG_STS", BIT(26)}, + {"ACE_PGD2_PG_STS", BIT(27)}, + {"ACE_PGD4_PG_STS", BIT(28)}, + {"PSF10_PGD0_PG_STS", BIT(30)}, + {"MPFPW5_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_power_gating_status_1_map[] = { + {"PSF9_PGD0_PG_STS", BIT(0)}, + {"MPFPW4_PGD0_PG_STS", BIT(1)}, + {"SBR0_PGD0_PG_STS", BIT(8)}, + {"SBR1_PGD0_PG_STS", BIT(9)}, + {"SBR2_PGD0_PG_STS", BIT(10)}, + {"SBR3_PGD0_PG_STS", BIT(11)}, + {"SBR4_PGD0_PG_STS", BIT(12)}, + {"SBR5_PGD0_PG_STS", BIT(13)}, + {"FIA_P5_PGD0_PG_STS", BIT(17)}, + {"ACE_PGD1_PGD0_PG_STS", BIT(23)}, + {"ACE_PGD5_PGD1_PG_STS", BIT(25)}, + {"G5FPW1_PGD0_PG_STS", BIT(27)}, + {"ACE_PGD6_PG_STS", BIT(29)}, + {"GBETSN1_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_power_gating_status_2_map[] = { + {"FIA_PGD0_PG_STS", BIT(1)}, + {"FIA_P_PGD0_PG_STS", BIT(3)}, + {"TAM_PGD0_PG_STS", BIT(4)}, + {"GBETSN_PGD0_PG_STS", BIT(5)}, + {"IOE_D2D_PGD2_PG_STS", BIT(6)}, + {"IOE_D2D_PGD1_PG_STS", BIT(7)}, + {"SPF_PGD0_PG_STS", BIT(8)}, + {"PMC_PGD1_PG_STS", BIT(9)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_d3_status_0_map[] = { + {"SPF_D3_STS", BIT(0)}, + {"SPA_D3_STS", BIT(12)}, + {"SPB_D3_STS", BIT(13)}, + {"SPC_D3_STS", BIT(14)}, + {"SPD_D3_STS", BIT(15)}, + {"SPE_D3_STS", BIT(16)}, + {"DMI_D3_STS", BIT(22)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_d3_status_1_map[] = { + {"GBETSN1_D3_STS", BIT(14)}, + {"P2S_D3_STS", BIT(24)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_d3_status_2_map[] = { + {} +}; + +static const struct pmc_bit_map mtl_ioep_d3_status_3_map[] = { + {"GBETSN_D3_STS", BIT(13)}, + {"ACE_D3_STS", BIT(23)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_vnn_req_status_0_map[] = { + {"FIA_VNN_REQ_STS", BIT(17)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_vnn_req_status_1_map[] = { + {"DFXAGG_VNN_REQ_STS", BIT(8)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_vnn_req_status_2_map[] = { + {} +}; + +static const struct pmc_bit_map mtl_ioep_vnn_req_status_3_map[] = { + {"DTS0_VNN_REQ_STS", BIT(7)}, + {"DISP_VNN_REQ_STS", BIT(19)}, + {} +}; + +static const struct pmc_bit_map mtl_ioep_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0)}, + {"TS_OFF_REQ_STS", BIT(1)}, + {"PNDE_MET_REQ_STS", BIT(2)}, + {"PCIE_DEEP_PM_REQ_STS", BIT(3)}, + {"PMC_CLK_THROTTLE_EN_REQ_STS", BIT(4)}, + {"NPK_VNNAON_REQ_STS", BIT(5)}, + {"VNN_SOC_REQ_STS", BIT(6)}, + {"USB_DEVICE_ATTACHED_REQ_STS", BIT(8)}, + {"FIA_EXIT_REQ_STS", BIT(9)}, + {"USB2_SUS_PG_REQ_STS", BIT(10)}, + {"PLT_GREATER_REQ_STS", BIT(11)}, + {"PCIE_CLKREQ_REQ_STS", BIT(12)}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13)}, + {"PM_SYNC_STATES_REQ_STS", BIT(14)}, + {"EA_REQ_STS", BIT(15)}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16)}, + {"BRK_EV_EN_REQ_STS", BIT(17)}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18)}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19)}, + {"LPC_CLK_SRC_REQ_STS", BIT(20)}, + {"ARC_IDLE_REQ_STS", BIT(21)}, + {"MPHY_SUS_REQ_STS", BIT(22)}, + {"FIA_DEEP_PM_REQ_STS", BIT(23)}, + {"UXD_CONNECTED_REQ_STS", BIT(24)}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25)}, + {"USB2_VNNAON_ACT_REQ_STS", BIT(26)}, + {"PRE_WAKE0_REQ_STS", BIT(27)}, + {"PRE_WAKE1_REQ_STS", BIT(28)}, + {"PRE_WAKE2_EN_REQ_STS", BIT(29)}, + {"WOV_REQ_STS", BIT(30)}, + {"CNVI_V1P05_REQ_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map *mtl_ioep_lpm_maps[] = { + mtl_ioep_clocksource_status_map, + mtl_ioep_power_gating_status_0_map, + mtl_ioep_power_gating_status_1_map, + mtl_ioep_power_gating_status_2_map, + mtl_ioep_d3_status_0_map, + mtl_ioep_d3_status_1_map, + mtl_ioep_d3_status_2_map, + mtl_ioep_d3_status_3_map, + mtl_ioep_vnn_req_status_0_map, + mtl_ioep_vnn_req_status_1_map, + mtl_ioep_vnn_req_status_2_map, + mtl_ioep_vnn_req_status_3_map, + mtl_ioep_vnn_misc_status_map, + mtl_socm_signal_status_map, + NULL +}; + +const struct pmc_reg_map mtl_ioep_reg_map = { + .regmap_length = MTL_IOE_PMC_MMIO_REG_LEN, + .pfear_sts = ext_mtl_ioep_pfear_map, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = MTL_IOE_PPFEAR_NUM_ENTRIES, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .lpm_sts = mtl_ioep_lpm_maps, + .ltr_show_sts = mtl_ioep_ltr_show_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .ltr_ignore_max = ADL_NUM_IP_IGN_ALLOWED, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_reg_index = MTL_LPM_REG_INDEX, + .lpm_req_guid = IOEP_LPM_REQ_GUID, +}; + +static const struct pmc_bit_map mtl_ioem_pfear_map[] = { + {"PMC_0", BIT(0)}, + {"OPI", BIT(1)}, + {"TCSS", BIT(2)}, + {"RSVD3", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"SPC", BIT(6)}, + {"IOE_D2D_3", BIT(7)}, + + {"RSVD8", BIT(0)}, + {"RSVD9", BIT(1)}, + {"SPE", BIT(2)}, + {"RSVD11", BIT(3)}, + {"RSVD12", BIT(4)}, + {"SPD", BIT(5)}, + {"ACE_7", BIT(6)}, + {"RSVD15", BIT(7)}, + + {"ACE_0", BIT(0)}, + {"FIACPCB_P", BIT(1)}, + {"P2S", BIT(2)}, + {"RSVD19", BIT(3)}, + {"ACE_8", BIT(4)}, + {"IOE_D2D_0", BIT(5)}, + {"FUSE", BIT(6)}, + {"RSVD23", BIT(7)}, + + {"FIACPCB_P5", BIT(0)}, + {"ACE_3", BIT(1)}, + {"RSF5", BIT(2)}, + {"ACE_2", BIT(3)}, + {"ACE_4", BIT(4)}, + {"RSVD29", BIT(5)}, + {"RSF10", BIT(6)}, + {"MPFPW5", BIT(7)}, + + {"PSF9", BIT(0)}, + {"MPFPW4", BIT(1)}, + {"RSVD34", BIT(2)}, + {"RSVD35", BIT(3)}, + {"RSVD36", BIT(4)}, + {"RSVD37", BIT(5)}, + {"RSVD38", BIT(6)}, + {"RSVD39", BIT(7)}, + + {"SBR0", BIT(0)}, + {"SBR1", BIT(1)}, + {"SBR2", BIT(2)}, + {"SBR3", BIT(3)}, + {"SBR4", BIT(4)}, + {"RSVD45", BIT(5)}, + {"RSVD46", BIT(6)}, + {"RSVD47", BIT(7)}, + + {"RSVD48", BIT(0)}, + {"FIA_P5", BIT(1)}, + {"RSVD50", BIT(2)}, + {"RSVD51", BIT(3)}, + {"RSVD52", BIT(4)}, + {"RSVD53", BIT(5)}, + {"RSVD54", BIT(6)}, + {"ACE_1", BIT(7)}, + + {"RSVD56", BIT(0)}, + {"ACE_5", BIT(1)}, + {"RSVD58", BIT(2)}, + {"G5FPW1", BIT(3)}, + {"RSVD60", BIT(4)}, + {"ACE_6", BIT(5)}, + {"RSVD62", BIT(6)}, + {"GBETSN1", BIT(7)}, + + {"RSVD64", BIT(0)}, + {"FIA", BIT(1)}, + {"RSVD66", BIT(2)}, + {"FIA_P", BIT(3)}, + {"TAM", BIT(4)}, + {"GBETSN", BIT(5)}, + {"IOE_D2D_2", BIT(6)}, + {"IOE_D2D_1", BIT(7)}, + + {"SPF", BIT(0)}, + {"PMC_1", BIT(1)}, + {} +}; + +static const struct pmc_bit_map *ext_mtl_ioem_pfear_map[] = { + mtl_ioem_pfear_map, + NULL +}; + +static const struct pmc_bit_map mtl_ioem_power_gating_status_1_map[] = { + {"PSF9_PGD0_PG_STS", BIT(0)}, + {"MPFPW4_PGD0_PG_STS", BIT(1)}, + {"SBR0_PGD0_PG_STS", BIT(8)}, + {"SBR1_PGD0_PG_STS", BIT(9)}, + {"SBR2_PGD0_PG_STS", BIT(10)}, + {"SBR3_PGD0_PG_STS", BIT(11)}, + {"SBR4_PGD0_PG_STS", BIT(12)}, + {"FIA_P5_PGD0_PG_STS", BIT(17)}, + {"ACE_PGD1_PGD0_PG_STS", BIT(23)}, + {"ACE_PGD5_PGD1_PG_STS", BIT(25)}, + {"G5FPW1_PGD0_PG_STS", BIT(27)}, + {"ACE_PGD6_PG_STS", BIT(29)}, + {"GBETSN1_PGD0_PG_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map *mtl_ioem_lpm_maps[] = { + mtl_ioep_clocksource_status_map, + mtl_ioep_power_gating_status_0_map, + mtl_ioem_power_gating_status_1_map, + mtl_ioep_power_gating_status_2_map, + mtl_ioep_d3_status_0_map, + mtl_ioep_d3_status_1_map, + mtl_ioep_d3_status_2_map, + mtl_ioep_d3_status_3_map, + mtl_ioep_vnn_req_status_0_map, + mtl_ioep_vnn_req_status_1_map, + mtl_ioep_vnn_req_status_2_map, + mtl_ioep_vnn_req_status_3_map, + mtl_ioep_vnn_misc_status_map, + mtl_socm_signal_status_map, + NULL +}; + +static const struct pmc_reg_map mtl_ioem_reg_map = { + .regmap_length = MTL_IOE_PMC_MMIO_REG_LEN, + .pfear_sts = ext_mtl_ioem_pfear_map, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = MTL_IOE_PPFEAR_NUM_ENTRIES, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .lpm_sts = mtl_ioem_lpm_maps, + .ltr_show_sts = mtl_ioep_ltr_show_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .ltr_ignore_max = ADL_NUM_IP_IGN_ALLOWED, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_num_maps = ADL_LPM_NUM_MAPS, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_reg_index = MTL_LPM_REG_INDEX, + .lpm_req_guid = IOEM_LPM_REQ_GUID, +}; + +static struct pmc_info mtl_pmc_info_list[] = { + { + .devid = PMC_DEVID_MTL_SOCM, + .map = &mtl_socm_reg_map, + }, + { + .devid = PMC_DEVID_MTL_IOEP, + .map = &mtl_ioep_reg_map, + }, + { + .devid = PMC_DEVID_MTL_IOEM, + .map = &mtl_ioem_reg_map + }, + {} +}; + +static const u8 mtl_pmc_list[] = {PMC_IDX_MAIN, PMC_IDX_IOE}; + +#define MTL_GNA_PCI_DEV 0x7e4c +#define MTL_IPU_PCI_DEV 0x7d19 +#define MTL_VPU_PCI_DEV 0x7d1d +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void mtl_d3_fixup(void) +{ + pmc_core_set_device_d3(MTL_GNA_PCI_DEV); + pmc_core_set_device_d3(MTL_IPU_PCI_DEV); + pmc_core_set_device_d3(MTL_VPU_PCI_DEV); +} + +static int mtl_resume(struct pmc_dev *pmcdev) +{ + mtl_d3_fixup(); + + return cnl_resume(pmcdev); +} + +static int mtl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + mtl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +static u32 MTL_PMT_DMU_GUIDS[] = {MTL_PMT_DMU_GUID, 0x0}; +struct pmc_dev_info mtl_pmc_dev = { + .dmu_guids = MTL_PMT_DMU_GUIDS, + .num_pmcs = ARRAY_SIZE(mtl_pmc_list), + .pmc_list = mtl_pmc_list, + .regmap_list = mtl_pmc_info_list, + .map = &mtl_socm_reg_map, + .sub_req_show = &pmc_core_substate_req_regs_fops, + .suspend = cnl_suspend, + .resume = mtl_resume, + .init = mtl_core_init, + .sub_req = pmc_core_pmt_get_lpm_req, + .ssram_hidden = true, + .die_c6_offset = MTL_PMT_DMU_DIE_C6_OFFSET, +}; diff --git a/drivers/platform/x86/intel/pmc/nvl.c b/drivers/platform/x86/intel/pmc/nvl.c new file mode 100644 index 000000000..8dabf2511 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/nvl.c @@ -0,0 +1,1539 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Nova Lake PCH. + * + * Copyright (c) 2026, Intel Corporation. + */ + +#include +#include + +#include "core.h" + +/* PMC SSRAM PMT Telemetry GUIDS */ +#define PCDH_LPM_REQ_GUID 0x01093101 +#define PCHS_LPM_REQ_GUID 0x01092101 +#define PCDS_LPM_REQ_GUID 0x01091102 + +/* + * Die Mapping to Product. + * Product PCDDie PCHDie + * NVL-H PCD-H None + * NVL-S PCD-S PCH-S + */ + +static const struct pmc_bit_map nvl_pcdh_pfear_map[] = { + {"PMC_PGD0", BIT(0)}, + {"FUSE_OSSE_PGD0", BIT(1)}, + {"SPI_PGD0", BIT(2)}, + {"XHCI_PGD0", BIT(3)}, + {"SPA_PGD0", BIT(4)}, + {"SPB_PGD0", BIT(5)}, + {"MPFPW2_PGD0", BIT(6)}, + {"GBE_PGD0", BIT(7)}, + + {"SBR16B20_PGD0", BIT(0)}, + {"DBG_SBR_PGD0", BIT(1)}, + {"SBR16B7_PGD0", BIT(2)}, + {"STRC_PGD0", BIT(3)}, + {"SBR16B8_PGD0", BIT(4)}, + {"D2D_DISP_PGD1", BIT(5)}, + {"LPSS_PGD0", BIT(6)}, + {"LPC_PGD0", BIT(7)}, + + {"SMB_PGD0", BIT(0)}, + {"ISH_PGD0", BIT(1)}, + {"SBR16B2_PGD0", BIT(2)}, + {"NPK_PGD0", BIT(3)}, + {"D2D_NOC_PGD1", BIT(4)}, + {"DBG_SBR16B_PGD0", BIT(5)}, + {"FUSE_PGD0", BIT(6)}, + {"SBR16B0_PGD0", BIT(7)}, + + {"P2SB0_PGD0", BIT(0)}, + {"OTG_PGD0", BIT(1)}, + {"EXI_PGD0", BIT(2)}, + {"CSE_PGD0", BIT(3)}, + {"CSME_KVM_PGD0", BIT(4)}, + {"CSME_PMT_PGD0", BIT(5)}, + {"CSME_CLINK_PGD0", BIT(6)}, + {"SBR16B21_PGD0", BIT(7)}, + + {"CSME_USBR_PGD0", BIT(0)}, + {"SBR16B22_PGD0", BIT(1)}, + {"CSME_SMT1_PGD0", BIT(2)}, + {"MPFPW1_PGD0", BIT(3)}, + {"CSME_SMS2_PGD0", BIT(4)}, + {"CSME_SMS_PGD0", BIT(5)}, + {"CSME_RTC_PGD0", BIT(6)}, + {"CSMEPSF_PGD0", BIT(7)}, + + {"D2D_NOC_PGD0", BIT(0)}, + {"ESE_PGD0", BIT(1)}, + {"SBR16B6_PGD0", BIT(2)}, + {"P2SB1_PGD0", BIT(3)}, + {"SBR16B3_PGD0", BIT(4)}, + {"OSSE_SMT1_PGD0", BIT(5)}, + {"D2D_DISP_PGD0", BIT(6)}, + {"SNPS_USB2_A_PGD0", BIT(7)}, + + {"U3FPW1_PGD0", BIT(0)}, + {"FIA_X_PGD0", BIT(1)}, + {"PSF4_PGD0", BIT(2)}, + {"CNVI_PGD0", BIT(3)}, + {"UFSX2_PGD0", BIT(4)}, + {"ENDBG_PGD0", BIT(5)}, + {"DBC_PGD0", BIT(6)}, + {"FIA_PG_PGD0", BIT(7)}, + + {"D2D_IPU_PGD0", BIT(0)}, + {"NPK_PGD1", BIT(1)}, + {"FIACPCB_X_PGD0", BIT(2)}, + {"SBR8B4_PGD0", BIT(3)}, + {"DBG_PSF_PGD0", BIT(4)}, + {"PSF6_PGD0", BIT(5)}, + {"UFSPW1_PGD0", BIT(6)}, + {"FIA_U_PGD0", BIT(7)}, + + {"PSF8_PGD0", BIT(0)}, + {"SBR16B9_PGD0", BIT(1)}, + {"PSF0_PGD0", BIT(2)}, + {"FIACPCB_U_PGD0", BIT(3)}, + {"TAM_PGD0", BIT(4)}, + {"D2D_NOC_PGD2", BIT(5)}, + {"SBR8B2_PGD0", BIT(6)}, + {"THC0_PGD0", BIT(7)}, + + {"THC1_PGD0", BIT(0)}, + {"PMC_PGD1", BIT(1)}, + {"DISP_PGA1_PGD0", BIT(2)}, + {"TCSS_PGD0", BIT(3)}, + {"DISP_PGA_PGD0", BIT(4)}, + {"SBR16B1_PGD0", BIT(5)}, + {"SBRG_PGD0", BIT(6)}, + {"PSF5_PGD0", BIT(7)}, + + {"SBR8B3_PGD0", BIT(0)}, + {"ACE_PGD0", BIT(1)}, + {"ACE_PGD1", BIT(2)}, + {"ACE_PGD2", BIT(3)}, + {"ACE_PGD3", BIT(4)}, + {"ACE_PGD4", BIT(5)}, + {"ACE_PGD5", BIT(6)}, + {"ACE_PGD6", BIT(7)}, + + {"ACE_PGD7", BIT(0)}, + {"ACE_PGD8", BIT(1)}, + {"ACE_PGD9", BIT(2)}, + {"ACE_PGD10", BIT(3)}, + {"FIACPCB_PG_PGD0", BIT(4)}, + {"SNPS_USB2_B_PGD0", BIT(5)}, + {"OSSE_PGD0", BIT(6)}, + {"SBR8B0_PGD0", BIT(7)}, + + {"SBR16B4_PGD0", BIT(0)}, + {"CSME_PTIO_PGD0", BIT(1)}, + {} +}; + +static const struct pmc_bit_map *ext_nvl_pcdh_pfear_map[] = { + nvl_pcdh_pfear_map, + NULL +}; + +static const struct pmc_bit_map nvl_pcdh_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0), 1}, + {"AON3_OFF_STS", BIT(1), 0}, + {"AON4_OFF_STS", BIT(2), 1}, + {"AON5_OFF_STS", BIT(3), 1}, + {"AON1_OFF_STS", BIT(4), 0}, + {"XTAL_LVM_OFF_STS", BIT(5), 0}, + {"MPFPW1_0_PLL_OFF_STS", BIT(6), 1}, + {"D2D_PLL_OFF_STS", BIT(7), 1}, + {"USB3_PLL_OFF_STS", BIT(8), 1}, + {"AON3_SPL_OFF_STS", BIT(9), 1}, + {"MPFPW2_0_PLL_OFF_STS", BIT(12), 1}, + {"XTAL_AGGR_OFF_STS", BIT(17), 1}, + {"USB2_PLL_OFF_STS", BIT(18), 0}, + {"DDI2_PLL_OFF_STS", BIT(19), 1}, + {"SE_TCSS_PLL_OFF_STS", BIT(20), 1}, + {"DDI_PLL_OFF_STS", BIT(21), 1}, + {"FILTER_PLL_OFF_STS", BIT(22), 1}, + {"ACE_PLL_OFF_STS", BIT(24), 0}, + {"FABRIC_PLL_OFF_STS", BIT(25), 1}, + {"SOC_PLL_OFF_STS", BIT(26), 1}, + {"REF_PLL_OFF_STS", BIT(28), 1}, + {"IMG_PLL_OFF_STS", BIT(29), 1}, + {"GENLOCK_FILTER_PLL_OFF_STS", BIT(30), 1}, + {"RTC_PLL_OFF_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FUSE_OSSE_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 1}, + {"SPA_PGD0_PG_STS", BIT(4), 1}, + {"SPB_PGD0_PG_STS", BIT(5), 1}, + {"MPFPW2_PGD0_PG_STS", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 1}, + {"SBR16B20_PGD0_PG_STS", BIT(8), 0}, + {"DBG_PGD0_PG_STS", BIT(9), 0}, + {"SBR16B7_PGD0_PG_STS", BIT(10), 0}, + {"STRC_PGD0_PG_STS", BIT(11), 0}, + {"SBR16B8_PGD0_PG_STS", BIT(12), 0}, + {"D2D_DISP_PGD1_PG_STS", BIT(13), 1}, + {"LPSS_PGD0_PG_STS", BIT(14), 1}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"SBR16B2_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD1_PG_STS", BIT(20), 1}, + {"DBG_SBR16B_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"SBR16B0_PGD0_PG_STS", BIT(23), 0}, + {"P2SB0_PGD0_PG_STS", BIT(24), 1}, + {"XDCI_PGD0_PG_STS", BIT(25), 1}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 1}, + {"PMT_PGD0_PG_STS", BIT(29), 1}, + {"CLINK_PGD0_PG_STS", BIT(30), 1}, + {"SBR16B21_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 1}, + {"SBR16B22_PGD0_PG_STS", BIT(1), 0}, + {"SMT1_PGD0_PG_STS", BIT(2), 1}, + {"MPFPW1_PGD0_PG_STS", BIT(3), 0}, + {"SMS2_PGD0_PG_STS", BIT(4), 1}, + {"SMS1_PGD0_PG_STS", BIT(5), 1}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(8), 0}, + {"ESE_PGD0_PG_STS", BIT(9), 1}, + {"SBR16B6_PGD0_PG_STS", BIT(10), 0}, + {"P2SB1_PGD0_PG_STS", BIT(11), 1}, + {"SBR16B3_PGD0_PG_STS", BIT(12), 0}, + {"OSSE_SMT1_PGD0_PG_STS", BIT(13), 1}, + {"D2D_DISP_PGD0_PG_STS", BIT(14), 1}, + {"SNPA_USB2_A_PGD0_PG_STS", BIT(15), 0}, + {"U3FPW1_PGD0_PG_STS", BIT(16), 0}, + {"FIA_X_PGD0_PG_STS", BIT(17), 0}, + {"PSF4_PGD0_PG_STS", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"UFSX2_PGD0_PG_STS", BIT(20), 1}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"DBC_PGD0_PG_STS", BIT(22), 0}, + {"FIA_PG_PGD0_PG_STS", BIT(23), 0}, + {"D2D_IPU_PGD0_PG_STS", BIT(24), 1}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"FIACPCB_X_PGD0_PG_STS", BIT(26), 0}, + {"SBR8B4_PGD0_PG_STS", BIT(27), 0}, + {"DBG_PSF_PGD0_PG_STS", BIT(28), 0}, + {"PSF6_PGD0_PG_STS", BIT(29), 0}, + {"UFSPW1_PGD0_PG_STS", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0), 0}, + {"SBR16B9_PGD0_PG_STS", BIT(1), 0}, + {"PSF0_PGD0_PG_STS", BIT(2), 0}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 1}, + {"D2D_NOC_PGD2_PG_STS", BIT(5), 1}, + {"SBR8B2_PGD0_PG_STS", BIT(6), 0}, + {"THC0_PGD0_PG_STS", BIT(7), 1}, + {"THC1_PGD0_PG_STS", BIT(8), 1}, + {"PMC_PGD1_PG_STS", BIT(9), 0}, + {"DISP_PGA1_PGD0_PG_STS", BIT(10), 0}, + {"TCSS_PGD0_PG_STS", BIT(11), 0}, + {"DISP_PGA_PGD0_PG_STS", BIT(12), 0}, + {"SBR16B1_PGD0_PG_STS", BIT(13), 0}, + {"SBRG_PGD0_PG_STS", BIT(14), 0}, + {"PSF5_PGD0_PG_STS", BIT(15), 0}, + {"SBR8B3_PGD0_PG_STS", BIT(16), 0}, + {"ACE_PGD0_PG_STS", BIT(17), 0}, + {"ACE_PGD1_PG_STS", BIT(18), 0}, + {"ACE_PGD2_PG_STS", BIT(19), 0}, + {"ACE_PGD3_PG_STS", BIT(20), 0}, + {"ACE_PGD4_PG_STS", BIT(21), 0}, + {"ACE_PGD5_PG_STS", BIT(22), 0}, + {"ACE_PGD6_PG_STS", BIT(23), 0}, + {"ACE_PGD7_PG_STS", BIT(24), 0}, + {"ACE_PGD8_PG_STS", BIT(25), 0}, + {"ACE_PGD9_PG_STS", BIT(26), 0}, + {"ACE_PGD10_PG_STS", BIT(27), 0}, + {"FIACPCB_PG_PGD0_PG_STS", BIT(28), 0}, + {"SNPS_USB2_B_PGD0_PG_STS", BIT(29), 0}, + {"OSSE_PGD0_PG_STS", BIT(30), 1}, + {"SBR8B0_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_power_gating_status_3_map[] = { + {"SBR16B4_PGD0_PG_STS", BIT(0), 0}, + {"PTIO_PGD0_PG_STS", BIT(1), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 1}, + {"OSSE_D3_STS", BIT(6), 0}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPB_D3_STS", BIT(13), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"PSTH_D3_STS", BIT(21), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_d3_status_1_map[] = { + {"OSSE_SMT1_D3_STS", BIT(0), 0}, + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {"UFSX2_D3_STS", BIT(28), 0}, + {"ESE_D3_STS", BIT(29), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"OSSE_SMT2_D3_STS", BIT(11), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14), 1}, + {"THC1_D3_STS", BIT(15), 1}, + {"OSSE_SMT3_D3_STS", BIT(16), 0}, + {"ACE_D3_STS", BIT(23), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3), 1}, + {"OSSE_VNN_REQ_STS", BIT(6), 1}, + {"ESPISPI_VNN_REQ_STS", BIT(18), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_vnn_req_status_1_map[] = { + {"OSSE_SMT1_VNN_REQ_STS", BIT(0), 1}, + {"NPK_VNN_REQ_STS", BIT(4), 1}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 1}, + {"P2D_VNN_REQ_STS", BIT(18), 1}, + {"GBE_VNN_REQ_STS", BIT(19), 1}, + {"SMB_VNN_REQ_STS", BIT(25), 1}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {"ESE_VNN_REQ_STS", BIT(29), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1), 1}, + {"CSE_VNN_REQ_STS", BIT(4), 1}, + {"ISH_VNN_REQ_STS", BIT(7), 1}, + {"SMT1_VNN_REQ_STS", BIT(8), 1}, + {"CLINK_VNN_REQ_STS", BIT(14), 1}, + {"SMS1_VNN_REQ_STS", BIT(18), 1}, + {"SMS2_VNN_REQ_STS", BIT(19), 1}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 1}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 1}, + {"DISP_SHIM_VNN_REQ_STS", BIT(22), 1}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 1}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_vnn_req_status_3_map[] = { + {"DTS0_VNN_REQ_STS", BIT(7), 0}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"PG5_PMA0_REQ_STS", BIT(3), 1}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4), 0}, + {"VNN_SOC_REQ_STS", BIT(6), 1}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"D2D_NOC_CFI_QACTIVE_REQ_STS", BIT(8), 1}, + {"D2D_NOC_GPSB_QACTIVE_REQ_STS", BIT(9), 1}, + {"D2D_IPU_QACTIVE_REQ_STS", BIT(10), 1}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"ALL_SBR_IDLE_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"PG5_PMA1_REQ_STS", BIT(22), 1}, + {"FIA_DEEP_PM_REQ_STS", BIT(23), 0}, + {"XDCI_ATTACHED_REQ_STS", BIT(24), 1}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"D2D_DISP_DDI_QACTIVE_REQ_STS", BIT(26), 1}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_REQ_STS", BIT(29), 1}, + {"PG5_PMA2_GVNN", BIT(30), 1}, + {"D2D_DISP_EDP_QACTIVE_REQ_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcdh_rsc_status_map[] = { + {"CORE", 0, 1}, + {"Memory", 0, 1}, + {"PRIM_D2D", 0, 1}, + {"PSF0", 0, 1}, + {"PSF4", 0, 1}, + {"PSF6", 0, 1}, + {"PSF8", 0, 1}, + {"SB", 0, 1}, + {} +}; + +static const struct pmc_bit_map *nvl_pcdh_lpm_maps[] = { + nvl_pcdh_clocksource_status_map, + nvl_pcdh_power_gating_status_0_map, + nvl_pcdh_power_gating_status_1_map, + nvl_pcdh_power_gating_status_2_map, + nvl_pcdh_power_gating_status_3_map, + nvl_pcdh_d3_status_0_map, + nvl_pcdh_d3_status_1_map, + nvl_pcdh_d3_status_2_map, + nvl_pcdh_d3_status_3_map, + nvl_pcdh_vnn_req_status_0_map, + nvl_pcdh_vnn_req_status_1_map, + nvl_pcdh_vnn_req_status_2_map, + nvl_pcdh_vnn_req_status_3_map, + nvl_pcdh_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *nvl_pcdh_blk_maps[] = { + nvl_pcdh_power_gating_status_0_map, + nvl_pcdh_power_gating_status_1_map, + nvl_pcdh_power_gating_status_2_map, + nvl_pcdh_power_gating_status_3_map, + nvl_pcdh_rsc_status_map, + nvl_pcdh_vnn_req_status_0_map, + nvl_pcdh_vnn_req_status_1_map, + nvl_pcdh_vnn_req_status_2_map, + nvl_pcdh_vnn_req_status_3_map, + nvl_pcdh_d3_status_0_map, + nvl_pcdh_d3_status_1_map, + nvl_pcdh_d3_status_2_map, + nvl_pcdh_d3_status_3_map, + nvl_pcdh_clocksource_status_map, + nvl_pcdh_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_bit_map nvl_pcds_pfear_map[] = { + {"PMC_PGD0", BIT(0)}, + {"FUSE_OSSE_PGD0", BIT(1)}, + {"SPI_PGD0", BIT(2)}, + {"XHCI_PGD0", BIT(3)}, + {"SPA_PGD0", BIT(4)}, + {"SPB_PGD0", BIT(5)}, + {"RSVD6", BIT(6)}, + {"GBE_PGD0", BIT(7)}, + + {"RSVD8", BIT(0)}, + {"RSVD9", BIT(1)}, + {"SBR16B7_PGD0", BIT(2)}, + {"SBR16B21_PGD0", BIT(3)}, + {"RSVD12", BIT(4)}, + {"D2D_DISP_PGD1", BIT(5)}, + {"LPSS_PGD0", BIT(6)}, + {"LPC_PGD0", BIT(7)}, + + {"SMB_PGD0", BIT(0)}, + {"ISH_PGD0", BIT(1)}, + {"SBR16B1_PGD0", BIT(2)}, + {"NPK_PGD0", BIT(3)}, + {"D2D_NOC_PGD1", BIT(4)}, + {"DBG_SBR16B_PGD0", BIT(5)}, + {"FUSE_PGD0", BIT(6)}, + {"RSVD23", BIT(7)}, + + {"P2SB0_PGD0", BIT(0)}, + {"OTG_PGD0", BIT(1)}, + {"EXI_PGD0", BIT(2)}, + {"CSE_PGD0", BIT(3)}, + {"CSME_KVM_PGD0", BIT(4)}, + {"CSME_PMT_PGD0", BIT(5)}, + {"CSME_CLINK_PGD0", BIT(6)}, + {"CSME_PTIO_PGD0", BIT(7)}, + + {"CSME_USBR_PGD0", BIT(0)}, + {"SBR16B22_PGD0", BIT(1)}, + {"CSME_SMT1_PGD0", BIT(2)}, + {"P2SB1_PGD0", BIT(3)}, + {"CSME_SMS2_PGD0", BIT(4)}, + {"CSME_SMS_PGD0", BIT(5)}, + {"CSME_RTC_PGD0", BIT(6)}, + {"CSMEPSF_PGD0", BIT(7)}, + + {"D2D_NOC_PGD0", BIT(0)}, + {"RSVD41", BIT(1)}, + {"RSVD42", BIT(2)}, + {"RSVD43", BIT(3)}, + {"SBR16B2_PGD0", BIT(4)}, + {"OSSE_SMT1_PGD0", BIT(5)}, + {"D2D_DISP_PGD0", BIT(6)}, + {"RSVD47_PGD0", BIT(7)}, + + {"RSVD48", BIT(0)}, + {"DBG_PSF_PGD0", BIT(1)}, + {"RSVD50", BIT(2)}, + {"CNVI_PGD0", BIT(3)}, + {"UFSX2_PGD0", BIT(4)}, + {"ENDBG_PGD0", BIT(5)}, + {"DBC_PGD0", BIT(6)}, + {"SBR16B4_PGD0", BIT(7)}, + + {"RSVD56", BIT(0)}, + {"NPK_PGD1", BIT(1)}, + {"RSVD58", BIT(2)}, + {"SBR16B20_PGD0", BIT(3)}, + {"RSVD60", BIT(4)}, + {"SBR8B20_PGD0", BIT(5)}, + {"RSVD62", BIT(6)}, + {"FIA_U_PGD0", BIT(7)}, + + {"PSF8_PGD0", BIT(0)}, + {"RSVD65", BIT(1)}, + {"RSVD66", BIT(2)}, + {"FIACPCB_U_PGD0", BIT(3)}, + {"TAM_PGD0", BIT(4)}, + {"D2D_NOC_PGD2", BIT(5)}, + {"SBR8B2_PGD0", BIT(6)}, + {"THC0_PGD0", BIT(7)}, + + {"THC1_PGD0", BIT(0)}, + {"PMC_PGD1", BIT(1)}, + {"SBR16B3_PGD0", BIT(2)}, + {"TCSS_PGD0", BIT(3)}, + {"DISP_PGA_PGD0", BIT(4)}, + {"RSVD77", BIT(5)}, + {"RSVD78", BIT(6)}, + {"RSVD79", BIT(7)}, + + {"SBRG_PGD0", BIT(0)}, + {"RSVD81", BIT(1)}, + {"SBR16B0_PGD0", BIT(2)}, + {"SBR8B0_PGD0", BIT(3)}, + {"PSF7_PGD0", BIT(4)}, + {"RSVD85", BIT(5)}, + {"RSVD86", BIT(6)}, + {"RSVD87", BIT(7)}, + + {"SBR16B6_PGD0", BIT(0)}, + {"PSD0_PGD0", BIT(1)}, + {"STRC_PGD0", BIT(2)}, + {"RSVD91", BIT(3)}, + {"DBG_SBR_PGD0", BIT(4)}, + {"RSVD93", BIT(5)}, + {"OSSE_PGD0", BIT(6)}, + {"DISP_PGA1_PGD0", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_nvl_pcds_pfear_map[] = { + nvl_pcds_pfear_map, + NULL +}; + +static const struct pmc_bit_map nvl_pcds_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", PTL_PMC_LTR_SATA2}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"RSVD", NVL_PCDS_PMC_LTR_RESERVED}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + {"OSSE", LNL_PMC_LTR_OSSE}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", PTL_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", PTL_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0), 1}, + {"AON3_OFF_STS", BIT(1), 0}, + {"AON4_OFF_STS", BIT(2), 1}, + {"AON5_OFF_STS", BIT(3), 1}, + {"AON1_OFF_STS", BIT(4), 0}, + {"XTAL_LVM_OFF_STS", BIT(5), 0}, + {"D2D_OFF_STS", BIT(8), 1}, + {"AON3_SPL_OFF_STS", BIT(9), 1}, + {"XTAL_AGGR_OFF_STS", BIT(17), 1}, + {"BCLK_EXT_INJ_OFF_STS", BIT(18), 1}, + {"DDI2_PLL_OFF_STS", BIT(19), 1}, + {"SE_TCSS_PLL_OFF_STS", BIT(20), 1}, + {"DDI_PLL_OFF_STS", BIT(21), 1}, + {"FILTER_PLL_OFF_STS", BIT(22), 1}, + {"PHY_OC_EXT_INJ_OFF_STS", BIT(23), 1}, + {"ACE_PLL_OFF_STS", BIT(24), 0}, + {"FABRIC_PLL_OFF_STS", BIT(25), 1}, + {"SOC_PLL_OFF_STS", BIT(26), 1}, + {"REF_PLL_OFF_STS", BIT(28), 1}, + {"GENLOCK_FILTER_PLL_OFF_STS", BIT(30), 1}, + {"RTC_PLL_OFF_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FUSE_OSSE_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 0}, + {"SPA_PGD0_PG_STS", BIT(4), 0}, + {"SPB_PGD0_PG_STS", BIT(5), 0}, + {"RSVD_6", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 0}, + {"RSVD_8", BIT(8), 0}, + {"RSVD_9", BIT(9), 0}, + {"SBR16B7_PGD0_PG_STS", BIT(10), 0}, + {"SBR16B21_PGD0_PG_STS", BIT(11), 0}, + {"RSVD_12", BIT(12), 0}, + {"D2D_DISP_PGD1_PG_STS", BIT(13), 1}, + {"LPSS_PGD0_PG_STS", BIT(14), 0}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"SBR16B1_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD1_PG_STS", BIT(20), 1}, + {"DBG_SBR16B_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"RSVD_23", BIT(23), 0}, + {"P2SB0_PGD0_PG_STS", BIT(24), 1}, + {"XDCI_PGD0_PG_STS", BIT(25), 0}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 0}, + {"PMT_PGD0_PG_STS", BIT(29), 0}, + {"CLINK_PGD0_PG_STS", BIT(30), 0}, + {"PTIO_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 0}, + {"SBR16B22_PGD0_PG_STS", BIT(1), 0}, + {"SMT1_PGD0_PG_STS", BIT(2), 0}, + {"P2SB1_PGD0_PG_STS", BIT(3), 1}, + {"SMS2_PGD0_PG_STS", BIT(4), 0}, + {"SMS1_PGD0_PG_STS", BIT(5), 0}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(8), 0}, + {"RSVD_9", BIT(9), 0}, + {"RSVD_10", BIT(10), 0}, + {"RSVD_11", BIT(11), 0}, + {"SBR16B2_PGD0_PG_STS", BIT(12), 0}, + {"OSSE_SMT1_PGD0_PG_STS", BIT(13), 1}, + {"D2D_DISP_PGD0_PG_STS", BIT(14), 1}, + {"RSVD_15", BIT(15), 0}, + {"RSVD_16", BIT(16), 0}, + {"DBG_PSF_PGD0_PG_STS", BIT(17), 0}, + {"RSVD_18", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"UFSX2_PGD0_PG_STS", BIT(20), 0}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"DBC_PGD0_PG_STS", BIT(22), 0}, + {"SBR16B4_PGD0_PG_STS", BIT(23), 0}, + {"RSVD_24", BIT(24), 0}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"RSVD_26", BIT(26), 0}, + {"SBR16B20_PGD0_PG_STS", BIT(27), 0}, + {"RSVD_28", BIT(28), 0}, + {"SBR8B20_PGD0_PG_STS", BIT(29), 0}, + {"RSVD_30", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0), 0}, + {"RSVD_1", BIT(1), 0}, + {"RSVD_2", BIT(2), 0}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 1}, + {"D2D_NOC_PGD2_PG_STS", BIT(5), 1}, + {"SBR8B2_PGD0_PG_STS", BIT(6), 0}, + {"THC0_PGD0_PG_STS", BIT(7), 0}, + {"THC1_PGD0_PG_STS", BIT(8), 0}, + {"PMC_PGD1_PG_STS", BIT(9), 0}, + {"SBR16B3_PGD0_PG_STS", BIT(10), 0}, + {"TCSS_PGD0_PG_STS", BIT(11), 0}, + {"DISP_PGA_PGD0_PG_STS", BIT(12), 0}, + {"RSVD_13", BIT(13), 0}, + {"RSVD_14", BIT(14), 0}, + {"RSVD_15", BIT(15), 0}, + {"SBRG_PGD0_PG_STS", BIT(16), 0}, + {"RSVD_17", BIT(17), 0}, + {"SBR16B0_PGD0_PG_STS", BIT(18), 0}, + {"SBR8B0_PGD0_PG_STS", BIT(19), 0}, + {"PSF7_PGD0_PG_STS", BIT(20), 0}, + {"RSVD_21", BIT(21), 0}, + {"RSVD_22", BIT(22), 0}, + {"RSVD_23", BIT(23), 0}, + {"SBR16B6_PGD0_PG_STS", BIT(24), 0}, + {"PSF0_PGD0_PG_STS", BIT(25), 0}, + {"STRC_PGD0_PG_STS", BIT(26), 0}, + {"RSVD_27", BIT(27), 0}, + {"DBG_SBR_PGD0_PG_STS", BIT(28), 0}, + {"RSVD_29", BIT(29), 0}, + {"OSSE_PGD0_PG_STS", BIT(30), 1}, + {"DISP_PGA1_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 1}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPB_D3_STS", BIT(13), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"PSTH_D3_STS", BIT(21), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_d3_status_1_map[] = { + {"OSSE_D3_STS", BIT(14), 0}, + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {"UFSX2_D3_STS", BIT(28), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"OSSE_SMT1_D3_STS", BIT(12), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_d3_status_3_map[] = { + {"OSSE_SMT2_D3_STS", BIT(0), 0}, + {"THC0_D3_STS", BIT(14), 1}, + {"THC1_D3_STS", BIT(15), 1}, + {"OSSE_SMT3_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3), 0}, + {"ESPISPI_VNN_REQ_STS", BIT(18), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4), 1}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 1}, + {"OSSE_VNN_REQ_STS", BIT(14), 1}, + {"P2D_VNN_REQ_STS", BIT(18), 1}, + {"GBE_VNN_REQ_STS", BIT(19), 0}, + {"SMB_VNN_REQ_STS", BIT(25), 1}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1), 0}, + {"CSE_VNN_REQ_STS", BIT(4), 1}, + {"ISH_VNN_REQ_STS", BIT(7), 0}, + {"SMT1_VNN_REQ_STS", BIT(8), 0}, + {"OSSE_SMT1_VNN_REQ_STS", BIT(12), 1}, + {"CLINK_VNN_REQ_STS", BIT(14), 0}, + {"SMS1_VNN_REQ_STS", BIT(18), 0}, + {"SMS2_VNN_REQ_STS", BIT(19), 0}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 0}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 1}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 1}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_vnn_req_status_3_map[] = { + {"DISP_SHIM_VNN_REQ_STS", BIT(4), 1}, + {"DTS0_VNN_REQ_STS", BIT(7), 0}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"PG5_PMA0_REQ_STS", BIT(3), 1}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4), 0}, + {"VNN_SOC_REQ_STS", BIT(6), 1}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"D2D_NOC_CFI_QACTIVE_REQ_STS", BIT(8), 1}, + {"D2D_NOC_GPSB_QACTIVE_REQ_STS", BIT(9), 1}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"ALL_SBR_IDLE_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"PG5_PMA1_REQ_STS", BIT(22), 1}, + {"DG5_PMA0_REQ_STS", BIT(23), 1}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"D2D_DISP_DDI_QACTIVE_REQ_STS", BIT(26), 1}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_REQ_STS", BIT(29), 1}, + {"D2D_DISP_EDP_QACTIVE_REQ_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_rsc_status_map[] = { + {"CORE", 0, 1}, + {"Memory", 0, 1}, + {"PRIM_D2D", 0, 1}, + {"PSF0", 0, 1}, + {"SB", 0, 1}, + {} +}; + +static const struct pmc_bit_map nvl_pcds_signal_status_map[] = { + {"LSX_Wake0_STS", BIT(0), 0}, + {"LSX_Wake1_STS", BIT(1), 0}, + {"LSX_Wake2_STS", BIT(2), 0}, + {"LSX_Wake3_STS", BIT(3), 0}, + {"LSX_Wake4_STS", BIT(4), 0}, + {"LSX_Wake5_STS", BIT(5), 0}, + {"LSX_Wake6_STS", BIT(6), 0}, + {"LSX_Wake7_STS", BIT(7), 0}, + {"LPSS_Wake0_STS", BIT(8), 1}, + {"LPSS_Wake1_STS", BIT(9), 1}, + {"Int_Timer_SS_Wake0_STS", BIT(10), 1}, + {"Int_Timer_SS_Wake1_STS", BIT(11), 1}, + {"Int_Timer_SS_Wake2_STS", BIT(12), 1}, + {"Int_Timer_SS_Wake3_STS", BIT(13), 1}, + {"Int_Timer_SS_Wake4_STS", BIT(14), 1}, + {"Int_Timer_SS_Wake5_STS", BIT(15), 1}, + {} +}; + +static const struct pmc_bit_map *nvl_pcds_lpm_maps[] = { + nvl_pcds_clocksource_status_map, + nvl_pcds_power_gating_status_0_map, + nvl_pcds_power_gating_status_1_map, + nvl_pcds_power_gating_status_2_map, + nvl_pcds_d3_status_0_map, + nvl_pcds_d3_status_1_map, + nvl_pcds_d3_status_2_map, + nvl_pcds_d3_status_3_map, + nvl_pcds_vnn_req_status_0_map, + nvl_pcds_vnn_req_status_1_map, + nvl_pcds_vnn_req_status_2_map, + nvl_pcds_vnn_req_status_3_map, + nvl_pcds_vnn_misc_status_map, + nvl_pcds_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *nvl_pcds_blk_maps[] = { + nvl_pcds_power_gating_status_0_map, + nvl_pcds_power_gating_status_1_map, + nvl_pcds_power_gating_status_2_map, + nvl_pcds_rsc_status_map, + nvl_pcds_vnn_req_status_0_map, + nvl_pcds_vnn_req_status_1_map, + nvl_pcds_vnn_req_status_2_map, + nvl_pcds_vnn_req_status_3_map, + nvl_pcds_d3_status_0_map, + nvl_pcds_d3_status_1_map, + nvl_pcds_d3_status_2_map, + nvl_pcds_d3_status_3_map, + nvl_pcds_clocksource_status_map, + nvl_pcds_vnn_misc_status_map, + nvl_pcds_signal_status_map, + NULL +}; + +static const struct pmc_bit_map nvl_pchs_pfear_map[] = { + {"PMC_PGD0", BIT(0)}, + {"FIA_D_PGD0", BIT(1)}, + {"SPI_PGD0", BIT(2)}, + {"XHCI_PGD0", BIT(3)}, + {"SPA_PGD0", BIT(4)}, + {"SPB_PGD0", BIT(5)}, + {"MPFPW2_PGD0", BIT(6)}, + {"GBE_PGD0", BIT(7)}, + + {"RSVD8", BIT(0)}, + {"PSF3_PGD0", BIT(1)}, + {"SBR5_PGD0", BIT(2)}, + {"SBR0_PGD0", BIT(3)}, + {"RSVD12", BIT(4)}, + {"D2D_DISP_PGD1", BIT(5)}, + {"LPSS_PGD0", BIT(6)}, + {"LPC_PGD0", BIT(7)}, + + {"SMB_PGD0", BIT(0)}, + {"ISH_PGD0", BIT(1)}, + {"P2SB_PGD0", BIT(2)}, + {"NPK_PGD0", BIT(3)}, + {"D2D_NOC_PGD1", BIT(4)}, + {"EAH_PGD0", BIT(5)}, + {"FUSE_PGD0", BIT(6)}, + {"SBR8_PGD0", BIT(7)}, + + {"PSF7_PGD0", BIT(0)}, + {"OTG_PGD0", BIT(1)}, + {"EXI_PGD0", BIT(2)}, + {"CSE_PGD0", BIT(3)}, + {"CSME_KVM_PGD0", BIT(4)}, + {"CSME_PMT_PGD0", BIT(5)}, + {"CSME_CLINK_PGD0", BIT(6)}, + {"CSME_PTIO_PGD0", BIT(7)}, + + {"CSME_USBR_PGD0", BIT(0)}, + {"SBR1_PGD0", BIT(1)}, + {"CSME_SMT1_PGD0", BIT(2)}, + {"MPFPW1_PGD0", BIT(3)}, + {"CSME_SMS2_PGD0", BIT(4)}, + {"CSME_SMS_PGD0", BIT(5)}, + {"CSME_RTC_PGD0", BIT(6)}, + {"CSMEPSF_PGD0", BIT(7)}, + + {"D2D_NOC_PGD0", BIT(0)}, + {"ESE_PGD0", BIT(1)}, + {"SBR2_PGD0", BIT(2)}, + {"SBR3_PGD0", BIT(3)}, + {"SBR4_PGD0", BIT(4)}, + {"RSVD45", BIT(5)}, + {"D2D_DISP_PGD0", BIT(6)}, + {"PSF1_PGD0", BIT(7)}, + + {"U3FPW1_PGD0", BIT(0)}, + {"DMI3FPW_PGD0", BIT(1)}, + {"PSF4_PGD0", BIT(2)}, + {"CNVI_PGD0", BIT(3)}, + {"RSVD52", BIT(4)}, + {"ENDBG_PGD0", BIT(5)}, + {"DBC_PGD0", BIT(6)}, + {"SMT4_PGD0", BIT(7)}, + + {"RSVD56", BIT(0)}, + {"NPK_PGD1", BIT(1)}, + {"RSVD58", BIT(2)}, + {"DMI3_PGD0", BIT(3)}, + {"RSVD60", BIT(4)}, + {"FIACPCB_D_PGD0", BIT(5)}, + {"RSVD62", BIT(6)}, + {"FIA_U_PGD0", BIT(7)}, + + {"FIACPCB_PGS_PGD0", BIT(0)}, + {"FIA_PGS_PGD0", BIT(1)}, + {"RSVD66", BIT(2)}, + {"FIACPCB_U_PGD0", BIT(3)}, + {"TAM_PGD0", BIT(4)}, + {"D2D_NOC_PGD2", BIT(5)}, + {"PSF2_PGD0", BIT(6)}, + {"THC0_PGD0", BIT(7)}, + + {"THC1_PGD0", BIT(0)}, + {"PMC_PGD1", BIT(1)}, + {"SBR9_PGD0", BIT(2)}, + {"U3FPW2_PGD0", BIT(3)}, + {"RSVD76", BIT(4)}, + {"DBG_PSF_PGD0", BIT(5)}, + {"DBG_SBR_PGD0", BIT(6)}, + {"SBR6_PGD0", BIT(7)}, + + {"SPC_PGD0", BIT(0)}, + {"ACE_PGD0", BIT(1)}, + {"ACE_PGD1", BIT(2)}, + {"ACE_PGD2", BIT(3)}, + {"ACE_PGD3", BIT(4)}, + {"ACE_PGD4", BIT(5)}, + {"ACE_PGD5", BIT(6)}, + {"ACE_PGD6", BIT(7)}, + + {"ACE_PGD7", BIT(0)}, + {"ACE_PGD8", BIT(1)}, + {"ACE_PGD9", BIT(2)}, + {"ACE_PGD10", BIT(3)}, + {"U3FPW3_PGD0", BIT(4)}, + {"SBR7_PGD0", BIT(5)}, + {"OSSE_PGD0", BIT(6)}, + {"ST_PGD0", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_nvl_pchs_pfear_map[] = { + nvl_pchs_pfear_map, + NULL +}; + +static const struct pmc_bit_map nvl_pchs_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0), 1}, + {"AON3_OFF_STS", BIT(1), 0}, + {"AON4_OFF_STS", BIT(2), 0}, + {"AON2_SPL_OFF_STS", BIT(3), 0}, + {"AONL_OFF_STS", BIT(4), 0}, + {"XTAL_LVM_OFF_STS", BIT(5), 0}, + {"AON5_OFF_STS", BIT(6), 0}, + {"USB3_PLL_OFF_STS", BIT(8), 1}, + {"MAIN_CRO_OFF_STS", BIT(11), 0}, + {"MAIN_DIVIDER_OFF_STS", BIT(12), 1}, + {"REF_PLL_NON_OC_OFF_STS", BIT(13), 1}, + {"DMI_PLL_OFF_STS", BIT(14), 1}, + {"PHY_EXT_INJ_OFF_STS", BIT(15), 1}, + {"AON6_MCRO_OFF_STS", BIT(16), 0}, + {"XTAL_AGGR_OFF_STS", BIT(17), 0}, + {"USB2_PLL_OFF_STS", BIT(18), 1}, + {"GBE_PLL_OFF_STS", BIT(21), 1}, + {"SATA_PLL_OFF_STS", BIT(22), 1}, + {"PCIE0_PLL_OFF_STS", BIT(23), 1}, + {"PCIE1_PLL_OFF_STS", BIT(24), 1}, + {"FABRIC_PLL_OFF_STS", BIT(25), 1}, + {"PCIE2_PLL_OFF_STS", BIT(26), 1}, + {"REF_PLL_OFF_STS", BIT(28), 1}, + {"REF38P4_PLL_OFF_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FIA_D_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 0}, + {"SPA_PGD0_PG_STS", BIT(4), 1}, + {"SPB_PGD0_PG_STS", BIT(5), 1}, + {"MPFPW2_PGD0_PG_STS", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 1}, + {"RSVD_8", BIT(8), 0}, + {"PSF3_PGD0_PG_STS", BIT(9), 0}, + {"SBR5_PGD0_PG_STS", BIT(10), 0}, + {"SBR0_PGD0_PG_STS", BIT(11), 0}, + {"RSVD_12", BIT(12), 0}, + {"D2D_DISP_PGD1_PG_STS", BIT(13), 0}, + {"LPSS_PGD0_PG_STS", BIT(14), 1}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"P2S_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD1_PG_STS", BIT(20), 0}, + {"EAH_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"SBR8_PGD0_PG_STS", BIT(23), 0}, + {"PSF7_PGD0_PG_STS", BIT(24), 0}, + {"XDCI_PGD0_PG_STS", BIT(25), 1}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 1}, + {"PMT_PGD0_PG_STS", BIT(29), 1}, + {"CLINK_PGD0_PG_STS", BIT(30), 1}, + {"PTIO_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 1}, + {"SBR1_PGD0_PG_STS", BIT(1), 0}, + {"SMT1_PGD0_PG_STS", BIT(2), 1}, + {"MPFPW1_PGD0_PG_STS", BIT(3), 0}, + {"SMS2_PGD0_PG_STS", BIT(4), 1}, + {"SMS1_PGD0_PG_STS", BIT(5), 1}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(8), 0}, + {"ESE_PGD0_PG_STS", BIT(9), 1}, + {"SBR2_PGD0_PG_STS", BIT(10), 0}, + {"SBR3_PGD0_PG_STS", BIT(11), 0}, + {"SBR4_PGD0_PG_STS", BIT(12), 0}, + {"RSVD_13", BIT(13), 0}, + {"D2D_DISP_PGD0_PG_STS", BIT(14), 0}, + {"PSF1_PGD0_PG_STS", BIT(15), 0}, + {"U3FPW1_PGD0_PG_STS", BIT(16), 0}, + {"DMI3FPW_PGD0_PG_STS", BIT(17), 0}, + {"PSF4_PGD0_PG_STS", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"RSVD_20", BIT(20), 0}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"DBC_PGD0_PG_STS", BIT(22), 0}, + {"SMT4_PGD0_PG_STS", BIT(23), 1}, + {"RSVD_24", BIT(24), 0}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"RSVD_26", BIT(26), 0}, + {"DMI3_PGD0_PG_STS", BIT(27), 1}, + {"RSVD_28", BIT(28), 0}, + {"FIACPCB_D_PGD0_PG_STS", BIT(29), 0}, + {"RSVD_30", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_power_gating_status_2_map[] = { + {"FIACPCB_PGS_PGD0_PG_STS", BIT(0), 0}, + {"FIA_PGS_PGD0_PG_STS", BIT(1), 0}, + {"RSVD_2", BIT(2), 0}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 0}, + {"D2D_NOC_PGD2_PG_STS", BIT(5), 0}, + {"PSF2_PGD0_PG_STS", BIT(6), 0}, + {"THC0_PGD0_PG_STS", BIT(7), 1}, + {"THC1_PGD0_PG_STS", BIT(8), 1}, + {"PMC_PGD1_PG_STS", BIT(9), 0}, + {"SBR9_PGA0_PGD0_PG_STS", BIT(10), 0}, + {"U3FPW2_PGD0_PG_STS", BIT(11), 0}, + {"RSVD_12", BIT(12), 0}, + {"DBG_PSF_PGD0_PG_STS", BIT(13), 0}, + {"DBG_SBR_PGD0_PG_STS", BIT(14), 0}, + {"SBR6_PGD0_PG_STS", BIT(15), 0}, + {"SPC_PGD0_PG_STS", BIT(16), 1}, + {"ACE_PGD0_PG_STS", BIT(17), 0}, + {"ACE_PGD1_PG_STS", BIT(18), 0}, + {"ACE_PGD2_PG_STS", BIT(19), 0}, + {"ACE_PGD3_PG_STS", BIT(20), 0}, + {"ACE_PGD4_PG_STS", BIT(21), 0}, + {"ACE_PGD5_PG_STS", BIT(22), 0}, + {"ACE_PGD6_PG_STS", BIT(23), 0}, + {"ACE_PGD7_PG_STS", BIT(24), 0}, + {"ACE_PGD8_PG_STS", BIT(25), 0}, + {"ACE_PGD9_PG_STS", BIT(26), 0}, + {"ACE_PGD10_PG_STS", BIT(27), 0}, + {"U3FPW3_PGD0_PG_STS", BIT(28), 0}, + {"SBR7_PGD0_PG_STS", BIT(29), 0}, + {"OSSE_PGD0_PG_STS", BIT(30), 0}, + {"SATA_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 0}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPB_D3_STS", BIT(13), 0}, + {"SPC_D3_STS", BIT(14), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"SATA_D3_STS", BIT(20), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_d3_status_1_map[] = { + {"OSSE_D3_STS", BIT(6), 0}, + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"P2S_D3_STS", BIT(24), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"SMT4_D3_STS", BIT(11), 0}, + {"SMT5_D3_STS", BIT(12), 0}, + {"SMT6_D3_STS", BIT(13), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14), 0}, + {"THC1_D3_STS", BIT(15), 0}, + {"ACE_D3_STS", BIT(23), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4), 0}, + {"OSSE_VNN_REQ_STS", BIT(6), 0}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 0}, + {"GBE_VNN_REQ_STS", BIT(19), 0}, + {"SMB_VNN_REQ_STS", BIT(25), 0}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1), 0}, + {"CSE_VNN_REQ_STS", BIT(4), 0}, + {"ISH_VNN_REQ_STS", BIT(7), 0}, + {"SMT1_VNN_REQ_STS", BIT(8), 0}, + {"SMT4_VNN_REQ_STS", BIT(11), 0}, + {"CLINK_VNN_REQ_STS", BIT(14), 0}, + {"SMS1_VNN_REQ_STS", BIT(18), 0}, + {"SMS2_VNN_REQ_STS", BIT(19), 0}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 0}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 0}, + {"GPIOCOM2_VNN_REQ_STS", BIT(22), 0}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 0}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 0}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"PG5_PMA0_GVNN_REQ_STS", BIT(3), 1}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4), 0}, + {"DMI_IN_L1_REQ_STS", BIT(6), 0}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"ALL_SBR_IDLE_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"DMI_CLKREQ_B_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"PG5_PMA1_GVNN_REQ_STS", BIT(22), 1}, + {"FIA_DEEP_PM_REQ_STS", BIT(23), 0}, + {"XDCI_ATTACHED_REQ_STS", BIT(24), 0}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_EN_REQ_STS", BIT(29), 0}, + {"PG5_PMA2_GVNN_REQ_STS", BIT(30), 1}, + {} +}; + +static const struct pmc_bit_map nvl_pchs_rsc_status_map[] = { + {"Memory", 0, 1}, + {"Memory_NS", 0, 1}, + {"PSF1", 0, 1}, + {"PSF2", 0, 1}, + {"PSF3", 0, 1}, + {"REF_PLL", 0, 1}, + {"SB", 0, 1}, + {} +}; + +static const struct pmc_bit_map *nvl_pchs_lpm_maps[] = { + nvl_pchs_clocksource_status_map, + nvl_pchs_power_gating_status_0_map, + nvl_pchs_power_gating_status_1_map, + nvl_pchs_power_gating_status_2_map, + nvl_pchs_d3_status_0_map, + nvl_pchs_d3_status_1_map, + nvl_pchs_d3_status_2_map, + nvl_pchs_d3_status_3_map, + nvl_pcds_vnn_req_status_0_map, + nvl_pchs_vnn_req_status_1_map, + nvl_pchs_vnn_req_status_2_map, + nvl_pcdh_vnn_req_status_3_map, + nvl_pchs_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *nvl_pchs_blk_maps[] = { + nvl_pchs_power_gating_status_0_map, + nvl_pchs_power_gating_status_1_map, + nvl_pchs_power_gating_status_2_map, + nvl_pchs_rsc_status_map, + nvl_pchs_d3_status_0_map, + nvl_pchs_clocksource_status_map, + nvl_pchs_vnn_misc_status_map, + NULL +}; + +static const struct pmc_reg_map nvl_pcdh_reg_map = { + .pfear_sts = ext_nvl_pcdh_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = ptl_pcdp_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = NVL_PCDH_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = NVL_PCDH_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = NVL_LPM_NUM_MAPS, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = NVL_LPM_PRI_OFFSET, + .lpm_en_offset = NVL_LPM_EN_OFFSET, + .lpm_residency_offset = NVL_LPM_RESIDENCY_OFFSET, + .lpm_sts = nvl_pcdh_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = NVL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = nvl_pcdh_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .num_s0ix_blocker = NVL_PCDH_NUM_S0IX_BLOCKER, + .blocker_req_offset = NVL_PCDH_BLK_REQ_OFFSET, + .lpm_req_guid = PCDH_LPM_REQ_GUID, +}; + +static const struct pmc_reg_map nvl_pcds_reg_map = { + .pfear_sts = ext_nvl_pcds_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = nvl_pcds_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = NVL_PCDS_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = LNL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = PTL_LPM_NUM_MAPS, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = nvl_pcds_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = nvl_pcds_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .num_s0ix_blocker = NVL_PCDS_NUM_S0IX_BLOCKER, + .lpm_req_guid = PCDS_LPM_REQ_GUID, + .blocker_req_offset = NVL_PCDS_BLK_REQ_OFFSET, +}; + +static const struct pmc_reg_map nvl_pchs_reg_map = { + .pfear_sts = ext_nvl_pchs_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = ptl_pcdp_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = NVL_PCHS_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = LNL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = PTL_LPM_NUM_MAPS, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = nvl_pchs_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = nvl_pchs_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .num_s0ix_blocker = NVL_PCHS_NUM_S0IX_BLOCKER, + .blocker_req_offset = NVL_PCHS_BLK_REQ_OFFSET, + .lpm_req_guid = PCHS_LPM_REQ_GUID, +}; + +static struct pmc_info nvl_pmc_info_list[] = { + { + .devid = PMC_DEVID_NVL_PCDH, + .map = &nvl_pcdh_reg_map, + }, + { + .devid = PMC_DEVID_NVL_PCDS, + .map = &nvl_pcds_reg_map, + }, + { + .devid = PMC_DEVID_NVL_PCHS, + .map = &nvl_pchs_reg_map, + }, + {} +}; + +static const char *nvl_ltr_block_counter_arr[] = { + "PKGC_PREVENT_LTR_IADOMAIN", + "PKGC_PREVENT_LTR_GDIE", + "PKGC_PREVENT_LTR_PCH", + "PKGC_PREVENT_LTR_DISPLAY", + "PKGC_PREVENT_LTR_IPU", + NULL +}; + +static const char *nvl_pkgc_blocker_residency[] = { + "PKGC_BLOCK_RESIDENCY_INVALID", + "PKGC_BLOCK_RESIDENCY_MISC", + "PKGC_BLOCK_RESIDENCY_CDIE_MISC", + "PKGC_BLOCK_RESIDENCY_MEDIA_MISC", + "PKGC_BLOCK_RESIDENCY_GT_MISC", + "PKGC_BLOCK_RESIDENCY_HUBATOM_MISC", + "PKGC_BLOCK_RESIDENCY_IPU_BUSY", + "PKGC_BLOCK_RESIDENCY_IPU_LTR", + "PKGC_BLOCK_RESIDENCY_IPU_TIMER", + "PKGC_BLOCK_RESIDENCY_DISP_BUSY", + "PKGC_BLOCK_RESIDENCY_DISP_LTR", + "PKGC_BLOCK_RESIDENCY_DISP_TIMER", + "PKGC_BLOCK_RESIDENCY_VPU_BUSY", + "PKGC_BLOCK_RESIDENCY_VPU_TIMER", + "PKGC_BLOCK_RESIDENCY_PMC_BUSY", + "PKGC_BLOCK_RESIDENCY_PMC_LTR", + "PKGC_BLOCK_RESIDENCY_PMC_TIMER", + "PKGC_BLOCK_RESIDENCY_HUBATOM_ARAT", + "PKGC_BLOCK_RESIDENCY_CDIE0_ARAT", + "PKGC_BLOCK_RESIDENCY_CDIE1_ARAT", + "PKGC_BLOCK_RESIDENCY_GT_ARAT", + "PKGC_BLOCK_RESIDENCY_MEDIA_ARAT", + "PKGC_BLOCK_RESIDENCY_DEMOTION", + "PKGC_BLOCK_RESIDENCY_THERMALS", + "PKGC_BLOCK_RESIDENCY_SNCU", + "PKGC_BLOCK_RESIDENCY_SVTU", + "PKGC_BLOCK_RESIDENCY_IAA", + "PKGC_BLOCK_RESIDENCY_IOC", + NULL, +}; + +static const u8 nvl_pmc_list[] = {PMC_IDX_MAIN, PMC_IDX_PCH}; + +#define NVL_NPU_PCI_DEV 0xd71d + +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void nvl_d3_fixup(void) +{ + pmc_core_set_device_d3(NVL_NPU_PCI_DEV); +} + +static int nvl_resume(struct pmc_dev *pmcdev) +{ + nvl_d3_fixup(); + return cnl_resume(pmcdev); +} + +static int nvl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + nvl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +static u32 nvl_pmt_dmu_guids[] = {NVL_PMT_DMU_GUID, 0x0}; +struct pmc_dev_info nvl_s_pmc_dev = { + .num_pmcs = ARRAY_SIZE(nvl_pmc_list), + .pmc_list = nvl_pmc_list, + .regmap_list = nvl_pmc_info_list, + .map = &nvl_pcds_reg_map, + .sub_req_show = &pmc_core_substate_blk_req_fops, + .suspend = cnl_suspend, + .resume = nvl_resume, + .init = nvl_core_init, + .sub_req = pmc_core_pmt_get_blk_sub_req, + .dmu_guids = nvl_pmt_dmu_guids, + .pc_guid = NVL_PMT_PC_GUID, + .pkgc_ltr_blocker_offset = NVL_LTR_BLK_OFFSET, + .pkgc_ltr_blocker_counters = nvl_ltr_block_counter_arr, + .pkgc_blocker_offset = NVL_PKGC_BLK_OFFSET, + .pkgc_blocker_counters = nvl_pkgc_blocker_residency, + .ssram_hidden = false, + .die_c6_offset = NVL_PMT_DMU_DIE_C6_OFFSET, +}; + +struct pmc_dev_info nvl_h_pmc_dev = { + .num_pmcs = ARRAY_SIZE(nvl_pmc_list), + .pmc_list = nvl_pmc_list, + .regmap_list = nvl_pmc_info_list, + .map = &nvl_pcdh_reg_map, + .sub_req_show = &pmc_core_substate_blk_req_fops, + .suspend = cnl_suspend, + .resume = nvl_resume, + .init = nvl_core_init, + .sub_req = pmc_core_pmt_get_blk_sub_req, + .dmu_guids = nvl_pmt_dmu_guids, + .pc_guid = NVL_PMT_PC_GUID, + .pkgc_ltr_blocker_offset = NVL_LTR_BLK_OFFSET, + .pkgc_ltr_blocker_counters = nvl_ltr_block_counter_arr, + .pkgc_blocker_offset = NVL_PKGC_BLK_OFFSET, + .pkgc_blocker_counters = nvl_pkgc_blocker_residency, + .ssram_hidden = false, + .die_c6_offset = NVL_PMT_DMU_DIE_C6_OFFSET, +}; diff --git a/drivers/platform/x86/intel/pmc/pltdrv.c b/drivers/platform/x86/intel/pmc/pltdrv.c new file mode 100644 index 000000000..71ebb9ea1 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/pltdrv.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * Intel PMC Core platform init + * Copyright (c) 2019, Google Inc. + * Author - Rajat Jain + * + * This code instantiates platform devices for intel_pmc_core driver, only + * on supported platforms that may not have the ACPI devices in the ACPI tables. + * No new platforms should be added here, because we expect that new platforms + * should all have the ACPI device, which is the preferred way of enumeration. + */ + +#include +#include +#include + +#include +#include + +#include + +static void intel_pmc_core_release(struct device *dev) +{ + kfree(dev); +} + +static struct platform_device *pmc_core_device; + +/* + * intel_pmc_core_platform_ids is the list of platforms where we want to + * instantiate the platform_device if not already instantiated. This is + * different than intel_pmc_core_ids in intel_pmc_core.c which is the + * list of platforms that the driver supports for pmc_core device. The + * other list may grow, but this list should not. + */ +static const struct x86_cpu_id intel_pmc_core_platform_ids[] = { + X86_MATCH_VFM(INTEL_SKYLAKE_L, &pmc_core_device), + X86_MATCH_VFM(INTEL_SKYLAKE, &pmc_core_device), + X86_MATCH_VFM(INTEL_KABYLAKE_L, &pmc_core_device), + X86_MATCH_VFM(INTEL_KABYLAKE, &pmc_core_device), + X86_MATCH_VFM(INTEL_CANNONLAKE_L, &pmc_core_device), + X86_MATCH_VFM(INTEL_ICELAKE_L, &pmc_core_device), + X86_MATCH_VFM(INTEL_COMETLAKE, &pmc_core_device), + X86_MATCH_VFM(INTEL_COMETLAKE_L, &pmc_core_device), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, intel_pmc_core_platform_ids); + +static int __init pmc_core_platform_init(void) +{ + int retval; + + /* Skip creating the platform device if ACPI already has a device */ + if (acpi_dev_present("INT33A1", NULL, -1)) + return -ENODEV; + + /* + * Skip forcefully attaching the device for VMs. Make an exception for + * Xen dom0, which does have full hardware access. + */ + if (cpu_feature_enabled(X86_FEATURE_HYPERVISOR) && !xen_initial_domain()) + return -ENODEV; + + if (!x86_match_cpu(intel_pmc_core_platform_ids)) + return -ENODEV; + + pmc_core_device = kzalloc_obj(*pmc_core_device); + if (!pmc_core_device) + return -ENOMEM; + + pmc_core_device->name = "intel_pmc_core"; + pmc_core_device->dev.release = intel_pmc_core_release; + + retval = platform_device_register(pmc_core_device); + if (retval) + platform_device_put(pmc_core_device); + + return retval; +} + +static void __exit pmc_core_platform_exit(void) +{ + platform_device_unregister(pmc_core_device); +} + +module_init(pmc_core_platform_init); +module_exit(pmc_core_platform_exit); +MODULE_DESCRIPTION("Intel PMC Core platform driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/pmc/ptl.c b/drivers/platform/x86/intel/pmc/ptl.c new file mode 100644 index 000000000..a7d5d4f77 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/ptl.c @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Panther Lake PCH. + * + * Copyright (c) 2025, Intel Corporation. + */ + +#include + +#include "core.h" + +/* PMC SSRAM PMT Telemetry GUIDS */ +#define PCDP_LPM_REQ_GUID 0x47179370 + +/* + * Die Mapping to Product. + * Product PCDDie + * PTL-H PCD-H + * PTL-P PCD-P + * PTL-U PCD-P + */ + +static const struct pmc_bit_map ptl_pcdp_pfear_map[] = { + {"PMC_0", BIT(0)}, + {"FUSE_OSSE", BIT(1)}, + {"ESPISPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"SPB", BIT(5)}, + {"MPFPW2", BIT(6)}, + {"GBE", BIT(7)}, + + {"SBR16B20", BIT(0)}, + {"SBR8B20", BIT(1)}, + {"SBR16B21", BIT(2)}, + {"DBG_SBR16B", BIT(3)}, + {"OSSE_HOTHAM", BIT(4)}, + {"D2D_DISP_1", BIT(5)}, + {"LPSS", BIT(6)}, + {"LPC", BIT(7)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"SBR16B2", BIT(2)}, + {"NPK_0", BIT(3)}, + {"D2D_NOC_1", BIT(4)}, + {"SBR8B2", BIT(5)}, + {"FUSE", BIT(6)}, + {"SBR16B0", BIT(7)}, + + {"PSF0", BIT(0)}, + {"XDCI", BIT(1)}, + {"EXI", BIT(2)}, + {"CSE", BIT(3)}, + {"KVMCC", BIT(4)}, + {"PMT", BIT(5)}, + {"CLINK", BIT(6)}, + {"PTIO", BIT(7)}, + + {"USBR0", BIT(0)}, + {"SUSRAM", BIT(1)}, + {"SMT1", BIT(2)}, + {"MPFPW1", BIT(3)}, + {"SMS2", BIT(4)}, + {"SMS1", BIT(5)}, + {"CSMERTC", BIT(6)}, + {"CSMEPSF", BIT(7)}, + + {"D2D_NOC_0", BIT(0)}, + {"ESE", BIT(1)}, + {"P2SB8B", BIT(2)}, + {"SBR16B7", BIT(3)}, + {"SBR16B3", BIT(4)}, + {"OSSE_SMT1", BIT(5)}, + {"D2D_DISP", BIT(6)}, + {"DBG_SBR", BIT(7)}, + + {"U3FPW1", BIT(0)}, + {"FIA_X", BIT(1)}, + {"PSF4", BIT(2)}, + {"CNVI", BIT(3)}, + {"UFSX2", BIT(4)}, + {"ENDBG", BIT(5)}, + {"DBC", BIT(6)}, + {"FIA_PG", BIT(7)}, + + {"D2D_IPU", BIT(0)}, + {"NPK1", BIT(1)}, + {"FIACPCB_X", BIT(2)}, + {"SBR8B4", BIT(3)}, + {"DBG_PSF", BIT(4)}, + {"PSF6", BIT(5)}, + {"UFSPW1", BIT(6)}, + {"FIA_U", BIT(7)}, + + {"PSF8", BIT(0)}, + {"SBR16B4", BIT(1)}, + {"SBR16B5", BIT(2)}, + {"FIACPCB_U", BIT(3)}, + {"TAM", BIT(4)}, + {"D2D_NOC_2", BIT(5)}, + {"TBTLSX", BIT(6)}, + {"THC0", BIT(7)}, + + {"THC1", BIT(0)}, + {"PMC_1", BIT(1)}, + {"SBR8B1", BIT(2)}, + {"TCSS", BIT(3)}, + {"DISP_PGA", BIT(4)}, + {"SBR16B1", BIT(5)}, + {"SBRG", BIT(6)}, + {"PSF5", BIT(7)}, + + {"P2SB16B", BIT(0)}, + {"ACE_0", BIT(1)}, + {"ACE_1", BIT(2)}, + {"ACE_2", BIT(3)}, + {"ACE_3", BIT(4)}, + {"ACE_4", BIT(5)}, + {"ACE_5", BIT(6)}, + {"ACE_6", BIT(7)}, + + {"ACE_7", BIT(0)}, + {"ACE_8", BIT(1)}, + {"ACE_9", BIT(2)}, + {"ACE_10", BIT(3)}, + {"FIACPCB_PG", BIT(4)}, + {"SBR16B6", BIT(5)}, + {"OSSE", BIT(6)}, + {"SBR8B0", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_ptl_pcdp_pfear_map[] = { + ptl_pcdp_pfear_map, + NULL +}; + +const struct pmc_bit_map ptl_pcdp_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"SOUTHPORT_B", CNP_PMC_LTR_SPB}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", PTL_PMC_LTR_SATA2}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"ESE", MTL_PMC_LTR_ESE}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + {"OSSE", LNL_PMC_LTR_OSSE}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", PTL_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", PTL_PMC_LTR_CUR_ASLT}, + {} +}; + +const struct pmc_bit_map ptl_pcdp_clocksource_status_map[] = { + {"AON2_OFF_STS", BIT(0), 1}, + {"AON3_OFF_STS", BIT(1), 0}, + {"AON4_OFF_STS", BIT(2), 1}, + {"AON5_OFF_STS", BIT(3), 1}, + {"AON1_OFF_STS", BIT(4), 0}, + {"XTAL_LVM_OFF_STS", BIT(5), 0}, + {"MPFPW1_0_PLL_OFF_STS", BIT(6), 1}, + {"USB3_PLL_OFF_STS", BIT(8), 1}, + {"AON3_SPL_OFF_STS", BIT(9), 1}, + {"MPFPW2_0_PLL_OFF_STS", BIT(12), 1}, + {"XTAL_AGGR_OFF_STS", BIT(17), 1}, + {"USB2_PLL_OFF_STS", BIT(18), 0}, + {"SAF_PLL_OFF_STS", BIT(19), 1}, + {"SE_TCSS_PLL_OFF_STS", BIT(20), 1}, + {"DDI_PLL_OFF_STS", BIT(21), 1}, + {"FILTER_PLL_OFF_STS", BIT(22), 1}, + {"ACE_PLL_OFF_STS", BIT(24), 0}, + {"FABRIC_PLL_OFF_STS", BIT(25), 1}, + {"SOC_PLL_OFF_STS", BIT(26), 1}, + {"REF_PLL_OFF_STS", BIT(28), 1}, + {"IMG_PLL_OFF_STS", BIT(29), 1}, + {"RTC_PLL_OFF_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FUSE_OSSE_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 1}, + {"SPA_PGD0_PG_STS", BIT(4), 1}, + {"SPB_PGD0_PG_STS", BIT(5), 1}, + {"MPFPW2_PGD0_PG_STS", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 1}, + {"SBR16B20_PGD0_PG_STS", BIT(8), 0}, + {"SBR8B20_PGD0_PG_STS", BIT(9), 0}, + {"SBR16B21_PGD0_PG_STS", BIT(10), 0}, + {"DBG_PGD0_PG_STS", BIT(11), 0}, + {"OSSE_HOTHAM_PGD0_PG_STS", BIT(12), 1}, + {"D2D_DISP_PGD1_PG_STS", BIT(13), 1}, + {"LPSS_PGD0_PG_STS", BIT(14), 1}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"SBR16B2_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD1_PG_STS", BIT(20), 1}, + {"SBR8B2_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"SBR16B0_PGD0_PG_STS", BIT(23), 0}, + {"PSF0_PGD0_PG_STS", BIT(24), 0}, + {"XDCI_PGD0_PG_STS", BIT(25), 1}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 1}, + {"PMT_PGD0_PG_STS", BIT(29), 1}, + {"CLINK_PGD0_PG_STS", BIT(30), 1}, + {"PTIO_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 1}, + {"SUSRAM_PGD0_PG_STS", BIT(1), 1}, + {"SMT1_PGD0_PG_STS", BIT(2), 1}, + {"MPFPW1_PGD0_PG_STS", BIT(3), 0}, + {"SMS2_PGD0_PG_STS", BIT(4), 1}, + {"SMS1_PGD0_PG_STS", BIT(5), 1}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(8), 0}, + {"ESE_PGD0_PG_STS", BIT(9), 1}, + {"P2SB8B_PGD0_PG_STS", BIT(10), 1}, + {"SBR16B7_PGD0_PG_STS", BIT(11), 0}, + {"SBR16B3_PGD0_PG_STS", BIT(12), 0}, + {"OSSE_SMT1_PGD0_PG_STS", BIT(13), 1}, + {"D2D_DISP_PGD0_PG_STS", BIT(14), 1}, + {"DBG_SBR_PGD0_PG_STS", BIT(15), 0}, + {"U3FPW1_PGD0_PG_STS", BIT(16), 0}, + {"FIA_X_PGD0_PG_STS", BIT(17), 0}, + {"PSF4_PGD0_PG_STS", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"UFSX2_PGD0_PG_STS", BIT(20), 1}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"DBC_PGD0_PG_STS", BIT(22), 0}, + {"FIA_PG_PGD0_PG_STS", BIT(23), 0}, + {"D2D_IPU_PGD0_PG_STS", BIT(24), 1}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"FIACPCB_X_PGD0_PG_STS", BIT(26), 0}, + {"SBR8B4_PGD0_PG_STS", BIT(27), 0}, + {"DBG_PSF_PGD0_PG_STS", BIT(28), 0}, + {"PSF6_PGD0_PG_STS", BIT(29), 0}, + {"UFSPW1_PGD0_PG_STS", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0), 0}, + {"SBR16B4_PGD0_PG_STS", BIT(1), 0}, + {"SBR16B5_PGD0_PG_STS", BIT(2), 0}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 1}, + {"D2D_NOC_PGD0_PG_STS", BIT(5), 1}, + {"TBTLSX_PGD0_PG_STS", BIT(6), 1}, + {"THC0_PGD0_PG_STS", BIT(7), 1}, + {"THC1_PGD0_PG_STS", BIT(8), 1}, + {"PMC_PGD1_PG_STS", BIT(9), 0}, + {"SBR8B1_PGD0_PG_STS", BIT(10), 0}, + {"TCSS_PGD0_PG_STS", BIT(11), 0}, + {"DISP_PGA_PGD0_PG_STS", BIT(12), 0}, + {"SBR16B1_PGD0_PG_STS", BIT(13), 0}, + {"SBRG_PGD0_PG_STS", BIT(14), 0}, + {"PSF5_PGD0_PG_STS", BIT(15), 0}, + {"P2SB16B_PGD0_PG_STS", BIT(16), 1}, + {"ACE_PGD0_PG_STS", BIT(17), 0}, + {"ACE_PGD1_PG_STS", BIT(18), 0}, + {"ACE_PGD2_PG_STS", BIT(19), 0}, + {"ACE_PGD3_PG_STS", BIT(20), 0}, + {"ACE_PGD4_PG_STS", BIT(21), 0}, + {"ACE_PGD5_PG_STS", BIT(22), 0}, + {"ACE_PGD6_PG_STS", BIT(23), 0}, + {"ACE_PGD7_PG_STS", BIT(24), 0}, + {"ACE_PGD8_PG_STS", BIT(25), 0}, + {"ACE_PGD9_PG_STS", BIT(26), 0}, + {"ACE_PGD10_PG_STS", BIT(27), 0}, + {"FIACPCB_PG_PGD0_PG_STS", BIT(28), 0}, + {"SBR16B6_PGD0_PG_STS", BIT(29), 0}, + {"OSSE_PGD0_PG_STS", BIT(30), 1}, + {"SBR8B0_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 1}, + {"OSSE_D3_STS", BIT(6), 0}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPB_D3_STS", BIT(13), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"PSTH_D3_STS", BIT(21), 0}, + {"OSSE_SMT1_D3_STS", BIT(30), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_d3_status_1_map[] = { + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {"UFSX2_D3_STS", BIT(28), 1}, + {"OSSE_HOTHAM_D3_STS", BIT(29), 0}, + {"ESE_D3_STS", BIT(30), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1), 0}, + {"SUSRAM_D3_STS", BIT(2), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"OSSE_SMT2_D3_STS", BIT(12), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14), 1}, + {"THC1_D3_STS", BIT(15), 1}, + {"OSSE_SMT3_D3_STS", BIT(18), 0}, + {"ACE_D3_STS", BIT(23), 0}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3), 1}, + {"OSSE_VNN_REQ_STS", BIT(6), 1}, + {"ESPISPI_VNN_REQ_STS", BIT(18), 1}, + {"OSSE_SMT1_VNN_REQ_STS", BIT(30), 1}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4), 1}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 1}, + {"P2D_VNN_REQ_STS", BIT(18), 1}, + {"GBE_VNN_REQ_STS", BIT(19), 1}, + {"SMB_VNN_REQ_STS", BIT(25), 1}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {"ESE_VNN_REQ_STS", BIT(30), 1}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1), 1}, + {"CSE_VNN_REQ_STS", BIT(4), 1}, + {"ISH_VNN_REQ_STS", BIT(7), 1}, + {"SMT1_VNN_REQ_STS", BIT(8), 1}, + {"CLINK_VNN_REQ_STS", BIT(14), 1}, + {"SMS1_VNN_REQ_STS", BIT(18), 1}, + {"SMS2_VNN_REQ_STS", BIT(19), 1}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 1}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 1}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 1}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 1}, + {"DISP_SHIM_VNN_REQ_STS", BIT(26), 1}, + {} +}; + +const struct pmc_bit_map ptl_pcdp_vnn_req_status_3_map[] = { + {"DTS0_VNN_REQ_STS", BIT(7), 0}, + {"GPIOCOM5_VNN_REQ_STS", BIT(11), 1}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"PG5_PMA0_REQ_STS", BIT(3), 0}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4), 0}, + {"VNN_SOC_REQ_STS", BIT(6), 1}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"D2D_NOC_CFI_QACTIVE_REQ_STS", BIT(8), 1}, + {"D2D_NOC_GPSB_QACTIVE_REQ_STS", BIT(9), 1}, + {"D2D_IPU_QACTIVE_REQ_STS", BIT(10), 1}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"ALL_SBR_IDLE_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"PG5_PMA1_REQ_STS", BIT(22), 0}, + {"FIA_DEEP_PM_REQ_STS", BIT(23), 0}, + {"XDCI_ATTACHED_REQ_STS", BIT(24), 1}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"D2D_DISP_DDI_QACTIVE_REQ_STS", BIT(26), 1}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_REQ_STS", BIT(29), 1}, + {"D2D_DISP_EDP_QACTIVE_REQ_STS", BIT(31), 1}, + {} +}; + +const struct pmc_bit_map ptl_pcdp_signal_status_map[] = { + {"LSX_Wake0_STS", BIT(0), 0}, + {"LSX_Wake1_STS", BIT(1), 0}, + {"LSX_Wake2_STS", BIT(2), 0}, + {"LSX_Wake3_STS", BIT(3), 0}, + {"LSX_Wake4_STS", BIT(4), 0}, + {"LSX_Wake5_STS", BIT(5), 0}, + {"LSX_Wake6_STS", BIT(6), 0}, + {"LSX_Wake7_STS", BIT(7), 0}, + {"LPSS_Wake0_STS", BIT(8), 1}, + {"LPSS_Wake1_STS", BIT(9), 1}, + {"Int_Timer_SS_Wake0_STS", BIT(10), 1}, + {"Int_Timer_SS_Wake1_STS", BIT(11), 1}, + {"Int_Timer_SS_Wake2_STS", BIT(12), 1}, + {"Int_Timer_SS_Wake3_STS", BIT(13), 1}, + {"Int_Timer_SS_Wake4_STS", BIT(14), 1}, + {"Int_Timer_SS_Wake5_STS", BIT(15), 1}, + {} +}; + +static const struct pmc_bit_map ptl_pcdp_rsc_status_map[] = { + {"Memory", 0, 1}, + {"PSF0", 0, 1}, + {"PSF4", 0, 1}, + {"PSF5", 0, 1}, + {"PSF6", 0, 1}, + {"PSF8", 0, 1}, + {"SAF_CFI_LINK", 0, 1}, + {"SB", 0, 1}, + {} +}; + +static const struct pmc_bit_map *ptl_pcdp_lpm_maps[] = { + ptl_pcdp_clocksource_status_map, + ptl_pcdp_power_gating_status_0_map, + ptl_pcdp_power_gating_status_1_map, + ptl_pcdp_power_gating_status_2_map, + ptl_pcdp_d3_status_0_map, + ptl_pcdp_d3_status_1_map, + ptl_pcdp_d3_status_2_map, + ptl_pcdp_d3_status_3_map, + ptl_pcdp_vnn_req_status_0_map, + ptl_pcdp_vnn_req_status_1_map, + ptl_pcdp_vnn_req_status_2_map, + ptl_pcdp_vnn_req_status_3_map, + ptl_pcdp_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *ptl_pcdp_blk_maps[] = { + ptl_pcdp_power_gating_status_0_map, + ptl_pcdp_power_gating_status_1_map, + ptl_pcdp_power_gating_status_2_map, + ptl_pcdp_rsc_status_map, + ptl_pcdp_vnn_req_status_0_map, + ptl_pcdp_vnn_req_status_1_map, + ptl_pcdp_vnn_req_status_2_map, + ptl_pcdp_vnn_req_status_3_map, + ptl_pcdp_d3_status_0_map, + ptl_pcdp_d3_status_1_map, + ptl_pcdp_d3_status_2_map, + ptl_pcdp_d3_status_3_map, + ptl_pcdp_clocksource_status_map, + ptl_pcdp_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_reg_map ptl_pcdp_reg_map = { + .pfear_sts = ext_ptl_pcdp_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = ptl_pcdp_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = PTL_PCD_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = LNL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = PTL_LPM_NUM_MAPS, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = ptl_pcdp_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = ptl_pcdp_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .num_s0ix_blocker = PTL_NUM_S0IX_BLOCKER, + .blocker_req_offset = PTL_BLK_REQ_OFFSET, + .lpm_req_guid = PCDP_LPM_REQ_GUID, +}; + +static struct pmc_info ptl_pmc_info_list[] = { + { + .devid = PMC_DEVID_PTL_PCDH, + .map = &ptl_pcdp_reg_map, + }, + { + .devid = PMC_DEVID_PTL_PCDP, + .map = &ptl_pcdp_reg_map, + }, + {} +}; + +static const u8 ptl_pmc_list[] = {PMC_IDX_MAIN}; + +#define PTL_NPU_PCI_DEV 0xb03e +#define PTL_IPU_PCI_DEV 0xb05d + +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void ptl_d3_fixup(void) +{ + pmc_core_set_device_d3(PTL_IPU_PCI_DEV); + pmc_core_set_device_d3(PTL_NPU_PCI_DEV); +} + +static int ptl_resume(struct pmc_dev *pmcdev) +{ + ptl_d3_fixup(); + return cnl_resume(pmcdev); +} + +static int ptl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + ptl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +struct pmc_dev_info ptl_pmc_dev = { + .num_pmcs = ARRAY_SIZE(ptl_pmc_list), + .pmc_list = ptl_pmc_list, + .regmap_list = ptl_pmc_info_list, + .map = &ptl_pcdp_reg_map, + .sub_req_show = &pmc_core_substate_blk_req_fops, + .suspend = cnl_suspend, + .resume = ptl_resume, + .init = ptl_core_init, + .sub_req = pmc_core_pmt_get_blk_sub_req, + .ssram_hidden = true, +}; diff --git a/drivers/platform/x86/intel/pmc/pwrm_telemetry.c b/drivers/platform/x86/intel/pmc/pwrm_telemetry.c new file mode 100644 index 000000000..4cde241e0 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/pwrm_telemetry.c @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Intel PMC PWRM ACPI driver + * + * Copyright (C) 2025, Intel Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" + +#define ENTRY_LEN 5 + +/* DWORD2 */ +#define DVSEC_ID_MASK GENMASK(15, 0) +#define NUM_ENTRIES_MASK GENMASK(23, 16) +#define ENTRY_SIZE_MASK GENMASK(31, 24) + +/* DWORD3 */ +#define TBIR_MASK GENMASK(2, 0) +#define DISC_TBL_OFF_MASK GENMASK(31, 3) + +const guid_t intel_vsec_guid = + GUID_INIT(0x294903fb, 0x634d, 0x4fc7, 0xaf, 0x1f, 0x0f, 0xb9, + 0x56, 0xb0, 0x4f, 0xc1); + +static bool is_valid_entry(union acpi_object *pkg) +{ + int i; + + if (!pkg || pkg->type != ACPI_TYPE_PACKAGE || pkg->package.count != ENTRY_LEN) + return false; + + if (pkg->package.elements[0].type != ACPI_TYPE_STRING) + return false; + + for (i = 1; i < ENTRY_LEN; i++) + if (pkg->package.elements[i].type != ACPI_TYPE_INTEGER) + return false; + + return true; +} + +acpi_disc_t pmc_parse_telem_dsd(union acpi_object *obj, + struct intel_vsec_header *header) +{ + union acpi_object *vsec_pkg; + union acpi_object *disc_pkg; + u64 hdr0, hdr1; + int num_regions; + int i; + + if (!header) + return ERR_PTR(-EINVAL); + + if (!obj || obj->type != ACPI_TYPE_PACKAGE || obj->package.count != 2) + return ERR_PTR(-EINVAL); + + /* First Package is DVSEC info */ + vsec_pkg = &obj->package.elements[0]; + if (!is_valid_entry(vsec_pkg)) + return ERR_PTR(-EINVAL); + + hdr0 = vsec_pkg->package.elements[3].integer.value; + hdr1 = vsec_pkg->package.elements[4].integer.value; + + header->id = FIELD_GET(DVSEC_ID_MASK, hdr0); + header->num_entries = FIELD_GET(NUM_ENTRIES_MASK, hdr0); + header->entry_size = FIELD_GET(ENTRY_SIZE_MASK, hdr0); + header->tbir = FIELD_GET(TBIR_MASK, hdr1); + header->offset = hdr1 & DISC_TBL_OFF_MASK; + + /* Second Package contains the discovery tables */ + disc_pkg = &obj->package.elements[1]; + if (disc_pkg->type != ACPI_TYPE_PACKAGE || disc_pkg->package.count < 1) + return ERR_PTR(-EINVAL); + + num_regions = disc_pkg->package.count; + if (header->num_entries != num_regions) + return ERR_PTR(-EINVAL); + + acpi_disc_t disc __free(kfree) = kmalloc_array(num_regions, sizeof(*disc), + GFP_KERNEL); + if (!disc) + return ERR_PTR(-ENOMEM); + + for (i = 0; i < num_regions; i++) { + union acpi_object *pkg; + u64 value; + int j; + + pkg = &disc_pkg->package.elements[i]; + if (!is_valid_entry(pkg)) + return ERR_PTR(-EINVAL); + + /* Element 0 is a descriptive string; DWORD values start at index 1. */ + for (j = 1; j < ENTRY_LEN; j++) { + value = pkg->package.elements[j].integer.value; + if (value > U32_MAX) + return ERR_PTR(-ERANGE); + + disc[i][j - 1] = value; + } + } + + return no_free_ptr(disc); +} +EXPORT_SYMBOL_NS_GPL(pmc_parse_telem_dsd, "INTEL_PMC_CORE"); + +union acpi_object *pmc_find_telem_guid(union acpi_object *dsd) +{ + int i; + + if (!dsd || dsd->type != ACPI_TYPE_PACKAGE) + return NULL; + + for (i = 0; i + 1 < dsd->package.count; i += 2) { + union acpi_object *uuid_obj, *data_obj; + guid_t uuid; + + uuid_obj = &dsd->package.elements[i]; + data_obj = &dsd->package.elements[i + 1]; + + if (uuid_obj->type != ACPI_TYPE_BUFFER || + uuid_obj->buffer.length != 16) + continue; + + memcpy(&uuid, uuid_obj->buffer.pointer, 16); + if (guid_equal(&uuid, &intel_vsec_guid)) + return data_obj; + } + + return NULL; +} +EXPORT_SYMBOL_NS_GPL(pmc_find_telem_guid, "INTEL_PMC_CORE"); + +static int pmc_pwrm_acpi_probe(struct platform_device *pdev) +{ + struct intel_vsec_header header; + struct intel_vsec_header *headers[2] = { &header, NULL }; + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + struct intel_vsec_platform_info info = { }; + struct device *dev = &pdev->dev; + union acpi_object *dsd; + struct resource *res; + acpi_handle handle; + acpi_status status; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + status = acpi_evaluate_object(handle, "_DSD", NULL, &buf); + if (ACPI_FAILURE(status)) { + return dev_err_probe(dev, -ENODEV, "Could not evaluate _DSD: %s\n", + acpi_format_exception(status)); + } + + void *dsd_buf __free(pmc_acpi_free) = buf.pointer; + + dsd = pmc_find_telem_guid(dsd_buf); + if (!dsd) + return -ENODEV; + + acpi_disc_t acpi_disc __free(kfree) = pmc_parse_telem_dsd(dsd, &header); + if (IS_ERR(acpi_disc)) + return PTR_ERR(acpi_disc); + + res = platform_get_resource(pdev, IORESOURCE_MEM, header.tbir); + if (!res) + return -EINVAL; + + info.headers = headers; + info.caps = VSEC_CAP_TELEMETRY; + info.acpi_disc = acpi_disc; + info.src = INTEL_VSEC_DISC_ACPI; + info.base_addr = res->start; + + return intel_vsec_register(&pdev->dev, &info); +} + +static const struct acpi_device_id pmc_pwrm_acpi_ids[] = { + { "INTC1122", 0 }, /* Nova Lake */ + { "INTC1129", 0 }, /* Nova Lake */ + { } +}; +MODULE_DEVICE_TABLE(acpi, pmc_pwrm_acpi_ids); + +static struct platform_driver pmc_pwrm_acpi_driver = { + .probe = pmc_pwrm_acpi_probe, + .driver = { + .name = "intel_pmc_pwrm_acpi", + .acpi_match_table = ACPI_PTR(pmc_pwrm_acpi_ids), + }, +}; +module_platform_driver(pmc_pwrm_acpi_driver); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMC PWRM ACPI driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("INTEL_VSEC"); diff --git a/drivers/platform/x86/intel/pmc/spt.c b/drivers/platform/x86/intel/pmc/spt.c new file mode 100644 index 000000000..b50534aa2 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/spt.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Sunrise Point PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include + +#include "core.h" + +static const struct pmc_bit_map spt_pll_map[] = { + {"MIPI PLL", SPT_PMC_BIT_MPHY_CMN_LANE0}, + {"GEN2 USB2PCIE2 PLL", SPT_PMC_BIT_MPHY_CMN_LANE1}, + {"DMIPCIE3 PLL", SPT_PMC_BIT_MPHY_CMN_LANE2}, + {"SATA PLL", SPT_PMC_BIT_MPHY_CMN_LANE3}, + {} +}; + +static const struct pmc_bit_map spt_mphy_map[] = { + {"MPHY CORE LANE 0", SPT_PMC_BIT_MPHY_LANE0}, + {"MPHY CORE LANE 1", SPT_PMC_BIT_MPHY_LANE1}, + {"MPHY CORE LANE 2", SPT_PMC_BIT_MPHY_LANE2}, + {"MPHY CORE LANE 3", SPT_PMC_BIT_MPHY_LANE3}, + {"MPHY CORE LANE 4", SPT_PMC_BIT_MPHY_LANE4}, + {"MPHY CORE LANE 5", SPT_PMC_BIT_MPHY_LANE5}, + {"MPHY CORE LANE 6", SPT_PMC_BIT_MPHY_LANE6}, + {"MPHY CORE LANE 7", SPT_PMC_BIT_MPHY_LANE7}, + {"MPHY CORE LANE 8", SPT_PMC_BIT_MPHY_LANE8}, + {"MPHY CORE LANE 9", SPT_PMC_BIT_MPHY_LANE9}, + {"MPHY CORE LANE 10", SPT_PMC_BIT_MPHY_LANE10}, + {"MPHY CORE LANE 11", SPT_PMC_BIT_MPHY_LANE11}, + {"MPHY CORE LANE 12", SPT_PMC_BIT_MPHY_LANE12}, + {"MPHY CORE LANE 13", SPT_PMC_BIT_MPHY_LANE13}, + {"MPHY CORE LANE 14", SPT_PMC_BIT_MPHY_LANE14}, + {"MPHY CORE LANE 15", SPT_PMC_BIT_MPHY_LANE15}, + {} +}; + +static const struct pmc_bit_map spt_pfear_map[] = { + {"PMC", SPT_PMC_BIT_PMC}, + {"OPI-DMI", SPT_PMC_BIT_OPI}, + {"SPI / eSPI", SPT_PMC_BIT_SPI}, + {"XHCI", SPT_PMC_BIT_XHCI}, + {"SPA", SPT_PMC_BIT_SPA}, + {"SPB", SPT_PMC_BIT_SPB}, + {"SPC", SPT_PMC_BIT_SPC}, + {"GBE", SPT_PMC_BIT_GBE}, + {"SATA", SPT_PMC_BIT_SATA}, + {"HDA-PGD0", SPT_PMC_BIT_HDA_PGD0}, + {"HDA-PGD1", SPT_PMC_BIT_HDA_PGD1}, + {"HDA-PGD2", SPT_PMC_BIT_HDA_PGD2}, + {"HDA-PGD3", SPT_PMC_BIT_HDA_PGD3}, + {"RSVD", SPT_PMC_BIT_RSVD_0B}, + {"LPSS", SPT_PMC_BIT_LPSS}, + {"LPC", SPT_PMC_BIT_LPC}, + {"SMB", SPT_PMC_BIT_SMB}, + {"ISH", SPT_PMC_BIT_ISH}, + {"P2SB", SPT_PMC_BIT_P2SB}, + {"DFX", SPT_PMC_BIT_DFX}, + {"SCC", SPT_PMC_BIT_SCC}, + {"RSVD", SPT_PMC_BIT_RSVD_0C}, + {"FUSE", SPT_PMC_BIT_FUSE}, + {"CAMERA", SPT_PMC_BIT_CAMREA}, + {"RSVD", SPT_PMC_BIT_RSVD_0D}, + {"USB3-OTG", SPT_PMC_BIT_USB3_OTG}, + {"EXI", SPT_PMC_BIT_EXI}, + {"CSE", SPT_PMC_BIT_CSE}, + {"CSME_KVM", SPT_PMC_BIT_CSME_KVM}, + {"CSME_PMT", SPT_PMC_BIT_CSME_PMT}, + {"CSME_CLINK", SPT_PMC_BIT_CSME_CLINK}, + {"CSME_PTIO", SPT_PMC_BIT_CSME_PTIO}, + {"CSME_USBR", SPT_PMC_BIT_CSME_USBR}, + {"CSME_SUSRAM", SPT_PMC_BIT_CSME_SUSRAM}, + {"CSME_SMT", SPT_PMC_BIT_CSME_SMT}, + {"RSVD", SPT_PMC_BIT_RSVD_1A}, + {"CSME_SMS2", SPT_PMC_BIT_CSME_SMS2}, + {"CSME_SMS1", SPT_PMC_BIT_CSME_SMS1}, + {"CSME_RTC", SPT_PMC_BIT_CSME_RTC}, + {"CSME_PSF", SPT_PMC_BIT_CSME_PSF}, + {} +}; + +static const struct pmc_bit_map *ext_spt_pfear_map[] = { + /* + * Check intel_pmc_core_ids[] users of spt_reg_map for + * a list of core SoCs using this. + */ + spt_pfear_map, + NULL +}; + +static const struct pmc_bit_map spt_ltr_show_map[] = { + {"SOUTHPORT_A", SPT_PMC_LTR_SPA}, + {"SOUTHPORT_B", SPT_PMC_LTR_SPB}, + {"SATA", SPT_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", SPT_PMC_LTR_GBE}, + {"XHCI", SPT_PMC_LTR_XHCI}, + {"Reserved", SPT_PMC_LTR_RESERVED}, + {"ME", SPT_PMC_LTR_ME}, + /* EVA is Enterprise Value Add, doesn't really exist on PCH */ + {"EVA", SPT_PMC_LTR_EVA}, + {"SOUTHPORT_C", SPT_PMC_LTR_SPC}, + {"HD_AUDIO", SPT_PMC_LTR_AZ}, + {"LPSS", SPT_PMC_LTR_LPSS}, + {"SOUTHPORT_D", SPT_PMC_LTR_SPD}, + {"SOUTHPORT_E", SPT_PMC_LTR_SPE}, + {"CAMERA", SPT_PMC_LTR_CAM}, + {"ESPI", SPT_PMC_LTR_ESPI}, + {"SCC", SPT_PMC_LTR_SCC}, + {"ISH", SPT_PMC_LTR_ISH}, + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", SPT_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", SPT_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_reg_map spt_reg_map = { + .pfear_sts = ext_spt_pfear_map, + .mphy_sts = spt_mphy_map, + .pll_sts = spt_pll_map, + .ltr_show_sts = spt_ltr_show_map, + .msr_sts = msr_map, + .slp_s0_offset = SPT_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = SPT_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_ignore_offset = SPT_PMC_LTR_IGNORE_OFFSET, + .regmap_length = SPT_PMC_MMIO_REG_LEN, + .ppfear0_offset = SPT_PMC_XRAM_PPFEAR0A, + .ppfear_buckets = SPT_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = SPT_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = SPT_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = SPT_NUM_IP_IGN_ALLOWED, + .pm_vric1_offset = SPT_PMC_VRIC1_OFFSET, +}; + +static const struct pci_device_id spt_pmc_pci_id[] = { + { PCI_VDEVICE(INTEL, SPT_PMC_PCI_DEVICE_ID) }, + { } +}; + +static int spt_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + /* + * Coffee Lake has CPU ID of Kaby Lake and Cannon Lake PCH. So here + * Sunrisepoint PCH regmap can't be used. Use Cannon Lake PCH regmap + * in this case. + */ + if (!pci_dev_present(spt_pmc_pci_id)) + return generic_core_init(pmcdev, &cnp_pmc_dev); + + return generic_core_init(pmcdev, pmc_dev_info); +} + +struct pmc_dev_info spt_pmc_dev = { + .map = &spt_reg_map, + .init = spt_core_init, +}; diff --git a/drivers/platform/x86/intel/pmc/ssram_telemetry.c b/drivers/platform/x86/intel/pmc/ssram_telemetry.c new file mode 100644 index 000000000..8e280ec52 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/ssram_telemetry.c @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel PMC SSRAM TELEMETRY PCI Driver + * + * Copyright (c) 2023, Intel Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "ssram_telemetry.h" + +#define SSRAM_HDR_SIZE 0x100 +#define SSRAM_PWRM_OFFSET 0x14 +#define SSRAM_DVSEC_OFFSET 0x1C +#define SSRAM_DVSEC_SIZE 0x10 +#define SSRAM_PCH_OFFSET 0x60 +#define SSRAM_IOE_OFFSET 0x68 +#define SSRAM_DEVID_OFFSET 0x70 +#define SSRAM_BASE_ADDR_MASK GENMASK_ULL(63, 3) +#define SSRAM_PCI_PMC_MASK (BIT(PMC_IDX_MAIN) | BIT(PMC_IDX_IOE) | BIT(PMC_IDX_PCH)) + +DEFINE_FREE(pmc_ssram_telemetry_iounmap, void __iomem *, if (_T) iounmap(_T)) + +enum resource_method { + RES_METHOD_PCI, + RES_METHOD_ACPI, +}; + +struct ssram_type { + enum resource_method method; + enum pmc_index p_index; +}; + +static const struct ssram_type pci_main = { + .method = RES_METHOD_PCI, + .p_index = PMC_IDX_MAIN, +}; + +static const struct ssram_type acpi_main = { + .method = RES_METHOD_ACPI, + .p_index = PMC_IDX_MAIN, +}; + +static const struct ssram_type acpi_pch = { + .method = RES_METHOD_ACPI, + .p_index = PMC_IDX_PCH, +}; + +enum pmc_ssram_state { + PMC_SSRAM_UNPROBED = 0, + PMC_SSRAM_PROBING, + PMC_SSRAM_PRESENT, + PMC_SSRAM_ABSENT, +}; + +static enum pmc_ssram_state pmc_ssram_state[MAX_NUM_PMC]; +static struct pmc_ssram_telemetry pmc_ssram_telems[MAX_NUM_PMC]; + +struct pmc_ssram_probe_cache { + /* Per-index values staged by probe before publishing globally. */ + struct pmc_ssram_telemetry telems[MAX_NUM_PMC]; + /* PMCs this probe instance is responsible for publishing. */ + unsigned long owned_mask; + /* Subset of owned_mask that was discovered successfully. */ + unsigned long valid_mask; +}; + +struct pmc_ssram_drvdata { + /* PMCs published by this bound device; used to unpublish on remove. */ + unsigned long owned_mask; +}; + +static inline u64 get_base(void __iomem *addr, u32 offset) +{ + return lo_hi_readq(addr + offset) & SSRAM_BASE_ADDR_MASK; +} + +static void pmc_ssram_get_devid_pwrmbase(struct pmc_ssram_probe_cache *probe_cache, + void __iomem *ssram, unsigned int pmc_idx) +{ + u64 pwrm_base; + u16 devid; + + pwrm_base = get_base(ssram, SSRAM_PWRM_OFFSET); + devid = readw(ssram + SSRAM_DEVID_OFFSET); + + probe_cache->telems[pmc_idx].base_addr = pwrm_base; + probe_cache->telems[pmc_idx].devid = devid; +} + +static void pmc_ssram_publish_absent(unsigned int pmc_idx) +{ + /* + * Publish only the state without modifying base_addr and devid. This + * lets a reader that already observed PRESENT finish copying the + * previous values even if unbind concurrently publishes ABSENT. Readers + * that observe ABSENT return -ENODEV without accessing data. + */ + smp_store_release(&pmc_ssram_state[pmc_idx], PMC_SSRAM_ABSENT); +} + +static void pmc_ssram_publish_present(struct pmc_ssram_probe_cache *probe_cache, + unsigned int pmc_idx) +{ + /* + * The devid and base_addr fields are read from hardware MMIO registers + * whose values are stable for a given PMC index. A reader that observed + * PRESENT from an earlier probe can safely copy them while a concurrent + * rebind republishes those fields because both probes read the same + * hardware values. + */ + pmc_ssram_telems[pmc_idx] = probe_cache->telems[pmc_idx]; + /* + * Publish base_addr and devid before publishing PRESENT. Pairs with the + * acquire load in the reader that consumes them after observing PRESENT. + */ + smp_store_release(&pmc_ssram_state[pmc_idx], PMC_SSRAM_PRESENT); +} + +static void pmc_ssram_mark_probing(unsigned long mask) +{ + unsigned long bit; + + for_each_set_bit(bit, &mask, MAX_NUM_PMC) + WRITE_ONCE(pmc_ssram_state[bit], PMC_SSRAM_PROBING); +} + +static int +pmc_ssram_telemetry_add_pmt(struct pci_dev *pcidev, u64 ssram_base, void __iomem *ssram) +{ + struct intel_vsec_platform_info info = {}; + struct intel_vsec_header *headers[2] = {}; + struct intel_vsec_header header; + void __iomem *dvsec; + u32 dvsec_offset; + u32 table, hdr; + + dvsec_offset = readl(ssram + SSRAM_DVSEC_OFFSET); + dvsec = ioremap(ssram_base + dvsec_offset, SSRAM_DVSEC_SIZE); + if (!dvsec) + return -ENOMEM; + + hdr = readl(dvsec + PCI_DVSEC_HEADER1); + header.id = readw(dvsec + PCI_DVSEC_HEADER2); + header.rev = PCI_DVSEC_HEADER1_REV(hdr); + header.length = PCI_DVSEC_HEADER1_LEN(hdr); + header.num_entries = readb(dvsec + INTEL_DVSEC_ENTRIES); + header.entry_size = readb(dvsec + INTEL_DVSEC_SIZE); + + table = readl(dvsec + INTEL_DVSEC_TABLE); + header.tbir = INTEL_DVSEC_TABLE_BAR(table); + header.offset = INTEL_DVSEC_TABLE_OFFSET(table); + iounmap(dvsec); + + headers[0] = &header; + info.caps = VSEC_CAP_TELEMETRY; + info.headers = headers; + info.base_addr = ssram_base; + info.parent = &pcidev->dev; + + return intel_vsec_register(&pcidev->dev, &info); +} + +static int +pmc_ssram_telemetry_get_pmc_pci(struct pci_dev *pcidev, + struct pmc_ssram_probe_cache *probe_cache, + unsigned int pmc_idx, u32 offset) +{ + void __iomem __free(pmc_ssram_telemetry_iounmap) *tmp_ssram = NULL; + void __iomem __free(pmc_ssram_telemetry_iounmap) *ssram = NULL; + u64 ssram_base; + int ret; + + ssram_base = pci_resource_start(pcidev, 0); + tmp_ssram = ioremap(ssram_base, SSRAM_HDR_SIZE); + if (!tmp_ssram) + return -ENOMEM; + + if (pmc_idx != PMC_IDX_MAIN) { + /* + * The secondary PMC BARS (which are behind hidden PCI devices) + * are read from fixed offsets in MMIO of the primary PMC BAR. + * If a device is not present, the value will be 0. + */ + ssram_base = get_base(tmp_ssram, offset); + if (!ssram_base) + return 0; + + ssram = ioremap(ssram_base, SSRAM_HDR_SIZE); + if (!ssram) + return -ENOMEM; + + } else { + ssram = no_free_ptr(tmp_ssram); + } + + pmc_ssram_get_devid_pwrmbase(probe_cache, ssram, pmc_idx); + + /* Find and register and PMC telemetry entries */ + ret = pmc_ssram_telemetry_add_pmt(pcidev, ssram_base, ssram); + if (ret) + dev_warn(&pcidev->dev, "could not register PMT\n"); + + probe_cache->valid_mask |= BIT(pmc_idx); + + return 0; +} + +static int pmc_ssram_telemetry_pci_init(struct pci_dev *pcidev, + struct pmc_ssram_probe_cache *probe_cache) +{ + int ret; + + ret = pmc_ssram_telemetry_get_pmc_pci(pcidev, probe_cache, PMC_IDX_MAIN, 0); + if (ret) + return ret; + + /* + * If MAIN PMC enumeration is successful but either IOE or PCH fail, + * don't fail probe as the MAIN PMC is still useful as it provides the + * global reset and slp_s0 counter access. Failed or missing secondary + * PMCs are left out of valid_mask and published as absent. + */ + pmc_ssram_telemetry_get_pmc_pci(pcidev, probe_cache, PMC_IDX_IOE, + SSRAM_IOE_OFFSET); + + pmc_ssram_telemetry_get_pmc_pci(pcidev, probe_cache, PMC_IDX_PCH, + SSRAM_PCH_OFFSET); + + return 0; +} + +static int pmc_ssram_telemetry_get_pmc_acpi(struct pci_dev *pcidev, + struct pmc_ssram_probe_cache *probe_cache, + unsigned int pmc_idx) +{ + u64 ssram_base; + + ssram_base = pci_resource_start(pcidev, 0); + if (!ssram_base) + return -ENODEV; + + void __iomem __free(pmc_ssram_telemetry_iounmap) *ssram = + ioremap(ssram_base, SSRAM_HDR_SIZE); + if (!ssram) + return -ENOMEM; + + pmc_ssram_get_devid_pwrmbase(probe_cache, ssram, pmc_idx); + probe_cache->valid_mask |= BIT(pmc_idx); + + return 0; +} + +static int pmc_ssram_telemetry_acpi_init(struct pci_dev *pcidev, + struct pmc_ssram_probe_cache *probe_cache, + enum pmc_index index) +{ + struct intel_vsec_header header; + struct intel_vsec_header *headers[2] = { &header, NULL }; + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + struct intel_vsec_platform_info info = { }; + union acpi_object *dsd; + acpi_handle handle; + acpi_status status; + int ret; + + handle = ACPI_HANDLE(&pcidev->dev); + if (!handle) + return -ENODEV; + + status = acpi_evaluate_object(handle, "_DSD", NULL, &buf); + if (ACPI_FAILURE(status)) + return -ENODEV; + + void *dsd_buf __free(pmc_acpi_free) = buf.pointer; + + dsd = pmc_find_telem_guid(dsd_buf); + if (!dsd) + return -ENODEV; + + acpi_disc_t disc __free(kfree) = pmc_parse_telem_dsd(dsd, &header); + if (IS_ERR(disc)) + return PTR_ERR(disc); + + info.headers = headers; + info.caps = VSEC_CAP_TELEMETRY; + info.acpi_disc = disc; + info.src = INTEL_VSEC_DISC_ACPI; + + /* This is an ACPI companion device. PCI BAR will be used for base addr. */ + info.base_addr = 0; + + ret = intel_vsec_register(&pcidev->dev, &info); + if (ret) + dev_warn(&pcidev->dev, "could not register PMT\n"); + + return pmc_ssram_telemetry_get_pmc_acpi(pcidev, probe_cache, index); +} + +/** + * pmc_ssram_telemetry_get_pmc_info() - Get a PMC devid and base_addr information + * @pmc_idx: Index of the PMC + * @pmc_ssram_telemetry: pmc_ssram_telemetry structure to store the PMC information + * + * State flow per PMC index: + * - PMC_SSRAM_UNPROBED: Probe has not started for this index. + * - PMC_SSRAM_PROBING: Probe is currently discovering data for this index. + * - PMC_SSRAM_PRESENT: base_addr/devid were published and can be read. + * - PMC_SSRAM_ABSENT: No data is available for this index. + * + * Readers use an acquire load of the state. If state is PRESENT, reads of + * base_addr/devid are ordered after the state observation and pair with the + * writer's release-store when publishing PRESENT. + * + * Return: + * * 0 - Success + * * -EAGAIN - Probe function has not finished yet. Try again. + * * -EINVAL - Invalid pmc_idx + * * -ENODEV - PMC device is not available (hardware absent or driver failed to initialize) + */ +int pmc_ssram_telemetry_get_pmc_info(unsigned int pmc_idx, + struct pmc_ssram_telemetry *pmc_ssram_telemetry) +{ + enum pmc_ssram_state state; + + if (pmc_idx >= MAX_NUM_PMC) + return -EINVAL; + + /* + * PMCs are discovered in probe function. If this function is called before + * probe function complete, the result would be invalid. Use per-PMC state + * to inform the consumer to call again later. + */ + state = smp_load_acquire(&pmc_ssram_state[pmc_idx]); + if (state == PMC_SSRAM_UNPROBED || state == PMC_SSRAM_PROBING) + return -EAGAIN; + + if (state == PMC_SSRAM_ABSENT) + return -ENODEV; + + /* + * Acquire semantics order reads of devid and base_addr after observing + * PRESENT and pair with the writer's release-store. + */ + pmc_ssram_telemetry->devid = pmc_ssram_telems[pmc_idx].devid; + pmc_ssram_telemetry->base_addr = pmc_ssram_telems[pmc_idx].base_addr; + return 0; +} +EXPORT_SYMBOL_GPL(pmc_ssram_telemetry_get_pmc_info); + +static void pmc_ssram_publish_absent_mask(unsigned long mask) +{ + unsigned long bit; + + for_each_set_bit(bit, &mask, MAX_NUM_PMC) + pmc_ssram_publish_absent(bit); +} + +static void pmc_ssram_publish_telems(struct pmc_ssram_probe_cache *probe_cache, int ret) +{ + unsigned long bit; + + /* If probe failed, all owned indexes become absent for readers. */ + if (ret) { + pmc_ssram_publish_absent_mask(probe_cache->owned_mask); + return; + } + + /* Publish each owned index independently based on discovery result. */ + for_each_set_bit(bit, &probe_cache->owned_mask, MAX_NUM_PMC) { + if (probe_cache->valid_mask & BIT(bit)) + pmc_ssram_publish_present(probe_cache, bit); + else + pmc_ssram_publish_absent(bit); + } +} + +static int pmc_ssram_telemetry_probe(struct pci_dev *pcidev, const struct pci_device_id *id) +{ + struct pmc_ssram_probe_cache probe_cache = {}; + struct pmc_ssram_drvdata *drvdata; + const struct ssram_type *ssram_type; + enum resource_method method; + enum pmc_index index; + int ret; + + ssram_type = (const struct ssram_type *)id->driver_data; + if (!ssram_type) { + dev_dbg(&pcidev->dev, "missing driver data\n"); + return -EINVAL; + } + + index = ssram_type->p_index; + method = ssram_type->method; + if (method == RES_METHOD_PCI) + probe_cache.owned_mask = SSRAM_PCI_PMC_MASK; + else if (method == RES_METHOD_ACPI) + probe_cache.owned_mask = BIT(index); + else + return -EINVAL; + + pmc_ssram_mark_probing(probe_cache.owned_mask); + + drvdata = devm_kzalloc(&pcidev->dev, sizeof(*drvdata), GFP_KERNEL); + if (!drvdata) { + ret = -ENOMEM; + goto probe_finish; + } + + ret = pcim_enable_device(pcidev); + if (ret) { + dev_dbg(&pcidev->dev, "failed to enable PMC SSRAM device\n"); + goto probe_finish; + } + + if (method == RES_METHOD_PCI) + ret = pmc_ssram_telemetry_pci_init(pcidev, &probe_cache); + else if (method == RES_METHOD_ACPI) + ret = pmc_ssram_telemetry_acpi_init(pcidev, &probe_cache, index); + else + ret = -EINVAL; + + if (!ret) { + drvdata->owned_mask = probe_cache.owned_mask; + pci_set_drvdata(pcidev, drvdata); + } + +probe_finish: + pmc_ssram_publish_telems(&probe_cache, ret); + + return ret; +} + +static void pmc_ssram_telemetry_remove(struct pci_dev *pcidev) +{ + struct pmc_ssram_drvdata *drvdata = pci_get_drvdata(pcidev); + + if (drvdata) + pmc_ssram_publish_absent_mask(drvdata->owned_mask); +} + +static const struct pci_device_id pmc_ssram_telemetry_pci_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_MTL_SOCM), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_ARL_SOCS), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_ARL_SOCM), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_LNL_SOCM), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_PTL_PCDH), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_PTL_PCDP), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_WCL_PCDN), + .driver_data = (kernel_ulong_t)&pci_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_NVL_PCDH), + .driver_data = (kernel_ulong_t)&acpi_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_NVL_PCDS), + .driver_data = (kernel_ulong_t)&acpi_main }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PMC_DEVID_NVL_PCHS), + .driver_data = (kernel_ulong_t)&acpi_pch }, + { } +}; +MODULE_DEVICE_TABLE(pci, pmc_ssram_telemetry_pci_ids); + +static struct pci_driver pmc_ssram_telemetry_driver = { + .name = "intel_pmc_ssram_telemetry", + .id_table = pmc_ssram_telemetry_pci_ids, + .probe = pmc_ssram_telemetry_probe, + .remove = pmc_ssram_telemetry_remove, +}; +module_pci_driver(pmc_ssram_telemetry_driver); + +MODULE_IMPORT_NS("INTEL_VSEC"); +MODULE_IMPORT_NS("INTEL_PMC_CORE"); +MODULE_AUTHOR("Xi Pardee "); +MODULE_DESCRIPTION("Intel PMC SSRAM Telemetry driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/pmc/ssram_telemetry.h b/drivers/platform/x86/intel/pmc/ssram_telemetry.h new file mode 100644 index 000000000..daf8aeeb2 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/ssram_telemetry.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Intel PMC SSRAM Telemetry PCI Driver Header File + * + * Copyright (c) 2024, Intel Corporation. + */ + +#ifndef PMC_SSRAM_H +#define PMC_SSRAM_H + +/** + * struct pmc_ssram_telemetry - Structure to keep pmc info in ssram device + * @devid: device id of the pmc device + * @base_addr: contains PWRM base address + */ +struct pmc_ssram_telemetry { + u16 devid; + u64 base_addr; +}; + +int pmc_ssram_telemetry_get_pmc_info(unsigned int pmc_idx, + struct pmc_ssram_telemetry *pmc_ssram_telemetry); + +#endif /* PMC_SSRAM_H */ diff --git a/drivers/platform/x86/intel/pmc/tgl.c b/drivers/platform/x86/intel/pmc/tgl.c new file mode 100644 index 000000000..fc5b4cacc --- /dev/null +++ b/drivers/platform/x86/intel/pmc/tgl.c @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Tiger Lake PCH. + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + */ + +#include "core.h" + +#define ACPI_S0IX_DSM_UUID "57a6512e-3979-4e9d-9708-ff13b2508972" +#define ACPI_GET_LOW_MODE_REGISTERS 1 + +enum pch_type { + PCH_H, + PCH_LP +}; + +static const struct pmc_bit_map tgl_pfear_map[] = { + {"PSF9", BIT(0)}, + {"RES_66", BIT(1)}, + {"RES_67", BIT(2)}, + {"RES_68", BIT(3)}, + {"RES_69", BIT(4)}, + {"RES_70", BIT(5)}, + {"TBTLSX", BIT(6)}, + {} +}; + +static const struct pmc_bit_map *ext_tgl_pfear_map[] = { + /* + * Check intel_pmc_core_ids[] users of tgl_reg_map for + * a list of core SoCs using this. + */ + cnp_pfear_map, + tgl_pfear_map, + NULL +}; + +static const struct pmc_bit_map tgl_clocksource_status_map[] = { + {"USB2PLL_OFF_STS", BIT(18)}, + {"PCIe/USB3.1_Gen2PLL_OFF_STS", BIT(19)}, + {"PCIe_Gen3PLL_OFF_STS", BIT(20)}, + {"OPIOPLL_OFF_STS", BIT(21)}, + {"OCPLL_OFF_STS", BIT(22)}, + {"MainPLL_OFF_STS", BIT(23)}, + {"MIPIPLL_OFF_STS", BIT(24)}, + {"Fast_XTAL_Osc_OFF_STS", BIT(25)}, + {"AC_Ring_Osc_OFF_STS", BIT(26)}, + {"MC_Ring_Osc_OFF_STS", BIT(27)}, + {"SATAPLL_OFF_STS", BIT(29)}, + {"XTAL_USB2PLL_OFF_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map tgl_power_gating_status_map[] = { + {"CSME_PG_STS", BIT(0)}, + {"SATA_PG_STS", BIT(1)}, + {"xHCI_PG_STS", BIT(2)}, + {"UFSX2_PG_STS", BIT(3)}, + {"OTG_PG_STS", BIT(5)}, + {"SPA_PG_STS", BIT(6)}, + {"SPB_PG_STS", BIT(7)}, + {"SPC_PG_STS", BIT(8)}, + {"SPD_PG_STS", BIT(9)}, + {"SPE_PG_STS", BIT(10)}, + {"SPF_PG_STS", BIT(11)}, + {"LSX_PG_STS", BIT(13)}, + {"P2SB_PG_STS", BIT(14)}, + {"PSF_PG_STS", BIT(15)}, + {"SBR_PG_STS", BIT(16)}, + {"OPIDMI_PG_STS", BIT(17)}, + {"THC0_PG_STS", BIT(18)}, + {"THC1_PG_STS", BIT(19)}, + {"GBETSN_PG_STS", BIT(20)}, + {"GBE_PG_STS", BIT(21)}, + {"LPSS_PG_STS", BIT(22)}, + {"MMP_UFSX2_PG_STS", BIT(23)}, + {"MMP_UFSX2B_PG_STS", BIT(24)}, + {"FIA_PG_STS", BIT(25)}, + {} +}; + +static const struct pmc_bit_map tgl_d3_status_map[] = { + {"ADSP_D3_STS", BIT(0)}, + {"SATA_D3_STS", BIT(1)}, + {"xHCI0_D3_STS", BIT(2)}, + {"xDCI1_D3_STS", BIT(5)}, + {"SDX_D3_STS", BIT(6)}, + {"EMMC_D3_STS", BIT(7)}, + {"IS_D3_STS", BIT(8)}, + {"THC0_D3_STS", BIT(9)}, + {"THC1_D3_STS", BIT(10)}, + {"GBE_D3_STS", BIT(11)}, + {"GBE_TSN_D3_STS", BIT(12)}, + {} +}; + +static const struct pmc_bit_map tgl_vnn_req_status_map[] = { + {"GPIO_COM0_VNN_REQ_STS", BIT(1)}, + {"GPIO_COM1_VNN_REQ_STS", BIT(2)}, + {"GPIO_COM2_VNN_REQ_STS", BIT(3)}, + {"GPIO_COM3_VNN_REQ_STS", BIT(4)}, + {"GPIO_COM4_VNN_REQ_STS", BIT(5)}, + {"GPIO_COM5_VNN_REQ_STS", BIT(6)}, + {"Audio_VNN_REQ_STS", BIT(7)}, + {"ISH_VNN_REQ_STS", BIT(8)}, + {"CNVI_VNN_REQ_STS", BIT(9)}, + {"eSPI_VNN_REQ_STS", BIT(10)}, + {"Display_VNN_REQ_STS", BIT(11)}, + {"DTS_VNN_REQ_STS", BIT(12)}, + {"SMBUS_VNN_REQ_STS", BIT(14)}, + {"CSME_VNN_REQ_STS", BIT(15)}, + {"SMLINK0_VNN_REQ_STS", BIT(16)}, + {"SMLINK1_VNN_REQ_STS", BIT(17)}, + {"CLINK_VNN_REQ_STS", BIT(20)}, + {"DCI_VNN_REQ_STS", BIT(21)}, + {"ITH_VNN_REQ_STS", BIT(22)}, + {"CSME_VNN_REQ_STS", BIT(24)}, + {"GBE_VNN_REQ_STS", BIT(25)}, + {} +}; + +static const struct pmc_bit_map tgl_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS_0", BIT(0)}, + {"PCIe_LPM_En_REQ_STS_3", BIT(3)}, + {"ITH_REQ_STS_5", BIT(5)}, + {"CNVI_REQ_STS_6", BIT(6)}, + {"ISH_REQ_STS_7", BIT(7)}, + {"USB2_SUS_PG_Sys_REQ_STS_10", BIT(10)}, + {"PCIe_Clk_REQ_STS_12", BIT(12)}, + {"MPHY_Core_DL_REQ_STS_16", BIT(16)}, + {"Break-even_En_REQ_STS_17", BIT(17)}, + {"Auto-demo_En_REQ_STS_18", BIT(18)}, + {"MPHY_SUS_REQ_STS_22", BIT(22)}, + {"xDCI_attached_REQ_STS_24", BIT(24)}, + {} +}; + +const struct pmc_bit_map tgl_signal_status_map[] = { + {"LSX_Wake0_En_STS", BIT(0)}, + {"LSX_Wake0_Pol_STS", BIT(1)}, + {"LSX_Wake1_En_STS", BIT(2)}, + {"LSX_Wake1_Pol_STS", BIT(3)}, + {"LSX_Wake2_En_STS", BIT(4)}, + {"LSX_Wake2_Pol_STS", BIT(5)}, + {"LSX_Wake3_En_STS", BIT(6)}, + {"LSX_Wake3_Pol_STS", BIT(7)}, + {"LSX_Wake4_En_STS", BIT(8)}, + {"LSX_Wake4_Pol_STS", BIT(9)}, + {"LSX_Wake5_En_STS", BIT(10)}, + {"LSX_Wake5_Pol_STS", BIT(11)}, + {"LSX_Wake6_En_STS", BIT(12)}, + {"LSX_Wake6_Pol_STS", BIT(13)}, + {"LSX_Wake7_En_STS", BIT(14)}, + {"LSX_Wake7_Pol_STS", BIT(15)}, + {"Intel_Se_IO_Wake0_En_STS", BIT(16)}, + {"Intel_Se_IO_Wake0_Pol_STS", BIT(17)}, + {"Intel_Se_IO_Wake1_En_STS", BIT(18)}, + {"Intel_Se_IO_Wake1_Pol_STS", BIT(19)}, + {"Int_Timer_SS_Wake0_En_STS", BIT(20)}, + {"Int_Timer_SS_Wake0_Pol_STS", BIT(21)}, + {"Int_Timer_SS_Wake1_En_STS", BIT(22)}, + {"Int_Timer_SS_Wake1_Pol_STS", BIT(23)}, + {"Int_Timer_SS_Wake2_En_STS", BIT(24)}, + {"Int_Timer_SS_Wake2_Pol_STS", BIT(25)}, + {"Int_Timer_SS_Wake3_En_STS", BIT(26)}, + {"Int_Timer_SS_Wake3_Pol_STS", BIT(27)}, + {"Int_Timer_SS_Wake4_En_STS", BIT(28)}, + {"Int_Timer_SS_Wake4_Pol_STS", BIT(29)}, + {"Int_Timer_SS_Wake5_En_STS", BIT(30)}, + {"Int_Timer_SS_Wake5_Pol_STS", BIT(31)}, + {} +}; + +static const struct pmc_bit_map *tgl_lpm_maps[] = { + tgl_clocksource_status_map, + tgl_power_gating_status_map, + tgl_d3_status_map, + tgl_vnn_req_status_map, + tgl_vnn_misc_status_map, + tgl_signal_status_map, + NULL +}; + +static const struct pmc_reg_map tgl_reg_map = { + .pfear_sts = ext_tgl_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = cnp_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = CNP_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = ICL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = TGL_NUM_IP_IGN_ALLOWED, + .lpm_num_maps = TGL_LPM_NUM_MAPS, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .lpm_sts_latch_en_offset = TGL_LPM_STS_LATCH_EN_OFFSET, + .lpm_en_offset = TGL_LPM_EN_OFFSET, + .lpm_priority_offset = TGL_LPM_PRI_OFFSET, + .lpm_residency_offset = TGL_LPM_RESIDENCY_OFFSET, + .lpm_sts = tgl_lpm_maps, + .lpm_status_offset = TGL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = TGL_LPM_LIVE_STATUS_OFFSET, + .etr3_offset = ETR3_OFFSET, +}; + +static const struct pmc_reg_map tgl_h_reg_map = { + .pfear_sts = ext_tgl_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = cnp_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = CNP_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = ICL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .ltr_ignore_max = TGL_NUM_IP_IGN_ALLOWED, + .lpm_num_maps = TGL_LPM_NUM_MAPS, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .lpm_sts_latch_en_offset = TGL_LPM_STS_LATCH_EN_OFFSET, + .lpm_en_offset = TGL_LPM_EN_OFFSET, + .lpm_priority_offset = TGL_LPM_PRI_OFFSET, + .lpm_residency_offset = TGL_LPM_RESIDENCY_OFFSET, + .lpm_sts = tgl_lpm_maps, + .lpm_status_offset = TGL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = TGL_LPM_LIVE_STATUS_OFFSET, + .etr3_offset = ETR3_OFFSET, + .pson_residency_offset = TGL_PSON_RESIDENCY_OFFSET, + .pson_residency_counter_step = TGL_PSON_RES_COUNTER_STEP, +}; + +void pmc_core_get_tgl_lpm_reqs(struct platform_device *pdev) +{ + struct pmc_dev *pmcdev = platform_get_drvdata(pdev); + struct pmc *pmc = pmcdev->pmcs[PMC_IDX_MAIN]; + const int num_maps = pmc->map->lpm_num_maps; + u32 lpm_size = LPM_MAX_NUM_MODES * num_maps * 4; + union acpi_object *out_obj; + struct acpi_device *adev; + guid_t s0ix_dsm_guid; + u32 *lpm_req_regs, *addr; + + adev = ACPI_COMPANION(&pdev->dev); + if (!adev) + return; + + guid_parse(ACPI_S0IX_DSM_UUID, &s0ix_dsm_guid); + + out_obj = acpi_evaluate_dsm_typed(adev->handle, &s0ix_dsm_guid, 0, + ACPI_GET_LOW_MODE_REGISTERS, NULL, ACPI_TYPE_BUFFER); + if (out_obj) { + u32 size = out_obj->buffer.length; + + if (size != lpm_size) { + acpi_handle_debug(adev->handle, + "_DSM returned unexpected buffer size, have %u, expect %u\n", + size, lpm_size); + goto free_acpi_obj; + } + } else { + acpi_handle_debug(adev->handle, + "_DSM function 0 evaluation failed\n"); + goto free_acpi_obj; + } + + addr = (u32 *)out_obj->buffer.pointer; + + lpm_req_regs = devm_kcalloc(&pdev->dev, lpm_size, sizeof(u32), + GFP_KERNEL); + if (!lpm_req_regs) + goto free_acpi_obj; + + memcpy(lpm_req_regs, addr, lpm_size); + pmc->lpm_req_regs = lpm_req_regs; + +free_acpi_obj: + ACPI_FREE(out_obj); +} + +static int tgl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + int ret; + + ret = generic_core_init(pmcdev, pmc_dev_info); + if (ret) + return ret; + + pmc_core_get_tgl_lpm_reqs(pmcdev->pdev); + return 0; +} + +struct pmc_dev_info tgl_l_pmc_dev = { + .map = &tgl_reg_map, + .suspend = cnl_suspend, + .resume = cnl_resume, + .init = tgl_core_init, +}; + +struct pmc_dev_info tgl_pmc_dev = { + .map = &tgl_h_reg_map, + .suspend = cnl_suspend, + .resume = cnl_resume, + .init = tgl_core_init, +}; diff --git a/drivers/platform/x86/intel/pmc/wcl.c b/drivers/platform/x86/intel/pmc/wcl.c new file mode 100644 index 000000000..d34ac9169 --- /dev/null +++ b/drivers/platform/x86/intel/pmc/wcl.c @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains platform specific structure definitions + * and init function used by Wildcat Lake PCH. + * + * Copyright (c) 2025, Intel Corporation. + */ + +#include +#include + +#include "core.h" + +/* PMC SSRAM PMT Telemetry GUIDS */ +#define PCDN_LPM_REQ_GUID 0x33747648 + +static const struct pmc_bit_map wcl_pcdn_pfear_map[] = { + {"PMC_0", BIT(0)}, + {"FUSE_OSSE", BIT(1)}, + {"ESPISPI", BIT(2)}, + {"XHCI", BIT(3)}, + {"SPA", BIT(4)}, + {"RSVD", BIT(5)}, + {"MPFPW2", BIT(6)}, + {"GBE", BIT(7)}, + + {"SBR16B21", BIT(0)}, + {"SBR16B5", BIT(1)}, + {"SBR8B1", BIT(2)}, + {"SBR8B0", BIT(3)}, + {"P2SB0", BIT(4)}, + {"D2D_DISP_1", BIT(5)}, + {"LPSS", BIT(6)}, + {"LPC", BIT(7)}, + + {"SMB", BIT(0)}, + {"ISH", BIT(1)}, + {"DBG_SBR16B", BIT(2)}, + {"NPK_0", BIT(3)}, + {"D2D_NOC_1", BIT(4)}, + {"FIA_P", BIT(5)}, + {"FUSE", BIT(6)}, + {"DBG_PSF", BIT(7)}, + + {"DISP_PGA1", BIT(0)}, + {"XDCI", BIT(1)}, + {"EXI", BIT(2)}, + {"CSE", BIT(3)}, + {"KVMCC", BIT(4)}, + {"PMT", BIT(5)}, + {"CLINK", BIT(6)}, + {"PTIO", BIT(7)}, + + {"USBR0", BIT(0)}, + {"SBR16B22", BIT(1)}, + {"SMT1", BIT(2)}, + {"MPFPW1", BIT(3)}, + {"SMS2", BIT(4)}, + {"SMS1", BIT(5)}, + {"CSMERTC", BIT(6)}, + {"CSMEPSF", BIT(7)}, + + {"D2D_NOC_0", BIT(0)}, + {"ESE", BIT(1)}, + {"FIACPCB_P", BIT(2)}, + {"RSVD", BIT(3)}, + {"SBR8B2", BIT(4)}, + {"OSSE_SMT1", BIT(5)}, + {"D2D_DISP", BIT(6)}, + {"P2SB1", BIT(7)}, + + {"U3FPW1", BIT(0)}, + {"SBR16B3", BIT(1)}, + {"PSF4", BIT(2)}, + {"CNVI", BIT(3)}, + {"UFSX2", BIT(4)}, + {"ENDBG", BIT(5)}, + {"DBC", BIT(6)}, + {"SBRG", BIT(7)}, + + {"RSVD", BIT(0)}, + {"NPK1", BIT(1)}, + {"SBR16B7", BIT(2)}, + {"SBR16B4", BIT(3)}, + {"FIA_XG", BIT(4)}, + {"PSF6", BIT(5)}, + {"UFSPW1", BIT(6)}, + {"FIA_U", BIT(7)}, + + {"PSF8", BIT(0)}, + {"PSF0", BIT(1)}, + {"RSVD", BIT(2)}, + {"FIACPCB_U", BIT(3)}, + {"TAM", BIT(4)}, + {"SBR16B0", BIT(5)}, + {"TBTLSX", BIT(6)}, + {"THC0", BIT(7)}, + + {"THC1", BIT(0)}, + {"PMC_1", BIT(1)}, + {"FIACPCB_XG", BIT(2)}, + {"TCSS", BIT(3)}, + {"DISP_PGA", BIT(4)}, + {"SBR16B20", BIT(5)}, + {"SBR8B20", BIT(6)}, + {"DBG_SBR", BIT(7)}, + + {"SPC", BIT(0)}, + {"ACE_0", BIT(1)}, + {"ACE_1", BIT(2)}, + {"ACE_2", BIT(3)}, + {"ACE_3", BIT(4)}, + {"ACE_4", BIT(5)}, + {"ACE_5", BIT(6)}, + {"ACE_6", BIT(7)}, + + {"ACE_7", BIT(0)}, + {"ACE_8", BIT(1)}, + {"ACE_9", BIT(2)}, + {"ACE_10", BIT(3)}, + {"SBR16B2", BIT(4)}, + {"SBR8B4", BIT(5)}, + {"OSSE", BIT(6)}, + {"SBR16B1", BIT(7)}, + {} +}; + +static const struct pmc_bit_map *ext_wcl_pcdn_pfear_map[] = { + wcl_pcdn_pfear_map, + NULL +}; + +static const struct pmc_bit_map wcl_pcdn_ltr_show_map[] = { + {"SOUTHPORT_A", CNP_PMC_LTR_SPA}, + {"RSVD", WCL_PMC_LTR_RESERVED}, + {"SATA", CNP_PMC_LTR_SATA}, + {"GIGABIT_ETHERNET", CNP_PMC_LTR_GBE}, + {"XHCI", CNP_PMC_LTR_XHCI}, + {"SOUTHPORT_F", ADL_PMC_LTR_SPF}, + {"ME", CNP_PMC_LTR_ME}, + {"SATA1", CNP_PMC_LTR_EVA}, + {"SOUTHPORT_C", CNP_PMC_LTR_SPC}, + {"HD_AUDIO", CNP_PMC_LTR_AZ}, + {"CNV", CNP_PMC_LTR_CNV}, + {"LPSS", CNP_PMC_LTR_LPSS}, + {"SOUTHPORT_D", CNP_PMC_LTR_SPD}, + {"SOUTHPORT_E", CNP_PMC_LTR_SPE}, + {"SATA2", PTL_PMC_LTR_SATA2}, + {"ESPI", CNP_PMC_LTR_ESPI}, + {"SCC", CNP_PMC_LTR_SCC}, + {"ISH", CNP_PMC_LTR_ISH}, + {"UFSX2", CNP_PMC_LTR_UFSX2}, + {"EMMC", CNP_PMC_LTR_EMMC}, + {"WIGIG", ICL_PMC_LTR_WIGIG}, + {"THC0", TGL_PMC_LTR_THC0}, + {"THC1", TGL_PMC_LTR_THC1}, + {"SOUTHPORT_G", MTL_PMC_LTR_SPG}, + {"ESE", MTL_PMC_LTR_ESE}, + {"IOE_PMC", MTL_PMC_LTR_IOE_PMC}, + {"DMI3", ARL_PMC_LTR_DMI3}, + {"OSSE", LNL_PMC_LTR_OSSE}, + + /* Below two cannot be used for LTR_IGNORE */ + {"CURRENT_PLATFORM", PTL_PMC_LTR_CUR_PLT}, + {"AGGREGATED_SYSTEM", PTL_PMC_LTR_CUR_ASLT}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_power_gating_status_0_map[] = { + {"PMC_PGD0_PG_STS", BIT(0), 0}, + {"FUSE_OSSE_PGD0_PG_STS", BIT(1), 0}, + {"ESPISPI_PGD0_PG_STS", BIT(2), 0}, + {"XHCI_PGD0_PG_STS", BIT(3), 1}, + {"SPA_PGD0_PG_STS", BIT(4), 1}, + {"RSVD_5", BIT(5), 0}, + {"MPFPW2_PGD0_PG_STS", BIT(6), 0}, + {"GBE_PGD0_PG_STS", BIT(7), 1}, + {"SBR16B21_PGD0_PG_STS", BIT(8), 0}, + {"SBR16B5_PGD0_PG_STS", BIT(9), 0}, + {"SBR8B1_PGD0_PG_STS", BIT(10), 0}, + {"SBR8B0_PGD0_PG_STS", BIT(11), 0}, + {"P2SB0_PG_STS", BIT(12), 1}, + {"D2D_DISP_PGD1_PG_STS", BIT(13), 0}, + {"LPSS_PGD0_PG_STS", BIT(14), 1}, + {"LPC_PGD0_PG_STS", BIT(15), 0}, + {"SMB_PGD0_PG_STS", BIT(16), 0}, + {"ISH_PGD0_PG_STS", BIT(17), 0}, + {"DBG_SBR16B_PGD0_PG_STS", BIT(18), 0}, + {"NPK_PGD0_PG_STS", BIT(19), 0}, + {"D2D_NOC_PGD1_PG_STS", BIT(20), 0}, + {"FIA_P_PGD0_PG_STS", BIT(21), 0}, + {"FUSE_PGD0_PG_STS", BIT(22), 0}, + {"DBG_PSF_PGD0_PG_STS", BIT(23), 0}, + {"DISP_PGA1_PGD0_PG_STS", BIT(24), 0}, + {"XDCI_PGD0_PG_STS", BIT(25), 1}, + {"EXI_PGD0_PG_STS", BIT(26), 0}, + {"CSE_PGD0_PG_STS", BIT(27), 1}, + {"KVMCC_PGD0_PG_STS", BIT(28), 1}, + {"PMT_PGD0_PG_STS", BIT(29), 1}, + {"CLINK_PGD0_PG_STS", BIT(30), 1}, + {"PTIO_PGD0_PG_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_power_gating_status_1_map[] = { + {"USBR0_PGD0_PG_STS", BIT(0), 1}, + {"SBR16B22_PGD0_PG_STS", BIT(1), 0}, + {"SMT1_PGD0_PG_STS", BIT(2), 1}, + {"MPFPW1_PGD0_PG_STS", BIT(3), 0}, + {"SMS2_PGD0_PG_STS", BIT(4), 1}, + {"SMS1_PGD0_PG_STS", BIT(5), 1}, + {"CSMERTC_PGD0_PG_STS", BIT(6), 0}, + {"CSMEPSF_PGD0_PG_STS", BIT(7), 0}, + {"D2D_NOC_PGD0_PG_STS", BIT(8), 0}, + {"ESE_PGD0_PG_STS", BIT(9), 1}, + {"FIACPCB_P_PGD0_PG_STS", BIT(10), 0}, + {"SBR8B2_PGD0_PG_STS", BIT(12), 0}, + {"OSSE_SMT1_PGD0_PG_STS", BIT(13), 1}, + {"D2D_DISP_PGD0_PG_STS", BIT(14), 0}, + {"P2SB1_PGD0_PG_STS", BIT(15), 1}, + {"U3FPW1_PGD0_PG_STS", BIT(16), 0}, + {"SBR16B3_PGD0_PG_STS", BIT(17), 0}, + {"PSF4_PGD0_PG_STS", BIT(18), 0}, + {"CNVI_PGD0_PG_STS", BIT(19), 0}, + {"UFSX2_PGD0_PG_STS", BIT(20), 1}, + {"ENDBG_PGD0_PG_STS", BIT(21), 0}, + {"DBC_PGD0_PG_STS", BIT(22), 0}, + {"SBRG_PGD0_PG_STS", BIT(23), 0}, + {"NPK_PGD1_PG_STS", BIT(25), 0}, + {"SBR16B7_PGD0_PG_STS", BIT(26), 0}, + {"SBR16B4_PGD0_PG_STS", BIT(27), 0}, + {"FIA_XG_PSF_PGD0_PG_STS", BIT(28), 0}, + {"PSF6_PGD0_PG_STS", BIT(29), 0}, + {"UFSPW1_PGD0_PG_STS", BIT(30), 0}, + {"FIA_U_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_power_gating_status_2_map[] = { + {"PSF8_PGD0_PG_STS", BIT(0), 0}, + {"PSF0_PGD0_PG_STS", BIT(1), 0}, + {"FIACPCB_U_PGD0_PG_STS", BIT(3), 0}, + {"TAM_PGD0_PG_STS", BIT(4), 1}, + {"SBR16B0_PGD0_PG_STS", BIT(5), 0}, + {"TBTLSX_PGD0_PG_STS", BIT(6), 1}, + {"THC0_PGD0_PG_STS", BIT(7), 1}, + {"THC1_PGD0_PG_STS", BIT(8), 1}, + {"PMC_PGD1_PG_STS", BIT(9), 0}, + {"FIACPCB_XG_PGD0_PG_STS", BIT(10), 0}, + {"TCSS_PGD0_PG_STS", BIT(11), 0}, + {"DISP_PGA_PGD0_PG_STS", BIT(12), 0}, + {"SBR8B4_PGD0_PG_STS", BIT(13), 0}, + {"SBR8B20_PGD0_PG_STS", BIT(14), 0}, + {"DBG_PGD0_PG_STS", BIT(15), 0}, + {"SPC_PGD0_PG_STS", BIT(16), 1}, + {"ACE_PGD0_PG_STS", BIT(17), 0}, + {"ACE_PGD1_PG_STS", BIT(18), 0}, + {"ACE_PGD2_PG_STS", BIT(19), 0}, + {"ACE_PGD3_PG_STS", BIT(20), 0}, + {"ACE_PGD4_PG_STS", BIT(21), 0}, + {"ACE_PGD5_PG_STS", BIT(22), 0}, + {"ACE_PGD6_PG_STS", BIT(23), 0}, + {"ACE_PGD7_PG_STS", BIT(24), 0}, + {"ACE_PGD8_PG_STS", BIT(25), 0}, + {"ACE_PGD9_PG_STS", BIT(26), 0}, + {"ACE_PGD10_PG_STS", BIT(27), 0}, + {"SBR16B2_PG_PGD0_PG_STS", BIT(28), 0}, + {"SBR16B20_PGD0_PG_STS", BIT(29), 0}, + {"OSSE_PGD0_PG_STS", BIT(30), 1}, + {"SBR16B1_PGD0_PG_STS", BIT(31), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_d3_status_0_map[] = { + {"LPSS_D3_STS", BIT(3), 1}, + {"XDCI_D3_STS", BIT(4), 1}, + {"XHCI_D3_STS", BIT(5), 1}, + {"SPA_D3_STS", BIT(12), 0}, + {"SPC_D3_STS", BIT(14), 0}, + {"OSSE_D3_STS", BIT(15), 0}, + {"ESPISPI_D3_STS", BIT(18), 0}, + {"PSTH_D3_STS", BIT(21), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_d3_status_1_map[] = { + {"OSSE_SMT1_D3_STS", BIT(16), 0}, + {"GBE_D3_STS", BIT(19), 0}, + {"ITSS_D3_STS", BIT(23), 0}, + {"CNVI_D3_STS", BIT(27), 0}, + {"UFSX2_D3_STS", BIT(28), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_d3_status_2_map[] = { + {"CSMERTC_D3_STS", BIT(1), 0}, + {"ESE_D3_STS", BIT(2), 0}, + {"CSE_D3_STS", BIT(4), 0}, + {"KVMCC_D3_STS", BIT(5), 0}, + {"USBR0_D3_STS", BIT(6), 0}, + {"ISH_D3_STS", BIT(7), 0}, + {"SMT1_D3_STS", BIT(8), 0}, + {"SMT2_D3_STS", BIT(9), 0}, + {"SMT3_D3_STS", BIT(10), 0}, + {"CLINK_D3_STS", BIT(14), 0}, + {"PTIO_D3_STS", BIT(16), 0}, + {"PMT_D3_STS", BIT(17), 0}, + {"SMS1_D3_STS", BIT(18), 0}, + {"SMS2_D3_STS", BIT(19), 0}, + {"OSSE_SMT2_D3_STS", BIT(22), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_d3_status_3_map[] = { + {"THC0_D3_STS", BIT(14), 1}, + {"THC1_D3_STS", BIT(15), 1}, + {"OSSE_SMT3_D3_STS", BIT(16), 0}, + {"ACE_D3_STS", BIT(23), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_vnn_req_status_0_map[] = { + {"LPSS_VNN_REQ_STS", BIT(3), 1}, + {"OSSE_VNN_REQ_STS", BIT(15), 1}, + {"ESPISPI_VNN_REQ_STS", BIT(18), 1}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_vnn_req_status_1_map[] = { + {"NPK_VNN_REQ_STS", BIT(4), 1}, + {"DFXAGG_VNN_REQ_STS", BIT(8), 0}, + {"EXI_VNN_REQ_STS", BIT(9), 1}, + {"OSSE_SMT1_VNN_REQ_STS", BIT(16), 1}, + {"P2D_VNN_REQ_STS", BIT(18), 1}, + {"GBE_VNN_REQ_STS", BIT(19), 1}, + {"SMB_VNN_REQ_STS", BIT(25), 1}, + {"LPC_VNN_REQ_STS", BIT(26), 0}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_vnn_req_status_2_map[] = { + {"CSMERTC_VNN_REQ_STS", BIT(1), 1}, + {"ESE_VNN_REQ_STS", BIT(2), 1}, + {"CSE_VNN_REQ_STS", BIT(4), 1}, + {"ISH_VNN_REQ_STS", BIT(7), 1}, + {"SMT1_VNN_REQ_STS", BIT(8), 1}, + {"CLINK_VNN_REQ_STS", BIT(14), 1}, + {"SMS1_VNN_REQ_STS", BIT(18), 1}, + {"SMS2_VNN_REQ_STS", BIT(19), 1}, + {"GPIOCOM4_VNN_REQ_STS", BIT(20), 1}, + {"GPIOCOM3_VNN_REQ_STS", BIT(21), 1}, + {"GPIOCOM1_VNN_REQ_STS", BIT(23), 1}, + {"GPIOCOM0_VNN_REQ_STS", BIT(24), 1}, + {"DISP_SHIM_VNN_REQ_STS", BIT(31), 1}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_vnn_misc_status_map[] = { + {"CPU_C10_REQ_STS", BIT(0), 0}, + {"TS_OFF_REQ_STS", BIT(1), 0}, + {"PNDE_MET_REQ_STS", BIT(2), 1}, + {"FW_THROTTLE_ALLOWED_REQ_STS", BIT(4), 0}, + {"VNN_SOC_REQ_STS", BIT(6), 1}, + {"ISH_VNNAON_REQ_STS", BIT(7), 0}, + {"D2D_NOC_CFI_QACTIVE_REQ_STS", BIT(8), 1}, + {"D2D_NOC_GPSB_QACTIVE_REQ_STS", BIT(9), 1}, + {"PLT_GREATER_REQ_STS", BIT(11), 1}, + {"ALL_SBR_IDLE_REQ_STS", BIT(12), 0}, + {"PMC_IDLE_FB_OCP_REQ_STS", BIT(13), 0}, + {"PM_SYNC_STATES_REQ_STS", BIT(14), 0}, + {"EA_REQ_STS", BIT(15), 0}, + {"MPHY_CORE_OFF_REQ_STS", BIT(16), 0}, + {"BRK_EV_EN_REQ_STS", BIT(17), 0}, + {"AUTO_DEMO_EN_REQ_STS", BIT(18), 0}, + {"ITSS_CLK_SRC_REQ_STS", BIT(19), 1}, + {"ARC_IDLE_REQ_STS", BIT(21), 0}, + {"FIA_DEEP_PM_REQ_STS", BIT(23), 0}, + {"XDCI_ATTACHED_REQ_STS", BIT(24), 1}, + {"ARC_INTERRUPT_WAKE_REQ_STS", BIT(25), 0}, + {"D2D_DISP_DDI_QACTIVE_REQ_STS", BIT(26), 1}, + {"PRE_WAKE0_REQ_STS", BIT(27), 1}, + {"PRE_WAKE1_REQ_STS", BIT(28), 1}, + {"PRE_WAKE2_REQ_STS", BIT(29), 1}, + {} +}; + +static const struct pmc_bit_map wcl_pcdn_rsc_status_map[] = { + {"Memory", 0, 1}, + {"PSF0", 0, 1}, + {"PSF6", 0, 1}, + {"PSF8", 0, 1}, + {"SAF_CFI_LINK", 0, 1}, + {"SB", 0, 1}, + {} +}; + +static const struct pmc_bit_map *wcl_pcdn_lpm_maps[] = { + ptl_pcdp_clocksource_status_map, + wcl_pcdn_power_gating_status_0_map, + wcl_pcdn_power_gating_status_1_map, + wcl_pcdn_power_gating_status_2_map, + wcl_pcdn_d3_status_0_map, + wcl_pcdn_d3_status_1_map, + wcl_pcdn_d3_status_2_map, + wcl_pcdn_d3_status_3_map, + wcl_pcdn_vnn_req_status_0_map, + wcl_pcdn_vnn_req_status_1_map, + wcl_pcdn_vnn_req_status_2_map, + ptl_pcdp_vnn_req_status_3_map, + wcl_pcdn_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_bit_map *wcl_pcdn_blk_maps[] = { + wcl_pcdn_power_gating_status_0_map, + wcl_pcdn_power_gating_status_1_map, + wcl_pcdn_power_gating_status_2_map, + wcl_pcdn_rsc_status_map, + wcl_pcdn_vnn_req_status_0_map, + wcl_pcdn_vnn_req_status_1_map, + wcl_pcdn_vnn_req_status_2_map, + ptl_pcdp_vnn_req_status_3_map, + wcl_pcdn_d3_status_0_map, + wcl_pcdn_d3_status_1_map, + wcl_pcdn_d3_status_2_map, + wcl_pcdn_d3_status_3_map, + ptl_pcdp_clocksource_status_map, + wcl_pcdn_vnn_misc_status_map, + ptl_pcdp_signal_status_map, + NULL +}; + +static const struct pmc_reg_map wcl_pcdn_reg_map = { + .pfear_sts = ext_wcl_pcdn_pfear_map, + .slp_s0_offset = CNP_PMC_SLP_S0_RES_COUNTER_OFFSET, + .slp_s0_res_counter_step = TGL_PMC_SLP_S0_RES_COUNTER_STEP, + .ltr_show_sts = wcl_pcdn_ltr_show_map, + .msr_sts = msr_map, + .ltr_ignore_offset = CNP_PMC_LTR_IGNORE_OFFSET, + .regmap_length = WCL_PCD_PMC_MMIO_REG_LEN, + .ppfear0_offset = CNP_PMC_HOST_PPFEAR0A, + .ppfear_buckets = LNL_PPFEAR_NUM_ENTRIES, + .pm_cfg_offset = CNP_PMC_PM_CFG_OFFSET, + .pm_read_disable_bit = CNP_PMC_READ_DISABLE_BIT, + .lpm_num_maps = PTL_LPM_NUM_MAPS, + .ltr_ignore_max = LNL_NUM_IP_IGN_ALLOWED, + .lpm_res_counter_step_x2 = TGL_PMC_LPM_RES_COUNTER_STEP_X2, + .etr3_offset = ETR3_OFFSET, + .lpm_sts_latch_en_offset = MTL_LPM_STATUS_LATCH_EN_OFFSET, + .lpm_priority_offset = MTL_LPM_PRI_OFFSET, + .lpm_en_offset = MTL_LPM_EN_OFFSET, + .lpm_residency_offset = MTL_LPM_RESIDENCY_OFFSET, + .lpm_sts = wcl_pcdn_lpm_maps, + .lpm_status_offset = MTL_LPM_STATUS_OFFSET, + .lpm_live_status_offset = MTL_LPM_LIVE_STATUS_OFFSET, + .s0ix_blocker_maps = wcl_pcdn_blk_maps, + .s0ix_blocker_offset = LNL_S0IX_BLOCKER_OFFSET, + .num_s0ix_blocker = WCL_NUM_S0IX_BLOCKER, + .blocker_req_offset = WCL_BLK_REQ_OFFSET, + .lpm_req_guid = PCDN_LPM_REQ_GUID, +}; + +static struct pmc_info wcl_pmc_info_list[] = { + { + .devid = PMC_DEVID_WCL_PCDN, + .map = &wcl_pcdn_reg_map, + }, + {} +}; + +static const u8 wcl_pmc_list[] = {PMC_IDX_MAIN}; + +#define WCL_NPU_PCI_DEV 0xfd3e + +/* + * Set power state of select devices that do not have drivers to D3 + * so that they do not block Package C entry. + */ +static void wcl_d3_fixup(void) +{ + pmc_core_set_device_d3(WCL_NPU_PCI_DEV); +} + +static int wcl_resume(struct pmc_dev *pmcdev) +{ + wcl_d3_fixup(); + return cnl_resume(pmcdev); +} + +static int wcl_core_init(struct pmc_dev *pmcdev, struct pmc_dev_info *pmc_dev_info) +{ + wcl_d3_fixup(); + return generic_core_init(pmcdev, pmc_dev_info); +} + +struct pmc_dev_info wcl_pmc_dev = { + .regmap_list = wcl_pmc_info_list, + .num_pmcs = ARRAY_SIZE(wcl_pmc_list), + .pmc_list = wcl_pmc_list, + .map = &wcl_pcdn_reg_map, + .sub_req_show = &pmc_core_substate_blk_req_fops, + .suspend = cnl_suspend, + .resume = wcl_resume, + .init = wcl_core_init, + .sub_req = pmc_core_pmt_get_blk_sub_req, + .ssram_hidden = true, +}; diff --git a/drivers/platform/x86/intel/pmt/Kconfig b/drivers/platform/x86/intel/pmt/Kconfig new file mode 100644 index 000000000..7363446b7 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/Kconfig @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel Platform Monitoring Technology drivers +# + +config INTEL_PMT_CLASS + tristate + help + The Intel Platform Monitoring Technology (PMT) class driver provides + the basic sysfs interface and file hierarchy used by PMT devices. + + For more information, see: + + + To compile this driver as a module, choose M here: the module + will be called intel_pmt_class. + +config INTEL_PMT_TELEMETRY + tristate "Intel Platform Monitoring Technology (PMT) Telemetry driver" + depends on INTEL_VSEC + select INTEL_PMT_DISCOVERY + select INTEL_PMT_CLASS + help + The Intel Platform Monitory Technology (PMT) Telemetry driver provides + access to hardware telemetry metrics on devices that support the + feature. + + To compile this driver as a module, choose M here: the module + will be called intel_pmt_telemetry. + +config INTEL_PMT_CRASHLOG + tristate "Intel Platform Monitoring Technology (PMT) Crashlog driver" + depends on INTEL_VSEC + select INTEL_PMT_CLASS + help + The Intel Platform Monitoring Technology (PMT) crashlog driver provides + access to hardware crashlog capabilities on devices that support the + feature. + + To compile this driver as a module, choose M here: the module + will be called intel_pmt_crashlog. + +config INTEL_PMT_DISCOVERY + tristate "Intel Platform Monitoring Technology (PMT) Discovery driver" + depends on INTEL_VSEC + select INTEL_PMT_CLASS + help + The Intel Platform Monitoring Technology (PMT) discovery driver provides + access to details about the various PMT features and feature specific + attributes. + + To compile this driver as a module, choose M here: the module + will be called pmt_discovery. + +config INTEL_PMT_KUNIT_TEST + tristate "KUnit tests for Intel PMT driver" + depends on INTEL_PMT_DISCOVERY + depends on INTEL_PMT_TELEMETRY || !INTEL_PMT_TELEMETRY + depends on KUNIT + help + Enable this option to compile and run a suite of KUnit tests for the Intel + Platform Monitoring Technology (PMT) driver. These tests are designed to + validate the driver's functionality, error handling, and overall stability, + helping developers catch regressions and ensure code quality during changes. + + This option is intended for development and testing environments. It is + recommended to disable it in production builds. To compile this driver as a + module, choose M here: the module will be called pmt-discovery-kunit. diff --git a/drivers/platform/x86/intel/pmt/Makefile b/drivers/platform/x86/intel/pmt/Makefile new file mode 100644 index 000000000..47f692c09 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/Makefile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/intel/pmt +# Intel Platform Monitoring Technology Drivers +# + +obj-$(CONFIG_INTEL_PMT_CLASS) += pmt_class.o +pmt_class-y := class.o +obj-$(CONFIG_INTEL_PMT_TELEMETRY) += pmt_telemetry.o +pmt_telemetry-y := telemetry.o +obj-$(CONFIG_INTEL_PMT_CRASHLOG) += pmt_crashlog.o +pmt_crashlog-y := crashlog.o +obj-$(CONFIG_INTEL_PMT_DISCOVERY) += pmt_discovery.o +pmt_discovery-y := discovery.o features.o +obj-$(CONFIG_INTEL_PMT_KUNIT_TEST) += pmt-discovery-kunit.o +pmt-discovery-kunit-y := discovery-kunit.o diff --git a/drivers/platform/x86/intel/pmt/class.c b/drivers/platform/x86/intel/pmt/class.c new file mode 100644 index 000000000..d0ab8e33c --- /dev/null +++ b/drivers/platform/x86/intel/pmt/class.c @@ -0,0 +1,565 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Telemetry driver + * + * Copyright (c) 2020, Intel Corporation. + * All Rights Reserved. + * + * Author: "Alexander Duyck" + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "class.h" + +#define PMT_XA_START 1 +#define PMT_XA_MAX INT_MAX +#define PMT_XA_LIMIT XA_LIMIT(PMT_XA_START, PMT_XA_MAX) +#define GUID_SPR_PUNIT 0x9956f43f + +bool intel_pmt_is_early_client_hw(struct device *dev) +{ + struct intel_vsec_device *ivdev = dev_to_ivdev(dev); + + /* + * Early implementations of PMT on client platforms have some + * differences from the server platforms (which use the Out Of Band + * Management Services Module OOBMSM). + */ + return !!(ivdev->quirks & VSEC_QUIRK_EARLY_HW); +} +EXPORT_SYMBOL_NS_GPL(intel_pmt_is_early_client_hw, "INTEL_PMT"); + +static inline int +pmt_memcpy64_fromio(void *to, const u64 __iomem *from, size_t count) +{ + int i, remain; + u64 *buf = to; + + if (!IS_ALIGNED((unsigned long)from, 8)) + return -EFAULT; + + for (i = 0; i < count/8; i++) + buf[i] = readq(&from[i]); + + /* Copy any remaining bytes */ + remain = count % 8; + if (remain) { + u64 tmp = readq(&from[i]); + + memcpy(&buf[i], &tmp, remain); + } + + return count; +} + +int pmt_telem_read_mmio(struct device *dev, struct pmt_callbacks *cb, u32 guid, void *buf, + void __iomem *addr, loff_t off, u32 count) +{ + if (cb && cb->read_telem) + return cb->read_telem(dev, guid, buf, off, count); + + addr += off; + + if (guid == GUID_SPR_PUNIT) + /* PUNIT on SPR only supports aligned 64-bit read */ + return pmt_memcpy64_fromio(buf, addr, count); + + memcpy_fromio(buf, addr, count); + + return count; +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_read_mmio, "INTEL_PMT"); + +/* + * sysfs + */ +static ssize_t +intel_pmt_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, loff_t off, + size_t count) +{ + struct intel_pmt_entry *entry = container_of(attr, + struct intel_pmt_entry, + pmt_bin_attr); + + if (off < 0) + return -EINVAL; + + if (off >= entry->size) + return 0; + + if (count > entry->size - off) + count = entry->size - off; + + count = pmt_telem_read_mmio(entry->ep->dev, entry->cb, entry->header.guid, buf, + entry->base, off, count); + + return count; +} + +static int +intel_pmt_mmap(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, struct vm_area_struct *vma) +{ + struct intel_pmt_entry *entry = container_of(attr, + struct intel_pmt_entry, + pmt_bin_attr); + unsigned long vsize = vma->vm_end - vma->vm_start; + struct device *dev = kobj_to_dev(kobj); + unsigned long phys = entry->base_addr; + unsigned long pfn = PFN_DOWN(phys); + unsigned long psize; + + if (vma->vm_flags & (VM_WRITE | VM_MAYWRITE)) + return -EROFS; + + psize = (PFN_UP(entry->base_addr + entry->size) - pfn) * PAGE_SIZE; + if (vsize > psize) { + dev_err(dev, "Requested mmap size is too large\n"); + return -EINVAL; + } + + vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); + if (io_remap_pfn_range(vma, vma->vm_start, pfn, + vsize, vma->vm_page_prot)) + return -EAGAIN; + + return 0; +} + +static ssize_t +guid_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct intel_pmt_entry *entry = dev_get_drvdata(dev); + + return sysfs_emit(buf, "0x%x\n", entry->guid); +} +static DEVICE_ATTR_RO(guid); + +static ssize_t size_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct intel_pmt_entry *entry = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%zu\n", entry->size); +} +static DEVICE_ATTR_RO(size); + +static ssize_t +offset_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct intel_pmt_entry *entry = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%lu\n", offset_in_page(entry->base_addr)); +} +static DEVICE_ATTR_RO(offset); + +static struct attribute *intel_pmt_attrs[] = { + &dev_attr_guid.attr, + &dev_attr_size.attr, + &dev_attr_offset.attr, + NULL +}; + +static umode_t intel_pmt_attr_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct auxiliary_device *auxdev = to_auxiliary_dev(dev->parent); + struct intel_vsec_device *ivdev = auxdev_to_ivdev(auxdev); + + /* + * Place the discovery features folder in /sys/class/intel_pmt, but + * exclude the common attributes as they are not applicable. + */ + if (ivdev->cap_id == ilog2(VSEC_CAP_DISCOVERY)) + return 0; + + return attr->mode; +} + +static bool intel_pmt_group_visible(struct kobject *kobj) +{ + return true; +} +DEFINE_SYSFS_GROUP_VISIBLE(intel_pmt); + +static const struct attribute_group intel_pmt_group = { + .attrs = intel_pmt_attrs, + .is_visible = SYSFS_GROUP_VISIBLE(intel_pmt), +}; +__ATTRIBUTE_GROUPS(intel_pmt); + +struct class intel_pmt_class = { + .name = "intel_pmt", + .dev_groups = intel_pmt_groups, +}; +EXPORT_SYMBOL_GPL(intel_pmt_class); + +static int pmt_resolve_access_pci(struct intel_pmt_entry *entry, + struct intel_vsec_device *ivdev, + int idx) +{ + struct pci_dev *pci_dev = to_pci_dev(ivdev->dev); + struct device *dev = &ivdev->auxdev.dev; + struct intel_pmt_header *header = &entry->header; + struct resource *disc_res; + u8 bir; + + /* + * The base offset should always be 8 byte aligned. + * + * For non-local access types the lower 3 bits of base offset + * contains the index of the base address register where the + * telemetry can be found. + */ + bir = GET_BIR(header->base_offset); + + /* Local access and BARID only for now */ + switch (header->access_type) { + case ACCESS_LOCAL: + if (bir) { + dev_err(dev, + "Unsupported BAR index %d for access type %d\n", + bir, header->access_type); + return -EINVAL; + } + /* + * For access_type LOCAL, the base address is as follows: + * base address = end of discovery region + base offset + */ + disc_res = &ivdev->resource[idx]; + entry->base_addr = disc_res->end + 1 + header->base_offset; + + /* + * Some hardware use a different calculation for the base address + * when access_type == ACCESS_LOCAL. On the these systems + * ACCESS_LOCAL refers to an address in the same BAR as the + * header but at a fixed offset. But as the header address was + * supplied to the driver, we don't know which BAR it was in. + * So search for the bar whose range includes the header address. + */ + if (intel_pmt_is_early_client_hw(dev)) { + int i; + + entry->base_addr = 0; + for (i = 0; i < 6; i++) + if (disc_res->start >= pci_resource_start(pci_dev, i) && + (disc_res->start <= pci_resource_end(pci_dev, i))) { + entry->base_addr = pci_resource_start(pci_dev, i) + + header->base_offset; + break; + } + if (!entry->base_addr) + return -EINVAL; + } + + break; + case ACCESS_BARID: + /* Use the provided base address if it exists */ + if (ivdev->base_addr) { + entry->base_addr = ivdev->base_addr + + GET_ADDRESS(header->base_offset); + break; + } + + /* + * If another BAR was specified then the base offset + * represents the offset within that BAR. SO retrieve the + * address from the parent PCI device and add offset. + */ + entry->base_addr = pci_resource_start(pci_dev, bir) + + GET_ADDRESS(header->base_offset); + break; + default: + dev_err(dev, "Unsupported access type %d\n", + header->access_type); + return -EINVAL; + } + + entry->pcidev = pci_dev; + + return 0; +} + +static int pmt_resolve_access_acpi(struct intel_pmt_entry *entry, + struct intel_vsec_device *ivdev) +{ + struct pci_dev *pci_dev = NULL; + struct device *dev = &ivdev->auxdev.dev; + struct intel_pmt_header *header = &entry->header; + u8 bir; + + if (dev_is_pci(ivdev->dev)) + pci_dev = to_pci_dev(ivdev->dev); + + /* + * The base offset should always be 8 byte aligned. + * + * For non-local access types the lower 3 bits of base offset + * contains the index of the base address register where the + * telemetry can be found. + */ + bir = GET_BIR(header->base_offset); + + switch (header->access_type) { + case ACCESS_BARID: + /* ACPI platform drivers use base_addr */ + if (ivdev->base_addr) { + entry->base_addr = ivdev->base_addr + + GET_ADDRESS(header->base_offset); + break; + } + + /* If base_addr is not provided, then this is an ACPI companion device */ + if (!pci_dev) { + dev_err(dev, "ACCESS_BARID requires PCI BAR resources or base_addr\n"); + return -EINVAL; + } + + entry->base_addr = pci_resource_start(pci_dev, bir) + + GET_ADDRESS(header->base_offset); + break; + default: + dev_err(dev, "Unsupported access type %d for ACPI based PMT\n", + header->access_type); + return -EINVAL; + } + + return 0; +} + +static int intel_pmt_populate_entry(struct intel_pmt_entry *entry, + struct intel_vsec_device *ivdev, + int idx) +{ + struct intel_pmt_header *header = &entry->header; + struct device *dev = &ivdev->auxdev.dev; + int ret; + + switch (ivdev->src) { + case INTEL_VSEC_DISC_PCI: + ret = pmt_resolve_access_pci(entry, ivdev, idx); + if (ret) + return ret; + break; + case INTEL_VSEC_DISC_ACPI: + ret = pmt_resolve_access_acpi(entry, ivdev); + if (ret) + return ret; + break; + default: + dev_err(dev, "Unknown discovery source: %d\n", ivdev->src); + return -EINVAL; + } + + entry->guid = header->guid; + entry->size = header->size; + entry->cb = ivdev->priv_data; + + return 0; +} + +static int intel_pmt_dev_register(struct intel_pmt_entry *entry, + struct intel_pmt_namespace *ns, + struct device *parent) +{ + struct intel_vsec_device *ivdev = dev_to_ivdev(parent); + struct resource res = {0}; + struct device *dev; + int ret; + + ret = xa_alloc(ns->xa, &entry->devid, entry, PMT_XA_LIMIT, GFP_KERNEL); + if (ret) + return ret; + + dev = device_create(&intel_pmt_class, parent, MKDEV(0, 0), entry, + "%s%d", ns->name, entry->devid); + + if (IS_ERR(dev)) { + dev_err(parent, "Could not create %s%d device node\n", + ns->name, entry->devid); + ret = PTR_ERR(dev); + goto fail_dev_create; + } + + entry->kobj = &dev->kobj; + + if (entry->attr_grp) { + ret = sysfs_create_group(entry->kobj, entry->attr_grp); + if (ret) + goto fail_sysfs_create_group; + } + + /* if size is 0 assume no data buffer, so no file needed */ + if (!entry->size) + return 0; + + res.start = entry->base_addr; + res.end = res.start + entry->size - 1; + res.flags = IORESOURCE_MEM; + + entry->base = devm_ioremap_resource(dev, &res); + if (IS_ERR(entry->base)) { + ret = PTR_ERR(entry->base); + goto fail_ioremap; + } + + sysfs_bin_attr_init(&entry->pmt_bin_attr); + entry->pmt_bin_attr.attr.name = ns->name; + entry->pmt_bin_attr.attr.mode = 0440; + entry->pmt_bin_attr.mmap = intel_pmt_mmap; + entry->pmt_bin_attr.read = intel_pmt_read; + entry->pmt_bin_attr.size = entry->size; + + ret = sysfs_create_bin_file(&dev->kobj, &entry->pmt_bin_attr); + if (ret) + goto fail_ioremap; + + if (ns->pmt_add_endpoint) { + ret = ns->pmt_add_endpoint(ivdev, entry); + if (ret) + goto fail_add_endpoint; + } + + return 0; + +fail_add_endpoint: + sysfs_remove_bin_file(entry->kobj, &entry->pmt_bin_attr); +fail_ioremap: + if (entry->attr_grp) + sysfs_remove_group(entry->kobj, entry->attr_grp); +fail_sysfs_create_group: + device_unregister(dev); +fail_dev_create: + xa_erase(ns->xa, entry->devid); + + return ret; +} + +static int pmt_get_headers(struct intel_vsec_device *ivdev, int idx, + struct intel_pmt_entry *entry) +{ + struct device *dev = &ivdev->auxdev.dev; + size_t header_bytes = sizeof(entry->disc_header); + + switch (ivdev->src) { + case INTEL_VSEC_DISC_PCI: { + struct resource *disc_res = &ivdev->resource[idx]; + void __iomem *disc_table; + + disc_table = devm_ioremap_resource(dev, disc_res); + if (IS_ERR(disc_table)) + return PTR_ERR(disc_table); + + /* + * The mapped resource is sized by the namespace's DVSEC + * entry_size (in dwords), which can be less than the default + * size (e.g. telemetry uses entry_size = 3, 12 bytes). Cap the + * copy to resource_size() to avoid reading past the mapped + * region. + */ + memset(entry->disc_header, 0, header_bytes); + memcpy_fromio(entry->disc_header, disc_table, + min(header_bytes, resource_size(disc_res))); + + /* Used by crashlog driver */ + entry->disc_table = disc_table; + + return 0; + } + case INTEL_VSEC_DISC_ACPI: { + memcpy(entry->disc_header, &ivdev->acpi_disc[idx][0], header_bytes); + /* + * No MMIO mapping exists on the ACPI source path; the cached + * headers are the only view of the discovery record. Consumers + * that dereference disc_table (e.g. crashlog) must therefore + * only be wired to namespaces backed by INTEL_VSEC_DISC_PCI. + */ + entry->disc_table = NULL; + + return 0; + } + default: + dev_err(dev, "Unknown discovery source type: %d\n", ivdev->src); + break; + } + + return -EINVAL; +} + +int intel_pmt_dev_create(struct intel_pmt_entry *entry, struct intel_pmt_namespace *ns, + struct intel_vsec_device *intel_vsec_dev, int idx) +{ + struct device *dev = &intel_vsec_dev->auxdev.dev; + int ret; + + ret = pmt_get_headers(intel_vsec_dev, idx, entry); + if (ret) + return ret; + + if (ns->pmt_pre_decode) { + ret = ns->pmt_pre_decode(intel_vsec_dev, entry); + if (ret) + return ret; + } + + ret = ns->pmt_header_decode(entry, dev); + if (ret) + return ret; + + if (ns->pmt_post_decode) { + ret = ns->pmt_post_decode(intel_vsec_dev, entry); + if (ret) + return ret; + } + + ret = intel_pmt_populate_entry(entry, intel_vsec_dev, idx); + if (ret) + return ret; + + return intel_pmt_dev_register(entry, ns, dev); +} +EXPORT_SYMBOL_NS_GPL(intel_pmt_dev_create, "INTEL_PMT"); + +void intel_pmt_dev_destroy(struct intel_pmt_entry *entry, + struct intel_pmt_namespace *ns) +{ + struct device *dev = kobj_to_dev(entry->kobj); + + if (entry->size) + sysfs_remove_bin_file(entry->kobj, &entry->pmt_bin_attr); + + if (entry->attr_grp) + sysfs_remove_group(entry->kobj, entry->attr_grp); + + device_unregister(dev); + xa_erase(ns->xa, entry->devid); +} +EXPORT_SYMBOL_NS_GPL(intel_pmt_dev_destroy, "INTEL_PMT"); + +static int __init pmt_class_init(void) +{ + return class_register(&intel_pmt_class); +} + +static void __exit pmt_class_exit(void) +{ + class_unregister(&intel_pmt_class); +} + +module_init(pmt_class_init); +module_exit(pmt_class_exit); + +MODULE_AUTHOR("Alexander Duyck "); +MODULE_DESCRIPTION("Intel PMT Class driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/pmt/class.h b/drivers/platform/x86/intel/pmt/class.h new file mode 100644 index 000000000..a0ece4fc3 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/class.h @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _INTEL_PMT_CLASS_H +#define _INTEL_PMT_CLASS_H + +#include +#include +#include +#include +#include +#include + +#include "telemetry.h" + +/* PMT access types */ +#define ACCESS_BARID 2 +#define ACCESS_LOCAL 3 + +/* PMT discovery base address/offset register layout */ +#define GET_BIR(v) ((v) & GENMASK(2, 0)) +#define GET_ADDRESS(v) ((v) & GENMASK(31, 3)) + +struct device; +struct pci_dev; +extern struct class intel_pmt_class; + +struct telem_endpoint { + struct device *dev; + struct telem_header header; + struct pmt_callbacks *cb; + void __iomem *base; + bool present; + struct kref kref; +}; + +struct intel_pmt_header { + u32 base_offset; + u32 size; + u32 guid; + u8 access_type; + u8 telem_type; +}; + +struct intel_pmt_entry { + struct telem_endpoint *ep; + struct pci_dev *pcidev; + struct intel_pmt_header header; + u32 disc_header[PMT_DISC_DWORDS]; + struct bin_attribute pmt_bin_attr; + const struct attribute_group *attr_grp; + struct kobject *kobj; + void __iomem *disc_table; + void __iomem *base; + struct pmt_callbacks *cb; + unsigned long base_addr; + size_t size; + u64 feature_flags; + u32 guid; + u32 num_rmids; /* Number of Resource Monitoring IDs */ + int devid; +}; + +struct intel_pmt_namespace { + const char *name; + struct xarray *xa; + int (*pmt_header_decode)(struct intel_pmt_entry *entry, + struct device *dev); + int (*pmt_pre_decode)(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry); + int (*pmt_post_decode)(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry); + int (*pmt_add_endpoint)(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry); +}; + +int pmt_telem_read_mmio(struct device *dev, struct pmt_callbacks *cb, u32 guid, void *buf, + void __iomem *addr, loff_t off, u32 count); +bool intel_pmt_is_early_client_hw(struct device *dev); +int intel_pmt_dev_create(struct intel_pmt_entry *entry, + struct intel_pmt_namespace *ns, + struct intel_vsec_device *dev, int idx); +void intel_pmt_dev_destroy(struct intel_pmt_entry *entry, + struct intel_pmt_namespace *ns); +#if IS_ENABLED(CONFIG_INTEL_PMT_DISCOVERY) +void intel_pmt_get_features(struct intel_pmt_entry *entry); +#else +static inline void intel_pmt_get_features(struct intel_pmt_entry *entry) {} +#endif + +#endif diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c new file mode 100644 index 000000000..f936daf99 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -0,0 +1,621 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitoring Technology Crashlog driver + * + * Copyright (c) 2020, Intel Corporation. + * All Rights Reserved. + * + * Author: "Alexander Duyck" + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "class.h" + +/* Crashlog discovery header types */ +#define CRASH_TYPE_OOBMSM 1 + +/* Crashlog Discovery Header */ +#define CONTROL_OFFSET 0x0 +#define GUID_OFFSET 0x4 +#define BASE_OFFSET 0x8 +#define SIZE_OFFSET 0xC +#define GET_ACCESS(v) ((v) & GENMASK(3, 0)) +#define GET_TYPE(v) (((v) & GENMASK(7, 4)) >> 4) +#define GET_VERSION(v) (((v) & GENMASK(19, 16)) >> 16) +/* size is in bytes */ +#define GET_SIZE(v) ((v) * sizeof(u32)) + +/* + * Type 1 Version 0 + * status and control registers are combined. + * + * Bits 29 and 30 control the state of bit 31. + * Bit 29 will clear bit 31, if set, allowing a new crashlog to be captured. + * Bit 30 will immediately trigger a crashlog to be generated, setting bit 31. + * Bit 31 is the read-only status with a 1 indicating log is complete. + */ +#define TYPE1_VER0_STATUS_OFFSET 0x00 +#define TYPE1_VER0_CONTROL_OFFSET 0x00 + +#define TYPE1_VER0_DISABLE BIT(28) +#define TYPE1_VER0_CLEAR BIT(29) +#define TYPE1_VER0_EXECUTE BIT(30) +#define TYPE1_VER0_COMPLETE BIT(31) +#define TYPE1_VER0_TRIGGER_MASK GENMASK(31, 28) + +/* + * Type 1 Version 2 + * status and control are different registers + */ +#define TYPE1_VER2_STATUS_OFFSET 0x00 +#define TYPE1_VER2_CONTROL_OFFSET 0x14 + +/* status register */ +#define TYPE1_VER2_CLEAR_SUPPORT BIT(20) +#define TYPE1_VER2_REARMED BIT(25) +#define TYPE1_VER2_ERROR BIT(26) +#define TYPE1_VER2_CONSUMED BIT(27) +#define TYPE1_VER2_DISABLED BIT(28) +#define TYPE1_VER2_CLEARED BIT(29) +#define TYPE1_VER2_IN_PROGRESS BIT(30) +#define TYPE1_VER2_COMPLETE BIT(31) + +/* control register */ +#define TYPE1_VER2_CONSUME BIT(25) +#define TYPE1_VER2_REARM BIT(28) +#define TYPE1_VER2_EXECUTE BIT(29) +#define TYPE1_VER2_CLEAR BIT(30) +#define TYPE1_VER2_DISABLE BIT(31) +#define TYPE1_VER2_TRIGGER_MASK \ + (TYPE1_VER2_EXECUTE | TYPE1_VER2_CLEAR | TYPE1_VER2_DISABLE) + +/* After offset, order alphabetically, not bit ordered */ +struct crashlog_status { + u32 offset; + u32 clear_supported; + u32 cleared; + u32 complete; + u32 consumed; + u32 disabled; + u32 error; + u32 in_progress; + u32 rearmed; +}; + +struct crashlog_control { + u32 offset; + u32 trigger_mask; + u32 clear; + u32 consume; + u32 disable; + u32 manual; + u32 rearm; +}; + +struct crashlog_info { + const struct crashlog_status status; + const struct crashlog_control control; + const struct attribute_group *attr_grp; +}; + +struct crashlog_entry { + /* entry must be first member of struct */ + struct intel_pmt_entry entry; + struct mutex control_mutex; + const struct crashlog_info *info; +}; + +struct pmt_crashlog_priv { + int num_entries; + struct crashlog_entry entry[]; +}; + +/* + * I/O + */ + +/* Read, modify, write the control register, setting or clearing @bit based on @set */ +static void pmt_crashlog_rmw(struct crashlog_entry *crashlog, u32 bit, bool set) +{ + const struct crashlog_control *control = &crashlog->info->control; + struct intel_pmt_entry *entry = &crashlog->entry; + u32 reg = readl(entry->disc_table + control->offset); + + reg &= ~control->trigger_mask; + + if (set) + reg |= bit; + else + reg &= ~bit; + + writel(reg, entry->disc_table + control->offset); +} + +/* Read the status register and see if the specified @bit is set */ +static bool pmt_crashlog_rc(struct crashlog_entry *crashlog, u32 bit) +{ + const struct crashlog_status *status = &crashlog->info->status; + u32 reg = readl(crashlog->entry.disc_table + status->offset); + + return !!(reg & bit); +} + +static bool pmt_crashlog_complete(struct crashlog_entry *crashlog) +{ + /* return current value of the crashlog complete flag */ + return pmt_crashlog_rc(crashlog, crashlog->info->status.complete); +} + +static bool pmt_crashlog_disabled(struct crashlog_entry *crashlog) +{ + /* return current value of the crashlog disabled flag */ + return pmt_crashlog_rc(crashlog, crashlog->info->status.disabled); +} + +static bool pmt_crashlog_supported(struct intel_pmt_entry *entry, u32 *crash_type, u32 *version) +{ + u32 discovery_header = readl(entry->disc_table + CONTROL_OFFSET); + + *crash_type = GET_TYPE(discovery_header); + *version = GET_VERSION(discovery_header); + + /* + * Currently we only recognize OOBMSM (type 1) and version 0 or 2 + * devices. + * + * Ignore all other crashlog devices in the system. + */ + if (*crash_type == CRASH_TYPE_OOBMSM && (*version == 0 || *version == 2)) + return true; + + return false; +} + +static void pmt_crashlog_set_disable(struct crashlog_entry *crashlog, + bool disable) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.disable, disable); +} + +static void pmt_crashlog_set_clear(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.clear, true); +} + +static void pmt_crashlog_set_execute(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.manual, true); +} + +static bool pmt_crashlog_cleared(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.cleared); +} + +static bool pmt_crashlog_consumed(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.consumed); +} + +static void pmt_crashlog_set_consumed(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.consume, true); +} + +static bool pmt_crashlog_error(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.error); +} + +static bool pmt_crashlog_rearm(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.rearmed); +} + +static void pmt_crashlog_set_rearm(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.rearm, true); +} + +/* + * sysfs + */ +static ssize_t +clear_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool cleared = pmt_crashlog_cleared(crashlog); + + return sysfs_emit(buf, "%d\n", cleared); +} + +static ssize_t +clear_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct crashlog_entry *crashlog; + bool clear; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &clear); + if (result) + return result; + + /* set bit only */ + if (!clear) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + pmt_crashlog_set_clear(crashlog); + + return count; +} +static DEVICE_ATTR_RW(clear); + +static ssize_t +consumed_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool consumed = pmt_crashlog_consumed(crashlog); + + return sysfs_emit(buf, "%d\n", consumed); +} + +static ssize_t +consumed_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct crashlog_entry *crashlog; + bool consumed; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &consumed); + if (result) + return result; + + /* set bit only */ + if (!consumed) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + if (pmt_crashlog_disabled(crashlog)) + return -EBUSY; + + if (!pmt_crashlog_complete(crashlog)) + return -EEXIST; + + pmt_crashlog_set_consumed(crashlog); + + return count; +} +static DEVICE_ATTR_RW(consumed); + +static ssize_t +enable_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool enabled = !pmt_crashlog_disabled(crashlog); + + return sprintf(buf, "%d\n", enabled); +} + +static ssize_t +enable_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct crashlog_entry *crashlog; + bool enabled; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &enabled); + if (result) + return result; + + guard(mutex)(&crashlog->control_mutex); + + pmt_crashlog_set_disable(crashlog, !enabled); + + return count; +} +static DEVICE_ATTR_RW(enable); + +static ssize_t +error_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool error = pmt_crashlog_error(crashlog); + + return sysfs_emit(buf, "%d\n", error); +} +static DEVICE_ATTR_RO(error); + +static ssize_t +rearm_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + int rearmed = pmt_crashlog_rearm(crashlog); + + return sysfs_emit(buf, "%d\n", rearmed); +} + +static ssize_t +rearm_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct crashlog_entry *crashlog; + bool rearm; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &rearm); + if (result) + return result; + + /* set only */ + if (!rearm) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + pmt_crashlog_set_rearm(crashlog); + + return count; +} +static DEVICE_ATTR_RW(rearm); + +static ssize_t +trigger_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog; + bool trigger; + + crashlog = dev_get_drvdata(dev); + trigger = pmt_crashlog_complete(crashlog); + + return sprintf(buf, "%d\n", trigger); +} + +static ssize_t +trigger_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct crashlog_entry *crashlog; + bool trigger; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &trigger); + if (result) + return result; + + guard(mutex)(&crashlog->control_mutex); + + /* if device is currently disabled, return busy */ + if (pmt_crashlog_disabled(crashlog)) + return -EBUSY; + + if (!trigger) { + pmt_crashlog_set_clear(crashlog); + return count; + } + + /* we cannot trigger a new crash if one is still pending */ + if (pmt_crashlog_complete(crashlog)) + return -EEXIST; + + pmt_crashlog_set_execute(crashlog); + + return count; +} +static DEVICE_ATTR_RW(trigger); + +static struct attribute *pmt_crashlog_type1_ver0_attrs[] = { + &dev_attr_enable.attr, + &dev_attr_trigger.attr, + NULL +}; + +static struct attribute *pmt_crashlog_type1_ver2_attrs[] = { + &dev_attr_clear.attr, + &dev_attr_consumed.attr, + &dev_attr_enable.attr, + &dev_attr_error.attr, + &dev_attr_rearm.attr, + &dev_attr_trigger.attr, + NULL +}; + +static const struct attribute_group pmt_crashlog_type1_ver0_group = { + .attrs = pmt_crashlog_type1_ver0_attrs, +}; + +static const struct attribute_group pmt_crashlog_type1_ver2_group = { + .attrs = pmt_crashlog_type1_ver2_attrs, +}; + +static const struct crashlog_info crashlog_type1_ver0 = { + .status.offset = TYPE1_VER0_STATUS_OFFSET, + .status.cleared = TYPE1_VER0_CLEAR, + .status.complete = TYPE1_VER0_COMPLETE, + .status.disabled = TYPE1_VER0_DISABLE, + + .control.offset = TYPE1_VER0_CONTROL_OFFSET, + .control.trigger_mask = TYPE1_VER0_TRIGGER_MASK, + .control.clear = TYPE1_VER0_CLEAR, + .control.disable = TYPE1_VER0_DISABLE, + .control.manual = TYPE1_VER0_EXECUTE, + .attr_grp = &pmt_crashlog_type1_ver0_group, +}; + +static const struct crashlog_info crashlog_type1_ver2 = { + .status.offset = TYPE1_VER2_STATUS_OFFSET, + .status.clear_supported = TYPE1_VER2_CLEAR_SUPPORT, + .status.cleared = TYPE1_VER2_CLEARED, + .status.complete = TYPE1_VER2_COMPLETE, + .status.consumed = TYPE1_VER2_CONSUMED, + .status.disabled = TYPE1_VER2_DISABLED, + .status.error = TYPE1_VER2_ERROR, + .status.in_progress = TYPE1_VER2_IN_PROGRESS, + .status.rearmed = TYPE1_VER2_REARMED, + + .control.offset = TYPE1_VER2_CONTROL_OFFSET, + .control.trigger_mask = TYPE1_VER2_TRIGGER_MASK, + .control.clear = TYPE1_VER2_CLEAR, + .control.consume = TYPE1_VER2_CONSUME, + .control.disable = TYPE1_VER2_DISABLE, + .control.manual = TYPE1_VER2_EXECUTE, + .control.rearm = TYPE1_VER2_REARM, + .attr_grp = &pmt_crashlog_type1_ver2_group, +}; + +static const struct crashlog_info *select_crashlog_info(u32 type, u32 version) +{ + if (version == 0) + return &crashlog_type1_ver0; + + return &crashlog_type1_ver2; +} + +static int pmt_crashlog_pre_decode(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry) +{ + struct crashlog_entry *crashlog; + u32 version; + u32 type; + + if (!pmt_crashlog_supported(entry, &type, &version)) + return 1; + + /* initialize the crashlog struct */ + crashlog = container_of(entry, struct crashlog_entry, entry); + mutex_init(&crashlog->control_mutex); + + crashlog->info = select_crashlog_info(type, version); + entry->attr_grp = crashlog->info->attr_grp; + + return 0; +} + +static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, + struct device *dev) +{ + void __iomem *disc_table = entry->disc_table; + struct intel_pmt_header *header = &entry->header; + + header->access_type = GET_ACCESS(readl(disc_table)); + header->guid = readl(disc_table + GUID_OFFSET); + header->base_offset = readl(disc_table + BASE_OFFSET); + + /* Size is measured in DWORDS, but accessor returns bytes */ + header->size = GET_SIZE(readl(disc_table + SIZE_OFFSET)); + + return 0; +} + +static DEFINE_XARRAY_ALLOC(crashlog_array); +static struct intel_pmt_namespace pmt_crashlog_ns = { + .name = "crashlog", + .xa = &crashlog_array, + .pmt_pre_decode = pmt_crashlog_pre_decode, + .pmt_header_decode = pmt_crashlog_header_decode, +}; + +/* + * initialization + */ +static void pmt_crashlog_remove(struct auxiliary_device *auxdev) +{ + struct pmt_crashlog_priv *priv = auxiliary_get_drvdata(auxdev); + int i; + + for (i = 0; i < priv->num_entries; i++) { + struct crashlog_entry *crashlog = &priv->entry[i]; + + intel_pmt_dev_destroy(&crashlog->entry, &pmt_crashlog_ns); + mutex_destroy(&crashlog->control_mutex); + } +} + +static int pmt_crashlog_probe(struct auxiliary_device *auxdev, + const struct auxiliary_device_id *id) +{ + struct intel_vsec_device *intel_vsec_dev = auxdev_to_ivdev(auxdev); + struct pmt_crashlog_priv *priv; + size_t size; + int i, ret; + + size = struct_size(priv, entry, intel_vsec_dev->num_resources); + priv = devm_kzalloc(&auxdev->dev, size, GFP_KERNEL); + if (!priv) + return -ENOMEM; + + auxiliary_set_drvdata(auxdev, priv); + + for (i = 0; i < intel_vsec_dev->num_resources; i++) { + struct intel_pmt_entry *entry = &priv->entry[priv->num_entries].entry; + + ret = intel_pmt_dev_create(entry, &pmt_crashlog_ns, intel_vsec_dev, i); + if (ret < 0) + goto abort_probe; + if (ret) + continue; + + priv->num_entries++; + } + + return 0; +abort_probe: + pmt_crashlog_remove(auxdev); + return ret; +} + +static const struct auxiliary_device_id pmt_crashlog_id_table[] = { + { .name = "intel_vsec.crashlog" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, pmt_crashlog_id_table); + +static struct auxiliary_driver pmt_crashlog_aux_driver = { + .id_table = pmt_crashlog_id_table, + .remove = pmt_crashlog_remove, + .probe = pmt_crashlog_probe, +}; + +static int __init pmt_crashlog_init(void) +{ + return auxiliary_driver_register(&pmt_crashlog_aux_driver); +} + +static void __exit pmt_crashlog_exit(void) +{ + auxiliary_driver_unregister(&pmt_crashlog_aux_driver); + xa_destroy(&crashlog_array); +} + +module_init(pmt_crashlog_init); +module_exit(pmt_crashlog_exit); + +MODULE_AUTHOR("Alexander Duyck "); +MODULE_DESCRIPTION("Intel PMT Crashlog driver"); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("INTEL_PMT"); diff --git a/drivers/platform/x86/intel/pmt/discovery-kunit.c b/drivers/platform/x86/intel/pmt/discovery-kunit.c new file mode 100644 index 000000000..f44eb41d5 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/discovery-kunit.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Discovery KUNIT tests + * + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + */ + +#include +#include +#include +#include +#include +#include + +#define PMT_FEATURE_COUNT (FEATURE_MAX + 1) + +static void +validate_pmt_regions(struct kunit *test, struct pmt_feature_group *feature_group, int feature_id) +{ + int i; + + kunit_info(test, "Feature ID %d [%s] has %d regions.\n", feature_id, + pmt_feature_names[feature_id], feature_group->count); + + for (i = 0; i < feature_group->count; i++) { + struct telemetry_region *region = &feature_group->regions[i]; + + kunit_info(test, " - Region %d: cdie_mask=%u, package_id=%u, partition=%u, segment=%u,", + i, region->plat_info.cdie_mask, region->plat_info.package_id, + region->plat_info.partition, region->plat_info.segment); + kunit_info(test, "\t\tbus=%u, device=%u, function=%u, guid=0x%x,", + region->plat_info.bus_number, region->plat_info.device_number, + region->plat_info.function_number, region->guid); + kunit_info(test, "\t\taddr=%p, size=%zu, num_rmids=%u", region->addr, region->size, + region->num_rmids); + + + KUNIT_ASSERT_GE(test, region->plat_info.cdie_mask, 0); + KUNIT_ASSERT_GE(test, region->plat_info.package_id, 0); + KUNIT_ASSERT_GE(test, region->plat_info.partition, 0); + KUNIT_ASSERT_GE(test, region->plat_info.segment, 0); + KUNIT_ASSERT_GE(test, region->plat_info.bus_number, 0); + KUNIT_ASSERT_GE(test, region->plat_info.device_number, 0); + KUNIT_ASSERT_GE(test, region->plat_info.function_number, 0); + + KUNIT_ASSERT_NE(test, region->guid, 0); + + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, (__force const void *)region->addr); + } +} + +static void linebreak(struct kunit *test) +{ + kunit_info(test, "*****************************************************************************\n"); +} + +static void test_intel_pmt_get_regions_by_feature(struct kunit *test) +{ + struct pmt_feature_group *feature_group; + int num_available = 0; + int feature_id; + + /* Iterate through all possible feature IDs */ + for (feature_id = 1; feature_id < PMT_FEATURE_COUNT; feature_id++, linebreak(test)) { + const char *name; + + if (!pmt_feature_id_is_valid(feature_id)) + continue; + + name = pmt_feature_names[feature_id]; + + feature_group = intel_pmt_get_regions_by_feature(feature_id); + if (IS_ERR(feature_group)) { + if (PTR_ERR(feature_group) == -ENOENT) + kunit_warn(test, "intel_pmt_get_regions_by_feature() reporting feature %d [%s] is not present.\n", + feature_id, name); + else + kunit_warn(test, "intel_pmt_get_regions_by_feature() returned error %ld while attempt to lookup %d [%s].\n", + PTR_ERR(feature_group), feature_id, name); + + continue; + } + + if (!feature_group) { + kunit_warn(test, "Feature ID %d: %s is not available.\n", feature_id, name); + continue; + } + + num_available++; + + validate_pmt_regions(test, feature_group, feature_id); + + intel_pmt_put_feature_group(feature_group); + } + + if (num_available == 0) + kunit_warn(test, "No PMT region groups were available for any feature ID (0-10).\n"); +} + +static struct kunit_case intel_pmt_discovery_test_cases[] = { + KUNIT_CASE(test_intel_pmt_get_regions_by_feature), + {} +}; + +static struct kunit_suite intel_pmt_discovery_test_suite = { + .name = "pmt_discovery_test", + .test_cases = intel_pmt_discovery_test_cases, +}; + +kunit_test_suite(intel_pmt_discovery_test_suite); + +MODULE_IMPORT_NS("INTEL_PMT_DISCOVERY"); +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMT Discovery KUNIT test driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/pmt/discovery.c b/drivers/platform/x86/intel/pmt/discovery.c new file mode 100644 index 000000000..c482368bf --- /dev/null +++ b/drivers/platform/x86/intel/pmt/discovery.c @@ -0,0 +1,637 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Discovery driver + * + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + */ + +#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 "class.h" + +#define MAX_FEATURE_VERSION 0 +#define DT_TBIR GENMASK(2, 0) +#define FEAT_ATTR_SIZE(x) ((x) * sizeof(u32)) +#define PMT_GUID_SIZE(x) ((x) * sizeof(u32)) +#define PMT_ACCESS_TYPE_RSVD 0xF +#define SKIP_FEATURE 1 + +struct feature_discovery_table { + u32 access_type:4; + u32 version:8; + u32 size:16; + u32 reserved:4; + u32 id; + u32 offset; + u32 reserved2; +}; + +/* Common feature table header */ +struct feature_header { + u32 attr_size:8; + u32 num_guids:8; + u32 reserved:16; +}; + +/* Feature attribute fields */ +struct caps { + u32 caps; +}; + +struct command { + u32 max_stream_size:16; + u32 max_command_size:16; +}; + +struct watcher { + u32 reserved:21; + u32 period:11; + struct command command; +}; + +struct rmid { + u32 num_rmids:16; /* Number of Resource Monitoring IDs */ + u32 reserved:16; + struct watcher watcher; +}; + +struct feature_table { + struct feature_header header; + struct caps caps; + union { + struct command command; + struct watcher watcher; + struct rmid rmid; + }; + u32 *guids; +}; + +/* For backreference in struct feature */ +struct pmt_features_priv; + +struct feature { + struct feature_table table; + struct kobject kobj; + struct pmt_features_priv *priv; + struct list_head list; + const struct attribute_group *attr_group; + enum pmt_feature_id id; +}; + +struct pmt_features_priv { + struct device *parent; + struct device *dev; + int count; + u32 mask; + struct feature feature[]; +}; + +static LIST_HEAD(pmt_feature_list); +static DEFINE_MUTEX(feature_list_lock); + +#define to_pmt_feature(x) container_of(x, struct feature, kobj) +static void pmt_feature_release(struct kobject *kobj) +{ +} + +static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct pmt_cap **pmt_caps; + u32 caps = feature->table.caps.caps; + ssize_t ret = 0; + + switch (feature->id) { + case FEATURE_PER_CORE_PERF_TELEM: + pmt_caps = pmt_caps_pcpt; + break; + case FEATURE_PER_CORE_ENV_TELEM: + pmt_caps = pmt_caps_pcet; + break; + case FEATURE_PER_RMID_PERF_TELEM: + pmt_caps = pmt_caps_rmid_perf; + break; + case FEATURE_ACCEL_TELEM: + pmt_caps = pmt_caps_accel; + break; + case FEATURE_UNCORE_TELEM: + pmt_caps = pmt_caps_uncore; + break; + case FEATURE_CRASH_LOG: + pmt_caps = pmt_caps_crashlog; + break; + case FEATURE_PETE_LOG: + pmt_caps = pmt_caps_pete; + break; + case FEATURE_TPMI_CTRL: + pmt_caps = pmt_caps_tpmi; + break; + case FEATURE_TRACING: + pmt_caps = pmt_caps_tracing; + break; + case FEATURE_PER_RMID_ENERGY_TELEM: + pmt_caps = pmt_caps_rmid_energy; + break; + default: + return -EINVAL; + } + + while (*pmt_caps) { + struct pmt_cap *pmt_cap = *pmt_caps; + + while (pmt_cap->name) { + ret += sysfs_emit_at(buf, ret, "%-40s Available: %s\n", pmt_cap->name, + str_yes_no(pmt_cap->mask & caps)); + pmt_cap++; + } + pmt_caps++; + } + + return ret; +} +static struct kobj_attribute caps_attribute = __ATTR_RO(caps); + +static struct watcher *get_watcher(struct feature *feature) +{ + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + return &feature->table.rmid.watcher; + case LAYOUT_WATCHER: + return &feature->table.watcher; + default: + return ERR_PTR(-EINVAL); + } +} + +static struct command *get_command(struct feature *feature) +{ + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + return &feature->table.rmid.watcher.command; + case LAYOUT_WATCHER: + return &feature->table.watcher.command; + case LAYOUT_COMMAND: + return &feature->table.command; + default: + return ERR_PTR(-EINVAL); + } +} + +static ssize_t num_rmids_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + + return sysfs_emit(buf, "%u\n", feature->table.rmid.num_rmids); +} +static struct kobj_attribute num_rmids_attribute = __ATTR_RO(num_rmids); + +static ssize_t min_watcher_period_ms_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct watcher *watcher = get_watcher(feature); + + if (IS_ERR(watcher)) + return PTR_ERR(watcher); + + return sysfs_emit(buf, "%u\n", watcher->period); +} +static struct kobj_attribute min_watcher_period_ms_attribute = + __ATTR_RO(min_watcher_period_ms); + +static ssize_t max_stream_size_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct command *command = get_command(feature); + + if (IS_ERR(command)) + return PTR_ERR(command); + + return sysfs_emit(buf, "%u\n", command->max_stream_size); +} +static struct kobj_attribute max_stream_size_attribute = + __ATTR_RO(max_stream_size); + +static ssize_t max_command_size_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct command *command = get_command(feature); + + if (IS_ERR(command)) + return PTR_ERR(command); + + return sysfs_emit(buf, "%u\n", command->max_command_size); +} +static struct kobj_attribute max_command_size_attribute = + __ATTR_RO(max_command_size); + +static ssize_t guids_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + int i, count = 0; + + for (i = 0; i < feature->table.header.num_guids; i++) + count += sysfs_emit_at(buf, count, "0x%x\n", + feature->table.guids[i]); + + return count; +} +static struct kobj_attribute guids_attribute = __ATTR_RO(guids); + +static struct attribute *pmt_feature_rmid_attrs[] = { + &caps_attribute.attr, + &num_rmids_attribute.attr, + &min_watcher_period_ms_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_rmid); + +static const struct kobj_type pmt_feature_rmid_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_rmid_groups, +}; + +static struct attribute *pmt_feature_watcher_attrs[] = { + &caps_attribute.attr, + &min_watcher_period_ms_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_watcher); + +static const struct kobj_type pmt_feature_watcher_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_watcher_groups, +}; + +static struct attribute *pmt_feature_command_attrs[] = { + &caps_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_command); + +static const struct kobj_type pmt_feature_command_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_command_groups, +}; + +static struct attribute *pmt_feature_guids_attrs[] = { + &caps_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_guids); + +static const struct kobj_type pmt_feature_guids_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_guids_groups, +}; + +static int +pmt_feature_get_disc_table(struct pmt_features_priv *priv, + struct resource *disc_res, + struct feature_discovery_table *disc_tbl) +{ + void __iomem *disc_base; + + disc_base = devm_ioremap_resource(priv->dev, disc_res); + if (IS_ERR(disc_base)) + return PTR_ERR(disc_base); + + memcpy_fromio(disc_tbl, disc_base, sizeof(*disc_tbl)); + + devm_iounmap(priv->dev, disc_base); + + if (priv->mask & BIT(disc_tbl->id)) + return dev_err_probe(priv->dev, -EINVAL, "Duplicate feature: %s\n", + pmt_feature_names[disc_tbl->id]); + + /* + * Some devices may expose non-functioning entries that are + * reserved for future use. They have zero size. Do not fail + * probe for these. Just ignore them. + */ + if (disc_tbl->size == 0 || disc_tbl->access_type == PMT_ACCESS_TYPE_RSVD) + return SKIP_FEATURE; + + if (disc_tbl->version > MAX_FEATURE_VERSION) + return SKIP_FEATURE; + + if (!pmt_feature_id_is_valid(disc_tbl->id)) + return SKIP_FEATURE; + + priv->mask |= BIT(disc_tbl->id); + + return 0; +} + +static int +pmt_feature_get_feature_table(struct pmt_features_priv *priv, + struct feature *feature, + struct feature_discovery_table *disc_tbl, + struct resource *disc_res) +{ + struct feature_table *feat_tbl = &feature->table; + struct feature_header *header; + struct resource res = {}; + resource_size_t res_size; + void __iomem *feat_base, *feat_offset; + void *tbl_offset; + size_t size; + u32 *guids; + u8 tbir; + + tbir = FIELD_GET(DT_TBIR, disc_tbl->offset); + + switch (disc_tbl->access_type) { + case ACCESS_LOCAL: + if (tbir) + return dev_err_probe(priv->dev, -EINVAL, + "Unsupported BAR index %u for access type %u\n", + tbir, disc_tbl->access_type); + + + /* + * For access_type LOCAL, the base address is as follows: + * base address = end of discovery region + base offset + 1 + */ + res = DEFINE_RES_MEM(disc_res->end + disc_tbl->offset + 1, + disc_tbl->size * sizeof(u32)); + break; + + default: + return dev_err_probe(priv->dev, -EINVAL, "Unrecognized access_type %u\n", + disc_tbl->access_type); + } + + feature->id = disc_tbl->id; + + /* Get the feature table */ + feat_base = devm_ioremap_resource(priv->dev, &res); + if (IS_ERR(feat_base)) + return PTR_ERR(feat_base); + + feat_offset = feat_base; + tbl_offset = feat_tbl; + + /* Get the header */ + header = &feat_tbl->header; + memcpy_fromio(header, feat_offset, sizeof(*header)); + + /* Validate fields fit within mapped resource */ + size = sizeof(*header) + FEAT_ATTR_SIZE(header->attr_size) + + PMT_GUID_SIZE(header->num_guids); + res_size = resource_size(&res); + if (WARN(size > res_size, "Bad table size %zu > %pa", size, &res_size)) + return -EINVAL; + + /* Get the feature attributes, including capability fields */ + tbl_offset += sizeof(*header); + feat_offset += sizeof(*header); + + memcpy_fromio(tbl_offset, feat_offset, FEAT_ATTR_SIZE(header->attr_size)); + + /* Finally, get the guids */ + guids = devm_kmalloc(priv->dev, PMT_GUID_SIZE(header->num_guids), GFP_KERNEL); + if (!guids) + return -ENOMEM; + + feat_offset += FEAT_ATTR_SIZE(header->attr_size); + + memcpy_fromio(guids, feat_offset, PMT_GUID_SIZE(header->num_guids)); + + feat_tbl->guids = guids; + + devm_iounmap(priv->dev, feat_base); + + return 0; +} + +static void pmt_features_add_feat(struct feature *feature) +{ + guard(mutex)(&feature_list_lock); + list_add(&feature->list, &pmt_feature_list); +} + +static void pmt_features_remove_feat(struct feature *feature) +{ + guard(mutex)(&feature_list_lock); + list_del(&feature->list); +} + +/* Get the discovery table and use it to get the feature table */ +static int pmt_features_discovery(struct pmt_features_priv *priv, + struct feature *feature, + struct intel_vsec_device *ivdev, + int idx) +{ + struct feature_discovery_table disc_tbl = {}; /* Avoid false warning */ + struct resource *disc_res = &ivdev->resource[idx]; + const struct kobj_type *ktype; + int ret; + + ret = pmt_feature_get_disc_table(priv, disc_res, &disc_tbl); + if (ret) + return ret; + + ret = pmt_feature_get_feature_table(priv, feature, &disc_tbl, disc_res); + if (ret) + return ret; + + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + ktype = &pmt_feature_rmid_ktype; + feature->attr_group = &pmt_feature_rmid_group; + break; + case LAYOUT_WATCHER: + ktype = &pmt_feature_watcher_ktype; + feature->attr_group = &pmt_feature_watcher_group; + break; + case LAYOUT_COMMAND: + ktype = &pmt_feature_command_ktype; + feature->attr_group = &pmt_feature_command_group; + break; + case LAYOUT_CAPS_ONLY: + ktype = &pmt_feature_guids_ktype; + feature->attr_group = &pmt_feature_guids_group; + break; + default: + return -EINVAL; + } + + ret = kobject_init_and_add(&feature->kobj, ktype, &priv->dev->kobj, + "%s", pmt_feature_names[feature->id]); + if (ret) { + kobject_put(&feature->kobj); + return ret; + } + + kobject_uevent(&feature->kobj, KOBJ_ADD); + pmt_features_add_feat(feature); + + return 0; +} + +static void pmt_features_remove(struct auxiliary_device *auxdev) +{ + struct pmt_features_priv *priv = auxiliary_get_drvdata(auxdev); + int i; + + for (i = 0; i < priv->count; i++) { + struct feature *feature = &priv->feature[i]; + + pmt_features_remove_feat(feature); + sysfs_remove_group(&feature->kobj, feature->attr_group); + kobject_put(&feature->kobj); + } + + device_unregister(priv->dev); +} + +static int pmt_features_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct intel_vsec_device *ivdev = auxdev_to_ivdev(auxdev); + struct pmt_features_priv *priv; + size_t size; + int ret, i; + + size = struct_size(priv, feature, ivdev->num_resources); + priv = devm_kzalloc(&auxdev->dev, size, GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->parent = ivdev->dev; + auxiliary_set_drvdata(auxdev, priv); + + priv->dev = device_create(&intel_pmt_class, &auxdev->dev, MKDEV(0, 0), priv, + "%s-%s", "features", dev_name(priv->parent)); + if (IS_ERR(priv->dev)) + return dev_err_probe(&auxdev->dev, PTR_ERR(priv->dev), + "Could not create %s-%s device node\n", + "features", dev_name(priv->parent)); + + /* Initialize each feature */ + for (i = 0; i < ivdev->num_resources; i++) { + struct feature *feature = &priv->feature[priv->count]; + + ret = pmt_features_discovery(priv, feature, ivdev, i); + if (ret == SKIP_FEATURE) + continue; + if (ret != 0) + goto abort_probe; + + feature->priv = priv; + priv->count++; + } + + return 0; + +abort_probe: + /* + * Only fully initialized features are tracked in priv->count, which is + * incremented only after a feature is completely set up (i.e., after + * discovery and sysfs registration). If feature initialization fails, + * the failing feature's state is local and does not require rollback. + * + * Therefore, on error, we can safely call the driver's remove() routine + * pmt_features_remove() to clean up only those features that were + * fully initialized and counted. All other resources are device-managed + * and will be cleaned up automatically during device_unregister(). + */ + pmt_features_remove(auxdev); + + return ret; +} + +static void pmt_get_features(struct intel_pmt_entry *entry, struct feature *f) +{ + int num_guids = f->table.header.num_guids; + int i; + + for (i = 0; i < num_guids; i++) { + if (f->table.guids[i] != entry->guid) + continue; + + entry->feature_flags |= BIT(f->id); + + if (feature_layout[f->id] == LAYOUT_RMID) + entry->num_rmids = f->table.rmid.num_rmids; + else + entry->num_rmids = 0; /* entry is kzalloc but set anyway */ + } +} + +void intel_pmt_get_features(struct intel_pmt_entry *entry) +{ + struct feature *feature; + + mutex_lock(&feature_list_lock); + list_for_each_entry(feature, &pmt_feature_list, list) { + if (feature->priv->parent != entry->ep->dev) + continue; + + pmt_get_features(entry, feature); + } + mutex_unlock(&feature_list_lock); +} +EXPORT_SYMBOL_NS_GPL(intel_pmt_get_features, "INTEL_PMT"); + +static const struct auxiliary_device_id pmt_features_id_table[] = { + { .name = "intel_vsec.discovery" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, pmt_features_id_table); + +static struct auxiliary_driver pmt_features_aux_driver = { + .id_table = pmt_features_id_table, + .remove = pmt_features_remove, + .probe = pmt_features_probe, +}; +module_auxiliary_driver(pmt_features_aux_driver); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMT Discovery driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("INTEL_PMT"); diff --git a/drivers/platform/x86/intel/pmt/features.c b/drivers/platform/x86/intel/pmt/features.c new file mode 100644 index 000000000..8a39cddc7 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/features.c @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + * + * Author: "David E. Box" + */ + +#include +#include + +#include + +const char * const pmt_feature_names[] = { + [FEATURE_PER_CORE_PERF_TELEM] = "per_core_performance_telemetry", + [FEATURE_PER_CORE_ENV_TELEM] = "per_core_environment_telemetry", + [FEATURE_PER_RMID_PERF_TELEM] = "per_rmid_perf_telemetry", + [FEATURE_ACCEL_TELEM] = "accelerator_telemetry", + [FEATURE_UNCORE_TELEM] = "uncore_telemetry", + [FEATURE_CRASH_LOG] = "crash_log", + [FEATURE_PETE_LOG] = "pete_log", + [FEATURE_TPMI_CTRL] = "tpmi_control", + [FEATURE_TRACING] = "tracing", + [FEATURE_PER_RMID_ENERGY_TELEM] = "per_rmid_energy_telemetry", +}; +EXPORT_SYMBOL_NS_GPL(pmt_feature_names, "INTEL_PMT_DISCOVERY"); + +enum feature_layout feature_layout[] = { + [FEATURE_PER_CORE_PERF_TELEM] = LAYOUT_WATCHER, + [FEATURE_PER_CORE_ENV_TELEM] = LAYOUT_WATCHER, + [FEATURE_PER_RMID_PERF_TELEM] = LAYOUT_RMID, + [FEATURE_ACCEL_TELEM] = LAYOUT_WATCHER, + [FEATURE_UNCORE_TELEM] = LAYOUT_WATCHER, + [FEATURE_CRASH_LOG] = LAYOUT_COMMAND, + [FEATURE_PETE_LOG] = LAYOUT_COMMAND, + [FEATURE_TPMI_CTRL] = LAYOUT_CAPS_ONLY, + [FEATURE_TRACING] = LAYOUT_CAPS_ONLY, + [FEATURE_PER_RMID_ENERGY_TELEM] = LAYOUT_RMID, +}; + +struct pmt_cap pmt_cap_common[] = { + {PMT_CAP_TELEM, "telemetry"}, + {PMT_CAP_WATCHER, "watcher"}, + {PMT_CAP_CRASHLOG, "crashlog"}, + {PMT_CAP_STREAMING, "streaming"}, + {PMT_CAP_THRESHOLD, "threshold"}, + {PMT_CAP_WINDOW, "window"}, + {PMT_CAP_CONFIG, "config"}, + {PMT_CAP_TRACING, "tracing"}, + {PMT_CAP_INBAND, "inband"}, + {PMT_CAP_OOB, "oob"}, + {PMT_CAP_SECURED_CHAN, "secure_chan"}, + {PMT_CAP_PMT_SP, "pmt_sp"}, + {PMT_CAP_PMT_SP_POLICY, "pmt_sp_policy"}, + {} +}; + +struct pmt_cap pmt_cap_pcpt[] = { + {PMT_CAP_PCPT_CORE_PERF, "core_performance"}, + {PMT_CAP_PCPT_CORE_C0_RES, "core_c0_residency"}, + {PMT_CAP_PCPT_CORE_ACTIVITY, "core_activity"}, + {PMT_CAP_PCPT_CACHE_PERF, "cache_performance"}, + {PMT_CAP_PCPT_QUALITY_TELEM, "quality_telemetry"}, + {} +}; + +struct pmt_cap *pmt_caps_pcpt[] = { + pmt_cap_common, + pmt_cap_pcpt, + NULL +}; + +struct pmt_cap pmt_cap_pcet[] = { + {PMT_CAP_PCET_WORKPOINT_HIST, "workpoint_histogram"}, + {PMT_CAP_PCET_CORE_CURR_TEMP, "core_current_temp"}, + {PMT_CAP_PCET_CORE_INST_RES, "core_inst_residency"}, + {PMT_CAP_PCET_QUALITY_TELEM, "quality_telemetry"}, + {PMT_CAP_PCET_CORE_CDYN_LVL, "core_cdyn_level"}, + {PMT_CAP_PCET_CORE_STRESS_LVL, "core_stress_level"}, + {PMT_CAP_PCET_CORE_DAS, "core_digital_aging_sensor"}, + {PMT_CAP_PCET_FIVR_HEALTH, "fivr_health"}, + {PMT_CAP_PCET_ENERGY, "energy"}, + {PMT_CAP_PCET_PEM_STATUS, "pem_status"}, + {PMT_CAP_PCET_CORE_C_STATE, "core_c_state"}, + {} +}; + +struct pmt_cap *pmt_caps_pcet[] = { + pmt_cap_common, + pmt_cap_pcet, + NULL +}; + +struct pmt_cap pmt_cap_rmid_perf[] = { + {PMT_CAP_RMID_CORES_PERF, "core_performance"}, + {PMT_CAP_RMID_CACHE_PERF, "cache_performance"}, + {PMT_CAP_RMID_PERF_QUAL, "performance_quality"}, + {} +}; + +struct pmt_cap *pmt_caps_rmid_perf[] = { + pmt_cap_common, + pmt_cap_rmid_perf, + NULL +}; + +struct pmt_cap pmt_cap_accel[] = { + {PMT_CAP_ACCEL_CPM_TELEM, "content_processing_module"}, + {PMT_CAP_ACCEL_TIP_TELEM, "content_turbo_ip"}, + {} +}; + +struct pmt_cap *pmt_caps_accel[] = { + pmt_cap_common, + pmt_cap_accel, + NULL +}; + +struct pmt_cap pmt_cap_uncore[] = { + {PMT_CAP_UNCORE_IO_CA_TELEM, "io_ca"}, + {PMT_CAP_UNCORE_RMID_TELEM, "rmid"}, + {PMT_CAP_UNCORE_D2D_ULA_TELEM, "d2d_ula"}, + {PMT_CAP_UNCORE_PKGC_TELEM, "package_c"}, + {} +}; + +struct pmt_cap *pmt_caps_uncore[] = { + pmt_cap_common, + pmt_cap_uncore, + NULL +}; + +struct pmt_cap pmt_cap_crashlog[] = { + {PMT_CAP_CRASHLOG_MAN_TRIG, "manual_trigger"}, + {PMT_CAP_CRASHLOG_CORE, "core"}, + {PMT_CAP_CRASHLOG_UNCORE, "uncore"}, + {PMT_CAP_CRASHLOG_TOR, "tor"}, + {PMT_CAP_CRASHLOG_S3M, "s3m"}, + {PMT_CAP_CRASHLOG_PERSISTENCY, "persistency"}, + {PMT_CAP_CRASHLOG_CLIP_GPIO, "crashlog_in_progress"}, + {PMT_CAP_CRASHLOG_PRE_RESET, "pre_reset_extraction"}, + {PMT_CAP_CRASHLOG_POST_RESET, "post_reset_extraction"}, + {} +}; + +struct pmt_cap *pmt_caps_crashlog[] = { + pmt_cap_common, + pmt_cap_crashlog, + NULL +}; + +struct pmt_cap pmt_cap_pete[] = { + {PMT_CAP_PETE_MAN_TRIG, "manual_trigger"}, + {PMT_CAP_PETE_ENCRYPTION, "encryption"}, + {PMT_CAP_PETE_PERSISTENCY, "persistency"}, + {PMT_CAP_PETE_REQ_TOKENS, "required_tokens"}, + {PMT_CAP_PETE_PROD_ENABLED, "production_enabled"}, + {PMT_CAP_PETE_DEBUG_ENABLED, "debug_enabled"}, + {} +}; + +struct pmt_cap *pmt_caps_pete[] = { + pmt_cap_common, + pmt_cap_pete, + NULL +}; + +struct pmt_cap pmt_cap_tpmi[] = { + {PMT_CAP_TPMI_MAILBOX, "mailbox"}, + {PMT_CAP_TPMI_LOCK, "bios_lock"}, + {} +}; + +struct pmt_cap *pmt_caps_tpmi[] = { + pmt_cap_common, + pmt_cap_tpmi, + NULL +}; + +struct pmt_cap pmt_cap_tracing[] = { + {PMT_CAP_TRACE_SRAR, "srar_errors"}, + {PMT_CAP_TRACE_CORRECTABLE, "correctable_errors"}, + {PMT_CAP_TRACE_MCTP, "mctp"}, + {PMT_CAP_TRACE_MRT, "memory_resiliency"}, + {} +}; + +struct pmt_cap *pmt_caps_tracing[] = { + pmt_cap_common, + pmt_cap_tracing, + NULL +}; + +struct pmt_cap pmt_cap_rmid_energy[] = { + {PMT_CAP_RMID_ENERGY, "energy"}, + {PMT_CAP_RMID_ACTIVITY, "activity"}, + {PMT_CAP_RMID_ENERGY_QUAL, "energy_quality"}, + {} +}; + +struct pmt_cap *pmt_caps_rmid_energy[] = { + pmt_cap_common, + pmt_cap_rmid_energy, + NULL +}; diff --git a/drivers/platform/x86/intel/pmt/telemetry.c b/drivers/platform/x86/intel/pmt/telemetry.c new file mode 100644 index 000000000..953f35b6d --- /dev/null +++ b/drivers/platform/x86/intel/pmt/telemetry.c @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Telemetry driver + * + * Copyright (c) 2020, Intel Corporation. + * All Rights Reserved. + * + * Author: "David E. Box" + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "class.h" + +#define TELEM_SIZE_OFFSET 0x0 +#define TELEM_GUID_OFFSET 0x4 +#define TELEM_BASE_OFFSET 0x8 +#define TELEM_ACCESS(v) ((v) & GENMASK(3, 0)) +#define TELEM_TYPE(v) (((v) & GENMASK(7, 4)) >> 4) +/* size is in bytes */ +#define TELEM_SIZE(v) (((v) & GENMASK(27, 12)) >> 10) + +/* Used by client hardware to identify a fixed telemetry entry*/ +#define TELEM_CLIENT_FIXED_BLOCK_GUID 0x10000000 + +#define NUM_BYTES_QWORD(v) ((v) << 3) +#define SAMPLE_ID_OFFSET(v) ((v) << 3) + +#define NUM_BYTES_DWORD(v) ((v) << 2) +#define SAMPLE_ID_OFFSET32(v) ((v) << 2) + +/* Protects access to the xarray of telemetry endpoint handles */ +static DEFINE_MUTEX(ep_lock); + +enum telem_type { + TELEM_TYPE_PUNIT = 0, + TELEM_TYPE_CRASHLOG, + TELEM_TYPE_PUNIT_FIXED, +}; + +struct pmt_telem_priv { + int num_entries; + struct intel_pmt_entry entry[]; +}; + +static bool pmt_telem_region_overlaps(struct device *dev, u32 guid, u32 type) +{ + if (intel_pmt_is_early_client_hw(dev)) { + if ((type == TELEM_TYPE_PUNIT_FIXED) || + (guid == TELEM_CLIENT_FIXED_BLOCK_GUID)) + return true; + } + + return false; +} + +static int pmt_telem_header_decode(struct intel_pmt_entry *entry, + struct device *dev) +{ + struct intel_pmt_header *header = &entry->header; + u32 *disc_header = entry->disc_header; + + header->access_type = TELEM_ACCESS(disc_header[0]); + header->guid = disc_header[1]; + header->base_offset = disc_header[2]; + + /* Size is measured in DWORDS, but accessor returns bytes */ + header->size = TELEM_SIZE(disc_header[0]); + header->telem_type = TELEM_TYPE(disc_header[0]); + + return 0; +} + +static int pmt_telem_post_decode(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry) +{ + struct intel_pmt_header *header = &entry->header; + struct device *dev = &ivdev->auxdev.dev; + + if (pmt_telem_region_overlaps(dev, header->guid, header->telem_type)) + return 1; + + /* + * Some devices may expose non-functioning entries that are + * reserved for future use. They have zero size. Do not fail + * probe for these. Just ignore them. + */ + if (header->size == 0 || header->access_type == 0xF) + return 1; + + return 0; +} + +static int pmt_telem_add_endpoint(struct intel_vsec_device *ivdev, + struct intel_pmt_entry *entry) +{ + struct telem_endpoint *ep; + + /* Endpoint lifetimes are managed by kref, not devres */ + entry->ep = kzalloc_obj(*(entry->ep)); + if (!entry->ep) + return -ENOMEM; + + ep = entry->ep; + ep->dev = ivdev->dev; + ep->header.access_type = entry->header.access_type; + ep->header.guid = entry->header.guid; + ep->header.base_offset = entry->header.base_offset; + ep->header.size = entry->header.size; + ep->base = entry->base; + ep->present = true; + ep->cb = ivdev->priv_data; + + kref_init(&ep->kref); + + return 0; +} + +static DEFINE_XARRAY_ALLOC(telem_array); +static struct intel_pmt_namespace pmt_telem_ns = { + .name = "telem", + .xa = &telem_array, + .pmt_header_decode = pmt_telem_header_decode, + .pmt_post_decode = pmt_telem_post_decode, + .pmt_add_endpoint = pmt_telem_add_endpoint, +}; + +/* Called when all users unregister and the device is removed */ +static void pmt_telem_ep_release(struct kref *kref) +{ + struct telem_endpoint *ep; + + ep = container_of(kref, struct telem_endpoint, kref); + kfree(ep); +} + +unsigned long pmt_telem_get_next_endpoint(unsigned long start) +{ + struct intel_pmt_entry *entry; + unsigned long found_idx; + + mutex_lock(&ep_lock); + xa_for_each_start(&telem_array, found_idx, entry, start) { + /* + * Return first found index after start. + * 0 is not valid id. + */ + if (found_idx > start) + break; + } + mutex_unlock(&ep_lock); + + return found_idx == start ? 0 : found_idx; +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_get_next_endpoint, "INTEL_PMT_TELEMETRY"); + +struct telem_endpoint *pmt_telem_register_endpoint(int devid) +{ + struct intel_pmt_entry *entry; + unsigned long index = devid; + + mutex_lock(&ep_lock); + entry = xa_find(&telem_array, &index, index, XA_PRESENT); + if (!entry) { + mutex_unlock(&ep_lock); + return ERR_PTR(-ENXIO); + } + + kref_get(&entry->ep->kref); + mutex_unlock(&ep_lock); + + return entry->ep; +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_register_endpoint, "INTEL_PMT_TELEMETRY"); + +void pmt_telem_unregister_endpoint(struct telem_endpoint *ep) +{ + kref_put(&ep->kref, pmt_telem_ep_release); +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_unregister_endpoint, "INTEL_PMT_TELEMETRY"); + +int pmt_telem_get_endpoint_info(int devid, struct telem_endpoint_info *info) +{ + struct intel_pmt_entry *entry; + unsigned long index = devid; + int err = 0; + + if (!info) + return -EINVAL; + + mutex_lock(&ep_lock); + entry = xa_find(&telem_array, &index, index, XA_PRESENT); + if (!entry) { + err = -ENXIO; + goto unlock; + } + + info->dev = entry->ep->dev; + info->header = entry->ep->header; + +unlock: + mutex_unlock(&ep_lock); + return err; + +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_get_endpoint_info, "INTEL_PMT_TELEMETRY"); + +static int pmt_copy_region(struct telemetry_region *region, + struct intel_pmt_entry *entry) +{ + + struct pci_dev *pdev = to_pci_dev(entry->ep->dev); + struct oobmsm_plat_info *plat_info; + + plat_info = intel_vsec_get_mapping(pdev); + if (IS_ERR(plat_info)) + return PTR_ERR(plat_info); + + region->plat_info = *plat_info; + region->guid = entry->guid; + region->addr = entry->ep->base; + region->size = entry->size; + region->num_rmids = entry->num_rmids; + + return 0; +} + +static void pmt_feature_group_release(struct kref *kref) +{ + struct pmt_feature_group *feature_group; + + feature_group = container_of(kref, struct pmt_feature_group, kref); + kfree(feature_group); +} + +struct pmt_feature_group *intel_pmt_get_regions_by_feature(enum pmt_feature_id id) +{ + struct pmt_feature_group *feature_group __free(kfree) = NULL; + struct telemetry_region *region; + struct intel_pmt_entry *entry; + unsigned long idx; + int count = 0; + size_t size; + + if (!pmt_feature_id_is_valid(id)) + return ERR_PTR(-EINVAL); + + guard(mutex)(&ep_lock); + xa_for_each(&telem_array, idx, entry) { + if (entry->feature_flags & BIT(id)) + count++; + } + + if (!count) + return ERR_PTR(-ENOENT); + + size = struct_size(feature_group, regions, count); + feature_group = kzalloc(size, GFP_KERNEL); + if (!feature_group) + return ERR_PTR(-ENOMEM); + + feature_group->count = count; + + region = feature_group->regions; + xa_for_each(&telem_array, idx, entry) { + int ret; + + if (!(entry->feature_flags & BIT(id))) + continue; + + ret = pmt_copy_region(region, entry); + if (ret) + return ERR_PTR(ret); + + region++; + } + + kref_init(&feature_group->kref); + + return no_free_ptr(feature_group); +} +EXPORT_SYMBOL(intel_pmt_get_regions_by_feature); + +void intel_pmt_put_feature_group(struct pmt_feature_group *feature_group) +{ + kref_put(&feature_group->kref, pmt_feature_group_release); +} +EXPORT_SYMBOL(intel_pmt_put_feature_group); + +int pmt_telem_read(struct telem_endpoint *ep, u32 id, u64 *data, u32 count) +{ + u32 offset, size; + + if (!ep->present) + return -ENODEV; + + offset = SAMPLE_ID_OFFSET(id); + size = ep->header.size; + + if (offset + NUM_BYTES_QWORD(count) > size) + return -EINVAL; + + pmt_telem_read_mmio(ep->dev, ep->cb, ep->header.guid, data, ep->base, offset, + NUM_BYTES_QWORD(count)); + + return ep->present ? 0 : -EPIPE; +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_read, "INTEL_PMT_TELEMETRY"); + +int pmt_telem_read32(struct telem_endpoint *ep, u32 id, u32 *data, u32 count) +{ + u32 offset, size; + + if (!ep->present) + return -ENODEV; + + offset = SAMPLE_ID_OFFSET32(id); + size = ep->header.size; + + if (offset + NUM_BYTES_DWORD(count) > size) + return -EINVAL; + + memcpy_fromio(data, ep->base + offset, NUM_BYTES_DWORD(count)); + + return ep->present ? 0 : -EPIPE; +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_read32, "INTEL_PMT_TELEMETRY"); + +struct telem_endpoint * +pmt_telem_find_and_register_endpoint(struct device *dev, u32 guid, u16 pos) +{ + int devid = 0; + int inst = 0; + int err = 0; + + while ((devid = pmt_telem_get_next_endpoint(devid))) { + struct telem_endpoint_info ep_info; + + err = pmt_telem_get_endpoint_info(devid, &ep_info); + if (err) + return ERR_PTR(err); + + if (ep_info.header.guid == guid && ep_info.dev == dev) { + if (inst == pos) + return pmt_telem_register_endpoint(devid); + ++inst; + } + } + + return ERR_PTR(-ENXIO); +} +EXPORT_SYMBOL_NS_GPL(pmt_telem_find_and_register_endpoint, "INTEL_PMT_TELEMETRY"); + +static void pmt_telem_remove(struct auxiliary_device *auxdev) +{ + struct pmt_telem_priv *priv = auxiliary_get_drvdata(auxdev); + int i; + + mutex_lock(&ep_lock); + for (i = 0; i < priv->num_entries; i++) { + struct intel_pmt_entry *entry = &priv->entry[i]; + + kref_put(&entry->ep->kref, pmt_telem_ep_release); + intel_pmt_dev_destroy(entry, &pmt_telem_ns); + } + mutex_unlock(&ep_lock); +}; + +static int pmt_telem_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct intel_vsec_device *intel_vsec_dev = auxdev_to_ivdev(auxdev); + struct pmt_telem_priv *priv; + size_t size; + int i, ret; + + size = struct_size(priv, entry, intel_vsec_dev->num_resources); + priv = devm_kzalloc(&auxdev->dev, size, GFP_KERNEL); + if (!priv) + return -ENOMEM; + + auxiliary_set_drvdata(auxdev, priv); + + for (i = 0; i < intel_vsec_dev->num_resources; i++) { + struct intel_pmt_entry *entry = &priv->entry[priv->num_entries]; + + mutex_lock(&ep_lock); + ret = intel_pmt_dev_create(entry, &pmt_telem_ns, intel_vsec_dev, i); + mutex_unlock(&ep_lock); + if (ret < 0) + goto abort_probe; + if (ret) + continue; + + priv->num_entries++; + + intel_pmt_get_features(entry); + } + + return 0; +abort_probe: + pmt_telem_remove(auxdev); + return ret; +} + +static const struct auxiliary_device_id pmt_telem_id_table[] = { + { .name = "intel_vsec.telemetry" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, pmt_telem_id_table); + +static struct auxiliary_driver pmt_telem_aux_driver = { + .id_table = pmt_telem_id_table, + .remove = pmt_telem_remove, + .probe = pmt_telem_probe, +}; + +static int __init pmt_telem_init(void) +{ + return auxiliary_driver_register(&pmt_telem_aux_driver); +} +module_init(pmt_telem_init); + +static void __exit pmt_telem_exit(void) +{ + auxiliary_driver_unregister(&pmt_telem_aux_driver); + xa_destroy(&telem_array); +} +module_exit(pmt_telem_exit); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMT Telemetry driver"); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("INTEL_PMT"); +MODULE_IMPORT_NS("INTEL_VSEC"); diff --git a/drivers/platform/x86/intel/pmt/telemetry.h b/drivers/platform/x86/intel/pmt/telemetry.h new file mode 100644 index 000000000..0f88c5e7d --- /dev/null +++ b/drivers/platform/x86/intel/pmt/telemetry.h @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _TELEMETRY_H +#define _TELEMETRY_H + +/* Telemetry types */ +#define PMT_TELEM_TELEMETRY 0 +#define PMT_TELEM_CRASHLOG 1 + +struct device; +struct telem_endpoint; + +struct telem_header { + u8 access_type; + u16 size; + u32 guid; + u32 base_offset; +}; + +struct telem_endpoint_info { + struct device *dev; + struct telem_header header; +}; + +/** + * pmt_telem_get_next_endpoint() - Get next device id for a telemetry endpoint + * @start: starting devid to look from + * + * This functions can be used in a while loop predicate to retrieve the devid + * of all available telemetry endpoints. Functions pmt_telem_get_next_endpoint() + * and pmt_telem_register_endpoint() can be used inside of the loop to examine + * endpoint info and register to receive a pointer to the endpoint. The pointer + * is then usable in the telemetry read calls to access the telemetry data. + * + * Return: + * * devid - devid of the next present endpoint from start + * * 0 - when no more endpoints are present after start + */ +unsigned long pmt_telem_get_next_endpoint(unsigned long start); + +/** + * pmt_telem_register_endpoint() - Register a telemetry endpoint + * @devid: device id/handle of the telemetry endpoint + * + * Increments the kref usage counter for the endpoint. + * + * Return: + * * endpoint - On success returns pointer to the telemetry endpoint + * * -ENXIO - telemetry endpoint not found + */ +struct telem_endpoint *pmt_telem_register_endpoint(int devid); + +/** + * pmt_telem_unregister_endpoint() - Unregister a telemetry endpoint + * @ep: ep structure to populate. + * + * Decrements the kref usage counter for the endpoint. + */ +void pmt_telem_unregister_endpoint(struct telem_endpoint *ep); + +/** + * pmt_telem_get_endpoint_info() - Get info for an endpoint from its devid + * @devid: device id/handle of the telemetry endpoint + * @info: Endpoint info structure to be populated + * + * Return: + * * 0 - Success + * * -ENXIO - telemetry endpoint not found for the devid + * * -EINVAL - @info is NULL + */ +int pmt_telem_get_endpoint_info(int devid, struct telem_endpoint_info *info); + +/** + * pmt_telem_find_and_register_endpoint() - Get a telemetry endpoint from + * device, guid and pos + * @dev: device inside the Intel vsec + * @guid: GUID of the telemetry space + * @pos: Instance of the guid + * + * Return: + * * endpoint - On success returns pointer to the telemetry endpoint + * * -ENXIO - telemetry endpoint not found + */ +struct telem_endpoint * +pmt_telem_find_and_register_endpoint(struct device *dev, u32 guid, u16 pos); + +/** + * pmt_telem_read() - Read qwords from counter sram using sample id + * @ep: Telemetry endpoint to be read + * @id: The beginning sample id of the metric(s) to be read + * @data: Allocated qword buffer + * @count: Number of qwords requested + * + * Callers must ensure reads are aligned. When the call returns -ENODEV, + * the device has been removed and callers should unregister the telemetry + * endpoint. + * + * Return: + * * 0 - Success + * * -ENODEV - The device is not present. + * * -EINVAL - The offset is out bounds + * * -EPIPE - The device was removed during the read. Data written + * but should be considered invalid. + */ +int pmt_telem_read(struct telem_endpoint *ep, u32 id, u64 *data, u32 count); + +/** + * pmt_telem_read32() - Read qwords from counter sram using sample id + * @ep: Telemetry endpoint to be read + * @id: The beginning sample id of the metric(s) to be read + * @data: Allocated dword buffer + * @count: Number of dwords requested + * + * Callers must ensure reads are aligned. When the call returns -ENODEV, + * the device has been removed and callers should unregister the telemetry + * endpoint. + * + * Return: + * * 0 - Success + * * -ENODEV - The device is not present. + * * -EINVAL - The offset is out bounds + * * -EPIPE - The device was removed during the read. Data written + * but should be considered invalid. + */ +int pmt_telem_read32(struct telem_endpoint *ep, u32 id, u32 *data, u32 count); + +#endif diff --git a/drivers/platform/x86/intel/punit_ipc.c b/drivers/platform/x86/intel/punit_ipc.c new file mode 100644 index 000000000..6d770b950 --- /dev/null +++ b/drivers/platform/x86/intel/punit_ipc.c @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for the Intel P-Unit Mailbox IPC mechanism + * + * (C) Copyright 2015 Intel Corporation + * + * The heart of the P-Unit is the Foxton microcontroller and its firmware, + * which provide mailbox interface for power management usage. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* IPC Mailbox registers */ +#define OFFSET_DATA_LOW 0x0 +#define OFFSET_DATA_HIGH 0x4 +/* bit field of interface register */ +#define CMD_RUN BIT(31) +#define CMD_ERRCODE_MASK GENMASK(7, 0) +#define CMD_PARA1_SHIFT 8 +#define CMD_PARA2_SHIFT 16 + +#define CMD_TIMEOUT_SECONDS 1 + +enum { + BASE_DATA = 0, + BASE_IFACE, + BASE_MAX, +}; + +typedef struct { + struct device *dev; + struct mutex lock; + int irq; + struct completion cmd_complete; + /* base of interface and data registers */ + void __iomem *base[RESERVED_IPC][BASE_MAX]; + IPC_TYPE type; +} IPC_DEV; + +static IPC_DEV *punit_ipcdev; + +static inline u32 ipc_read_status(IPC_DEV *ipcdev, IPC_TYPE type) +{ + return readl(ipcdev->base[type][BASE_IFACE]); +} + +static inline void ipc_write_cmd(IPC_DEV *ipcdev, IPC_TYPE type, u32 cmd) +{ + writel(cmd, ipcdev->base[type][BASE_IFACE]); +} + +static inline u32 ipc_read_data_low(IPC_DEV *ipcdev, IPC_TYPE type) +{ + return readl(ipcdev->base[type][BASE_DATA] + OFFSET_DATA_LOW); +} + +static inline u32 ipc_read_data_high(IPC_DEV *ipcdev, IPC_TYPE type) +{ + return readl(ipcdev->base[type][BASE_DATA] + OFFSET_DATA_HIGH); +} + +static inline void ipc_write_data_low(IPC_DEV *ipcdev, IPC_TYPE type, u32 data) +{ + writel(data, ipcdev->base[type][BASE_DATA] + OFFSET_DATA_LOW); +} + +static inline void ipc_write_data_high(IPC_DEV *ipcdev, IPC_TYPE type, u32 data) +{ + writel(data, ipcdev->base[type][BASE_DATA] + OFFSET_DATA_HIGH); +} + +static const char *ipc_err_string(int error) +{ + if (error == IPC_PUNIT_ERR_SUCCESS) + return "no error"; + else if (error == IPC_PUNIT_ERR_INVALID_CMD) + return "invalid command"; + else if (error == IPC_PUNIT_ERR_INVALID_PARAMETER) + return "invalid parameter"; + else if (error == IPC_PUNIT_ERR_CMD_TIMEOUT) + return "command timeout"; + else if (error == IPC_PUNIT_ERR_CMD_LOCKED) + return "command locked"; + else if (error == IPC_PUNIT_ERR_INVALID_VR_ID) + return "invalid vr id"; + else if (error == IPC_PUNIT_ERR_VR_ERR) + return "vr error"; + else + return "unknown error"; +} + +static int intel_punit_ipc_check_status(IPC_DEV *ipcdev, IPC_TYPE type) +{ + int loops = CMD_TIMEOUT_SECONDS * USEC_PER_SEC; + int errcode; + int status; + + if (ipcdev->irq) { + if (!wait_for_completion_timeout(&ipcdev->cmd_complete, + CMD_TIMEOUT_SECONDS * HZ)) { + dev_err(ipcdev->dev, "IPC timed out\n"); + return -ETIMEDOUT; + } + } else { + while ((ipc_read_status(ipcdev, type) & CMD_RUN) && --loops) + udelay(1); + if (!loops) { + dev_err(ipcdev->dev, "IPC timed out\n"); + return -ETIMEDOUT; + } + } + + status = ipc_read_status(ipcdev, type); + errcode = status & CMD_ERRCODE_MASK; + if (errcode) { + dev_err(ipcdev->dev, "IPC failed: %s, IPC_STS=0x%x\n", + ipc_err_string(errcode), status); + return -EIO; + } + + return 0; +} + +/** + * intel_punit_ipc_command() - IPC command with data and pointers + * @cmd: IPC command code. + * @para1: First 8bit parameter, set 0 if not used. + * @para2: Second 8bit parameter, set 0 if not used. + * @in: Input data, 32bit for BIOS cmd, two 32bit for GTD and ISPD. + * @out: Output data. + * + * Send a IPC command to P-Unit with data transaction + * + * Return: IPC error code or 0 on success. + */ +int intel_punit_ipc_command(u32 cmd, u32 para1, u32 para2, u32 *in, u32 *out) +{ + IPC_DEV *ipcdev = punit_ipcdev; + IPC_TYPE type; + u32 val; + int ret; + + mutex_lock(&ipcdev->lock); + + reinit_completion(&ipcdev->cmd_complete); + type = (cmd & IPC_PUNIT_CMD_TYPE_MASK) >> IPC_TYPE_OFFSET; + + if (in) { + ipc_write_data_low(ipcdev, type, *in); + if (type == GTDRIVER_IPC || type == ISPDRIVER_IPC) + ipc_write_data_high(ipcdev, type, *++in); + } + + val = cmd & ~IPC_PUNIT_CMD_TYPE_MASK; + val |= CMD_RUN | para2 << CMD_PARA2_SHIFT | para1 << CMD_PARA1_SHIFT; + ipc_write_cmd(ipcdev, type, val); + + ret = intel_punit_ipc_check_status(ipcdev, type); + if (ret) + goto out; + + if (out) { + *out = ipc_read_data_low(ipcdev, type); + if (type == GTDRIVER_IPC || type == ISPDRIVER_IPC) + *++out = ipc_read_data_high(ipcdev, type); + } + +out: + mutex_unlock(&ipcdev->lock); + return ret; +} +EXPORT_SYMBOL_GPL(intel_punit_ipc_command); + +static irqreturn_t intel_punit_ioc(int irq, void *dev_id) +{ + IPC_DEV *ipcdev = dev_id; + + complete(&ipcdev->cmd_complete); + return IRQ_HANDLED; +} + +static int intel_punit_get_bars(struct platform_device *pdev) +{ + void __iomem *addr; + + /* + * The following resources are required + * - BIOS_IPC BASE_DATA + * - BIOS_IPC BASE_IFACE + */ + addr = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(addr)) + return PTR_ERR(addr); + punit_ipcdev->base[BIOS_IPC][BASE_DATA] = addr; + + addr = devm_platform_ioremap_resource(pdev, 1); + if (IS_ERR(addr)) + return PTR_ERR(addr); + punit_ipcdev->base[BIOS_IPC][BASE_IFACE] = addr; + + /* + * The following resources are optional + * - ISPDRIVER_IPC BASE_DATA + * - ISPDRIVER_IPC BASE_IFACE + * - GTDRIVER_IPC BASE_DATA + * - GTDRIVER_IPC BASE_IFACE + */ + addr = devm_platform_ioremap_resource(pdev, 2); + if (!IS_ERR(addr)) + punit_ipcdev->base[ISPDRIVER_IPC][BASE_DATA] = addr; + + addr = devm_platform_ioremap_resource(pdev, 3); + if (!IS_ERR(addr)) + punit_ipcdev->base[ISPDRIVER_IPC][BASE_IFACE] = addr; + + addr = devm_platform_ioremap_resource(pdev, 4); + if (!IS_ERR(addr)) + punit_ipcdev->base[GTDRIVER_IPC][BASE_DATA] = addr; + + addr = devm_platform_ioremap_resource(pdev, 5); + if (!IS_ERR(addr)) + punit_ipcdev->base[GTDRIVER_IPC][BASE_IFACE] = addr; + + return 0; +} + +static int intel_punit_ipc_probe(struct platform_device *pdev) +{ + int irq, ret; + + punit_ipcdev = devm_kzalloc(&pdev->dev, + sizeof(*punit_ipcdev), GFP_KERNEL); + if (!punit_ipcdev) + return -ENOMEM; + + platform_set_drvdata(pdev, punit_ipcdev); + + irq = platform_get_irq_optional(pdev, 0); + if (irq < 0) { + dev_warn(&pdev->dev, "Invalid IRQ, using polling mode\n"); + } else { + ret = devm_request_irq(&pdev->dev, irq, intel_punit_ioc, + IRQF_NO_SUSPEND, "intel_punit_ipc", + punit_ipcdev); + if (ret) { + dev_err(&pdev->dev, "Failed to request irq: %d\n", irq); + return ret; + } + punit_ipcdev->irq = irq; + } + + ret = intel_punit_get_bars(pdev); + if (ret) + return ret; + + punit_ipcdev->dev = &pdev->dev; + mutex_init(&punit_ipcdev->lock); + init_completion(&punit_ipcdev->cmd_complete); + + return 0; +} + +static const struct acpi_device_id punit_ipc_acpi_ids[] = { + { "INT34D4", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, punit_ipc_acpi_ids); + +static struct platform_driver intel_punit_ipc_driver = { + .probe = intel_punit_ipc_probe, + .driver = { + .name = "intel_punit_ipc", + .acpi_match_table = punit_ipc_acpi_ids, + }, +}; + +static int __init intel_punit_ipc_init(void) +{ + return platform_driver_register(&intel_punit_ipc_driver); +} + +static void __exit intel_punit_ipc_exit(void) +{ + platform_driver_unregister(&intel_punit_ipc_driver); +} + +MODULE_AUTHOR("Zha Qipeng "); +MODULE_DESCRIPTION("Intel P-Unit IPC driver"); +MODULE_LICENSE("GPL v2"); + +/* Some modules are dependent on this, so init earlier */ +fs_initcall(intel_punit_ipc_init); +module_exit(intel_punit_ipc_exit); diff --git a/drivers/platform/x86/intel/rst.c b/drivers/platform/x86/intel/rst.c new file mode 100644 index 000000000..bb19f0d89 --- /dev/null +++ b/drivers/platform/x86/intel/rst.c @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2013 Matthew Garrett + */ + +#include +#include +#include +#include + +MODULE_DESCRIPTION("Intel Rapid Start Technology Driver"); +MODULE_LICENSE("GPL"); + +static ssize_t irst_show_wakeup_events(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi; + unsigned long long value; + acpi_status status; + + acpi = to_acpi_device(dev); + + status = acpi_evaluate_integer(acpi->handle, "GFFS", NULL, &value); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return sprintf(buf, "%lld\n", value); +} + +static ssize_t irst_store_wakeup_events(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi; + acpi_status status; + unsigned long value; + int error; + + acpi = to_acpi_device(dev); + + error = kstrtoul(buf, 0, &value); + if (error) + return error; + + status = acpi_execute_simple_method(acpi->handle, "SFFS", value); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return count; +} + +static struct device_attribute irst_wakeup_attr = { + .attr = { .name = "wakeup_events", .mode = 0600 }, + .show = irst_show_wakeup_events, + .store = irst_store_wakeup_events +}; + +static ssize_t irst_show_wakeup_time(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct acpi_device *acpi; + unsigned long long value; + acpi_status status; + + acpi = to_acpi_device(dev); + + status = acpi_evaluate_integer(acpi->handle, "GFTV", NULL, &value); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return sprintf(buf, "%lld\n", value); +} + +static ssize_t irst_store_wakeup_time(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi; + acpi_status status; + unsigned long value; + int error; + + acpi = to_acpi_device(dev); + + error = kstrtoul(buf, 0, &value); + if (error) + return error; + + status = acpi_execute_simple_method(acpi->handle, "SFTV", value); + if (ACPI_FAILURE(status)) + return -EINVAL; + + return count; +} + +static struct device_attribute irst_timeout_attr = { + .attr = { .name = "wakeup_time", .mode = 0600 }, + .show = irst_show_wakeup_time, + .store = irst_store_wakeup_time +}; + +static int irst_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi; + int error; + + acpi = ACPI_COMPANION(&pdev->dev); + if (!acpi) + return -ENODEV; + + error = device_create_file(&acpi->dev, &irst_timeout_attr); + if (unlikely(error)) + return error; + + error = device_create_file(&acpi->dev, &irst_wakeup_attr); + if (unlikely(error)) + device_remove_file(&acpi->dev, &irst_timeout_attr); + + return error; +} + +static void irst_remove(struct platform_device *pdev) +{ + struct acpi_device *acpi = ACPI_COMPANION(&pdev->dev); + + device_remove_file(&acpi->dev, &irst_wakeup_attr); + device_remove_file(&acpi->dev, &irst_timeout_attr); +} + +static const struct acpi_device_id irst_ids[] = { + {"INT3392", 0}, + {"", 0} +}; + +static struct platform_driver irst_driver = { + .probe = irst_probe, + .remove = irst_remove, + .driver = { + .name = "intel_rapid_start", + .acpi_match_table = irst_ids, + }, +}; + +module_platform_driver(irst_driver); + +MODULE_DEVICE_TABLE(acpi, irst_ids); diff --git a/drivers/platform/x86/intel/sdsi.c b/drivers/platform/x86/intel/sdsi.c new file mode 100644 index 000000000..d7e37d4ac --- /dev/null +++ b/drivers/platform/x86/intel/sdsi.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel On Demand (Software Defined Silicon) driver + * + * Copyright (c) 2022, Intel Corporation. + * All Rights Reserved. + * + * Author: "David E. Box" + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ACCESS_TYPE_BARID 2 +#define ACCESS_TYPE_LOCAL 3 + +#define SDSI_MIN_SIZE_DWORDS 276 +#define SDSI_SIZE_MAILBOX 1024 +#define SDSI_SIZE_REGS 80 +#define SDSI_SIZE_CMD sizeof(u64) + +/* + * Write messages are currently up to the size of the mailbox + * while read messages are up to 4 times the size of the + * mailbox, sent in packets + */ +#define SDSI_SIZE_WRITE_MSG SDSI_SIZE_MAILBOX +#define SDSI_SIZE_READ_MSG (SDSI_SIZE_MAILBOX * 4) + +#define SDSI_ENABLED_FEATURES_OFFSET 16 +#define SDSI_FEATURE_SDSI BIT(3) +#define SDSI_FEATURE_METERING BIT(26) + +#define SDSI_SOCKET_ID_OFFSET 64 +#define SDSI_SOCKET_ID GENMASK(3, 0) + +#define SDSI_MBOX_CMD_SUCCESS 0x40 +#define SDSI_MBOX_CMD_TIMEOUT 0x80 + +#define MBOX_TIMEOUT_US 500000 +#define MBOX_TIMEOUT_ACQUIRE_US 1000 +#define MBOX_POLLING_PERIOD_US 100 +#define MBOX_ACQUIRE_NUM_RETRIES 5 +#define MBOX_ACQUIRE_RETRY_DELAY_MS 500 +#define MBOX_MAX_PACKETS 4 + +#define MBOX_OWNER_NONE 0x00 +#define MBOX_OWNER_INBAND 0x01 + +#define CTRL_RUN_BUSY BIT(0) +#define CTRL_READ_WRITE BIT(1) +#define CTRL_SOM BIT(2) +#define CTRL_EOM BIT(3) +#define CTRL_OWNER GENMASK(5, 4) +#define CTRL_COMPLETE BIT(6) +#define CTRL_READY BIT(7) +#define CTRL_INBAND_LOCK BIT(32) +#define CTRL_METER_ENABLE_DRAM BIT(33) +#define CTRL_STATUS GENMASK(15, 8) +#define CTRL_PACKET_SIZE GENMASK(31, 16) +#define CTRL_MSG_SIZE GENMASK(63, 48) + +#define DISC_TABLE_SIZE 12 +#define DT_ACCESS_TYPE GENMASK(3, 0) +#define DT_SIZE GENMASK(27, 12) +#define DT_TBIR GENMASK(2, 0) +#define DT_OFFSET(v) ((v) & GENMASK(31, 3)) + +#define SDSI_GUID_V1 0x006DD191 +#define GUID_V1_CNTRL_SIZE 8 +#define GUID_V1_REGS_SIZE 72 +#define SDSI_GUID_V2 0xF210D9EF +#define GUID_V2_CNTRL_SIZE 16 +#define GUID_V2_REGS_SIZE 80 + +enum sdsi_command { + SDSI_CMD_PROVISION_AKC = 0x0004, + SDSI_CMD_PROVISION_CAP = 0x0008, + SDSI_CMD_READ_STATE = 0x0010, + SDSI_CMD_READ_METER = 0x0014, +}; + +struct sdsi_mbox_info { + u64 *payload; + void *buffer; + u64 control_flags; + int size; +}; + +struct disc_table { + u32 access_info; + u32 guid; + u32 offset; +}; + +struct sdsi_priv { + struct mutex mb_lock; /* Mailbox access lock */ + struct device *dev; + void __iomem *control_addr; + void __iomem *mbox_addr; + void __iomem *regs_addr; + int control_size; + int maibox_size; + int registers_size; + u32 guid; + u32 features; +}; + +/* SDSi mailbox operations must be performed using 64bit mov instructions */ +static __always_inline void +sdsi_memcpy64_toio(u64 __iomem *to, const u64 *from, size_t count_bytes) +{ + size_t count = count_bytes / sizeof(*to); + int i; + + for (i = 0; i < count; i++) + writeq(from[i], &to[i]); +} + +static __always_inline void +sdsi_memcpy64_fromio(u64 *to, const u64 __iomem *from, size_t count_bytes) +{ + size_t count = count_bytes / sizeof(*to); + int i; + + for (i = 0; i < count; i++) + to[i] = readq(&from[i]); +} + +static inline void sdsi_complete_transaction(struct sdsi_priv *priv) +{ + u64 control = FIELD_PREP(CTRL_COMPLETE, 1); + + lockdep_assert_held(&priv->mb_lock); + writeq(control, priv->control_addr); +} + +static int sdsi_status_to_errno(u32 status) +{ + switch (status) { + case SDSI_MBOX_CMD_SUCCESS: + return 0; + case SDSI_MBOX_CMD_TIMEOUT: + return -ETIMEDOUT; + default: + return -EIO; + } +} + +static int sdsi_mbox_poll(struct sdsi_priv *priv, struct sdsi_mbox_info *info, + size_t *data_size) +{ + struct device *dev = priv->dev; + u32 total, loop, eom, status, message_size; + u64 control; + int ret; + + lockdep_assert_held(&priv->mb_lock); + + /* For reads, data sizes that are larger than the mailbox size are read in packets. */ + total = 0; + loop = 0; + do { + u32 packet_size; + + /* Poll on ready bit */ + ret = readq_poll_timeout(priv->control_addr, control, control & CTRL_READY, + MBOX_POLLING_PERIOD_US, MBOX_TIMEOUT_US); + if (ret) + break; + + eom = FIELD_GET(CTRL_EOM, control); + status = FIELD_GET(CTRL_STATUS, control); + packet_size = FIELD_GET(CTRL_PACKET_SIZE, control); + message_size = FIELD_GET(CTRL_MSG_SIZE, control); + + ret = sdsi_status_to_errno(status); + if (ret) + break; + + if (!packet_size) { + sdsi_complete_transaction(priv); + break; + } + + /* Only the last packet can be less than the mailbox size. */ + if (!eom && packet_size != SDSI_SIZE_MAILBOX) { + dev_err(dev, "Invalid packet size\n"); + ret = -EPROTO; + break; + } + + if (packet_size > SDSI_SIZE_MAILBOX) { + dev_err(dev, "Packet size too large\n"); + ret = -EPROTO; + break; + } + + if (info->buffer) { + void *buf = info->buffer + array_size(SDSI_SIZE_MAILBOX, loop); + + sdsi_memcpy64_fromio(buf, priv->mbox_addr, + round_up(packet_size, SDSI_SIZE_CMD)); + total += packet_size; + } + + sdsi_complete_transaction(priv); + } while (!eom && ++loop < MBOX_MAX_PACKETS); + + if (ret) { + sdsi_complete_transaction(priv); + return ret; + } + + if (!eom) { + dev_err(dev, "Exceeded read attempts\n"); + return -EPROTO; + } + + /* Message size check is only valid for multi-packet transfers */ + if (loop && total != message_size) + dev_warn(dev, "Read count %u differs from expected count %u\n", + total, message_size); + + if (data_size) + *data_size = total; + + return 0; +} + +static int sdsi_mbox_cmd_read(struct sdsi_priv *priv, struct sdsi_mbox_info *info, + size_t *data_size) +{ + u64 control; + + lockdep_assert_held(&priv->mb_lock); + + /* Format and send the read command */ + control = FIELD_PREP(CTRL_EOM, 1) | + FIELD_PREP(CTRL_SOM, 1) | + FIELD_PREP(CTRL_RUN_BUSY, 1) | + FIELD_PREP(CTRL_PACKET_SIZE, info->size) | + info->control_flags; + writeq(control, priv->control_addr); + + return sdsi_mbox_poll(priv, info, data_size); +} + +static int sdsi_mbox_cmd_write(struct sdsi_priv *priv, struct sdsi_mbox_info *info, + size_t *data_size) +{ + u64 control; + + lockdep_assert_held(&priv->mb_lock); + + /* Write rest of the payload */ + sdsi_memcpy64_toio(priv->mbox_addr + SDSI_SIZE_CMD, info->payload + 1, + info->size - SDSI_SIZE_CMD); + + /* Format and send the write command */ + control = FIELD_PREP(CTRL_EOM, 1) | + FIELD_PREP(CTRL_SOM, 1) | + FIELD_PREP(CTRL_RUN_BUSY, 1) | + FIELD_PREP(CTRL_READ_WRITE, 1) | + FIELD_PREP(CTRL_MSG_SIZE, info->size) | + FIELD_PREP(CTRL_PACKET_SIZE, info->size); + writeq(control, priv->control_addr); + + return sdsi_mbox_poll(priv, info, data_size); +} + +static int sdsi_mbox_acquire(struct sdsi_priv *priv, struct sdsi_mbox_info *info) +{ + u64 control; + u32 owner; + int ret, retries = 0; + + lockdep_assert_held(&priv->mb_lock); + + /* Check mailbox is available */ + control = readq(priv->control_addr); + owner = FIELD_GET(CTRL_OWNER, control); + if (owner != MBOX_OWNER_NONE) + return -EBUSY; + + /* + * If there has been no recent transaction and no one owns the mailbox, + * we should acquire it in under 1ms. However, if we've accessed it + * recently it may take up to 2.1 seconds to acquire it again. + */ + do { + /* Write first qword of payload */ + writeq(info->payload[0], priv->mbox_addr); + + /* Check for ownership */ + ret = readq_poll_timeout(priv->control_addr, control, + FIELD_GET(CTRL_OWNER, control) == MBOX_OWNER_INBAND, + MBOX_POLLING_PERIOD_US, MBOX_TIMEOUT_ACQUIRE_US); + + if (FIELD_GET(CTRL_OWNER, control) == MBOX_OWNER_NONE && + retries++ < MBOX_ACQUIRE_NUM_RETRIES) { + msleep(MBOX_ACQUIRE_RETRY_DELAY_MS); + continue; + } + + /* Either we got it or someone else did. */ + break; + } while (true); + + return ret; +} + +static int sdsi_mbox_write(struct sdsi_priv *priv, struct sdsi_mbox_info *info, + size_t *data_size) +{ + int ret; + + lockdep_assert_held(&priv->mb_lock); + + ret = sdsi_mbox_acquire(priv, info); + if (ret) + return ret; + + return sdsi_mbox_cmd_write(priv, info, data_size); +} + +static int sdsi_mbox_read(struct sdsi_priv *priv, struct sdsi_mbox_info *info, size_t *data_size) +{ + int ret; + + lockdep_assert_held(&priv->mb_lock); + + ret = sdsi_mbox_acquire(priv, info); + if (ret) + return ret; + + return sdsi_mbox_cmd_read(priv, info, data_size); +} + +static bool sdsi_ib_locked(struct sdsi_priv *priv) +{ + return !!FIELD_GET(CTRL_INBAND_LOCK, readq(priv->control_addr)); +} + +static ssize_t sdsi_provision(struct sdsi_priv *priv, char *buf, size_t count, + enum sdsi_command command) +{ + struct sdsi_mbox_info info = {}; + int ret; + + if (count > (SDSI_SIZE_WRITE_MSG - SDSI_SIZE_CMD)) + return -EOVERFLOW; + + /* Make sure In-band lock is not set */ + if (sdsi_ib_locked(priv)) + return -EPERM; + + /* Qword aligned message + command qword */ + info.size = round_up(count, SDSI_SIZE_CMD) + SDSI_SIZE_CMD; + + info.payload = kzalloc(info.size, GFP_KERNEL); + if (!info.payload) + return -ENOMEM; + + /* Copy message to payload buffer */ + memcpy(info.payload, buf, count); + + /* Command is last qword of payload buffer */ + info.payload[(info.size - SDSI_SIZE_CMD) / SDSI_SIZE_CMD] = command; + + ret = mutex_lock_interruptible(&priv->mb_lock); + if (ret) + goto free_payload; + + ret = sdsi_mbox_write(priv, &info, NULL); + + mutex_unlock(&priv->mb_lock); + +free_payload: + kfree(info.payload); + + if (ret) + return ret; + + return count; +} + +static ssize_t provision_akc_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + if (off) + return -ESPIPE; + + return sdsi_provision(priv, buf, count, SDSI_CMD_PROVISION_AKC); +} +static const BIN_ATTR_WO(provision_akc, SDSI_SIZE_WRITE_MSG); + +static ssize_t provision_cap_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + if (off) + return -ESPIPE; + + return sdsi_provision(priv, buf, count, SDSI_CMD_PROVISION_CAP); +} +static const BIN_ATTR_WO(provision_cap, SDSI_SIZE_WRITE_MSG); + +static ssize_t +certificate_read(u64 command, u64 control_flags, struct sdsi_priv *priv, + char *buf, loff_t off, size_t count) +{ + struct sdsi_mbox_info info = {}; + size_t size; + int ret; + + if (off) + return 0; + + /* Buffer for return data */ + info.buffer = kmalloc(SDSI_SIZE_READ_MSG, GFP_KERNEL); + if (!info.buffer) + return -ENOMEM; + + info.payload = &command; + info.size = sizeof(command); + info.control_flags = control_flags; + + ret = mutex_lock_interruptible(&priv->mb_lock); + if (ret) + goto free_buffer; + ret = sdsi_mbox_read(priv, &info, &size); + mutex_unlock(&priv->mb_lock); + if (ret < 0) + goto free_buffer; + + if (size > count) + size = count; + + memcpy(buf, info.buffer, size); + +free_buffer: + kfree(info.buffer); + + if (ret) + return ret; + + return size; +} + +static ssize_t +state_certificate_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, loff_t off, + size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + return certificate_read(SDSI_CMD_READ_STATE, 0, priv, buf, off, count); +} +static const BIN_ATTR_ADMIN_RO(state_certificate, SDSI_SIZE_READ_MSG); + +static ssize_t +meter_certificate_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, loff_t off, + size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + return certificate_read(SDSI_CMD_READ_METER, 0, priv, buf, off, count); +} +static const BIN_ATTR_ADMIN_RO(meter_certificate, SDSI_SIZE_READ_MSG); + +static ssize_t +meter_current_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, loff_t off, + size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + return certificate_read(SDSI_CMD_READ_METER, CTRL_METER_ENABLE_DRAM, + priv, buf, off, count); +} +static const BIN_ATTR_ADMIN_RO(meter_current, SDSI_SIZE_READ_MSG); + +static ssize_t registers_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + void __iomem *addr = priv->regs_addr; + int size = priv->registers_size; + + /* + * The check below is performed by the sysfs caller based on the static + * file size. But this may be greater than the actual size which is based + * on the GUID. So check here again based on actual size before reading. + */ + if (off >= size) + return 0; + + if (off + count > size) + count = size - off; + + memcpy_fromio(buf, addr + off, count); + + return count; +} +static const BIN_ATTR_ADMIN_RO(registers, SDSI_SIZE_REGS); + +static const struct bin_attribute *const sdsi_bin_attrs[] = { + &bin_attr_registers, + &bin_attr_state_certificate, + &bin_attr_meter_certificate, + &bin_attr_meter_current, + &bin_attr_provision_akc, + &bin_attr_provision_cap, + NULL +}; + +static umode_t +sdsi_battr_is_visible(struct kobject *kobj, const struct bin_attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct sdsi_priv *priv = dev_get_drvdata(dev); + + /* Registers file is always readable if the device is present */ + if (attr == &bin_attr_registers) + return attr->attr.mode; + + /* All other attributes not visible if BIOS has not enabled On Demand */ + if (!(priv->features & SDSI_FEATURE_SDSI)) + return 0; + + if (attr == &bin_attr_meter_certificate || attr == &bin_attr_meter_current) + return (priv->features & SDSI_FEATURE_METERING) ? + attr->attr.mode : 0; + + return attr->attr.mode; +} + +static ssize_t guid_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct sdsi_priv *priv = dev_get_drvdata(dev); + + return sysfs_emit(buf, "0x%x\n", priv->guid); +} +static DEVICE_ATTR_RO(guid); + +static struct attribute *sdsi_attrs[] = { + &dev_attr_guid.attr, + NULL +}; + +static const struct attribute_group sdsi_group = { + .attrs = sdsi_attrs, + .bin_attrs = sdsi_bin_attrs, + .is_bin_visible = sdsi_battr_is_visible, +}; +__ATTRIBUTE_GROUPS(sdsi); + +static int sdsi_get_layout(struct sdsi_priv *priv, struct disc_table *table) +{ + switch (table->guid) { + case SDSI_GUID_V1: + priv->control_size = GUID_V1_CNTRL_SIZE; + priv->registers_size = GUID_V1_REGS_SIZE; + break; + case SDSI_GUID_V2: + priv->control_size = GUID_V2_CNTRL_SIZE; + priv->registers_size = GUID_V2_REGS_SIZE; + break; + default: + dev_err(priv->dev, "Unrecognized GUID 0x%x\n", table->guid); + return -EINVAL; + } + return 0; +} + +static int sdsi_map_mbox_registers(struct sdsi_priv *priv, struct device *dev, + struct disc_table *disc_table, struct resource *disc_res) +{ + u32 access_type = FIELD_GET(DT_ACCESS_TYPE, disc_table->access_info); + u32 size = FIELD_GET(DT_SIZE, disc_table->access_info); + u32 tbir = FIELD_GET(DT_TBIR, disc_table->offset); + u32 offset = DT_OFFSET(disc_table->offset); + struct pci_dev *parent = to_pci_dev(dev); + struct resource res = {}; + + /* Starting location of SDSi MMIO region based on access type */ + switch (access_type) { + case ACCESS_TYPE_LOCAL: + if (tbir) { + dev_err(priv->dev, "Unsupported BAR index %u for access type %u\n", + tbir, access_type); + return -EINVAL; + } + + /* + * For access_type LOCAL, the base address is as follows: + * base address = end of discovery region + base offset + 1 + */ + res.start = disc_res->end + offset + 1; + break; + + case ACCESS_TYPE_BARID: + res.start = pci_resource_start(parent, tbir) + offset; + break; + + default: + dev_err(priv->dev, "Unrecognized access_type %u\n", access_type); + return -EINVAL; + } + + res.end = res.start + size * sizeof(u32) - 1; + res.flags = IORESOURCE_MEM; + + priv->control_addr = devm_ioremap_resource(priv->dev, &res); + if (IS_ERR(priv->control_addr)) + return PTR_ERR(priv->control_addr); + + priv->mbox_addr = priv->control_addr + priv->control_size; + priv->regs_addr = priv->mbox_addr + SDSI_SIZE_MAILBOX; + + priv->features = readq(priv->regs_addr + SDSI_ENABLED_FEATURES_OFFSET); + + return 0; +} + +static int sdsi_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct intel_vsec_device *intel_cap_dev = auxdev_to_ivdev(auxdev); + struct disc_table disc_table; + struct resource *disc_res; + void __iomem *disc_addr; + struct sdsi_priv *priv; + int ret; + + priv = devm_kzalloc(&auxdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = &auxdev->dev; + mutex_init(&priv->mb_lock); + auxiliary_set_drvdata(auxdev, priv); + + /* Get the SDSi discovery table */ + disc_res = &intel_cap_dev->resource[0]; + disc_addr = devm_ioremap_resource(&auxdev->dev, disc_res); + if (IS_ERR(disc_addr)) + return PTR_ERR(disc_addr); + + memcpy_fromio(&disc_table, disc_addr, DISC_TABLE_SIZE); + + priv->guid = disc_table.guid; + + /* Get guid based layout info */ + ret = sdsi_get_layout(priv, &disc_table); + if (ret) + return ret; + + /* Map the SDSi mailbox registers */ + ret = sdsi_map_mbox_registers(priv, intel_cap_dev->dev, &disc_table, disc_res); + if (ret) + return ret; + + return 0; +} + +static const struct auxiliary_device_id sdsi_aux_id_table[] = { + { .name = "intel_vsec.sdsi" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, sdsi_aux_id_table); + +static struct auxiliary_driver sdsi_aux_driver = { + .driver = { + .dev_groups = sdsi_groups, + }, + .id_table = sdsi_aux_id_table, + .probe = sdsi_probe, + /* No remove. All resources are handled under devm */ +}; +module_auxiliary_driver(sdsi_aux_driver); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel On Demand (SDSi) driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/smartconnect.c b/drivers/platform/x86/intel/smartconnect.c new file mode 100644 index 000000000..71e91ac60 --- /dev/null +++ b/drivers/platform/x86/intel/smartconnect.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2013 Matthew Garrett + */ + +#include +#include +#include + +MODULE_DESCRIPTION("Intel Smart Connect disabling driver"); +MODULE_LICENSE("GPL"); + +static int smartconnect_acpi_probe(struct platform_device *pdev) +{ + unsigned long long value; + acpi_handle handle; + acpi_status status; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + status = acpi_evaluate_integer(handle, "GAOS", NULL, &value); + if (ACPI_FAILURE(status)) + return -EINVAL; + + if (value & 0x1) { + dev_info(&pdev->dev, "Disabling Intel Smart Connect\n"); + status = acpi_execute_simple_method(handle, "SAOS", 0); + } + + return 0; +} + +static const struct acpi_device_id smartconnect_ids[] = { + {"INT33A0", 0}, + {"", 0} +}; +MODULE_DEVICE_TABLE(acpi, smartconnect_ids); + +static struct platform_driver smartconnect_driver = { + .probe = smartconnect_acpi_probe, + .driver = { + .name = "intel_smart_connect", + .acpi_match_table = smartconnect_ids, + }, +}; + +module_platform_driver(smartconnect_driver); diff --git a/drivers/platform/x86/intel/speed_select_if/Kconfig b/drivers/platform/x86/intel/speed_select_if/Kconfig new file mode 100644 index 000000000..4eb3ad299 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/Kconfig @@ -0,0 +1,21 @@ +menu "Intel Speed Select Technology interface support" + depends on PCI + depends on X86_64 || COMPILE_TEST + +config INTEL_SPEED_SELECT_TPMI + tristate + +config INTEL_SPEED_SELECT_INTERFACE + tristate "Intel(R) Speed Select Technology interface drivers" + select INTEL_SPEED_SELECT_TPMI if INTEL_TPMI + help + This config enables the Intel(R) Speed Select Technology interface + drivers. The Intel(R) speed select technology features are non + architectural and only supported on specific Xeon(R) servers. + These drivers provide interface to directly communicate with hardware + via MMIO and Mail boxes to enumerate and control all the speed select + features. + + Enable this config, if there is a need to enable and control the + Intel(R) Speed Select Technology features from the user space. +endmenu diff --git a/drivers/platform/x86/intel/speed_select_if/Makefile b/drivers/platform/x86/intel/speed_select_if/Makefile new file mode 100644 index 000000000..1d878a36d --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile - Intel Speed Select Interface drivers +# Copyright (c) 2019, Intel Corporation. +# + +obj-$(CONFIG_INTEL_SPEED_SELECT_INTERFACE) += isst_if_common.o +obj-$(CONFIG_INTEL_SPEED_SELECT_INTERFACE) += isst_if_mmio.o +obj-$(CONFIG_INTEL_SPEED_SELECT_INTERFACE) += isst_if_mbox_pci.o +obj-$(CONFIG_INTEL_SPEED_SELECT_INTERFACE) += isst_if_mbox_msr.o +obj-$(CONFIG_INTEL_SPEED_SELECT_TPMI) += isst_tpmi_core.o +obj-$(CONFIG_INTEL_SPEED_SELECT_TPMI) += isst_tpmi.o diff --git a/drivers/platform/x86/intel/speed_select_if/isst_if_common.c b/drivers/platform/x86/intel/speed_select_if/isst_if_common.c new file mode 100644 index 000000000..1c48bf6d5 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_if_common.c @@ -0,0 +1,826 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Speed Select Interface: Common functions + * Copyright (c) 2019, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "isst_if_common.h" + +#define MSR_THREAD_ID_INFO 0x53 +#define MSR_PM_LOGICAL_ID 0x54 +#define MSR_CPU_BUS_NUMBER 0x128 + +static struct isst_if_cmd_cb punit_callbacks[ISST_IF_DEV_MAX]; + +static int punit_msr_white_list[] = { + MSR_TURBO_RATIO_LIMIT, + MSR_CONFIG_TDP_CONTROL, + MSR_TURBO_RATIO_LIMIT1, + MSR_TURBO_RATIO_LIMIT2, + MSR_PM_LOGICAL_ID, +}; + +struct isst_valid_cmd_ranges { + u16 cmd; + u16 sub_cmd_beg; + u16 sub_cmd_end; +}; + +struct isst_cmd_set_req_type { + u16 cmd; + u16 sub_cmd; + u16 param; +}; + +static const struct isst_valid_cmd_ranges isst_valid_cmds[] = { + {0xD0, 0x00, 0x03}, + {0x7F, 0x00, 0x0C}, + {0x7F, 0x10, 0x12}, + {0x7F, 0x20, 0x23}, + {0x94, 0x03, 0x03}, + {0x95, 0x03, 0x03}, +}; + +static const struct isst_cmd_set_req_type isst_cmd_set_reqs[] = { + {0xD0, 0x00, 0x08}, + {0xD0, 0x01, 0x08}, + {0xD0, 0x02, 0x08}, + {0xD0, 0x03, 0x08}, + {0x7F, 0x02, 0x00}, + {0x7F, 0x08, 0x00}, + {0x95, 0x03, 0x03}, +}; + +struct isst_cmd { + struct hlist_node hnode; + u64 data; + u32 cmd; + int cpu; + int mbox_cmd_type; + u32 param; +}; + +static bool isst_hpm_support; + +static DECLARE_HASHTABLE(isst_hash, 8); +static DEFINE_MUTEX(isst_hash_lock); + +static int isst_store_new_cmd(int cmd, u32 cpu, int mbox_cmd_type, u32 param, + u64 data) +{ + struct isst_cmd *sst_cmd; + + sst_cmd = kmalloc_obj(*sst_cmd); + if (!sst_cmd) + return -ENOMEM; + + sst_cmd->cpu = cpu; + sst_cmd->cmd = cmd; + sst_cmd->mbox_cmd_type = mbox_cmd_type; + sst_cmd->param = param; + sst_cmd->data = data; + + hash_add(isst_hash, &sst_cmd->hnode, sst_cmd->cmd); + + return 0; +} + +static void isst_delete_hash(void) +{ + struct isst_cmd *sst_cmd; + struct hlist_node *tmp; + int i; + + hash_for_each_safe(isst_hash, i, tmp, sst_cmd, hnode) { + hash_del(&sst_cmd->hnode); + kfree(sst_cmd); + } +} + +/** + * isst_store_cmd() - Store command to a hash table + * @cmd: Mailbox command. + * @sub_cmd: Mailbox sub-command or MSR id. + * @cpu: Target CPU for the command + * @mbox_cmd_type: Mailbox or MSR command. + * @param: Mailbox parameter. + * @data: Mailbox request data or MSR data. + * + * Stores the command to a hash table if there is no such command already + * stored. If already stored update the latest parameter and data for the + * command. + * + * Return: Return result of store to hash table, 0 for success, others for + * failure. + */ +int isst_store_cmd(int cmd, int sub_cmd, u32 cpu, int mbox_cmd_type, + u32 param, u64 data) +{ + struct isst_cmd *sst_cmd; + int full_cmd, ret; + + full_cmd = (cmd & GENMASK_ULL(15, 0)) << 16; + full_cmd |= (sub_cmd & GENMASK_ULL(15, 0)); + mutex_lock(&isst_hash_lock); + hash_for_each_possible(isst_hash, sst_cmd, hnode, full_cmd) { + if (sst_cmd->cmd == full_cmd && sst_cmd->cpu == cpu && + sst_cmd->mbox_cmd_type == mbox_cmd_type) { + sst_cmd->param = param; + sst_cmd->data = data; + mutex_unlock(&isst_hash_lock); + return 0; + } + } + + ret = isst_store_new_cmd(full_cmd, cpu, mbox_cmd_type, param, data); + mutex_unlock(&isst_hash_lock); + + return ret; +} +EXPORT_SYMBOL_GPL(isst_store_cmd); + +static void isst_mbox_resume_command(struct isst_if_cmd_cb *cb, + struct isst_cmd *sst_cmd) +{ + struct isst_if_mbox_cmd mbox_cmd; + int wr_only; + + mbox_cmd.command = (sst_cmd->cmd & GENMASK_ULL(31, 16)) >> 16; + mbox_cmd.sub_command = sst_cmd->cmd & GENMASK_ULL(15, 0); + mbox_cmd.parameter = sst_cmd->param; + mbox_cmd.req_data = sst_cmd->data; + mbox_cmd.logical_cpu = sst_cmd->cpu; + (cb->cmd_callback)((u8 *)&mbox_cmd, &wr_only, 1); +} + +/** + * isst_resume_common() - Process Resume request + * + * On resume replay all mailbox commands and MSRs. + * + * Return: None. + */ +void isst_resume_common(void) +{ + struct isst_cmd *sst_cmd; + int i; + + hash_for_each(isst_hash, i, sst_cmd, hnode) { + struct isst_if_cmd_cb *cb; + + if (sst_cmd->mbox_cmd_type) { + cb = &punit_callbacks[ISST_IF_DEV_MBOX]; + if (cb->registered) + isst_mbox_resume_command(cb, sst_cmd); + } else { + wrmsrq_safe_on_cpu(sst_cmd->cpu, sst_cmd->cmd, + sst_cmd->data); + } + } +} +EXPORT_SYMBOL_GPL(isst_resume_common); + +/** + * isst_if_mbox_cmd_invalid() - Check invalid mailbox commands + * @cmd: Pointer to the command structure to verify. + * + * Invalid command to PUNIT to may result in instability of the platform. + * This function has a whitelist of commands, which are allowed. + * + * Return: Return true if the command is invalid, else false. + */ +bool isst_if_mbox_cmd_invalid(struct isst_if_mbox_cmd *cmd) +{ + int i; + + if (cmd->logical_cpu >= nr_cpu_ids) + return true; + + for (i = 0; i < ARRAY_SIZE(isst_valid_cmds); ++i) { + if (cmd->command == isst_valid_cmds[i].cmd && + (cmd->sub_command >= isst_valid_cmds[i].sub_cmd_beg && + cmd->sub_command <= isst_valid_cmds[i].sub_cmd_end)) { + return false; + } + } + + return true; +} +EXPORT_SYMBOL_GPL(isst_if_mbox_cmd_invalid); + +/** + * isst_if_mbox_cmd_set_req() - Check mailbox command is a set request + * @cmd: Pointer to the command structure to verify. + * + * Check if the given mail box level is set request and not a get request. + * + * Return: Return true if the command is set_req, else false. + */ +bool isst_if_mbox_cmd_set_req(struct isst_if_mbox_cmd *cmd) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(isst_cmd_set_reqs); ++i) { + if (cmd->command == isst_cmd_set_reqs[i].cmd && + cmd->sub_command == isst_cmd_set_reqs[i].sub_cmd && + cmd->parameter == isst_cmd_set_reqs[i].param) { + return true; + } + } + + return false; +} +EXPORT_SYMBOL_GPL(isst_if_mbox_cmd_set_req); + +static int isst_if_api_version; + +static int isst_if_get_platform_info(void __user *argp) +{ + struct isst_if_platform_info info; + + info.api_version = isst_if_api_version; + info.driver_version = ISST_IF_DRIVER_VERSION; + info.max_cmds_per_ioctl = ISST_IF_CMD_LIMIT; + info.mbox_supported = punit_callbacks[ISST_IF_DEV_MBOX].registered; + info.mmio_supported = punit_callbacks[ISST_IF_DEV_MMIO].registered; + + if (copy_to_user(argp, &info, sizeof(info))) + return -EFAULT; + + return 0; +} + +#define ISST_MAX_BUS_NUMBER 2 + +struct isst_if_cpu_info { + /* For BUS 0 and BUS 1 only, which we need for PUNIT interface */ + int bus_info[ISST_MAX_BUS_NUMBER]; + struct pci_dev *pci_dev[ISST_MAX_BUS_NUMBER]; + int punit_cpu_id; + int numa_node; +}; + +struct isst_if_pkg_info { + struct pci_dev *pci_dev[ISST_MAX_BUS_NUMBER]; +}; + +static struct isst_if_cpu_info *isst_cpu_info; +static struct isst_if_pkg_info *isst_pkg_info; + +static struct pci_dev *_isst_if_get_pci_dev(int cpu, int bus_no, int dev, int fn) +{ + struct pci_dev *matched_pci_dev = NULL; + struct pci_dev *pci_dev = NULL; + struct pci_dev *_pci_dev = NULL; + int no_matches = 0, pkg_id; + int bus_number; + + if (bus_no < 0 || bus_no >= ISST_MAX_BUS_NUMBER || cpu < 0 || + cpu >= nr_cpu_ids || cpu >= num_possible_cpus()) + return NULL; + + pkg_id = topology_logical_package_id(cpu); + if (pkg_id >= topology_max_packages()) + return NULL; + + bus_number = isst_cpu_info[cpu].bus_info[bus_no]; + if (bus_number < 0) + return NULL; + + for_each_pci_dev(_pci_dev) { + int node; + + if (_pci_dev->bus->number != bus_number || + _pci_dev->devfn != PCI_DEVFN(dev, fn)) + continue; + + ++no_matches; + if (!matched_pci_dev) + matched_pci_dev = _pci_dev; + + node = dev_to_node(&_pci_dev->dev); + if (node == NUMA_NO_NODE) { + pr_info_once("Fail to get numa node for CPU:%d bus:%d dev:%d fn:%d\n", + cpu, bus_no, dev, fn); + continue; + } + + if (node == isst_cpu_info[cpu].numa_node) { + isst_pkg_info[pkg_id].pci_dev[bus_no] = _pci_dev; + + pci_dev = _pci_dev; + break; + } + } + + /* + * If there is no numa matched pci_dev, then there can be following cases: + * 1. CONFIG_NUMA is not defined: In this case if there is only single device + * match, then we don't need numa information. Simply return last match. + * Othewise return NULL. + * 2. NUMA information is not exposed via _SEG method. In this case it is similar + * to case 1. + * 3. Numa information doesn't match with CPU numa node and more than one match + * return NULL. + */ + if (!pci_dev && no_matches == 1) + pci_dev = matched_pci_dev; + + /* Return pci_dev pointer for any matched CPU in the package */ + if (!pci_dev) + pci_dev = isst_pkg_info[pkg_id].pci_dev[bus_no]; + + return pci_dev; +} + +/** + * isst_if_get_pci_dev() - Get the PCI device instance for a CPU + * @cpu: Logical CPU number. + * @bus_no: The bus number assigned by the hardware. + * @dev: The device number assigned by the hardware. + * @fn: The function number assigned by the hardware. + * + * Using cached bus information, find out the PCI device for a bus number, + * device and function. + * + * Return: Return pci_dev pointer or NULL. + */ +struct pci_dev *isst_if_get_pci_dev(int cpu, int bus_no, int dev, int fn) +{ + struct pci_dev *pci_dev; + + if (bus_no < 0 || bus_no >= ISST_MAX_BUS_NUMBER || cpu < 0 || + cpu >= nr_cpu_ids || cpu >= num_possible_cpus()) + return NULL; + + pci_dev = isst_cpu_info[cpu].pci_dev[bus_no]; + + if (pci_dev && pci_dev->devfn == PCI_DEVFN(dev, fn)) + return pci_dev; + + return _isst_if_get_pci_dev(cpu, bus_no, dev, fn); +} +EXPORT_SYMBOL_GPL(isst_if_get_pci_dev); + +static int isst_if_cpu_online(unsigned int cpu) +{ + u64 data; + int ret; + + isst_cpu_info[cpu].numa_node = cpu_to_node(cpu); + + ret = rdmsrq_safe(MSR_CPU_BUS_NUMBER, &data); + if (ret) { + /* This is not a fatal error on MSR mailbox only I/F */ + isst_cpu_info[cpu].bus_info[0] = -1; + isst_cpu_info[cpu].bus_info[1] = -1; + } else { + isst_cpu_info[cpu].bus_info[0] = data & 0xff; + isst_cpu_info[cpu].bus_info[1] = (data >> 8) & 0xff; + isst_cpu_info[cpu].pci_dev[0] = _isst_if_get_pci_dev(cpu, 0, 0, 1); + isst_cpu_info[cpu].pci_dev[1] = _isst_if_get_pci_dev(cpu, 1, 30, 1); + } + + if (isst_hpm_support) { + + ret = rdmsrq_safe(MSR_PM_LOGICAL_ID, &data); + if (!ret) + goto set_punit_id; + } + + ret = rdmsrq_safe(MSR_THREAD_ID_INFO, &data); + if (ret) { + isst_cpu_info[cpu].punit_cpu_id = -1; + return ret; + } + +set_punit_id: + isst_cpu_info[cpu].punit_cpu_id = data; + + return 0; +} + +static int isst_if_online_id; + +static int isst_if_cpu_info_init(void) +{ + int ret; + + isst_cpu_info = kzalloc_objs(*isst_cpu_info, num_possible_cpus()); + if (!isst_cpu_info) + return -ENOMEM; + + isst_pkg_info = kzalloc_objs(*isst_pkg_info, topology_max_packages()); + if (!isst_pkg_info) { + kfree(isst_cpu_info); + return -ENOMEM; + } + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, + "platform/x86/isst-if:online", + isst_if_cpu_online, NULL); + if (ret < 0) { + kfree(isst_pkg_info); + kfree(isst_cpu_info); + return ret; + } + + isst_if_online_id = ret; + + return 0; +} + +static void isst_if_cpu_info_exit(void) +{ + cpuhp_remove_state(isst_if_online_id); + kfree(isst_pkg_info); + kfree(isst_cpu_info); +}; + +static long isst_if_proc_phyid_req(u8 *cmd_ptr, int *write_only, int resume) +{ + struct isst_if_cpu_map *cpu_map; + + cpu_map = (struct isst_if_cpu_map *)cmd_ptr; + if (cpu_map->logical_cpu >= nr_cpu_ids || + cpu_map->logical_cpu >= num_possible_cpus()) + return -EINVAL; + + *write_only = 0; + cpu_map->physical_cpu = isst_cpu_info[cpu_map->logical_cpu].punit_cpu_id; + + return 0; +} + +static bool match_punit_msr_white_list(int msr) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(punit_msr_white_list); ++i) { + if (punit_msr_white_list[i] == msr) + return true; + } + + return false; +} + +static long isst_if_msr_cmd_req(u8 *cmd_ptr, int *write_only, int resume) +{ + struct isst_if_msr_cmd *msr_cmd; + int ret; + + msr_cmd = (struct isst_if_msr_cmd *)cmd_ptr; + + if (!match_punit_msr_white_list(msr_cmd->msr)) + return -EINVAL; + + if (msr_cmd->logical_cpu >= nr_cpu_ids) + return -EINVAL; + + if (msr_cmd->read_write) { + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + ret = wrmsrq_safe_on_cpu(msr_cmd->logical_cpu, + msr_cmd->msr, + msr_cmd->data); + *write_only = 1; + if (!ret && !resume) + ret = isst_store_cmd(0, msr_cmd->msr, + msr_cmd->logical_cpu, + 0, 0, msr_cmd->data); + } else { + u64 data; + + ret = rdmsrq_safe_on_cpu(msr_cmd->logical_cpu, + msr_cmd->msr, &data); + if (!ret) { + msr_cmd->data = data; + *write_only = 0; + } + } + + + return ret; +} + +static long isst_if_exec_multi_cmd(void __user *argp, struct isst_if_cmd_cb *cb) +{ + unsigned char __user *ptr; + u32 cmd_count; + u8 *cmd_ptr; + long ret; + int i; + + /* Each multi command has u32 command count as the first field */ + if (copy_from_user(&cmd_count, argp, sizeof(cmd_count))) + return -EFAULT; + + if (!cmd_count || cmd_count > ISST_IF_CMD_LIMIT) + return -EINVAL; + + cmd_ptr = kmalloc(cb->cmd_size, GFP_KERNEL); + if (!cmd_ptr) + return -ENOMEM; + + /* cb->offset points to start of the command after the command count */ + ptr = argp + cb->offset; + + for (i = 0; i < cmd_count; ++i) { + int wr_only; + + if (signal_pending(current)) { + ret = -EINTR; + break; + } + + if (copy_from_user(cmd_ptr, ptr, cb->cmd_size)) { + ret = -EFAULT; + break; + } + + ret = cb->cmd_callback(cmd_ptr, &wr_only, 0); + if (ret) + break; + + if (!wr_only && copy_to_user(ptr, cmd_ptr, cb->cmd_size)) { + ret = -EFAULT; + break; + } + + ptr += cb->cmd_size; + } + + kfree(cmd_ptr); + + return i ? i : ret; +} + +static long isst_if_def_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + void __user *argp = (void __user *)arg; + struct isst_if_cmd_cb cmd_cb; + struct isst_if_cmd_cb *cb; + long ret = -ENOTTY; + int i; + + switch (cmd) { + case ISST_IF_GET_PLATFORM_INFO: + ret = isst_if_get_platform_info(argp); + break; + case ISST_IF_GET_PHY_ID: + cmd_cb.cmd_size = sizeof(struct isst_if_cpu_map); + cmd_cb.offset = offsetof(struct isst_if_cpu_maps, cpu_map); + cmd_cb.cmd_callback = isst_if_proc_phyid_req; + ret = isst_if_exec_multi_cmd(argp, &cmd_cb); + break; + case ISST_IF_IO_CMD: + cb = &punit_callbacks[ISST_IF_DEV_MMIO]; + if (cb->registered) + ret = isst_if_exec_multi_cmd(argp, cb); + break; + case ISST_IF_MBOX_COMMAND: + cb = &punit_callbacks[ISST_IF_DEV_MBOX]; + if (cb->registered) + ret = isst_if_exec_multi_cmd(argp, cb); + break; + case ISST_IF_MSR_COMMAND: + cmd_cb.cmd_size = sizeof(struct isst_if_msr_cmd); + cmd_cb.offset = offsetof(struct isst_if_msr_cmds, msr_cmd); + cmd_cb.cmd_callback = isst_if_msr_cmd_req; + ret = isst_if_exec_multi_cmd(argp, &cmd_cb); + break; + default: + for (i = 0; i < ISST_IF_DEV_MAX; ++i) { + struct isst_if_cmd_cb *cb = &punit_callbacks[i]; + int ret; + + if (cb->def_ioctl) { + ret = cb->def_ioctl(file, cmd, arg); + if (!ret) + return ret; + } + } + break; + } + + return ret; +} + +/* Lock to prevent module registration when already opened by user space */ +static DEFINE_MUTEX(punit_misc_dev_open_lock); +static int misc_device_open; + +static int isst_if_open(struct inode *inode, struct file *file) +{ + int i, ret = 0; + + /* Fail open, if a module is going away */ + mutex_lock(&punit_misc_dev_open_lock); + for (i = 0; i < ISST_IF_DEV_MAX; ++i) { + struct isst_if_cmd_cb *cb = &punit_callbacks[i]; + + if (cb->registered && !try_module_get(cb->owner)) { + ret = -ENODEV; + break; + } + } + if (ret) { + int j; + + for (j = 0; j < i; ++j) { + struct isst_if_cmd_cb *cb; + + cb = &punit_callbacks[j]; + if (cb->registered) + module_put(cb->owner); + } + } else { + misc_device_open++; + } + mutex_unlock(&punit_misc_dev_open_lock); + + return ret; +} + +static int isst_if_relase(struct inode *inode, struct file *f) +{ + int i; + + mutex_lock(&punit_misc_dev_open_lock); + misc_device_open--; + for (i = 0; i < ISST_IF_DEV_MAX; ++i) { + struct isst_if_cmd_cb *cb = &punit_callbacks[i]; + + if (cb->registered) + module_put(cb->owner); + } + mutex_unlock(&punit_misc_dev_open_lock); + + return 0; +} + +static const struct file_operations isst_if_char_driver_ops = { + .open = isst_if_open, + .unlocked_ioctl = isst_if_def_ioctl, + .release = isst_if_relase, +}; + +static struct miscdevice isst_if_char_driver = { + .minor = MISC_DYNAMIC_MINOR, + .name = "isst_interface", + .fops = &isst_if_char_driver_ops, +}; + +static int isst_misc_reg(void) +{ + int ret; + + ret = isst_if_cpu_info_init(); + if (ret) + return ret; + + ret = misc_register(&isst_if_char_driver); + if (ret) + isst_if_cpu_info_exit(); + + return ret; +} + +static void isst_misc_unreg(void) +{ + misc_deregister(&isst_if_char_driver); + isst_if_cpu_info_exit(); +} + +/** + * isst_if_cdev_register() - Register callback for IOCTL + * @device_type: The device type this callback handling. + * @cb: Callback structure. + * + * This function registers a callback to device type. On very first call + * it will register a misc device, which is used for user kernel interface. + * Other calls simply increment ref count. Registry will fail, if the user + * already opened misc device for operation. Also if the misc device + * creation failed, then it will not try again and all callers will get + * failure code. + * + * Return: Return the return value from the misc creation device or -EINVAL + * for unsupported device type. + */ +int isst_if_cdev_register(int device_type, struct isst_if_cmd_cb *cb) +{ + if (device_type >= ISST_IF_DEV_MAX) + return -EINVAL; + + if (device_type < ISST_IF_DEV_TPMI && isst_hpm_support) + return -ENODEV; + + mutex_lock(&punit_misc_dev_open_lock); + /* Device is already open, we don't want to add new callbacks */ + if (misc_device_open) { + mutex_unlock(&punit_misc_dev_open_lock); + return -EAGAIN; + } + if (!cb->api_version) + cb->api_version = ISST_IF_API_VERSION; + if (cb->api_version > isst_if_api_version) + isst_if_api_version = cb->api_version; + memcpy(&punit_callbacks[device_type], cb, sizeof(*cb)); + punit_callbacks[device_type].registered = 1; + mutex_unlock(&punit_misc_dev_open_lock); + + return 0; +} +EXPORT_SYMBOL_GPL(isst_if_cdev_register); + +/** + * isst_if_cdev_unregister() - Unregister callback for IOCTL + * @device_type: The device type to unregister. + * + * This function unregisters the previously registered callback. If this + * is the last callback unregistering, then misc device is removed. + * + * Return: None. + */ +void isst_if_cdev_unregister(int device_type) +{ + mutex_lock(&punit_misc_dev_open_lock); + punit_callbacks[device_type].def_ioctl = NULL; + punit_callbacks[device_type].registered = 0; + if (device_type == ISST_IF_DEV_MBOX) + isst_delete_hash(); + mutex_unlock(&punit_misc_dev_open_lock); +} +EXPORT_SYMBOL_GPL(isst_if_cdev_unregister); + +#define SST_HPM_SUPPORTED 0x01 +#define SST_MBOX_SUPPORTED 0x02 + +static const struct x86_cpu_id isst_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_CRESTMONT, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_ATOM_CRESTMONT_X, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_ATOM_DARKMONT_X, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_EMERALDRAPIDS_X, 0), + X86_MATCH_VFM(INTEL_GRANITERAPIDS_D, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_GRANITERAPIDS_X, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_ICELAKE_D, 0), + X86_MATCH_VFM(INTEL_ICELAKE_X, 0), + X86_MATCH_VFM(INTEL_DIAMONDRAPIDS_X, SST_HPM_SUPPORTED), + X86_MATCH_VFM(INTEL_SAPPHIRERAPIDS_X, 0), + X86_MATCH_VFM(INTEL_SKYLAKE_X, SST_MBOX_SUPPORTED), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, isst_cpu_ids); + +static int __init isst_if_common_init(void) +{ + const struct x86_cpu_id *id; + + id = x86_match_cpu(isst_cpu_ids); + if (!id) + return -ENODEV; + + if (id->driver_data == SST_HPM_SUPPORTED) { + isst_hpm_support = true; + } else if (id->driver_data == SST_MBOX_SUPPORTED) { + u64 data; + + /* Can fail only on some Skylake-X generations */ + if (rdmsrq_safe(MSR_OS_MAILBOX_INTERFACE, &data) || + rdmsrq_safe(MSR_OS_MAILBOX_DATA, &data)) + return -ENODEV; + } + + return isst_misc_reg(); +} +module_init(isst_if_common_init) + +static void __exit isst_if_common_exit(void) +{ + isst_misc_unreg(); +} +module_exit(isst_if_common_exit) + +MODULE_DESCRIPTION("ISST common interface module"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_if_common.h b/drivers/platform/x86/intel/speed_select_if/isst_if_common.h new file mode 100644 index 000000000..378055fe1 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_if_common.h @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Intel Speed Select Interface: Drivers Internal defines + * Copyright (c) 2019, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ + +#ifndef __ISST_IF_COMMON_H +#define __ISST_IF_COMMON_H + +#define PCI_DEVICE_ID_INTEL_RAPL_PRIO_DEVID_0 0x3451 +#define PCI_DEVICE_ID_INTEL_CFG_MBOX_DEVID_0 0x3459 + +#define PCI_DEVICE_ID_INTEL_RAPL_PRIO_DEVID_1 0x3251 +#define PCI_DEVICE_ID_INTEL_CFG_MBOX_DEVID_1 0x3259 + +#define MSR_OS_MAILBOX_INTERFACE 0xB0 +#define MSR_OS_MAILBOX_DATA 0xB1 + +/* + * Validate maximum commands in a single request. + * This is enough to handle command to every core in one ioctl, or all + * possible message id to one CPU. Limit is also helpful for resonse time + * per IOCTL request, as PUNIT may take different times to process each + * request and may hold for long for too many commands. + */ +#define ISST_IF_CMD_LIMIT 64 + +#define ISST_IF_API_VERSION 0x01 +#define ISST_IF_DRIVER_VERSION 0x01 + +#define ISST_IF_DEV_MBOX 0 +#define ISST_IF_DEV_MMIO 1 +#define ISST_IF_DEV_TPMI 2 +#define ISST_IF_DEV_MAX 3 + +/** + * struct isst_if_cmd_cb - Used to register a IOCTL handler + * @registered: Used by the common code to store registry. Caller don't + * to touch this field + * @cmd_size: The command size of the individual command in IOCTL + * @offset: Offset to the first valid member in command structure. + * This will be the offset of the start of the command + * after command count field + * @api_version: API version supported for this target. 0, if none. + * @owner: Registered module owner + * @cmd_callback: Callback function to handle IOCTL. The callback has the + * command pointer with data for command. There is a pointer + * called write_only, which when set, will not copy the + * response to user ioctl buffer. The "resume" argument + * can be used to avoid storing the command for replay + * during system resume + * @def_ioctl: Default IOCTL handler callback, if there is no match in + * the existing list of IOCTL handled by the common handler. + * + * This structure is used to register an handler for IOCTL. To avoid + * code duplication common code handles all the IOCTL command read/write + * including handling multiple command in single IOCTL. The caller just + * need to execute a command via the registered callback. + */ +struct isst_if_cmd_cb { + int registered; + int cmd_size; + int offset; + int api_version; + struct module *owner; + long (*cmd_callback)(u8 *ptr, int *write_only, int resume); + long (*def_ioctl)(struct file *file, unsigned int cmd, unsigned long arg); +}; + +/* Internal interface functions */ +int isst_if_cdev_register(int type, struct isst_if_cmd_cb *cb); +void isst_if_cdev_unregister(int type); +struct pci_dev *isst_if_get_pci_dev(int cpu, int bus, int dev, int fn); +bool isst_if_mbox_cmd_set_req(struct isst_if_mbox_cmd *mbox_cmd); +bool isst_if_mbox_cmd_invalid(struct isst_if_mbox_cmd *cmd); +int isst_store_cmd(int cmd, int sub_command, u32 cpu, int mbox_cmd, + u32 param, u64 data); +void isst_resume_common(void); +#endif diff --git a/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_msr.c b/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_msr.c new file mode 100644 index 000000000..22745b217 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_msr.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Speed Select Interface: Mbox via MSR Interface + * Copyright (c) 2019, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "isst_if_common.h" + +#define MSR_OS_MAILBOX_BUSY_BIT 31 + +/* + * Based on experiments count is never more than 1, as the MSR overhead + * is enough to finish the command. So here this is the worst case number. + */ +#define OS_MAILBOX_RETRY_COUNT 3 + +static int isst_if_send_mbox_cmd(u8 command, u8 sub_command, u32 parameter, + u32 command_data, u32 *response_data) +{ + u32 retries; + u64 data; + int ret; + + /* Poll for rb bit == 0 */ + retries = OS_MAILBOX_RETRY_COUNT; + do { + rdmsrq(MSR_OS_MAILBOX_INTERFACE, data); + if (data & BIT_ULL(MSR_OS_MAILBOX_BUSY_BIT)) { + ret = -EBUSY; + continue; + } + ret = 0; + break; + } while (--retries); + + if (ret) + return ret; + + /* Write DATA register */ + wrmsrq(MSR_OS_MAILBOX_DATA, command_data); + + /* Write command register */ + data = BIT_ULL(MSR_OS_MAILBOX_BUSY_BIT) | + (parameter & GENMASK_ULL(13, 0)) << 16 | + (sub_command << 8) | + command; + wrmsrq(MSR_OS_MAILBOX_INTERFACE, data); + + /* Poll for rb bit == 0 */ + retries = OS_MAILBOX_RETRY_COUNT; + do { + rdmsrq(MSR_OS_MAILBOX_INTERFACE, data); + if (data & BIT_ULL(MSR_OS_MAILBOX_BUSY_BIT)) { + ret = -EBUSY; + continue; + } + + if (data & 0xff) + return -ENXIO; + + if (response_data) { + rdmsrq(MSR_OS_MAILBOX_DATA, data); + *response_data = data; + } + ret = 0; + break; + } while (--retries); + + return ret; +} + +struct msrl_action { + int err; + struct isst_if_mbox_cmd *mbox_cmd; +}; + +/* revisit, smp_call_function_single should be enough for atomic mailbox! */ +static void msrl_update_func(void *info) +{ + struct msrl_action *act = info; + + act->err = isst_if_send_mbox_cmd(act->mbox_cmd->command, + act->mbox_cmd->sub_command, + act->mbox_cmd->parameter, + act->mbox_cmd->req_data, + &act->mbox_cmd->resp_data); +} + +static long isst_if_mbox_proc_cmd(u8 *cmd_ptr, int *write_only, int resume) +{ + struct msrl_action action; + int ret; + + action.mbox_cmd = (struct isst_if_mbox_cmd *)cmd_ptr; + + if (isst_if_mbox_cmd_invalid(action.mbox_cmd)) + return -EINVAL; + + if (isst_if_mbox_cmd_set_req(action.mbox_cmd) && + !capable(CAP_SYS_ADMIN)) + return -EPERM; + + /* + * To complete mailbox command, we need to access two MSRs. + * So we don't want race to complete a mailbox transcation. + * Here smp_call ensures that msrl_update_func() has no race + * and also with wait flag, wait for completion. + * smp_call_function_single is using get_cpu() and put_cpu(). + */ + ret = smp_call_function_single(action.mbox_cmd->logical_cpu, + msrl_update_func, &action, 1); + if (ret) + return ret; + + if (!action.err && !resume && isst_if_mbox_cmd_set_req(action.mbox_cmd)) + action.err = isst_store_cmd(action.mbox_cmd->command, + action.mbox_cmd->sub_command, + action.mbox_cmd->logical_cpu, 1, + action.mbox_cmd->parameter, + action.mbox_cmd->req_data); + *write_only = 0; + + return action.err; +} + + +static int isst_pm_notify(struct notifier_block *nb, + unsigned long mode, void *_unused) +{ + switch (mode) { + case PM_POST_HIBERNATION: + case PM_POST_RESTORE: + case PM_POST_SUSPEND: + isst_resume_common(); + break; + default: + break; + } + return 0; +} + +static struct notifier_block isst_pm_nb = { + .notifier_call = isst_pm_notify, +}; + +static const struct x86_cpu_id isst_if_cpu_ids[] = { + X86_MATCH_VFM(INTEL_SKYLAKE_X, NULL), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, isst_if_cpu_ids); + +static int __init isst_if_mbox_init(void) +{ + struct isst_if_cmd_cb cb; + const struct x86_cpu_id *id; + u64 data; + int ret; + + id = x86_match_cpu(isst_if_cpu_ids); + if (!id) + return -ENODEV; + + /* Check presence of mailbox MSRs */ + ret = rdmsrq_safe(MSR_OS_MAILBOX_INTERFACE, &data); + if (ret) + return ret; + + ret = rdmsrq_safe(MSR_OS_MAILBOX_DATA, &data); + if (ret) + return ret; + + memset(&cb, 0, sizeof(cb)); + cb.cmd_size = sizeof(struct isst_if_mbox_cmd); + cb.offset = offsetof(struct isst_if_mbox_cmds, mbox_cmd); + cb.cmd_callback = isst_if_mbox_proc_cmd; + cb.owner = THIS_MODULE; + ret = isst_if_cdev_register(ISST_IF_DEV_MBOX, &cb); + if (ret) + return ret; + + ret = register_pm_notifier(&isst_pm_nb); + if (ret) + isst_if_cdev_unregister(ISST_IF_DEV_MBOX); + + return ret; +} +module_init(isst_if_mbox_init) + +static void __exit isst_if_mbox_exit(void) +{ + unregister_pm_notifier(&isst_pm_nb); + isst_if_cdev_unregister(ISST_IF_DEV_MBOX); +} +module_exit(isst_if_mbox_exit) + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel speed select interface mailbox driver"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_pci.c b/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_pci.c new file mode 100644 index 000000000..df1fc6c71 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_if_mbox_pci.c @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Speed Select Interface: Mbox via PCI Interface + * Copyright (c) 2019, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ + +#include +#include +#include +#include +#include +#include + +#include "isst_if_common.h" + +#define PUNIT_MAILBOX_DATA 0xA0 +#define PUNIT_MAILBOX_INTERFACE 0xA4 +#define PUNIT_MAILBOX_BUSY_BIT 31 + +/* + * The average time to complete mailbox commands is less than 40us. Most of + * the commands complete in few micro seconds. But the same firmware handles + * requests from all power management features. + * We can create a scenario where we flood the firmware with requests then + * the mailbox response can be delayed for 100s of micro seconds. So define + * two timeouts. One for average case and one for long. + * If the firmware is taking more than average, just call cond_resched(). + */ +#define OS_MAILBOX_TIMEOUT_AVG_US 40 +#define OS_MAILBOX_TIMEOUT_MAX_US 1000 + +struct isst_if_device { + struct mutex mutex; +}; + +static int isst_if_mbox_cmd(struct pci_dev *pdev, + struct isst_if_mbox_cmd *mbox_cmd) +{ + s64 tm_delta = 0; + ktime_t tm; + u32 data; + int ret; + + /* Poll for rb bit == 0 */ + tm = ktime_get(); + do { + ret = pci_read_config_dword(pdev, PUNIT_MAILBOX_INTERFACE, + &data); + if (ret) + return ret; + + if (data & BIT_ULL(PUNIT_MAILBOX_BUSY_BIT)) { + ret = -EBUSY; + tm_delta = ktime_us_delta(ktime_get(), tm); + if (tm_delta > OS_MAILBOX_TIMEOUT_AVG_US) + cond_resched(); + continue; + } + ret = 0; + break; + } while (tm_delta < OS_MAILBOX_TIMEOUT_MAX_US); + + if (ret) + return ret; + + /* Write DATA register */ + ret = pci_write_config_dword(pdev, PUNIT_MAILBOX_DATA, + mbox_cmd->req_data); + if (ret) + return ret; + + /* Write command register */ + data = BIT_ULL(PUNIT_MAILBOX_BUSY_BIT) | + (mbox_cmd->parameter & GENMASK_ULL(13, 0)) << 16 | + (mbox_cmd->sub_command << 8) | + mbox_cmd->command; + + ret = pci_write_config_dword(pdev, PUNIT_MAILBOX_INTERFACE, data); + if (ret) + return ret; + + /* Poll for rb bit == 0 */ + tm_delta = 0; + tm = ktime_get(); + do { + ret = pci_read_config_dword(pdev, PUNIT_MAILBOX_INTERFACE, + &data); + if (ret) + return ret; + + if (data & BIT_ULL(PUNIT_MAILBOX_BUSY_BIT)) { + ret = -EBUSY; + tm_delta = ktime_us_delta(ktime_get(), tm); + if (tm_delta > OS_MAILBOX_TIMEOUT_AVG_US) + cond_resched(); + continue; + } + + if (data & 0xff) + return -ENXIO; + + ret = pci_read_config_dword(pdev, PUNIT_MAILBOX_DATA, &data); + if (ret) + return ret; + + mbox_cmd->resp_data = data; + ret = 0; + break; + } while (tm_delta < OS_MAILBOX_TIMEOUT_MAX_US); + + return ret; +} + +static long isst_if_mbox_proc_cmd(u8 *cmd_ptr, int *write_only, int resume) +{ + struct isst_if_mbox_cmd *mbox_cmd; + struct isst_if_device *punit_dev; + struct pci_dev *pdev; + int ret; + + mbox_cmd = (struct isst_if_mbox_cmd *)cmd_ptr; + + if (isst_if_mbox_cmd_invalid(mbox_cmd)) + return -EINVAL; + + if (isst_if_mbox_cmd_set_req(mbox_cmd) && !capable(CAP_SYS_ADMIN)) + return -EPERM; + + pdev = isst_if_get_pci_dev(mbox_cmd->logical_cpu, 1, 30, 1); + if (!pdev) + return -EINVAL; + + punit_dev = pci_get_drvdata(pdev); + if (!punit_dev) + return -EINVAL; + + /* + * Basically we are allowing one complete mailbox transaction on + * a mapped PCI device at a time. + */ + mutex_lock(&punit_dev->mutex); + ret = isst_if_mbox_cmd(pdev, mbox_cmd); + if (!ret && !resume && isst_if_mbox_cmd_set_req(mbox_cmd)) + ret = isst_store_cmd(mbox_cmd->command, + mbox_cmd->sub_command, + mbox_cmd->logical_cpu, 1, + mbox_cmd->parameter, + mbox_cmd->req_data); + mutex_unlock(&punit_dev->mutex); + if (ret) + return ret; + + *write_only = 0; + + return 0; +} + +static const struct pci_device_id isst_if_mbox_ids[] = { + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_CFG_MBOX_DEVID_0)}, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_CFG_MBOX_DEVID_1)}, + { 0 }, +}; +MODULE_DEVICE_TABLE(pci, isst_if_mbox_ids); + +static int isst_if_mbox_probe(struct pci_dev *pdev, + const struct pci_device_id *ent) +{ + struct isst_if_device *punit_dev; + struct isst_if_cmd_cb cb; + int ret; + + punit_dev = devm_kzalloc(&pdev->dev, sizeof(*punit_dev), GFP_KERNEL); + if (!punit_dev) + return -ENOMEM; + + ret = pcim_enable_device(pdev); + if (ret) + return ret; + + mutex_init(&punit_dev->mutex); + pci_set_drvdata(pdev, punit_dev); + + memset(&cb, 0, sizeof(cb)); + cb.cmd_size = sizeof(struct isst_if_mbox_cmd); + cb.offset = offsetof(struct isst_if_mbox_cmds, mbox_cmd); + cb.cmd_callback = isst_if_mbox_proc_cmd; + cb.owner = THIS_MODULE; + ret = isst_if_cdev_register(ISST_IF_DEV_MBOX, &cb); + + if (ret) + mutex_destroy(&punit_dev->mutex); + + return ret; +} + +static void isst_if_mbox_remove(struct pci_dev *pdev) +{ + struct isst_if_device *punit_dev; + + punit_dev = pci_get_drvdata(pdev); + isst_if_cdev_unregister(ISST_IF_DEV_MBOX); + mutex_destroy(&punit_dev->mutex); +} + +static int __maybe_unused isst_if_resume(struct device *device) +{ + isst_resume_common(); + return 0; +} + +static SIMPLE_DEV_PM_OPS(isst_if_pm_ops, NULL, isst_if_resume); + +static struct pci_driver isst_if_pci_driver = { + .name = "isst_if_mbox_pci", + .id_table = isst_if_mbox_ids, + .probe = isst_if_mbox_probe, + .remove = isst_if_mbox_remove, + .driver.pm = &isst_if_pm_ops, +}; + +module_pci_driver(isst_if_pci_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel speed select interface pci mailbox driver"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_if_mmio.c b/drivers/platform/x86/intel/speed_select_if/isst_if_mmio.c new file mode 100644 index 000000000..950ede5ea --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_if_mmio.c @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Speed Select Interface: MMIO Interface + * Copyright (c) 2019, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ + +#include +#include +#include +#include +#include + +#include "isst_if_common.h" + +struct isst_mmio_range { + int beg; + int end; + int size; +}; + +static struct isst_mmio_range mmio_range_devid_0[] = { + {0x04, 0x14, 0x18}, + {0x20, 0xD0, 0xD4}, +}; + +static struct isst_mmio_range mmio_range_devid_1[] = { + {0x04, 0x14, 0x18}, + {0x20, 0x11C, 0x120}, +}; + +struct isst_if_device { + void __iomem *punit_mmio; + u32 range_0[5]; + u32 range_1[64]; + struct isst_mmio_range *mmio_range; + struct mutex mutex; +}; + +static long isst_if_mmio_rd_wr(u8 *cmd_ptr, int *write_only, int resume) +{ + struct isst_if_device *punit_dev; + struct isst_if_io_reg *io_reg; + struct pci_dev *pdev; + + io_reg = (struct isst_if_io_reg *)cmd_ptr; + + if (io_reg->reg % 4) + return -EINVAL; + + if (io_reg->read_write && !capable(CAP_SYS_ADMIN)) + return -EPERM; + + pdev = isst_if_get_pci_dev(io_reg->logical_cpu, 0, 0, 1); + if (!pdev) + return -EINVAL; + + punit_dev = pci_get_drvdata(pdev); + if (!punit_dev) + return -EINVAL; + + if (io_reg->reg < punit_dev->mmio_range[0].beg || + io_reg->reg > punit_dev->mmio_range[1].end) + return -EINVAL; + + /* + * Ensure that operation is complete on a PCI device to avoid read + * write race by using per PCI device mutex. + */ + mutex_lock(&punit_dev->mutex); + if (io_reg->read_write) { + writel(io_reg->value, punit_dev->punit_mmio+io_reg->reg); + *write_only = 1; + } else { + io_reg->value = readl(punit_dev->punit_mmio+io_reg->reg); + *write_only = 0; + } + mutex_unlock(&punit_dev->mutex); + + return 0; +} + +static const struct pci_device_id isst_if_ids[] = { + { PCI_DEVICE_DATA(INTEL, RAPL_PRIO_DEVID_0, &mmio_range_devid_0)}, + { PCI_DEVICE_DATA(INTEL, RAPL_PRIO_DEVID_1, &mmio_range_devid_1)}, + { 0 }, +}; +MODULE_DEVICE_TABLE(pci, isst_if_ids); + +static int isst_if_probe(struct pci_dev *pdev, const struct pci_device_id *ent) +{ + struct isst_if_device *punit_dev; + struct isst_if_cmd_cb cb; + u32 mmio_base, pcu_base; + struct resource r; + u64 base_addr; + int ret; + + punit_dev = devm_kzalloc(&pdev->dev, sizeof(*punit_dev), GFP_KERNEL); + if (!punit_dev) + return -ENOMEM; + + ret = pcim_enable_device(pdev); + if (ret) + return ret; + + ret = pci_read_config_dword(pdev, 0xD0, &mmio_base); + if (ret) + return pcibios_err_to_errno(ret); + + ret = pci_read_config_dword(pdev, 0xFC, &pcu_base); + if (ret) + return pcibios_err_to_errno(ret); + + pcu_base &= GENMASK(10, 0); + base_addr = (u64)mmio_base << 23 | (u64) pcu_base << 12; + + punit_dev->mmio_range = (struct isst_mmio_range *) ent->driver_data; + + r = DEFINE_RES_MEM(base_addr, punit_dev->mmio_range[1].size); + punit_dev->punit_mmio = devm_ioremap_resource(&pdev->dev, &r); + if (IS_ERR(punit_dev->punit_mmio)) + return PTR_ERR(punit_dev->punit_mmio); + + mutex_init(&punit_dev->mutex); + pci_set_drvdata(pdev, punit_dev); + + memset(&cb, 0, sizeof(cb)); + cb.cmd_size = sizeof(struct isst_if_io_reg); + cb.offset = offsetof(struct isst_if_io_regs, io_reg); + cb.cmd_callback = isst_if_mmio_rd_wr; + cb.owner = THIS_MODULE; + ret = isst_if_cdev_register(ISST_IF_DEV_MMIO, &cb); + if (ret) + mutex_destroy(&punit_dev->mutex); + + return ret; +} + +static void isst_if_remove(struct pci_dev *pdev) +{ + struct isst_if_device *punit_dev; + + punit_dev = pci_get_drvdata(pdev); + isst_if_cdev_unregister(ISST_IF_DEV_MMIO); + mutex_destroy(&punit_dev->mutex); +} + +static int __maybe_unused isst_if_suspend(struct device *device) +{ + struct isst_if_device *punit_dev = dev_get_drvdata(device); + int i; + + for (i = 0; i < ARRAY_SIZE(punit_dev->range_0); ++i) + punit_dev->range_0[i] = readl(punit_dev->punit_mmio + + punit_dev->mmio_range[0].beg + 4 * i); + for (i = 0; i < ARRAY_SIZE(punit_dev->range_1); ++i) { + u32 addr; + + addr = punit_dev->mmio_range[1].beg + 4 * i; + if (addr > punit_dev->mmio_range[1].end) + break; + punit_dev->range_1[i] = readl(punit_dev->punit_mmio + addr); + } + + return 0; +} + +static int __maybe_unused isst_if_resume(struct device *device) +{ + struct isst_if_device *punit_dev = dev_get_drvdata(device); + int i; + + for (i = 0; i < ARRAY_SIZE(punit_dev->range_0); ++i) + writel(punit_dev->range_0[i], punit_dev->punit_mmio + + punit_dev->mmio_range[0].beg + 4 * i); + for (i = 0; i < ARRAY_SIZE(punit_dev->range_1); ++i) { + u32 addr; + + addr = punit_dev->mmio_range[1].beg + 4 * i; + if (addr > punit_dev->mmio_range[1].end) + break; + + writel(punit_dev->range_1[i], punit_dev->punit_mmio + addr); + } + + return 0; +} + +static SIMPLE_DEV_PM_OPS(isst_if_pm_ops, isst_if_suspend, isst_if_resume); + +static struct pci_driver isst_if_pci_driver = { + .name = "isst_if_pci", + .id_table = isst_if_ids, + .probe = isst_if_probe, + .remove = isst_if_remove, + .driver.pm = &isst_if_pm_ops, +}; + +module_pci_driver(isst_if_pci_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel speed select interface mmio driver"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_tpmi.c b/drivers/platform/x86/intel/speed_select_if/isst_tpmi.c new file mode 100644 index 000000000..bcf0a5cbc --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_tpmi.c @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * isst_tpmi.c: SST TPMI interface + * + * Copyright (c) 2023, Intel Corporation. + * All Rights Reserved. + * + */ + +#include +#include +#include + +#include "isst_tpmi_core.h" + +static int intel_sst_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + int ret; + + ret = tpmi_sst_init(); + if (ret) + return ret; + + ret = tpmi_sst_dev_add(auxdev); + if (ret) + tpmi_sst_exit(); + + return ret; +} + +static void intel_sst_remove(struct auxiliary_device *auxdev) +{ + tpmi_sst_dev_remove(auxdev); + tpmi_sst_exit(); +} + +static int intel_sst_suspend(struct device *dev) +{ + tpmi_sst_dev_suspend(to_auxiliary_dev(dev)); + + return 0; +} + +static int intel_sst_resume(struct device *dev) +{ + tpmi_sst_dev_resume(to_auxiliary_dev(dev)); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(intel_sst_pm, intel_sst_suspend, intel_sst_resume); + +static const struct auxiliary_device_id intel_sst_id_table[] = { + { .name = "intel_vsec.tpmi-sst" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, intel_sst_id_table); + +static struct auxiliary_driver intel_sst_aux_driver = { + .id_table = intel_sst_id_table, + .remove = intel_sst_remove, + .probe = intel_sst_probe, + .driver = { + .pm = pm_sleep_ptr(&intel_sst_pm), + }, +}; + +module_auxiliary_driver(intel_sst_aux_driver); + +MODULE_IMPORT_NS("INTEL_TPMI_SST"); +MODULE_DESCRIPTION("Intel TPMI SST Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c new file mode 100644 index 000000000..5ded0e25b --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c @@ -0,0 +1,1870 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * isst_tpmi.c: SST TPMI interface core + * + * Copyright (c) 2023, Intel Corporation. + * All Rights Reserved. + * + * This information will be useful to understand flows: + * In the current generation of platforms, TPMI is supported via OOB + * PCI device. This PCI device has one instance per CPU package. + * There is a unique TPMI ID for SST. Each TPMI ID also has multiple + * entries, representing per power domain information. + * + * There is one dev file for complete SST information and control same as the + * prior generation of hardware. User spaces don't need to know how the + * information is presented by the hardware. The TPMI core module implements + * the hardware mapping. + */ + +#define dev_fmt(fmt) "tpmi_sst: " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "isst_tpmi_core.h" +#include "isst_if_common.h" + +/* Supported SST hardware version by this driver */ +#define ISST_MAJOR_VERSION 0 +#define ISST_MINOR_VERSION 3 + +/* + * Used to indicate if value read from MMIO needs to get multiplied + * to get to a standard unit or not. + */ +#define SST_MUL_FACTOR_NONE 1 + +/* Define 100 as a scaling factor frequency ratio to frequency conversion */ +#define SST_MUL_FACTOR_FREQ 100 + +/* All SST regs are 64 bit size */ +#define SST_REG_SIZE 8 + +/** + * struct sst_header - SST main header + * @interface_version: Version number for this interface + * @cap_mask: Bitmask of the supported sub features. 1=the sub feature is enabled. + * 0=disabled. + * Bit[8]= SST_CP enable (1), disable (0) + * bit[9]= SST_PP enable (1), disable (0) + * other bits are reserved for future use + * @cp_offset: Qword (8 bytes) offset to the SST_CP register bank + * @pp_offset: Qword (8 bytes) offset to the SST_PP register bank + * @reserved: Reserved for future use + * + * This register allows SW to discover SST capability and the offsets to SST-CP + * and SST-PP register banks. + */ +struct sst_header { + u8 interface_version; + u8 cap_mask; + u8 cp_offset; + u8 pp_offset; + u32 reserved; +} __packed; + +/** + * struct cp_header - SST-CP (core-power) header + * @feature_id: 0=SST-CP, 1=SST-PP, 2=SST-BF, 3=SST-TF + * @feature_rev: Interface Version number for this SST feature + * @ratio_unit: Frequency ratio unit. 00: 100MHz. All others are reserved + * @reserved: Reserved for future use + * + * This structure is used store SST-CP header. This is packed to the same + * format as defined in the specifications. + */ +struct cp_header { + u64 feature_id :4; + u64 feature_rev :8; + u64 ratio_unit :2; + u64 reserved :50; +} __packed; + +/** + * struct pp_header - SST-PP (Perf profile) header + * @feature_id: 0=SST-CP, 1=SST-PP, 2=SST-BF, 3=SST-TF + * @feature_rev: Interface Version number for this SST feature + * @level_en_mask: SST-PP level enable/disable fuse mask + * @allowed_level_mask: Allowed level mask used for dynamic config level switching + * @reserved0: Reserved for future use + * @ratio_unit: Frequency ratio unit. 00: 100MHz. All others are reserved + * @block_size: Size of PP block in Qword unit (8 bytes) + * @dynamic_switch: If set (1), dynamic switching of SST PP is supported + * @memory_ratio_unit: Memory Controller frequency ratio unit. 00: 100MHz, others reserved + * @reserved1: Reserved for future use + * + * This structure is used store SST-PP header. This is packed to the same + * format as defined in the specifications. + */ +struct pp_header { + u64 feature_id :4; + u64 feature_rev :8; + u64 level_en_mask :8; + u64 allowed_level_mask :8; + u64 reserved0 :4; + u64 ratio_unit :2; + u64 block_size :8; + u64 dynamic_switch :1; + u64 memory_ratio_unit :2; + u64 reserved1 :19; +} __packed; + +/** + * struct feature_offset - Offsets to SST-PP features + * @pp_offset: Qword offset within PP level for the SST_PP register bank + * @bf_offset: Qword offset within PP level for the SST_BF register bank + * @tf_offset: Qword offset within PP level for the SST_TF register bank + * @reserved: Reserved for future use + * + * This structure is used store offsets for SST features in the register bank. + * This is packed to the same format as defined in the specifications. + */ +struct feature_offset { + u64 pp_offset :8; + u64 bf_offset :8; + u64 tf_offset :8; + u64 reserved :40; +} __packed; + +/** + * struct levels_offset - Offsets to each SST PP level + * @sst_pp_level0_offset: Qword offset to the register block of PP level 0 + * @sst_pp_level1_offset: Qword offset to the register block of PP level 1 + * @sst_pp_level2_offset: Qword offset to the register block of PP level 2 + * @sst_pp_level3_offset: Qword offset to the register block of PP level 3 + * @sst_pp_level4_offset: Qword offset to the register block of PP level 4 + * @reserved: Reserved for future use + * + * This structure is used store offsets of SST PP levels in the register bank. + * This is packed to the same format as defined in the specifications. + */ +struct levels_offset { + u64 sst_pp_level0_offset :8; + u64 sst_pp_level1_offset :8; + u64 sst_pp_level2_offset :8; + u64 sst_pp_level3_offset :8; + u64 sst_pp_level4_offset :8; + u64 reserved :24; +} __packed; + +/** + * struct pp_control_offset - Offsets for SST PP controls + * @perf_level: A SST-PP level that SW intends to switch to + * @perf_level_lock: SST-PP level select lock. 0 - unlocked. 1 - locked till next reset + * @resvd0: Reserved for future use + * @current_state: Bit mask to control the enable(1)/disable(0) state of each feature + * of the current PP level, bit 0 = BF, bit 1 = TF, bit 2-7 = reserved + * @reserved: Reserved for future use + * + * This structure is used store offsets of SST PP controls in the register bank. + * This is packed to the same format as defined in the specifications. + */ +struct pp_control_offset { + u64 perf_level :3; + u64 perf_level_lock :1; + u64 resvd0 :4; + u64 current_state :8; + u64 reserved :48; +} __packed; + +/** + * struct pp_status_offset - Offsets for SST PP status fields + * @sst_pp_level: Returns the current SST-PP level + * @sst_pp_lock: Returns the lock bit setting of perf_level_lock in pp_control_offset + * @error_type: Returns last error of SST-PP level change request. 0: no error, + * 1: level change not allowed, others: reserved + * @feature_state: Bit mask to indicate the enable(1)/disable(0) state of each feature of the + * current PP level. bit 0 = BF, bit 1 = TF, bit 2-7 reserved + * @reserved0: Reserved for future use + * @feature_error_type: Returns last error of the specific feature. Three error_type bits per + * feature. i.e. ERROR_TYPE[2:0] for BF, ERROR_TYPE[5:3] for TF, etc. + * 0x0: no error, 0x1: The specific feature is not supported by the hardware. + * 0x2-0x6: Reserved. 0x7: feature state change is not allowed. + * @reserved1: Reserved for future use + * + * This structure is used store offsets of SST PP status in the register bank. + * This is packed to the same format as defined in the specifications. + */ +struct pp_status_offset { + u64 sst_pp_level :3; + u64 sst_pp_lock :1; + u64 error_type :4; + u64 feature_state :8; + u64 reserved0 :16; + u64 feature_error_type : 24; + u64 reserved1 :8; +} __packed; + +/** + * struct perf_level - Used to store perf level and mmio offset + * @mmio_offset: mmio offset for a perf level + * @level: perf level for this offset + * + * This structure is used store final mmio offset of each perf level from the + * SST base mmio offset. + */ +struct perf_level { + int mmio_offset; + int level; +}; + +/** + * struct tpmi_per_power_domain_info - Store per power_domain SST info + * @package_id: Package id for this power_domain + * @power_domain_id: Power domain id, Each entry from the SST-TPMI instance is a power_domain. + * @max_level: Max possible PP level possible for this power_domain + * @ratio_unit: Ratio unit for converting to MHz + * @avx_levels: Number of AVX levels + * @pp_block_size: Block size from PP header + * @sst_header: Store SST header for this power_domain + * @cp_header: Store SST-CP header for this power_domain + * @pp_header: Store SST-PP header for this power_domain + * @perf_levels: Pointer to each perf level to map level to mmio offset + * @feature_offsets: Store feature offsets for each PP-level + * @control_offset: Store the control offset for each PP-level + * @status_offset: Store the status offset for each PP-level + * @sst_base: Mapped SST base IO memory + * @auxdev: Auxiliary device instance enumerated this instance + * @saved_sst_cp_control: Save SST-CP control configuration to store restore for suspend/resume + * @saved_clos_configs: Save SST-CP CLOS configuration to store restore for suspend/resume + * @saved_clos_assocs: Save SST-CP CLOS association to store restore for suspend/resume + * @saved_pp_control: Save SST-PP control information to store restore for suspend/resume + * @write_blocked: Write operation is blocked, so can't change SST state + * + * This structure is used store complete SST information for a power_domain. This information + * is used to read/write request for any SST IOCTL. Each physical CPU package can have multiple + * power_domains. Each power domain describes its own SST information and has its own controls. + */ +struct tpmi_per_power_domain_info { + int package_id; + int power_domain_id; + int max_level; + int ratio_unit; + int avx_levels; + int pp_block_size; + struct sst_header sst_header; + struct cp_header cp_header; + struct pp_header pp_header; + struct perf_level *perf_levels; + struct feature_offset feature_offsets; + struct pp_control_offset control_offset; + struct pp_status_offset status_offset; + void __iomem *sst_base; + struct auxiliary_device *auxdev; + u64 saved_sst_cp_control; + u64 saved_clos_configs[4]; + u64 saved_clos_assocs[4]; + u64 saved_pp_control; + bool write_blocked; +}; + +/* Supported maximum partitions */ +#define SST_MAX_PARTITIONS 2 + +/** + * struct tpmi_sst_struct - Store sst info for a package + * @package_id: Package id for this aux device instance + * @number_of_power_domains: Number of power_domains pointed by power_domain_info pointer + * @power_domain_info: Pointer to power domains information + * @cdie_mask: Mask of compute dies present in a partition from hardware. + * This mask is not present in the version 1 information header. + * @io_dies: Number of IO dies in a partition. This will be 0 for TPMI + * version 1 information header. + * @partition_mask: Mask of all partitions. + * @partition_mask_current: Current partition mask as some may have been unbound. + * + * This structure is used store full SST information for a package. + * Each package has one or multiple OOB PCI devices. Each package can contain multiple + * power domains. + */ +struct tpmi_sst_struct { + int package_id; + struct tpmi_per_power_domain_info *power_domain_info[SST_MAX_PARTITIONS]; + u16 cdie_mask[SST_MAX_PARTITIONS]; + u8 number_of_power_domains[SST_MAX_PARTITIONS]; + u8 io_dies[SST_MAX_PARTITIONS]; + u8 partition_mask; + u8 partition_mask_current; +}; + +/** + * struct tpmi_sst_common_struct - Store all SST instances + * @max_index: Maximum instances currently present + * @sst_inst: Pointer to per package instance + * + * Stores every SST Package instance. + */ +struct tpmi_sst_common_struct { + int max_index; + struct tpmi_sst_struct **sst_inst; +}; + +/* + * Each IOCTL request is processed under this lock. Also used to protect + * registration functions and common data structures. + */ +static DEFINE_MUTEX(isst_tpmi_dev_lock); + +/* Usage count to track, number of TPMI SST instances registered to this core. */ +static int isst_core_usage_count; + +/* Stores complete SST information for every package and power_domain */ +static struct tpmi_sst_common_struct isst_common; + +#define SST_MAX_AVX_LEVELS 3 + +#define SST_PP_OFFSET_0 8 +#define SST_PP_OFFSET_1 16 +#define SST_PP_OFFSET_SIZE 8 + +static int sst_add_perf_profiles(struct auxiliary_device *auxdev, + struct tpmi_per_power_domain_info *pd_info, + int levels) +{ + struct device *dev = &auxdev->dev; + u64 perf_level_offsets; + int i; + + pd_info->perf_levels = devm_kcalloc(dev, levels, sizeof(struct perf_level), GFP_KERNEL); + if (!pd_info->perf_levels) + return 0; + + pd_info->ratio_unit = pd_info->pp_header.ratio_unit; + pd_info->avx_levels = SST_MAX_AVX_LEVELS; + pd_info->pp_block_size = pd_info->pp_header.block_size; + + /* Read PP Offset 0: Get feature offset with PP level */ + *((u64 *)&pd_info->feature_offsets) = readq(pd_info->sst_base + + pd_info->sst_header.pp_offset + + SST_PP_OFFSET_0); + + perf_level_offsets = readq(pd_info->sst_base + pd_info->sst_header.pp_offset + + SST_PP_OFFSET_1); + + for (i = 0; i < levels; ++i) { + u64 offset; + + offset = perf_level_offsets & (0xffULL << (i * SST_PP_OFFSET_SIZE)); + offset >>= (i * 8); + offset &= 0xff; + offset *= 8; /* Convert to byte from QWORD offset */ + pd_info->perf_levels[i].mmio_offset = pd_info->sst_header.pp_offset + offset; + } + + return 0; +} + +static int sst_main(struct auxiliary_device *auxdev, struct tpmi_per_power_domain_info *pd_info) +{ + struct device *dev = &auxdev->dev; + int i, mask, levels; + + *((u64 *)&pd_info->sst_header) = readq(pd_info->sst_base); + pd_info->sst_header.cp_offset *= 8; + pd_info->sst_header.pp_offset *= 8; + + if (pd_info->sst_header.interface_version == TPMI_VERSION_INVALID) + return -ENODEV; + + if (TPMI_MAJOR_VERSION(pd_info->sst_header.interface_version) != ISST_MAJOR_VERSION) { + dev_err(dev, "SST: Unsupported major version:%lx\n", + TPMI_MAJOR_VERSION(pd_info->sst_header.interface_version)); + return -ENODEV; + } + + if (TPMI_MINOR_VERSION(pd_info->sst_header.interface_version) > ISST_MINOR_VERSION) + dev_info(dev, "SST: Ignore: Unsupported minor version:%lx\n", + TPMI_MINOR_VERSION(pd_info->sst_header.interface_version)); + + /* Read SST CP Header */ + *((u64 *)&pd_info->cp_header) = readq(pd_info->sst_base + pd_info->sst_header.cp_offset); + + /* Read PP header */ + *((u64 *)&pd_info->pp_header) = readq(pd_info->sst_base + pd_info->sst_header.pp_offset); + + mask = 0x01; + levels = 0; + for (i = 0; i < 8; ++i) { + if (pd_info->pp_header.level_en_mask & mask) + levels = i; + mask <<= 1; + } + pd_info->max_level = levels; + sst_add_perf_profiles(auxdev, pd_info, levels + 1); + + return 0; +} + +static u8 isst_instance_count(struct tpmi_sst_struct *sst_inst) +{ + u8 i, max_part, count = 0; + + /* Partition mask starts from bit 0 and contains 1s only */ + max_part = hweight8(sst_inst->partition_mask); + for (i = 0; i < max_part; i++) + count += sst_inst->number_of_power_domains[i]; + + return count; +} + +/** + * map_cdies() - Map user domain ID to compute domain ID + * @sst_inst: TPMI Instance + * @id: User domain ID + * @partition: Resolved partition + * + * Helper function to map_partition_power_domain_id() to resolve compute + * domain ID and partition. Use hardware provided cdie_mask for a partition + * as is to resolve a compute domain ID. + * + * Return: %-EINVAL on error, otherwise mapped domain ID >= 0. + */ +static int map_cdies(struct tpmi_sst_struct *sst_inst, u8 id, u8 *partition) +{ + u8 i, max_part; + + max_part = hweight8(sst_inst->partition_mask); + for (i = 0; i < max_part; i++) { + if (!(sst_inst->cdie_mask[i] & BIT(id))) + continue; + + *partition = i; + return id - ffs(sst_inst->cdie_mask[i]) + 1; + } + + return -EINVAL; +} + +/** + * map_partition_power_domain_id() - Map user domain ID to partition domain ID + * @sst_inst: TPMI Instance + * @id: User domain ID + * @partition: Resolved partition + * + * In a partitioned system a CPU package has two separate MMIO ranges (Under + * two PCI devices). But the CPU package compute die/power domain IDs are + * unique in a package. User space can get compute die/power domain ID from + * CPUID and MSR 0x54 for a CPU. So, those IDs need to be preserved even if + * they are present in two different partitions with its own order. + * + * For example for command ISST_IF_COUNT_TPMI_INSTANCES, the valid_mask + * is 111111b for a 4 compute and 2 IO dies system. This is presented as + * provided by the hardware in a non-partitioned system with the following + * order: + * I1-I0-C3-C2-C1-C0 + * Here: "C": for compute and "I" for IO die. + * Compute dies are always present first in TPMI instances, as they have + * to map to the real power domain/die ID of a system. In a non-partitioned + * system there is no way to identify compute and IO die boundaries from + * this driver without reading each CPU's mapping. + * + * The same order needs to be preserved, even if those compute dies are + * distributed among multiple partitions. For example: + * Partition 1 can contain: I1-C1-C0 + * Partition 2 can contain: I2-C3-C2 + * + * This will require a conversion of user space IDs to the actual index into + * array of stored power domains for each partition. For the above example + * this function will return partition and index as follows: + * + * ============= ========= ===== ======== + * User space ID Partition Index Die type + * ============= ========= ===== ======== + * 0 0 0 Compute + * 1 0 1 Compute + * 2 1 0 Compute + * 3 1 1 Compute + * 4 0 2 IO + * 5 1 2 IO + * ============= ========= ===== ======== + * + * Return: %-EINVAL on error, otherwise mapped domain ID >= 0. + */ +static int map_partition_power_domain_id(struct tpmi_sst_struct *sst_inst, u8 id, u8 *partition) +{ + u8 i, io_start_id, max_part; + + *partition = 0; + + /* If any PCI device for partition is unbound, treat this as failure */ + if (sst_inst->partition_mask != sst_inst->partition_mask_current) + return -EINVAL; + + max_part = hweight8(sst_inst->partition_mask); + + /* IO Index begin here */ + io_start_id = fls(sst_inst->cdie_mask[max_part - 1]); + + if (id < io_start_id) + return map_cdies(sst_inst, id, partition); + + for (i = 0; i < max_part; i++) { + u8 io_id; + + io_id = id - io_start_id; + if (io_id < sst_inst->io_dies[i]) { + u8 cdie_range; + + cdie_range = fls(sst_inst->cdie_mask[i]) - ffs(sst_inst->cdie_mask[i]) + 1; + *partition = i; + return cdie_range + io_id; + } + io_start_id += sst_inst->io_dies[i]; + } + + return -EINVAL; +} + +/* + * Map a package and power_domain id to SST information structure unique for a power_domain. + * The caller should call under isst_tpmi_dev_lock. + */ +static struct tpmi_per_power_domain_info *get_instance(int pkg_id, int power_domain_id) +{ + struct tpmi_per_power_domain_info *power_domain_info; + struct tpmi_sst_struct *sst_inst; + u8 part; + + if (!in_range(pkg_id, 0, topology_max_packages()) || pkg_id > isst_common.max_index) + return NULL; + + sst_inst = isst_common.sst_inst[pkg_id]; + if (!sst_inst) + return NULL; + + power_domain_id = map_partition_power_domain_id(sst_inst, power_domain_id, &part); + if (power_domain_id < 0) + return NULL; + + power_domain_info = &sst_inst->power_domain_info[part][power_domain_id]; + + if (power_domain_info && !power_domain_info->sst_base) + return NULL; + + return power_domain_info; +} + +static bool disable_dynamic_sst_features(void) +{ + u64 value; + + if (!cpu_feature_enabled(X86_FEATURE_HWP)) + return true; + + rdmsrq(MSR_PM_ENABLE, value); + return !(value & 0x1); +} + +#define _read_cp_info(name_str, name, offset, start, width, mult_factor)\ +{\ + u64 val, mask;\ + \ + val = readq(power_domain_info->sst_base + power_domain_info->sst_header.cp_offset +\ + (offset));\ + mask = GENMASK_ULL((start + width - 1), start);\ + val &= mask; \ + val >>= start;\ + name = (val * mult_factor);\ +} + +#define _write_cp_info(name_str, name, offset, start, width, div_factor)\ +{\ + u64 val, mask;\ + \ + val = readq(power_domain_info->sst_base +\ + power_domain_info->sst_header.cp_offset + (offset));\ + mask = GENMASK_ULL((start + width - 1), start);\ + val &= ~mask;\ + val |= (name / div_factor) << start;\ + writeq(val, power_domain_info->sst_base + power_domain_info->sst_header.cp_offset +\ + (offset));\ +} + +#define SST_CP_CONTROL_OFFSET 8 +#define SST_CP_STATUS_OFFSET 16 + +#define SST_CP_ENABLE_START 0 +#define SST_CP_ENABLE_WIDTH 1 + +#define SST_CP_PRIORITY_TYPE_START 1 +#define SST_CP_PRIORITY_TYPE_WIDTH 1 + +static long isst_if_core_power_state(void __user *argp) +{ + struct tpmi_per_power_domain_info *power_domain_info; + struct isst_core_power core_power; + + if (copy_from_user(&core_power, argp, sizeof(core_power))) + return -EFAULT; + + if (core_power.get_set && disable_dynamic_sst_features()) + return -EFAULT; + + power_domain_info = get_instance(core_power.socket_id, core_power.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (core_power.get_set) { + if (power_domain_info->write_blocked || !capable(CAP_SYS_ADMIN)) + return -EPERM; + + _write_cp_info("cp_enable", core_power.enable, SST_CP_CONTROL_OFFSET, + SST_CP_ENABLE_START, SST_CP_ENABLE_WIDTH, SST_MUL_FACTOR_NONE) + _write_cp_info("cp_prio_type", core_power.priority_type, SST_CP_CONTROL_OFFSET, + SST_CP_PRIORITY_TYPE_START, SST_CP_PRIORITY_TYPE_WIDTH, + SST_MUL_FACTOR_NONE) + } else { + /* get */ + _read_cp_info("cp_enable", core_power.enable, SST_CP_STATUS_OFFSET, + SST_CP_ENABLE_START, SST_CP_ENABLE_WIDTH, SST_MUL_FACTOR_NONE) + _read_cp_info("cp_prio_type", core_power.priority_type, SST_CP_STATUS_OFFSET, + SST_CP_PRIORITY_TYPE_START, SST_CP_PRIORITY_TYPE_WIDTH, + SST_MUL_FACTOR_NONE) + core_power.supported = !!(power_domain_info->sst_header.cap_mask & BIT(0)); + if (copy_to_user(argp, &core_power, sizeof(core_power))) + return -EFAULT; + } + + return 0; +} + +#define SST_CLOS_CONFIG_0_OFFSET 24 + +#define SST_CLOS_CONFIG_PRIO_START 4 +#define SST_CLOS_CONFIG_PRIO_WIDTH 4 + +#define SST_CLOS_CONFIG_MIN_START 8 +#define SST_CLOS_CONFIG_MIN_WIDTH 8 + +#define SST_CLOS_CONFIG_MAX_START 16 +#define SST_CLOS_CONFIG_MAX_WIDTH 8 + +static long isst_if_clos_param(void __user *argp) +{ + struct tpmi_per_power_domain_info *power_domain_info; + struct isst_clos_param clos_param; + + if (copy_from_user(&clos_param, argp, sizeof(clos_param))) + return -EFAULT; + + power_domain_info = get_instance(clos_param.socket_id, clos_param.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (clos_param.get_set) { + if (power_domain_info->write_blocked || !capable(CAP_SYS_ADMIN)) + return -EPERM; + + _write_cp_info("clos.min_freq", clos_param.min_freq_mhz, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_MIN_START, SST_CLOS_CONFIG_MIN_WIDTH, + SST_MUL_FACTOR_FREQ); + _write_cp_info("clos.max_freq", clos_param.max_freq_mhz, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_MAX_START, SST_CLOS_CONFIG_MAX_WIDTH, + SST_MUL_FACTOR_FREQ); + _write_cp_info("clos.prio", clos_param.prop_prio, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_PRIO_START, SST_CLOS_CONFIG_PRIO_WIDTH, + SST_MUL_FACTOR_NONE); + } else { + /* get */ + _read_cp_info("clos.min_freq", clos_param.min_freq_mhz, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_MIN_START, SST_CLOS_CONFIG_MIN_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_cp_info("clos.max_freq", clos_param.max_freq_mhz, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_MAX_START, SST_CLOS_CONFIG_MAX_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_cp_info("clos.prio", clos_param.prop_prio, + (SST_CLOS_CONFIG_0_OFFSET + clos_param.clos * SST_REG_SIZE), + SST_CLOS_CONFIG_PRIO_START, SST_CLOS_CONFIG_PRIO_WIDTH, + SST_MUL_FACTOR_NONE) + + if (copy_to_user(argp, &clos_param, sizeof(clos_param))) + return -EFAULT; + } + + return 0; +} + +#define SST_CLOS_ASSOC_0_OFFSET 56 +#define SST_CLOS_ASSOC_CPUS_PER_REG 16 +#define SST_CLOS_ASSOC_BITS_PER_CPU 4 + +static long isst_if_clos_assoc(void __user *argp) +{ + struct isst_if_clos_assoc_cmds assoc_cmds; + unsigned char __user *ptr; + int i; + + /* Each multi command has u16 command count as the first field */ + if (copy_from_user(&assoc_cmds, argp, sizeof(assoc_cmds))) + return -EFAULT; + + if (!assoc_cmds.cmd_count || assoc_cmds.cmd_count > ISST_IF_CMD_LIMIT) + return -EINVAL; + + ptr = argp + offsetof(struct isst_if_clos_assoc_cmds, assoc_info); + for (i = 0; i < assoc_cmds.cmd_count; ++i) { + struct tpmi_per_power_domain_info *power_domain_info; + struct isst_if_clos_assoc clos_assoc; + int punit_id, punit_cpu_no, pkg_id; + struct tpmi_sst_struct *sst_inst; + int offset, shift, cpu; + u64 val, mask, clos; + u8 part; + + if (copy_from_user(&clos_assoc, ptr, sizeof(clos_assoc))) + return -EFAULT; + + if (clos_assoc.socket_id > topology_max_packages()) + return -EINVAL; + + cpu = clos_assoc.logical_cpu; + clos = clos_assoc.clos; + + if (assoc_cmds.punit_cpu_map) + punit_cpu_no = cpu; + else + return -EOPNOTSUPP; + + if (punit_cpu_no < 0) + return -EINVAL; + + punit_id = clos_assoc.power_domain_id; + pkg_id = clos_assoc.socket_id; + + sst_inst = isst_common.sst_inst[pkg_id]; + + punit_id = map_partition_power_domain_id(sst_inst, punit_id, &part); + if (punit_id < 0) + return -EINVAL; + + power_domain_info = &sst_inst->power_domain_info[part][punit_id]; + + if (assoc_cmds.get_set && (power_domain_info->write_blocked || + !capable(CAP_SYS_ADMIN))) + return -EPERM; + + offset = SST_CLOS_ASSOC_0_OFFSET + + (punit_cpu_no / SST_CLOS_ASSOC_CPUS_PER_REG) * SST_REG_SIZE; + shift = punit_cpu_no % SST_CLOS_ASSOC_CPUS_PER_REG; + shift *= SST_CLOS_ASSOC_BITS_PER_CPU; + + val = readq(power_domain_info->sst_base + + power_domain_info->sst_header.cp_offset + offset); + if (assoc_cmds.get_set) { + mask = GENMASK_ULL((shift + SST_CLOS_ASSOC_BITS_PER_CPU - 1), shift); + val &= ~mask; + val |= (clos << shift); + writeq(val, power_domain_info->sst_base + + power_domain_info->sst_header.cp_offset + offset); + } else { + val >>= shift; + clos_assoc.clos = val & GENMASK(SST_CLOS_ASSOC_BITS_PER_CPU - 1, 0); + if (copy_to_user(ptr, &clos_assoc, sizeof(clos_assoc))) + return -EFAULT; + } + + ptr += sizeof(clos_assoc); + } + + return 0; +} + +#define _read_pp_info(name_str, name, offset, start, width, mult_factor)\ +{\ + u64 val, _mask;\ + \ + val = readq(power_domain_info->sst_base + power_domain_info->sst_header.pp_offset +\ + (offset));\ + _mask = GENMASK_ULL((start + width - 1), start);\ + val &= _mask;\ + val >>= start;\ + name = (val * mult_factor);\ +} + +#define _write_pp_info(name_str, name, offset, start, width, div_factor)\ +{\ + u64 val, _mask;\ + \ + val = readq(power_domain_info->sst_base + power_domain_info->sst_header.pp_offset +\ + (offset));\ + _mask = GENMASK((start + width - 1), start);\ + val &= ~_mask;\ + val |= (name / div_factor) << start;\ + writeq(val, power_domain_info->sst_base + power_domain_info->sst_header.pp_offset +\ + (offset));\ +} + +#define _read_bf_level_info(name_str, name, level, offset, start, width, mult_factor)\ +{\ + u64 val, _mask;\ + \ + val = readq(power_domain_info->sst_base +\ + power_domain_info->perf_levels[level].mmio_offset +\ + (power_domain_info->feature_offsets.bf_offset * 8) + (offset));\ + _mask = GENMASK_ULL((start + width - 1), start);\ + val &= _mask; \ + val >>= start;\ + name = (val * mult_factor);\ +} + +#define _read_tf_level_info(name_str, name, level, offset, start, width, mult_factor)\ +{\ + u64 val, _mask;\ + \ + val = readq(power_domain_info->sst_base +\ + power_domain_info->perf_levels[level].mmio_offset +\ + (power_domain_info->feature_offsets.tf_offset * 8) + (offset));\ + _mask = GENMASK_ULL((start + width - 1), start);\ + val &= _mask; \ + val >>= start;\ + name = (val * mult_factor);\ +} + +#define SST_PP_STATUS_OFFSET 32 + +#define SST_PP_LEVEL_START 0 +#define SST_PP_LEVEL_WIDTH 3 + +#define SST_PP_LOCK_START 3 +#define SST_PP_LOCK_WIDTH 1 + +#define SST_PP_FEATURE_STATE_START 8 +#define SST_PP_FEATURE_STATE_WIDTH 8 + +#define SST_BF_FEATURE_SUPPORTED_START 12 +#define SST_BF_FEATURE_SUPPORTED_WIDTH 1 + +#define SST_TF_FEATURE_SUPPORTED_START 12 +#define SST_TF_FEATURE_SUPPORTED_WIDTH 1 + +static int isst_if_get_perf_level(void __user *argp) +{ + struct isst_perf_level_info perf_level; + struct tpmi_per_power_domain_info *power_domain_info; + unsigned long level_mask; + u8 level, support; + + if (copy_from_user(&perf_level, argp, sizeof(perf_level))) + return -EFAULT; + + power_domain_info = get_instance(perf_level.socket_id, perf_level.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + perf_level.max_level = power_domain_info->max_level; + perf_level.level_mask = power_domain_info->pp_header.level_en_mask; + perf_level.feature_rev = power_domain_info->pp_header.feature_rev; + _read_pp_info("current_level", perf_level.current_level, SST_PP_STATUS_OFFSET, + SST_PP_LEVEL_START, SST_PP_LEVEL_WIDTH, SST_MUL_FACTOR_NONE) + _read_pp_info("locked", perf_level.locked, SST_PP_STATUS_OFFSET, + SST_PP_LOCK_START, SST_PP_LOCK_WIDTH, SST_MUL_FACTOR_NONE) + _read_pp_info("feature_state", perf_level.feature_state, SST_PP_STATUS_OFFSET, + SST_PP_FEATURE_STATE_START, SST_PP_FEATURE_STATE_WIDTH, SST_MUL_FACTOR_NONE) + perf_level.enabled = !!(power_domain_info->sst_header.cap_mask & BIT(1)); + + level_mask = perf_level.level_mask; + perf_level.sst_bf_support = 0; + for_each_set_bit(level, &level_mask, BITS_PER_BYTE) { + /* + * Read BF support for a level. Read output is updated + * to "support" variable by the below macro. + */ + _read_bf_level_info("bf_support", support, level, 0, SST_BF_FEATURE_SUPPORTED_START, + SST_BF_FEATURE_SUPPORTED_WIDTH, SST_MUL_FACTOR_NONE); + + /* If supported set the bit for the level */ + if (support) + perf_level.sst_bf_support |= BIT(level); + } + + perf_level.sst_tf_support = 0; + for_each_set_bit(level, &level_mask, BITS_PER_BYTE) { + /* + * Read TF support for a level. Read output is updated + * to "support" variable by the below macro. + */ + _read_tf_level_info("tf_support", support, level, 0, SST_TF_FEATURE_SUPPORTED_START, + SST_TF_FEATURE_SUPPORTED_WIDTH, SST_MUL_FACTOR_NONE); + + /* If supported set the bit for the level */ + if (support) + perf_level.sst_tf_support |= BIT(level); + } + + if (copy_to_user(argp, &perf_level, sizeof(perf_level))) + return -EFAULT; + + return 0; +} + +#define SST_PP_CONTROL_OFFSET 24 +#define SST_PP_LEVEL_CHANGE_TIME_MS 5 +#define SST_PP_LEVEL_CHANGE_RETRY_COUNT 3 + +static int isst_if_set_perf_level(void __user *argp) +{ + struct isst_perf_level_control perf_level; + struct tpmi_per_power_domain_info *power_domain_info; + int level, retry = 0; + + if (disable_dynamic_sst_features()) + return -EFAULT; + + if (copy_from_user(&perf_level, argp, sizeof(perf_level))) + return -EFAULT; + + power_domain_info = get_instance(perf_level.socket_id, perf_level.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (power_domain_info->write_blocked || !capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!(power_domain_info->pp_header.allowed_level_mask & BIT(perf_level.level))) + return -EINVAL; + + _read_pp_info("current_level", level, SST_PP_STATUS_OFFSET, + SST_PP_LEVEL_START, SST_PP_LEVEL_WIDTH, SST_MUL_FACTOR_NONE) + + /* If the requested new level is same as the current level, reject */ + if (perf_level.level == level) + return -EINVAL; + + _write_pp_info("perf_level", perf_level.level, SST_PP_CONTROL_OFFSET, + SST_PP_LEVEL_START, SST_PP_LEVEL_WIDTH, SST_MUL_FACTOR_NONE) + + /* It is possible that firmware is busy (although unlikely), so retry */ + do { + /* Give time to FW to process */ + msleep(SST_PP_LEVEL_CHANGE_TIME_MS); + + _read_pp_info("current_level", level, SST_PP_STATUS_OFFSET, + SST_PP_LEVEL_START, SST_PP_LEVEL_WIDTH, SST_MUL_FACTOR_NONE) + + /* Check if the new level is active */ + if (perf_level.level == level) + break; + + } while (retry++ < SST_PP_LEVEL_CHANGE_RETRY_COUNT); + + /* If the level change didn't happen, return fault */ + if (perf_level.level != level) + return -EFAULT; + + /* Reset the feature state on level change */ + _write_pp_info("perf_feature", 0, SST_PP_CONTROL_OFFSET, + SST_PP_FEATURE_STATE_START, SST_PP_FEATURE_STATE_WIDTH, + SST_MUL_FACTOR_NONE) + + /* Give time to FW to process */ + msleep(SST_PP_LEVEL_CHANGE_TIME_MS); + + return 0; +} + +static int isst_if_set_perf_feature(void __user *argp) +{ + struct isst_perf_feature_control perf_feature; + struct tpmi_per_power_domain_info *power_domain_info; + + if (disable_dynamic_sst_features()) + return -EFAULT; + + if (copy_from_user(&perf_feature, argp, sizeof(perf_feature))) + return -EFAULT; + + power_domain_info = get_instance(perf_feature.socket_id, perf_feature.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (power_domain_info->write_blocked || !capable(CAP_SYS_ADMIN)) + return -EPERM; + + _write_pp_info("perf_feature", perf_feature.feature, SST_PP_CONTROL_OFFSET, + SST_PP_FEATURE_STATE_START, SST_PP_FEATURE_STATE_WIDTH, + SST_MUL_FACTOR_NONE) + + return 0; +} + +#define _read_pp_level_info(name_str, name, level, offset, start, width, mult_factor)\ +{\ + u64 val, _mask;\ + \ + val = readq(power_domain_info->sst_base +\ + power_domain_info->perf_levels[level].mmio_offset +\ + (power_domain_info->feature_offsets.pp_offset * 8) + (offset));\ + _mask = GENMASK_ULL((start + width - 1), start);\ + val &= _mask; \ + val >>= start;\ + name = (val * mult_factor);\ +} + +#define SST_PP_INFO_0_OFFSET 0 +#define SST_PP_INFO_1_OFFSET 8 +#define SST_PP_INFO_2_OFFSET 16 +#define SST_PP_INFO_3_OFFSET 24 + +/* SST_PP_INFO_4_OFFSET to SST_PP_INFO_9_OFFSET are trl levels */ +#define SST_PP_INFO_4_OFFSET 32 + +#define SST_PP_INFO_10_OFFSET 80 +#define SST_PP_INFO_11_OFFSET 88 +#define SST_PP_INFO_12_OFFSET 96 + +#define SST_PP_P1_SSE_START 0 +#define SST_PP_P1_SSE_WIDTH 8 + +#define SST_PP_P1_AVX2_START 8 +#define SST_PP_P1_AVX2_WIDTH 8 + +#define SST_PP_P1_AVX512_START 16 +#define SST_PP_P1_AVX512_WIDTH 8 + +#define SST_PP_P1_AMX_START 24 +#define SST_PP_P1_AMX_WIDTH 8 + +#define SST_PP_TDP_START 32 +#define SST_PP_TDP_WIDTH 15 + +#define SST_PP_T_PROCHOT_START 47 +#define SST_PP_T_PROCHOT_WIDTH 8 + +#define SST_PP_MAX_MEMORY_FREQ_START 55 +#define SST_PP_MAX_MEMORY_FREQ_WIDTH 7 + +#define SST_PP_COOLING_TYPE_START 62 +#define SST_PP_COOLING_TYPE_WIDTH 2 + +#define SST_PP_TRL_0_RATIO_0_START 0 +#define SST_PP_TRL_0_RATIO_0_WIDTH 8 + +#define SST_PP_TRL_CORES_BUCKET_0_START 0 +#define SST_PP_TRL_CORES_BUCKET_0_WIDTH 8 + +#define SST_PP_CORE_RATIO_P0_START 0 +#define SST_PP_CORE_RATIO_P0_WIDTH 8 + +#define SST_PP_CORE_RATIO_P1_START 8 +#define SST_PP_CORE_RATIO_P1_WIDTH 8 + +#define SST_PP_CORE_RATIO_PN_START 16 +#define SST_PP_CORE_RATIO_PN_WIDTH 8 + +#define SST_PP_CORE_RATIO_PM_START 24 +#define SST_PP_CORE_RATIO_PM_WIDTH 8 + +#define SST_PP_CORE_RATIO_P0_FABRIC_START 32 +#define SST_PP_CORE_RATIO_P0_FABRIC_WIDTH 8 + +#define SST_PP_CORE_RATIO_P1_FABRIC_START 40 +#define SST_PP_CORE_RATIO_P1_FABRIC_WIDTH 8 + +#define SST_PP_CORE_RATIO_PM_FABRIC_START 48 +#define SST_PP_CORE_RATIO_PM_FABRIC_WIDTH 8 + +#define SST_PP_CORE_RATIO_P0_FABRIC_1_START 0 +#define SST_PP_CORE_RATIO_P0_FABRIC_1_WIDTH 8 + +#define SST_PP_CORE_RATIO_P1_FABRIC_1_START 8 +#define SST_PP_CORE_RATIO_P1_FABRIC_1_WIDTH 8 + +#define SST_PP_CORE_RATIO_PM_FABRIC_1_START 16 +#define SST_PP_CORE_RATIO_PM_FABRIC_1_WIDTH 8 + +static int isst_if_get_perf_level_info(void __user *argp) +{ + struct isst_perf_level_data_info perf_level; + struct tpmi_per_power_domain_info *power_domain_info; + int i, j; + + if (copy_from_user(&perf_level, argp, sizeof(perf_level))) + return -EFAULT; + + power_domain_info = get_instance(perf_level.socket_id, perf_level.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (perf_level.level > power_domain_info->max_level) + return -EINVAL; + + if (!(power_domain_info->pp_header.level_en_mask & BIT(perf_level.level))) + return -EINVAL; + + _read_pp_level_info("tdp_ratio", perf_level.tdp_ratio, perf_level.level, + SST_PP_INFO_0_OFFSET, SST_PP_P1_SSE_START, SST_PP_P1_SSE_WIDTH, + SST_MUL_FACTOR_NONE) + _read_pp_level_info("base_freq_mhz", perf_level.base_freq_mhz, perf_level.level, + SST_PP_INFO_0_OFFSET, SST_PP_P1_SSE_START, SST_PP_P1_SSE_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_pp_level_info("base_freq_avx2_mhz", perf_level.base_freq_avx2_mhz, perf_level.level, + SST_PP_INFO_0_OFFSET, SST_PP_P1_AVX2_START, SST_PP_P1_AVX2_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_pp_level_info("base_freq_avx512_mhz", perf_level.base_freq_avx512_mhz, + perf_level.level, SST_PP_INFO_0_OFFSET, SST_PP_P1_AVX512_START, + SST_PP_P1_AVX512_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("base_freq_amx_mhz", perf_level.base_freq_amx_mhz, perf_level.level, + SST_PP_INFO_0_OFFSET, SST_PP_P1_AMX_START, SST_PP_P1_AMX_WIDTH, + SST_MUL_FACTOR_FREQ) + + _read_pp_level_info("thermal_design_power_w", perf_level.thermal_design_power_w, + perf_level.level, SST_PP_INFO_1_OFFSET, SST_PP_TDP_START, + SST_PP_TDP_WIDTH, SST_MUL_FACTOR_NONE) + perf_level.thermal_design_power_w /= 8; /* units are in 1/8th watt */ + _read_pp_level_info("tjunction_max_c", perf_level.tjunction_max_c, perf_level.level, + SST_PP_INFO_1_OFFSET, SST_PP_T_PROCHOT_START, SST_PP_T_PROCHOT_WIDTH, + SST_MUL_FACTOR_NONE) + _read_pp_level_info("max_memory_freq_mhz", perf_level.max_memory_freq_mhz, + perf_level.level, SST_PP_INFO_1_OFFSET, SST_PP_MAX_MEMORY_FREQ_START, + SST_PP_MAX_MEMORY_FREQ_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("cooling_type", perf_level.cooling_type, perf_level.level, + SST_PP_INFO_1_OFFSET, SST_PP_COOLING_TYPE_START, + SST_PP_COOLING_TYPE_WIDTH, SST_MUL_FACTOR_NONE) + + for (i = 0; i < TRL_MAX_LEVELS; ++i) { + for (j = 0; j < TRL_MAX_BUCKETS; ++j) + _read_pp_level_info("trl*_bucket*_freq_mhz", + perf_level.trl_freq_mhz[i][j], perf_level.level, + SST_PP_INFO_4_OFFSET + (i * SST_PP_TRL_0_RATIO_0_WIDTH), + j * SST_PP_TRL_0_RATIO_0_WIDTH, + SST_PP_TRL_0_RATIO_0_WIDTH, + SST_MUL_FACTOR_FREQ); + } + + for (i = 0; i < TRL_MAX_BUCKETS; ++i) + _read_pp_level_info("bucket*_core_count", perf_level.bucket_core_counts[i], + perf_level.level, SST_PP_INFO_10_OFFSET, + SST_PP_TRL_CORES_BUCKET_0_WIDTH * i, + SST_PP_TRL_CORES_BUCKET_0_WIDTH, SST_MUL_FACTOR_NONE) + + perf_level.max_buckets = TRL_MAX_BUCKETS; + perf_level.max_trl_levels = TRL_MAX_LEVELS; + + _read_pp_level_info("p0_freq_mhz", perf_level.p0_freq_mhz, perf_level.level, + SST_PP_INFO_11_OFFSET, SST_PP_CORE_RATIO_P0_START, + SST_PP_CORE_RATIO_P0_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("p1_freq_mhz", perf_level.p1_freq_mhz, perf_level.level, + SST_PP_INFO_11_OFFSET, SST_PP_CORE_RATIO_P1_START, + SST_PP_CORE_RATIO_P1_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("pn_freq_mhz", perf_level.pn_freq_mhz, perf_level.level, + SST_PP_INFO_11_OFFSET, SST_PP_CORE_RATIO_PN_START, + SST_PP_CORE_RATIO_PN_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("pm_freq_mhz", perf_level.pm_freq_mhz, perf_level.level, + SST_PP_INFO_11_OFFSET, SST_PP_CORE_RATIO_PM_START, + SST_PP_CORE_RATIO_PM_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("p0_fabric_freq_mhz", perf_level.p0_fabric_freq_mhz, + perf_level.level, SST_PP_INFO_11_OFFSET, + SST_PP_CORE_RATIO_P0_FABRIC_START, + SST_PP_CORE_RATIO_P0_FABRIC_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("p1_fabric_freq_mhz", perf_level.p1_fabric_freq_mhz, + perf_level.level, SST_PP_INFO_11_OFFSET, + SST_PP_CORE_RATIO_P1_FABRIC_START, + SST_PP_CORE_RATIO_P1_FABRIC_WIDTH, SST_MUL_FACTOR_FREQ) + _read_pp_level_info("pm_fabric_freq_mhz", perf_level.pm_fabric_freq_mhz, + perf_level.level, SST_PP_INFO_11_OFFSET, + SST_PP_CORE_RATIO_PM_FABRIC_START, + SST_PP_CORE_RATIO_PM_FABRIC_WIDTH, SST_MUL_FACTOR_FREQ) + + if (copy_to_user(argp, &perf_level, sizeof(perf_level))) + return -EFAULT; + + return 0; +} + +static int isst_if_get_perf_level_fabric_info(void __user *argp) +{ + struct isst_perf_level_fabric_info perf_level_fabric; + struct tpmi_per_power_domain_info *power_domain_info; + int start = SST_PP_CORE_RATIO_P0_FABRIC_START; + int width = SST_PP_CORE_RATIO_P0_FABRIC_WIDTH; + int offset = SST_PP_INFO_11_OFFSET; + int i; + + if (copy_from_user(&perf_level_fabric, argp, sizeof(perf_level_fabric))) + return -EFAULT; + + power_domain_info = get_instance(perf_level_fabric.socket_id, + perf_level_fabric.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (perf_level_fabric.level > power_domain_info->max_level) + return -EINVAL; + + if (power_domain_info->pp_header.feature_rev < 2) + return -EINVAL; + + if (!(power_domain_info->pp_header.level_en_mask & BIT(perf_level_fabric.level))) + return -EINVAL; + + /* For revision 2, maximum number of fabrics is 2 */ + perf_level_fabric.max_fabrics = 2; + + for (i = 0; i < perf_level_fabric.max_fabrics; i++) { + _read_pp_level_info("p0_fabric_freq_mhz", perf_level_fabric.p0_fabric_freq_mhz[i], + perf_level_fabric.level, offset, start, width, + SST_MUL_FACTOR_FREQ) + start += width; + + _read_pp_level_info("p1_fabric_freq_mhz", perf_level_fabric.p1_fabric_freq_mhz[i], + perf_level_fabric.level, offset, start, width, + SST_MUL_FACTOR_FREQ) + start += width; + + _read_pp_level_info("pm_fabric_freq_mhz", perf_level_fabric.pm_fabric_freq_mhz[i], + perf_level_fabric.level, offset, start, width, + SST_MUL_FACTOR_FREQ) + offset = SST_PP_INFO_12_OFFSET; + start = SST_PP_CORE_RATIO_P0_FABRIC_1_START; + } + + if (copy_to_user(argp, &perf_level_fabric, sizeof(perf_level_fabric))) + return -EFAULT; + + return 0; +} + +#define SST_PP_FUSED_CORE_COUNT_START 0 +#define SST_PP_FUSED_CORE_COUNT_WIDTH 8 + +#define SST_PP_RSLVD_CORE_COUNT_START 8 +#define SST_PP_RSLVD_CORE_COUNT_WIDTH 8 + +#define SST_PP_RSLVD_CORE_MASK_START 0 +#define SST_PP_RSLVD_CORE_MASK_WIDTH 64 + +static int isst_if_get_perf_level_mask(void __user *argp) +{ + static struct isst_perf_level_cpu_mask cpumask; + struct tpmi_per_power_domain_info *power_domain_info; + u64 mask; + + if (copy_from_user(&cpumask, argp, sizeof(cpumask))) + return -EFAULT; + + power_domain_info = get_instance(cpumask.socket_id, cpumask.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + _read_pp_level_info("mask", mask, cpumask.level, SST_PP_INFO_2_OFFSET, + SST_PP_RSLVD_CORE_MASK_START, SST_PP_RSLVD_CORE_MASK_WIDTH, + SST_MUL_FACTOR_NONE) + + cpumask.mask = mask; + + if (!cpumask.punit_cpu_map) + return -EOPNOTSUPP; + + if (copy_to_user(argp, &cpumask, sizeof(cpumask))) + return -EFAULT; + + return 0; +} + +#define SST_BF_INFO_0_OFFSET 0 +#define SST_BF_INFO_1_OFFSET 8 + +#define SST_BF_P1_HIGH_START 13 +#define SST_BF_P1_HIGH_WIDTH 8 + +#define SST_BF_P1_LOW_START 21 +#define SST_BF_P1_LOW_WIDTH 8 + +#define SST_BF_T_PROHOT_START 38 +#define SST_BF_T_PROHOT_WIDTH 8 + +#define SST_BF_TDP_START 46 +#define SST_BF_TDP_WIDTH 15 + +static int isst_if_get_base_freq_info(void __user *argp) +{ + static struct isst_base_freq_info base_freq; + struct tpmi_per_power_domain_info *power_domain_info; + + if (copy_from_user(&base_freq, argp, sizeof(base_freq))) + return -EFAULT; + + power_domain_info = get_instance(base_freq.socket_id, base_freq.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (base_freq.level > power_domain_info->max_level) + return -EINVAL; + + _read_bf_level_info("p1_high", base_freq.high_base_freq_mhz, base_freq.level, + SST_BF_INFO_0_OFFSET, SST_BF_P1_HIGH_START, SST_BF_P1_HIGH_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_bf_level_info("p1_low", base_freq.low_base_freq_mhz, base_freq.level, + SST_BF_INFO_0_OFFSET, SST_BF_P1_LOW_START, SST_BF_P1_LOW_WIDTH, + SST_MUL_FACTOR_FREQ) + _read_bf_level_info("BF-TJ", base_freq.tjunction_max_c, base_freq.level, + SST_BF_INFO_0_OFFSET, SST_BF_T_PROHOT_START, SST_BF_T_PROHOT_WIDTH, + SST_MUL_FACTOR_NONE) + _read_bf_level_info("BF-tdp", base_freq.thermal_design_power_w, base_freq.level, + SST_BF_INFO_0_OFFSET, SST_BF_TDP_START, SST_BF_TDP_WIDTH, + SST_MUL_FACTOR_NONE) + base_freq.thermal_design_power_w /= 8; /*unit = 1/8th watt*/ + + if (copy_to_user(argp, &base_freq, sizeof(base_freq))) + return -EFAULT; + + return 0; +} + +#define P1_HI_CORE_MASK_START 0 +#define P1_HI_CORE_MASK_WIDTH 64 + +static int isst_if_get_base_freq_mask(void __user *argp) +{ + static struct isst_perf_level_cpu_mask cpumask; + struct tpmi_per_power_domain_info *power_domain_info; + u64 mask; + + if (copy_from_user(&cpumask, argp, sizeof(cpumask))) + return -EFAULT; + + power_domain_info = get_instance(cpumask.socket_id, cpumask.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + _read_bf_level_info("BF-cpumask", mask, cpumask.level, SST_BF_INFO_1_OFFSET, + P1_HI_CORE_MASK_START, P1_HI_CORE_MASK_WIDTH, + SST_MUL_FACTOR_NONE) + + cpumask.mask = mask; + + if (!cpumask.punit_cpu_map) + return -EOPNOTSUPP; + + if (copy_to_user(argp, &cpumask, sizeof(cpumask))) + return -EFAULT; + + return 0; +} + +static int isst_if_get_tpmi_instance_count(void __user *argp) +{ + struct isst_tpmi_instance_count tpmi_inst; + struct tpmi_sst_struct *sst_inst; + int i; + + if (copy_from_user(&tpmi_inst, argp, sizeof(tpmi_inst))) + return -EFAULT; + + if (tpmi_inst.socket_id >= topology_max_packages()) + return -EINVAL; + + sst_inst = isst_common.sst_inst[tpmi_inst.socket_id]; + + tpmi_inst.count = isst_instance_count(sst_inst); + + tpmi_inst.valid_mask = 0; + for (i = 0; i < tpmi_inst.count; i++) { + struct tpmi_per_power_domain_info *pd_info; + u8 part; + int pd; + + pd = map_partition_power_domain_id(sst_inst, i, &part); + if (pd < 0) + continue; + + pd_info = &sst_inst->power_domain_info[part][pd]; + if (pd_info->sst_base) + tpmi_inst.valid_mask |= BIT(i); + } + + if (!tpmi_inst.valid_mask) + tpmi_inst.count = 0; + + if (copy_to_user(argp, &tpmi_inst, sizeof(tpmi_inst))) + return -EFAULT; + + return 0; +} + +#define SST_TF_INFO_0_OFFSET 0 +#define SST_TF_INFO_1_OFFSET 8 +#define SST_TF_INFO_2_OFFSET 16 +#define SST_TF_INFO_8_OFFSET 64 +#define SST_TF_INFO_8_BUCKETS 3 + +#define SST_TF_MAX_LP_CLIP_RATIOS TRL_MAX_LEVELS + +#define SST_TF_FEATURE_REV_START 4 +#define SST_TF_FEATURE_REV_WIDTH 8 + +#define SST_TF_LP_CLIP_RATIO_0_START 16 +#define SST_TF_LP_CLIP_RATIO_0_WIDTH 8 + +#define SST_TF_RATIO_0_START 0 +#define SST_TF_RATIO_0_WIDTH 8 + +#define SST_TF_NUM_CORE_0_START 0 +#define SST_TF_NUM_CORE_0_WIDTH 8 + +#define SST_TF_NUM_MOD_0_START 0 +#define SST_TF_NUM_MOD_0_WIDTH 16 + +static int isst_if_get_turbo_freq_info(void __user *argp) +{ + static struct isst_turbo_freq_info turbo_freq; + struct tpmi_per_power_domain_info *power_domain_info; + u8 feature_rev; + int i, j; + + if (copy_from_user(&turbo_freq, argp, sizeof(turbo_freq))) + return -EFAULT; + + power_domain_info = get_instance(turbo_freq.socket_id, turbo_freq.power_domain_id); + if (!power_domain_info) + return -EINVAL; + + if (turbo_freq.level > power_domain_info->max_level) + return -EINVAL; + + turbo_freq.max_buckets = TRL_MAX_BUCKETS; + turbo_freq.max_trl_levels = TRL_MAX_LEVELS; + turbo_freq.max_clip_freqs = SST_TF_MAX_LP_CLIP_RATIOS; + + _read_tf_level_info("feature_rev", feature_rev, turbo_freq.level, + SST_TF_INFO_0_OFFSET, SST_TF_FEATURE_REV_START, + SST_TF_FEATURE_REV_WIDTH, SST_MUL_FACTOR_NONE); + + for (i = 0; i < turbo_freq.max_clip_freqs; ++i) + _read_tf_level_info("lp_clip*", turbo_freq.lp_clip_freq_mhz[i], + turbo_freq.level, SST_TF_INFO_0_OFFSET, + SST_TF_LP_CLIP_RATIO_0_START + + (i * SST_TF_LP_CLIP_RATIO_0_WIDTH), + SST_TF_LP_CLIP_RATIO_0_WIDTH, SST_MUL_FACTOR_FREQ) + + for (i = 0; i < TRL_MAX_LEVELS; ++i) { + for (j = 0; j < TRL_MAX_BUCKETS; ++j) + _read_tf_level_info("cydn*_bucket_*_trl", + turbo_freq.trl_freq_mhz[i][j], turbo_freq.level, + SST_TF_INFO_2_OFFSET + (i * SST_TF_RATIO_0_WIDTH), + j * SST_TF_RATIO_0_WIDTH, SST_TF_RATIO_0_WIDTH, + SST_MUL_FACTOR_FREQ) + } + + memset(turbo_freq.bucket_core_counts, 0, sizeof(turbo_freq.bucket_core_counts)); + + if (feature_rev >= 2) { + bool has_tf_info_8 = false; + + for (i = 0; i < SST_TF_INFO_8_BUCKETS; ++i) { + _read_tf_level_info("bucket_*_mod_count", turbo_freq.bucket_core_counts[i], + turbo_freq.level, SST_TF_INFO_8_OFFSET, + SST_TF_NUM_MOD_0_WIDTH * i, SST_TF_NUM_MOD_0_WIDTH, + SST_MUL_FACTOR_NONE) + + if (turbo_freq.bucket_core_counts[i]) + has_tf_info_8 = true; + } + + if (has_tf_info_8) + goto done_core_count; + } + + for (i = 0; i < TRL_MAX_BUCKETS; ++i) + _read_tf_level_info("bucket_*_core_count", turbo_freq.bucket_core_counts[i], + turbo_freq.level, SST_TF_INFO_1_OFFSET, + SST_TF_NUM_CORE_0_WIDTH * i, SST_TF_NUM_CORE_0_WIDTH, + SST_MUL_FACTOR_NONE) + + +done_core_count: + + if (copy_to_user(argp, &turbo_freq, sizeof(turbo_freq))) + return -EFAULT; + + return 0; +} + +static long isst_if_def_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + void __user *argp = (void __user *)arg; + long ret = -ENOTTY; + + mutex_lock(&isst_tpmi_dev_lock); + switch (cmd) { + case ISST_IF_COUNT_TPMI_INSTANCES: + ret = isst_if_get_tpmi_instance_count(argp); + break; + case ISST_IF_CORE_POWER_STATE: + ret = isst_if_core_power_state(argp); + break; + case ISST_IF_CLOS_PARAM: + ret = isst_if_clos_param(argp); + break; + case ISST_IF_CLOS_ASSOC: + ret = isst_if_clos_assoc(argp); + break; + case ISST_IF_PERF_LEVELS: + ret = isst_if_get_perf_level(argp); + break; + case ISST_IF_PERF_SET_LEVEL: + ret = isst_if_set_perf_level(argp); + break; + case ISST_IF_PERF_SET_FEATURE: + ret = isst_if_set_perf_feature(argp); + break; + case ISST_IF_GET_PERF_LEVEL_INFO: + ret = isst_if_get_perf_level_info(argp); + break; + case ISST_IF_GET_PERF_LEVEL_FABRIC_INFO: + ret = isst_if_get_perf_level_fabric_info(argp); + break; + case ISST_IF_GET_PERF_LEVEL_CPU_MASK: + ret = isst_if_get_perf_level_mask(argp); + break; + case ISST_IF_GET_BASE_FREQ_INFO: + ret = isst_if_get_base_freq_info(argp); + break; + case ISST_IF_GET_BASE_FREQ_CPU_MASK: + ret = isst_if_get_base_freq_mask(argp); + break; + case ISST_IF_GET_TURBO_FREQ_INFO: + ret = isst_if_get_turbo_freq_info(argp); + break; + default: + break; + } + mutex_unlock(&isst_tpmi_dev_lock); + + return ret; +} + +#define TPMI_SST_AUTO_SUSPEND_DELAY_MS 2000 + +int tpmi_sst_dev_add(struct auxiliary_device *auxdev) +{ + struct tpmi_per_power_domain_info *pd_info; + bool read_blocked = 0, write_blocked = 0; + struct oobmsm_plat_info *plat_info; + struct device *dev = &auxdev->dev; + struct tpmi_sst_struct *tpmi_sst; + u8 i, num_resources, io_die_cnt; + int ret, pkg = 0, inst = 0; + bool first_enum = false; + u16 cdie_mask; + u8 partition; + + ret = tpmi_get_feature_status(auxdev, TPMI_ID_SST, &read_blocked, &write_blocked); + if (ret) + dev_info(dev, "Can't read feature status: ignoring read/write blocked status\n"); + + if (read_blocked) { + dev_info(dev, "Firmware has blocked reads, exiting\n"); + return -ENODEV; + } + + plat_info = tpmi_get_platform_data(auxdev); + if (!plat_info) { + dev_err(dev, "No platform info\n"); + return -EINVAL; + } + + pkg = plat_info->package_id; + if (pkg >= topology_max_packages()) { + dev_err(dev, "Invalid package id :%x\n", pkg); + return -EINVAL; + } + + partition = plat_info->partition; + if (partition >= SST_MAX_PARTITIONS) { + dev_err(&auxdev->dev, "Invalid partition :%x\n", partition); + return -EINVAL; + } + + num_resources = tpmi_get_resource_count(auxdev); + + if (!num_resources) + return -EINVAL; + + mutex_lock(&isst_tpmi_dev_lock); + + if (isst_common.sst_inst[pkg]) { + tpmi_sst = isst_common.sst_inst[pkg]; + } else { + /* + * tpmi_sst instance is for a package. So needs to be + * allocated only once for both partitions. We can't use + * devm_* allocation here as each partition is a + * different device, which can be unbound. + */ + tpmi_sst = kzalloc_obj(*tpmi_sst); + if (!tpmi_sst) { + ret = -ENOMEM; + goto unlock_exit; + } + first_enum = true; + } + + ret = 0; + + pd_info = devm_kcalloc(dev, num_resources, sizeof(*pd_info), GFP_KERNEL); + if (!pd_info) { + ret = -ENOMEM; + goto unlock_free; + } + + /* Get the IO die count, if cdie_mask is present */ + if (plat_info->cdie_mask) { + u8 cdie_range; + + cdie_mask = plat_info->cdie_mask; + cdie_range = fls(cdie_mask) - ffs(cdie_mask) + 1; + io_die_cnt = num_resources - cdie_range; + } else { + /* + * This is a synthetic mask, careful when assuming that + * they are compute dies only. + */ + cdie_mask = (1 << num_resources) - 1; + io_die_cnt = 0; + } + + for (i = 0; i < num_resources; ++i) { + struct resource *res; + + res = tpmi_get_resource_at_index(auxdev, i); + if (!res) { + pd_info[i].sst_base = NULL; + continue; + } + + pd_info[i].package_id = pkg; + pd_info[i].power_domain_id = i; + pd_info[i].auxdev = auxdev; + pd_info[i].write_blocked = write_blocked; + pd_info[i].sst_base = devm_ioremap_resource(dev, res); + if (IS_ERR(pd_info[i].sst_base)) { + ret = PTR_ERR(pd_info[i].sst_base); + goto unlock_free; + } + + if (sst_main(auxdev, &pd_info[i])) { + /* + * This entry is not valid, hardware can partially + * populate dies. In this case MMIO will have 0xFFs. + * Also possible some pre-production hardware has + * invalid data. But don't fail and continue to use + * other dies with valid data. + */ + devm_iounmap(dev, pd_info[i].sst_base); + pd_info[i].sst_base = NULL; + continue; + } + + ++inst; + } + + if (!inst) { + ret = -ENODEV; + goto unlock_free; + } + + tpmi_sst->package_id = pkg; + + tpmi_sst->power_domain_info[partition] = pd_info; + tpmi_sst->number_of_power_domains[partition] = num_resources; + tpmi_sst->cdie_mask[partition] = cdie_mask; + tpmi_sst->io_dies[partition] = io_die_cnt; + tpmi_sst->partition_mask |= BIT(partition); + tpmi_sst->partition_mask_current |= BIT(partition); + + auxiliary_set_drvdata(auxdev, tpmi_sst); + + if (isst_common.max_index < pkg) + isst_common.max_index = pkg; + isst_common.sst_inst[pkg] = tpmi_sst; + +unlock_free: + if (ret && first_enum) + kfree(tpmi_sst); +unlock_exit: + mutex_unlock(&isst_tpmi_dev_lock); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_dev_add, "INTEL_TPMI_SST"); + +void tpmi_sst_dev_remove(struct auxiliary_device *auxdev) +{ + struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); + struct oobmsm_plat_info *plat_info; + + plat_info = tpmi_get_platform_data(auxdev); + if (!plat_info) + return; + + mutex_lock(&isst_tpmi_dev_lock); + tpmi_sst->power_domain_info[plat_info->partition] = NULL; + tpmi_sst->partition_mask_current &= ~BIT(plat_info->partition); + /* Free the package instance when the all partitions are removed */ + if (!tpmi_sst->partition_mask_current) { + isst_common.sst_inst[tpmi_sst->package_id] = NULL; + kfree(tpmi_sst); + } + mutex_unlock(&isst_tpmi_dev_lock); +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_dev_remove, "INTEL_TPMI_SST"); + +#define SST_PP_CAP_CP_ENABLE BIT(0) +#define SST_PP_CAP_PP_ENABLE BIT(1) + +void tpmi_sst_dev_suspend(struct auxiliary_device *auxdev) +{ + struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); + struct tpmi_per_power_domain_info *power_domain_info, *pd_info; + struct oobmsm_plat_info *plat_info; + void __iomem *cp_base; + int num_resources, i; + + plat_info = tpmi_get_platform_data(auxdev); + if (!plat_info) + return; + + power_domain_info = tpmi_sst->power_domain_info[plat_info->partition]; + num_resources = tpmi_sst->number_of_power_domains[plat_info->partition]; + + for (i = 0; i < num_resources; i++) { + pd_info = &power_domain_info[i]; + if (!pd_info || !pd_info->sst_base) + continue; + + if (!(pd_info->sst_header.cap_mask & SST_PP_CAP_CP_ENABLE)) + goto process_pp_suspend; + + cp_base = pd_info->sst_base + pd_info->sst_header.cp_offset; + pd_info->saved_sst_cp_control = readq(cp_base + SST_CP_CONTROL_OFFSET); + memcpy_fromio(pd_info->saved_clos_configs, cp_base + SST_CLOS_CONFIG_0_OFFSET, + sizeof(pd_info->saved_clos_configs)); + memcpy_fromio(pd_info->saved_clos_assocs, cp_base + SST_CLOS_ASSOC_0_OFFSET, + sizeof(pd_info->saved_clos_assocs)); + +process_pp_suspend: + if (!(pd_info->sst_header.cap_mask & SST_PP_CAP_PP_ENABLE)) + continue; + + pd_info->saved_pp_control = readq(pd_info->sst_base + + pd_info->sst_header.pp_offset + + SST_PP_CONTROL_OFFSET); + } +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_dev_suspend, "INTEL_TPMI_SST"); + +void tpmi_sst_dev_resume(struct auxiliary_device *auxdev) +{ + struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); + struct tpmi_per_power_domain_info *power_domain_info, *pd_info; + struct oobmsm_plat_info *plat_info; + void __iomem *cp_base; + int num_resources, i; + + plat_info = tpmi_get_platform_data(auxdev); + if (!plat_info) + return; + + power_domain_info = tpmi_sst->power_domain_info[plat_info->partition]; + num_resources = tpmi_sst->number_of_power_domains[plat_info->partition]; + + for (i = 0; i < num_resources; i++) { + pd_info = &power_domain_info[i]; + if (!pd_info || !pd_info->sst_base) + continue; + + if (!(pd_info->sst_header.cap_mask & SST_PP_CAP_CP_ENABLE)) + goto process_pp_resume; + + cp_base = pd_info->sst_base + pd_info->sst_header.cp_offset; + writeq(pd_info->saved_sst_cp_control, cp_base + SST_CP_CONTROL_OFFSET); + memcpy_toio(cp_base + SST_CLOS_CONFIG_0_OFFSET, pd_info->saved_clos_configs, + sizeof(pd_info->saved_clos_configs)); + memcpy_toio(cp_base + SST_CLOS_ASSOC_0_OFFSET, pd_info->saved_clos_assocs, + sizeof(pd_info->saved_clos_assocs)); + +process_pp_resume: + if (!(pd_info->sst_header.cap_mask & SST_PP_CAP_PP_ENABLE)) + continue; + + writeq(pd_info->saved_pp_control, pd_info->sst_base + + pd_info->sst_header.pp_offset + SST_PP_CONTROL_OFFSET); + } +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_dev_resume, "INTEL_TPMI_SST"); + +#define ISST_TPMI_API_VERSION 0x03 + +int tpmi_sst_init(void) +{ + struct isst_if_cmd_cb cb; + int ret = 0; + + mutex_lock(&isst_tpmi_dev_lock); + + if (isst_core_usage_count) { + ++isst_core_usage_count; + goto init_done; + } + + isst_common.sst_inst = kzalloc_objs(*isst_common.sst_inst, + topology_max_packages()); + if (!isst_common.sst_inst) { + ret = -ENOMEM; + goto init_done; + } + + memset(&cb, 0, sizeof(cb)); + cb.cmd_size = sizeof(struct isst_if_io_reg); + cb.offset = offsetof(struct isst_if_io_regs, io_reg); + cb.cmd_callback = NULL; + cb.api_version = ISST_TPMI_API_VERSION; + cb.def_ioctl = isst_if_def_ioctl; + cb.owner = THIS_MODULE; + ret = isst_if_cdev_register(ISST_IF_DEV_TPMI, &cb); + if (ret) + kfree(isst_common.sst_inst); + else + ++isst_core_usage_count; +init_done: + mutex_unlock(&isst_tpmi_dev_lock); + return ret; +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_init, "INTEL_TPMI_SST"); + +void tpmi_sst_exit(void) +{ + mutex_lock(&isst_tpmi_dev_lock); + if (isst_core_usage_count) + --isst_core_usage_count; + + if (!isst_core_usage_count) { + isst_if_cdev_unregister(ISST_IF_DEV_TPMI); + kfree(isst_common.sst_inst); + } + mutex_unlock(&isst_tpmi_dev_lock); +} +EXPORT_SYMBOL_NS_GPL(tpmi_sst_exit, "INTEL_TPMI_SST"); + +MODULE_IMPORT_NS("INTEL_TPMI"); +MODULE_IMPORT_NS("INTEL_TPMI_POWER_DOMAIN"); + +MODULE_DESCRIPTION("ISST TPMI interface module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.h b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.h new file mode 100644 index 000000000..900b48370 --- /dev/null +++ b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Intel Speed Select Interface: Drivers Internal defines + * Copyright (c) 2023, Intel Corporation. + * All rights reserved. + * + */ + +#ifndef _ISST_TPMI_CORE_H +#define _ISST_TPMI_CORE_H + +int tpmi_sst_init(void); +void tpmi_sst_exit(void); +int tpmi_sst_dev_add(struct auxiliary_device *auxdev); +void tpmi_sst_dev_remove(struct auxiliary_device *auxdev); +void tpmi_sst_dev_suspend(struct auxiliary_device *auxdev); +void tpmi_sst_dev_resume(struct auxiliary_device *auxdev); +#endif diff --git a/drivers/platform/x86/intel/telemetry/Kconfig b/drivers/platform/x86/intel/telemetry/Kconfig new file mode 100644 index 000000000..da887bd03 --- /dev/null +++ b/drivers/platform/x86/intel/telemetry/Kconfig @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +config INTEL_TELEMETRY + tristate "Intel SoC Telemetry driver" + depends on X86_64 + depends on MFD_INTEL_PMC_BXT + depends on INTEL_PUNIT_IPC + help + This driver provides interfaces to configure and use + telemetry for Intel SoC from Apollo Lake onwards. + It is also used to get various SoC events and parameters + directly via debugfs files. Various tools may use + this interface for SoC state monitoring. diff --git a/drivers/platform/x86/intel/telemetry/Makefile b/drivers/platform/x86/intel/telemetry/Makefile new file mode 100644 index 000000000..bfdba5b6c --- /dev/null +++ b/drivers/platform/x86/intel/telemetry/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +intel_telemetry_core-y := core.o +obj-$(CONFIG_INTEL_TELEMETRY) += intel_telemetry_core.o +intel_telemetry_pltdrv-y := pltdrv.o +obj-$(CONFIG_INTEL_TELEMETRY) += intel_telemetry_pltdrv.o +intel_telemetry_debugfs-y := debugfs.o +obj-$(CONFIG_INTEL_TELEMETRY) += intel_telemetry_debugfs.o diff --git a/drivers/platform/x86/intel/telemetry/core.c b/drivers/platform/x86/intel/telemetry/core.c new file mode 100644 index 000000000..f312864b8 --- /dev/null +++ b/drivers/platform/x86/intel/telemetry/core.c @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel SoC Core Telemetry Driver + * Copyright (C) 2015, Intel Corporation. + * All Rights Reserved. + * + * Telemetry Framework provides platform related PM and performance statistics. + * This file provides the core telemetry API implementation. + */ +#include +#include + +#include + +#define DRIVER_NAME "intel_telemetry_core" + +struct telemetry_core_config { + struct telemetry_plt_config *plt_config; + const struct telemetry_core_ops *telem_ops; +}; + +static struct telemetry_core_config telm_core_conf; + +static int telemetry_def_get_trace_verbosity(enum telemetry_unit telem_unit, + u32 *verbosity) +{ + return 0; +} + + +static int telemetry_def_set_trace_verbosity(enum telemetry_unit telem_unit, + u32 verbosity) +{ + return 0; +} + +static int telemetry_def_raw_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, + int len, int log_all_evts) +{ + return 0; +} + +static int telemetry_def_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, + int len, int log_all_evts) +{ + return 0; +} + +static const struct telemetry_core_ops telm_defpltops = { + .get_trace_verbosity = telemetry_def_get_trace_verbosity, + .set_trace_verbosity = telemetry_def_set_trace_verbosity, + .raw_read_eventlog = telemetry_def_raw_read_eventlog, + .read_eventlog = telemetry_def_read_eventlog, +}; + +/** + * telemetry_read_events() - Fetches samples as specified by evtlog.telem_evt_id + * @telem_unit: Specify whether IOSS or PSS Read + * @evtlog: Array of telemetry_evtlog structs to fill data + * evtlog.telem_evt_id specifies the ids to read + * @len: Length of array of evtlog + * + * Return: number of eventlogs read for success, < 0 for failure + */ +int telemetry_read_events(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, int len) +{ + return telm_core_conf.telem_ops->read_eventlog(telem_unit, evtlog, + len, 0); +} +EXPORT_SYMBOL_GPL(telemetry_read_events); + +/** + * telemetry_read_eventlog() - Fetch the Telemetry log from PSS or IOSS + * @telem_unit: Specify whether IOSS or PSS Read + * @evtlog: Array of telemetry_evtlog structs to fill data + * @len: Length of array of evtlog + * + * Return: number of eventlogs read for success, < 0 for failure + */ +int telemetry_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, int len) +{ + return telm_core_conf.telem_ops->read_eventlog(telem_unit, evtlog, + len, 1); +} +EXPORT_SYMBOL_GPL(telemetry_read_eventlog); + +/** + * telemetry_raw_read_eventlog() - Fetch the Telemetry log from PSS or IOSS + * @telem_unit: Specify whether IOSS or PSS Read + * @evtlog: Array of telemetry_evtlog structs to fill data + * @len: Length of array of evtlog + * + * The caller must take care of locking in this case. + * + * Return: number of eventlogs read for success, < 0 for failure + */ +int telemetry_raw_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, int len) +{ + return telm_core_conf.telem_ops->raw_read_eventlog(telem_unit, evtlog, + len, 1); +} +EXPORT_SYMBOL_GPL(telemetry_raw_read_eventlog); + + +/** + * telemetry_get_trace_verbosity() - Get the IOSS & PSS Trace verbosity + * @telem_unit: Specify whether IOSS or PSS Read + * @verbosity: Pointer to return Verbosity + * + * Return: 0 success, < 0 for failure + */ +int telemetry_get_trace_verbosity(enum telemetry_unit telem_unit, + u32 *verbosity) +{ + return telm_core_conf.telem_ops->get_trace_verbosity(telem_unit, + verbosity); +} +EXPORT_SYMBOL_GPL(telemetry_get_trace_verbosity); + + +/** + * telemetry_set_trace_verbosity() - Update the IOSS & PSS Trace verbosity + * @telem_unit: Specify whether IOSS or PSS Read + * @verbosity: Verbosity to set + * + * Return: 0 success, < 0 for failure + */ +int telemetry_set_trace_verbosity(enum telemetry_unit telem_unit, u32 verbosity) +{ + return telm_core_conf.telem_ops->set_trace_verbosity(telem_unit, + verbosity); +} +EXPORT_SYMBOL_GPL(telemetry_set_trace_verbosity); + +/** + * telemetry_set_pltdata() - Set the platform specific Data + * @ops: Pointer to ops structure + * @pltconfig: Platform config data + * + * Usage by other than telemetry pltdrv module is invalid + * + * Return: 0 success, < 0 for failure + */ +int telemetry_set_pltdata(const struct telemetry_core_ops *ops, + struct telemetry_plt_config *pltconfig) +{ + if (ops) + telm_core_conf.telem_ops = ops; + + if (pltconfig) + telm_core_conf.plt_config = pltconfig; + + return 0; +} +EXPORT_SYMBOL_GPL(telemetry_set_pltdata); + +/** + * telemetry_clear_pltdata() - Clear the platform specific Data + * + * Usage by other than telemetry pltdrv module is invalid + * + * Return: 0 success, < 0 for failure + */ +int telemetry_clear_pltdata(void) +{ + telm_core_conf.telem_ops = &telm_defpltops; + telm_core_conf.plt_config = NULL; + + return 0; +} +EXPORT_SYMBOL_GPL(telemetry_clear_pltdata); + +/** + * telemetry_get_pltdata() - Return telemetry platform config + * + * May be used by other telemetry modules to get platform specific + * configuration. + */ +struct telemetry_plt_config *telemetry_get_pltdata(void) +{ + return telm_core_conf.plt_config; +} +EXPORT_SYMBOL_GPL(telemetry_get_pltdata); + +static inline int telemetry_get_pssevtname(enum telemetry_unit telem_unit, + const char **name, int len) +{ + struct telemetry_unit_config psscfg; + int i; + + if (!telm_core_conf.plt_config) + return -EINVAL; + + psscfg = telm_core_conf.plt_config->pss_config; + + if (len > psscfg.ssram_evts_used) + len = psscfg.ssram_evts_used; + + for (i = 0; i < len; i++) + name[i] = psscfg.telem_evts[i].name; + + return 0; +} + +static inline int telemetry_get_iossevtname(enum telemetry_unit telem_unit, + const char **name, int len) +{ + struct telemetry_unit_config iosscfg; + int i; + + if (!(telm_core_conf.plt_config)) + return -EINVAL; + + iosscfg = telm_core_conf.plt_config->ioss_config; + + if (len > iosscfg.ssram_evts_used) + len = iosscfg.ssram_evts_used; + + for (i = 0; i < len; i++) + name[i] = iosscfg.telem_evts[i].name; + + return 0; + +} + +/** + * telemetry_get_evtname() - Checkif platform config is valid + * @telem_unit: Telemetry Unit to check + * @name: Array of character pointers to contain name + * @len: length of array name provided by user + * + * Usage by other than telemetry debugfs module is invalid + * + * Return: 0 success, < 0 for failure + */ +int telemetry_get_evtname(enum telemetry_unit telem_unit, + const char **name, int len) +{ + int ret = -EINVAL; + + if (telem_unit == TELEM_PSS) + ret = telemetry_get_pssevtname(telem_unit, name, len); + + else if (telem_unit == TELEM_IOSS) + ret = telemetry_get_iossevtname(telem_unit, name, len); + + return ret; +} +EXPORT_SYMBOL_GPL(telemetry_get_evtname); + +static int __init telemetry_module_init(void) +{ + pr_info(pr_fmt(DRIVER_NAME) " Init\n"); + + telm_core_conf.telem_ops = &telm_defpltops; + return 0; +} + +static void __exit telemetry_module_exit(void) +{ +} + +module_init(telemetry_module_init); +module_exit(telemetry_module_exit); + +MODULE_AUTHOR("Souvik Kumar Chakravarty "); +MODULE_DESCRIPTION("Intel SoC Telemetry Interface"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/telemetry/debugfs.c b/drivers/platform/x86/intel/telemetry/debugfs.c new file mode 100644 index 000000000..189c61ff7 --- /dev/null +++ b/drivers/platform/x86/intel/telemetry/debugfs.c @@ -0,0 +1,961 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel SOC Telemetry debugfs Driver: Currently supports APL + * Copyright (c) 2015, Intel Corporation. + * All Rights Reserved. + * + * This file provides the debugfs interfaces for telemetry. + * /sys/kernel/debug/telemetry/pss_info: Shows Primary Control Sub-Sys Counters + * /sys/kernel/debug/telemetry/ioss_info: Shows IO Sub-System Counters + * /sys/kernel/debug/telemetry/soc_states: Shows SoC State + * /sys/kernel/debug/telemetry/pss_trace_verbosity: Read and Change Tracing + * Verbosity via firmware + * /sys/kernel/debug/telemetry/ioss_race_verbosity: Write and Change Tracing + * Verbosity via firmware + */ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define DRIVER_NAME "telemetry_soc_debugfs" +#define DRIVER_VERSION "1.0.0" + +/* ApolloLake SoC Event-IDs */ +#define TELEM_APL_PSS_PSTATES_ID 0x2802 +#define TELEM_APL_PSS_IDLE_ID 0x2806 +#define TELEM_APL_PCS_IDLE_BLOCKED_ID 0x2C00 +#define TELEM_APL_PCS_S0IX_BLOCKED_ID 0x2C01 +#define TELEM_APL_PSS_WAKEUP_ID 0x2C02 +#define TELEM_APL_PSS_LTR_BLOCKING_ID 0x2C03 + +#define TELEM_APL_S0IX_TOTAL_OCC_ID 0x4000 +#define TELEM_APL_S0IX_SHLW_OCC_ID 0x4001 +#define TELEM_APL_S0IX_DEEP_OCC_ID 0x4002 +#define TELEM_APL_S0IX_TOTAL_RES_ID 0x4800 +#define TELEM_APL_S0IX_SHLW_RES_ID 0x4801 +#define TELEM_APL_S0IX_DEEP_RES_ID 0x4802 +#define TELEM_APL_D0IX_ID 0x581A +#define TELEM_APL_D3_ID 0x5819 +#define TELEM_APL_PG_ID 0x5818 + +#define TELEM_INFO_SRAMEVTS_MASK 0xFF00 +#define TELEM_INFO_SRAMEVTS_SHIFT 0x8 +#define TELEM_SSRAM_READ_TIMEOUT 10 + +#define TELEM_MASK_BIT 1 +#define TELEM_MASK_BYTE 0xFF +#define BYTES_PER_LONG 8 +#define TELEM_APL_MASK_PCS_STATE 0xF + +/* Max events in bitmap to check for */ +#define TELEM_PSS_IDLE_EVTS 25 +#define TELEM_PSS_IDLE_BLOCKED_EVTS 20 +#define TELEM_PSS_S0IX_BLOCKED_EVTS 20 +#define TELEM_PSS_S0IX_WAKEUP_EVTS 20 +#define TELEM_PSS_LTR_BLOCKING_EVTS 20 +#define TELEM_IOSS_DX_D0IX_EVTS 25 +#define TELEM_IOSS_PG_EVTS 30 + +#define TELEM_CHECK_AND_PARSE_EVTS(EVTID, EVTNUM, BUF, EVTLOG, EVTDAT, MASK) { \ + if (evtlog[index].telem_evtid == (EVTID)) { \ + for (idx = 0; idx < (EVTNUM); idx++) \ + (BUF)[idx] = ((EVTLOG) >> (EVTDAT)[idx].bit_pos) & \ + (MASK); \ + continue; \ + } \ +} + +#define TELEM_CHECK_AND_PARSE_CTRS(EVTID, CTR) { \ + if (evtlog[index].telem_evtid == (EVTID)) { \ + (CTR) = evtlog[index].telem_evtlog; \ + continue; \ + } \ +} + +static u8 suspend_prep_ok; +static u32 suspend_shlw_ctr_temp, suspend_deep_ctr_temp; +static u64 suspend_shlw_res_temp, suspend_deep_res_temp; + +struct telemetry_susp_stats { + u32 shlw_ctr; + u32 deep_ctr; + u64 shlw_res; + u64 deep_res; +}; + +/* Bitmap definitions for default counters in APL */ +struct telem_pss_idle_stateinfo { + const char *name; + u32 bit_pos; +}; + +static struct telem_pss_idle_stateinfo telem_apl_pss_idle_data[] = { + {"IA_CORE0_C1E", 0}, + {"IA_CORE1_C1E", 1}, + {"IA_CORE2_C1E", 2}, + {"IA_CORE3_C1E", 3}, + {"IA_CORE0_C6", 16}, + {"IA_CORE1_C6", 17}, + {"IA_CORE2_C6", 18}, + {"IA_CORE3_C6", 19}, + {"IA_MODULE0_C7", 32}, + {"IA_MODULE1_C7", 33}, + {"GT_RC6", 40}, + {"IUNIT_PROCESSING_IDLE", 41}, + {"FAR_MEM_IDLE", 43}, + {"DISPLAY_IDLE", 44}, + {"IUNIT_INPUT_SYSTEM_IDLE", 45}, + {"PCS_STATUS", 60}, +}; + +struct telem_pcs_blkd_info { + const char *name; + u32 bit_pos; +}; + +static struct telem_pcs_blkd_info telem_apl_pcs_idle_blkd_data[] = { + {"COMPUTE", 0}, + {"MISC", 8}, + {"MODULE_ACTIONS_PENDING", 16}, + {"LTR", 24}, + {"DISPLAY_WAKE", 32}, + {"ISP_WAKE", 40}, + {"PSF0_ACTIVE", 48}, +}; + +static struct telem_pcs_blkd_info telem_apl_pcs_s0ix_blkd_data[] = { + {"LTR", 0}, + {"IRTL", 8}, + {"WAKE_DEADLINE_PENDING", 16}, + {"DISPLAY", 24}, + {"ISP", 32}, + {"CORE", 40}, + {"PMC", 48}, + {"MISC", 56}, +}; + +struct telem_pss_ltr_info { + const char *name; + u32 bit_pos; +}; + +static struct telem_pss_ltr_info telem_apl_pss_ltr_data[] = { + {"CORE_ACTIVE", 0}, + {"MEM_UP", 8}, + {"DFX", 16}, + {"DFX_FORCE_LTR", 24}, + {"DISPLAY", 32}, + {"ISP", 40}, + {"SOUTH", 48}, +}; + +struct telem_pss_wakeup_info { + const char *name; + u32 bit_pos; +}; + +static struct telem_pss_wakeup_info telem_apl_pss_wakeup[] = { + {"IP_IDLE", 0}, + {"DISPLAY_WAKE", 8}, + {"VOLTAGE_REG_INT", 16}, + {"DROWSY_TIMER (HOTPLUG)", 24}, + {"CORE_WAKE", 32}, + {"MISC_S0IX", 40}, + {"MISC_ABORT", 56}, +}; + +struct telem_ioss_d0ix_stateinfo { + const char *name; + u32 bit_pos; +}; + +static struct telem_ioss_d0ix_stateinfo telem_apl_ioss_d0ix_data[] = { + {"CSE", 0}, + {"SCC2", 1}, + {"GMM", 2}, + {"XDCI", 3}, + {"XHCI", 4}, + {"ISH", 5}, + {"AVS", 6}, + {"PCIE0P1", 7}, + {"PECI0P0", 8}, + {"LPSS", 9}, + {"SCC", 10}, + {"PWM", 11}, + {"PCIE1_P3", 12}, + {"PCIE1_P2", 13}, + {"PCIE1_P1", 14}, + {"PCIE1_P0", 15}, + {"CNV", 16}, + {"SATA", 17}, + {"PRTC", 18}, +}; + +struct telem_ioss_pg_info { + const char *name; + u32 bit_pos; +}; + +static struct telem_ioss_pg_info telem_apl_ioss_pg_data[] = { + {"LPSS", 0}, + {"SCC", 1}, + {"P2SB", 2}, + {"SCC2", 3}, + {"GMM", 4}, + {"PCIE0", 5}, + {"XDCI", 6}, + {"xHCI", 7}, + {"CSE", 8}, + {"SPI", 9}, + {"AVSPGD4", 10}, + {"AVSPGD3", 11}, + {"AVSPGD2", 12}, + {"AVSPGD1", 13}, + {"ISH", 14}, + {"EXI", 15}, + {"NPKVRC", 16}, + {"NPKVNN", 17}, + {"CUNIT", 18}, + {"FUSE_CTRL", 19}, + {"PCIE1", 20}, + {"CNV", 21}, + {"LPC", 22}, + {"SATA", 23}, + {"SMB", 24}, + {"PRTC", 25}, +}; + +struct telemetry_debugfs_conf { + struct telemetry_susp_stats suspend_stats; + struct dentry *telemetry_dbg_dir; + + /* Bitmap Data */ + struct telem_ioss_d0ix_stateinfo *ioss_d0ix_data; + struct telem_pss_idle_stateinfo *pss_idle_data; + struct telem_pcs_blkd_info *pcs_idle_blkd_data; + struct telem_pcs_blkd_info *pcs_s0ix_blkd_data; + struct telem_pss_wakeup_info *pss_wakeup; + struct telem_pss_ltr_info *pss_ltr_data; + struct telem_ioss_pg_info *ioss_pg_data; + u8 pcs_idle_blkd_evts; + u8 pcs_s0ix_blkd_evts; + u8 pss_wakeup_evts; + u8 pss_idle_evts; + u8 pss_ltr_evts; + u8 ioss_d0ix_evts; + u8 ioss_pg_evts; + + /* IDs */ + u16 pss_ltr_blocking_id; + u16 pcs_idle_blkd_id; + u16 pcs_s0ix_blkd_id; + u16 s0ix_total_occ_id; + u16 s0ix_shlw_occ_id; + u16 s0ix_deep_occ_id; + u16 s0ix_total_res_id; + u16 s0ix_shlw_res_id; + u16 s0ix_deep_res_id; + u16 pss_wakeup_id; + u16 ioss_d0ix_id; + u16 pstates_id; + u16 pss_idle_id; + u16 ioss_d3_id; + u16 ioss_pg_id; +}; + +static struct telemetry_debugfs_conf *debugfs_conf; + +static struct telemetry_debugfs_conf telem_apl_debugfs_conf = { + .pss_idle_data = telem_apl_pss_idle_data, + .pcs_idle_blkd_data = telem_apl_pcs_idle_blkd_data, + .pcs_s0ix_blkd_data = telem_apl_pcs_s0ix_blkd_data, + .pss_ltr_data = telem_apl_pss_ltr_data, + .pss_wakeup = telem_apl_pss_wakeup, + .ioss_d0ix_data = telem_apl_ioss_d0ix_data, + .ioss_pg_data = telem_apl_ioss_pg_data, + + .pss_idle_evts = ARRAY_SIZE(telem_apl_pss_idle_data), + .pcs_idle_blkd_evts = ARRAY_SIZE(telem_apl_pcs_idle_blkd_data), + .pcs_s0ix_blkd_evts = ARRAY_SIZE(telem_apl_pcs_s0ix_blkd_data), + .pss_ltr_evts = ARRAY_SIZE(telem_apl_pss_ltr_data), + .pss_wakeup_evts = ARRAY_SIZE(telem_apl_pss_wakeup), + .ioss_d0ix_evts = ARRAY_SIZE(telem_apl_ioss_d0ix_data), + .ioss_pg_evts = ARRAY_SIZE(telem_apl_ioss_pg_data), + + .pstates_id = TELEM_APL_PSS_PSTATES_ID, + .pss_idle_id = TELEM_APL_PSS_IDLE_ID, + .pcs_idle_blkd_id = TELEM_APL_PCS_IDLE_BLOCKED_ID, + .pcs_s0ix_blkd_id = TELEM_APL_PCS_S0IX_BLOCKED_ID, + .pss_wakeup_id = TELEM_APL_PSS_WAKEUP_ID, + .pss_ltr_blocking_id = TELEM_APL_PSS_LTR_BLOCKING_ID, + .s0ix_total_occ_id = TELEM_APL_S0IX_TOTAL_OCC_ID, + .s0ix_shlw_occ_id = TELEM_APL_S0IX_SHLW_OCC_ID, + .s0ix_deep_occ_id = TELEM_APL_S0IX_DEEP_OCC_ID, + .s0ix_total_res_id = TELEM_APL_S0IX_TOTAL_RES_ID, + .s0ix_shlw_res_id = TELEM_APL_S0IX_SHLW_RES_ID, + .s0ix_deep_res_id = TELEM_APL_S0IX_DEEP_RES_ID, + .ioss_d0ix_id = TELEM_APL_D0IX_ID, + .ioss_d3_id = TELEM_APL_D3_ID, + .ioss_pg_id = TELEM_APL_PG_ID, +}; + +static const struct x86_cpu_id telemetry_debugfs_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT, &telem_apl_debugfs_conf), + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT_PLUS, &telem_apl_debugfs_conf), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, telemetry_debugfs_cpu_ids); + +static int telemetry_debugfs_check_evts(void) +{ + if ((debugfs_conf->pss_idle_evts > TELEM_PSS_IDLE_EVTS) || + (debugfs_conf->pcs_idle_blkd_evts > TELEM_PSS_IDLE_BLOCKED_EVTS) || + (debugfs_conf->pcs_s0ix_blkd_evts > TELEM_PSS_S0IX_BLOCKED_EVTS) || + (debugfs_conf->pss_ltr_evts > TELEM_PSS_LTR_BLOCKING_EVTS) || + (debugfs_conf->pss_wakeup_evts > TELEM_PSS_S0IX_WAKEUP_EVTS) || + (debugfs_conf->ioss_d0ix_evts > TELEM_IOSS_DX_D0IX_EVTS) || + (debugfs_conf->ioss_pg_evts > TELEM_IOSS_PG_EVTS)) + return -EINVAL; + + return 0; +} + +static int telem_pss_states_show(struct seq_file *s, void *unused) +{ + struct telemetry_evtlog evtlog[TELEM_MAX_OS_ALLOCATED_EVENTS]; + struct telemetry_debugfs_conf *conf = debugfs_conf; + const char *name[TELEM_MAX_OS_ALLOCATED_EVENTS]; + u32 pcs_idle_blkd[TELEM_PSS_IDLE_BLOCKED_EVTS], + pcs_s0ix_blkd[TELEM_PSS_S0IX_BLOCKED_EVTS], + pss_s0ix_wakeup[TELEM_PSS_S0IX_WAKEUP_EVTS], + pss_ltr_blkd[TELEM_PSS_LTR_BLOCKING_EVTS], + pss_idle[TELEM_PSS_IDLE_EVTS]; + int index, idx, ret, err = 0; + u64 pstates = 0; + + ret = telemetry_read_eventlog(TELEM_PSS, evtlog, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (ret < 0) + return ret; + + err = telemetry_get_evtname(TELEM_PSS, name, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (err < 0) + return err; + + seq_puts(s, "\n----------------------------------------------------\n"); + seq_puts(s, "\tPSS TELEM EVENTLOG (Residency = field/19.2 us\n"); + seq_puts(s, "----------------------------------------------------\n"); + for (index = 0; index < ret; index++) { + seq_printf(s, "%-32s %llu\n", + name[index], evtlog[index].telem_evtlog); + + /* Fetch PSS IDLE State */ + if (evtlog[index].telem_evtid == conf->pss_idle_id) { + pss_idle[conf->pss_idle_evts - 1] = + (evtlog[index].telem_evtlog >> + conf->pss_idle_data[conf->pss_idle_evts - 1].bit_pos) & + TELEM_APL_MASK_PCS_STATE; + } + + TELEM_CHECK_AND_PARSE_EVTS(conf->pss_idle_id, + conf->pss_idle_evts - 1, + pss_idle, evtlog[index].telem_evtlog, + conf->pss_idle_data, TELEM_MASK_BIT); + + TELEM_CHECK_AND_PARSE_EVTS(conf->pcs_idle_blkd_id, + conf->pcs_idle_blkd_evts, + pcs_idle_blkd, + evtlog[index].telem_evtlog, + conf->pcs_idle_blkd_data, + TELEM_MASK_BYTE); + + TELEM_CHECK_AND_PARSE_EVTS(conf->pcs_s0ix_blkd_id, + conf->pcs_s0ix_blkd_evts, + pcs_s0ix_blkd, + evtlog[index].telem_evtlog, + conf->pcs_s0ix_blkd_data, + TELEM_MASK_BYTE); + + TELEM_CHECK_AND_PARSE_EVTS(conf->pss_wakeup_id, + conf->pss_wakeup_evts, + pss_s0ix_wakeup, + evtlog[index].telem_evtlog, + conf->pss_wakeup, TELEM_MASK_BYTE); + + TELEM_CHECK_AND_PARSE_EVTS(conf->pss_ltr_blocking_id, + conf->pss_ltr_evts, pss_ltr_blkd, + evtlog[index].telem_evtlog, + conf->pss_ltr_data, TELEM_MASK_BYTE); + + if (evtlog[index].telem_evtid == debugfs_conf->pstates_id) + pstates = evtlog[index].telem_evtlog; + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "PStates\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Domain\t\t\t\tFreq(Mhz)\n"); + seq_printf(s, " IA\t\t\t\t %llu\n GT\t\t\t\t %llu\n", + (pstates & TELEM_MASK_BYTE)*100, + ((pstates >> 8) & TELEM_MASK_BYTE)*50/3); + + seq_printf(s, " IUNIT\t\t\t\t %llu\n SA\t\t\t\t %llu\n", + ((pstates >> 16) & TELEM_MASK_BYTE)*25, + ((pstates >> 24) & TELEM_MASK_BYTE)*50/3); + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "PSS IDLE Status\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Device\t\t\t\t\tIDLE\n"); + for (index = 0; index < debugfs_conf->pss_idle_evts; index++) { + seq_printf(s, "%-32s\t%u\n", + debugfs_conf->pss_idle_data[index].name, + pss_idle[index]); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "PSS Idle blkd Status (~1ms saturating bucket)\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Blocker\t\t\t\t\tCount\n"); + for (index = 0; index < debugfs_conf->pcs_idle_blkd_evts; index++) { + seq_printf(s, "%-32s\t%u\n", + debugfs_conf->pcs_idle_blkd_data[index].name, + pcs_idle_blkd[index]); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "PSS S0ix blkd Status (~1ms saturating bucket)\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Blocker\t\t\t\t\tCount\n"); + for (index = 0; index < debugfs_conf->pcs_s0ix_blkd_evts; index++) { + seq_printf(s, "%-32s\t%u\n", + debugfs_conf->pcs_s0ix_blkd_data[index].name, + pcs_s0ix_blkd[index]); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "LTR Blocking Status (~1ms saturating bucket)\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Blocker\t\t\t\t\tCount\n"); + for (index = 0; index < debugfs_conf->pss_ltr_evts; index++) { + seq_printf(s, "%-32s\t%u\n", + debugfs_conf->pss_ltr_data[index].name, + pss_ltr_blkd[index]); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "Wakes Status (~1ms saturating bucket)\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Wakes\t\t\t\t\tCount\n"); + for (index = 0; index < debugfs_conf->pss_wakeup_evts; index++) { + seq_printf(s, "%-32s\t%u\n", + debugfs_conf->pss_wakeup[index].name, + pss_s0ix_wakeup[index]); + } + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(telem_pss_states); + +static int telem_ioss_states_show(struct seq_file *s, void *unused) +{ + struct telemetry_evtlog evtlog[TELEM_MAX_OS_ALLOCATED_EVENTS]; + const char *name[TELEM_MAX_OS_ALLOCATED_EVENTS]; + int index, ret, err; + + ret = telemetry_read_eventlog(TELEM_IOSS, evtlog, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (ret < 0) + return ret; + + err = telemetry_get_evtname(TELEM_IOSS, name, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (err < 0) + return err; + + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "\tI0SS TELEMETRY EVENTLOG\n"); + seq_puts(s, "--------------------------------------\n"); + for (index = 0; index < ret; index++) { + seq_printf(s, "%-32s 0x%llx\n", + name[index], evtlog[index].telem_evtlog); + } + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(telem_ioss_states); + +static int telem_soc_states_show(struct seq_file *s, void *unused) +{ + u32 d3_sts[TELEM_IOSS_DX_D0IX_EVTS], d0ix_sts[TELEM_IOSS_DX_D0IX_EVTS]; + u32 pg_sts[TELEM_IOSS_PG_EVTS], pss_idle[TELEM_PSS_IDLE_EVTS]; + struct telemetry_evtlog evtlog[TELEM_MAX_OS_ALLOCATED_EVENTS]; + u32 s0ix_total_ctr = 0, s0ix_shlw_ctr = 0, s0ix_deep_ctr = 0; + u64 s0ix_total_res = 0, s0ix_shlw_res = 0, s0ix_deep_res = 0; + struct telemetry_debugfs_conf *conf = debugfs_conf; + struct pci_dev *dev = NULL; + int index, idx, ret; + u32 d3_state; + u16 pmcsr; + + ret = telemetry_read_eventlog(TELEM_IOSS, evtlog, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (ret < 0) + return ret; + + for (index = 0; index < ret; index++) { + TELEM_CHECK_AND_PARSE_EVTS(conf->ioss_d3_id, + conf->ioss_d0ix_evts, + d3_sts, evtlog[index].telem_evtlog, + conf->ioss_d0ix_data, + TELEM_MASK_BIT); + + TELEM_CHECK_AND_PARSE_EVTS(conf->ioss_pg_id, conf->ioss_pg_evts, + pg_sts, evtlog[index].telem_evtlog, + conf->ioss_pg_data, TELEM_MASK_BIT); + + TELEM_CHECK_AND_PARSE_EVTS(conf->ioss_d0ix_id, + conf->ioss_d0ix_evts, + d0ix_sts, evtlog[index].telem_evtlog, + conf->ioss_d0ix_data, + TELEM_MASK_BIT); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_total_occ_id, + s0ix_total_ctr); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_occ_id, + s0ix_shlw_ctr); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_occ_id, + s0ix_deep_ctr); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_total_res_id, + s0ix_total_res); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_res_id, + s0ix_shlw_res); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_res_id, + s0ix_deep_res); + } + + seq_puts(s, "\n---------------------------------------------------\n"); + seq_puts(s, "S0IX Type\t\t\t Occurrence\t\t Residency(us)\n"); + seq_puts(s, "---------------------------------------------------\n"); + + seq_printf(s, "S0IX Shallow\t\t\t %10u\t %10llu\n", + s0ix_shlw_ctr - + conf->suspend_stats.shlw_ctr, + (u64)((s0ix_shlw_res - + conf->suspend_stats.shlw_res)*10/192)); + + seq_printf(s, "S0IX Deep\t\t\t %10u\t %10llu\n", + s0ix_deep_ctr - + conf->suspend_stats.deep_ctr, + (u64)((s0ix_deep_res - + conf->suspend_stats.deep_res)*10/192)); + + seq_printf(s, "Suspend(With S0ixShallow)\t %10u\t %10llu\n", + conf->suspend_stats.shlw_ctr, + (u64)(conf->suspend_stats.shlw_res*10)/192); + + seq_printf(s, "Suspend(With S0ixDeep)\t\t %10u\t %10llu\n", + conf->suspend_stats.deep_ctr, + (u64)(conf->suspend_stats.deep_res*10)/192); + + seq_printf(s, "TOTAL S0IX\t\t\t %10u\t %10llu\n", s0ix_total_ctr, + (u64)(s0ix_total_res*10/192)); + seq_puts(s, "\n-------------------------------------------------\n"); + seq_puts(s, "\t\tDEVICE STATES\n"); + seq_puts(s, "-------------------------------------------------\n"); + + for_each_pci_dev(dev) { + pci_read_config_word(dev, dev->pm_cap + PCI_PM_CTRL, &pmcsr); + d3_state = ((pmcsr & PCI_PM_CTRL_STATE_MASK) == + (__force int)PCI_D3hot) ? 1 : 0; + + seq_printf(s, "pci %04x %04X %s %20.20s: ", + dev->vendor, dev->device, dev_name(&dev->dev), + dev_driver_string(&dev->dev)); + seq_printf(s, " d3:%x\n", d3_state); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "D3/D0i3 Status\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Block\t\t D3\t D0i3\n"); + for (index = 0; index < conf->ioss_d0ix_evts; index++) { + seq_printf(s, "%-10s\t %u\t %u\n", + conf->ioss_d0ix_data[index].name, + d3_sts[index], d0ix_sts[index]); + } + + seq_puts(s, "\n--------------------------------------\n"); + seq_puts(s, "South Complex PowerGate Status\n"); + seq_puts(s, "--------------------------------------\n"); + seq_puts(s, "Device\t\t PG\n"); + for (index = 0; index < conf->ioss_pg_evts; index++) { + seq_printf(s, "%-10s\t %u\n", + conf->ioss_pg_data[index].name, + pg_sts[index]); + } + + evtlog->telem_evtid = conf->pss_idle_id; + ret = telemetry_read_events(TELEM_PSS, evtlog, 1); + if (ret < 0) + return ret; + + seq_puts(s, "\n-----------------------------------------\n"); + seq_puts(s, "North Idle Status\n"); + seq_puts(s, "-----------------------------------------\n"); + for (idx = 0; idx < conf->pss_idle_evts - 1; idx++) { + pss_idle[idx] = (evtlog->telem_evtlog >> + conf->pss_idle_data[idx].bit_pos) & + TELEM_MASK_BIT; + } + + pss_idle[idx] = (evtlog->telem_evtlog >> + conf->pss_idle_data[idx].bit_pos) & + TELEM_APL_MASK_PCS_STATE; + + for (index = 0; index < conf->pss_idle_evts; index++) { + seq_printf(s, "%-30s %u\n", + conf->pss_idle_data[index].name, + pss_idle[index]); + } + + seq_puts(s, "\nPCS_STATUS Code\n"); + seq_puts(s, "0:C0 1:C1 2:C1_DN_WT_DEV 3:C2 4:C2_WT_DE_MEM_UP\n"); + seq_puts(s, "5:C2_WT_DE_MEM_DOWN 6:C2_UP_WT_DEV 7:C2_DN 8:C2_VOA\n"); + seq_puts(s, "9:C2_VOA_UP 10:S0IX_PRE 11:S0IX\n"); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(telem_soc_states); + +static int telem_s0ix_res_get(void *data, u64 *val) +{ + struct telemetry_plt_config *plt_config = telemetry_get_pltdata(); + u64 s0ix_total_res; + int ret; + + ret = intel_pmc_s0ix_counter_read(plt_config->pmc, &s0ix_total_res); + if (ret) { + pr_err("Failed to read S0ix residency"); + return ret; + } + + *val = s0ix_total_res; + + return 0; +} + +DEFINE_DEBUGFS_ATTRIBUTE(telem_s0ix_fops, telem_s0ix_res_get, NULL, "%llu\n"); + +static int telem_pss_trc_verb_show(struct seq_file *s, void *unused) +{ + u32 verbosity; + int err; + + err = telemetry_get_trace_verbosity(TELEM_PSS, &verbosity); + if (err) { + pr_err("Get PSS Trace Verbosity Failed with Error %d\n", err); + return -EFAULT; + } + + seq_printf(s, "PSS Trace Verbosity %u\n", verbosity); + return 0; +} + +static ssize_t telem_pss_trc_verb_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *ppos) +{ + u32 verbosity; + int err; + + err = kstrtou32_from_user(userbuf, count, 0, &verbosity); + if (err) + return err; + + err = telemetry_set_trace_verbosity(TELEM_PSS, verbosity); + if (err) { + pr_err("Changing PSS Trace Verbosity Failed. Error %d\n", err); + return err; + } + + return count; +} + +static int telem_pss_trc_verb_open(struct inode *inode, struct file *file) +{ + return single_open(file, telem_pss_trc_verb_show, inode->i_private); +} + +static const struct file_operations telem_pss_trc_verb_ops = { + .open = telem_pss_trc_verb_open, + .read = seq_read, + .write = telem_pss_trc_verb_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static int telem_ioss_trc_verb_show(struct seq_file *s, void *unused) +{ + u32 verbosity; + int err; + + err = telemetry_get_trace_verbosity(TELEM_IOSS, &verbosity); + if (err) { + pr_err("Get IOSS Trace Verbosity Failed with Error %d\n", err); + return -EFAULT; + } + + seq_printf(s, "IOSS Trace Verbosity %u\n", verbosity); + return 0; +} + +static ssize_t telem_ioss_trc_verb_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *ppos) +{ + u32 verbosity; + int err; + + err = kstrtou32_from_user(userbuf, count, 0, &verbosity); + if (err) + return err; + + err = telemetry_set_trace_verbosity(TELEM_IOSS, verbosity); + if (err) { + pr_err("Changing IOSS Trace Verbosity Failed. Error %d\n", err); + return err; + } + + return count; +} + +static int telem_ioss_trc_verb_open(struct inode *inode, struct file *file) +{ + return single_open(file, telem_ioss_trc_verb_show, inode->i_private); +} + +static const struct file_operations telem_ioss_trc_verb_ops = { + .open = telem_ioss_trc_verb_open, + .read = seq_read, + .write = telem_ioss_trc_verb_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static int pm_suspend_prep_cb(void) +{ + struct telemetry_evtlog evtlog[TELEM_MAX_OS_ALLOCATED_EVENTS]; + struct telemetry_debugfs_conf *conf = debugfs_conf; + int ret, index; + + ret = telemetry_raw_read_eventlog(TELEM_IOSS, evtlog, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (ret < 0) { + suspend_prep_ok = 0; + goto out; + } + + for (index = 0; index < ret; index++) { + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_occ_id, + suspend_shlw_ctr_temp); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_occ_id, + suspend_deep_ctr_temp); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_res_id, + suspend_shlw_res_temp); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_res_id, + suspend_deep_res_temp); + } + suspend_prep_ok = 1; +out: + return NOTIFY_OK; +} + +static int pm_suspend_exit_cb(void) +{ + struct telemetry_evtlog evtlog[TELEM_MAX_OS_ALLOCATED_EVENTS]; + static u32 suspend_shlw_ctr_exit, suspend_deep_ctr_exit; + static u64 suspend_shlw_res_exit, suspend_deep_res_exit; + struct telemetry_debugfs_conf *conf = debugfs_conf; + int ret, index; + + if (!suspend_prep_ok) + goto out; + + ret = telemetry_raw_read_eventlog(TELEM_IOSS, evtlog, + TELEM_MAX_OS_ALLOCATED_EVENTS); + if (ret < 0) + goto out; + + for (index = 0; index < ret; index++) { + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_occ_id, + suspend_shlw_ctr_exit); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_occ_id, + suspend_deep_ctr_exit); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_shlw_res_id, + suspend_shlw_res_exit); + + TELEM_CHECK_AND_PARSE_CTRS(conf->s0ix_deep_res_id, + suspend_deep_res_exit); + } + + if ((suspend_shlw_ctr_exit < suspend_shlw_ctr_temp) || + (suspend_deep_ctr_exit < suspend_deep_ctr_temp) || + (suspend_shlw_res_exit < suspend_shlw_res_temp) || + (suspend_deep_res_exit < suspend_deep_res_temp)) { + pr_err("Wrong s0ix counters detected\n"); + goto out; + } + + /* + * Due to some design limitations in the firmware, sometimes the + * counters do not get updated by the time we reach here. As a + * workaround, we try to see if this was a genuine case of sleep + * failure or not by cross-checking from PMC GCR registers directly. + */ + if (suspend_shlw_ctr_exit == suspend_shlw_ctr_temp && + suspend_deep_ctr_exit == suspend_deep_ctr_temp) { + struct telemetry_plt_config *plt_config = telemetry_get_pltdata(); + struct intel_pmc_dev *pmc = plt_config->pmc; + + ret = intel_pmc_gcr_read64(pmc, PMC_GCR_TELEM_SHLW_S0IX_REG, + &suspend_shlw_res_exit); + if (ret < 0) + goto out; + + ret = intel_pmc_gcr_read64(pmc, PMC_GCR_TELEM_DEEP_S0IX_REG, + &suspend_deep_res_exit); + if (ret < 0) + goto out; + + if (suspend_shlw_res_exit > suspend_shlw_res_temp) + suspend_shlw_ctr_exit++; + + if (suspend_deep_res_exit > suspend_deep_res_temp) + suspend_deep_ctr_exit++; + } + + suspend_shlw_ctr_exit -= suspend_shlw_ctr_temp; + suspend_deep_ctr_exit -= suspend_deep_ctr_temp; + suspend_shlw_res_exit -= suspend_shlw_res_temp; + suspend_deep_res_exit -= suspend_deep_res_temp; + + if (suspend_shlw_ctr_exit != 0) { + conf->suspend_stats.shlw_ctr += + suspend_shlw_ctr_exit; + + conf->suspend_stats.shlw_res += + suspend_shlw_res_exit; + } + + if (suspend_deep_ctr_exit != 0) { + conf->suspend_stats.deep_ctr += + suspend_deep_ctr_exit; + + conf->suspend_stats.deep_res += + suspend_deep_res_exit; + } + +out: + suspend_prep_ok = 0; + return NOTIFY_OK; +} + +static int pm_notification(struct notifier_block *this, + unsigned long event, void *ptr) +{ + switch (event) { + case PM_SUSPEND_PREPARE: + return pm_suspend_prep_cb(); + case PM_POST_SUSPEND: + return pm_suspend_exit_cb(); + } + + return NOTIFY_DONE; +} + +static struct notifier_block pm_notifier = { + .notifier_call = pm_notification, +}; + +static int __init telemetry_debugfs_init(void) +{ + const struct x86_cpu_id *id; + int err; + struct dentry *dir; + + /* Only APL supported for now */ + id = x86_match_cpu(telemetry_debugfs_cpu_ids); + if (!id) + return -ENODEV; + + debugfs_conf = (struct telemetry_debugfs_conf *)id->driver_data; + + if (!telemetry_get_pltdata()) { + pr_info("Invalid pltconfig, ensure IPC1 device is enabled in BIOS\n"); + return -ENODEV; + } + + err = telemetry_debugfs_check_evts(); + if (err < 0) { + pr_info("telemetry_debugfs_check_evts failed\n"); + return -EINVAL; + } + + register_pm_notifier(&pm_notifier); + + dir = debugfs_create_dir("telemetry", NULL); + debugfs_conf->telemetry_dbg_dir = dir; + + debugfs_create_file("pss_info", S_IFREG | S_IRUGO, dir, NULL, + &telem_pss_states_fops); + debugfs_create_file("ioss_info", S_IFREG | S_IRUGO, dir, NULL, + &telem_ioss_states_fops); + debugfs_create_file("soc_states", S_IFREG | S_IRUGO, dir, NULL, + &telem_soc_states_fops); + debugfs_create_file("s0ix_residency_usec", S_IFREG | S_IRUGO, dir, NULL, + &telem_s0ix_fops); + debugfs_create_file("pss_trace_verbosity", S_IFREG | S_IRUGO, dir, NULL, + &telem_pss_trc_verb_ops); + debugfs_create_file("ioss_trace_verbosity", S_IFREG | S_IRUGO, dir, + NULL, &telem_ioss_trc_verb_ops); + return 0; +} + +static void __exit telemetry_debugfs_exit(void) +{ + debugfs_remove_recursive(debugfs_conf->telemetry_dbg_dir); + debugfs_conf->telemetry_dbg_dir = NULL; + unregister_pm_notifier(&pm_notifier); +} + +late_initcall(telemetry_debugfs_init); +module_exit(telemetry_debugfs_exit); + +MODULE_AUTHOR("Souvik Kumar Chakravarty "); +MODULE_DESCRIPTION("Intel SoC Telemetry debugfs Interface"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/telemetry/pltdrv.c b/drivers/platform/x86/intel/telemetry/pltdrv.c new file mode 100644 index 000000000..d9aa349f8 --- /dev/null +++ b/drivers/platform/x86/intel/telemetry/pltdrv.c @@ -0,0 +1,957 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel SOC Telemetry Platform Driver: Currently supports APL + * Copyright (c) 2015, Intel Corporation. + * All Rights Reserved. + * + * This file provides the platform specific telemetry implementation for APL. + * It used the PUNIT and PMC IPC interfaces for configuring the counters. + * The accumulated results are fetched from SRAM. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#define DRIVER_NAME "intel_telemetry" +#define DRIVER_VERSION "1.0.0" + +#define TELEM_TRC_VERBOSITY_MASK 0x3 + +#define TELEM_MIN_PERIOD(x) ((x) & 0x7F0000) +#define TELEM_MAX_PERIOD(x) ((x) & 0x7F000000) +#define TELEM_SAMPLE_PERIOD_INVALID(x) ((x) & (BIT(7))) +#define TELEM_CLEAR_SAMPLE_PERIOD(x) ((x) &= ~0x7F) + +#define TELEM_SAMPLING_DEFAULT_PERIOD 0xD + +#define TELEM_MAX_EVENTS_SRAM 28 +#define TELEM_SSRAM_STARTTIME_OFFSET 8 +#define TELEM_SSRAM_EVTLOG_OFFSET 16 + +#define IOSS_TELEM 0xeb +#define IOSS_TELEM_EVENT_READ 0x0 +#define IOSS_TELEM_EVENT_WRITE 0x1 +#define IOSS_TELEM_INFO_READ 0x2 +#define IOSS_TELEM_TRACE_CTL_READ 0x5 +#define IOSS_TELEM_TRACE_CTL_WRITE 0x6 +#define IOSS_TELEM_EVENT_CTL_READ 0x7 +#define IOSS_TELEM_EVENT_CTL_WRITE 0x8 +#define IOSS_TELEM_EVT_WRITE_SIZE 0x3 + +#define TELEM_INFO_SRAMEVTS_MASK 0xFF00 +#define TELEM_INFO_SRAMEVTS_SHIFT 0x8 +#define TELEM_SSRAM_READ_TIMEOUT 10 + +#define TELEM_INFO_NENABLES_MASK 0xFF +#define TELEM_EVENT_ENABLE 0x8000 + +#define TELEM_MASK_BIT 1 +#define TELEM_MASK_BYTE 0xFF +#define BYTES_PER_LONG 8 +#define TELEM_MASK_PCS_STATE 0xF + +#define TELEM_DISABLE(x) ((x) &= ~(BIT(31))) +#define TELEM_CLEAR_EVENTS(x) ((x) |= (BIT(30))) +#define TELEM_ENABLE_SRAM_EVT_TRACE(x) ((x) &= ~(BIT(30) | BIT(24))) +#define TELEM_ENABLE_PERIODIC(x) ((x) |= (BIT(23) | BIT(31) | BIT(7))) +#define TELEM_EXTRACT_VERBOSITY(x, y) ((y) = (((x) >> 27) & 0x3)) +#define TELEM_CLEAR_VERBOSITY_BITS(x) ((x) &= ~(BIT(27) | BIT(28))) +#define TELEM_SET_VERBOSITY_BITS(x, y) ((x) |= ((y) << 27)) + +enum telemetry_action { + TELEM_UPDATE = 0, + TELEM_ADD, + TELEM_RESET, + TELEM_ACTION_NONE +}; + +struct telem_ssram_region { + u64 timestamp; + u64 start_time; + u64 events[TELEM_MAX_EVENTS_SRAM]; +}; + +static struct telemetry_plt_config *telm_conf; + +/* + * The following counters are programmed by default during setup. + * Only 20 allocated to kernel driver + */ +static struct telemetry_evtmap + telemetry_apl_ioss_default_events[TELEM_MAX_OS_ALLOCATED_EVENTS] = { + {"SOC_S0IX_TOTAL_RES", 0x4800}, + {"SOC_S0IX_TOTAL_OCC", 0x4000}, + {"SOC_S0IX_SHALLOW_RES", 0x4801}, + {"SOC_S0IX_SHALLOW_OCC", 0x4001}, + {"SOC_S0IX_DEEP_RES", 0x4802}, + {"SOC_S0IX_DEEP_OCC", 0x4002}, + {"PMC_POWER_GATE", 0x5818}, + {"PMC_D3_STATES", 0x5819}, + {"PMC_D0I3_STATES", 0x581A}, + {"PMC_S0IX_WAKE_REASON_GPIO", 0x6000}, + {"PMC_S0IX_WAKE_REASON_TIMER", 0x6001}, + {"PMC_S0IX_WAKE_REASON_VNNREQ", 0x6002}, + {"PMC_S0IX_WAKE_REASON_LOWPOWER", 0x6003}, + {"PMC_S0IX_WAKE_REASON_EXTERNAL", 0x6004}, + {"PMC_S0IX_WAKE_REASON_MISC", 0x6005}, + {"PMC_S0IX_BLOCKING_IPS_D3_D0I3", 0x6006}, + {"PMC_S0IX_BLOCKING_IPS_PG", 0x6007}, + {"PMC_S0IX_BLOCKING_MISC_IPS_PG", 0x6008}, + {"PMC_S0IX_BLOCK_IPS_VNN_REQ", 0x6009}, + {"PMC_S0IX_BLOCK_IPS_CLOCKS", 0x600B}, +}; + + +static struct telemetry_evtmap + telemetry_apl_pss_default_events[TELEM_MAX_OS_ALLOCATED_EVENTS] = { + {"IA_CORE0_C6_RES", 0x0400}, + {"IA_CORE0_C6_CTR", 0x0000}, + {"IA_MODULE0_C7_RES", 0x0410}, + {"IA_MODULE0_C7_CTR", 0x000E}, + {"IA_C0_RES", 0x0805}, + {"PCS_LTR", 0x2801}, + {"PSTATES", 0x2802}, + {"SOC_S0I3_RES", 0x0409}, + {"SOC_S0I3_CTR", 0x000A}, + {"PCS_S0I3_CTR", 0x0009}, + {"PCS_C1E_RES", 0x041A}, + {"PCS_IDLE_STATUS", 0x2806}, + {"IA_PERF_LIMITS", 0x280B}, + {"GT_PERF_LIMITS", 0x280C}, + {"PCS_WAKEUP_S0IX_CTR", 0x0030}, + {"PCS_IDLE_BLOCKED", 0x2C00}, + {"PCS_S0IX_BLOCKED", 0x2C01}, + {"PCS_S0IX_WAKE_REASONS", 0x2C02}, + {"PCS_LTR_BLOCKING", 0x2C03}, + {"PC2_AND_MEM_SHALLOW_IDLE_RES", 0x1D40}, +}; + +static struct telemetry_evtmap + telemetry_glk_pss_default_events[TELEM_MAX_OS_ALLOCATED_EVENTS] = { + {"IA_CORE0_C6_RES", 0x0400}, + {"IA_CORE0_C6_CTR", 0x0000}, + {"IA_MODULE0_C7_RES", 0x0410}, + {"IA_MODULE0_C7_CTR", 0x000C}, + {"IA_C0_RES", 0x0805}, + {"PCS_LTR", 0x2801}, + {"PSTATES", 0x2802}, + {"SOC_S0I3_RES", 0x0407}, + {"SOC_S0I3_CTR", 0x0008}, + {"PCS_S0I3_CTR", 0x0007}, + {"PCS_C1E_RES", 0x0414}, + {"PCS_IDLE_STATUS", 0x2806}, + {"IA_PERF_LIMITS", 0x280B}, + {"GT_PERF_LIMITS", 0x280C}, + {"PCS_WAKEUP_S0IX_CTR", 0x0025}, + {"PCS_IDLE_BLOCKED", 0x2C00}, + {"PCS_S0IX_BLOCKED", 0x2C01}, + {"PCS_S0IX_WAKE_REASONS", 0x2C02}, + {"PCS_LTR_BLOCKING", 0x2C03}, + {"PC2_AND_MEM_SHALLOW_IDLE_RES", 0x1D40}, +}; + +/* APL specific Data */ +static struct telemetry_plt_config telem_apl_config = { + .pss_config = { + .telem_evts = telemetry_apl_pss_default_events, + }, + .ioss_config = { + .telem_evts = telemetry_apl_ioss_default_events, + }, +}; + +/* GLK specific Data */ +static struct telemetry_plt_config telem_glk_config = { + .pss_config = { + .telem_evts = telemetry_glk_pss_default_events, + }, + .ioss_config = { + .telem_evts = telemetry_apl_ioss_default_events, + }, +}; + +static const struct x86_cpu_id telemetry_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT, &telem_apl_config), + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT_PLUS, &telem_glk_config), + {} +}; + +MODULE_DEVICE_TABLE(x86cpu, telemetry_cpu_ids); + +static inline int telem_get_unitconfig(enum telemetry_unit telem_unit, + struct telemetry_unit_config **unit_config) +{ + if (telem_unit == TELEM_PSS) + *unit_config = &(telm_conf->pss_config); + else if (telem_unit == TELEM_IOSS) + *unit_config = &(telm_conf->ioss_config); + else + return -EINVAL; + + return 0; + +} + +static int telemetry_check_evtid(enum telemetry_unit telem_unit, + u32 *evtmap, u8 len, + enum telemetry_action action) +{ + struct telemetry_unit_config *unit_config; + int ret; + + ret = telem_get_unitconfig(telem_unit, &unit_config); + if (ret < 0) + return ret; + + switch (action) { + case TELEM_RESET: + if (len > TELEM_MAX_EVENTS_SRAM) + return -EINVAL; + + break; + + case TELEM_UPDATE: + if (len > TELEM_MAX_EVENTS_SRAM) + return -EINVAL; + + if ((len > 0) && (evtmap == NULL)) + return -EINVAL; + + break; + + case TELEM_ADD: + if ((len + unit_config->ssram_evts_used) > + TELEM_MAX_EVENTS_SRAM) + return -EINVAL; + + if ((len > 0) && (evtmap == NULL)) + return -EINVAL; + + break; + + default: + pr_err("Unknown Telemetry action specified %d\n", action); + return -EINVAL; + } + + return 0; +} + + +static inline int telemetry_plt_config_ioss_event(u32 evt_id, int index) +{ + u32 write_buf; + + write_buf = evt_id | TELEM_EVENT_ENABLE; + write_buf <<= BITS_PER_BYTE; + write_buf |= index; + + return intel_scu_ipc_dev_command(telm_conf->scu, IOSS_TELEM, + IOSS_TELEM_EVENT_WRITE, &write_buf, + IOSS_TELEM_EVT_WRITE_SIZE, NULL, 0); +} + +static inline int telemetry_plt_config_pss_event(u32 evt_id, int index) +{ + u32 write_buf; + int ret; + + write_buf = evt_id | TELEM_EVENT_ENABLE; + ret = intel_punit_ipc_command(IPC_PUNIT_BIOS_WRITE_TELE_EVENT, + index, 0, &write_buf, NULL); + + return ret; +} + +static int telemetry_setup_iossevtconfig(struct telemetry_evtconfig evtconfig, + enum telemetry_action action) +{ + struct intel_scu_ipc_dev *scu = telm_conf->scu; + u8 num_ioss_evts, ioss_period; + int ret, index, idx; + u32 *ioss_evtmap; + u32 telem_ctrl; + + num_ioss_evts = evtconfig.num_evts; + ioss_period = evtconfig.period; + ioss_evtmap = evtconfig.evtmap; + + /* Get telemetry EVENT CTL */ + ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, + IOSS_TELEM_EVENT_CTL_READ, NULL, 0, + &telem_ctrl, sizeof(telem_ctrl)); + if (ret) { + pr_err("IOSS TELEM_CTRL Read Failed\n"); + return ret; + } + + /* Disable Telemetry */ + TELEM_DISABLE(telem_ctrl); + + ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, + IOSS_TELEM_EVENT_CTL_WRITE, &telem_ctrl, + sizeof(telem_ctrl), NULL, 0); + if (ret) { + pr_err("IOSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + + + /* Reset Everything */ + if (action == TELEM_RESET) { + /* Clear All Events */ + TELEM_CLEAR_EVENTS(telem_ctrl); + + ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, + IOSS_TELEM_EVENT_CTL_WRITE, + &telem_ctrl, sizeof(telem_ctrl), + NULL, 0); + if (ret) { + pr_err("IOSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + telm_conf->ioss_config.ssram_evts_used = 0; + + /* Configure Events */ + for (idx = 0; idx < num_ioss_evts; idx++) { + if (telemetry_plt_config_ioss_event( + telm_conf->ioss_config.telem_evts[idx].evt_id, + idx)) { + pr_err("IOSS TELEM_RESET Fail for data: %x\n", + telm_conf->ioss_config.telem_evts[idx].evt_id); + continue; + } + telm_conf->ioss_config.ssram_evts_used++; + } + } + + /* Re-Configure Everything */ + if (action == TELEM_UPDATE) { + /* Clear All Events */ + TELEM_CLEAR_EVENTS(telem_ctrl); + + ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, + IOSS_TELEM_EVENT_CTL_WRITE, + &telem_ctrl, sizeof(telem_ctrl), + NULL, 0); + if (ret) { + pr_err("IOSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + telm_conf->ioss_config.ssram_evts_used = 0; + + /* Configure Events */ + for (index = 0; index < num_ioss_evts; index++) { + telm_conf->ioss_config.telem_evts[index].evt_id = + ioss_evtmap[index]; + + if (telemetry_plt_config_ioss_event( + telm_conf->ioss_config.telem_evts[index].evt_id, + index)) { + pr_err("IOSS TELEM_UPDATE Fail for Evt%x\n", + ioss_evtmap[index]); + continue; + } + telm_conf->ioss_config.ssram_evts_used++; + } + } + + /* Add some Events */ + if (action == TELEM_ADD) { + /* Configure Events */ + for (index = telm_conf->ioss_config.ssram_evts_used, idx = 0; + idx < num_ioss_evts; index++, idx++) { + telm_conf->ioss_config.telem_evts[index].evt_id = + ioss_evtmap[idx]; + + if (telemetry_plt_config_ioss_event( + telm_conf->ioss_config.telem_evts[index].evt_id, + index)) { + pr_err("IOSS TELEM_ADD Fail for Event %x\n", + ioss_evtmap[idx]); + continue; + } + telm_conf->ioss_config.ssram_evts_used++; + } + } + + /* Enable Periodic Telemetry Events and enable SRAM trace */ + TELEM_CLEAR_SAMPLE_PERIOD(telem_ctrl); + TELEM_ENABLE_SRAM_EVT_TRACE(telem_ctrl); + TELEM_ENABLE_PERIODIC(telem_ctrl); + telem_ctrl |= ioss_period; + + ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, + IOSS_TELEM_EVENT_CTL_WRITE, + &telem_ctrl, sizeof(telem_ctrl), NULL, 0); + if (ret) { + pr_err("IOSS TELEM_CTRL Event Enable Write Failed\n"); + return ret; + } + + telm_conf->ioss_config.curr_period = ioss_period; + + return 0; +} + + +static int telemetry_setup_pssevtconfig(struct telemetry_evtconfig evtconfig, + enum telemetry_action action) +{ + u8 num_pss_evts, pss_period; + int ret, index, idx; + u32 *pss_evtmap; + u32 telem_ctrl; + + num_pss_evts = evtconfig.num_evts; + pss_period = evtconfig.period; + pss_evtmap = evtconfig.evtmap; + + /* PSS Config */ + /* Get telemetry EVENT CTL */ + ret = intel_punit_ipc_command(IPC_PUNIT_BIOS_READ_TELE_EVENT_CTRL, + 0, 0, NULL, &telem_ctrl); + if (ret) { + pr_err("PSS TELEM_CTRL Read Failed\n"); + return ret; + } + + /* Disable Telemetry */ + TELEM_DISABLE(telem_ctrl); + ret = intel_punit_ipc_command(IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, + 0, 0, &telem_ctrl, NULL); + if (ret) { + pr_err("PSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + + /* Reset Everything */ + if (action == TELEM_RESET) { + /* Clear All Events */ + TELEM_CLEAR_EVENTS(telem_ctrl); + + ret = intel_punit_ipc_command( + IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, + 0, 0, &telem_ctrl, NULL); + if (ret) { + pr_err("PSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + telm_conf->pss_config.ssram_evts_used = 0; + /* Configure Events */ + for (idx = 0; idx < num_pss_evts; idx++) { + if (telemetry_plt_config_pss_event( + telm_conf->pss_config.telem_evts[idx].evt_id, + idx)) { + pr_err("PSS TELEM_RESET Fail for Event %x\n", + telm_conf->pss_config.telem_evts[idx].evt_id); + continue; + } + telm_conf->pss_config.ssram_evts_used++; + } + } + + /* Re-Configure Everything */ + if (action == TELEM_UPDATE) { + /* Clear All Events */ + TELEM_CLEAR_EVENTS(telem_ctrl); + + ret = intel_punit_ipc_command( + IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, + 0, 0, &telem_ctrl, NULL); + if (ret) { + pr_err("PSS TELEM_CTRL Event Disable Write Failed\n"); + return ret; + } + telm_conf->pss_config.ssram_evts_used = 0; + + /* Configure Events */ + for (index = 0; index < num_pss_evts; index++) { + telm_conf->pss_config.telem_evts[index].evt_id = + pss_evtmap[index]; + + if (telemetry_plt_config_pss_event( + telm_conf->pss_config.telem_evts[index].evt_id, + index)) { + pr_err("PSS TELEM_UPDATE Fail for Event %x\n", + pss_evtmap[index]); + continue; + } + telm_conf->pss_config.ssram_evts_used++; + } + } + + /* Add some Events */ + if (action == TELEM_ADD) { + /* Configure Events */ + for (index = telm_conf->pss_config.ssram_evts_used, idx = 0; + idx < num_pss_evts; index++, idx++) { + + telm_conf->pss_config.telem_evts[index].evt_id = + pss_evtmap[idx]; + + if (telemetry_plt_config_pss_event( + telm_conf->pss_config.telem_evts[index].evt_id, + index)) { + pr_err("PSS TELEM_ADD Fail for Event %x\n", + pss_evtmap[idx]); + continue; + } + telm_conf->pss_config.ssram_evts_used++; + } + } + + /* Enable Periodic Telemetry Events and enable SRAM trace */ + TELEM_CLEAR_SAMPLE_PERIOD(telem_ctrl); + TELEM_ENABLE_SRAM_EVT_TRACE(telem_ctrl); + TELEM_ENABLE_PERIODIC(telem_ctrl); + telem_ctrl |= pss_period; + + ret = intel_punit_ipc_command(IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, + 0, 0, &telem_ctrl, NULL); + if (ret) { + pr_err("PSS TELEM_CTRL Event Enable Write Failed\n"); + return ret; + } + + telm_conf->pss_config.curr_period = pss_period; + + return 0; +} + +static int telemetry_setup_evtconfig(struct telemetry_evtconfig pss_evtconfig, + struct telemetry_evtconfig ioss_evtconfig, + enum telemetry_action action) +{ + int ret; + + mutex_lock(&(telm_conf->telem_lock)); + + if ((action == TELEM_UPDATE) && (telm_conf->telem_in_use)) { + ret = -EBUSY; + goto out; + } + + ret = telemetry_check_evtid(TELEM_PSS, pss_evtconfig.evtmap, + pss_evtconfig.num_evts, action); + if (ret) + goto out; + + ret = telemetry_check_evtid(TELEM_IOSS, ioss_evtconfig.evtmap, + ioss_evtconfig.num_evts, action); + if (ret) + goto out; + + if (ioss_evtconfig.num_evts) { + ret = telemetry_setup_iossevtconfig(ioss_evtconfig, action); + if (ret) + goto out; + } + + if (pss_evtconfig.num_evts) { + ret = telemetry_setup_pssevtconfig(pss_evtconfig, action); + if (ret) + goto out; + } + + if ((action == TELEM_UPDATE) || (action == TELEM_ADD)) + telm_conf->telem_in_use = true; + else + telm_conf->telem_in_use = false; + +out: + mutex_unlock(&(telm_conf->telem_lock)); + return ret; +} + +static int telemetry_setup(struct platform_device *pdev) +{ + struct telemetry_evtconfig pss_evtconfig, ioss_evtconfig; + u32 read_buf, events, event_regs; + int ret; + + ret = intel_scu_ipc_dev_command(telm_conf->scu, IOSS_TELEM, + IOSS_TELEM_INFO_READ, NULL, 0, + &read_buf, sizeof(read_buf)); + if (ret) { + dev_err(&pdev->dev, "IOSS TELEM_INFO Read Failed\n"); + return ret; + } + + /* Get telemetry Info */ + events = (read_buf & TELEM_INFO_SRAMEVTS_MASK) >> + TELEM_INFO_SRAMEVTS_SHIFT; + event_regs = read_buf & TELEM_INFO_NENABLES_MASK; + if ((events < TELEM_MAX_EVENTS_SRAM) || + (event_regs < TELEM_MAX_EVENTS_SRAM)) { + dev_err(&pdev->dev, "IOSS:Insufficient Space for SRAM Trace\n"); + dev_err(&pdev->dev, "SRAM Events %d; Event Regs %d\n", + events, event_regs); + return -ENOMEM; + } + + telm_conf->ioss_config.min_period = TELEM_MIN_PERIOD(read_buf); + telm_conf->ioss_config.max_period = TELEM_MAX_PERIOD(read_buf); + + /* PUNIT Mailbox Setup */ + ret = intel_punit_ipc_command(IPC_PUNIT_BIOS_READ_TELE_INFO, 0, 0, + NULL, &read_buf); + if (ret) { + dev_err(&pdev->dev, "PSS TELEM_INFO Read Failed\n"); + return ret; + } + + /* Get telemetry Info */ + events = (read_buf & TELEM_INFO_SRAMEVTS_MASK) >> + TELEM_INFO_SRAMEVTS_SHIFT; + event_regs = read_buf & TELEM_INFO_NENABLES_MASK; + if ((events < TELEM_MAX_EVENTS_SRAM) || + (event_regs < TELEM_MAX_EVENTS_SRAM)) { + dev_err(&pdev->dev, "PSS:Insufficient Space for SRAM Trace\n"); + dev_err(&pdev->dev, "SRAM Events %d; Event Regs %d\n", + events, event_regs); + return -ENOMEM; + } + + telm_conf->pss_config.min_period = TELEM_MIN_PERIOD(read_buf); + telm_conf->pss_config.max_period = TELEM_MAX_PERIOD(read_buf); + + pss_evtconfig.evtmap = NULL; + pss_evtconfig.num_evts = TELEM_MAX_OS_ALLOCATED_EVENTS; + pss_evtconfig.period = TELEM_SAMPLING_DEFAULT_PERIOD; + + ioss_evtconfig.evtmap = NULL; + ioss_evtconfig.num_evts = TELEM_MAX_OS_ALLOCATED_EVENTS; + ioss_evtconfig.period = TELEM_SAMPLING_DEFAULT_PERIOD; + + ret = telemetry_setup_evtconfig(pss_evtconfig, ioss_evtconfig, + TELEM_RESET); + if (ret) { + dev_err(&pdev->dev, "TELEMETRY Setup Failed\n"); + return ret; + } + return 0; +} + +static int telem_evtlog_read(enum telemetry_unit telem_unit, + struct telem_ssram_region *ssram_region, u8 len) +{ + struct telemetry_unit_config *unit_config; + u64 timestamp_prev, timestamp_next; + int ret, index, timeout = 0; + + ret = telem_get_unitconfig(telem_unit, &unit_config); + if (ret < 0) + return ret; + + if (len > unit_config->ssram_evts_used) + len = unit_config->ssram_evts_used; + + do { + timestamp_prev = readq(unit_config->regmap); + if (!timestamp_prev) { + pr_err("Ssram under update. Please Try Later\n"); + return -EBUSY; + } + + ssram_region->start_time = readq(unit_config->regmap + + TELEM_SSRAM_STARTTIME_OFFSET); + + for (index = 0; index < len; index++) { + ssram_region->events[index] = + readq(unit_config->regmap + TELEM_SSRAM_EVTLOG_OFFSET + + BYTES_PER_LONG*index); + } + + timestamp_next = readq(unit_config->regmap); + if (!timestamp_next) { + pr_err("Ssram under update. Please Try Later\n"); + return -EBUSY; + } + + if (timeout++ > TELEM_SSRAM_READ_TIMEOUT) { + pr_err("Timeout while reading Events\n"); + return -EBUSY; + } + + } while (timestamp_prev != timestamp_next); + + ssram_region->timestamp = timestamp_next; + + return len; +} + +static int telemetry_plt_raw_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, + int len, int log_all_evts) +{ + int index, idx1, ret, readlen = len; + struct telem_ssram_region ssram_region; + struct telemetry_evtmap *evtmap; + + switch (telem_unit) { + case TELEM_PSS: + evtmap = telm_conf->pss_config.telem_evts; + break; + + case TELEM_IOSS: + evtmap = telm_conf->ioss_config.telem_evts; + break; + + default: + pr_err("Unknown Telemetry Unit Specified %d\n", telem_unit); + return -EINVAL; + } + + if (!log_all_evts) + readlen = TELEM_MAX_EVENTS_SRAM; + + ret = telem_evtlog_read(telem_unit, &ssram_region, readlen); + if (ret < 0) + return ret; + + /* Invalid evt-id array specified via length mismatch */ + if ((!log_all_evts) && (len > ret)) + return -EINVAL; + + if (log_all_evts) + for (index = 0; index < ret; index++) { + evtlog[index].telem_evtlog = ssram_region.events[index]; + evtlog[index].telem_evtid = evtmap[index].evt_id; + } + else + for (index = 0, readlen = 0; (index < ret) && (readlen < len); + index++) { + for (idx1 = 0; idx1 < len; idx1++) { + /* Elements matched */ + if (evtmap[index].evt_id == + evtlog[idx1].telem_evtid) { + evtlog[idx1].telem_evtlog = + ssram_region.events[index]; + readlen++; + + break; + } + } + } + + return readlen; +} + +static int telemetry_plt_read_eventlog(enum telemetry_unit telem_unit, + struct telemetry_evtlog *evtlog, int len, int log_all_evts) +{ + int ret; + + mutex_lock(&(telm_conf->telem_lock)); + ret = telemetry_plt_raw_read_eventlog(telem_unit, evtlog, + len, log_all_evts); + mutex_unlock(&(telm_conf->telem_lock)); + + return ret; +} + +static int telemetry_plt_get_trace_verbosity(enum telemetry_unit telem_unit, + u32 *verbosity) +{ + u32 temp = 0; + int ret; + + if (verbosity == NULL) + return -EINVAL; + + mutex_lock(&(telm_conf->telem_trace_lock)); + switch (telem_unit) { + case TELEM_PSS: + ret = intel_punit_ipc_command( + IPC_PUNIT_BIOS_READ_TELE_TRACE_CTRL, + 0, 0, NULL, &temp); + if (ret) { + pr_err("PSS TRACE_CTRL Read Failed\n"); + goto out; + } + + break; + + case TELEM_IOSS: + ret = intel_scu_ipc_dev_command(telm_conf->scu, + IOSS_TELEM, IOSS_TELEM_TRACE_CTL_READ, + NULL, 0, &temp, sizeof(temp)); + if (ret) { + pr_err("IOSS TRACE_CTL Read Failed\n"); + goto out; + } + + break; + + default: + pr_err("Unknown Telemetry Unit Specified %d\n", telem_unit); + ret = -EINVAL; + break; + } + TELEM_EXTRACT_VERBOSITY(temp, *verbosity); + +out: + mutex_unlock(&(telm_conf->telem_trace_lock)); + return ret; +} + +static int telemetry_plt_set_trace_verbosity(enum telemetry_unit telem_unit, + u32 verbosity) +{ + u32 temp = 0; + int ret; + + verbosity &= TELEM_TRC_VERBOSITY_MASK; + + mutex_lock(&(telm_conf->telem_trace_lock)); + switch (telem_unit) { + case TELEM_PSS: + ret = intel_punit_ipc_command( + IPC_PUNIT_BIOS_READ_TELE_TRACE_CTRL, + 0, 0, NULL, &temp); + if (ret) { + pr_err("PSS TRACE_CTRL Read Failed\n"); + goto out; + } + + TELEM_CLEAR_VERBOSITY_BITS(temp); + TELEM_SET_VERBOSITY_BITS(temp, verbosity); + + ret = intel_punit_ipc_command( + IPC_PUNIT_BIOS_WRITE_TELE_TRACE_CTRL, + 0, 0, &temp, NULL); + if (ret) { + pr_err("PSS TRACE_CTRL Verbosity Set Failed\n"); + goto out; + } + break; + + case TELEM_IOSS: + ret = intel_scu_ipc_dev_command(telm_conf->scu, IOSS_TELEM, + IOSS_TELEM_TRACE_CTL_READ, + NULL, 0, &temp, sizeof(temp)); + if (ret) { + pr_err("IOSS TRACE_CTL Read Failed\n"); + goto out; + } + + TELEM_CLEAR_VERBOSITY_BITS(temp); + TELEM_SET_VERBOSITY_BITS(temp, verbosity); + + ret = intel_scu_ipc_dev_command(telm_conf->scu, IOSS_TELEM, + IOSS_TELEM_TRACE_CTL_WRITE, + &temp, sizeof(temp), NULL, 0); + if (ret) { + pr_err("IOSS TRACE_CTL Verbosity Set Failed\n"); + goto out; + } + break; + + default: + pr_err("Unknown Telemetry Unit Specified %d\n", telem_unit); + ret = -EINVAL; + break; + } + +out: + mutex_unlock(&(telm_conf->telem_trace_lock)); + return ret; +} + +static const struct telemetry_core_ops telm_pltops = { + .get_trace_verbosity = telemetry_plt_get_trace_verbosity, + .set_trace_verbosity = telemetry_plt_set_trace_verbosity, + .raw_read_eventlog = telemetry_plt_raw_read_eventlog, + .read_eventlog = telemetry_plt_read_eventlog, +}; + +static int telemetry_pltdrv_probe(struct platform_device *pdev) +{ + const struct x86_cpu_id *id; + void __iomem *mem; + int ret; + + id = x86_match_cpu(telemetry_cpu_ids); + if (!id) + return -ENODEV; + + telm_conf = (struct telemetry_plt_config *)id->driver_data; + + telm_conf->pmc = dev_get_drvdata(pdev->dev.parent); + + mem = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(mem)) + return PTR_ERR(mem); + + telm_conf->pss_config.regmap = mem; + + mem = devm_platform_ioremap_resource(pdev, 1); + if (IS_ERR(mem)) + return PTR_ERR(mem); + + telm_conf->ioss_config.regmap = mem; + + telm_conf->scu = devm_intel_scu_ipc_dev_get(&pdev->dev); + if (!telm_conf->scu) { + ret = -EPROBE_DEFER; + goto out; + } + + mutex_init(&telm_conf->telem_lock); + mutex_init(&telm_conf->telem_trace_lock); + + ret = telemetry_setup(pdev); + if (ret) + goto out; + + ret = telemetry_set_pltdata(&telm_pltops, telm_conf); + if (ret) { + dev_err(&pdev->dev, "TELEMETRY Set Pltops Failed.\n"); + goto out; + } + + return 0; + +out: + dev_err(&pdev->dev, "TELEMETRY Setup Failed.\n"); + + return ret; +} + +static void telemetry_pltdrv_remove(struct platform_device *pdev) +{ + telemetry_clear_pltdata(); +} + +static struct platform_driver telemetry_soc_driver = { + .probe = telemetry_pltdrv_probe, + .remove = telemetry_pltdrv_remove, + .driver = { + .name = DRIVER_NAME, + }, +}; + +static int __init telemetry_module_init(void) +{ + return platform_driver_register(&telemetry_soc_driver); +} + +static void __exit telemetry_module_exit(void) +{ + platform_driver_unregister(&telemetry_soc_driver); +} + +device_initcall(telemetry_module_init); +module_exit(telemetry_module_exit); + +MODULE_AUTHOR("Souvik Kumar Chakravarty "); +MODULE_DESCRIPTION("Intel SoC Telemetry Platform Driver"); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/tpmi_power_domains.c b/drivers/platform/x86/intel/tpmi_power_domains.c new file mode 100644 index 000000000..833052dc3 --- /dev/null +++ b/drivers/platform/x86/intel/tpmi_power_domains.c @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Mapping of TPMI power domains CPU mapping + * + * Copyright (c) 2024, Intel Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "tpmi_power_domains.h" + +#define MSR_PM_LOGICAL_ID 0x54 + +/* + * Struct of MSR 0x54 + * [15:11] PM_DOMAIN_ID + * [10:3] MODULE_ID (aka IDI_AGENT_ID) + * [2:0] LP_ID + * For Atom: + * [2] Always 0 + * [1:0] core ID within module + * For Core + * [2:1] Always 0 + * [0] thread ID + */ + +#define LP_ID_MASK GENMASK_ULL(2, 0) +#define MODULE_ID_MASK GENMASK_ULL(10, 3) +#define PM_DOMAIN_ID_MASK GENMASK_ULL(15, 11) + +/** + * struct tpmi_cpu_info - Mapping information for a CPU + * @hnode: Used to add mapping information to hash list + * @linux_cpu: Linux CPU number + * @pkg_id: Package ID of this CPU + * @punit_thread_id: Punit thread id of this CPU + * @punit_core_id: Punit core id + * @punit_domain_id: Power domain id from Punit + * + * Structure to store mapping information for a Linux CPU + * to a Punit core, thread and power domain. + */ +struct tpmi_cpu_info { + struct hlist_node hnode; + int linux_cpu; + u8 pkg_id; + u8 punit_thread_id; + u8 punit_core_id; + u8 punit_domain_id; +}; + +static DEFINE_PER_CPU(struct tpmi_cpu_info, tpmi_cpu_info); + +/* The dynamically assigned cpu hotplug state to free later */ +static enum cpuhp_state tpmi_hp_state __read_mostly; + +#define MAX_POWER_DOMAINS 8 + +static cpumask_t *tpmi_power_domain_mask; + +static u16 *domain_die_map; + +/* Lock to protect tpmi_power_domain_mask and tpmi_cpu_hash */ +static DEFINE_MUTEX(tpmi_lock); + +static const struct x86_cpu_id tpmi_cpu_ids[] = { + X86_MATCH_VFM(INTEL_GRANITERAPIDS_X, NULL), + X86_MATCH_VFM(INTEL_ATOM_CRESTMONT_X, NULL), + X86_MATCH_VFM(INTEL_ATOM_CRESTMONT, NULL), + X86_MATCH_VFM(INTEL_ATOM_DARKMONT_X, NULL), + X86_MATCH_VFM(INTEL_GRANITERAPIDS_D, NULL), + X86_MATCH_VFM(INTEL_DIAMONDRAPIDS_X, NULL), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, tpmi_cpu_ids); + +static DECLARE_HASHTABLE(tpmi_cpu_hash, 8); + +static bool tpmi_domain_is_valid(struct tpmi_cpu_info *info) +{ + return info->pkg_id < topology_max_packages() && + info->punit_domain_id < MAX_POWER_DOMAINS; +} + +int tpmi_get_linux_cpu_number(int package_id, int domain_id, int punit_core_id) +{ + struct tpmi_cpu_info *info; + int ret = -EINVAL; + + guard(mutex)(&tpmi_lock); + hash_for_each_possible(tpmi_cpu_hash, info, hnode, punit_core_id) { + if (info->punit_domain_id == domain_id && info->pkg_id == package_id) { + ret = info->linux_cpu; + break; + } + } + + return ret; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_linux_cpu_number, "INTEL_TPMI_POWER_DOMAIN"); + +int tpmi_get_punit_core_number(int cpu_no) +{ + if (cpu_no >= num_possible_cpus()) + return -EINVAL; + + return per_cpu(tpmi_cpu_info, cpu_no).punit_core_id; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_punit_core_number, "INTEL_TPMI_POWER_DOMAIN"); + +int tpmi_get_power_domain_id(int cpu_no) +{ + if (cpu_no >= num_possible_cpus()) + return -EINVAL; + + return per_cpu(tpmi_cpu_info, cpu_no).punit_domain_id; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_power_domain_id, "INTEL_TPMI_POWER_DOMAIN"); + +cpumask_t *tpmi_get_power_domain_mask(int cpu_no) +{ + struct tpmi_cpu_info *info; + cpumask_t *mask; + int index; + + if (cpu_no >= num_possible_cpus()) + return NULL; + + info = &per_cpu(tpmi_cpu_info, cpu_no); + if (!tpmi_domain_is_valid(info)) + return NULL; + + index = info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id; + guard(mutex)(&tpmi_lock); + mask = &tpmi_power_domain_mask[index]; + + return mask; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_power_domain_mask, "INTEL_TPMI_POWER_DOMAIN"); + +int tpmi_get_linux_die_id(int pkg_id, int domain_id) +{ + if (pkg_id >= topology_max_packages() || domain_id >= MAX_POWER_DOMAINS) + return -EINVAL; + + return domain_die_map[pkg_id * MAX_POWER_DOMAINS + domain_id]; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_linux_die_id, "INTEL_TPMI_POWER_DOMAIN"); + +static int tpmi_get_logical_id(unsigned int cpu, struct tpmi_cpu_info *info) +{ + u64 data; + int ret; + + ret = rdmsrq_safe(MSR_PM_LOGICAL_ID, &data); + if (ret) + return ret; + + info->punit_domain_id = FIELD_GET(PM_DOMAIN_ID_MASK, data); + if (info->punit_domain_id >= MAX_POWER_DOMAINS) + return -EINVAL; + + info->punit_thread_id = FIELD_GET(LP_ID_MASK, data); + info->punit_core_id = FIELD_GET(MODULE_ID_MASK, data); + info->pkg_id = topology_logical_package_id(cpu); + info->linux_cpu = cpu; + + return 0; +} + +static int tpmi_cpu_online(unsigned int cpu) +{ + struct tpmi_cpu_info *info = &per_cpu(tpmi_cpu_info, cpu); + int ret, index; + + /* Don't fail CPU online for some bad mapping of CPUs */ + ret = tpmi_get_logical_id(cpu, info); + if (ret) + return 0; + + index = info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id; + + guard(mutex)(&tpmi_lock); + cpumask_set_cpu(cpu, &tpmi_power_domain_mask[index]); + hash_add(tpmi_cpu_hash, &info->hnode, info->punit_core_id); + + domain_die_map[info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id] = + topology_die_id(cpu); + + return 0; +} + +static int __init tpmi_init(void) +{ + const struct x86_cpu_id *id; + u64 data; + int ret; + + id = x86_match_cpu(tpmi_cpu_ids); + if (!id) + return -ENODEV; + + /* Check for MSR 0x54 presence */ + ret = rdmsrq_safe(MSR_PM_LOGICAL_ID, &data); + if (ret) + return ret; + + tpmi_power_domain_mask = kzalloc_objs(*tpmi_power_domain_mask, + size_mul(topology_max_packages(), MAX_POWER_DOMAINS)); + if (!tpmi_power_domain_mask) + return -ENOMEM; + + domain_die_map = kcalloc(size_mul(topology_max_packages(), MAX_POWER_DOMAINS), + sizeof(*domain_die_map), GFP_KERNEL); + if (!domain_die_map) { + ret = -ENOMEM; + goto free_domain_mask; + } + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, + "platform/x86/tpmi_power_domains:online", + tpmi_cpu_online, NULL); + if (ret < 0) + goto free_domain_map; + + tpmi_hp_state = ret; + + return 0; + +free_domain_map: + kfree(domain_die_map); + +free_domain_mask: + kfree(tpmi_power_domain_mask); + + return ret; +} +module_init(tpmi_init) + +static void __exit tpmi_exit(void) +{ + cpuhp_remove_state(tpmi_hp_state); + kfree(tpmi_power_domain_mask); + kfree(domain_die_map); +} +module_exit(tpmi_exit) + +MODULE_DESCRIPTION("TPMI Power Domains Mapping"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/tpmi_power_domains.h b/drivers/platform/x86/intel/tpmi_power_domains.h new file mode 100644 index 000000000..2fd0dd7af --- /dev/null +++ b/drivers/platform/x86/intel/tpmi_power_domains.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Mapping of TPMI power domain and CPUs + * + * Copyright (c) 2024, Intel Corporation. + */ + +#ifndef _TPMI_POWER_DOMAINS_H_ +#define _TPMI_POWER_DOMAINS_H_ + +#include + +int tpmi_get_linux_cpu_number(int package_id, int die_id, int punit_core_id); +int tpmi_get_punit_core_number(int cpu_no); +int tpmi_get_power_domain_id(int cpu_no); +cpumask_t *tpmi_get_power_domain_mask(int cpu_no); +int tpmi_get_linux_die_id(int pkg_id, int domain_id); + +#endif diff --git a/drivers/platform/x86/intel/turbo_max_3.c b/drivers/platform/x86/intel/turbo_max_3.c new file mode 100644 index 000000000..b5af3e91b --- /dev/null +++ b/drivers/platform/x86/intel/turbo_max_3.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Turbo Boost Max Technology 3.0 legacy (non HWP) enumeration driver + * Copyright (c) 2017, Intel Corporation. + * All rights reserved. + * + * Author: Srinivas Pandruvada + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MSR_OC_MAILBOX 0x150 +#define MSR_OC_MAILBOX_CMD_OFFSET 32 +#define MSR_OC_MAILBOX_RSP_OFFSET 32 +#define MSR_OC_MAILBOX_BUSY_BIT 63 +#define OC_MAILBOX_FC_CONTROL_CMD 0x1C + +/* + * Typical latency to get mail box response is ~3us, It takes +3 us to + * process reading mailbox after issuing mailbox write on a Broadwell 3.4 GHz + * system. So for most of the time, the first mailbox read should have the + * response, but to avoid some boundary cases retry twice. + */ +#define OC_MAILBOX_RETRY_COUNT 2 + +static int get_oc_core_priority(unsigned int cpu) +{ + u64 value, cmd = OC_MAILBOX_FC_CONTROL_CMD; + int ret, i; + + /* Issue favored core read command */ + value = cmd << MSR_OC_MAILBOX_CMD_OFFSET; + /* Set the busy bit to indicate OS is trying to issue command */ + value |= BIT_ULL(MSR_OC_MAILBOX_BUSY_BIT); + ret = wrmsrq_safe(MSR_OC_MAILBOX, value); + if (ret) { + pr_debug("cpu %d OC mailbox write failed\n", cpu); + return ret; + } + + for (i = 0; i < OC_MAILBOX_RETRY_COUNT; ++i) { + ret = rdmsrq_safe(MSR_OC_MAILBOX, &value); + if (ret) { + pr_debug("cpu %d OC mailbox read failed\n", cpu); + break; + } + + if (value & BIT_ULL(MSR_OC_MAILBOX_BUSY_BIT)) { + pr_debug("cpu %d OC mailbox still processing\n", cpu); + ret = -EBUSY; + continue; + } + + if ((value >> MSR_OC_MAILBOX_RSP_OFFSET) & 0xff) { + pr_debug("cpu %d OC mailbox cmd failed\n", cpu); + ret = -ENXIO; + break; + } + + ret = value & 0xff; + pr_debug("cpu %d max_ratio %d\n", cpu, ret); + break; + } + + return ret; +} + +/* + * The work item is needed to avoid CPU hotplug locking issues. The function + * itmt_legacy_set_priority() is called from CPU online callback, so can't + * call sched_set_itmt_support() from there as this function will aquire + * hotplug locks in its path. + */ +static void itmt_legacy_work_fn(struct work_struct *work) +{ + sched_set_itmt_support(); +} + +static DECLARE_WORK(sched_itmt_work, itmt_legacy_work_fn); + +static int itmt_legacy_cpu_online(unsigned int cpu) +{ + static u32 max_highest_perf = 0, min_highest_perf = U32_MAX; + int priority; + + priority = get_oc_core_priority(cpu); + if (priority < 0) + return 0; + + sched_set_itmt_core_prio(priority, cpu); + + /* Enable ITMT feature when a core with different priority is found */ + if (max_highest_perf <= min_highest_perf) { + if (priority > max_highest_perf) + max_highest_perf = priority; + + if (priority < min_highest_perf) + min_highest_perf = priority; + + if (max_highest_perf > min_highest_perf) + schedule_work(&sched_itmt_work); + } + + return 0; +} + +static const struct x86_cpu_id itmt_legacy_cpu_ids[] = { + X86_MATCH_VFM(INTEL_BROADWELL_X, NULL), + X86_MATCH_VFM(INTEL_SKYLAKE_X, NULL), + {} +}; + +static int __init itmt_legacy_init(void) +{ + const struct x86_cpu_id *id; + int ret; + + id = x86_match_cpu(itmt_legacy_cpu_ids); + if (!id) + return -ENODEV; + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, + "platform/x86/turbo_max_3:online", + itmt_legacy_cpu_online, NULL); + if (ret < 0) + return ret; + + return 0; +} +late_initcall(itmt_legacy_init) diff --git a/drivers/platform/x86/intel/uncore-frequency/Kconfig b/drivers/platform/x86/intel/uncore-frequency/Kconfig new file mode 100644 index 000000000..a56d55056 --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/Kconfig @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Uncore Frquency control drivers +# + +menu "Intel Uncore Frequency Control" + depends on X86_64 || COMPILE_TEST + +config INTEL_UNCORE_FREQ_CONTROL_TPMI + tristate + +config INTEL_UNCORE_FREQ_CONTROL + tristate "Intel Uncore frequency control driver" + depends on X86_64 + select INTEL_UNCORE_FREQ_CONTROL_TPMI if INTEL_TPMI + help + This driver allows control of Uncore frequency limits on + supported server platforms. + + Uncore frequency controls RING/LLC (last-level cache) clocks. + + To compile this driver as a module, choose M here: the module + will be called intel-uncore-frequency. + +endmenu diff --git a/drivers/platform/x86/intel/uncore-frequency/Makefile b/drivers/platform/x86/intel/uncore-frequency/Makefile new file mode 100644 index 000000000..08ff57492 --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/intel/uncore-frequency +# + +obj-$(CONFIG_INTEL_UNCORE_FREQ_CONTROL) += intel-uncore-frequency.o +intel-uncore-frequency-y := uncore-frequency.o +obj-$(CONFIG_INTEL_UNCORE_FREQ_CONTROL) += intel-uncore-frequency-common.o +intel-uncore-frequency-common-y := uncore-frequency-common.o +obj-$(CONFIG_INTEL_UNCORE_FREQ_CONTROL_TPMI) += intel-uncore-frequency-tpmi.o +intel-uncore-frequency-tpmi-y := uncore-frequency-tpmi.o diff --git a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.c b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.c new file mode 100644 index 000000000..4bcafedf6 --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.c @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Uncore Frequency Control: Common code implementation + * Copyright (c) 2022, Intel Corporation. + * All rights reserved. + * + */ +#include +#include +#include "uncore-frequency-common.h" + +/* Mutex to control all mutual exclusions */ +static DEFINE_MUTEX(uncore_lock); +/* Root of the all uncore sysfs kobjs */ +static struct kobject *uncore_root_kobj; +/* uncore instance count */ +static int uncore_instance_count; + +static DEFINE_IDA(intel_uncore_ida); + +/* callbacks for actual HW read/write */ +static int (*uncore_read)(struct uncore_data *data, unsigned int *value, enum uncore_index index); +static int (*uncore_write)(struct uncore_data *data, unsigned int input, enum uncore_index index); + +static ssize_t show_domain_id(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct uncore_data *data = container_of(attr, struct uncore_data, domain_id_kobj_attr); + + return sysfs_emit(buf, "%u\n", data->domain_id); +} + +static ssize_t show_instance_id(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct uncore_data *data = container_of(attr, struct uncore_data, instance_id_kobj_attr); + + return sysfs_emit(buf, "%u\n", data->instance_id); +} + +static ssize_t show_fabric_cluster_id(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct uncore_data *data = container_of(attr, struct uncore_data, fabric_cluster_id_kobj_attr); + + return sysfs_emit(buf, "%u\n", data->cluster_id); +} + +static ssize_t show_package_id(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct uncore_data *data = container_of(attr, struct uncore_data, package_id_kobj_attr); + + return sysfs_emit(buf, "%u\n", data->package_id); +} + +#define MAX_UNCORE_AGENT_TYPES 4 + +/* The order follows AGENT_TYPE_* defines */ +static const char *agent_name[MAX_UNCORE_AGENT_TYPES] = {"core", "cache", "memory", "io"}; + +static ssize_t show_agent_types(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct uncore_data *data = container_of(attr, struct uncore_data, agent_types_kobj_attr); + unsigned long agent_mask = data->agent_type_mask; + int agent, length = 0; + + for_each_set_bit(agent, &agent_mask, MAX_UNCORE_AGENT_TYPES) { + if (length) + length += sysfs_emit_at(buf, length, " "); + + length += sysfs_emit_at(buf, length, "%s", agent_name[agent]); + } + + length += sysfs_emit_at(buf, length, "\n"); + + return length; +} + +static ssize_t show_attr(struct uncore_data *data, char *buf, enum uncore_index index) +{ + unsigned int value; + int ret; + + mutex_lock(&uncore_lock); + ret = uncore_read(data, &value, index); + mutex_unlock(&uncore_lock); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", value); +} + +static ssize_t store_attr(struct uncore_data *data, const char *buf, ssize_t count, + enum uncore_index index) +{ + unsigned int input = 0; + int ret; + + if (index == UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE) { + if (kstrtobool(buf, (bool *)&input)) + return -EINVAL; + } else { + if (kstrtouint(buf, 10, &input)) + return -EINVAL; + } + + mutex_lock(&uncore_lock); + ret = uncore_write(data, input, index); + mutex_unlock(&uncore_lock); + + if (ret) + return ret; + + return count; +} + +#define store_uncore_attr(name, index) \ + static ssize_t store_##name(struct kobject *kobj, \ + struct kobj_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct uncore_data *data = container_of(attr, struct uncore_data, name##_kobj_attr);\ + \ + return store_attr(data, buf, count, index); \ + } + +#define show_uncore_attr(name, index) \ + static ssize_t show_##name(struct kobject *kobj, \ + struct kobj_attribute *attr, char *buf)\ + { \ + struct uncore_data *data = container_of(attr, struct uncore_data, name##_kobj_attr);\ + \ + return show_attr(data, buf, index); \ + } + +store_uncore_attr(min_freq_khz, UNCORE_INDEX_MIN_FREQ); +store_uncore_attr(max_freq_khz, UNCORE_INDEX_MAX_FREQ); + +show_uncore_attr(min_freq_khz, UNCORE_INDEX_MIN_FREQ); +show_uncore_attr(max_freq_khz, UNCORE_INDEX_MAX_FREQ); + +show_uncore_attr(current_freq_khz, UNCORE_INDEX_CURRENT_FREQ); + +store_uncore_attr(elc_low_threshold_percent, UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD); +store_uncore_attr(elc_high_threshold_percent, UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD); +store_uncore_attr(elc_high_threshold_enable, + UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE); +store_uncore_attr(elc_floor_freq_khz, UNCORE_INDEX_EFF_LAT_CTRL_FREQ); + +show_uncore_attr(elc_low_threshold_percent, UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD); +show_uncore_attr(elc_high_threshold_percent, UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD); +show_uncore_attr(elc_high_threshold_enable, + UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE); +show_uncore_attr(elc_floor_freq_khz, UNCORE_INDEX_EFF_LAT_CTRL_FREQ); + +show_uncore_attr(die_id, UNCORE_INDEX_DIE_ID); + +#define show_uncore_data(member_name) \ + static ssize_t show_##member_name(struct kobject *kobj, \ + struct kobj_attribute *attr, char *buf)\ + { \ + struct uncore_data *data = container_of(attr, struct uncore_data,\ + member_name##_kobj_attr);\ + \ + return sysfs_emit(buf, "%u\n", \ + data->member_name); \ + } \ + +show_uncore_data(initial_min_freq_khz); +show_uncore_data(initial_max_freq_khz); + +#define init_attribute_rw(_name) \ + do { \ + sysfs_attr_init(&data->_name##_kobj_attr.attr); \ + data->_name##_kobj_attr.show = show_##_name; \ + data->_name##_kobj_attr.store = store_##_name; \ + data->_name##_kobj_attr.attr.name = #_name; \ + data->_name##_kobj_attr.attr.mode = 0644; \ + } while (0) + +#define init_attribute_ro(_name) \ + do { \ + sysfs_attr_init(&data->_name##_kobj_attr.attr); \ + data->_name##_kobj_attr.show = show_##_name; \ + data->_name##_kobj_attr.store = NULL; \ + data->_name##_kobj_attr.attr.name = #_name; \ + data->_name##_kobj_attr.attr.mode = 0444; \ + } while (0) + +#define init_attribute_root_ro(_name) \ + do { \ + sysfs_attr_init(&data->_name##_kobj_attr.attr); \ + data->_name##_kobj_attr.show = show_##_name; \ + data->_name##_kobj_attr.store = NULL; \ + data->_name##_kobj_attr.attr.name = #_name; \ + data->_name##_kobj_attr.attr.mode = 0400; \ + } while (0) + +static int create_attr_group(struct uncore_data *data, char *name) +{ + int ret, index = 0; + unsigned int val; + + init_attribute_rw(max_freq_khz); + init_attribute_rw(min_freq_khz); + init_attribute_ro(initial_min_freq_khz); + init_attribute_ro(initial_max_freq_khz); + init_attribute_root_ro(current_freq_khz); + + if (data->domain_id != UNCORE_DOMAIN_ID_INVALID) { + init_attribute_root_ro(domain_id); + data->uncore_attrs[index++] = &data->domain_id_kobj_attr.attr; + init_attribute_root_ro(instance_id); + data->uncore_attrs[index++] = &data->instance_id_kobj_attr.attr; + + init_attribute_root_ro(fabric_cluster_id); + data->uncore_attrs[index++] = &data->fabric_cluster_id_kobj_attr.attr; + init_attribute_root_ro(package_id); + data->uncore_attrs[index++] = &data->package_id_kobj_attr.attr; + if (data->agent_type_mask) { + init_attribute_ro(agent_types); + data->uncore_attrs[index++] = &data->agent_types_kobj_attr.attr; + } + if (topology_max_dies_per_package() > 1 && + data->agent_type_mask & AGENT_TYPE_CORE) { + init_attribute_ro(die_id); + data->uncore_attrs[index++] = &data->die_id_kobj_attr.attr; + } + } + + data->uncore_attrs[index++] = &data->max_freq_khz_kobj_attr.attr; + data->uncore_attrs[index++] = &data->min_freq_khz_kobj_attr.attr; + data->uncore_attrs[index++] = &data->initial_min_freq_khz_kobj_attr.attr; + data->uncore_attrs[index++] = &data->initial_max_freq_khz_kobj_attr.attr; + + ret = uncore_read(data, &val, UNCORE_INDEX_CURRENT_FREQ); + if (!ret) + data->uncore_attrs[index++] = &data->current_freq_khz_kobj_attr.attr; + + ret = uncore_read(data, &val, UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD); + if (!ret) { + init_attribute_rw(elc_low_threshold_percent); + init_attribute_rw(elc_high_threshold_percent); + init_attribute_rw(elc_high_threshold_enable); + init_attribute_rw(elc_floor_freq_khz); + + data->uncore_attrs[index++] = &data->elc_low_threshold_percent_kobj_attr.attr; + data->uncore_attrs[index++] = &data->elc_high_threshold_percent_kobj_attr.attr; + data->uncore_attrs[index++] = + &data->elc_high_threshold_enable_kobj_attr.attr; + data->uncore_attrs[index++] = &data->elc_floor_freq_khz_kobj_attr.attr; + } + + data->uncore_attrs[index] = NULL; + + data->uncore_attr_group.name = name; + data->uncore_attr_group.attrs = data->uncore_attrs; + ret = sysfs_create_group(uncore_root_kobj, &data->uncore_attr_group); + + return ret; +} + +static void delete_attr_group(struct uncore_data *data, char *name) +{ + sysfs_remove_group(uncore_root_kobj, &data->uncore_attr_group); +} + +int uncore_freq_add_entry(struct uncore_data *data, int cpu) +{ + int ret = 0; + + mutex_lock(&uncore_lock); + if (data->valid) { + /* control cpu changed */ + data->control_cpu = cpu; + goto uncore_unlock; + } + + if (data->domain_id != UNCORE_DOMAIN_ID_INVALID) { + ret = ida_alloc(&intel_uncore_ida, GFP_KERNEL); + if (ret < 0) + goto uncore_unlock; + + data->seqnum_id = ret; + scnprintf(data->name, sizeof(data->name), "uncore%02d", ret); + } else { + scnprintf(data->name, sizeof(data->name), "package_%02d_die_%02d", + data->package_id, data->die_id); + } + + /* + * Set the control CPU before any read path so entry recreation after CPU + * hotplug can populate read-only attributes from the new online CPU. + */ + data->control_cpu = cpu; + uncore_read(data, &data->initial_min_freq_khz, UNCORE_INDEX_MIN_FREQ); + uncore_read(data, &data->initial_max_freq_khz, UNCORE_INDEX_MAX_FREQ); + + ret = create_attr_group(data, data->name); + if (ret) { + data->control_cpu = -1; + if (data->domain_id != UNCORE_DOMAIN_ID_INVALID) + ida_free(&intel_uncore_ida, data->seqnum_id); + } else { + data->valid = true; + } + +uncore_unlock: + mutex_unlock(&uncore_lock); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(uncore_freq_add_entry, "INTEL_UNCORE_FREQUENCY"); + +void uncore_freq_remove_die_entry(struct uncore_data *data) +{ + mutex_lock(&uncore_lock); + delete_attr_group(data, data->name); + data->control_cpu = -1; + data->valid = false; + if (data->domain_id != UNCORE_DOMAIN_ID_INVALID) + ida_free(&intel_uncore_ida, data->seqnum_id); + + mutex_unlock(&uncore_lock); +} +EXPORT_SYMBOL_NS_GPL(uncore_freq_remove_die_entry, "INTEL_UNCORE_FREQUENCY"); + +int uncore_freq_common_init(int (*read)(struct uncore_data *data, unsigned int *value, + enum uncore_index index), + int (*write)(struct uncore_data *data, unsigned int input, + enum uncore_index index)) +{ + mutex_lock(&uncore_lock); + + uncore_read = read; + uncore_write = write; + + if (!uncore_root_kobj) { + struct device *dev_root = bus_get_dev_root(&cpu_subsys); + + if (dev_root) { + uncore_root_kobj = kobject_create_and_add("intel_uncore_frequency", + &dev_root->kobj); + put_device(dev_root); + } + } + if (uncore_root_kobj) + ++uncore_instance_count; + mutex_unlock(&uncore_lock); + + return uncore_root_kobj ? 0 : -ENOMEM; +} +EXPORT_SYMBOL_NS_GPL(uncore_freq_common_init, "INTEL_UNCORE_FREQUENCY"); + +void uncore_freq_common_exit(void) +{ + mutex_lock(&uncore_lock); + --uncore_instance_count; + if (!uncore_instance_count) { + kobject_put(uncore_root_kobj); + uncore_root_kobj = NULL; + } + mutex_unlock(&uncore_lock); +} +EXPORT_SYMBOL_NS_GPL(uncore_freq_common_exit, "INTEL_UNCORE_FREQUENCY"); + + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel Uncore Frequency Common Module"); diff --git a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.h b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.h new file mode 100644 index 000000000..e319448dc --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-common.h @@ -0,0 +1,121 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Intel Uncore Frequency Control: Common defines and prototypes + * Copyright (c) 2022, Intel Corporation. + * All rights reserved. + * + */ + +#ifndef __INTEL_UNCORE_FREQ_COMMON_H +#define __INTEL_UNCORE_FREQ_COMMON_H + +#include + +/* + * Define uncore agents, which are under uncore frequency control. + * Defined in the same order as specified in the TPMI UFS Specifications. + * It is possible that there are common uncore frequency control to more than + * one hardware agents. So, these defines are used as a bit mask. +*/ + +#define AGENT_TYPE_CORE 0x01 +#define AGENT_TYPE_CACHE 0x02 +#define AGENT_TYPE_MEMORY 0x04 +#define AGENT_TYPE_IO 0x08 + +/** + * struct uncore_data - Encapsulate all uncore data + * @stored_uncore_data: Last user changed MSR 620 value, which will be restored + * on system resume. + * @initial_min_freq_khz: Sampled minimum uncore frequency at driver init + * @initial_max_freq_khz: Sampled maximum uncore frequency at driver init + * @control_cpu: Designated CPU for a die to read/write + * @valid: Mark the data valid/invalid + * @package_id: Package id for this instance + * @die_id: Die id for this instance + * @domain_id: Power domain id for this instance + * @cluster_id: cluster id in a domain + * @seqnum_id: Unique sequential id to append to directory name + * @instance_id: Die indices or feature instances for a single TPMI device + * @name: Sysfs entry name for this instance + * @agent_type_mask: Bit mask of all hardware agents for this domain + * @uncore_attr_group: Attribute group storage + * @max_freq_khz_kobj_attr: Storage for kobject attribute max_freq_khz + * @min_freq_khz_kobj_attr: Storage for kobject attribute min_freq_khz + * @initial_max_freq_khz_kobj_attr: Storage for kobject attribute initial_max_freq_khz + * @initial_min_freq_khz_kobj_attr: Storage for kobject attribute initial_min_freq_khz + * @current_freq_khz_kobj_attr: Storage for kobject attribute current_freq_khz + * @domain_id_kobj_attr: Storage for kobject attribute domain_id + * @fabric_cluster_id_kobj_attr: Storage for kobject attribute fabric_cluster_id + * @package_id_kobj_attr: Storage for kobject attribute package_id + * @elc_low_threshold_percent_kobj_attr: + * Storage for kobject attribute elc_low_threshold_percent + * @elc_high_threshold_percent_kobj_attr: + * Storage for kobject attribute elc_high_threshold_percent + * @elc_high_threshold_enable_kobj_attr: + * Storage for kobject attribute elc_high_threshold_enable + * @elc_floor_freq_khz_kobj_attr: Storage for kobject attribute elc_floor_freq_khz + * @agent_types_kobj_attr: Storage for kobject attribute agent_type + * @die_id_kobj_attr: Attribute storage for die_id information + * @instance_id_kobj_attr: Attribute storage for instance_id value + * @uncore_attrs: Attribute storage for group creation + * + * This structure is used to encapsulate all data related to uncore sysfs + * settings for a die/package. + */ +struct uncore_data { + u64 stored_uncore_data; + u32 initial_min_freq_khz; + u32 initial_max_freq_khz; + int control_cpu; + bool valid; + int package_id; + int die_id; + int domain_id; + int cluster_id; + int seqnum_id; + int instance_id; + char name[32]; + u16 agent_type_mask; + + struct attribute_group uncore_attr_group; + struct kobj_attribute max_freq_khz_kobj_attr; + struct kobj_attribute min_freq_khz_kobj_attr; + struct kobj_attribute initial_max_freq_khz_kobj_attr; + struct kobj_attribute initial_min_freq_khz_kobj_attr; + struct kobj_attribute current_freq_khz_kobj_attr; + struct kobj_attribute domain_id_kobj_attr; + struct kobj_attribute fabric_cluster_id_kobj_attr; + struct kobj_attribute package_id_kobj_attr; + struct kobj_attribute elc_low_threshold_percent_kobj_attr; + struct kobj_attribute elc_high_threshold_percent_kobj_attr; + struct kobj_attribute elc_high_threshold_enable_kobj_attr; + struct kobj_attribute elc_floor_freq_khz_kobj_attr; + struct kobj_attribute agent_types_kobj_attr; + struct kobj_attribute die_id_kobj_attr; + struct kobj_attribute instance_id_kobj_attr; + struct attribute *uncore_attrs[16]; +}; + +#define UNCORE_DOMAIN_ID_INVALID -1 + +enum uncore_index { + UNCORE_INDEX_MIN_FREQ, + UNCORE_INDEX_MAX_FREQ, + UNCORE_INDEX_CURRENT_FREQ, + UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD, + UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD, + UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE, + UNCORE_INDEX_EFF_LAT_CTRL_FREQ, + UNCORE_INDEX_DIE_ID, +}; + +int uncore_freq_common_init(int (*read)(struct uncore_data *data, unsigned int *value, + enum uncore_index index), + int (*write)(struct uncore_data *data, unsigned int input, + enum uncore_index index)); +void uncore_freq_common_exit(void); +int uncore_freq_add_entry(struct uncore_data *data, int cpu); +void uncore_freq_remove_die_entry(struct uncore_data *data); + +#endif diff --git a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c new file mode 100644 index 000000000..44e2a9000 --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * uncore-frquency-tpmi: Uncore frequency scaling using TPMI + * + * Copyright (c) 2023, Intel Corporation. + * All Rights Reserved. + * + * The hardware interface to read/write is basically substitution of + * MSR 0x620 and 0x621. + * There are specific MMIO offset and bits to get/set minimum and + * maximum uncore ratio, similar to MSRs. + * The scope of the uncore MSRs was package scope. But TPMI allows + * new gen CPUs to have multiple uncore controls at uncore-cluster + * level. Each package can have multiple power domains which further + * can have multiple clusters. + * Here number of power domains = number of resources in this aux + * device. There are offsets and bits to discover number of clusters + * and offset for each cluster level controls. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "../tpmi_power_domains.h" +#include "uncore-frequency-common.h" + +#define UNCORE_MAJOR_VERSION 0 +#define UNCORE_MINOR_VERSION 3 +#define UNCORE_ELC_SUPPORTED_VERSION 2 +#define UNCORE_HEADER_INDEX 0 +#define UNCORE_FABRIC_CLUSTER_OFFSET 8 + +/* status + control + adv_ctl1 + adv_ctl2 */ +#define UNCORE_FABRIC_CLUSTER_SIZE (4 * 8) + +#define UNCORE_STATUS_INDEX 0 +#define UNCORE_CONTROL_INDEX 8 + +#define UNCORE_FREQ_KHZ_MULTIPLIER 100000 + +struct tpmi_uncore_struct; + +/* Information for each cluster */ +struct tpmi_uncore_cluster_info { + bool root_domain; + bool elc_supported; + u8 __iomem *cluster_base; + u16 cdie_id; + struct uncore_data uncore_data; + struct tpmi_uncore_struct *uncore_root; +}; + +/* Information for each power domain */ +struct tpmi_uncore_power_domain_info { + u8 __iomem *uncore_base; + int ufs_header_ver; + int cluster_count; + struct tpmi_uncore_cluster_info *cluster_infos; +}; + +/* Information for all power domains in a package */ +struct tpmi_uncore_struct { + int power_domain_count; + int max_ratio; + int min_ratio; + struct tpmi_uncore_power_domain_info *pd_info; + struct tpmi_uncore_cluster_info root_cluster; + bool write_blocked; +}; + +/* Bit definitions for STATUS register */ +#define UNCORE_CURRENT_RATIO_MASK GENMASK_ULL(6, 0) + +/* Bit definitions for CONTROL register */ +#define UNCORE_MAX_RATIO_MASK GENMASK_ULL(14, 8) +#define UNCORE_MIN_RATIO_MASK GENMASK_ULL(21, 15) +#define UNCORE_EFF_LAT_CTRL_RATIO_MASK GENMASK_ULL(28, 22) +#define UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK GENMASK_ULL(38, 32) +#define UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE BIT(39) +#define UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK GENMASK_ULL(46, 40) + +/* Helper function to read MMIO offset for max/min control frequency */ +static void read_control_freq(struct tpmi_uncore_cluster_info *cluster_info, + unsigned int *value, enum uncore_index index) +{ + u64 control; + + control = readq(cluster_info->cluster_base + UNCORE_CONTROL_INDEX); + if (index == UNCORE_INDEX_MAX_FREQ) + *value = FIELD_GET(UNCORE_MAX_RATIO_MASK, control) * UNCORE_FREQ_KHZ_MULTIPLIER; + else + *value = FIELD_GET(UNCORE_MIN_RATIO_MASK, control) * UNCORE_FREQ_KHZ_MULTIPLIER; +} + +/* Helper function to read efficiency latency control values over MMIO */ +static int read_eff_lat_ctrl(struct uncore_data *data, unsigned int *val, enum uncore_index index) +{ + struct tpmi_uncore_cluster_info *cluster_info; + u64 ctrl; + + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + if (cluster_info->root_domain) + return -ENODATA; + + if (!cluster_info->elc_supported) + return -EOPNOTSUPP; + + ctrl = readq(cluster_info->cluster_base + UNCORE_CONTROL_INDEX); + + switch (index) { + case UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD: + *val = FIELD_GET(UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK, ctrl); + *val *= 100; + *val = DIV_ROUND_UP(*val, FIELD_MAX(UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK)); + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD: + *val = FIELD_GET(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK, ctrl); + *val *= 100; + *val = DIV_ROUND_UP(*val, FIELD_MAX(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK)); + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE: + *val = FIELD_GET(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE, ctrl); + break; + case UNCORE_INDEX_EFF_LAT_CTRL_FREQ: + *val = FIELD_GET(UNCORE_EFF_LAT_CTRL_RATIO_MASK, ctrl) * UNCORE_FREQ_KHZ_MULTIPLIER; + break; + + default: + return -EOPNOTSUPP; + } + + return 0; +} + +#define UNCORE_MAX_RATIO FIELD_MAX(UNCORE_MAX_RATIO_MASK) + +/* Helper for sysfs read for max/min frequencies. Called under mutex locks */ +static int uncore_read_control_freq(struct uncore_data *data, unsigned int *value, + enum uncore_index index) +{ + struct tpmi_uncore_cluster_info *cluster_info; + + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + + if (cluster_info->root_domain) { + struct tpmi_uncore_struct *uncore_root = cluster_info->uncore_root; + unsigned int min, max, v; + int i; + + min = UNCORE_MAX_RATIO * UNCORE_FREQ_KHZ_MULTIPLIER; + max = 0; + + /* + * Get the max/min by looking at each cluster. Get the lowest + * min and highest max. + */ + for (i = 0; i < uncore_root->power_domain_count; ++i) { + int j; + + for (j = 0; j < uncore_root->pd_info[i].cluster_count; ++j) { + read_control_freq(&uncore_root->pd_info[i].cluster_infos[j], + &v, index); + if (v < min) + min = v; + if (v > max) + max = v; + } + } + + if (index == UNCORE_INDEX_MIN_FREQ) + *value = min; + else + *value = max; + + return 0; + } + + read_control_freq(cluster_info, value, index); + + return 0; +} + +/* Helper function for writing efficiency latency control values over MMIO */ +static int write_eff_lat_ctrl(struct uncore_data *data, unsigned int val, enum uncore_index index) +{ + struct tpmi_uncore_cluster_info *cluster_info; + struct tpmi_uncore_struct *uncore_root; + u64 control; + + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + uncore_root = cluster_info->uncore_root; + + if (uncore_root->write_blocked) + return -EPERM; + + if (cluster_info->root_domain) + return -ENODATA; + + if (!cluster_info->elc_supported) + return -EOPNOTSUPP; + + switch (index) { + case UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD: + if (val > 100) + return -EINVAL; + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD: + if (val > 100) + return -EINVAL; + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE: + if (val > 1) + return -EINVAL; + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_FREQ: + val /= UNCORE_FREQ_KHZ_MULTIPLIER; + if (val > FIELD_MAX(UNCORE_EFF_LAT_CTRL_RATIO_MASK)) + return -EINVAL; + break; + + default: + return -EOPNOTSUPP; + } + + control = readq(cluster_info->cluster_base + UNCORE_CONTROL_INDEX); + + switch (index) { + case UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD: + val *= FIELD_MAX(UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK); + val /= 100; + control &= ~UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK; + control |= FIELD_PREP(UNCORE_EFF_LAT_CTRL_LOW_THRESHOLD_MASK, val); + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD: + val *= FIELD_MAX(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK); + val /= 100; + control &= ~UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK; + control |= FIELD_PREP(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_MASK, val); + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE: + control &= ~UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE; + control |= FIELD_PREP(UNCORE_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE, val); + break; + + case UNCORE_INDEX_EFF_LAT_CTRL_FREQ: + control &= ~UNCORE_EFF_LAT_CTRL_RATIO_MASK; + control |= FIELD_PREP(UNCORE_EFF_LAT_CTRL_RATIO_MASK, val); + break; + + default: + break; + } + + writeq(control, cluster_info->cluster_base + UNCORE_CONTROL_INDEX); + + return 0; +} + +/* Helper function to write MMIO offset for max/min control frequency */ +static void write_control_freq(struct tpmi_uncore_cluster_info *cluster_info, unsigned int input, + unsigned int index) +{ + u64 control; + + control = readq(cluster_info->cluster_base + UNCORE_CONTROL_INDEX); + + if (index == UNCORE_INDEX_MAX_FREQ) { + control &= ~UNCORE_MAX_RATIO_MASK; + control |= FIELD_PREP(UNCORE_MAX_RATIO_MASK, input); + } else { + control &= ~UNCORE_MIN_RATIO_MASK; + control |= FIELD_PREP(UNCORE_MIN_RATIO_MASK, input); + } + + writeq(control, (cluster_info->cluster_base + UNCORE_CONTROL_INDEX)); +} + +/* Helper for sysfs write for max/min frequencies. Called under mutex locks */ +static int uncore_write_control_freq(struct uncore_data *data, unsigned int input, + enum uncore_index index) +{ + struct tpmi_uncore_cluster_info *cluster_info; + struct tpmi_uncore_struct *uncore_root; + + input /= UNCORE_FREQ_KHZ_MULTIPLIER; + if (!input || input > UNCORE_MAX_RATIO) + return -EINVAL; + + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + uncore_root = cluster_info->uncore_root; + + if (uncore_root->write_blocked) + return -EPERM; + + /* Update each cluster in a package */ + if (cluster_info->root_domain) { + struct tpmi_uncore_struct *uncore_root = cluster_info->uncore_root; + int i; + + for (i = 0; i < uncore_root->power_domain_count; ++i) { + int j; + + for (j = 0; j < uncore_root->pd_info[i].cluster_count; ++j) + write_control_freq(&uncore_root->pd_info[i].cluster_infos[j], + input, index); + } + + if (index == UNCORE_INDEX_MAX_FREQ) + uncore_root->max_ratio = input; + else + uncore_root->min_ratio = input; + + return 0; + } + + if (index == UNCORE_INDEX_MAX_FREQ && uncore_root->max_ratio && + uncore_root->max_ratio < input) + return -EINVAL; + + if (index == UNCORE_INDEX_MIN_FREQ && uncore_root->min_ratio && + uncore_root->min_ratio > input) + return -EINVAL; + + write_control_freq(cluster_info, input, index); + + return 0; +} + +/* Helper for sysfs read for the current uncore frequency. Called under mutex locks */ +static int uncore_read_freq(struct uncore_data *data, unsigned int *freq) +{ + struct tpmi_uncore_cluster_info *cluster_info; + u64 status; + + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + if (cluster_info->root_domain) + return -ENODATA; + + status = readq((u8 __iomem *)cluster_info->cluster_base + UNCORE_STATUS_INDEX); + *freq = FIELD_GET(UNCORE_CURRENT_RATIO_MASK, status) * UNCORE_FREQ_KHZ_MULTIPLIER; + + return 0; +} + +/* + * Agent types as per the TPMI UFS Specification for UFS_STATUS + * Agent Type - Core Bit: 23 + * Agent Type - Cache Bit: 24 + * Agent Type - Memory Bit: 25 + * Agent Type - IO Bit: 26 + */ + +#define UNCORE_AGENT_TYPES GENMASK_ULL(26, 23) + +/* Helper function to read agent type over MMIO and set the agent type mask */ +static void uncore_set_agent_type(struct tpmi_uncore_cluster_info *cluster_info) +{ + u64 status; + + status = readq((u8 __iomem *)cluster_info->cluster_base + UNCORE_STATUS_INDEX); + cluster_info->uncore_data.agent_type_mask = FIELD_GET(UNCORE_AGENT_TYPES, status); +} + +#define MAX_PARTITIONS 2 + +/* IO domain ID start index for a partition */ +static u8 io_die_start[MAX_PARTITIONS]; + +/* Next IO domain ID index after the current partition IO die IDs */ +static u8 io_die_index_next; + +/* Lock to protect io_die_start, io_die_index_next */ +static DEFINE_MUTEX(domain_lock); + +static inline void set_instance_id(int id, struct tpmi_uncore_cluster_info *cluster_info) +{ + /* + * On non-partitioned systems domain_id can be used for mapping both + * CPUs to compute die IDs and physical die indexes to MMIO mapped + * memory. However on partitioned systems domain_id loses the second + * association. Therefore instance_id should be used for that instead, + * while domain_id should still be used to match CPUs to compute dies. + */ + cluster_info->uncore_data.instance_id = id; +} + +static void set_domain_id(int id, int num_resources, + struct oobmsm_plat_info *plat_info, + struct tpmi_uncore_cluster_info *cluster_info) +{ + u8 part_io_index, cdie_range, pkg_io_index, max_dies; + + if (plat_info->partition >= MAX_PARTITIONS) { + cluster_info->uncore_data.domain_id = id; + return; + } + + if (cluster_info->uncore_data.agent_type_mask & AGENT_TYPE_CORE) { + cluster_info->uncore_data.domain_id = cluster_info->cdie_id; + return; + } + + /* Unlikely but cdie_mask may have holes, so take range */ + cdie_range = fls(plat_info->cdie_mask) - ffs(plat_info->cdie_mask) + 1; + max_dies = topology_max_dies_per_package(); + + /* + * If the CPU doesn't enumerate dies, then use current cdie range + * as the max. + */ + if (cdie_range > max_dies) + max_dies = cdie_range; + + guard(mutex)(&domain_lock); + + if (!io_die_index_next) + io_die_index_next = max_dies; + + if (!io_die_start[plat_info->partition]) { + io_die_start[plat_info->partition] = io_die_index_next; + /* + * number of IO dies = num_resources - cdie_range. Hence + * next partition io_die_index_next is set after IO dies + * in the current partition. + */ + io_die_index_next += (num_resources - cdie_range); + } + + /* + * Index from IO die start within the partition: + * This is the first valid domain after the cdies. + * For example the current resource index 5 and cdies end at + * index 3 (cdie_cnt = 4). Then the IO only index 5 - 4 = 1. + */ + part_io_index = id - cdie_range; + + /* + * Add to the IO die start index for this partition in this package + * to make unique in the package. + */ + pkg_io_index = io_die_start[plat_info->partition] + part_io_index; + + /* Assign this to domain ID */ + cluster_info->uncore_data.domain_id = pkg_io_index; +} + +/* Callback for sysfs read for TPMI uncore values. Called under mutex locks. */ +static int uncore_read(struct uncore_data *data, unsigned int *value, enum uncore_index index) +{ + struct tpmi_uncore_cluster_info *cluster_info; + int ret; + + switch (index) { + case UNCORE_INDEX_MIN_FREQ: + case UNCORE_INDEX_MAX_FREQ: + return uncore_read_control_freq(data, value, index); + + case UNCORE_INDEX_CURRENT_FREQ: + return uncore_read_freq(data, value); + + case UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD: + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD: + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE: + case UNCORE_INDEX_EFF_LAT_CTRL_FREQ: + return read_eff_lat_ctrl(data, value, index); + + case UNCORE_INDEX_DIE_ID: + cluster_info = container_of(data, struct tpmi_uncore_cluster_info, uncore_data); + ret = tpmi_get_linux_die_id(cluster_info->uncore_data.package_id, + cluster_info->cdie_id); + if (ret < 0) + return ret; + + *value = ret; + return 0; + + default: + break; + } + + return -EOPNOTSUPP; +} + +/* Callback for sysfs write for TPMI uncore data. Called under mutex locks. */ +static int uncore_write(struct uncore_data *data, unsigned int value, enum uncore_index index) +{ + switch (index) { + case UNCORE_INDEX_EFF_LAT_CTRL_LOW_THRESHOLD: + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD: + case UNCORE_INDEX_EFF_LAT_CTRL_HIGH_THRESHOLD_ENABLE: + case UNCORE_INDEX_EFF_LAT_CTRL_FREQ: + return write_eff_lat_ctrl(data, value, index); + + case UNCORE_INDEX_MIN_FREQ: + case UNCORE_INDEX_MAX_FREQ: + return uncore_write_control_freq(data, value, index); + + default: + break; + } + + return -EOPNOTSUPP; +} + +static void remove_cluster_entries(struct tpmi_uncore_struct *tpmi_uncore) +{ + int i; + + for (i = 0; i < tpmi_uncore->power_domain_count; ++i) { + struct tpmi_uncore_power_domain_info *pd_info; + int j; + + pd_info = &tpmi_uncore->pd_info[i]; + if (!pd_info->uncore_base) + continue; + + for (j = 0; j < pd_info->cluster_count; ++j) { + struct tpmi_uncore_cluster_info *cluster_info; + + cluster_info = &pd_info->cluster_infos[j]; + uncore_freq_remove_die_entry(&cluster_info->uncore_data); + } + } +} + +static void set_cdie_id(int domain_id, struct tpmi_uncore_cluster_info *cluster_info, + struct oobmsm_plat_info *plat_info) +{ + + cluster_info->cdie_id = domain_id; + + if (plat_info->cdie_mask && cluster_info->uncore_data.agent_type_mask & AGENT_TYPE_CORE) + cluster_info->cdie_id = domain_id + ffs(plat_info->cdie_mask) - 1; +} + +#define UNCORE_VERSION_MASK GENMASK_ULL(7, 0) +#define UNCORE_LOCAL_FABRIC_CLUSTER_ID_MASK GENMASK_ULL(15, 8) +#define UNCORE_CLUSTER_OFF_MASK GENMASK_ULL(7, 0) +#define UNCORE_AUTONOMOUS_UFS_DISABLED BIT(32) +#define UNCORE_MAX_CLUSTER_PER_DOMAIN 8 + +static int uncore_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + bool read_blocked = 0, write_blocked = 0; + struct oobmsm_plat_info *plat_info; + struct tpmi_uncore_struct *tpmi_uncore; + bool uncore_sysfs_added = false; + int ret, i, pkg = 0; + int num_resources; + + ret = tpmi_get_feature_status(auxdev, TPMI_ID_UNCORE, &read_blocked, &write_blocked); + if (ret) + dev_info(&auxdev->dev, "Can't read feature status: ignoring blocked status\n"); + + if (read_blocked) { + dev_info(&auxdev->dev, "Firmware has blocked reads, exiting\n"); + return -ENODEV; + } + + /* Get number of power domains, which is equal to number of resources */ + num_resources = tpmi_get_resource_count(auxdev); + if (!num_resources) + return -EINVAL; + + /* Register callbacks to uncore core */ + ret = uncore_freq_common_init(uncore_read, uncore_write); + if (ret) + return ret; + + /* Allocate uncore instance per package */ + tpmi_uncore = devm_kzalloc(&auxdev->dev, sizeof(*tpmi_uncore), GFP_KERNEL); + if (!tpmi_uncore) { + ret = -ENOMEM; + goto err_rem_common; + } + + /* Allocate memory for all power domains in a package */ + tpmi_uncore->pd_info = devm_kcalloc(&auxdev->dev, num_resources, + sizeof(*tpmi_uncore->pd_info), + GFP_KERNEL); + if (!tpmi_uncore->pd_info) { + ret = -ENOMEM; + goto err_rem_common; + } + + tpmi_uncore->power_domain_count = num_resources; + tpmi_uncore->write_blocked = write_blocked; + + /* Get the package ID from the TPMI core */ + plat_info = tpmi_get_platform_data(auxdev); + if (unlikely(!plat_info)) { + dev_info(&auxdev->dev, "Platform information is NULL\n"); + ret = -ENODEV; + goto err_rem_common; + } + + pkg = plat_info->package_id; + + for (i = 0; i < num_resources; ++i) { + struct tpmi_uncore_power_domain_info *pd_info; + bool auto_ufs_enabled; + struct resource *res; + u64 cluster_offset; + u8 cluster_mask; + int mask, j; + u64 header; + + res = tpmi_get_resource_at_index(auxdev, i); + if (!res) + continue; + + pd_info = &tpmi_uncore->pd_info[i]; + + pd_info->uncore_base = devm_ioremap_resource(&auxdev->dev, res); + if (IS_ERR(pd_info->uncore_base)) { + ret = PTR_ERR(pd_info->uncore_base); + /* + * Set to NULL so that clean up can still remove other + * entries already created if any by + * remove_cluster_entries() + */ + pd_info->uncore_base = NULL; + goto remove_clusters; + } + + /* Check for version and skip this resource if there is mismatch */ + header = readq(pd_info->uncore_base); + pd_info->ufs_header_ver = header & UNCORE_VERSION_MASK; + + if (pd_info->ufs_header_ver == TPMI_VERSION_INVALID) + continue; + + if (TPMI_MAJOR_VERSION(pd_info->ufs_header_ver) != UNCORE_MAJOR_VERSION) { + dev_err(&auxdev->dev, "Uncore: Unsupported major version:%lx\n", + TPMI_MAJOR_VERSION(pd_info->ufs_header_ver)); + ret = -ENODEV; + goto remove_clusters; + } + + if (TPMI_MINOR_VERSION(pd_info->ufs_header_ver) > UNCORE_MINOR_VERSION) + dev_info(&auxdev->dev, "Uncore: Ignore: Unsupported minor version:%lx\n", + TPMI_MINOR_VERSION(pd_info->ufs_header_ver)); + + /* Get Cluster ID Mask */ + cluster_mask = FIELD_GET(UNCORE_LOCAL_FABRIC_CLUSTER_ID_MASK, header); + if (!cluster_mask) { + dev_info(&auxdev->dev, "Uncore: Invalid cluster mask:%x\n", cluster_mask); + continue; + } + + auto_ufs_enabled = !(header & UNCORE_AUTONOMOUS_UFS_DISABLED); + + /* Find out number of clusters in this resource */ + pd_info->cluster_count = hweight8(cluster_mask); + + pd_info->cluster_infos = devm_kcalloc(&auxdev->dev, pd_info->cluster_count, + sizeof(struct tpmi_uncore_cluster_info), + GFP_KERNEL); + if (!pd_info->cluster_infos) { + ret = -ENOMEM; + goto remove_clusters; + } + /* + * Each byte in the register point to status and control + * registers belonging to cluster id 0-8. + */ + cluster_offset = readq(pd_info->uncore_base + + UNCORE_FABRIC_CLUSTER_OFFSET); + + for (j = 0; j < pd_info->cluster_count; ++j) { + struct tpmi_uncore_cluster_info *cluster_info; + + /* Get the offset for this cluster */ + mask = (cluster_offset & UNCORE_CLUSTER_OFF_MASK); + /* Offset in QWORD, so change to bytes */ + mask <<= 3; + + cluster_info = &pd_info->cluster_infos[j]; + + cluster_info->cluster_base = pd_info->uncore_base + mask; + + uncore_set_agent_type(cluster_info); + + cluster_info->uncore_data.package_id = pkg; + /* There are no dies like Cascade Lake */ + cluster_info->uncore_data.die_id = 0; + cluster_info->uncore_data.cluster_id = j; + + set_cdie_id(i, cluster_info, plat_info); + + set_domain_id(i, num_resources, plat_info, cluster_info); + set_instance_id(i, cluster_info); + + cluster_info->uncore_root = tpmi_uncore; + + if ((TPMI_MINOR_VERSION(pd_info->ufs_header_ver) >= + UNCORE_ELC_SUPPORTED_VERSION) && + auto_ufs_enabled) + cluster_info->elc_supported = true; + + ret = uncore_freq_add_entry(&cluster_info->uncore_data, 0); + if (ret) { + cluster_info->cluster_base = NULL; + goto remove_clusters; + } + /* Point to next cluster offset */ + cluster_offset >>= UNCORE_MAX_CLUSTER_PER_DOMAIN; + uncore_sysfs_added = true; + } + } + + if (!uncore_sysfs_added) { + ret = -ENODEV; + goto remove_clusters; + } + + auxiliary_set_drvdata(auxdev, tpmi_uncore); + + if (topology_max_dies_per_package() > 1 || plat_info->partition) + return 0; + + tpmi_uncore->root_cluster.root_domain = true; + tpmi_uncore->root_cluster.uncore_root = tpmi_uncore; + + tpmi_uncore->root_cluster.uncore_data.package_id = pkg; + tpmi_uncore->root_cluster.uncore_data.domain_id = UNCORE_DOMAIN_ID_INVALID; + ret = uncore_freq_add_entry(&tpmi_uncore->root_cluster.uncore_data, 0); + if (ret) + goto remove_clusters; + + return 0; + +remove_clusters: + remove_cluster_entries(tpmi_uncore); +err_rem_common: + uncore_freq_common_exit(); + + return ret; +} + +static void uncore_remove(struct auxiliary_device *auxdev) +{ + struct tpmi_uncore_struct *tpmi_uncore = auxiliary_get_drvdata(auxdev); + + if (tpmi_uncore->root_cluster.root_domain) + uncore_freq_remove_die_entry(&tpmi_uncore->root_cluster.uncore_data); + + remove_cluster_entries(tpmi_uncore); + + uncore_freq_common_exit(); +} + +static const struct auxiliary_device_id intel_uncore_id_table[] = { + { .name = "intel_vsec.tpmi-uncore" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, intel_uncore_id_table); + +static struct auxiliary_driver intel_uncore_aux_driver = { + .id_table = intel_uncore_id_table, + .remove = uncore_remove, + .probe = uncore_probe, +}; + +module_auxiliary_driver(intel_uncore_aux_driver); + +MODULE_IMPORT_NS("INTEL_TPMI"); +MODULE_IMPORT_NS("INTEL_UNCORE_FREQUENCY"); +MODULE_IMPORT_NS("INTEL_TPMI_POWER_DOMAIN"); +MODULE_DESCRIPTION("Intel TPMI UFS Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency.c b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency.c new file mode 100644 index 000000000..667f2c8b9 --- /dev/null +++ b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency.c @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Uncore Frequency Setting + * Copyright (c) 2022, Intel Corporation. + * All rights reserved. + * + * Provide interface to set MSR 620 at a granularity of per die. On CPU online, + * one control CPU is identified per die to read/write limit. This control CPU + * is changed, if the CPU state is changed to offline. When the last CPU is + * offline in a die then remove the sysfs object for that die. + * The majority of actual code is related to sysfs create and read/write + * attributes. + * + * Author: Srinivas Pandruvada + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "uncore-frequency-common.h" + +/* Max instances for uncore data, one for each die */ +static int uncore_max_entries __read_mostly; +/* Storage for uncore data for all instances */ +static struct uncore_data *uncore_instances; +/* Stores the CPU mask of the target CPUs to use during uncore read/write */ +static cpumask_t uncore_cpu_mask; +/* CPU online callback register instance */ +static enum cpuhp_state uncore_hp_state __read_mostly; + +#define MSR_UNCORE_RATIO_LIMIT 0x620 +#define MSR_UNCORE_PERF_STATUS 0x621 +#define UNCORE_FREQ_KHZ_MULTIPLIER 100000 + +#define UNCORE_MAX_RATIO_MASK GENMASK_ULL(6, 0) +#define UNCORE_MIN_RATIO_MASK GENMASK_ULL(14, 8) + +#define UNCORE_CURRENT_RATIO_MASK GENMASK_ULL(6, 0) + +static int uncore_read_control_freq(struct uncore_data *data, unsigned int *value, + enum uncore_index index) +{ + u64 cap; + int ret; + + if (data->control_cpu < 0) + return -ENXIO; + + ret = rdmsrq_on_cpu(data->control_cpu, MSR_UNCORE_RATIO_LIMIT, &cap); + if (ret) + return ret; + + if (index == UNCORE_INDEX_MAX_FREQ) + *value = FIELD_GET(UNCORE_MAX_RATIO_MASK, cap) * UNCORE_FREQ_KHZ_MULTIPLIER; + else + *value = FIELD_GET(UNCORE_MIN_RATIO_MASK, cap) * UNCORE_FREQ_KHZ_MULTIPLIER; + + return 0; +} + +static int uncore_write_control_freq(struct uncore_data *data, unsigned int input, + enum uncore_index index) +{ + int ret; + u64 cap; + + input /= UNCORE_FREQ_KHZ_MULTIPLIER; + if (!input || input > FIELD_MAX(UNCORE_MAX_RATIO_MASK)) + return -EINVAL; + + if (data->control_cpu < 0) + return -ENXIO; + + ret = rdmsrq_on_cpu(data->control_cpu, MSR_UNCORE_RATIO_LIMIT, &cap); + if (ret) + return ret; + + if (index == UNCORE_INDEX_MAX_FREQ) { + cap &= ~UNCORE_MAX_RATIO_MASK; + cap |= FIELD_PREP(UNCORE_MAX_RATIO_MASK, input); + } else { + cap &= ~UNCORE_MIN_RATIO_MASK; + cap |= FIELD_PREP(UNCORE_MIN_RATIO_MASK, input); + } + + ret = wrmsrq_on_cpu(data->control_cpu, MSR_UNCORE_RATIO_LIMIT, cap); + if (ret) + return ret; + + data->stored_uncore_data = cap; + + return 0; +} + +static int uncore_read_freq(struct uncore_data *data, unsigned int *freq) +{ + u64 ratio; + int ret; + + if (data->control_cpu < 0) + return -ENXIO; + + ret = rdmsrq_on_cpu(data->control_cpu, MSR_UNCORE_PERF_STATUS, &ratio); + if (ret) + return ret; + + *freq = FIELD_GET(UNCORE_CURRENT_RATIO_MASK, ratio) * UNCORE_FREQ_KHZ_MULTIPLIER; + + return 0; +} + +static int uncore_read(struct uncore_data *data, unsigned int *value, enum uncore_index index) +{ + switch (index) { + case UNCORE_INDEX_MIN_FREQ: + case UNCORE_INDEX_MAX_FREQ: + return uncore_read_control_freq(data, value, index); + + case UNCORE_INDEX_CURRENT_FREQ: + return uncore_read_freq(data, value); + + default: + break; + } + + return -EOPNOTSUPP; +} + +/* Caller provides protection */ +static struct uncore_data *uncore_get_instance(unsigned int cpu) +{ + int id = topology_logical_die_id(cpu); + + if (id >= 0 && id < uncore_max_entries) + return &uncore_instances[id]; + + return NULL; +} + +static int uncore_event_cpu_online(unsigned int cpu) +{ + struct uncore_data *data; + int target; + int ret; + + /* Check if there is an online cpu in the package for uncore MSR */ + target = cpumask_any_and(&uncore_cpu_mask, topology_die_cpumask(cpu)); + if (target < nr_cpu_ids) + return 0; + + data = uncore_get_instance(cpu); + if (!data) + return 0; + + data->package_id = topology_physical_package_id(cpu); + data->die_id = topology_die_id(cpu); + data->domain_id = UNCORE_DOMAIN_ID_INVALID; + + ret = uncore_freq_add_entry(data, cpu); + if (ret) + return ret; + + /* Use this CPU on this die as a control CPU */ + cpumask_set_cpu(cpu, &uncore_cpu_mask); + + return 0; +} + +static int uncore_event_cpu_offline(unsigned int cpu) +{ + struct uncore_data *data; + int target; + + data = uncore_get_instance(cpu); + if (!data) + return 0; + + /* Check if existing cpu is used for uncore MSRs */ + if (!cpumask_test_and_clear_cpu(cpu, &uncore_cpu_mask)) + return 0; + + /* Find a new cpu to set uncore MSR */ + target = cpumask_any_but(topology_die_cpumask(cpu), cpu); + + if (target < nr_cpu_ids) { + cpumask_set_cpu(target, &uncore_cpu_mask); + uncore_freq_add_entry(data, target); + } else { + uncore_freq_remove_die_entry(data); + } + + return 0; +} + +static int uncore_pm_notify(struct notifier_block *nb, unsigned long mode, + void *_unused) +{ + int i; + + switch (mode) { + case PM_POST_HIBERNATION: + case PM_POST_RESTORE: + case PM_POST_SUSPEND: + for (i = 0; i < uncore_max_entries; ++i) { + struct uncore_data *data = &uncore_instances[i]; + + if (!data || !data->valid || !data->stored_uncore_data) + return 0; + + wrmsrq_on_cpu(data->control_cpu, MSR_UNCORE_RATIO_LIMIT, + data->stored_uncore_data); + } + break; + default: + break; + } + return 0; +} + +static struct notifier_block uncore_pm_nb = { + .notifier_call = uncore_pm_notify, +}; + +static const struct x86_cpu_id intel_uncore_cpu_ids[] = { + X86_MATCH_VFM(INTEL_BROADWELL_G, NULL), + X86_MATCH_VFM(INTEL_BROADWELL_X, NULL), + X86_MATCH_VFM(INTEL_BROADWELL_D, NULL), + X86_MATCH_VFM(INTEL_SKYLAKE_X, NULL), + X86_MATCH_VFM(INTEL_ICELAKE_X, NULL), + X86_MATCH_VFM(INTEL_ICELAKE_D, NULL), + X86_MATCH_VFM(INTEL_SAPPHIRERAPIDS_X, NULL), + X86_MATCH_VFM(INTEL_EMERALDRAPIDS_X, NULL), + X86_MATCH_VFM(INTEL_KABYLAKE, NULL), + X86_MATCH_VFM(INTEL_KABYLAKE_L, NULL), + X86_MATCH_VFM(INTEL_COMETLAKE, NULL), + X86_MATCH_VFM(INTEL_COMETLAKE_L, NULL), + X86_MATCH_VFM(INTEL_CANNONLAKE_L, NULL), + X86_MATCH_VFM(INTEL_ICELAKE, NULL), + X86_MATCH_VFM(INTEL_ICELAKE_L, NULL), + X86_MATCH_VFM(INTEL_ROCKETLAKE, NULL), + X86_MATCH_VFM(INTEL_TIGERLAKE, NULL), + X86_MATCH_VFM(INTEL_TIGERLAKE_L, NULL), + X86_MATCH_VFM(INTEL_ALDERLAKE, NULL), + X86_MATCH_VFM(INTEL_ALDERLAKE_L, NULL), + X86_MATCH_VFM(INTEL_RAPTORLAKE, NULL), + X86_MATCH_VFM(INTEL_RAPTORLAKE_P, NULL), + X86_MATCH_VFM(INTEL_RAPTORLAKE_S, NULL), + X86_MATCH_VFM(INTEL_METEORLAKE, NULL), + X86_MATCH_VFM(INTEL_METEORLAKE_L, NULL), + X86_MATCH_VFM(INTEL_ARROWLAKE, NULL), + X86_MATCH_VFM(INTEL_ARROWLAKE_H, NULL), + X86_MATCH_VFM(INTEL_LUNARLAKE_M, NULL), + X86_MATCH_VFM(INTEL_PANTHERLAKE_L, NULL), + X86_MATCH_VFM(INTEL_WILDCATLAKE_L, NULL), + X86_MATCH_VFM(INTEL_NOVALAKE, NULL), + X86_MATCH_VFM(INTEL_NOVALAKE_L, NULL), + {} +}; +MODULE_DEVICE_TABLE(x86cpu, intel_uncore_cpu_ids); + +static int __init intel_uncore_init(void) +{ + const struct x86_cpu_id *id; + int ret; + + if (cpu_feature_enabled(X86_FEATURE_HYPERVISOR)) + return -ENODEV; + + id = x86_match_cpu(intel_uncore_cpu_ids); + if (!id) + return -ENODEV; + + uncore_max_entries = topology_max_packages() * + topology_max_dies_per_package(); + uncore_instances = kzalloc_objs(*uncore_instances, uncore_max_entries); + if (!uncore_instances) + return -ENOMEM; + + ret = uncore_freq_common_init(uncore_read, uncore_write_control_freq); + if (ret) + goto err_free; + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, + "platform/x86/uncore-freq:online", + uncore_event_cpu_online, + uncore_event_cpu_offline); + if (ret < 0) + goto err_rem_kobj; + + uncore_hp_state = ret; + + ret = register_pm_notifier(&uncore_pm_nb); + if (ret) + goto err_rem_state; + + return 0; + +err_rem_state: + cpuhp_remove_state(uncore_hp_state); +err_rem_kobj: + uncore_freq_common_exit(); +err_free: + kfree(uncore_instances); + + return ret; +} +module_init(intel_uncore_init) + +static void __exit intel_uncore_exit(void) +{ + int i; + + unregister_pm_notifier(&uncore_pm_nb); + cpuhp_remove_state(uncore_hp_state); + for (i = 0; i < uncore_max_entries; ++i) + uncore_freq_remove_die_entry(&uncore_instances[i]); + uncore_freq_common_exit(); + kfree(uncore_instances); +} +module_exit(intel_uncore_exit) + +MODULE_IMPORT_NS("INTEL_UNCORE_FREQUENCY"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel Uncore Frequency Limits Driver"); diff --git a/drivers/platform/x86/intel/vbtn.c b/drivers/platform/x86/intel/vbtn.c new file mode 100644 index 000000000..874023c38 --- /dev/null +++ b/drivers/platform/x86/intel/vbtn.c @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Intel Virtual Button driver for Windows 8.1+ + * + * Copyright (C) 2016 AceLan Kao + * Copyright (C) 2016 Alex Hung + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../dual_accel_detect.h" + +/* Returned when NOT in tablet mode on some HP Stream x360 11 models */ +#define VGBS_TABLET_MODE_FLAG_ALT 0x10 +/* When NOT in tablet mode, VGBS returns with the flag 0x40 */ +#define VGBS_TABLET_MODE_FLAG 0x40 +#define VGBS_DOCK_MODE_FLAG 0x80 + +#define VGBS_TABLET_MODE_FLAGS (VGBS_TABLET_MODE_FLAG | VGBS_TABLET_MODE_FLAG_ALT) + +MODULE_DESCRIPTION("Intel Virtual Button driver"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("AceLan Kao"); + +static const struct acpi_device_id intel_vbtn_ids[] = { + {"INT33D6", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, intel_vbtn_ids); + +/* In theory, these are HID usages. */ +static const struct key_entry intel_vbtn_keymap[] = { + { KE_KEY, 0xC0, { KEY_POWER } }, /* power key press */ + { KE_IGNORE, 0xC1, { KEY_POWER } }, /* power key release */ + { KE_KEY, 0xC2, { KEY_LEFTMETA } }, /* 'Windows' key press */ + { KE_KEY, 0xC3, { KEY_LEFTMETA } }, /* 'Windows' key release */ + { KE_KEY, 0xC4, { KEY_VOLUMEUP } }, /* volume-up key press */ + { KE_IGNORE, 0xC5, { KEY_VOLUMEUP } }, /* volume-up key release */ + { KE_KEY, 0xC6, { KEY_VOLUMEDOWN } }, /* volume-down key press */ + { KE_IGNORE, 0xC7, { KEY_VOLUMEDOWN } }, /* volume-down key release */ + { KE_KEY, 0xC8, { KEY_ROTATE_LOCK_TOGGLE } }, /* rotate-lock key press */ + { KE_KEY, 0xC9, { KEY_ROTATE_LOCK_TOGGLE } }, /* rotate-lock key release */ + { KE_END } +}; + +static const struct key_entry intel_vbtn_switchmap[] = { + /* + * SW_DOCK should only be reported for docking stations, but DSDTs using the + * intel-vbtn code, always seem to use this for 2-in-1s / convertibles and set + * SW_DOCK=1 when in laptop-mode (in tandem with setting SW_TABLET_MODE=0). + * This causes userspace to think the laptop is docked to a port-replicator + * and to disable suspend-on-lid-close, which is undesirable. + * Map the dock events to KEY_IGNORE to avoid this broken SW_DOCK reporting. + */ + { KE_IGNORE, 0xCA, { .sw = { SW_DOCK, 1 } } }, /* Docked */ + { KE_IGNORE, 0xCB, { .sw = { SW_DOCK, 0 } } }, /* Undocked */ + { KE_SW, 0xCC, { .sw = { SW_TABLET_MODE, 1 } } }, /* Tablet */ + { KE_SW, 0xCD, { .sw = { SW_TABLET_MODE, 0 } } }, /* Laptop */ + { KE_END } +}; + +struct intel_vbtn_priv { + struct mutex mutex; /* Avoid notify_handler() racing with itself */ + struct input_dev *buttons_dev; + struct input_dev *switches_dev; + bool dual_accel; + bool has_buttons; + bool has_switches; + bool wakeup_mode; +}; + +static void detect_tablet_mode(struct device *dev) +{ + struct intel_vbtn_priv *priv = dev_get_drvdata(dev); + acpi_handle handle = ACPI_HANDLE(dev); + unsigned long long vgbs; + acpi_status status; + int m; + + status = acpi_evaluate_integer(handle, "VGBS", NULL, &vgbs); + if (ACPI_FAILURE(status)) + return; + + m = !(vgbs & VGBS_TABLET_MODE_FLAGS); + input_report_switch(priv->switches_dev, SW_TABLET_MODE, m); + m = (vgbs & VGBS_DOCK_MODE_FLAG) ? 1 : 0; + input_report_switch(priv->switches_dev, SW_DOCK, m); + + input_sync(priv->switches_dev); +} + +/* + * Note this unconditionally creates the 2 input_dev-s and sets up + * the sparse-keymaps. Only the registration is conditional on + * have_buttons / have_switches. This is done so that the notify + * handler can always call sparse_keymap_entry_from_scancode() + * on the input_dev-s do determine the event type. + */ +static int intel_vbtn_input_setup(struct platform_device *device) +{ + struct intel_vbtn_priv *priv = dev_get_drvdata(&device->dev); + int ret; + + priv->buttons_dev = devm_input_allocate_device(&device->dev); + if (!priv->buttons_dev) + return -ENOMEM; + + ret = sparse_keymap_setup(priv->buttons_dev, intel_vbtn_keymap, NULL); + if (ret) + return ret; + + priv->buttons_dev->dev.parent = &device->dev; + priv->buttons_dev->name = "Intel Virtual Buttons"; + priv->buttons_dev->id.bustype = BUS_HOST; + + if (priv->has_buttons) { + ret = input_register_device(priv->buttons_dev); + if (ret) + return ret; + } + + priv->switches_dev = devm_input_allocate_device(&device->dev); + if (!priv->switches_dev) + return -ENOMEM; + + ret = sparse_keymap_setup(priv->switches_dev, intel_vbtn_switchmap, NULL); + if (ret) + return ret; + + priv->switches_dev->dev.parent = &device->dev; + priv->switches_dev->name = "Intel Virtual Switches"; + priv->switches_dev->id.bustype = BUS_HOST; + + if (priv->has_switches) { + ret = input_register_device(priv->switches_dev); + if (ret) + return ret; + } + + return 0; +} + +static void notify_handler(acpi_handle handle, u32 event, void *context) +{ + struct platform_device *device = context; + struct intel_vbtn_priv *priv = dev_get_drvdata(&device->dev); + unsigned int val = !(event & 1); /* Even=press, Odd=release */ + const struct key_entry *ke, *ke_rel; + struct input_dev *input_dev; + bool autorelease; + int ret; + + guard(mutex)(&priv->mutex); + + if ((ke = sparse_keymap_entry_from_scancode(priv->buttons_dev, event))) { + if (!priv->has_buttons) { + dev_warn(&device->dev, "Warning: received 0x%02x button event on a device without buttons, please report this.\n", + event); + return; + } + input_dev = priv->buttons_dev; + } else if ((ke = sparse_keymap_entry_from_scancode(priv->switches_dev, event))) { + if (!priv->has_switches) { + /* See dual_accel_detect.h for more info */ + if (priv->dual_accel) + return; + + dev_info(&device->dev, "Registering Intel Virtual Switches input-dev after receiving a switch event\n"); + ret = input_register_device(priv->switches_dev); + if (ret) + return; + + priv->has_switches = true; + } + input_dev = priv->switches_dev; + } else { + dev_dbg(&device->dev, "unknown event index 0x%x\n", event); + return; + } + + if (priv->wakeup_mode) { + pm_wakeup_hard_event(&device->dev); + + /* + * Skip reporting an evdev event for button wake events, + * mirroring how the drivers/acpi/button.c code skips this too. + */ + if (ke->type == KE_KEY) + return; + } + + /* + * Even press events are autorelease if there is no corresponding odd + * release event, or if the odd event is KE_IGNORE. + */ + ke_rel = sparse_keymap_entry_from_scancode(input_dev, event | 1); + autorelease = val && (!ke_rel || ke_rel->type == KE_IGNORE); + + sparse_keymap_report_event(input_dev, event, val, autorelease); +} + +/* + * There are several laptops (non 2-in-1) models out there which support VGBS, + * but simply always return 0, which we translate to SW_TABLET_MODE=1. This in + * turn causes userspace (libinput) to suppress events from the builtin + * keyboard and touchpad, making the laptop essentially unusable. + * + * Since the problem of wrongly reporting SW_TABLET_MODE=1 in combination + * with libinput, leads to a non-usable system. Where as OTOH many people will + * not even notice when SW_TABLET_MODE is not being reported, a DMI based allow + * list is used here. This list mainly matches on the chassis-type of 2-in-1s. + * + * There are also some 2-in-1s which use the intel-vbtn ACPI interface to report + * SW_TABLET_MODE with a chassis-type of 8 ("Portable") or 10 ("Notebook"), + * these are matched on a per model basis, since many normal laptops with a + * possible broken VGBS ACPI-method also use these chassis-types. + */ +static const struct dmi_system_id dmi_switches_allow_list[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */), + }, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Venue 11 Pro 7130"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Hewlett-Packard"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP Pavilion 13 x360 PC"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Acer"), + DMI_MATCH(DMI_PRODUCT_NAME, "Switch SA5-271"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 7352"), + }, + }, + {} /* Array terminator */ +}; + +static bool intel_vbtn_has_switches(acpi_handle handle, bool dual_accel) +{ + /* See dual_accel_detect.h for more info */ + if (dual_accel) + return false; + + if (!dmi_check_system(dmi_switches_allow_list)) + return false; + + return acpi_has_method(handle, "VGBS"); +} + +static int intel_vbtn_probe(struct platform_device *device) +{ + bool dual_accel, has_buttons, has_switches; + struct intel_vbtn_priv *priv; + acpi_handle handle; + acpi_status status; + int err; + + handle = ACPI_HANDLE(&device->dev); + if (!handle) + return -ENODEV; + + dual_accel = dual_accel_detect(); + has_buttons = acpi_has_method(handle, "VBDL"); + has_switches = intel_vbtn_has_switches(handle, dual_accel); + + if (!has_buttons && !has_switches) { + dev_warn(&device->dev, "failed to read Intel Virtual Button driver\n"); + return -ENODEV; + } + + priv = devm_kzalloc(&device->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + dev_set_drvdata(&device->dev, priv); + + err = devm_mutex_init(&device->dev, &priv->mutex); + if (err) + return err; + + priv->dual_accel = dual_accel; + priv->has_buttons = has_buttons; + priv->has_switches = has_switches; + + err = intel_vbtn_input_setup(device); + if (err) { + pr_err("Failed to setup Intel Virtual Button\n"); + return err; + } + + status = acpi_install_notify_handler(handle, + ACPI_DEVICE_NOTIFY, + notify_handler, + device); + if (ACPI_FAILURE(status)) + return -EBUSY; + + if (has_buttons) { + status = acpi_evaluate_object(handle, "VBDL", NULL, NULL); + if (ACPI_FAILURE(status)) + dev_err(&device->dev, "Error VBDL failed with ACPI status %d\n", status); + } + // Check switches after buttons since VBDL may have side effects. + if (has_switches) + detect_tablet_mode(&device->dev); + + device_init_wakeup(&device->dev, true); + /* + * In order for system wakeup to work, the EC GPE has to be marked as + * a wakeup one, so do that here (this setting will persist, but it has + * no effect until the wakeup mask is set for the EC GPE). + */ + acpi_ec_mark_gpe_for_wake(); + return 0; +} + +static void intel_vbtn_remove(struct platform_device *device) +{ + acpi_handle handle = ACPI_HANDLE(&device->dev); + + device_init_wakeup(&device->dev, false); + acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, notify_handler); +} + +static int intel_vbtn_pm_prepare(struct device *dev) +{ + if (device_may_wakeup(dev)) { + struct intel_vbtn_priv *priv = dev_get_drvdata(dev); + + priv->wakeup_mode = true; + } + return 0; +} + +static void intel_vbtn_pm_complete(struct device *dev) +{ + struct intel_vbtn_priv *priv = dev_get_drvdata(dev); + + priv->wakeup_mode = false; +} + +static int intel_vbtn_pm_resume(struct device *dev) +{ + struct intel_vbtn_priv *priv = dev_get_drvdata(dev); + + intel_vbtn_pm_complete(dev); + + if (priv->has_switches) + detect_tablet_mode(dev); + + return 0; +} + +static const struct dev_pm_ops intel_vbtn_pm_ops = { + .prepare = intel_vbtn_pm_prepare, + .complete = intel_vbtn_pm_complete, + .resume = intel_vbtn_pm_resume, + .restore = intel_vbtn_pm_resume, + .thaw = intel_vbtn_pm_resume, +}; + +static struct platform_driver intel_vbtn_pl_driver = { + .driver = { + .name = "intel-vbtn", + .acpi_match_table = intel_vbtn_ids, + .pm = &intel_vbtn_pm_ops, + }, + .probe = intel_vbtn_probe, + .remove = intel_vbtn_remove, +}; + +module_platform_driver(intel_vbtn_pl_driver); diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c new file mode 100644 index 000000000..5ab2215fd --- /dev/null +++ b/drivers/platform/x86/intel/vsec.c @@ -0,0 +1,908 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Vendor Specific Extended Capabilities auxiliary bus driver + * + * Copyright (c) 2021, Intel Corporation. + * All Rights Reserved. + * + * Author: David E. Box + * + * This driver discovers and creates auxiliary devices for Intel defined PCIe + * "Vendor Specific" and "Designated Vendor Specific" Extended Capabilities, + * VSEC and DVSEC respectively. The driver supports features on specific PCIe + * endpoints that exist primarily to expose them. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PMT_XA_START 0 +#define PMT_XA_MAX INT_MAX +#define PMT_XA_LIMIT XA_LIMIT(PMT_XA_START, PMT_XA_MAX) + +static DEFINE_IDA(intel_vsec_ida); +static DEFINE_IDA(intel_vsec_sdsi_ida); +static DEFINE_XARRAY_ALLOC(auxdev_array); + +enum vsec_device_state { + STATE_NOT_FOUND, + STATE_REGISTERED, + STATE_SKIP, +}; + +struct vsec_priv { + const struct intel_vsec_platform_info *info; + struct device *suppliers[VSEC_FEATURE_COUNT]; + struct oobmsm_plat_info plat_info; + enum vsec_device_state state[VSEC_FEATURE_COUNT]; + unsigned long found_caps; +}; + +static const char *intel_vsec_name(enum intel_vsec_id id) +{ + switch (id) { + case VSEC_ID_TELEMETRY: + return "telemetry"; + + case VSEC_ID_WATCHER: + return "watcher"; + + case VSEC_ID_CRASHLOG: + return "crashlog"; + + case VSEC_ID_SDSI: + return "sdsi"; + + case VSEC_ID_TPMI: + return "tpmi"; + + case VSEC_ID_DISCOVERY: + return "discovery"; + + default: + return NULL; + } +} + +static bool intel_vsec_supported(u16 id, unsigned long caps) +{ + switch (id) { + case VSEC_ID_TELEMETRY: + return !!(caps & VSEC_CAP_TELEMETRY); + case VSEC_ID_WATCHER: + return !!(caps & VSEC_CAP_WATCHER); + case VSEC_ID_CRASHLOG: + return !!(caps & VSEC_CAP_CRASHLOG); + case VSEC_ID_SDSI: + return !!(caps & VSEC_CAP_SDSI); + case VSEC_ID_TPMI: + return !!(caps & VSEC_CAP_TPMI); + case VSEC_ID_DISCOVERY: + return !!(caps & VSEC_CAP_DISCOVERY); + default: + return false; + } +} + +static void intel_vsec_remove_aux(void *data) +{ + auxiliary_device_delete(data); + auxiliary_device_uninit(data); +} + +static void intel_vsec_dev_free(struct intel_vsec_device *intel_vsec_dev) +{ + kfree(intel_vsec_dev->acpi_disc); + kfree(intel_vsec_dev); +} + +static void intel_vsec_dev_release(struct device *dev) +{ + struct intel_vsec_device *intel_vsec_dev = dev_to_ivdev(dev); + + xa_erase(&auxdev_array, intel_vsec_dev->id); + + ida_free(intel_vsec_dev->ida, intel_vsec_dev->auxdev.id); + + intel_vsec_dev_free(intel_vsec_dev); +} + +static const struct vsec_feature_dependency * +get_consumer_dependencies(struct vsec_priv *priv, int cap_id) +{ + const struct vsec_feature_dependency *deps = priv->info->deps; + int consumer_id = priv->info->num_deps; + + if (!deps) + return NULL; + + while (consumer_id--) + if (deps[consumer_id].feature == BIT(cap_id)) + return &deps[consumer_id]; + + return NULL; +} + +static bool vsec_driver_present(int cap_id) +{ + unsigned long bit = BIT(cap_id); + + switch (bit) { + case VSEC_CAP_TELEMETRY: + return IS_ENABLED(CONFIG_INTEL_PMT_TELEMETRY); + case VSEC_CAP_WATCHER: + return IS_ENABLED(CONFIG_INTEL_PMT_WATCHER); + case VSEC_CAP_CRASHLOG: + return IS_ENABLED(CONFIG_INTEL_PMT_CRASHLOG); + case VSEC_CAP_SDSI: + return IS_ENABLED(CONFIG_INTEL_SDSI); + case VSEC_CAP_TPMI: + return IS_ENABLED(CONFIG_INTEL_TPMI); + case VSEC_CAP_DISCOVERY: + return IS_ENABLED(CONFIG_INTEL_PMT_DISCOVERY); + default: + return false; + } +} + +/* + * Although pci_device_id table is available in the pdev, this prototype is + * necessary because the code using it can be called by an exported API that + * might pass a different pdev. + */ +static const struct pci_device_id intel_vsec_pci_ids[]; + +static int intel_vsec_link_devices(struct device *parent, struct device *dev, + int consumer_id) +{ + const struct vsec_feature_dependency *deps; + enum vsec_device_state *state; + struct device **suppliers; + struct vsec_priv *priv; + struct pci_dev *pdev; + int supplier_id; + + if (!consumer_id) + return 0; + + if (!dev_is_pci(parent)) + return 0; + + pdev = to_pci_dev(parent); + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return 0; + + priv = pci_get_drvdata(pdev); + state = priv->state; + suppliers = priv->suppliers; + + priv->suppliers[consumer_id] = dev; + + deps = get_consumer_dependencies(priv, consumer_id); + if (!deps) + return 0; + + for_each_set_bit(supplier_id, &deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + struct device_link *link; + + if (state[supplier_id] != STATE_REGISTERED || + !vsec_driver_present(supplier_id)) + continue; + + if (!suppliers[supplier_id]) { + dev_err(dev, "Bad supplier list\n"); + return -EINVAL; + } + + link = device_link_add(dev, suppliers[supplier_id], + DL_FLAG_AUTOPROBE_CONSUMER); + if (!link) + return -EINVAL; + } + + return 0; +} + +int intel_vsec_add_aux(struct device *parent, + struct intel_vsec_device *intel_vsec_dev, + const char *name) +{ + struct auxiliary_device *auxdev = &intel_vsec_dev->auxdev; + int ret, id; + + if (!parent) { + intel_vsec_dev_free(intel_vsec_dev); + return -EINVAL; + } + + ret = xa_alloc(&auxdev_array, &intel_vsec_dev->id, intel_vsec_dev, + PMT_XA_LIMIT, GFP_KERNEL); + if (ret < 0) { + intel_vsec_dev_free(intel_vsec_dev); + return ret; + } + + id = ida_alloc(intel_vsec_dev->ida, GFP_KERNEL); + if (id < 0) { + xa_erase(&auxdev_array, intel_vsec_dev->id); + intel_vsec_dev_free(intel_vsec_dev); + return id; + } + + auxdev->id = id; + auxdev->name = name; + auxdev->dev.parent = parent; + auxdev->dev.release = intel_vsec_dev_release; + + ret = auxiliary_device_init(auxdev); + if (ret < 0) { + intel_vsec_dev_release(&auxdev->dev); + return ret; + } + + /* + * Assign a name now to ensure that the device link doesn't contain + * a null string for the consumer name. This is a problem when a supplier + * supplies more than one consumer and can lead to a duplicate name error + * when the link is created in sysfs. + */ + ret = dev_set_name(&auxdev->dev, "%s.%s.%d", KBUILD_MODNAME, auxdev->name, + auxdev->id); + if (ret) + goto cleanup_aux; + + ret = intel_vsec_link_devices(parent, &auxdev->dev, intel_vsec_dev->cap_id); + if (ret) + goto cleanup_aux; + + ret = auxiliary_device_add(auxdev); + if (ret) + goto cleanup_aux; + + return devm_add_action_or_reset(parent, intel_vsec_remove_aux, + auxdev); + +cleanup_aux: + auxiliary_device_uninit(auxdev); + return ret; +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_add_aux, "INTEL_VSEC"); + +static int intel_vsec_add_dev(struct device *dev, struct intel_vsec_header *header, + const struct intel_vsec_platform_info *info, + unsigned long cap_id, u64 base_addr) +{ + struct intel_vsec_device __free(kfree) *intel_vsec_dev = NULL; + struct resource *res; + struct resource *tmp; + struct device *parent; + unsigned long quirks = info->quirks; + int i; + + if (info->parent) + parent = info->parent; + else + parent = dev; + + if (!intel_vsec_supported(header->id, info->caps)) + return -EINVAL; + + if (!header->num_entries) { + dev_dbg(dev, "Invalid 0 entry count for header id %d\n", header->id); + return -EINVAL; + } + + if (!header->entry_size) { + dev_dbg(dev, "Invalid 0 entry size for header id %d\n", header->id); + return -EINVAL; + } + + intel_vsec_dev = kzalloc_flex(*intel_vsec_dev, resource, header->num_entries); + if (!intel_vsec_dev) + return -ENOMEM; + + intel_vsec_dev->num_resources = header->num_entries; + res = intel_vsec_dev->resource; + + if (quirks & VSEC_QUIRK_TABLE_SHIFT) + header->offset >>= TABLE_OFFSET_SHIFT; + + /* + * The DVSEC/VSEC contains the starting offset and count for a block of + * discovery tables. Create a resource array of these tables to the + * auxiliary device driver. + */ + for (i = 0, tmp = res; i < header->num_entries; i++, tmp++) { + /* + * Skip resource mapping check for ACPI-based discovery + * since those tables are read from _DSD, not MMIO. + */ + if (info->src == INTEL_VSEC_DISC_ACPI) + break; + + tmp->start = base_addr + header->offset + i * (header->entry_size * sizeof(u32)); + tmp->end = tmp->start + (header->entry_size * sizeof(u32)) - 1; + tmp->flags = IORESOURCE_MEM; + + /* Check resource is not in use */ + if (!request_mem_region(tmp->start, resource_size(tmp), "")) + return -EBUSY; + + release_mem_region(tmp->start, resource_size(tmp)); + } + + intel_vsec_dev->dev = dev; + intel_vsec_dev->quirks = info->quirks; + intel_vsec_dev->base_addr = info->base_addr; + intel_vsec_dev->priv_data = info->priv_data; + intel_vsec_dev->cap_id = cap_id; + intel_vsec_dev->src = info->src; + + if (info->src == INTEL_VSEC_DISC_ACPI) { + size_t bytes; + + if (check_mul_overflow(intel_vsec_dev->num_resources, + sizeof(*info->acpi_disc), &bytes)) + return -EOVERFLOW; + + intel_vsec_dev->acpi_disc = kmemdup(info->acpi_disc, bytes, GFP_KERNEL); + if (!intel_vsec_dev->acpi_disc) + return -ENOMEM; + } + + if (header->id == VSEC_ID_SDSI) + intel_vsec_dev->ida = &intel_vsec_sdsi_ida; + else + intel_vsec_dev->ida = &intel_vsec_ida; + + /* + * Pass the ownership of intel_vsec_dev and resource within it to + * intel_vsec_add_aux() + */ + return intel_vsec_add_aux(parent, no_free_ptr(intel_vsec_dev), + intel_vsec_name(header->id)); +} + +static bool suppliers_ready(struct vsec_priv *priv, + const struct vsec_feature_dependency *consumer_deps, + int cap_id) +{ + enum vsec_device_state *state = priv->state; + int supplier_id; + + if (WARN_ON_ONCE(consumer_deps->feature != BIT(cap_id))) + return false; + + /* + * Verify that all required suppliers have been found. Return false + * immediately if any are still missing. + */ + for_each_set_bit(supplier_id, &consumer_deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + if (state[supplier_id] == STATE_SKIP) + continue; + + if (state[supplier_id] == STATE_NOT_FOUND) + return false; + } + + /* + * All suppliers have been found and the consumer is ready to be + * registered. + */ + return true; +} + +static int get_cap_id(u32 header_id, unsigned long *cap_id) +{ + switch (header_id) { + case VSEC_ID_TELEMETRY: + *cap_id = ilog2(VSEC_CAP_TELEMETRY); + break; + case VSEC_ID_WATCHER: + *cap_id = ilog2(VSEC_CAP_WATCHER); + break; + case VSEC_ID_CRASHLOG: + *cap_id = ilog2(VSEC_CAP_CRASHLOG); + break; + case VSEC_ID_SDSI: + *cap_id = ilog2(VSEC_CAP_SDSI); + break; + case VSEC_ID_TPMI: + *cap_id = ilog2(VSEC_CAP_TPMI); + break; + case VSEC_ID_DISCOVERY: + *cap_id = ilog2(VSEC_CAP_DISCOVERY); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int intel_vsec_register_device(struct device *dev, + struct intel_vsec_header *header, + const struct intel_vsec_platform_info *info, + u64 base_addr) +{ + const struct vsec_feature_dependency *consumer_deps; + struct vsec_priv *priv; + struct pci_dev *pdev; + unsigned long cap_id; + int ret; + + ret = get_cap_id(header->id, &cap_id); + if (ret) + return ret; + + /* + * Only track dependencies for devices probed by the VSEC driver. + * For others using the exported APIs, add the device directly. + */ + if (!dev_is_pci(dev)) + return intel_vsec_add_dev(dev, header, info, cap_id, base_addr); + + pdev = to_pci_dev(dev); + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return intel_vsec_add_dev(dev, header, info, cap_id, base_addr); + + priv = pci_get_drvdata(pdev); + if (priv->state[cap_id] == STATE_REGISTERED || + priv->state[cap_id] == STATE_SKIP) + return -EEXIST; + + priv->found_caps |= BIT(cap_id); + + if (!vsec_driver_present(cap_id)) { + priv->state[cap_id] = STATE_SKIP; + return -ENODEV; + } + + consumer_deps = get_consumer_dependencies(priv, cap_id); + if (!consumer_deps || suppliers_ready(priv, consumer_deps, cap_id)) { + ret = intel_vsec_add_dev(dev, header, info, cap_id, base_addr); + if (ret) + priv->state[cap_id] = STATE_SKIP; + else + priv->state[cap_id] = STATE_REGISTERED; + + return ret; + } + + return -EAGAIN; +} + +static int intel_vsec_walk_header(struct device *dev, + const struct intel_vsec_platform_info *info) +{ + struct intel_vsec_header **header = info->headers; + u64 base_addr; + int ret; + + for ( ; *header; header++) { + if (info->base_addr) { + base_addr = info->base_addr; + } else { + struct pci_dev *pdev; + + if (!dev_is_pci(dev)) { + dev_err(dev, "non-PCI device without a base address\n"); + return -EINVAL; + } + + pdev = to_pci_dev(dev); + base_addr = pci_resource_start(pdev, (*header)->tbir); + } + + ret = intel_vsec_register_device(dev, *header, info, base_addr); + if (ret) + return ret; + } + + return 0; +} + +static bool intel_vsec_walk_dvsec(struct pci_dev *pdev, + const struct intel_vsec_platform_info *info) +{ + bool have_devices = false; + int pos = 0; + + do { + struct intel_vsec_header header; + u32 table, hdr; + u16 vid; + int ret; + + pos = pci_find_next_ext_capability(pdev, pos, PCI_EXT_CAP_ID_DVSEC); + if (!pos) + break; + + pci_read_config_dword(pdev, pos + PCI_DVSEC_HEADER1, &hdr); + vid = PCI_DVSEC_HEADER1_VID(hdr); + if (vid != PCI_VENDOR_ID_INTEL) + continue; + + /* Support only revision 1 */ + header.rev = PCI_DVSEC_HEADER1_REV(hdr); + if (header.rev != 1) { + dev_info(&pdev->dev, "Unsupported DVSEC revision %d\n", header.rev); + continue; + } + + header.length = PCI_DVSEC_HEADER1_LEN(hdr); + + pci_read_config_byte(pdev, pos + INTEL_DVSEC_ENTRIES, &header.num_entries); + pci_read_config_byte(pdev, pos + INTEL_DVSEC_SIZE, &header.entry_size); + pci_read_config_dword(pdev, pos + INTEL_DVSEC_TABLE, &table); + + header.tbir = INTEL_DVSEC_TABLE_BAR(table); + header.offset = INTEL_DVSEC_TABLE_OFFSET(table); + + pci_read_config_dword(pdev, pos + PCI_DVSEC_HEADER2, &hdr); + header.id = PCI_DVSEC_HEADER2_ID(hdr); + + ret = intel_vsec_register_device(&pdev->dev, &header, info, + pci_resource_start(pdev, header.tbir)); + if (ret) + continue; + + have_devices = true; + } while (true); + + return have_devices; +} + +static bool intel_vsec_walk_vsec(struct pci_dev *pdev, + const struct intel_vsec_platform_info *info) +{ + bool have_devices = false; + int pos = 0; + + do { + struct intel_vsec_header header; + u32 table, hdr; + int ret; + + pos = pci_find_next_ext_capability(pdev, pos, PCI_EXT_CAP_ID_VNDR); + if (!pos) + break; + + pci_read_config_dword(pdev, pos + PCI_VNDR_HEADER, &hdr); + + /* Support only revision 1 */ + header.rev = PCI_VNDR_HEADER_REV(hdr); + if (header.rev != 1) { + dev_info(&pdev->dev, "Unsupported VSEC revision %d\n", header.rev); + continue; + } + + header.id = PCI_VNDR_HEADER_ID(hdr); + header.length = PCI_VNDR_HEADER_LEN(hdr); + + /* entry, size, and table offset are the same as DVSEC */ + pci_read_config_byte(pdev, pos + INTEL_DVSEC_ENTRIES, &header.num_entries); + pci_read_config_byte(pdev, pos + INTEL_DVSEC_SIZE, &header.entry_size); + pci_read_config_dword(pdev, pos + INTEL_DVSEC_TABLE, &table); + + header.tbir = INTEL_DVSEC_TABLE_BAR(table); + header.offset = INTEL_DVSEC_TABLE_OFFSET(table); + + ret = intel_vsec_register_device(&pdev->dev, &header, info, + pci_resource_start(pdev, header.tbir)); + if (ret) + continue; + + have_devices = true; + } while (true); + + return have_devices; +} + +int intel_vsec_register(struct device *dev, + const struct intel_vsec_platform_info *info) +{ + if (!dev || !info || !info->headers) + return -EINVAL; + + return intel_vsec_walk_header(dev, info); +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_register, "INTEL_VSEC"); + +static bool intel_vsec_get_features(struct pci_dev *pdev, + const struct intel_vsec_platform_info *info) +{ + bool found = false; + + /* + * Both DVSEC and VSEC capabilities can exist on the same device, + * so both intel_vsec_walk_dvsec() and intel_vsec_walk_vsec() must be + * called independently. Additionally, intel_vsec_walk_header() is + * needed for devices that do not have VSEC/DVSEC but provide the + * information via device_data. + */ + if (intel_vsec_walk_dvsec(pdev, info)) + found = true; + + if (intel_vsec_walk_vsec(pdev, info)) + found = true; + + if (info && (info->quirks & VSEC_QUIRK_NO_DVSEC) && + intel_vsec_walk_header(&pdev->dev, info)) + found = true; + + return found; +} + +static void intel_vsec_skip_missing_dependencies(struct pci_dev *pdev) +{ + struct vsec_priv *priv = pci_get_drvdata(pdev); + const struct vsec_feature_dependency *deps = priv->info->deps; + int consumer_id = priv->info->num_deps; + + while (consumer_id--) { + int supplier_id; + + deps = &priv->info->deps[consumer_id]; + + for_each_set_bit(supplier_id, &deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + if (!(BIT(supplier_id) & priv->found_caps)) + priv->state[supplier_id] = STATE_SKIP; + } + } +} + +static int intel_vsec_pci_init(struct pci_dev *pdev) +{ + struct vsec_priv *priv = pci_get_drvdata(pdev); + const struct intel_vsec_platform_info *info = priv->info; + int run_once = 0; + bool found_any = false; + int num_caps; + + num_caps = hweight_long(info->caps); + while (num_caps--) { + found_any |= intel_vsec_get_features(pdev, info); + + if (priv->found_caps == info->caps) + break; + + if (!run_once) { + intel_vsec_skip_missing_dependencies(pdev); + run_once = 1; + } + } + + if (!found_any) + return -ENODEV; + + return 0; +} + +static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) +{ + const struct intel_vsec_platform_info *info; + struct vsec_priv *priv; + int ret; + + ret = pcim_enable_device(pdev); + if (ret) + return ret; + + pci_save_state(pdev); + info = (const struct intel_vsec_platform_info *)id->driver_data; + if (!info) + return -EINVAL; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->info = info; + pci_set_drvdata(pdev, priv); + + return intel_vsec_pci_init(pdev); +} + +int intel_vsec_set_mapping(struct oobmsm_plat_info *plat_info, + struct intel_vsec_device *vsec_dev) +{ + struct vsec_priv *priv; + + if (!dev_is_pci(vsec_dev->dev)) + return -ENODEV; + + priv = pci_get_drvdata(to_pci_dev(vsec_dev->dev)); + if (!priv) + return -EINVAL; + + priv->plat_info = *plat_info; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_set_mapping, "INTEL_VSEC"); + +struct oobmsm_plat_info *intel_vsec_get_mapping(struct pci_dev *pdev) +{ + struct vsec_priv *priv; + + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return ERR_PTR(-EINVAL); + + priv = pci_get_drvdata(pdev); + if (!priv) + return ERR_PTR(-EINVAL); + + return &priv->plat_info; +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_get_mapping, "INTEL_VSEC"); + +/* DG1 info */ +static struct intel_vsec_header dg1_header = { + .length = 0x10, + .id = 2, + .num_entries = 1, + .entry_size = 3, + .tbir = 0, + .offset = 0x466000, +}; + +static struct intel_vsec_header *dg1_headers[] = { + &dg1_header, + NULL +}; + +static const struct intel_vsec_platform_info dg1_info = { + .caps = VSEC_CAP_TELEMETRY, + .headers = dg1_headers, + .quirks = VSEC_QUIRK_NO_DVSEC | VSEC_QUIRK_EARLY_HW, +}; + +/* MTL info */ +static const struct intel_vsec_platform_info mtl_info = { + .caps = VSEC_CAP_TELEMETRY, +}; + +static const struct vsec_feature_dependency oobmsm_deps[] = { + { + .feature = VSEC_CAP_TELEMETRY, + .supplier_bitmap = VSEC_CAP_DISCOVERY | VSEC_CAP_TPMI, + }, +}; + +/* OOBMSM info */ +static const struct intel_vsec_platform_info oobmsm_info = { + .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_SDSI | VSEC_CAP_TPMI | + VSEC_CAP_DISCOVERY, + .deps = oobmsm_deps, + .num_deps = ARRAY_SIZE(oobmsm_deps), +}; + +/* DMR OOBMSM info */ +static const struct intel_vsec_platform_info dmr_oobmsm_info = { + .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_TPMI | VSEC_CAP_DISCOVERY, + .deps = oobmsm_deps, + .num_deps = ARRAY_SIZE(oobmsm_deps), +}; + +/* TGL info */ +static const struct intel_vsec_platform_info tgl_info = { + .caps = VSEC_CAP_TELEMETRY, + .quirks = VSEC_QUIRK_TABLE_SHIFT | VSEC_QUIRK_EARLY_HW, +}; + +/* LNL info */ +static const struct intel_vsec_platform_info lnl_info = { + .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_WATCHER, +}; + +#define PCI_DEVICE_ID_INTEL_VSEC_ADL 0x467d +#define PCI_DEVICE_ID_INTEL_VSEC_DG1 0x490e +#define PCI_DEVICE_ID_INTEL_VSEC_MTL_M 0x7d0d +#define PCI_DEVICE_ID_INTEL_VSEC_MTL_S 0xad0d +#define PCI_DEVICE_ID_INTEL_VSEC_OOBMSM 0x09a7 +#define PCI_DEVICE_ID_INTEL_VSEC_OOBMSM_DMR 0x09a1 +#define PCI_DEVICE_ID_INTEL_VSEC_RPL 0xa77d +#define PCI_DEVICE_ID_INTEL_VSEC_TGL 0x9a0d +#define PCI_DEVICE_ID_INTEL_VSEC_LNL_M 0x647d +#define PCI_DEVICE_ID_INTEL_VSEC_PTL 0xb07d +#define PCI_DEVICE_ID_INTEL_VSEC_WCL 0xfd7d +#define PCI_DEVICE_ID_INTEL_VSEC_NVL 0xd70d +static const struct pci_device_id intel_vsec_pci_ids[] = { + { PCI_DEVICE_DATA(INTEL, VSEC_ADL, &tgl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_DG1, &dg1_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_MTL_M, &mtl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_MTL_S, &mtl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_OOBMSM, &oobmsm_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_OOBMSM_DMR, &dmr_oobmsm_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_RPL, &tgl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_TGL, &tgl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_LNL_M, &lnl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_PTL, &mtl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_WCL, &mtl_info) }, + { PCI_DEVICE_DATA(INTEL, VSEC_NVL, &mtl_info) }, + { } +}; +MODULE_DEVICE_TABLE(pci, intel_vsec_pci_ids); + +static pci_ers_result_t intel_vsec_pci_error_detected(struct pci_dev *pdev, + pci_channel_state_t state) +{ + pci_ers_result_t status = PCI_ERS_RESULT_NEED_RESET; + + dev_info(&pdev->dev, "PCI error detected, state %d", state); + + if (state == pci_channel_io_perm_failure) + status = PCI_ERS_RESULT_DISCONNECT; + else + pci_disable_device(pdev); + + return status; +} + +static pci_ers_result_t intel_vsec_pci_slot_reset(struct pci_dev *pdev) +{ + struct intel_vsec_device *intel_vsec_dev; + pci_ers_result_t status = PCI_ERS_RESULT_DISCONNECT; + unsigned long index; + + dev_info(&pdev->dev, "Resetting PCI slot\n"); + + msleep(2000); + if (pci_enable_device(pdev)) { + dev_info(&pdev->dev, + "Failed to re-enable PCI device after reset.\n"); + goto out; + } + + status = PCI_ERS_RESULT_RECOVERED; + + xa_for_each(&auxdev_array, index, intel_vsec_dev) { + /* check if pdev doesn't match */ + if (&pdev->dev != intel_vsec_dev->dev) + continue; + devm_release_action(&pdev->dev, intel_vsec_remove_aux, + &intel_vsec_dev->auxdev); + } + pci_restore_state(pdev); + intel_vsec_pci_init(pdev); + +out: + return status; +} + +static void intel_vsec_pci_resume(struct pci_dev *pdev) +{ + dev_info(&pdev->dev, "Done resuming PCI device\n"); +} + +static const struct pci_error_handlers intel_vsec_pci_err_handlers = { + .error_detected = intel_vsec_pci_error_detected, + .slot_reset = intel_vsec_pci_slot_reset, + .resume = intel_vsec_pci_resume, +}; + +static struct pci_driver intel_vsec_pci_driver = { + .name = "intel_vsec", + .id_table = intel_vsec_pci_ids, + .probe = intel_vsec_pci_probe, + .err_handler = &intel_vsec_pci_err_handlers, +}; +module_pci_driver(intel_vsec_pci_driver); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel Extended Capabilities auxiliary bus driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/vsec_tpmi.c b/drivers/platform/x86/intel/vsec_tpmi.c new file mode 100644 index 000000000..0153dd578 --- /dev/null +++ b/drivers/platform/x86/intel/vsec_tpmi.c @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver to enumerate TPMI features and create devices + * + * Copyright (c) 2023, Intel Corporation. + * All Rights Reserved. + * + * The TPMI (Topology Aware Register and PM Capsule Interface) provides a + * flexible, extendable and PCIe enumerable MMIO interface for PM features. + * + * For example Intel RAPL (Running Average Power Limit) provides a MMIO + * interface using TPMI. This has advantage over traditional MSR + * (Model Specific Register) interface, where a thread needs to be scheduled + * on the target CPU to read or write. Also the RAPL features vary between + * CPU models, and hence lot of model specific code. Here TPMI provides an + * architectural interface by providing hierarchical tables and fields, + * which will not need any model specific implementation. + * + * The TPMI interface uses a PCI VSEC structure to expose the location of + * MMIO region. + * + * This VSEC structure is present in the PCI configuration space of the + * Intel Out-of-Band (OOB) device, which is handled by the Intel VSEC + * driver. The Intel VSEC driver parses VSEC structures present in the PCI + * configuration space of the given device and creates an auxiliary device + * object for each of them. In particular, it creates an auxiliary device + * object representing TPMI that can be bound by an auxiliary driver. + * + * This TPMI driver will bind to the TPMI auxiliary device object created + * by the Intel VSEC driver. + * + * The TPMI specification defines a PFS (PM Feature Structure) table. + * This table is present in the TPMI MMIO region. The starting address + * of PFS is derived from the tBIR (Bar Indicator Register) and "Address" + * field from the VSEC header. + * + * Each TPMI PM feature has one entry in the PFS with a unique TPMI + * ID and its access details. The TPMI driver creates device nodes + * for the supported PM features. + * + * The names of the devices created by the TPMI driver start with the + * "intel_vsec.tpmi-" prefix which is followed by a specific name of the + * given PM feature (for example, "intel_vsec.tpmi-rapl.0"). + * + * The device nodes are create by using interface "intel_vsec_add_aux()" + * provided by the Intel VSEC driver. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * struct intel_tpmi_pfs_entry - TPMI PM Feature Structure (PFS) entry + * @tpmi_id: TPMI feature identifier (what the feature is and its data format). + * @num_entries: Number of feature interface instances present in the PFS. + * This represents the maximum number of Power domains in the SoC. + * @entry_size: Interface instance entry size in 32-bit words. + * @cap_offset: Offset from the PM_Features base address to the base of the PM VSEC + * register bank in KB. + * @attribute: Feature attribute: 0=BIOS. 1=OS. 2-3=Reserved. + * @reserved: Bits for use in the future. + * + * Represents one TPMI feature entry data in the PFS retrieved as is + * from the hardware. + */ +struct intel_tpmi_pfs_entry { + u64 tpmi_id:8; + u64 num_entries:8; + u64 entry_size:16; + u64 cap_offset:16; + u64 attribute:2; + u64 reserved:14; +} __packed; + +/** + * struct intel_tpmi_pm_feature - TPMI PM Feature information for a TPMI ID + * @pfs_header: PFS header retireved from the hardware. + * @vsec_offset: Starting MMIO address for this feature in bytes. Essentially + * this offset = "Address" from VSEC header + PFS Capability + * offset for this feature entry. + * @vsec_dev: Pointer to intel_vsec_device structure for this TPMI device + * + * Represents TPMI instance information for one TPMI ID. + */ +struct intel_tpmi_pm_feature { + struct intel_tpmi_pfs_entry pfs_header; + u64 vsec_offset; + struct intel_vsec_device *vsec_dev; +}; + +/** + * struct intel_tpmi_info - TPMI information for all IDs in an instance + * @tpmi_features: Pointer to a list of TPMI feature instances + * @vsec_dev: Pointer to intel_vsec_device structure for this TPMI device + * @feature_count: Number of TPMI of TPMI instances pointed by tpmi_features + * @pfs_start: Start of PFS offset for the TPMI instances in this device + * @plat_info: Stores platform info which can be used by the client drivers + * @tpmi_control_mem: Memory mapped IO for getting control information + * @dbgfs_dir: debugfs entry pointer + * + * Stores the information for all TPMI devices enumerated from a single PCI device. + */ +struct intel_tpmi_info { + struct intel_tpmi_pm_feature *tpmi_features; + struct intel_vsec_device *vsec_dev; + int feature_count; + u64 pfs_start; + struct oobmsm_plat_info plat_info; + void __iomem *tpmi_control_mem; + struct dentry *dbgfs_dir; +}; + +/** + * struct tpmi_info_header - CPU package ID to PCI device mapping information + * @fn: PCI function number + * @dev: PCI device number + * @bus: PCI bus number + * @pkg: CPU Package id + * @segment: PCI segment id + * @partition: Package Partition id + * @cdie_mask: Bitmap of compute dies in the current partition + * @reserved: Reserved for future use + * @lock: When set to 1 the register is locked and becomes read-only + * until next reset. Not for use by the OS driver. + * + * The structure to read hardware provided mapping information. + */ +struct tpmi_info_header { + u64 fn:3; + u64 dev:5; + u64 bus:8; + u64 pkg:8; + u64 segment:8; + u64 partition:2; + u64 cdie_mask:16; + u64 reserved:13; + u64 lock:1; +} __packed; + +/** + * struct tpmi_feature_state - Structure to read hardware state of a feature + * @enabled: Enable state of a feature, 1: enabled, 0: disabled + * @reserved_1: Reserved for future use + * @write_blocked: Writes are blocked means all write operations are ignored + * @read_blocked: Reads are blocked means will read 0xFFs + * @pcs_select: Interface used by out of band software, not used in OS + * @reserved_2: Reserved for future use + * @id: TPMI ID of the feature + * @reserved_3: Reserved for future use + * @locked: When set to 1, OS can't change this register. + * + * The structure is used to read hardware state of a TPMI feature. This + * information is used for debug and restricting operations for this feature. + */ +struct tpmi_feature_state { + u32 enabled:1; + u32 reserved_1:3; + u32 write_blocked:1; + u32 read_blocked:1; + u32 pcs_select:1; + u32 reserved_2:1; + u32 id:8; + u32 reserved_3:15; + u32 locked:1; +} __packed; + +/* + * The size from hardware is in u32 units. This size is from a trusted hardware, + * but better to verify for pre silicon platforms. Set size to 0, when invalid. + */ +#define TPMI_GET_SINGLE_ENTRY_SIZE(pfs) \ +({ \ + pfs->pfs_header.entry_size > SZ_1K ? 0 : pfs->pfs_header.entry_size << 2; \ +}) + +/* Used during auxbus device creation */ +static DEFINE_IDA(intel_vsec_tpmi_ida); + +static BLOCKING_NOTIFIER_HEAD(tpmi_notify_list); + +int tpmi_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&tpmi_notify_list, nb); +} +EXPORT_SYMBOL_NS_GPL(tpmi_register_notifier, "INTEL_TPMI"); + +int tpmi_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&tpmi_notify_list, nb); +} +EXPORT_SYMBOL_NS_GPL(tpmi_unregister_notifier, "INTEL_TPMI"); + +struct oobmsm_plat_info *tpmi_get_platform_data(struct auxiliary_device *auxdev) +{ + struct intel_vsec_device *vsec_dev = auxdev_to_ivdev(auxdev); + + return vsec_dev->priv_data; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_platform_data, "INTEL_TPMI"); + +int tpmi_get_resource_count(struct auxiliary_device *auxdev) +{ + struct intel_vsec_device *vsec_dev = auxdev_to_ivdev(auxdev); + + if (vsec_dev) + return vsec_dev->num_resources; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_resource_count, "INTEL_TPMI"); + +struct resource *tpmi_get_resource_at_index(struct auxiliary_device *auxdev, int index) +{ + struct intel_vsec_device *vsec_dev = auxdev_to_ivdev(auxdev); + + if (vsec_dev && index < vsec_dev->num_resources) + return &vsec_dev->resource[index]; + + return NULL; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_resource_at_index, "INTEL_TPMI"); + +/* TPMI Control Interface */ + +#define TPMI_CONTROL_STATUS_OFFSET 0x00 +#define TPMI_COMMAND_OFFSET 0x08 +#define TMPI_CONTROL_DATA_VAL_OFFSET 0x0c + +/* + * Spec is calling for max 1 seconds to get ownership at the worst + * case. Read at 10 ms timeouts and repeat up to 1 second. + */ +#define TPMI_CONTROL_TIMEOUT_US (10 * USEC_PER_MSEC) +#define TPMI_CONTROL_TIMEOUT_MAX_US (1 * USEC_PER_SEC) + +#define TPMI_RB_TIMEOUT_US (10 * USEC_PER_MSEC) +#define TPMI_RB_TIMEOUT_MAX_US USEC_PER_SEC + +/* TPMI Control status register defines */ + +#define TPMI_CONTROL_STATUS_RB BIT_ULL(0) + +#define TPMI_CONTROL_STATUS_OWNER GENMASK_ULL(5, 4) +#define TPMI_OWNER_NONE 0 +#define TPMI_OWNER_IN_BAND 1 + +#define TPMI_CONTROL_STATUS_CPL BIT_ULL(6) +#define TPMI_CONTROL_STATUS_RESULT GENMASK_ULL(15, 8) +#define TPMI_CONTROL_STATUS_LEN GENMASK_ULL(31, 16) + +#define TPMI_CMD_PKT_LEN 2 +#define TPMI_CMD_STATUS_SUCCESS 0x40 + +/* TPMI command data registers */ +#define TMPI_CONTROL_DATA_CMD GENMASK_ULL(7, 0) +#define TPMI_CONTROL_DATA_VAL_FEATURE GENMASK_ULL(48, 40) + +/* Command to send via control interface */ +#define TPMI_CONTROL_GET_STATE_CMD 0x10 + +#define TPMI_CONTROL_CMD_MASK GENMASK_ULL(48, 40) + +#define TPMI_CMD_LEN_MASK GENMASK_ULL(18, 16) + +/* Mutex to complete get feature status without interruption */ +static DEFINE_MUTEX(tpmi_dev_lock); + +static int tpmi_wait_for_owner(struct intel_tpmi_info *tpmi_info, u8 owner) +{ + u64 control; + + return readq_poll_timeout(tpmi_info->tpmi_control_mem + TPMI_CONTROL_STATUS_OFFSET, + control, owner == FIELD_GET(TPMI_CONTROL_STATUS_OWNER, control), + TPMI_CONTROL_TIMEOUT_US, TPMI_CONTROL_TIMEOUT_MAX_US); +} + +static int tpmi_read_feature_status(struct intel_tpmi_info *tpmi_info, int feature_id, + struct tpmi_feature_state *feature_state) +{ + u64 control, data; + int ret; + + if (!tpmi_info->tpmi_control_mem) + return -EFAULT; + + mutex_lock(&tpmi_dev_lock); + + /* Wait for owner bit set to 0 (none) */ + ret = tpmi_wait_for_owner(tpmi_info, TPMI_OWNER_NONE); + if (ret) + goto err_unlock; + + /* set command id to 0x10 for TPMI_GET_STATE */ + data = FIELD_PREP(TMPI_CONTROL_DATA_CMD, TPMI_CONTROL_GET_STATE_CMD); + + /* 32 bits for DATA offset and +8 for feature_id field */ + data |= FIELD_PREP(TPMI_CONTROL_DATA_VAL_FEATURE, feature_id); + + /* Write at command offset for qword access */ + writeq(data, tpmi_info->tpmi_control_mem + TPMI_COMMAND_OFFSET); + + /* Wait for owner bit set to in-band */ + ret = tpmi_wait_for_owner(tpmi_info, TPMI_OWNER_IN_BAND); + if (ret) + goto err_unlock; + + /* Set Run Busy and packet length of 2 dwords */ + control = TPMI_CONTROL_STATUS_RB; + control |= FIELD_PREP(TPMI_CONTROL_STATUS_LEN, TPMI_CMD_PKT_LEN); + + /* Write at status offset for qword access */ + writeq(control, tpmi_info->tpmi_control_mem + TPMI_CONTROL_STATUS_OFFSET); + + /* Wait for Run Busy clear */ + ret = readq_poll_timeout(tpmi_info->tpmi_control_mem + TPMI_CONTROL_STATUS_OFFSET, + control, !(control & TPMI_CONTROL_STATUS_RB), + TPMI_RB_TIMEOUT_US, TPMI_RB_TIMEOUT_MAX_US); + if (ret) + goto done_proc; + + control = FIELD_GET(TPMI_CONTROL_STATUS_RESULT, control); + if (control != TPMI_CMD_STATUS_SUCCESS) { + ret = -EBUSY; + goto done_proc; + } + + /* Response is ready */ + memcpy_fromio(feature_state, tpmi_info->tpmi_control_mem + TMPI_CONTROL_DATA_VAL_OFFSET, + sizeof(*feature_state)); + + ret = 0; + +done_proc: + /* Set CPL "completion" bit */ + writeq(TPMI_CONTROL_STATUS_CPL, tpmi_info->tpmi_control_mem + TPMI_CONTROL_STATUS_OFFSET); + +err_unlock: + mutex_unlock(&tpmi_dev_lock); + + return ret; +} + +int tpmi_get_feature_status(struct auxiliary_device *auxdev, + int feature_id, bool *read_blocked, bool *write_blocked) +{ + struct intel_vsec_device *intel_vsec_dev = dev_to_ivdev(auxdev->dev.parent); + struct intel_tpmi_info *tpmi_info = auxiliary_get_drvdata(&intel_vsec_dev->auxdev); + struct tpmi_feature_state feature_state; + int ret; + + ret = tpmi_read_feature_status(tpmi_info, feature_id, &feature_state); + if (ret) + return ret; + + *read_blocked = feature_state.read_blocked; + *write_blocked = feature_state.write_blocked; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_feature_status, "INTEL_TPMI"); + +struct dentry *tpmi_get_debugfs_dir(struct auxiliary_device *auxdev) +{ + struct intel_vsec_device *intel_vsec_dev = dev_to_ivdev(auxdev->dev.parent); + struct intel_tpmi_info *tpmi_info = auxiliary_get_drvdata(&intel_vsec_dev->auxdev); + + return tpmi_info->dbgfs_dir; +} +EXPORT_SYMBOL_NS_GPL(tpmi_get_debugfs_dir, "INTEL_TPMI"); + +static int tpmi_pfs_dbg_show(struct seq_file *s, void *unused) +{ + struct intel_tpmi_info *tpmi_info = s->private; + int locked, disabled, read_blocked, write_blocked; + struct tpmi_feature_state feature_state; + struct intel_tpmi_pm_feature *pfs; + int ret, i; + + + seq_printf(s, "tpmi PFS start offset 0x:%llx\n", tpmi_info->pfs_start); + seq_puts(s, "tpmi_id\t\tentries\t\tsize\t\tcap_offset\tattribute\tvsec_offset\tlocked\tdisabled\tread_blocked\twrite_blocked\n"); + for (i = 0; i < tpmi_info->feature_count; ++i) { + pfs = &tpmi_info->tpmi_features[i]; + ret = tpmi_read_feature_status(tpmi_info, pfs->pfs_header.tpmi_id, &feature_state); + if (ret) { + locked = 'U'; + disabled = 'U'; + read_blocked = 'U'; + write_blocked = 'U'; + } else { + disabled = feature_state.enabled ? 'N' : 'Y'; + locked = feature_state.locked ? 'Y' : 'N'; + read_blocked = feature_state.read_blocked ? 'Y' : 'N'; + write_blocked = feature_state.write_blocked ? 'Y' : 'N'; + } + seq_printf(s, "0x%02x\t\t0x%02x\t\t0x%04x\t\t0x%04x\t\t0x%02x\t\t0x%016llx\t%c\t%c\t\t%c\t\t%c\n", + pfs->pfs_header.tpmi_id, pfs->pfs_header.num_entries, + pfs->pfs_header.entry_size, pfs->pfs_header.cap_offset, + pfs->pfs_header.attribute, pfs->vsec_offset, locked, disabled, + read_blocked, write_blocked); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tpmi_pfs_dbg); + +#define MEM_DUMP_COLUMN_COUNT 8 + +static int tpmi_mem_dump_show(struct seq_file *s, void *unused) +{ + size_t row_size = MEM_DUMP_COLUMN_COUNT * sizeof(u32); + struct intel_tpmi_pm_feature *pfs = s->private; + int count, ret = 0; + void __iomem *mem; + u32 size; + u64 off; + u8 *buffer; + + size = TPMI_GET_SINGLE_ENTRY_SIZE(pfs); + if (!size) + return -EIO; + + buffer = kmalloc(size, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + off = pfs->vsec_offset; + + mutex_lock(&tpmi_dev_lock); + + for (count = 0; count < pfs->pfs_header.num_entries; ++count) { + seq_printf(s, "TPMI Instance:%d offset:0x%llx\n", count, off); + + mem = ioremap(off, size); + if (!mem) { + ret = -ENOMEM; + break; + } + + memcpy_fromio(buffer, mem, size); + + seq_hex_dump(s, " ", DUMP_PREFIX_OFFSET, row_size, sizeof(u32), buffer, size, + false); + + iounmap(mem); + + off += size; + } + + mutex_unlock(&tpmi_dev_lock); + + kfree(buffer); + + return ret; +} +DEFINE_SHOW_ATTRIBUTE(tpmi_mem_dump); + +static ssize_t mem_write(struct file *file, const char __user *userbuf, size_t len, loff_t *ppos) +{ + struct seq_file *m = file->private_data; + struct intel_tpmi_pm_feature *pfs = m->private; + u32 addr, value, punit, size; + u32 num_elems; + void __iomem *mem; + int ret; + + size = TPMI_GET_SINGLE_ENTRY_SIZE(pfs); + if (!size) + return -EIO; + + u32 *array __free(kfree) = NULL; + ret = parse_int_array_user(userbuf, len, (int **)&array); + if (ret < 0) + return ret; + + num_elems = *array; + if (num_elems != 3) + return -EINVAL; + + punit = array[1]; + addr = array[2]; + value = array[3]; + + if (!IS_ALIGNED(addr, sizeof(u32))) + return -EINVAL; + + if (punit >= pfs->pfs_header.num_entries) + return -EINVAL; + + if (addr >= size) + return -EINVAL; + + guard(mutex)(&tpmi_dev_lock); + + mem = ioremap(pfs->vsec_offset + punit * size, size); + if (!mem) + return -ENOMEM; + + writel(value, mem + addr); + + iounmap(mem); + + return len; +} + +static int mem_write_show(struct seq_file *s, void *unused) +{ + return 0; +} + +static int mem_write_open(struct inode *inode, struct file *file) +{ + return single_open(file, mem_write_show, inode->i_private); +} + +static const struct file_operations mem_write_ops = { + .open = mem_write_open, + .read = seq_read, + .write = mem_write, + .llseek = seq_lseek, + .release = single_release, +}; + +#define tpmi_to_dev(info) ((info)->vsec_dev->dev) + +static void tpmi_dbgfs_register(struct intel_tpmi_info *tpmi_info) +{ + char name[64]; + int i; + + snprintf(name, sizeof(name), "tpmi-%s", dev_name(tpmi_to_dev(tpmi_info))); + tpmi_info->dbgfs_dir = debugfs_create_dir(name, NULL); + + debugfs_create_file("pfs_dump", 0444, tpmi_info->dbgfs_dir, tpmi_info, &tpmi_pfs_dbg_fops); + + for (i = 0; i < tpmi_info->feature_count; ++i) { + struct intel_tpmi_pm_feature *pfs; + struct dentry *dir; + + pfs = &tpmi_info->tpmi_features[i]; + snprintf(name, sizeof(name), "tpmi-id-%02x", pfs->pfs_header.tpmi_id); + dir = debugfs_create_dir(name, tpmi_info->dbgfs_dir); + + debugfs_create_file("mem_dump", 0444, dir, pfs, &tpmi_mem_dump_fops); + debugfs_create_file("mem_write", 0644, dir, pfs, &mem_write_ops); + } +} + +static void tpmi_set_control_base(struct auxiliary_device *auxdev, + struct intel_tpmi_info *tpmi_info, + struct intel_tpmi_pm_feature *pfs) +{ + void __iomem *mem; + u32 size; + + size = TPMI_GET_SINGLE_ENTRY_SIZE(pfs); + if (!size) + return; + + mem = devm_ioremap(&auxdev->dev, pfs->vsec_offset, size); + if (!mem) + return; + + /* mem is pointing to TPMI CONTROL base */ + tpmi_info->tpmi_control_mem = mem; +} + +static const char *intel_tpmi_name(enum intel_tpmi_id id) +{ + switch (id) { + case TPMI_ID_RAPL: + return "rapl"; + case TPMI_ID_PEM: + return "pem"; + case TPMI_ID_UNCORE: + return "uncore"; + case TPMI_ID_SST: + return "sst"; + case TPMI_ID_PLR: + return "plr"; + default: + return NULL; + } +} + +/* String Length for tpmi-"feature_name(upto 8 bytes)" */ +#define TPMI_FEATURE_NAME_LEN 14 + +static int tpmi_create_device(struct intel_tpmi_info *tpmi_info, + struct intel_tpmi_pm_feature *pfs, + u64 pfs_start) +{ + struct intel_vsec_device *vsec_dev = tpmi_info->vsec_dev; + char feature_id_name[TPMI_FEATURE_NAME_LEN]; + struct intel_vsec_device *feature_vsec_dev; + struct tpmi_feature_state feature_state; + struct resource *res, *tmp; + const char *name; + int i, ret; + + ret = tpmi_read_feature_status(tpmi_info, pfs->pfs_header.tpmi_id, &feature_state); + if (ret) + return ret; + + /* + * If not enabled, continue to look at other features in the PFS, so return -EOPNOTSUPP. + * This will not cause failure of loading of this driver. + */ + if (!feature_state.enabled) + return -EOPNOTSUPP; + + name = intel_tpmi_name(pfs->pfs_header.tpmi_id); + if (!name) + return -EOPNOTSUPP; + + feature_vsec_dev = kzalloc_flex(*feature_vsec_dev, resource, pfs->pfs_header.num_entries); + if (!feature_vsec_dev) + return -ENOMEM; + + feature_vsec_dev->num_resources = pfs->pfs_header.num_entries; + res = feature_vsec_dev->resource; + + snprintf(feature_id_name, sizeof(feature_id_name), "tpmi-%s", name); + + for (i = 0, tmp = res; i < pfs->pfs_header.num_entries; i++, tmp++) { + u64 entry_size_bytes = pfs->pfs_header.entry_size * sizeof(u32); + + tmp->start = pfs->vsec_offset + entry_size_bytes * i; + tmp->end = tmp->start + entry_size_bytes - 1; + tmp->flags = IORESOURCE_MEM; + } + + feature_vsec_dev->dev = vsec_dev->dev; + feature_vsec_dev->priv_data = &tpmi_info->plat_info; + feature_vsec_dev->priv_data_size = sizeof(tpmi_info->plat_info); + feature_vsec_dev->ida = &intel_vsec_tpmi_ida; + + /* + * intel_vsec_add_aux() is resource managed, no explicit + * delete is required on error or on module unload. + * feature_vsec_dev and res memory are also freed as part of + * device deletion. + */ + return intel_vsec_add_aux(&vsec_dev->auxdev.dev, + feature_vsec_dev, feature_id_name); +} + +static int tpmi_create_devices(struct intel_tpmi_info *tpmi_info) +{ + struct intel_vsec_device *vsec_dev = tpmi_info->vsec_dev; + int ret, i; + + for (i = 0; i < vsec_dev->num_resources; i++) { + ret = tpmi_create_device(tpmi_info, &tpmi_info->tpmi_features[i], + tpmi_info->pfs_start); + /* + * Fail, if the supported features fails to create device, + * otherwise, continue. Even if one device failed to create, + * fail the loading of driver. Since intel_vsec_add_aux() + * is resource managed, no clean up is required for the + * successfully created devices. + */ + if (ret && ret != -EOPNOTSUPP) + return ret; + } + + return 0; +} + +#define TPMI_INFO_BUS_INFO_OFFSET 0x08 +#define TPMI_INFO_MAJOR_VERSION 0x00 +#define TPMI_INFO_MINOR_VERSION 0x02 + +static int tpmi_process_info(struct intel_tpmi_info *tpmi_info, + struct intel_tpmi_pm_feature *pfs) +{ + struct tpmi_info_header header; + void __iomem *info_mem; + u64 feature_header; + int ret = 0; + + info_mem = ioremap(pfs->vsec_offset, pfs->pfs_header.entry_size * sizeof(u32)); + if (!info_mem) + return -ENOMEM; + + feature_header = readq(info_mem); + if (TPMI_MAJOR_VERSION(feature_header) != TPMI_INFO_MAJOR_VERSION) { + ret = -ENODEV; + goto error_info_header; + } + + memcpy_fromio(&header, info_mem + TPMI_INFO_BUS_INFO_OFFSET, sizeof(header)); + + tpmi_info->plat_info.package_id = header.pkg; + tpmi_info->plat_info.bus_number = header.bus; + tpmi_info->plat_info.device_number = header.dev; + tpmi_info->plat_info.function_number = header.fn; + + if (TPMI_MINOR_VERSION(feature_header) >= TPMI_INFO_MINOR_VERSION) { + tpmi_info->plat_info.cdie_mask = header.cdie_mask; + tpmi_info->plat_info.partition = header.partition; + tpmi_info->plat_info.segment = header.segment; + } + +error_info_header: + iounmap(info_mem); + + return ret; +} + +static int tpmi_fetch_pfs_header(struct intel_tpmi_pm_feature *pfs, u64 start, int size) +{ + void __iomem *pfs_mem; + + pfs_mem = ioremap(start, size); + if (!pfs_mem) + return -ENOMEM; + + memcpy_fromio(&pfs->pfs_header, pfs_mem, sizeof(pfs->pfs_header)); + + iounmap(pfs_mem); + + return 0; +} + +#define TPMI_CAP_OFFSET_UNIT 1024 + +static int intel_vsec_tpmi_init(struct auxiliary_device *auxdev) +{ + struct intel_vsec_device *vsec_dev = auxdev_to_ivdev(auxdev); + struct pci_dev *pci_dev = to_pci_dev(vsec_dev->dev); + struct intel_tpmi_info *tpmi_info; + u64 pfs_start = 0; + int ret, i; + + tpmi_info = devm_kzalloc(&auxdev->dev, sizeof(*tpmi_info), GFP_KERNEL); + if (!tpmi_info) + return -ENOMEM; + + tpmi_info->vsec_dev = vsec_dev; + tpmi_info->feature_count = vsec_dev->num_resources; + tpmi_info->plat_info.bus_number = pci_dev->bus->number; + + tpmi_info->tpmi_features = devm_kcalloc(&auxdev->dev, vsec_dev->num_resources, + sizeof(*tpmi_info->tpmi_features), + GFP_KERNEL); + if (!tpmi_info->tpmi_features) + return -ENOMEM; + + for (i = 0; i < vsec_dev->num_resources; i++) { + struct intel_tpmi_pm_feature *pfs; + struct resource *res; + u64 res_start; + int size, ret; + + pfs = &tpmi_info->tpmi_features[i]; + pfs->vsec_dev = vsec_dev; + + res = &vsec_dev->resource[i]; + if (!res) + continue; + + res_start = res->start; + size = resource_size(res); + if (size < 0) + continue; + + ret = tpmi_fetch_pfs_header(pfs, res_start, size); + if (ret) + continue; + + if (!pfs_start) + pfs_start = res_start; + + pfs->vsec_offset = pfs_start + pfs->pfs_header.cap_offset * TPMI_CAP_OFFSET_UNIT; + + /* + * Process TPMI_INFO to get PCI device to CPU package ID. + * Device nodes for TPMI features are not created in this + * for loop. So, the mapping information will be available + * when actual device nodes created outside this + * loop via tpmi_create_devices(). + */ + if (pfs->pfs_header.tpmi_id == TPMI_INFO_ID) { + ret = tpmi_process_info(tpmi_info, pfs); + if (ret) + return ret; + + ret = intel_vsec_set_mapping(&tpmi_info->plat_info, vsec_dev); + if (ret) + return ret; + } + + if (pfs->pfs_header.tpmi_id == TPMI_CONTROL_ID) + tpmi_set_control_base(auxdev, tpmi_info, pfs); + } + + tpmi_info->pfs_start = pfs_start; + + auxiliary_set_drvdata(auxdev, tpmi_info); + + /* + * Allow debugfs when security policy allows. Everything this debugfs + * interface provides, can also be done via /dev/mem access. If + * /dev/mem interface is locked, don't allow debugfs to present any + * information. Also check for CAP_SYS_RAWIO as /dev/mem interface. + */ + if (!security_locked_down(LOCKDOWN_DEV_MEM) && capable(CAP_SYS_RAWIO)) + tpmi_dbgfs_register(tpmi_info); + + ret = tpmi_create_devices(tpmi_info); + if (ret) { + debugfs_remove_recursive(tpmi_info->dbgfs_dir); + return ret; + } + + blocking_notifier_call_chain(&tpmi_notify_list, TPMI_CORE_INIT, auxdev); + + return 0; +} + +static int tpmi_probe(struct auxiliary_device *auxdev, + const struct auxiliary_device_id *id) +{ + return intel_vsec_tpmi_init(auxdev); +} + +static void tpmi_remove(struct auxiliary_device *auxdev) +{ + struct intel_tpmi_info *tpmi_info = auxiliary_get_drvdata(auxdev); + + blocking_notifier_call_chain(&tpmi_notify_list, TPMI_CORE_EXIT, auxdev); + + debugfs_remove_recursive(tpmi_info->dbgfs_dir); +} + +static const struct auxiliary_device_id tpmi_id_table[] = { + { .name = "intel_vsec.tpmi" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, tpmi_id_table); + +static struct auxiliary_driver tpmi_aux_driver = { + .id_table = tpmi_id_table, + .probe = tpmi_probe, + .remove = tpmi_remove, +}; + +module_auxiliary_driver(tpmi_aux_driver); + +MODULE_IMPORT_NS("INTEL_VSEC"); +MODULE_DESCRIPTION("Intel TPMI enumeration module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/intel/wmi/Kconfig b/drivers/platform/x86/intel/wmi/Kconfig new file mode 100644 index 000000000..8e159f712 --- /dev/null +++ b/drivers/platform/x86/intel/wmi/Kconfig @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +config INTEL_WMI + bool + +config INTEL_WMI_SBL_FW_UPDATE + tristate "Intel WMI Slim Bootloader firmware update signaling driver" + depends on ACPI_WMI + select INTEL_WMI + help + Say Y here if you want to be able to use the WMI interface to signal + Slim Bootloader to trigger update on next reboot. + + To compile this driver as a module, choose M here: the module will + be called intel-wmi-sbl-fw-update. + +config INTEL_WMI_THUNDERBOLT + tristate "Intel WMI thunderbolt force power driver" + depends on ACPI_WMI + select INTEL_WMI + help + Say Y here if you want to be able to use the WMI interface on select + systems to force the power control of Intel Thunderbolt controllers. + This is useful for updating the firmware when devices are not plugged + into the controller. + + To compile this driver as a module, choose M here: the module will + be called intel-wmi-thunderbolt. diff --git a/drivers/platform/x86/intel/wmi/Makefile b/drivers/platform/x86/intel/wmi/Makefile new file mode 100644 index 000000000..c2d56d25d --- /dev/null +++ b/drivers/platform/x86/intel/wmi/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Intel x86 Platform Specific Drivers +# + +intel-wmi-sbl-fw-update-y := sbl-fw-update.o +obj-$(CONFIG_INTEL_WMI_SBL_FW_UPDATE) += intel-wmi-sbl-fw-update.o +intel-wmi-thunderbolt-y := thunderbolt.o +obj-$(CONFIG_INTEL_WMI_THUNDERBOLT) += intel-wmi-thunderbolt.o diff --git a/drivers/platform/x86/intel/wmi/sbl-fw-update.c b/drivers/platform/x86/intel/wmi/sbl-fw-update.c new file mode 100644 index 000000000..62c9c7f18 --- /dev/null +++ b/drivers/platform/x86/intel/wmi/sbl-fw-update.c @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Slim Bootloader(SBL) firmware update signaling driver + * + * Slim Bootloader is a small, open-source, non UEFI compliant, boot firmware + * optimized for running on certain Intel platforms. + * + * SBL exposes an ACPI-WMI device via /sys/bus/wmi/devices/. + * This driver further adds "firmware_update_request" device attribute. + * This attribute normally has a value of 0 and userspace can signal SBL + * to update firmware, on next reboot, by writing a value of 1. + * + * More details of SBL firmware update process is available at: + * https://slimbootloader.github.io/security/firmware-update.html + */ + +#include +#include +#include +#include +#include + +#define INTEL_WMI_SBL_GUID "44FADEB1-B204-40F2-8581-394BBDC1B651" + +static int get_fwu_request(struct device *dev, u32 *out) +{ + struct wmi_buffer buffer; + __le32 *result; + int ret; + + ret = wmidev_query_block(to_wmi_device(dev), 0, &buffer, sizeof(*result)); + if (ret < 0) + return ret; + + result = buffer.data; + *out = le32_to_cpu(*result); + kfree(result); + + return 0; +} + +static int set_fwu_request(struct device *dev, u32 in) +{ + __le32 value = cpu_to_le32(in); + struct wmi_buffer buffer = { + .length = sizeof(value), + .data = &value, + }; + + return wmidev_set_block(to_wmi_device(dev), 0, &buffer); +} + +static ssize_t firmware_update_request_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u32 val; + int ret; + + ret = get_fwu_request(dev, &val); + if (ret) + return ret; + + return sprintf(buf, "%d\n", val); +} + +static ssize_t firmware_update_request_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int val; + int ret; + + ret = kstrtouint(buf, 0, &val); + if (ret) + return ret; + + /* May later be extended to support values other than 0 and 1 */ + if (val > 1) + return -ERANGE; + + ret = set_fwu_request(dev, val); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW(firmware_update_request); + +static struct attribute *firmware_update_attrs[] = { + &dev_attr_firmware_update_request.attr, + NULL +}; +ATTRIBUTE_GROUPS(firmware_update); + +static int intel_wmi_sbl_fw_update_probe(struct wmi_device *wdev, + const void *context) +{ + dev_info(&wdev->dev, "Slim Bootloader signaling driver attached\n"); + return 0; +} + +static void intel_wmi_sbl_fw_update_remove(struct wmi_device *wdev) +{ + dev_info(&wdev->dev, "Slim Bootloader signaling driver removed\n"); +} + +static const struct wmi_device_id intel_wmi_sbl_id_table[] = { + { .guid_string = INTEL_WMI_SBL_GUID }, + {} +}; +MODULE_DEVICE_TABLE(wmi, intel_wmi_sbl_id_table); + +static struct wmi_driver intel_wmi_sbl_fw_update_driver = { + .driver = { + .name = "intel-wmi-sbl-fw-update", + .dev_groups = firmware_update_groups, + }, + .probe = intel_wmi_sbl_fw_update_probe, + .remove = intel_wmi_sbl_fw_update_remove, + .id_table = intel_wmi_sbl_id_table, + .no_singleton = true, +}; +module_wmi_driver(intel_wmi_sbl_fw_update_driver); + +MODULE_AUTHOR("Jithu Joseph "); +MODULE_DESCRIPTION("Slim Bootloader firmware update signaling driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel/wmi/thunderbolt.c b/drivers/platform/x86/intel/wmi/thunderbolt.c new file mode 100644 index 000000000..9b1920d61 --- /dev/null +++ b/drivers/platform/x86/intel/wmi/thunderbolt.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * WMI Thunderbolt driver + * + * Copyright (C) 2017 Dell Inc. All Rights Reserved. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define INTEL_WMI_THUNDERBOLT_GUID "86CCFD48-205E-4A77-9C48-2021CBEDE341" + +static ssize_t force_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct wmi_buffer buffer; + int ret; + u8 mode; + + buffer.length = sizeof(mode); + buffer.data = &mode; + + mode = hex_to_bin(buf[0]); + if (mode > 1) + return -EINVAL; + + ret = wmidev_invoke_procedure(to_wmi_device(dev), 0, 1, &buffer); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR_WO(force_power); + +static struct attribute *tbt_attrs[] = { + &dev_attr_force_power.attr, + NULL +}; +ATTRIBUTE_GROUPS(tbt); + +static const struct wmi_device_id intel_wmi_thunderbolt_id_table[] = { + { .guid_string = INTEL_WMI_THUNDERBOLT_GUID }, + { }, +}; + +static struct wmi_driver intel_wmi_thunderbolt_driver = { + .driver = { + .name = "intel-wmi-thunderbolt", + .dev_groups = tbt_groups, + }, + .id_table = intel_wmi_thunderbolt_id_table, + .no_singleton = true, +}; + +module_wmi_driver(intel_wmi_thunderbolt_driver); + +MODULE_DEVICE_TABLE(wmi, intel_wmi_thunderbolt_id_table); +MODULE_AUTHOR("Mario Limonciello "); +MODULE_DESCRIPTION("Intel WMI Thunderbolt force power driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel_ips.c b/drivers/platform/x86/intel_ips.c new file mode 100644 index 000000000..b1b2d9cab --- /dev/null +++ b/drivers/platform/x86/intel_ips.c @@ -0,0 +1,1624 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2009-2010 Intel Corporation + * + * Authors: + * Jesse Barnes + */ + +/* + * Some Intel Ibex Peak based platforms support so-called "intelligent + * power sharing", which allows the CPU and GPU to cooperate to maximize + * performance within a given TDP (thermal design point). This driver + * performs the coordination between the CPU and GPU, monitors thermal and + * power statistics in the platform, and initializes power monitoring + * hardware. It also provides a few tunables to control behavior. Its + * primary purpose is to safely allow CPU and GPU turbo modes to be enabled + * by tracking power and thermal budget; secondarily it can boost turbo + * performance by allocating more power or thermal budget to the CPU or GPU + * based on available headroom and activity. + * + * The basic algorithm is driven by a 5s moving average of temperature. If + * thermal headroom is available, the CPU and/or GPU power clamps may be + * adjusted upwards. If we hit the thermal ceiling or a thermal trigger, + * we scale back the clamp. Aside from trigger events (when we're critically + * close or over our TDP) we don't adjust the clamps more than once every + * five seconds. + * + * The thermal device (device 31, function 6) has a set of registers that + * are updated by the ME firmware. The ME should also take the clamp values + * written to those registers and write them to the CPU, but we currently + * bypass that functionality and write the CPU MSR directly. + * + * UNSUPPORTED: + * - dual MCP configs + * + * TODO: + * - handle CPU hotplug + * - provide turbo enable/disable api + * + * Related documents: + * - CDI 403777, 403778 - Auburndale EDS vol 1 & 2 + * - CDI 401376 - Ibex Peak EDS + * - ref 26037, 26641 - IPS BIOS spec + * - ref 26489 - Nehalem BIOS writer's guide + * - ref 26921 - Ibex Peak BIOS Specification + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "intel_ips.h" + +#include + +#define PCI_DEVICE_ID_INTEL_THERMAL_SENSOR 0x3b32 + +/* + * Package level MSRs for monitor/control + */ +#define PLATFORM_INFO 0xce +#define PLATFORM_TDP (1<<29) +#define PLATFORM_RATIO (1<<28) + +#define IA32_MISC_ENABLE 0x1a0 +#define IA32_MISC_TURBO_EN (1ULL<<38) + +#define TURBO_POWER_CURRENT_LIMIT 0x1ac +#define TURBO_TDC_OVR_EN (1UL<<31) +#define TURBO_TDC_MASK (0x000000007fff0000UL) +#define TURBO_TDC_SHIFT (16) +#define TURBO_TDP_OVR_EN (1UL<<15) +#define TURBO_TDP_MASK (0x0000000000003fffUL) + +/* + * Core/thread MSRs for monitoring + */ +#define IA32_PERF_CTL 0x199 +#define IA32_PERF_TURBO_DIS (1ULL<<32) + +/* + * Thermal PCI device regs + */ +#define THM_CFG_TBAR 0x10 +#define THM_CFG_TBAR_HI 0x14 + +#define THM_TSIU 0x00 +#define THM_TSE 0x01 +#define TSE_EN 0xb8 +#define THM_TSS 0x02 +#define THM_TSTR 0x03 +#define THM_TSTTP 0x04 +#define THM_TSCO 0x08 +#define THM_TSES 0x0c +#define THM_TSGPEN 0x0d +#define TSGPEN_HOT_LOHI (1<<1) +#define TSGPEN_CRIT_LOHI (1<<2) +#define THM_TSPC 0x0e +#define THM_PPEC 0x10 +#define THM_CTA 0x12 +#define THM_PTA 0x14 +#define PTA_SLOPE_MASK (0xff00) +#define PTA_SLOPE_SHIFT 8 +#define PTA_OFFSET_MASK (0x00ff) +#define THM_MGTA 0x16 +#define MGTA_SLOPE_MASK (0xff00) +#define MGTA_SLOPE_SHIFT 8 +#define MGTA_OFFSET_MASK (0x00ff) +#define THM_TRC 0x1a +#define TRC_CORE2_EN (1<<15) +#define TRC_THM_EN (1<<12) +#define TRC_C6_WAR (1<<8) +#define TRC_CORE1_EN (1<<7) +#define TRC_CORE_PWR (1<<6) +#define TRC_PCH_EN (1<<5) +#define TRC_MCH_EN (1<<4) +#define TRC_DIMM4 (1<<3) +#define TRC_DIMM3 (1<<2) +#define TRC_DIMM2 (1<<1) +#define TRC_DIMM1 (1<<0) +#define THM_TES 0x20 +#define THM_TEN 0x21 +#define TEN_UPDATE_EN 1 +#define THM_PSC 0x24 +#define PSC_NTG (1<<0) /* No GFX turbo support */ +#define PSC_NTPC (1<<1) /* No CPU turbo support */ +#define PSC_PP_DEF (0<<2) /* Perf policy up to driver */ +#define PSP_PP_PC (1<<2) /* BIOS prefers CPU perf */ +#define PSP_PP_BAL (2<<2) /* BIOS wants balanced perf */ +#define PSP_PP_GFX (3<<2) /* BIOS prefers GFX perf */ +#define PSP_PBRT (1<<4) /* BIOS run time support */ +#define THM_CTV1 0x30 +#define CTV_TEMP_ERROR (1<<15) +#define CTV_TEMP_MASK 0x3f +#define CTV_ +#define THM_CTV2 0x32 +#define THM_CEC 0x34 /* undocumented power accumulator in joules */ +#define THM_AE 0x3f +#define THM_HTS 0x50 /* 32 bits */ +#define HTS_PCPL_MASK (0x7fe00000) +#define HTS_PCPL_SHIFT 21 +#define HTS_GPL_MASK (0x001ff000) +#define HTS_GPL_SHIFT 12 +#define HTS_PP_MASK (0x00000c00) +#define HTS_PP_SHIFT 10 +#define HTS_PP_DEF 0 +#define HTS_PP_PROC 1 +#define HTS_PP_BAL 2 +#define HTS_PP_GFX 3 +#define HTS_PCTD_DIS (1<<9) +#define HTS_GTD_DIS (1<<8) +#define HTS_PTL_MASK (0x000000fe) +#define HTS_PTL_SHIFT 1 +#define HTS_NVV (1<<0) +#define THM_HTSHI 0x54 /* 16 bits */ +#define HTS2_PPL_MASK (0x03ff) +#define HTS2_PRST_MASK (0x3c00) +#define HTS2_PRST_SHIFT 10 +#define HTS2_PRST_UNLOADED 0 +#define HTS2_PRST_RUNNING 1 +#define HTS2_PRST_TDISOP 2 /* turbo disabled due to power */ +#define HTS2_PRST_TDISHT 3 /* turbo disabled due to high temp */ +#define HTS2_PRST_TDISUSR 4 /* user disabled turbo */ +#define HTS2_PRST_TDISPLAT 5 /* platform disabled turbo */ +#define HTS2_PRST_TDISPM 6 /* power management disabled turbo */ +#define HTS2_PRST_TDISERR 7 /* some kind of error disabled turbo */ +#define THM_PTL 0x56 +#define THM_MGTV 0x58 +#define TV_MASK 0x000000000000ff00 +#define TV_SHIFT 8 +#define THM_PTV 0x60 +#define PTV_MASK 0x00ff +#define THM_MMGPC 0x64 +#define THM_MPPC 0x66 +#define THM_MPCPC 0x68 +#define THM_TSPIEN 0x82 +#define TSPIEN_AUX_LOHI (1<<0) +#define TSPIEN_HOT_LOHI (1<<1) +#define TSPIEN_CRIT_LOHI (1<<2) +#define TSPIEN_AUX2_LOHI (1<<3) +#define THM_TSLOCK 0x83 +#define THM_ATR 0x84 +#define THM_TOF 0x87 +#define THM_STS 0x98 +#define STS_PCPL_MASK (0x7fe00000) +#define STS_PCPL_SHIFT 21 +#define STS_GPL_MASK (0x001ff000) +#define STS_GPL_SHIFT 12 +#define STS_PP_MASK (0x00000c00) +#define STS_PP_SHIFT 10 +#define STS_PP_DEF 0 +#define STS_PP_PROC 1 +#define STS_PP_BAL 2 +#define STS_PP_GFX 3 +#define STS_PCTD_DIS (1<<9) +#define STS_GTD_DIS (1<<8) +#define STS_PTL_MASK (0x000000fe) +#define STS_PTL_SHIFT 1 +#define STS_NVV (1<<0) +#define THM_SEC 0x9c +#define SEC_ACK (1<<0) +#define THM_TC3 0xa4 +#define THM_TC1 0xa8 +#define STS_PPL_MASK (0x0003ff00) +#define STS_PPL_SHIFT 16 +#define THM_TC2 0xac +#define THM_DTV 0xb0 +#define THM_ITV 0xd8 +#define ITV_ME_SEQNO_MASK 0x00ff0000 /* ME should update every ~200ms */ +#define ITV_ME_SEQNO_SHIFT (16) +#define ITV_MCH_TEMP_MASK 0x0000ff00 +#define ITV_MCH_TEMP_SHIFT (8) +#define ITV_PCH_TEMP_MASK 0x000000ff + +#define thm_readb(off) readb(ips->regmap + (off)) +#define thm_readw(off) readw(ips->regmap + (off)) +#define thm_readl(off) readl(ips->regmap + (off)) +#define thm_readq(off) readq(ips->regmap + (off)) + +#define thm_writeb(off, val) writeb((val), ips->regmap + (off)) +#define thm_writew(off, val) writew((val), ips->regmap + (off)) +#define thm_writel(off, val) writel((val), ips->regmap + (off)) + +static const int IPS_ADJUST_PERIOD = 5000; /* ms */ +static bool late_i915_load = false; + +/* For initial average collection */ +static const int IPS_SAMPLE_PERIOD = 200; /* ms */ +static const int IPS_SAMPLE_WINDOW = 5000; /* 5s moving window of samples */ +#define IPS_SAMPLE_COUNT (IPS_SAMPLE_WINDOW / IPS_SAMPLE_PERIOD) + +/* Per-SKU limits */ +struct ips_mcp_limits { + int mcp_power_limit; /* mW units */ + int core_power_limit; + int mch_power_limit; + int core_temp_limit; /* degrees C */ + int mch_temp_limit; +}; + +/* Max temps are -10 degrees C to avoid PROCHOT# */ + +static struct ips_mcp_limits ips_sv_limits = { + .mcp_power_limit = 35000, + .core_power_limit = 29000, + .mch_power_limit = 20000, + .core_temp_limit = 95, + .mch_temp_limit = 90 +}; + +static struct ips_mcp_limits ips_lv_limits = { + .mcp_power_limit = 25000, + .core_power_limit = 21000, + .mch_power_limit = 13000, + .core_temp_limit = 95, + .mch_temp_limit = 90 +}; + +static struct ips_mcp_limits ips_ulv_limits = { + .mcp_power_limit = 18000, + .core_power_limit = 14000, + .mch_power_limit = 11000, + .core_temp_limit = 95, + .mch_temp_limit = 90 +}; + +struct ips_driver { + struct device *dev; + void __iomem *regmap; + int irq; + + struct task_struct *monitor; + struct task_struct *adjust; + struct dentry *debug_root; + struct timer_list timer; + + /* Average CPU core temps (all averages in .01 degrees C for precision) */ + u16 ctv1_avg_temp; + u16 ctv2_avg_temp; + /* GMCH average */ + u16 mch_avg_temp; + /* Average for the CPU (both cores?) */ + u16 mcp_avg_temp; + /* Average power consumption (in mW) */ + u32 cpu_avg_power; + u32 mch_avg_power; + + /* Offset values */ + u16 cta_val; + u16 pta_val; + u16 mgta_val; + + /* Maximums & prefs, protected by turbo status lock */ + spinlock_t turbo_status_lock; + u16 mcp_temp_limit; + u16 mcp_power_limit; + u16 core_power_limit; + u16 mch_power_limit; + bool cpu_turbo_enabled; + bool __cpu_turbo_on; + bool gpu_turbo_enabled; + bool __gpu_turbo_on; + bool gpu_preferred; + bool poll_turbo_status; + bool second_cpu; + bool turbo_toggle_allowed; + struct ips_mcp_limits *limits; + + /* Optional MCH interfaces for if i915 is in use */ + unsigned long (*read_mch_val)(void); + bool (*gpu_raise)(void); + bool (*gpu_lower)(void); + bool (*gpu_busy)(void); + bool (*gpu_turbo_disable)(void); + + /* For restoration at unload */ + u64 orig_turbo_limit; + u64 orig_turbo_ratios; +}; + +static bool +ips_gpu_turbo_enabled(struct ips_driver *ips); + +/** + * ips_cpu_busy - is CPU busy? + * @ips: IPS driver struct + * + * Check CPU for load to see whether we should increase its thermal budget. + * + * RETURNS: + * True if the CPU could use more power, false otherwise. + */ +static bool ips_cpu_busy(struct ips_driver *ips) +{ + if ((avenrun[0] >> FSHIFT) > 1) + return true; + + return false; +} + +/** + * ips_cpu_raise - raise CPU power clamp + * @ips: IPS driver struct + * + * Raise the CPU power clamp by %IPS_CPU_STEP, in accordance with TDP for + * this platform. + * + * We do this by adjusting the TURBO_POWER_CURRENT_LIMIT MSR upwards (as + * long as we haven't hit the TDP limit for the SKU). + */ +static void ips_cpu_raise(struct ips_driver *ips) +{ + u64 turbo_override; + u16 cur_tdp_limit, new_tdp_limit; + + if (!ips->cpu_turbo_enabled) + return; + + rdmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + + cur_tdp_limit = turbo_override & TURBO_TDP_MASK; + new_tdp_limit = cur_tdp_limit + 8; /* 1W increase */ + + /* Clamp to SKU TDP limit */ + if (((new_tdp_limit * 10) / 8) > ips->core_power_limit) + new_tdp_limit = cur_tdp_limit; + + thm_writew(THM_MPCPC, (new_tdp_limit * 10) / 8); + + turbo_override |= TURBO_TDC_OVR_EN | TURBO_TDP_OVR_EN; + wrmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + + turbo_override &= ~TURBO_TDP_MASK; + turbo_override |= new_tdp_limit; + + wrmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); +} + +/** + * ips_cpu_lower - lower CPU power clamp + * @ips: IPS driver struct + * + * Lower CPU power clamp b %IPS_CPU_STEP if possible. + * + * We do this by adjusting the TURBO_POWER_CURRENT_LIMIT MSR down, going + * as low as the platform limits will allow (though we could go lower there + * wouldn't be much point). + */ +static void ips_cpu_lower(struct ips_driver *ips) +{ + u64 turbo_override; + u16 cur_limit, new_limit; + + rdmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + + cur_limit = turbo_override & TURBO_TDP_MASK; + new_limit = cur_limit - 8; /* 1W decrease */ + + /* Clamp to SKU TDP limit */ + if (new_limit < (ips->orig_turbo_limit & TURBO_TDP_MASK)) + new_limit = ips->orig_turbo_limit & TURBO_TDP_MASK; + + thm_writew(THM_MPCPC, (new_limit * 10) / 8); + + turbo_override |= TURBO_TDC_OVR_EN | TURBO_TDP_OVR_EN; + wrmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + + turbo_override &= ~TURBO_TDP_MASK; + turbo_override |= new_limit; + + wrmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); +} + +/** + * do_enable_cpu_turbo - internal turbo enable function + * @data: unused + * + * Internal function for actually updating MSRs. When we enable/disable + * turbo, we need to do it on each CPU; this function is the one called + * by on_each_cpu() when needed. + */ +static void do_enable_cpu_turbo(void *data) +{ + u64 perf_ctl; + + rdmsrq(IA32_PERF_CTL, perf_ctl); + if (perf_ctl & IA32_PERF_TURBO_DIS) { + perf_ctl &= ~IA32_PERF_TURBO_DIS; + wrmsrq(IA32_PERF_CTL, perf_ctl); + } +} + +/** + * ips_enable_cpu_turbo - enable turbo mode on all CPUs + * @ips: IPS driver struct + * + * Enable turbo mode by clearing the disable bit in IA32_PERF_CTL on + * all logical threads. + */ +static void ips_enable_cpu_turbo(struct ips_driver *ips) +{ + /* Already on, no need to mess with MSRs */ + if (ips->__cpu_turbo_on) + return; + + if (ips->turbo_toggle_allowed) + on_each_cpu(do_enable_cpu_turbo, ips, 1); + + ips->__cpu_turbo_on = true; +} + +/** + * do_disable_cpu_turbo - internal turbo disable function + * @data: unused + * + * Internal function for actually updating MSRs. When we enable/disable + * turbo, we need to do it on each CPU; this function is the one called + * by on_each_cpu() when needed. + */ +static void do_disable_cpu_turbo(void *data) +{ + u64 perf_ctl; + + rdmsrq(IA32_PERF_CTL, perf_ctl); + if (!(perf_ctl & IA32_PERF_TURBO_DIS)) { + perf_ctl |= IA32_PERF_TURBO_DIS; + wrmsrq(IA32_PERF_CTL, perf_ctl); + } +} + +/** + * ips_disable_cpu_turbo - disable turbo mode on all CPUs + * @ips: IPS driver struct + * + * Disable turbo mode by setting the disable bit in IA32_PERF_CTL on + * all logical threads. + */ +static void ips_disable_cpu_turbo(struct ips_driver *ips) +{ + /* Already off, leave it */ + if (!ips->__cpu_turbo_on) + return; + + if (ips->turbo_toggle_allowed) + on_each_cpu(do_disable_cpu_turbo, ips, 1); + + ips->__cpu_turbo_on = false; +} + +/** + * ips_gpu_busy - is GPU busy? + * @ips: IPS driver struct + * + * Check GPU for load to see whether we should increase its thermal budget. + * We need to call into the i915 driver in this case. + * + * RETURNS: + * True if the GPU could use more power, false otherwise. + */ +static bool ips_gpu_busy(struct ips_driver *ips) +{ + if (!ips_gpu_turbo_enabled(ips)) + return false; + + return ips->gpu_busy(); +} + +/** + * ips_gpu_raise - raise GPU power clamp + * @ips: IPS driver struct + * + * Raise the GPU frequency/power if possible. We need to call into the + * i915 driver in this case. + */ +static void ips_gpu_raise(struct ips_driver *ips) +{ + if (!ips_gpu_turbo_enabled(ips)) + return; + + if (!ips->gpu_raise()) + ips->gpu_turbo_enabled = false; + + return; +} + +/** + * ips_gpu_lower - lower GPU power clamp + * @ips: IPS driver struct + * + * Lower GPU frequency/power if possible. Need to call i915. + */ +static void ips_gpu_lower(struct ips_driver *ips) +{ + if (!ips_gpu_turbo_enabled(ips)) + return; + + if (!ips->gpu_lower()) + ips->gpu_turbo_enabled = false; + + return; +} + +/** + * ips_enable_gpu_turbo - notify the gfx driver turbo is available + * @ips: IPS driver struct + * + * Call into the graphics driver indicating that it can safely use + * turbo mode. + */ +static void ips_enable_gpu_turbo(struct ips_driver *ips) +{ + if (ips->__gpu_turbo_on) + return; + ips->__gpu_turbo_on = true; +} + +/** + * ips_disable_gpu_turbo - notify the gfx driver to disable turbo mode + * @ips: IPS driver struct + * + * Request that the graphics driver disable turbo mode. + */ +static void ips_disable_gpu_turbo(struct ips_driver *ips) +{ + /* Avoid calling i915 if turbo is already disabled */ + if (!ips->__gpu_turbo_on) + return; + + if (!ips->gpu_turbo_disable()) + dev_err(ips->dev, "failed to disable graphics turbo\n"); + else + ips->__gpu_turbo_on = false; +} + +/** + * mcp_exceeded - check whether we're outside our thermal & power limits + * @ips: IPS driver struct + * + * Check whether the MCP is over its thermal or power budget. + * + * Returns: %true if the temp or power has exceeded its maximum, else %false + */ +static bool mcp_exceeded(struct ips_driver *ips) +{ + unsigned long flags; + bool ret = false; + u32 temp_limit; + u32 avg_power; + + spin_lock_irqsave(&ips->turbo_status_lock, flags); + + temp_limit = ips->mcp_temp_limit * 100; + if (ips->mcp_avg_temp > temp_limit) + ret = true; + + avg_power = ips->cpu_avg_power + ips->mch_avg_power; + if (avg_power > ips->mcp_power_limit) + ret = true; + + spin_unlock_irqrestore(&ips->turbo_status_lock, flags); + + return ret; +} + +/** + * cpu_exceeded - check whether a CPU core is outside its limits + * @ips: IPS driver struct + * @cpu: CPU number to check + * + * Check a given CPU's average temp or power is over its limit. + * + * Returns: %true if the temp or power has exceeded its maximum, else %false + */ +static bool cpu_exceeded(struct ips_driver *ips, int cpu) +{ + unsigned long flags; + int avg; + bool ret = false; + + spin_lock_irqsave(&ips->turbo_status_lock, flags); + avg = cpu ? ips->ctv2_avg_temp : ips->ctv1_avg_temp; + if (avg > (ips->limits->core_temp_limit * 100)) + ret = true; + if (ips->cpu_avg_power > ips->core_power_limit * 100) + ret = true; + spin_unlock_irqrestore(&ips->turbo_status_lock, flags); + + if (ret) + dev_info(ips->dev, "CPU power or thermal limit exceeded\n"); + + return ret; +} + +/** + * mch_exceeded - check whether the GPU is over budget + * @ips: IPS driver struct + * + * Check the MCH temp & power against their maximums. + * + * Returns: %true if the temp or power has exceeded its maximum, else %false + */ +static bool mch_exceeded(struct ips_driver *ips) +{ + unsigned long flags; + bool ret = false; + + spin_lock_irqsave(&ips->turbo_status_lock, flags); + if (ips->mch_avg_temp > (ips->limits->mch_temp_limit * 100)) + ret = true; + if (ips->mch_avg_power > ips->mch_power_limit) + ret = true; + spin_unlock_irqrestore(&ips->turbo_status_lock, flags); + + return ret; +} + +/** + * verify_limits - verify BIOS provided limits + * @ips: IPS structure + * + * BIOS can optionally provide non-default limits for power and temp. Check + * them here and use the defaults if the BIOS values are not provided or + * are otherwise unusable. + */ +static void verify_limits(struct ips_driver *ips) +{ + if (ips->mcp_power_limit < ips->limits->mcp_power_limit || + ips->mcp_power_limit > 35000) + ips->mcp_power_limit = ips->limits->mcp_power_limit; + + if (ips->mcp_temp_limit < ips->limits->core_temp_limit || + ips->mcp_temp_limit < ips->limits->mch_temp_limit || + ips->mcp_temp_limit > 150) + ips->mcp_temp_limit = min(ips->limits->core_temp_limit, + ips->limits->mch_temp_limit); +} + +/** + * update_turbo_limits - get various limits & settings from regs + * @ips: IPS driver struct + * + * Update the IPS power & temp limits, along with turbo enable flags, + * based on latest register contents. + * + * Used at init time and for runtime BIOS support, which requires polling + * the regs for updates (as a result of AC->DC transition for example). + * + * LOCKING: + * Caller must hold turbo_status_lock (outside of init) + */ +static void update_turbo_limits(struct ips_driver *ips) +{ + u32 hts = thm_readl(THM_HTS); + + ips->cpu_turbo_enabled = !(hts & HTS_PCTD_DIS); + /* + * Disable turbo for now, until we can figure out why the power figures + * are wrong + */ + ips->cpu_turbo_enabled = false; + + if (ips->gpu_busy) + ips->gpu_turbo_enabled = !(hts & HTS_GTD_DIS); + + ips->core_power_limit = thm_readw(THM_MPCPC); + ips->mch_power_limit = thm_readw(THM_MMGPC); + ips->mcp_temp_limit = thm_readw(THM_PTL); + ips->mcp_power_limit = thm_readw(THM_MPPC); + + verify_limits(ips); + /* Ignore BIOS CPU vs GPU pref */ +} + +/** + * ips_adjust - adjust power clamp based on thermal state + * @data: ips driver structure + * + * Wake up every 5s or so and check whether we should adjust the power clamp. + * Check CPU and GPU load to determine which needs adjustment. There are + * several things to consider here: + * - do we need to adjust up or down? + * - is CPU busy? + * - is GPU busy? + * - is CPU in turbo? + * - is GPU in turbo? + * - is CPU or GPU preferred? (CPU is default) + * + * So, given the above, we do the following: + * - up (TDP available) + * - CPU not busy, GPU not busy - nothing + * - CPU busy, GPU not busy - adjust CPU up + * - CPU not busy, GPU busy - adjust GPU up + * - CPU busy, GPU busy - adjust preferred unit up, taking headroom from + * non-preferred unit if necessary + * - down (at TDP limit) + * - adjust both CPU and GPU down if possible + * + * |cpu+ gpu+ cpu+gpu- cpu-gpu+ cpu-gpu- + * cpu < gpu < |cpu+gpu+ cpu+ gpu+ nothing + * cpu < gpu >= |cpu+gpu-(mcp<) cpu+gpu-(mcp<) gpu- gpu- + * cpu >= gpu < |cpu-gpu+(mcp<) cpu- cpu-gpu+(mcp<) cpu- + * cpu >= gpu >=|cpu-gpu- cpu-gpu- cpu-gpu- cpu-gpu- + * + * Returns: %0 + */ +static int ips_adjust(void *data) +{ + struct ips_driver *ips = data; + unsigned long flags; + + dev_dbg(ips->dev, "starting ips-adjust thread\n"); + + /* + * Adjust CPU and GPU clamps every 5s if needed. Doing it more + * often isn't recommended due to ME interaction. + */ + do { + bool cpu_busy = ips_cpu_busy(ips); + bool gpu_busy = ips_gpu_busy(ips); + + spin_lock_irqsave(&ips->turbo_status_lock, flags); + if (ips->poll_turbo_status) + update_turbo_limits(ips); + spin_unlock_irqrestore(&ips->turbo_status_lock, flags); + + /* Update turbo status if necessary */ + if (ips->cpu_turbo_enabled) + ips_enable_cpu_turbo(ips); + else + ips_disable_cpu_turbo(ips); + + if (ips->gpu_turbo_enabled) + ips_enable_gpu_turbo(ips); + else + ips_disable_gpu_turbo(ips); + + /* We're outside our comfort zone, crank them down */ + if (mcp_exceeded(ips)) { + ips_cpu_lower(ips); + ips_gpu_lower(ips); + goto sleep; + } + + if (!cpu_exceeded(ips, 0) && cpu_busy) + ips_cpu_raise(ips); + else + ips_cpu_lower(ips); + + if (!mch_exceeded(ips) && gpu_busy) + ips_gpu_raise(ips); + else + ips_gpu_lower(ips); + +sleep: + schedule_timeout_interruptible(msecs_to_jiffies(IPS_ADJUST_PERIOD)); + } while (!kthread_should_stop()); + + dev_dbg(ips->dev, "ips-adjust thread stopped\n"); + + return 0; +} + +/* + * Helpers for reading out temp/power values and calculating their + * averages for the decision making and monitoring functions. + */ + +static u16 calc_avg_temp(struct ips_driver *ips, u16 *array) +{ + u64 total = 0; + int i; + u16 avg; + + for (i = 0; i < IPS_SAMPLE_COUNT; i++) + total += (u64)(array[i] * 100); + + do_div(total, IPS_SAMPLE_COUNT); + + avg = (u16)total; + + return avg; +} + +static u16 read_mgtv(struct ips_driver *ips) +{ + u16 __maybe_unused ret; + u64 slope, offset; + u64 val; + + val = thm_readq(THM_MGTV); + val = (val & TV_MASK) >> TV_SHIFT; + + slope = offset = thm_readw(THM_MGTA); + slope = (slope & MGTA_SLOPE_MASK) >> MGTA_SLOPE_SHIFT; + offset = offset & MGTA_OFFSET_MASK; + + ret = ((val * slope + 0x40) >> 7) + offset; + + return 0; /* MCH temp reporting buggy */ +} + +static u16 read_ptv(struct ips_driver *ips) +{ + u16 val; + + val = thm_readw(THM_PTV) & PTV_MASK; + + return val; +} + +static u16 read_ctv(struct ips_driver *ips, int cpu) +{ + int reg = cpu ? THM_CTV2 : THM_CTV1; + u16 val; + + val = thm_readw(reg); + if (!(val & CTV_TEMP_ERROR)) + val = (val) >> 6; /* discard fractional component */ + else + val = 0; + + return val; +} + +static u32 get_cpu_power(struct ips_driver *ips, u32 *last, int period) +{ + u32 val; + u32 ret; + + /* + * CEC is in joules/65535. Take difference over time to + * get watts. + */ + val = thm_readl(THM_CEC); + + /* period is in ms and we want mW */ + ret = (((val - *last) * 1000) / period); + ret = (ret * 1000) / 65535; + *last = val; + + return 0; +} + +static const u16 temp_decay_factor = 2; +static u16 update_average_temp(u16 avg, u16 val) +{ + u16 ret; + + /* Multiply by 100 for extra precision */ + ret = (val * 100 / temp_decay_factor) + + (((temp_decay_factor - 1) * avg) / temp_decay_factor); + return ret; +} + +static const u16 power_decay_factor = 2; +static u16 update_average_power(u32 avg, u32 val) +{ + u32 ret; + + ret = (val / power_decay_factor) + + (((power_decay_factor - 1) * avg) / power_decay_factor); + + return ret; +} + +static u32 calc_avg_power(struct ips_driver *ips, u32 *array) +{ + u64 total = 0; + u32 avg; + int i; + + for (i = 0; i < IPS_SAMPLE_COUNT; i++) + total += array[i]; + + do_div(total, IPS_SAMPLE_COUNT); + avg = (u32)total; + + return avg; +} + +static void monitor_timeout(struct timer_list *t) +{ + struct ips_driver *ips = timer_container_of(ips, t, timer); + wake_up_process(ips->monitor); +} + +/** + * ips_monitor - temp/power monitoring thread + * @data: ips driver structure + * + * This is the main function for the IPS driver. It monitors power and + * temperature in the MCP and adjusts CPU and GPU power clamps accordingly. + * + * We keep a 5s moving average of power consumption and temperature. Using + * that data, along with CPU vs GPU preference, we adjust the power clamps + * up or down. + * + * Returns: %0 on success or -errno on error + */ +static int ips_monitor(void *data) +{ + struct ips_driver *ips = data; + unsigned long seqno_timestamp, expire, last_msecs, last_sample_period; + int i; + u32 *cpu_samples, *mchp_samples, old_cpu_power; + u16 *mcp_samples, *ctv1_samples, *ctv2_samples, *mch_samples; + u8 cur_seqno, last_seqno; + + mcp_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u16), GFP_KERNEL); + ctv1_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u16), GFP_KERNEL); + ctv2_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u16), GFP_KERNEL); + mch_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u16), GFP_KERNEL); + cpu_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u32), GFP_KERNEL); + mchp_samples = kcalloc(IPS_SAMPLE_COUNT, sizeof(u32), GFP_KERNEL); + if (!mcp_samples || !ctv1_samples || !ctv2_samples || !mch_samples || + !cpu_samples || !mchp_samples) { + dev_err(ips->dev, + "failed to allocate sample array, ips disabled\n"); + kfree(mcp_samples); + kfree(ctv1_samples); + kfree(ctv2_samples); + kfree(mch_samples); + kfree(cpu_samples); + kfree(mchp_samples); + return -ENOMEM; + } + + last_seqno = (thm_readl(THM_ITV) & ITV_ME_SEQNO_MASK) >> + ITV_ME_SEQNO_SHIFT; + seqno_timestamp = get_jiffies_64(); + + old_cpu_power = thm_readl(THM_CEC); + schedule_timeout_interruptible(msecs_to_jiffies(IPS_SAMPLE_PERIOD)); + + /* Collect an initial average */ + for (i = 0; i < IPS_SAMPLE_COUNT; i++) { + u32 mchp, cpu_power; + u16 val; + + mcp_samples[i] = read_ptv(ips); + + val = read_ctv(ips, 0); + ctv1_samples[i] = val; + + val = read_ctv(ips, 1); + ctv2_samples[i] = val; + + val = read_mgtv(ips); + mch_samples[i] = val; + + cpu_power = get_cpu_power(ips, &old_cpu_power, + IPS_SAMPLE_PERIOD); + cpu_samples[i] = cpu_power; + + if (ips->read_mch_val) { + mchp = ips->read_mch_val(); + mchp_samples[i] = mchp; + } + + schedule_timeout_interruptible(msecs_to_jiffies(IPS_SAMPLE_PERIOD)); + if (kthread_should_stop()) + break; + } + + ips->mcp_avg_temp = calc_avg_temp(ips, mcp_samples); + ips->ctv1_avg_temp = calc_avg_temp(ips, ctv1_samples); + ips->ctv2_avg_temp = calc_avg_temp(ips, ctv2_samples); + ips->mch_avg_temp = calc_avg_temp(ips, mch_samples); + ips->cpu_avg_power = calc_avg_power(ips, cpu_samples); + ips->mch_avg_power = calc_avg_power(ips, mchp_samples); + kfree(mcp_samples); + kfree(ctv1_samples); + kfree(ctv2_samples); + kfree(mch_samples); + kfree(cpu_samples); + kfree(mchp_samples); + + /* Start the adjustment thread now that we have data */ + wake_up_process(ips->adjust); + + /* + * Ok, now we have an initial avg. From here on out, we track the + * running avg using a decaying average calculation. This allows + * us to reduce the sample frequency if the CPU and GPU are idle. + */ + old_cpu_power = thm_readl(THM_CEC); + schedule_timeout_interruptible(msecs_to_jiffies(IPS_SAMPLE_PERIOD)); + last_sample_period = IPS_SAMPLE_PERIOD; + + timer_setup(&ips->timer, monitor_timeout, TIMER_DEFERRABLE); + do { + u32 cpu_val, mch_val; + u16 val; + + /* MCP itself */ + val = read_ptv(ips); + ips->mcp_avg_temp = update_average_temp(ips->mcp_avg_temp, val); + + /* Processor 0 */ + val = read_ctv(ips, 0); + ips->ctv1_avg_temp = + update_average_temp(ips->ctv1_avg_temp, val); + /* Power */ + cpu_val = get_cpu_power(ips, &old_cpu_power, + last_sample_period); + ips->cpu_avg_power = + update_average_power(ips->cpu_avg_power, cpu_val); + + if (ips->second_cpu) { + /* Processor 1 */ + val = read_ctv(ips, 1); + ips->ctv2_avg_temp = + update_average_temp(ips->ctv2_avg_temp, val); + } + + /* MCH */ + val = read_mgtv(ips); + ips->mch_avg_temp = update_average_temp(ips->mch_avg_temp, val); + /* Power */ + if (ips->read_mch_val) { + mch_val = ips->read_mch_val(); + ips->mch_avg_power = + update_average_power(ips->mch_avg_power, + mch_val); + } + + /* + * Make sure ME is updating thermal regs. + * Note: + * If it's been more than a second since the last update, + * the ME is probably hung. + */ + cur_seqno = (thm_readl(THM_ITV) & ITV_ME_SEQNO_MASK) >> + ITV_ME_SEQNO_SHIFT; + if (cur_seqno == last_seqno && + time_after(jiffies, seqno_timestamp + HZ)) { + dev_warn(ips->dev, + "ME failed to update for more than 1s, likely hung\n"); + } else { + seqno_timestamp = get_jiffies_64(); + last_seqno = cur_seqno; + } + + last_msecs = jiffies_to_msecs(jiffies); + expire = jiffies + msecs_to_jiffies(IPS_SAMPLE_PERIOD); + + __set_current_state(TASK_INTERRUPTIBLE); + mod_timer(&ips->timer, expire); + schedule(); + + /* Calculate actual sample period for power averaging */ + last_sample_period = jiffies_to_msecs(jiffies) - last_msecs; + if (!last_sample_period) + last_sample_period = 1; + } while (!kthread_should_stop()); + + timer_delete_sync(&ips->timer); + + dev_dbg(ips->dev, "ips-monitor thread stopped\n"); + + return 0; +} + +/** + * ips_irq_handler - handle temperature triggers and other IPS events + * @irq: irq number + * @arg: unused + * + * Handle temperature limit trigger events, generally by lowering the clamps. + * If we're at a critical limit, we clamp back to the lowest possible value + * to prevent emergency shutdown. + * + * Returns: IRQ_NONE or IRQ_HANDLED + */ +static irqreturn_t ips_irq_handler(int irq, void *arg) +{ + struct ips_driver *ips = arg; + u8 tses = thm_readb(THM_TSES); + u8 tes = thm_readb(THM_TES); + + if (!tses && !tes) + return IRQ_NONE; + + dev_info(ips->dev, "TSES: 0x%02x\n", tses); + dev_info(ips->dev, "TES: 0x%02x\n", tes); + + /* STS update from EC? */ + if (tes & 1) { + u32 sts, tc1; + + sts = thm_readl(THM_STS); + tc1 = thm_readl(THM_TC1); + + if (sts & STS_NVV) { + spin_lock(&ips->turbo_status_lock); + ips->core_power_limit = (sts & STS_PCPL_MASK) >> + STS_PCPL_SHIFT; + ips->mch_power_limit = (sts & STS_GPL_MASK) >> + STS_GPL_SHIFT; + /* ignore EC CPU vs GPU pref */ + ips->cpu_turbo_enabled = !(sts & STS_PCTD_DIS); + /* + * Disable turbo for now, until we can figure + * out why the power figures are wrong + */ + ips->cpu_turbo_enabled = false; + if (ips->gpu_busy) + ips->gpu_turbo_enabled = !(sts & STS_GTD_DIS); + ips->mcp_temp_limit = (sts & STS_PTL_MASK) >> + STS_PTL_SHIFT; + ips->mcp_power_limit = (tc1 & STS_PPL_MASK) >> + STS_PPL_SHIFT; + verify_limits(ips); + spin_unlock(&ips->turbo_status_lock); + + thm_writeb(THM_SEC, SEC_ACK); + } + thm_writeb(THM_TES, tes); + } + + /* Thermal trip */ + if (tses) { + dev_warn(ips->dev, "thermal trip occurred, tses: 0x%04x\n", + tses); + thm_writeb(THM_TSES, tses); + } + + return IRQ_HANDLED; +} + +#ifndef CONFIG_DEBUG_FS +static void ips_debugfs_init(struct ips_driver *ips) { return; } +static void ips_debugfs_cleanup(struct ips_driver *ips) { return; } +#else + +/* Expose current state and limits in debugfs if possible */ + +static int cpu_temp_show(struct seq_file *m, void *data) +{ + struct ips_driver *ips = m->private; + + seq_printf(m, "%d.%02d\n", ips->ctv1_avg_temp / 100, + ips->ctv1_avg_temp % 100); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(cpu_temp); + +static int cpu_power_show(struct seq_file *m, void *data) +{ + struct ips_driver *ips = m->private; + + seq_printf(m, "%dmW\n", ips->cpu_avg_power); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(cpu_power); + +static int cpu_clamp_show(struct seq_file *m, void *data) +{ + u64 turbo_override; + int tdp, tdc; + + rdmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + + tdp = (int)(turbo_override & TURBO_TDP_MASK); + tdc = (int)((turbo_override & TURBO_TDC_MASK) >> TURBO_TDC_SHIFT); + + /* Convert to .1W/A units */ + tdp = tdp * 10 / 8; + tdc = tdc * 10 / 8; + + /* Watts Amperes */ + seq_printf(m, "%d.%dW %d.%dA\n", tdp / 10, tdp % 10, + tdc / 10, tdc % 10); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(cpu_clamp); + +static int mch_temp_show(struct seq_file *m, void *data) +{ + struct ips_driver *ips = m->private; + + seq_printf(m, "%d.%02d\n", ips->mch_avg_temp / 100, + ips->mch_avg_temp % 100); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(mch_temp); + +static int mch_power_show(struct seq_file *m, void *data) +{ + struct ips_driver *ips = m->private; + + seq_printf(m, "%dmW\n", ips->mch_avg_power); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(mch_power); + +static void ips_debugfs_cleanup(struct ips_driver *ips) +{ + debugfs_remove_recursive(ips->debug_root); +} + +static void ips_debugfs_init(struct ips_driver *ips) +{ + ips->debug_root = debugfs_create_dir("ips", NULL); + + debugfs_create_file("cpu_temp", 0444, ips->debug_root, ips, &cpu_temp_fops); + debugfs_create_file("cpu_power", 0444, ips->debug_root, ips, &cpu_power_fops); + debugfs_create_file("cpu_clamp", 0444, ips->debug_root, ips, &cpu_clamp_fops); + debugfs_create_file("mch_temp", 0444, ips->debug_root, ips, &mch_temp_fops); + debugfs_create_file("mch_power", 0444, ips->debug_root, ips, &mch_power_fops); +} +#endif /* CONFIG_DEBUG_FS */ + +/** + * ips_detect_cpu - detect whether CPU supports IPS + * @ips: IPS driver struct + * + * Walk our list and see if we're on a supported CPU. If we find one, + * return the limits for it. + * + * Returns: the &ips_mcp_limits struct that matches the boot CPU or %NULL + */ +static struct ips_mcp_limits *ips_detect_cpu(struct ips_driver *ips) +{ + u64 turbo_power, misc_en; + struct ips_mcp_limits *limits = NULL; + u16 tdp; + + if (!(boot_cpu_data.x86_vfm == INTEL_WESTMERE)) { + dev_info(ips->dev, "Non-IPS CPU detected.\n"); + return NULL; + } + + rdmsrq(IA32_MISC_ENABLE, misc_en); + /* + * If the turbo enable bit isn't set, we shouldn't try to enable/disable + * turbo manually or we'll get an illegal MSR access, even though + * turbo will still be available. + */ + if (misc_en & IA32_MISC_TURBO_EN) + ips->turbo_toggle_allowed = true; + else + ips->turbo_toggle_allowed = false; + + if (strstr(boot_cpu_data.x86_model_id, "CPU M")) + limits = &ips_sv_limits; + else if (strstr(boot_cpu_data.x86_model_id, "CPU L")) + limits = &ips_lv_limits; + else if (strstr(boot_cpu_data.x86_model_id, "CPU U")) + limits = &ips_ulv_limits; + else { + dev_info(ips->dev, "No CPUID match found.\n"); + return NULL; + } + + rdmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_power); + tdp = turbo_power & TURBO_TDP_MASK; + + /* Sanity check TDP against CPU */ + if (limits->core_power_limit != (tdp / 8) * 1000) { + dev_info(ips->dev, + "CPU TDP doesn't match expected value (found %d, expected %d)\n", + tdp / 8, limits->core_power_limit / 1000); + limits->core_power_limit = (tdp / 8) * 1000; + } + + return limits; +} + +/** + * ips_get_i915_syms - try to get GPU control methods from i915 driver + * @ips: IPS driver + * + * The i915 driver exports several interfaces to allow the IPS driver to + * monitor and control graphics turbo mode. If we can find them, we can + * enable graphics turbo, otherwise we must disable it to avoid exceeding + * thermal and power limits in the MCP. + * + * Returns: %true if the required symbols are found, else %false + */ +static bool ips_get_i915_syms(struct ips_driver *ips) +{ + ips->read_mch_val = symbol_get(i915_read_mch_val); + if (!ips->read_mch_val) + goto out_err; + ips->gpu_raise = symbol_get(i915_gpu_raise); + if (!ips->gpu_raise) + goto out_put_mch; + ips->gpu_lower = symbol_get(i915_gpu_lower); + if (!ips->gpu_lower) + goto out_put_raise; + ips->gpu_busy = symbol_get(i915_gpu_busy); + if (!ips->gpu_busy) + goto out_put_lower; + ips->gpu_turbo_disable = symbol_get(i915_gpu_turbo_disable); + if (!ips->gpu_turbo_disable) + goto out_put_busy; + + return true; + +out_put_busy: + symbol_put(i915_gpu_busy); +out_put_lower: + symbol_put(i915_gpu_lower); +out_put_raise: + symbol_put(i915_gpu_raise); +out_put_mch: + symbol_put(i915_read_mch_val); +out_err: + return false; +} + +static bool +ips_gpu_turbo_enabled(struct ips_driver *ips) +{ + if (!ips->gpu_busy && late_i915_load) { + if (ips_get_i915_syms(ips)) { + dev_info(ips->dev, + "i915 driver attached, reenabling gpu turbo\n"); + ips->gpu_turbo_enabled = !(thm_readl(THM_HTS) & HTS_GTD_DIS); + } + } + + return ips->gpu_turbo_enabled; +} + +void +ips_link_to_i915_driver(void) +{ + /* We can't cleanly get at the various ips_driver structs from + * this caller (the i915 driver), so just set a flag saying + * that it's time to try getting the symbols again. + */ + late_i915_load = true; +} +EXPORT_SYMBOL_GPL(ips_link_to_i915_driver); + +static const struct pci_device_id ips_id_table[] = { + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_THERMAL_SENSOR), }, + { 0, } +}; + +MODULE_DEVICE_TABLE(pci, ips_id_table); + +static int ips_blacklist_callback(const struct dmi_system_id *id) +{ + pr_info("Blacklisted intel_ips for %s\n", id->ident); + return 1; +} + +static const struct dmi_system_id ips_blacklist[] = { + { + .callback = ips_blacklist_callback, + .ident = "HP ProBook", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Hewlett-Packard"), + DMI_MATCH(DMI_PRODUCT_NAME, "HP ProBook"), + }, + }, + { } /* terminating entry */ +}; + +static int ips_probe(struct pci_dev *dev, const struct pci_device_id *id) +{ + u64 platform_info; + struct ips_driver *ips; + u32 hts; + int ret = 0; + u16 htshi, trc, trc_required_mask; + u8 tse; + + if (dmi_check_system(ips_blacklist)) + return -ENODEV; + + ips = devm_kzalloc(&dev->dev, sizeof(*ips), GFP_KERNEL); + if (!ips) + return -ENOMEM; + + spin_lock_init(&ips->turbo_status_lock); + ips->dev = &dev->dev; + + ips->limits = ips_detect_cpu(ips); + if (!ips->limits) { + dev_info(&dev->dev, "IPS not supported on this CPU\n"); + return -ENXIO; + } + + ret = pcim_enable_device(dev); + if (ret) { + dev_err(&dev->dev, "can't enable PCI device, aborting\n"); + return ret; + } + + ret = pcim_iomap_regions(dev, 1 << 0, pci_name(dev)); + if (ret) { + dev_err(&dev->dev, "failed to map thermal regs, aborting\n"); + return ret; + } + ips->regmap = pcim_iomap_table(dev)[0]; + + pci_set_drvdata(dev, ips); + + tse = thm_readb(THM_TSE); + if (tse != TSE_EN) { + dev_err(&dev->dev, "thermal device not enabled (0x%02x), aborting\n", tse); + return -ENXIO; + } + + trc = thm_readw(THM_TRC); + trc_required_mask = TRC_CORE1_EN | TRC_CORE_PWR | TRC_MCH_EN; + if ((trc & trc_required_mask) != trc_required_mask) { + dev_err(&dev->dev, "thermal reporting for required devices not enabled, aborting\n"); + return -ENXIO; + } + + if (trc & TRC_CORE2_EN) + ips->second_cpu = true; + + update_turbo_limits(ips); + dev_dbg(&dev->dev, "max cpu power clamp: %dW\n", + ips->mcp_power_limit / 10); + dev_dbg(&dev->dev, "max core power clamp: %dW\n", + ips->core_power_limit / 10); + /* BIOS may update limits at runtime */ + if (thm_readl(THM_PSC) & PSP_PBRT) + ips->poll_turbo_status = true; + + if (!ips_get_i915_syms(ips)) { + dev_info(&dev->dev, "failed to get i915 symbols, graphics turbo disabled until i915 loads\n"); + ips->gpu_turbo_enabled = false; + } else { + dev_dbg(&dev->dev, "graphics turbo enabled\n"); + ips->gpu_turbo_enabled = true; + } + + /* + * Check PLATFORM_INFO MSR to make sure this chip is + * turbo capable. + */ + rdmsrq(PLATFORM_INFO, platform_info); + if (!(platform_info & PLATFORM_TDP)) { + dev_err(&dev->dev, "platform indicates TDP override unavailable, aborting\n"); + return -ENODEV; + } + + /* + * IRQ handler for ME interaction + * Note: don't use MSI here as the PCH has bugs. + */ + ret = pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_INTX); + if (ret < 0) + return ret; + + ips->irq = pci_irq_vector(dev, 0); + + ret = request_irq(ips->irq, ips_irq_handler, IRQF_SHARED, "ips", ips); + if (ret) { + dev_err(&dev->dev, "request irq failed, aborting\n"); + return ret; + } + + /* Enable aux, hot & critical interrupts */ + thm_writeb(THM_TSPIEN, TSPIEN_AUX2_LOHI | TSPIEN_CRIT_LOHI | + TSPIEN_HOT_LOHI | TSPIEN_AUX_LOHI); + thm_writeb(THM_TEN, TEN_UPDATE_EN); + + /* Collect adjustment values */ + ips->cta_val = thm_readw(THM_CTA); + ips->pta_val = thm_readw(THM_PTA); + ips->mgta_val = thm_readw(THM_MGTA); + + /* Save turbo limits & ratios */ + rdmsrq(TURBO_POWER_CURRENT_LIMIT, ips->orig_turbo_limit); + + ips_disable_cpu_turbo(ips); + ips->cpu_turbo_enabled = false; + + /* Create thermal adjust thread */ + ips->adjust = kthread_create(ips_adjust, ips, "ips-adjust"); + if (IS_ERR(ips->adjust)) { + dev_err(&dev->dev, + "failed to create thermal adjust thread, aborting\n"); + ret = -ENOMEM; + goto error_free_irq; + + } + + /* + * Set up the work queue and monitor thread. The monitor thread + * will wake up ips_adjust thread. + */ + ips->monitor = kthread_run(ips_monitor, ips, "ips-monitor"); + if (IS_ERR(ips->monitor)) { + dev_err(&dev->dev, + "failed to create thermal monitor thread, aborting\n"); + ret = -ENOMEM; + goto error_thread_cleanup; + } + + hts = (ips->core_power_limit << HTS_PCPL_SHIFT) | + (ips->mcp_temp_limit << HTS_PTL_SHIFT) | HTS_NVV; + htshi = HTS2_PRST_RUNNING << HTS2_PRST_SHIFT; + + thm_writew(THM_HTSHI, htshi); + thm_writel(THM_HTS, hts); + + ips_debugfs_init(ips); + + dev_info(&dev->dev, "IPS driver initialized, MCP temp limit %d\n", + ips->mcp_temp_limit); + return ret; + +error_thread_cleanup: + kthread_stop(ips->adjust); +error_free_irq: + free_irq(ips->irq, ips); + pci_free_irq_vectors(dev); + return ret; +} + +static void ips_remove(struct pci_dev *dev) +{ + struct ips_driver *ips = pci_get_drvdata(dev); + u64 turbo_override; + + ips_debugfs_cleanup(ips); + + /* Release i915 driver */ + if (ips->read_mch_val) + symbol_put(i915_read_mch_val); + if (ips->gpu_raise) + symbol_put(i915_gpu_raise); + if (ips->gpu_lower) + symbol_put(i915_gpu_lower); + if (ips->gpu_busy) + symbol_put(i915_gpu_busy); + if (ips->gpu_turbo_disable) + symbol_put(i915_gpu_turbo_disable); + + rdmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + turbo_override &= ~(TURBO_TDC_OVR_EN | TURBO_TDP_OVR_EN); + wrmsrq(TURBO_POWER_CURRENT_LIMIT, turbo_override); + wrmsrq(TURBO_POWER_CURRENT_LIMIT, ips->orig_turbo_limit); + + free_irq(ips->irq, ips); + pci_free_irq_vectors(dev); + if (ips->adjust) + kthread_stop(ips->adjust); + if (ips->monitor) + kthread_stop(ips->monitor); + dev_dbg(&dev->dev, "IPS driver removed\n"); +} + +static struct pci_driver ips_pci_driver = { + .name = "intel ips", + .id_table = ips_id_table, + .probe = ips_probe, + .remove = ips_remove, +}; + +module_pci_driver(ips_pci_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Jesse Barnes "); +MODULE_DESCRIPTION("Intelligent Power Sharing Driver"); diff --git a/drivers/platform/x86/intel_ips.h b/drivers/platform/x86/intel_ips.h new file mode 100644 index 000000000..35ed9711c --- /dev/null +++ b/drivers/platform/x86/intel_ips.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2010 Intel Corporation + */ + +void ips_link_to_i915_driver(void); diff --git a/drivers/platform/x86/intel_scu_ipc.c b/drivers/platform/x86/intel_scu_ipc.c new file mode 100644 index 000000000..a2472ad10 --- /dev/null +++ b/drivers/platform/x86/intel_scu_ipc.c @@ -0,0 +1,705 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for the Intel SCU IPC mechanism + * + * (C) Copyright 2008-2010,2015 Intel Corporation + * Author: Sreedhara DS (sreedhara.ds@intel.com) + * + * SCU running in ARC processor communicates with other entity running in IA + * core through IPC mechanism which in turn messaging between IA core ad SCU. + * SCU has two IPC mechanism IPC-1 and IPC-2. IPC-1 is used between IA32 and + * SCU where IPC-2 is used between P-Unit and SCU. This driver delas with + * IPC-1 Driver provides an API for power control unit registers (e.g. MSIC) + * along with other APIs. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* IPC defines the following message types */ +#define IPCMSG_PCNTRL 0xff /* Power controller unit read/write */ + +/* Command id associated with message IPCMSG_PCNTRL */ +#define IPC_CMD_PCNTRL_W 0 /* Register write */ +#define IPC_CMD_PCNTRL_R 1 /* Register read */ +#define IPC_CMD_PCNTRL_M 2 /* Register read-modify-write */ + +/* + * IPC register summary + * + * IPC register blocks are memory mapped at fixed address of PCI BAR 0. + * To read or write information to the SCU, driver writes to IPC-1 memory + * mapped registers. The following is the IPC mechanism + * + * 1. IA core cDMI interface claims this transaction and converts it to a + * Transaction Layer Packet (TLP) message which is sent across the cDMI. + * + * 2. South Complex cDMI block receives this message and writes it to + * the IPC-1 register block, causing an interrupt to the SCU + * + * 3. SCU firmware decodes this interrupt and IPC message and the appropriate + * message handler is called within firmware. + */ + +#define IPC_WWBUF_SIZE 20 /* IPC Write buffer Size */ +#define IPC_RWBUF_SIZE 20 /* IPC Read buffer Size */ +#define IPC_IOC 0x100 /* IPC command register IOC bit */ + +struct intel_scu_ipc_dev { + struct device dev; + struct module *owner; + void __iomem *ipc_base; + struct completion cmd_complete; + + struct intel_scu_ipc_data data; +}; + +#define IPC_STATUS 0x04 +#define IPC_STATUS_IRQ BIT(2) +#define IPC_STATUS_ERR BIT(1) +#define IPC_STATUS_BUSY BIT(0) + +/* + * IPC Write/Read Buffers: + * 16 byte buffer for sending and receiving data to and from SCU. + */ +#define IPC_WRITE_BUFFER 0x80 +#define IPC_READ_BUFFER 0x90 + +/* Timeout in jiffies */ +#define IPC_TIMEOUT (10 * HZ) + +static struct intel_scu_ipc_dev *ipcdev; /* Only one for now */ +static DEFINE_MUTEX(ipclock); /* lock used to prevent multiple call to SCU */ + +static struct class intel_scu_ipc_class = { + .name = "intel_scu_ipc", +}; + +/** + * intel_scu_ipc_dev_get() - Get SCU IPC instance + * + * The recommended new API takes SCU IPC instance as parameter and this + * function can be called by driver to get the instance. This also makes + * sure the driver providing the IPC functionality cannot be unloaded + * while the caller has the instance. + * + * Call intel_scu_ipc_dev_put() to release the instance. + * + * Returns %NULL if SCU IPC is not currently available. + */ +struct intel_scu_ipc_dev *intel_scu_ipc_dev_get(void) +{ + guard(mutex)(&ipclock); + + if (ipcdev) { + get_device(&ipcdev->dev); + /* + * Prevent the IPC provider from being unloaded while it + * is being used. + */ + if (try_module_get(ipcdev->owner)) + return ipcdev; + + put_device(&ipcdev->dev); + } + + return NULL; +} +EXPORT_SYMBOL_GPL(intel_scu_ipc_dev_get); + +/** + * intel_scu_ipc_dev_put() - Put SCU IPC instance + * @scu: SCU IPC instance + * + * This function releases the SCU IPC instance retrieved from + * intel_scu_ipc_dev_get() and allows the driver providing IPC to be + * unloaded. + */ +void intel_scu_ipc_dev_put(struct intel_scu_ipc_dev *scu) +{ + if (scu) { + module_put(scu->owner); + put_device(&scu->dev); + } +} +EXPORT_SYMBOL_GPL(intel_scu_ipc_dev_put); + +struct intel_scu_ipc_devres { + struct intel_scu_ipc_dev *scu; +}; + +static void devm_intel_scu_ipc_dev_release(struct device *dev, void *res) +{ + struct intel_scu_ipc_devres *dr = res; + struct intel_scu_ipc_dev *scu = dr->scu; + + intel_scu_ipc_dev_put(scu); +} + +/** + * devm_intel_scu_ipc_dev_get() - Allocate managed SCU IPC device + * @dev: Device requesting the SCU IPC device + * + * The recommended new API takes SCU IPC instance as parameter and this + * function can be called by driver to get the instance. This also makes + * sure the driver providing the IPC functionality cannot be unloaded + * while the caller has the instance. + * + * Returns %NULL if SCU IPC is not currently available. + */ +struct intel_scu_ipc_dev *devm_intel_scu_ipc_dev_get(struct device *dev) +{ + struct intel_scu_ipc_devres *dr; + struct intel_scu_ipc_dev *scu; + + dr = devres_alloc(devm_intel_scu_ipc_dev_release, sizeof(*dr), GFP_KERNEL); + if (!dr) + return NULL; + + scu = intel_scu_ipc_dev_get(); + if (!scu) { + devres_free(dr); + return NULL; + } + + dr->scu = scu; + devres_add(dev, dr); + + return scu; +} +EXPORT_SYMBOL_GPL(devm_intel_scu_ipc_dev_get); + +/* + * Send ipc command + * Command Register (Write Only): + * A write to this register results in an interrupt to the SCU core processor + * Format: + * |rfu2(8) | size(8) | command id(4) | rfu1(3) | ioc(1) | command(8)| + */ +static inline void ipc_command(struct intel_scu_ipc_dev *scu, u32 cmd) +{ + reinit_completion(&scu->cmd_complete); + writel(cmd | IPC_IOC, scu->ipc_base); +} + +/* + * Write ipc data + * IPC Write Buffer (Write Only): + * 16-byte buffer for sending data associated with IPC command to + * SCU. Size of the data is specified in the IPC_COMMAND_REG register + */ +static inline void ipc_data_writel(struct intel_scu_ipc_dev *scu, u32 data, u32 offset) +{ + writel(data, scu->ipc_base + IPC_WRITE_BUFFER + offset); +} + +/* + * Status Register (Read Only): + * Driver will read this register to get the ready/busy status of the IPC + * block and error status of the IPC command that was just processed by SCU + * Format: + * |rfu3(8)|error code(8)|initiator id(8)|cmd id(4)|rfu1(2)|error(1)|busy(1)| + */ +static inline u8 ipc_read_status(struct intel_scu_ipc_dev *scu) +{ + return __raw_readl(scu->ipc_base + IPC_STATUS); +} + +/* Read ipc u32 data */ +static inline u32 ipc_data_readl(struct intel_scu_ipc_dev *scu, u32 offset) +{ + return readl(scu->ipc_base + IPC_READ_BUFFER + offset); +} + +/* Wait till scu status is busy */ +static inline int busy_loop(struct intel_scu_ipc_dev *scu) +{ + u8 status; + int err; + + err = readx_poll_timeout(ipc_read_status, scu, status, !(status & IPC_STATUS_BUSY), + 100, jiffies_to_usecs(IPC_TIMEOUT)); + if (err) + return err; + + return (status & IPC_STATUS_ERR) ? -EIO : 0; +} + +/* Wait till ipc ioc interrupt is received or timeout in 10 HZ */ +static inline int ipc_wait_for_interrupt(struct intel_scu_ipc_dev *scu) +{ + int status; + + wait_for_completion_timeout(&scu->cmd_complete, IPC_TIMEOUT); + + status = ipc_read_status(scu); + if (status & IPC_STATUS_BUSY) + return -ETIMEDOUT; + + if (status & IPC_STATUS_ERR) + return -EIO; + + return 0; +} + +static int intel_scu_ipc_check_status(struct intel_scu_ipc_dev *scu) +{ + return scu->data.irq > 0 ? ipc_wait_for_interrupt(scu) : busy_loop(scu); +} + +static struct intel_scu_ipc_dev *intel_scu_ipc_get(struct intel_scu_ipc_dev *scu) +{ + u8 status; + + if (!scu) + scu = ipcdev; + if (!scu) + return ERR_PTR(-ENODEV); + + status = ipc_read_status(scu); + if (status & IPC_STATUS_BUSY) { + dev_dbg(&scu->dev, "device is busy\n"); + return ERR_PTR(-EBUSY); + } + + return scu; +} + +/* Read/Write power control(PMIC in Langwell, MSIC in PenWell) registers */ +static int pwr_reg_rdwr(struct intel_scu_ipc_dev *scu, u16 *addr, u8 *data, + u32 count, u32 op, u32 id) +{ + int nc; + u32 offset = 0; + int err; + u8 cbuf[IPC_WWBUF_SIZE]; + u32 *wbuf = (u32 *)&cbuf; + + memset(cbuf, 0, sizeof(cbuf)); + + guard(mutex)(&ipclock); + + scu = intel_scu_ipc_get(scu); + if (IS_ERR(scu)) + return PTR_ERR(scu); + + for (nc = 0; nc < count; nc++, offset += 2) { + cbuf[offset] = addr[nc]; + cbuf[offset + 1] = addr[nc] >> 8; + } + + if (id == IPC_CMD_PCNTRL_R) { + for (nc = 0, offset = 0; nc < count; nc++, offset += 4) + ipc_data_writel(scu, wbuf[nc], offset); + ipc_command(scu, (count * 2) << 16 | id << 12 | 0 << 8 | op); + } else if (id == IPC_CMD_PCNTRL_W) { + for (nc = 0; nc < count; nc++, offset += 1) + cbuf[offset] = data[nc]; + for (nc = 0, offset = 0; nc < count; nc++, offset += 4) + ipc_data_writel(scu, wbuf[nc], offset); + ipc_command(scu, (count * 3) << 16 | id << 12 | 0 << 8 | op); + } else if (id == IPC_CMD_PCNTRL_M) { + cbuf[offset] = data[0]; + cbuf[offset + 1] = data[1]; + ipc_data_writel(scu, wbuf[0], 0); /* Write wbuff */ + ipc_command(scu, 4 << 16 | id << 12 | 0 << 8 | op); + } + + err = intel_scu_ipc_check_status(scu); + if (err) + return err; + + /* Read rbuf */ + for (nc = 0, offset = 0; nc < 4; nc++, offset += 4) + wbuf[nc] = ipc_data_readl(scu, offset); + memcpy(data, wbuf, count); + + return 0; +} + +/** + * intel_scu_ipc_dev_ioread8() - Read a byte via the SCU + * @scu: Optional SCU IPC instance + * @addr: Register on SCU + * @data: Return pointer for read byte + * + * Read a single register. Returns %0 on success or an error code. All + * locking between SCU accesses is handled for the caller. + * + * This function may sleep. + */ +int intel_scu_ipc_dev_ioread8(struct intel_scu_ipc_dev *scu, u16 addr, u8 *data) +{ + return pwr_reg_rdwr(scu, &addr, data, 1, IPCMSG_PCNTRL, IPC_CMD_PCNTRL_R); +} +EXPORT_SYMBOL(intel_scu_ipc_dev_ioread8); + +/** + * intel_scu_ipc_dev_iowrite8() - Write a byte via the SCU + * @scu: Optional SCU IPC instance + * @addr: Register on SCU + * @data: Byte to write + * + * Write a single register. Returns %0 on success or an error code. All + * locking between SCU accesses is handled for the caller. + * + * This function may sleep. + */ +int intel_scu_ipc_dev_iowrite8(struct intel_scu_ipc_dev *scu, u16 addr, u8 data) +{ + return pwr_reg_rdwr(scu, &addr, &data, 1, IPCMSG_PCNTRL, IPC_CMD_PCNTRL_W); +} +EXPORT_SYMBOL(intel_scu_ipc_dev_iowrite8); + +/** + * intel_scu_ipc_dev_readv() - Read a set of registers + * @scu: Optional SCU IPC instance + * @addr: Register list + * @data: Bytes to return + * @len: Length of array + * + * Read registers. Returns %0 on success or an error code. All locking + * between SCU accesses is handled for the caller. + * + * The largest array length permitted by the hardware is 5 items. + * + * This function may sleep. + */ +int intel_scu_ipc_dev_readv(struct intel_scu_ipc_dev *scu, u16 *addr, u8 *data, + size_t len) +{ + return pwr_reg_rdwr(scu, addr, data, len, IPCMSG_PCNTRL, IPC_CMD_PCNTRL_R); +} +EXPORT_SYMBOL(intel_scu_ipc_dev_readv); + +/** + * intel_scu_ipc_dev_writev() - Write a set of registers + * @scu: Optional SCU IPC instance + * @addr: Register list + * @data: Bytes to write + * @len: Length of array + * + * Write registers. Returns %0 on success or an error code. All locking + * between SCU accesses is handled for the caller. + * + * The largest array length permitted by the hardware is 5 items. + * + * This function may sleep. + */ +int intel_scu_ipc_dev_writev(struct intel_scu_ipc_dev *scu, u16 *addr, u8 *data, + size_t len) +{ + return pwr_reg_rdwr(scu, addr, data, len, IPCMSG_PCNTRL, IPC_CMD_PCNTRL_W); +} +EXPORT_SYMBOL(intel_scu_ipc_dev_writev); + +/** + * intel_scu_ipc_dev_update() - Update a register + * @scu: Optional SCU IPC instance + * @addr: Register address + * @data: Bits to update + * @mask: Mask of bits to update + * + * Read-modify-write power control unit register. The first data argument + * must be register value and second is mask value mask is a bitmap that + * indicates which bits to update. %0 = masked. Don't modify this bit, %1 = + * modify this bit. returns %0 on success or an error code. + * + * This function may sleep. Locking between SCU accesses is handled + * for the caller. + */ +int intel_scu_ipc_dev_update(struct intel_scu_ipc_dev *scu, u16 addr, u8 data, + u8 mask) +{ + u8 tmp[2] = { data, mask }; + return pwr_reg_rdwr(scu, &addr, tmp, 1, IPCMSG_PCNTRL, IPC_CMD_PCNTRL_M); +} +EXPORT_SYMBOL(intel_scu_ipc_dev_update); + +/** + * intel_scu_ipc_dev_simple_command() - Send a simple command + * @scu: Optional SCU IPC instance + * @cmd: Command + * @sub: Sub type + * + * Issue a simple command to the SCU. Do not use this interface if you must + * then access data as any data values may be overwritten by another SCU + * access by the time this function returns. + * + * This function may sleep. Locking for SCU accesses is handled for the + * caller. + */ +int intel_scu_ipc_dev_simple_command(struct intel_scu_ipc_dev *scu, int cmd, + int sub) +{ + u32 cmdval; + int err; + + guard(mutex)(&ipclock); + + scu = intel_scu_ipc_get(scu); + if (IS_ERR(scu)) + return PTR_ERR(scu); + + cmdval = sub << 12 | cmd; + ipc_command(scu, cmdval); + err = intel_scu_ipc_check_status(scu); + if (err) + dev_err(&scu->dev, "IPC command %#x failed with %d\n", cmdval, err); + return err; +} +EXPORT_SYMBOL(intel_scu_ipc_dev_simple_command); + +/** + * intel_scu_ipc_dev_command_with_size() - Command with data + * @scu: Optional SCU IPC instance + * @cmd: Command + * @sub: Sub type + * @in: Input data + * @inlen: Input length in bytes + * @size: Input size written to the IPC command register in whatever + * units (dword, byte) the particular firmware requires. Normally + * should be the same as @inlen. + * @out: Output data + * @outlen: Output length in bytes + * + * Issue a command to the SCU which involves data transfers. Do the + * data copies under the lock but leave it for the caller to interpret. + */ +int intel_scu_ipc_dev_command_with_size(struct intel_scu_ipc_dev *scu, int cmd, + int sub, const void *in, size_t inlen, + size_t size, void *out, size_t outlen) +{ + size_t outbuflen = DIV_ROUND_UP(outlen, sizeof(u32)); + size_t inbuflen = DIV_ROUND_UP(inlen, sizeof(u32)); + u32 cmdval, inbuf[4] = {}, outbuf[4] = {}; + int i, err; + + if (inbuflen > 4 || outbuflen > 4) + return -EINVAL; + + guard(mutex)(&ipclock); + + scu = intel_scu_ipc_get(scu); + if (IS_ERR(scu)) + return PTR_ERR(scu); + + memcpy(inbuf, in, inlen); + for (i = 0; i < inbuflen; i++) + ipc_data_writel(scu, inbuf[i], 4 * i); + + cmdval = (size << 16) | (sub << 12) | cmd; + ipc_command(scu, cmdval); + err = intel_scu_ipc_check_status(scu); + if (err) { + dev_err(&scu->dev, "IPC command %#x failed with %d\n", cmdval, err); + return err; + } + + for (i = 0; i < outbuflen; i++) + outbuf[i] = ipc_data_readl(scu, 4 * i); + + memcpy(out, outbuf, outlen); + + return 0; +} +EXPORT_SYMBOL(intel_scu_ipc_dev_command_with_size); + +/* + * Interrupt handler gets called when ioc bit of IPC_COMMAND_REG set to 1 + * When ioc bit is set to 1, caller api must wait for interrupt handler called + * which in turn unlocks the caller api. Currently this is not used + * + * This is edge triggered so we need take no action to clear anything + */ +static irqreturn_t ioc(int irq, void *dev_id) +{ + struct intel_scu_ipc_dev *scu = dev_id; + int status = ipc_read_status(scu); + + writel(status | IPC_STATUS_IRQ, scu->ipc_base + IPC_STATUS); + complete(&scu->cmd_complete); + + return IRQ_HANDLED; +} + +static void intel_scu_ipc_release(struct device *dev) +{ + struct intel_scu_ipc_dev *scu = container_of(dev, struct intel_scu_ipc_dev, dev); + struct intel_scu_ipc_data *data = &scu->data; + + if (data->irq > 0) + free_irq(data->irq, scu); + iounmap(scu->ipc_base); + release_mem_region(data->mem.start, resource_size(&data->mem)); + kfree(scu); +} + +/** + * __intel_scu_ipc_register() - Register SCU IPC device + * @parent: Parent device + * @scu_data: Data used to configure SCU IPC + * @owner: Module registering the SCU IPC device + * + * Call this function to register SCU IPC mechanism under @parent. + * Returns pointer to the new SCU IPC device or ERR_PTR() in case of + * failure. The caller may use the returned instance if it needs to do + * SCU IPC calls itself. + */ +struct intel_scu_ipc_dev * +__intel_scu_ipc_register(struct device *parent, + const struct intel_scu_ipc_data *scu_data, + struct module *owner) +{ + int err; + struct intel_scu_ipc_data *data; + struct intel_scu_ipc_dev *scu; + void __iomem *ipc_base; + + guard(mutex)(&ipclock); + + /* We support only one IPC */ + if (ipcdev) + return ERR_PTR(-EBUSY); + + scu = kzalloc_obj(*scu); + if (!scu) + return ERR_PTR(-ENOMEM); + + scu->owner = owner; + scu->dev.parent = parent; + scu->dev.class = &intel_scu_ipc_class; + scu->dev.release = intel_scu_ipc_release; + + memcpy(&scu->data, scu_data, sizeof(scu->data)); + data = &scu->data; + + if (!request_mem_region(data->mem.start, resource_size(&data->mem), "intel_scu_ipc")) { + err = -EBUSY; + goto err_free; + } + + ipc_base = ioremap(data->mem.start, resource_size(&data->mem)); + if (!ipc_base) { + err = -ENOMEM; + goto err_release; + } + + scu->ipc_base = ipc_base; + init_completion(&scu->cmd_complete); + + if (data->irq > 0) { + err = request_irq(data->irq, ioc, 0, "intel_scu_ipc", scu); + if (err) + goto err_unmap; + } + + /* + * After this point intel_scu_ipc_release() takes care of + * releasing the SCU IPC resources once refcount drops to zero. + */ + dev_set_name(&scu->dev, "intel_scu_ipc"); + err = device_register(&scu->dev); + if (err) { + put_device(&scu->dev); + return ERR_PTR(err); + } + + /* Assign device at last */ + ipcdev = scu; + return scu; + +err_unmap: + iounmap(ipc_base); +err_release: + release_mem_region(data->mem.start, resource_size(&data->mem)); +err_free: + kfree(scu); + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(__intel_scu_ipc_register); + +/** + * intel_scu_ipc_unregister() - Unregister SCU IPC + * @scu: SCU IPC handle + * + * This unregisters the SCU IPC device and releases the acquired + * resources once the refcount goes to zero. + */ +void intel_scu_ipc_unregister(struct intel_scu_ipc_dev *scu) +{ + guard(mutex)(&ipclock); + + if (!WARN_ON(!ipcdev)) { + ipcdev = NULL; + device_unregister(&scu->dev); + } +} +EXPORT_SYMBOL_GPL(intel_scu_ipc_unregister); + +static void devm_intel_scu_ipc_unregister(struct device *dev, void *res) +{ + struct intel_scu_ipc_devres *dr = res; + struct intel_scu_ipc_dev *scu = dr->scu; + + intel_scu_ipc_unregister(scu); +} + +/** + * __devm_intel_scu_ipc_register() - Register managed SCU IPC device + * @parent: Parent device + * @scu_data: Data used to configure SCU IPC + * @owner: Module registering the SCU IPC device + * + * Call this function to register managed SCU IPC mechanism under + * @parent. Returns pointer to the new SCU IPC device or ERR_PTR() in + * case of failure. The caller may use the returned instance if it needs + * to do SCU IPC calls itself. + */ +struct intel_scu_ipc_dev * +__devm_intel_scu_ipc_register(struct device *parent, + const struct intel_scu_ipc_data *scu_data, + struct module *owner) +{ + struct intel_scu_ipc_devres *dr; + struct intel_scu_ipc_dev *scu; + + dr = devres_alloc(devm_intel_scu_ipc_unregister, sizeof(*dr), GFP_KERNEL); + if (!dr) + return NULL; + + scu = __intel_scu_ipc_register(parent, scu_data, owner); + if (IS_ERR(scu)) { + devres_free(dr); + return scu; + } + + dr->scu = scu; + devres_add(parent, dr); + + return scu; +} +EXPORT_SYMBOL_GPL(__devm_intel_scu_ipc_register); + +static int __init intel_scu_ipc_init(void) +{ + return class_register(&intel_scu_ipc_class); +} +subsys_initcall(intel_scu_ipc_init); + +static void __exit intel_scu_ipc_exit(void) +{ + class_unregister(&intel_scu_ipc_class); +} +module_exit(intel_scu_ipc_exit); diff --git a/drivers/platform/x86/intel_scu_ipcutil.c b/drivers/platform/x86/intel_scu_ipcutil.c new file mode 100644 index 000000000..69b36ce41 --- /dev/null +++ b/drivers/platform/x86/intel_scu_ipcutil.c @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for the Intel SCU IPC mechanism + * + * (C) Copyright 2008-2010 Intel Corporation + * Author: Sreedhara DS (sreedhara.ds@intel.com) + * + * This driver provides IOCTL interfaces to call Intel SCU IPC driver API. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static int major; + +static struct intel_scu_ipc_dev *scu; +static DEFINE_MUTEX(scu_lock); + +/* IOCTL commands */ +#define INTE_SCU_IPC_REGISTER_READ 0 +#define INTE_SCU_IPC_REGISTER_WRITE 1 +#define INTE_SCU_IPC_REGISTER_UPDATE 2 + +struct scu_ipc_data { + u32 count; /* No. of registers */ + u16 addr[5]; /* Register addresses */ + u8 data[5]; /* Register data */ + u8 mask; /* Valid for read-modify-write */ +}; + +/** + * scu_reg_access - implement register access ioctls + * @cmd: command we are doing (read/write/update) + * @data: kernel copy of ioctl data + * + * Allow the user to perform register accesses on the SCU via the + * kernel interface + */ + +static int scu_reg_access(u32 cmd, struct scu_ipc_data *data) +{ + unsigned int count = data->count; + + if (count == 0 || count == 3 || count > 4) + return -EINVAL; + + switch (cmd) { + case INTE_SCU_IPC_REGISTER_READ: + return intel_scu_ipc_dev_readv(scu, data->addr, data->data, count); + case INTE_SCU_IPC_REGISTER_WRITE: + return intel_scu_ipc_dev_writev(scu, data->addr, data->data, count); + case INTE_SCU_IPC_REGISTER_UPDATE: + return intel_scu_ipc_dev_update(scu, data->addr[0], data->data[0], + data->mask); + default: + return -ENOTTY; + } +} + +/** + * scu_ipc_ioctl - control ioctls for the SCU + * @fp: file handle of the SCU device + * @cmd: ioctl coce + * @arg: pointer to user passed structure + * + * Support the I/O and firmware flashing interfaces of the SCU + */ +static long scu_ipc_ioctl(struct file *fp, unsigned int cmd, + unsigned long arg) +{ + int ret; + struct scu_ipc_data data; + void __user *argp = (void __user *)arg; + + if (!capable(CAP_SYS_RAWIO)) + return -EPERM; + + if (copy_from_user(&data, argp, sizeof(struct scu_ipc_data))) + return -EFAULT; + ret = scu_reg_access(cmd, &data); + if (ret < 0) + return ret; + if (copy_to_user(argp, &data, sizeof(struct scu_ipc_data))) + return -EFAULT; + return 0; +} + +static int scu_ipc_open(struct inode *inode, struct file *file) +{ + int ret = 0; + + /* Only single open at the time */ + mutex_lock(&scu_lock); + if (scu) { + ret = -EBUSY; + goto unlock; + } + + scu = intel_scu_ipc_dev_get(); + if (!scu) + ret = -ENODEV; + +unlock: + mutex_unlock(&scu_lock); + return ret; +} + +static int scu_ipc_release(struct inode *inode, struct file *file) +{ + mutex_lock(&scu_lock); + intel_scu_ipc_dev_put(scu); + scu = NULL; + mutex_unlock(&scu_lock); + + return 0; +} + +static const struct file_operations scu_ipc_fops = { + .unlocked_ioctl = scu_ipc_ioctl, + .open = scu_ipc_open, + .release = scu_ipc_release, +}; + +static int __init ipc_module_init(void) +{ + major = register_chrdev(0, "intel_mid_scu", &scu_ipc_fops); + if (major < 0) + return major; + + return 0; +} + +static void __exit ipc_module_exit(void) +{ + unregister_chrdev(major, "intel_mid_scu"); +} + +module_init(ipc_module_init); +module_exit(ipc_module_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Utility driver for intel scu ipc"); +MODULE_AUTHOR("Sreedhara "); diff --git a/drivers/platform/x86/intel_scu_pcidrv.c b/drivers/platform/x86/intel_scu_pcidrv.c new file mode 100644 index 000000000..d7f72d6de --- /dev/null +++ b/drivers/platform/x86/intel_scu_pcidrv.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * PCI driver for the Intel SCU. + * + * Copyright (C) 2008-2010, 2015, 2020 Intel Corporation + * Authors: Sreedhara DS (sreedhara.ds@intel.com) + * Mika Westerberg + */ + +#include +#include +#include + +#include + +static int intel_scu_pci_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + struct intel_scu_ipc_data scu_data = {}; + struct intel_scu_ipc_dev *scu; + int ret; + + ret = pcim_enable_device(pdev); + if (ret) + return ret; + + scu_data.mem = pdev->resource[0]; + scu_data.irq = pdev->irq; + + scu = intel_scu_ipc_register(&pdev->dev, &scu_data); + return PTR_ERR_OR_ZERO(scu); +} + +static const struct pci_device_id pci_ids[] = { + { PCI_VDEVICE(INTEL, 0x080e) }, + { PCI_VDEVICE(INTEL, 0x082a) }, + { PCI_VDEVICE(INTEL, 0x08ea) }, + { PCI_VDEVICE(INTEL, 0x0a94) }, + { PCI_VDEVICE(INTEL, 0x11a0) }, + { PCI_VDEVICE(INTEL, 0x1a94) }, + { PCI_VDEVICE(INTEL, 0x5a94) }, + {} +}; + +static struct pci_driver intel_scu_pci_driver = { + .driver = { + .suppress_bind_attrs = true, + }, + .name = "intel_scu", + .id_table = pci_ids, + .probe = intel_scu_pci_probe, +}; + +builtin_pci_driver(intel_scu_pci_driver); diff --git a/drivers/platform/x86/intel_scu_pltdrv.c b/drivers/platform/x86/intel_scu_pltdrv.c new file mode 100644 index 000000000..d5ab62cbf --- /dev/null +++ b/drivers/platform/x86/intel_scu_pltdrv.c @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Platform driver for the Intel SCU. + * + * Copyright (C) 2019, Intel Corporation + * Authors: Divya Sasidharan + * Mika Westerberg + * Rajmohan Mani + */ + +#include +#include +#include +#include +#include + +#include + +static int intel_scu_platform_probe(struct platform_device *pdev) +{ + struct intel_scu_ipc_data scu_data = {}; + struct intel_scu_ipc_dev *scu; + const struct resource *res; + + scu_data.irq = platform_get_irq_optional(pdev, 0); + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENOMEM; + + scu_data.mem = *res; + + scu = devm_intel_scu_ipc_register(&pdev->dev, &scu_data); + if (IS_ERR(scu)) + return PTR_ERR(scu); + + platform_set_drvdata(pdev, scu); + return 0; +} + +static const struct acpi_device_id intel_scu_acpi_ids[] = { + { "INTC1026" }, + {} +}; +MODULE_DEVICE_TABLE(acpi, intel_scu_acpi_ids); + +static struct platform_driver intel_scu_platform_driver = { + .probe = intel_scu_platform_probe, + .driver = { + .name = "intel_scu", + .acpi_match_table = intel_scu_acpi_ids, + }, +}; +module_platform_driver(intel_scu_platform_driver); + +MODULE_AUTHOR("Divya Sasidharan "); +MODULE_AUTHOR("Mika Westerberg "); +MODULE_DESCRIPTION("Intel SCU platform driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/intel_scu_wdt.c b/drivers/platform/x86/intel_scu_wdt.c new file mode 100644 index 000000000..746d47d33 --- /dev/null +++ b/drivers/platform/x86/intel_scu_wdt.c @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Intel Merrifield watchdog platform device library file + * + * (C) Copyright 2014 Intel Corporation + * Author: David Cohen + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define TANGIER_EXT_TIMER0_MSI 12 + +static struct platform_device wdt_dev = { + .name = "intel_mid_wdt", + .id = -1, +}; + +static int tangier_probe(struct platform_device *pdev) +{ + struct irq_alloc_info info; + struct intel_mid_wdt_pdata *pdata = pdev->dev.platform_data; + int gsi = TANGIER_EXT_TIMER0_MSI; + int irq; + + if (!pdata) + return -EINVAL; + + /* IOAPIC builds identity mapping between GSI and IRQ on MID */ + ioapic_set_alloc_attr(&info, cpu_to_node(0), 1, 0); + irq = mp_map_gsi_to_irq(gsi, IOAPIC_MAP_ALLOC, &info); + if (irq < 0) { + dev_warn(&pdev->dev, "cannot find interrupt %d in ioapic\n", gsi); + return irq; + } + + pdata->irq = irq; + return 0; +} + +static struct intel_mid_wdt_pdata tangier_pdata = { + .probe = tangier_probe, +}; + +static const struct x86_cpu_id intel_mid_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_SILVERMONT_MID, &tangier_pdata), + {} +}; + +static int __init register_mid_wdt(void) +{ + const struct x86_cpu_id *id; + + id = x86_match_cpu(intel_mid_cpu_ids); + if (!id) + return -ENODEV; + + wdt_dev.dev.platform_data = (struct intel_mid_wdt_pdata *)id->driver_data; + return platform_device_register(&wdt_dev); +} +arch_initcall(register_mid_wdt); + +static void __exit unregister_mid_wdt(void) +{ + platform_device_unregister(&wdt_dev); +} +__exitcall(unregister_mid_wdt); diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig new file mode 100644 index 000000000..4443f40ef --- /dev/null +++ b/drivers/platform/x86/lenovo/Kconfig @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Lenovo X86 Platform Specific Drivers +# + +config IDEAPAD_LAPTOP + tristate "Lenovo IdeaPad Laptop Extras" + depends on ACPI + depends on ACPI_BATTERY + depends on RFKILL && INPUT + depends on SERIO_I8042 + depends on BACKLIGHT_CLASS_DEVICE + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on ACPI_WMI || ACPI_WMI = n + select ACPI_PLATFORM_PROFILE + select INPUT_SPARSEKMAP + select NEW_LEDS + select LEDS_CLASS + help + This is a driver for Lenovo IdeaPad netbooks contains drivers for + rfkill switch, hotkey, fan control and backlight control. + +config LENOVO_WMI_HOTKEY_UTILITIES + tristate "Lenovo Hotkey Utility WMI extras driver" + depends on ACPI_WMI + select NEW_LEDS + select LEDS_CLASS + imply IDEAPAD_LAPTOP + help + This driver provides WMI support for Lenovo customized hotkeys function, + such as LED control for audio/mic mute event for Ideapad, YOGA, XiaoXin, + Gaming, ThinkBook and so on. + +config LENOVO_WMI_CAMERA + tristate "Lenovo WMI Camera Button driver" + depends on ACPI_WMI + depends on INPUT + help + This driver provides support for Lenovo camera button. The Camera + button is a GPIO device. This driver receives ACPI notifications when + the camera button is switched on/off. + + To compile this driver as a module, choose M here: the module + will be called lenovo-wmi-camera. + +config LENOVO_YMC + tristate "Lenovo Yoga Tablet Mode Control" + depends on ACPI_WMI + depends on INPUT + depends on IDEAPAD_LAPTOP + select INPUT_SPARSEKMAP + help + This driver maps the Tablet Mode Control switch to SW_TABLET_MODE input + events for Lenovo Yoga notebooks. + +config THINKPAD_ACPI + tristate "ThinkPad ACPI Laptop Extras" + depends on ACPI_EC + depends on ACPI_BATTERY + depends on INPUT + depends on RFKILL || RFKILL = n + depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on BACKLIGHT_CLASS_DEVICE + depends on I2C + depends on DRM + select ACPI_PLATFORM_PROFILE + select DRM_PRIVACY_SCREEN + select HWMON + select NVRAM + select NEW_LEDS + select LEDS_CLASS + select INPUT_SPARSEKMAP + help + This is a driver for the IBM and Lenovo ThinkPad laptops. It adds + support for Fn-Fx key combinations, Bluetooth control, video + output switching, ThinkLight control, UltraBay eject and more. + For more information about this driver see + and + . + + This driver was formerly known as ibm-acpi. + + Extra functionality will be available if the rfkill (CONFIG_RFKILL) + and/or ALSA (CONFIG_SND) subsystems are available in the kernel. + Note that if you want ThinkPad-ACPI to be built-in instead of + modular, ALSA and rfkill will also have to be built-in. + + If you have an IBM or Lenovo ThinkPad laptop, say Y or M here. + +config THINKPAD_ACPI_ALSA_SUPPORT + bool "Console audio control ALSA interface" + depends on THINKPAD_ACPI + depends on SND + depends on SND = y || THINKPAD_ACPI = SND + default y + help + Enables monitoring of the built-in console audio output control + (headphone and speakers), which is operated by the mute and (in + some ThinkPad models) volume hotkeys. + + If this option is enabled, ThinkPad-ACPI will export an ALSA card + with a single read-only mixer control, which should be used for + on-screen-display feedback purposes by the Desktop Environment. + + Optionally, the driver will also allow software control (the + ALSA mixer will be made read-write). Please refer to the driver + documentation for details. + + All IBM models have both volume and mute control. Newer Lenovo + models only have mute control (the volume hotkeys are just normal + keys and volume control is done through the main HDA mixer). + +config THINKPAD_ACPI_DEBUGFACILITIES + bool "Maintainer debug facilities" + depends on THINKPAD_ACPI + help + Enables extra stuff in the thinkpad-acpi which is completely useless + for normal use. Read the driver source to find out what it does. + + Say N here, unless you were told by a kernel maintainer to do + otherwise. + +config THINKPAD_ACPI_DEBUG + bool "Verbose debug mode" + depends on THINKPAD_ACPI + help + Enables extra debugging information, at the expense of a slightly + increase in driver size. + + If you are not sure, say N here. + +config THINKPAD_ACPI_UNSAFE_LEDS + bool "Allow control of important LEDs (unsafe)" + depends on THINKPAD_ACPI + help + Overriding LED state on ThinkPads can mask important + firmware alerts (like critical battery condition), or misled + the user into damaging the hardware (undocking or ejecting + the bay while buses are still active), etc. + + LED control on the ThinkPad is write-only (with very few + exceptions on very ancient models), which makes it + impossible to know beforehand if important information will + be lost when one changes LED state. + + Users that know what they are doing can enable this option + and the driver will allow control of every LED, including + the ones on the dock stations. + + Never enable this option on a distribution kernel. + + Say N here, unless you are building a kernel for your own + use, and need to control the important firmware LEDs. + +config THINKPAD_ACPI_VIDEO + bool "Video output control support" + depends on THINKPAD_ACPI + default y + help + Allows the thinkpad_acpi driver to provide an interface to control + the various video output ports. + + This feature often won't work well, depending on ThinkPad model, + display state, video output devices in use, whether there is a X + server running, phase of the moon, and the current mood of + Schroedinger's cat. If you can use X.org's RandR to control + your ThinkPad's video output ports instead of this feature, + don't think twice: do it and say N here to save memory and avoid + bad interactions with X.org. + + NOTE: access to this feature is limited to processes with the + CAP_SYS_ADMIN capability, to avoid local DoS issues in platforms + where it interacts badly with X.org. + + If you are not sure, say Y here but do try to check if you could + be using X.org RandR instead. + +config THINKPAD_ACPI_HOTKEY_POLL + bool "Support NVRAM polling for hot keys" + depends on THINKPAD_ACPI + default y + help + Some thinkpad models benefit from NVRAM polling to detect a few of + the hot key press events. If you know your ThinkPad model does not + need to do NVRAM polling to support any of the hot keys you use, + unselecting this option will save about 1kB of memory. + + ThinkPads T40 and newer, R52 and newer, and X31 and newer are + unlikely to need NVRAM polling in their latest BIOS versions. + + NVRAM polling can detect at most the following keys: ThinkPad/Access + IBM, Zoom, Switch Display (fn+F7), ThinkLight, Volume up/down/mute, + Brightness up/down, Display Expand (fn+F8), Hibernate (fn+F12). + + If you are not sure, say Y here. The driver enables polling only if + it is strictly necessary to do so. + +config THINKPAD_LMI + tristate "Lenovo WMI-based systems management driver" + depends on ACPI_WMI + depends on DMI + select FW_ATTR_CLASS + help + This driver allows changing BIOS settings on Lenovo machines whose + BIOS support the WMI interface. + + To compile this driver as a module, choose M here: the module will + be called think-lmi. + +config YOGABOOK + tristate "Lenovo Yoga Book tablet key driver" + depends on ACPI_WMI + depends on INPUT + depends on I2C + select LEDS_CLASS + select NEW_LEDS + help + Say Y here if you want to support the 'Pen' key and keyboard backlight + control on the Lenovo Yoga Book tablets. + + To compile this driver as a module, choose M here: the module will + be called lenovo-yogabook. + +config YT2_1380 + tristate "Lenovo Yoga Tablet 2 1380 fast charge driver" + depends on SERIAL_DEV_BUS + depends on EXTCON + depends on ACPI + help + Say Y here to enable support for the custom fast charging protocol + found on the Lenovo Yoga Tablet 2 1380F / 1380L models. + + To compile this driver as a module, choose M here: the module will + be called lenovo-yogabook. + +config LENOVO_WMI_CAPDATA + tristate + depends on ACPI_WMI + depends on LENOVO_WMI_HELPERS + +config LENOVO_WMI_EVENTS + tristate + depends on ACPI_WMI + +config LENOVO_WMI_HELPERS + tristate + depends on ACPI_WMI + +config LENOVO_WMI_GAMEZONE + tristate "Lenovo GameZone WMI Driver" + depends on ACPI_WMI + depends on DMI + select ACPI_PLATFORM_PROFILE + select LENOVO_WMI_EVENTS + select LENOVO_WMI_HELPERS + help + Say Y here if you have a WMI aware Lenovo Legion device and would like to use the + platform-profile firmware interface to manage power usage. + + To compile this driver as a module, choose M here: the module will + be called lenovo-wmi-gamezone. + +config LENOVO_WMI_TUNING + tristate "Lenovo Other Mode WMI Driver" + depends on ACPI_WMI + depends on ACPI_BATTERY + select HWMON + select FW_ATTR_CLASS + select LENOVO_WMI_CAPDATA + select LENOVO_WMI_EVENTS + select LENOVO_WMI_HELPERS + help + Say Y here if you have a WMI aware Lenovo Legion device and would like to use the + firmware_attributes API to control various tunable settings typically exposed by + Lenovo software in Windows. + + To compile this driver as a module, choose M here: the module will + be called lenovo-wmi-other. diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile new file mode 100644 index 000000000..91a9370f1 --- /dev/null +++ b/drivers/platform/x86/lenovo/Makefile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/lenovo +# Lenovo x86 Platform Specific Drivers +# +obj-$(CONFIG_IDEAPAD_LAPTOP) += ideapad-laptop.o +obj-$(CONFIG_THINKPAD_LMI) += think-lmi.o +obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o + +lenovo-target-$(CONFIG_LENOVO_WMI_HOTKEY_UTILITIES) += wmi-hotkey-utilities.o +lenovo-target-$(CONFIG_LENOVO_YMC) += ymc.o +lenovo-target-$(CONFIG_YOGABOOK) += yogabook.o +lenovo-target-$(CONFIG_YT2_1380) += yoga-tab2-pro-1380-fastcharger.o +lenovo-target-$(CONFIG_LENOVO_WMI_CAMERA) += wmi-camera.o +lenovo-target-$(CONFIG_LENOVO_WMI_CAPDATA) += wmi-capdata.o +lenovo-target-$(CONFIG_LENOVO_WMI_EVENTS) += wmi-events.o +lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS) += wmi-helpers.o +lenovo-target-$(CONFIG_LENOVO_WMI_GAMEZONE) += wmi-gamezone.o +lenovo-target-$(CONFIG_LENOVO_WMI_TUNING) += wmi-other.o + +# Add 'lenovo' prefix to each module listed in lenovo-target-* +define LENOVO_OBJ_TARGET +lenovo-$(1)-y := $(1).o +obj-$(2) += lenovo-$(1).o +endef + +$(foreach target, $(basename $(lenovo-target-y)), $(eval $(call LENOVO_OBJ_TARGET,$(target),y))) +$(foreach target, $(basename $(lenovo-target-m)), $(eval $(call LENOVO_OBJ_TARGET,$(target),m))) diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c new file mode 100644 index 000000000..4fbc904f1 --- /dev/null +++ b/drivers/platform/x86/lenovo/ideapad-laptop.c @@ -0,0 +1,2574 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ideapad-laptop.c - Lenovo IdeaPad ACPI Extras + * + * Copyright © 2010 Intel Corporation + * Copyright © 2010 David Woodhouse + */ + +#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 +#include +#include +#include +#include +#include +#include +#include +#include "ideapad-laptop.h" + +#include +#include + +#include + +#define IDEAPAD_RFKILL_DEV_NUM 3 + +enum { + CFG_CAP_BT_BIT = 16, + CFG_CAP_3G_BIT = 17, + CFG_CAP_WIFI_BIT = 18, + CFG_CAP_CAM_BIT = 19, + + /* + * These are OnScreenDisplay support bits that can be useful to determine + * whether a hotkey exists/should show OSD. But they aren't particularly + * meaningful since they were introduced later, i.e. 2010 IdeaPads + * don't have these, but they still have had OSD for hotkeys. + */ + CFG_OSD_NUMLK_BIT = 27, + CFG_OSD_CAPSLK_BIT = 28, + CFG_OSD_MICMUTE_BIT = 29, + CFG_OSD_TOUCHPAD_BIT = 30, + CFG_OSD_CAM_BIT = 31, +}; + +/* + * There are two charge modes supported by the GBMD/SBMC interface: + * - "Rapid Charge": increase power to speed up charging + * - "Conservation Mode": stop charging at 60-80% (depends on model) + * + * The interface doesn't prohibit enabling both modes at the same time. + * However, doing so is essentially meaningless, and the manufacturer utilities + * on Windows always make them mutually exclusive. + */ + +enum { + GBMD_RAPID_CHARGE_STATE_BIT = 2, + GBMD_CONSERVATION_STATE_BIT = 5, + GBMD_RAPID_CHARGE_SUPPORTED_BIT = 17, +}; + +enum { + SBMC_CONSERVATION_ON = 3, + SBMC_CONSERVATION_OFF = 5, + SBMC_RAPID_CHARGE_ON = 7, + SBMC_RAPID_CHARGE_OFF = 8, +}; + +enum { + HALS_KBD_BL_SUPPORT_BIT = 4, + HALS_KBD_BL_STATE_BIT = 5, + HALS_USB_CHARGING_SUPPORT_BIT = 6, + HALS_USB_CHARGING_STATE_BIT = 7, + HALS_FNLOCK_SUPPORT_BIT = 9, + HALS_FNLOCK_STATE_BIT = 10, + HALS_HOTKEYS_PRIMARY_BIT = 11, +}; + +enum { + SALS_KBD_BL_ON = 0x8, + SALS_KBD_BL_OFF = 0x9, + SALS_USB_CHARGING_ON = 0xa, + SALS_USB_CHARGING_OFF = 0xb, + SALS_FNLOCK_ON = 0xe, + SALS_FNLOCK_OFF = 0xf, +}; + +enum { + VPCCMD_R_VPC1 = 0x10, + VPCCMD_R_BL_MAX, + VPCCMD_R_BL, + VPCCMD_W_BL, + VPCCMD_R_WIFI, + VPCCMD_W_WIFI, + VPCCMD_R_BT, + VPCCMD_W_BT, + VPCCMD_R_BL_POWER, + VPCCMD_R_NOVO, + VPCCMD_R_VPC2, + VPCCMD_R_TOUCHPAD, + VPCCMD_W_TOUCHPAD, + VPCCMD_R_CAMERA, + VPCCMD_W_CAMERA, + VPCCMD_R_3G, + VPCCMD_W_3G, + VPCCMD_R_ODD, /* 0x21 */ + VPCCMD_W_FAN, + VPCCMD_R_RF, + VPCCMD_W_RF, + VPCCMD_W_YMC = 0x2A, + VPCCMD_R_FAN = 0x2B, + VPCCMD_R_SPECIAL_BUTTONS = 0x31, + VPCCMD_W_BL_POWER = 0x33, +}; + +/* + * These correspond to the number of supported states - 1 + * Future keyboard types may need a new system, if there's a collision + * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state + * so it effectively has 3 states, but needs to handle 4 + */ +enum { + KBD_BL_STANDARD = 1, + KBD_BL_TRISTATE = 2, + KBD_BL_TRISTATE_AUTO = 3, +}; + +#define KBD_BL_QUERY_TYPE 0x1 +#define KBD_BL_TRISTATE_TYPE 0x5 +#define KBD_BL_TRISTATE_AUTO_TYPE 0x7 + +#define KBD_BL_COMMAND_GET 0x2 +#define KBD_BL_COMMAND_SET 0x3 +#define KBD_BL_COMMAND_TYPE GENMASK(7, 4) + +#define KBD_BL_GET_BRIGHTNESS GENMASK(15, 1) +#define KBD_BL_SET_BRIGHTNESS GENMASK(19, 16) + +#define KBD_BL_KBLC_CHANGED_EVENT 12 + +struct ideapad_dytc_priv { + enum platform_profile_option current_profile; + struct device *ppdev; /* platform profile device */ + struct mutex mutex; /* protects the DYTC interface */ + struct ideapad_private *priv; +}; + +struct ideapad_rfk_priv { + int dev; + struct ideapad_private *priv; +}; + +struct ideapad_private { + struct acpi_device *adev; + struct mutex vpc_mutex; /* protects the VPC calls */ + struct mutex gbmd_sbmc_mutex; /* protects GBMD/SBMC calls */ + struct rfkill *rfk[IDEAPAD_RFKILL_DEV_NUM]; + struct ideapad_rfk_priv rfk_priv[IDEAPAD_RFKILL_DEV_NUM]; + struct platform_device *platform_device; + struct input_dev *inputdev; + struct backlight_device *blightdev; + struct ideapad_dytc_priv *dytc; + struct dentry *debug; + struct acpi_battery_hook battery_hook; + const struct power_supply_ext *battery_ext; + unsigned long cfg; + unsigned long r_touchpad_val; + struct { + bool rapid_charge : 1; + bool conservation_mode : 1; + bool dytc : 1; + bool fan_mode : 1; + bool fn_lock : 1; + bool set_fn_lock_led : 1; + bool hw_rfkill_switch : 1; + bool kbd_bl : 1; + bool touchpad_ctrl_via_ec : 1; + bool ctrl_ps2_aux_port : 1; + bool usb_charging : 1; + bool ymc_ec_trigger : 1; + } features; + struct { + bool initialized; + int type; + struct led_classdev led; + unsigned int last_brightness; + } kbd_bl; + struct { + bool initialized; + struct led_classdev led; + unsigned int last_brightness; + } fn_lock; +}; + +static bool no_bt_rfkill; +module_param(no_bt_rfkill, bool, 0444); +MODULE_PARM_DESC(no_bt_rfkill, "No rfkill for bluetooth."); + +static bool allow_v4_dytc; +module_param(allow_v4_dytc, bool, 0444); +MODULE_PARM_DESC(allow_v4_dytc, + "Enable DYTC version 4 platform-profile support. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool hw_rfkill_switch; +module_param(hw_rfkill_switch, bool, 0444); +MODULE_PARM_DESC(hw_rfkill_switch, + "Enable rfkill support for laptops with a hw on/off wifi switch/slider. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool set_fn_lock_led; +module_param(set_fn_lock_led, bool, 0444); +MODULE_PARM_DESC(set_fn_lock_led, + "Enable driver based updates of the fn-lock LED on fn-lock changes. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool ctrl_ps2_aux_port; +module_param(ctrl_ps2_aux_port, bool, 0444); +MODULE_PARM_DESC(ctrl_ps2_aux_port, + "Enable driver based PS/2 aux port en-/dis-abling on touchpad on/off toggle. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool touchpad_ctrl_via_ec; +module_param(touchpad_ctrl_via_ec, bool, 0444); +MODULE_PARM_DESC(touchpad_ctrl_via_ec, + "Enable registering a 'touchpad' sysfs-attribute which can be used to manually tell the EC to enable/disable the touchpad. This may not work on all models."); + +static bool ymc_ec_trigger __read_mostly; +module_param(ymc_ec_trigger, bool, 0444); +MODULE_PARM_DESC(ymc_ec_trigger, + "Enable EC triggering work-around to force emitting tablet mode events. If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +/* + * shared data + */ + +static struct ideapad_private *ideapad_shared; +static DEFINE_MUTEX(ideapad_shared_mutex); + +static int ideapad_shared_init(struct ideapad_private *priv) +{ + int ret; + + guard(mutex)(&ideapad_shared_mutex); + + if (!ideapad_shared) { + ideapad_shared = priv; + ret = 0; + } else { + dev_warn(&priv->adev->dev, "found multiple platform devices\n"); + ret = -EINVAL; + } + + return ret; +} + +static void ideapad_shared_exit(struct ideapad_private *priv) +{ + guard(mutex)(&ideapad_shared_mutex); + + if (ideapad_shared == priv) + ideapad_shared = NULL; +} + +/* + * ACPI Helpers + */ +#define IDEAPAD_EC_TIMEOUT 200 /* in ms */ + +/* + * Some models (e.g., ThinkBook since 2024) have a low tolerance for being + * polled too frequently. Doing so may break the state machine in the EC, + * resulting in a hard shutdown. + * + * It is also observed that frequent polls may disturb the ongoing operation + * and notably delay the availability of EC response. + * + * These values are used as the delay before the first poll and the interval + * between subsequent polls to solve the above issues. + */ +#define IDEAPAD_EC_POLL_MIN_US 150 +#define IDEAPAD_EC_POLL_MAX_US 300 + +static int eval_int(acpi_handle handle, const char *name, unsigned long *res) +{ + unsigned long long result; + acpi_status status; + + status = acpi_evaluate_integer(handle, (char *)name, NULL, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + *res = result; + + return 0; +} + +static int eval_int_with_arg(acpi_handle handle, const char *name, unsigned long arg, + unsigned long *res) +{ + struct acpi_object_list params; + unsigned long long result; + union acpi_object in_obj; + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = arg; + + status = acpi_evaluate_integer(handle, (char *)name, ¶ms, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + if (res) + *res = result; + + return 0; +} + +static int exec_simple_method(acpi_handle handle, const char *name, unsigned long arg) +{ + acpi_status status = acpi_execute_simple_method(handle, (char *)name, arg); + + return ACPI_FAILURE(status) ? -EIO : 0; +} + +static int eval_gbmd(acpi_handle handle, unsigned long *res) +{ + return eval_int(handle, "GBMD", res); +} + +static int exec_sbmc(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "SBMC", arg); +} + +static int eval_hals(acpi_handle handle, unsigned long *res) +{ + return eval_int(handle, "HALS", res); +} + +static int exec_sals(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "SALS", arg); +} + +static int exec_kblc(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "KBLC", arg); +} + +static int eval_kblc(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "KBLC", cmd, res); +} + +static int eval_dytc(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "DYTC", cmd, res); +} + +static int eval_vpcr(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "VPCR", cmd, res); +} + +static int eval_vpcw(acpi_handle handle, unsigned long cmd, unsigned long data) +{ + struct acpi_object_list params; + union acpi_object in_obj[2]; + acpi_status status; + + params.count = 2; + params.pointer = in_obj; + in_obj[0].type = ACPI_TYPE_INTEGER; + in_obj[0].integer.value = cmd; + in_obj[1].type = ACPI_TYPE_INTEGER; + in_obj[1].integer.value = data; + + status = acpi_evaluate_object(handle, "VPCW", ¶ms, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +static int read_ec_data(acpi_handle handle, unsigned long cmd, unsigned long *data) +{ + unsigned long end_jiffies, val; + int err; + + err = eval_vpcw(handle, 1, cmd); + if (err) + return err; + + end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; + + while (time_before(jiffies, end_jiffies)) { + usleep_range(IDEAPAD_EC_POLL_MIN_US, IDEAPAD_EC_POLL_MAX_US); + + err = eval_vpcr(handle, 1, &val); + if (err) + return err; + + if (val == 0) + return eval_vpcr(handle, 0, data); + } + + acpi_handle_err(handle, "timeout in %s\n", __func__); + + return -ETIMEDOUT; +} + +static int write_ec_cmd(acpi_handle handle, unsigned long cmd, unsigned long data) +{ + unsigned long end_jiffies, val; + int err; + + err = eval_vpcw(handle, 0, data); + if (err) + return err; + + err = eval_vpcw(handle, 1, cmd); + if (err) + return err; + + end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; + + while (time_before(jiffies, end_jiffies)) { + usleep_range(IDEAPAD_EC_POLL_MIN_US, IDEAPAD_EC_POLL_MAX_US); + + err = eval_vpcr(handle, 1, &val); + if (err) + return err; + + if (val == 0) + return 0; + } + + acpi_handle_err(handle, "timeout in %s\n", __func__); + + return -ETIMEDOUT; +} + +/* + * debugfs + */ +static int debugfs_status_show(struct seq_file *s, void *data) +{ + struct ideapad_private *priv = s->private; + unsigned long value; + + scoped_guard(mutex, &priv->vpc_mutex) { + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &value)) + seq_printf(s, "Backlight max: %lu\n", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL, &value)) + seq_printf(s, "Backlight now: %lu\n", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &value)) + seq_printf(s, "BL power value: %s (%lu)\n", str_on_off(value), value); + + seq_puts(s, "=====================\n"); + + if (!read_ec_data(priv->adev->handle, VPCCMD_R_RF, &value)) + seq_printf(s, "Radio status: %s (%lu)\n", str_on_off(value), value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_WIFI, &value)) + seq_printf(s, "Wifi status: %s (%lu)\n", str_on_off(value), value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BT, &value)) + seq_printf(s, "BT status: %s (%lu)\n", str_on_off(value), value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_3G, &value)) + seq_printf(s, "3G status: %s (%lu)\n", str_on_off(value), value); + + seq_puts(s, "=====================\n"); + + if (!read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value)) + seq_printf(s, "Touchpad status: %s (%lu)\n", str_on_off(value), value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &value)) + seq_printf(s, "Camera status: %s (%lu)\n", str_on_off(value), value); + } + + seq_puts(s, "=====================\n"); + + scoped_guard(mutex, &priv->gbmd_sbmc_mutex) { + if (!eval_gbmd(priv->adev->handle, &value)) + seq_printf(s, "GBMD: %#010lx\n", value); + } + + if (!eval_hals(priv->adev->handle, &value)) + seq_printf(s, "HALS: %#010lx\n", value); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(debugfs_status); + +static int debugfs_cfg_show(struct seq_file *s, void *data) +{ + struct ideapad_private *priv = s->private; + + seq_printf(s, "_CFG: %#010lx\n\n", priv->cfg); + + seq_puts(s, "Capabilities:"); + if (test_bit(CFG_CAP_BT_BIT, &priv->cfg)) + seq_puts(s, " bluetooth"); + if (test_bit(CFG_CAP_3G_BIT, &priv->cfg)) + seq_puts(s, " 3G"); + if (test_bit(CFG_CAP_WIFI_BIT, &priv->cfg)) + seq_puts(s, " wifi"); + if (test_bit(CFG_CAP_CAM_BIT, &priv->cfg)) + seq_puts(s, " camera"); + seq_puts(s, "\n"); + + seq_puts(s, "OSD support:"); + if (test_bit(CFG_OSD_NUMLK_BIT, &priv->cfg)) + seq_puts(s, " num-lock"); + if (test_bit(CFG_OSD_CAPSLK_BIT, &priv->cfg)) + seq_puts(s, " caps-lock"); + if (test_bit(CFG_OSD_MICMUTE_BIT, &priv->cfg)) + seq_puts(s, " mic-mute"); + if (test_bit(CFG_OSD_TOUCHPAD_BIT, &priv->cfg)) + seq_puts(s, " touchpad"); + if (test_bit(CFG_OSD_CAM_BIT, &priv->cfg)) + seq_puts(s, " camera"); + seq_puts(s, "\n"); + + seq_puts(s, "Graphics: "); + switch (priv->cfg & 0x700) { + case 0x100: + seq_puts(s, "Intel"); + break; + case 0x200: + seq_puts(s, "ATI"); + break; + case 0x300: + seq_puts(s, "Nvidia"); + break; + case 0x400: + seq_puts(s, "Intel and ATI"); + break; + case 0x500: + seq_puts(s, "Intel and Nvidia"); + break; + } + seq_puts(s, "\n"); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(debugfs_cfg); + +static void ideapad_debugfs_init(struct ideapad_private *priv) +{ + struct dentry *dir; + + dir = debugfs_create_dir("ideapad", NULL); + priv->debug = dir; + + debugfs_create_file("cfg", 0444, dir, priv, &debugfs_cfg_fops); + debugfs_create_file("status", 0444, dir, priv, &debugfs_status_fops); +} + +static void ideapad_debugfs_exit(struct ideapad_private *priv) +{ + debugfs_remove_recursive(priv->debug); + priv->debug = NULL; +} + +/* + * sysfs + */ +static ssize_t camera_power_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &result); + if (err) + return err; + } + + return sysfs_emit(buf, "%d\n", !!result); +} + +static ssize_t camera_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_CAMERA, state); + if (err) + return err; + } + + return count; +} + +static DEVICE_ATTR_RW(camera_power); + +static void show_conservation_mode_deprecation_warning(struct device *dev) +{ + dev_warn_once(dev, "conservation_mode attribute has been deprecated, see charge_types.\n"); +} + +static ssize_t conservation_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result; + int err; + + show_conservation_mode_deprecation_warning(dev); + + scoped_guard(mutex, &priv->gbmd_sbmc_mutex) { + err = eval_gbmd(priv->adev->handle, &result); + if (err) + return err; + } + + /* + * For backward compatibility, ignore Rapid Charge while reporting the + * state of Conservation Mode. + */ + return sysfs_emit(buf, "%d\n", !!test_bit(GBMD_CONSERVATION_STATE_BIT, &result)); +} + +static ssize_t conservation_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + show_conservation_mode_deprecation_warning(dev); + + err = kstrtobool(buf, &state); + if (err) + return err; + + guard(mutex)(&priv->gbmd_sbmc_mutex); + + /* + * Prevent mutually exclusive modes from being set at the same time, + * but do not disable Rapid Charge while disabling Conservation Mode. + */ + if (priv->features.rapid_charge && state) { + err = exec_sbmc(priv->adev->handle, SBMC_RAPID_CHARGE_OFF); + if (err) + return err; + } + + err = exec_sbmc(priv->adev->handle, state ? SBMC_CONSERVATION_ON : SBMC_CONSERVATION_OFF); + if (err) + return err; + + return count; +} + +static DEVICE_ATTR_RW(conservation_mode); + +static ssize_t fan_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_FAN, &result); + if (err) + return err; + } + + return sysfs_emit(buf, "%lu\n", result); +} + +static ssize_t fan_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned int state; + int err; + + err = kstrtouint(buf, 0, &state); + if (err) + return err; + + if (state > 4 || state == 3) + return -EINVAL; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_FAN, state); + if (err) + return err; + } + + return count; +} + +static DEVICE_ATTR_RW(fan_mode); + +static int ideapad_fn_lock_get(struct ideapad_private *priv) +{ + unsigned long hals; + int err; + + err = eval_hals(priv->adev->handle, &hals); + if (err) + return err; + + return !!test_bit(HALS_FNLOCK_STATE_BIT, &hals); +} + +static int ideapad_fn_lock_set(struct ideapad_private *priv, bool state) +{ + return exec_sals(priv->adev->handle, + state ? SALS_FNLOCK_ON : SALS_FNLOCK_OFF); +} + +static void ideapad_fn_lock_led_notify(struct ideapad_private *priv, int brightness) +{ + if (!priv->fn_lock.initialized) + return; + + if (brightness == priv->fn_lock.last_brightness) + return; + + priv->fn_lock.last_brightness = brightness; + + led_classdev_notify_brightness_hw_changed(&priv->fn_lock.led, brightness); +} + +static ssize_t fn_lock_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + int brightness; + + brightness = ideapad_fn_lock_get(priv); + if (brightness < 0) + return brightness; + + return sysfs_emit(buf, "%d\n", brightness); +} + +static ssize_t fn_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + err = ideapad_fn_lock_set(priv, state); + if (err) + return err; + + ideapad_fn_lock_led_notify(priv, state); + + return count; +} + +static DEVICE_ATTR_RW(fn_lock); + +static ssize_t touchpad_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &result); + if (err) + return err; + } + + priv->r_touchpad_val = result; + + return sysfs_emit(buf, "%d\n", !!result); +} + +static ssize_t touchpad_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_TOUCHPAD, state); + if (err) + return err; + } + + priv->r_touchpad_val = state; + + return count; +} + +static DEVICE_ATTR_RW(touchpad); + +static ssize_t usb_charging_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long hals; + int err; + + err = eval_hals(priv->adev->handle, &hals); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", !!test_bit(HALS_USB_CHARGING_STATE_BIT, &hals)); +} + +static ssize_t usb_charging_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + err = exec_sals(priv->adev->handle, state ? SALS_USB_CHARGING_ON : SALS_USB_CHARGING_OFF); + if (err) + return err; + + return count; +} + +static DEVICE_ATTR_RW(usb_charging); + +static struct attribute *ideapad_attributes[] = { + &dev_attr_camera_power.attr, + &dev_attr_conservation_mode.attr, + &dev_attr_fan_mode.attr, + &dev_attr_fn_lock.attr, + &dev_attr_touchpad.attr, + &dev_attr_usb_charging.attr, + NULL +}; + +static umode_t ideapad_is_visible(struct kobject *kobj, + struct attribute *attr, + int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct ideapad_private *priv = dev_get_drvdata(dev); + bool supported = true; + + if (attr == &dev_attr_camera_power.attr) + supported = test_bit(CFG_CAP_CAM_BIT, &priv->cfg); + else if (attr == &dev_attr_conservation_mode.attr) + supported = priv->features.conservation_mode; + else if (attr == &dev_attr_fan_mode.attr) + supported = priv->features.fan_mode; + else if (attr == &dev_attr_fn_lock.attr) + supported = priv->features.fn_lock; + else if (attr == &dev_attr_touchpad.attr) + supported = priv->features.touchpad_ctrl_via_ec; + else if (attr == &dev_attr_usb_charging.attr) + supported = priv->features.usb_charging; + + return supported ? attr->mode : 0; +} + +static const struct attribute_group ideapad_attribute_group = { + .is_visible = ideapad_is_visible, + .attrs = ideapad_attributes +}; +__ATTRIBUTE_GROUPS(ideapad_attribute); + +/* + * DYTC Platform profile + */ +#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ +#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ +#define DYTC_CMD_GET 2 /* To get current IC function and mode */ +#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ + +#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ +#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ +#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ + +#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ +#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ + +#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ +#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ +#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ + +#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ +#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ +#define DYTC_FUNCTION_MMC 11 /* Function = 11, desk mode */ + +#define DYTC_MODE_PERFORM 2 /* High power mode aka performance */ +#define DYTC_MODE_LOW_POWER 3 /* Low power mode aka quiet */ +#define DYTC_MODE_BALANCE 0xF /* Default mode aka balanced */ + +#define DYTC_SET_COMMAND(function, mode, on) \ + (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ + (mode) << DYTC_SET_MODE_BIT | \ + (on) << DYTC_SET_VALID_BIT) + +#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 0) + +#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 1) + +static int convert_dytc_to_profile(int dytcmode, enum platform_profile_option *profile) +{ + switch (dytcmode) { + case DYTC_MODE_LOW_POWER: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case DYTC_MODE_BALANCE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case DYTC_MODE_PERFORM: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + default: /* Unknown mode */ + return -EINVAL; + } + + return 0; +} + +static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) +{ + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + *perfmode = DYTC_MODE_LOW_POWER; + break; + case PLATFORM_PROFILE_BALANCED: + *perfmode = DYTC_MODE_BALANCE; + break; + case PLATFORM_PROFILE_PERFORMANCE: + *perfmode = DYTC_MODE_PERFORM; + break; + default: /* Unknown profile */ + return -EOPNOTSUPP; + } + + return 0; +} + +/* + * dytc_profile_get: Function to register with platform_profile + * handler. Returns current platform profile. + */ +static int dytc_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); + + *profile = dytc->current_profile; + return 0; +} + +/* + * Helper function - check if we are in CQL mode and if we are + * - disable CQL, + * - run the command + * - enable CQL + * If not in CQL mode, just run the command + */ +static int dytc_cql_command(struct ideapad_private *priv, unsigned long cmd, + unsigned long *output) +{ + int err, cmd_err, cur_funcmode; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = eval_dytc(priv->adev->handle, DYTC_CMD_GET, output); + if (err) + return err; + + cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; + /* Check if we're OK to return immediately */ + if (cmd == DYTC_CMD_GET && cur_funcmode != DYTC_FUNCTION_CQL) + return 0; + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = eval_dytc(priv->adev->handle, DYTC_DISABLE_CQL, NULL); + if (err) + return err; + } + + cmd_err = eval_dytc(priv->adev->handle, cmd, output); + /* Check return condition after we've restored CQL state */ + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = eval_dytc(priv->adev->handle, DYTC_ENABLE_CQL, NULL); + if (err) + return err; + } + + return cmd_err; +} + +/* + * dytc_profile_set: Function to register with platform_profile + * handler. Sets current platform profile. + */ +static int dytc_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); + struct ideapad_private *priv = dytc->priv; + unsigned long output; + int err; + + scoped_guard(mutex_intr, &dytc->mutex) { + if (profile == PLATFORM_PROFILE_BALANCED) { + /* To get back to balanced mode we just issue a reset command */ + err = eval_dytc(priv->adev->handle, DYTC_CMD_RESET, NULL); + if (err) + return err; + } else { + int perfmode; + + err = convert_profile_to_dytc(profile, &perfmode); + if (err) + return err; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_cql_command(priv, + DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), + &output); + if (err) + return err; + } + + /* Success - update current profile */ + dytc->current_profile = profile; + return 0; + } + + return -EINTR; +} + +static int dytc_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); + + return 0; +} + +static void dytc_profile_refresh(struct ideapad_private *priv) +{ + enum platform_profile_option profile; + unsigned long output; + int err, perfmode; + + scoped_guard(mutex, &priv->dytc->mutex) + err = dytc_cql_command(priv, DYTC_CMD_GET, &output); + if (err) + return; + + perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; + + if (convert_dytc_to_profile(perfmode, &profile)) + return; + + if (profile != priv->dytc->current_profile) { + priv->dytc->current_profile = profile; + platform_profile_notify(priv->dytc->ppdev); + } +} + +static const struct dmi_system_id ideapad_dytc_v4_allow_table[] = { + { + /* Ideapad 5 Pro 16ACH6 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82L5") + } + }, + { + /* Ideapad 5 15ITL05 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "IdeaPad 5 15ITL05") + } + }, + {} +}; + +static const struct platform_profile_ops dytc_profile_ops = { + .probe = dytc_profile_probe, + .profile_get = dytc_profile_get, + .profile_set = dytc_profile_set, +}; + +static int ideapad_dytc_profile_init(struct ideapad_private *priv) +{ + int err, dytc_version; + unsigned long output; + + if (!priv->features.dytc) + return -ENODEV; + + err = eval_dytc(priv->adev->handle, DYTC_CMD_QUERY, &output); + /* For all other errors we can flag the failure */ + if (err) + return err; + + /* Check DYTC is enabled and supports mode setting */ + if (!test_bit(DYTC_QUERY_ENABLE_BIT, &output)) { + dev_info(&priv->platform_device->dev, "DYTC_QUERY_ENABLE_BIT returned false\n"); + return -ENODEV; + } + + dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; + + if (dytc_version < 4) { + dev_info(&priv->platform_device->dev, "DYTC_VERSION < 4 is not supported\n"); + return -ENODEV; + } + + if (dytc_version < 5 && + !(allow_v4_dytc || dmi_check_system(ideapad_dytc_v4_allow_table))) { + dev_info(&priv->platform_device->dev, + "DYTC_VERSION 4 support may not work. Pass ideapad_laptop.allow_v4_dytc=Y on the kernel commandline to enable\n"); + return -ENODEV; + } + + priv->dytc = kzalloc_obj(*priv->dytc); + if (!priv->dytc) + return -ENOMEM; + + mutex_init(&priv->dytc->mutex); + + priv->dytc->priv = priv; + + /* Create platform_profile structure and register */ + priv->dytc->ppdev = devm_platform_profile_register(&priv->platform_device->dev, + "ideapad-laptop", priv->dytc, + &dytc_profile_ops); + if (IS_ERR(priv->dytc->ppdev)) { + err = PTR_ERR(priv->dytc->ppdev); + goto pp_reg_failed; + } + + /* Ensure initial values are correct */ + dytc_profile_refresh(priv); + + return 0; + +pp_reg_failed: + mutex_destroy(&priv->dytc->mutex); + kfree(priv->dytc); + priv->dytc = NULL; + + return err; +} + +static void ideapad_dytc_profile_exit(struct ideapad_private *priv) +{ + if (!priv->dytc) + return; + + mutex_destroy(&priv->dytc->mutex); + kfree(priv->dytc); + + priv->dytc = NULL; +} + +/* + * Rfkill + */ +struct ideapad_rfk_data { + char *name; + int cfgbit; + int opcode; + int type; +}; + +static const struct ideapad_rfk_data ideapad_rfk_data[] = { + { "ideapad_wlan", CFG_CAP_WIFI_BIT, VPCCMD_W_WIFI, RFKILL_TYPE_WLAN }, + { "ideapad_bluetooth", CFG_CAP_BT_BIT, VPCCMD_W_BT, RFKILL_TYPE_BLUETOOTH }, + { "ideapad_3g", CFG_CAP_3G_BIT, VPCCMD_W_3G, RFKILL_TYPE_WWAN }, +}; + +static int ideapad_rfk_set(void *data, bool blocked) +{ + struct ideapad_rfk_priv *priv = data; + int opcode = ideapad_rfk_data[priv->dev].opcode; + + guard(mutex)(&priv->priv->vpc_mutex); + + return write_ec_cmd(priv->priv->adev->handle, opcode, !blocked); +} + +static const struct rfkill_ops ideapad_rfk_ops = { + .set_block = ideapad_rfk_set, +}; + +static void ideapad_sync_rfk_state(struct ideapad_private *priv) +{ + unsigned long hw_blocked = 0; + int i; + + if (priv->features.hw_rfkill_switch) { + guard(mutex)(&priv->vpc_mutex); + + if (read_ec_data(priv->adev->handle, VPCCMD_R_RF, &hw_blocked)) + return; + hw_blocked = !hw_blocked; + } + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + if (priv->rfk[i]) + rfkill_set_hw_state(priv->rfk[i], hw_blocked); +} + +static int ideapad_register_rfkill(struct ideapad_private *priv, int dev) +{ + unsigned long rf_enabled; + int err; + + if (no_bt_rfkill && ideapad_rfk_data[dev].type == RFKILL_TYPE_BLUETOOTH) { + /* Force to enable bluetooth when no_bt_rfkill=1 */ + write_ec_cmd(priv->adev->handle, ideapad_rfk_data[dev].opcode, 1); + return 0; + } + + priv->rfk_priv[dev].dev = dev; + priv->rfk_priv[dev].priv = priv; + + priv->rfk[dev] = rfkill_alloc(ideapad_rfk_data[dev].name, + &priv->platform_device->dev, + ideapad_rfk_data[dev].type, + &ideapad_rfk_ops, + &priv->rfk_priv[dev]); + if (!priv->rfk[dev]) + return -ENOMEM; + + err = read_ec_data(priv->adev->handle, ideapad_rfk_data[dev].opcode - 1, &rf_enabled); + if (err) + rf_enabled = 1; + + rfkill_init_sw_state(priv->rfk[dev], !rf_enabled); + + err = rfkill_register(priv->rfk[dev]); + if (err) + rfkill_destroy(priv->rfk[dev]); + + return err; +} + +static void ideapad_unregister_rfkill(struct ideapad_private *priv, int dev) +{ + if (!priv->rfk[dev]) + return; + + rfkill_unregister(priv->rfk[dev]); + rfkill_destroy(priv->rfk[dev]); +} + +/* + * input device + */ +#define IDEAPAD_WMI_KEY 0x100 + +static const struct key_entry ideapad_keymap[] = { + { KE_KEY, 6, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 7, { KEY_CAMERA } }, + { KE_KEY, 8, { KEY_MICMUTE } }, + { KE_KEY, 11, { KEY_F16 } }, + { KE_KEY, 13, { KEY_WLAN } }, + { KE_KEY, 16, { KEY_PROG1 } }, + { KE_KEY, 17, { KEY_PROG2 } }, + { KE_KEY, 64, { KEY_PROG3 } }, + { KE_KEY, 65, { KEY_PROG4 } }, + { KE_KEY, 66, { KEY_TOUCHPAD_OFF } }, + { KE_KEY, 67, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 128, { KEY_ESC } }, + + /* + * WMI keys + */ + + /* FnLock (handled by the firmware) */ + { KE_IGNORE, 0x02 | IDEAPAD_WMI_KEY }, + /* Esc (handled by the firmware) */ + { KE_IGNORE, 0x03 | IDEAPAD_WMI_KEY }, + /* Customizable Lenovo Hotkey ("star" with 'S' inside) */ + { KE_KEY, 0x01 | IDEAPAD_WMI_KEY, { KEY_FAVORITES } }, + { KE_KEY, 0x04 | IDEAPAD_WMI_KEY, { KEY_SELECTIVE_SCREENSHOT } }, + /* Lenovo Support */ + { KE_KEY, 0x07 | IDEAPAD_WMI_KEY, { KEY_HELP } }, + { KE_KEY, 0x0e | IDEAPAD_WMI_KEY, { KEY_PICKUP_PHONE } }, + { KE_KEY, 0x0f | IDEAPAD_WMI_KEY, { KEY_HANGUP_PHONE } }, + /* Refresh Rate Toggle (Fn+R) */ + { KE_KEY, 0x10 | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, + /* Dark mode toggle */ + { KE_KEY, 0x13 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, + /* Sound profile switch */ + { KE_KEY, 0x12 | IDEAPAD_WMI_KEY, { KEY_PROG2 } }, + /* Lenovo Virtual Background application */ + { KE_KEY, 0x28 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, + /* Lenovo Support */ + { KE_KEY, 0x27 | IDEAPAD_WMI_KEY, { KEY_HELP } }, + /* Refresh Rate Toggle */ + { KE_KEY, 0x0a | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, + /* Specific to some newer models */ + { KE_KEY, 0x3e | IDEAPAD_WMI_KEY, { KEY_MICMUTE } }, + { KE_KEY, 0x3f | IDEAPAD_WMI_KEY, { KEY_RFKILL } }, + /* Star- (User Assignable Key) */ + { KE_KEY, 0x44 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, + /* Eye */ + { KE_KEY, 0x45 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, + /* Performance toggle also Fn+Q, handled inside ideapad_wmi_notify() */ + { KE_KEY, 0x3d | IDEAPAD_WMI_KEY, { KEY_PROG4 } }, + /* shift + prtsc */ + { KE_KEY, 0x2d | IDEAPAD_WMI_KEY, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_KEY, 0x29 | IDEAPAD_WMI_KEY, { KEY_TOUCHPAD_TOGGLE } }, + { KE_KEY, 0x2a | IDEAPAD_WMI_KEY, { KEY_ROOT_MENU } }, + + { KE_END }, +}; + +static int ideapad_input_init(struct ideapad_private *priv) +{ + struct input_dev *inputdev; + int err; + + inputdev = input_allocate_device(); + if (!inputdev) + return -ENOMEM; + + inputdev->name = "Ideapad extra buttons"; + inputdev->phys = "ideapad/input0"; + inputdev->id.bustype = BUS_HOST; + inputdev->dev.parent = &priv->platform_device->dev; + + err = sparse_keymap_setup(inputdev, ideapad_keymap, NULL); + if (err) { + dev_err(&priv->platform_device->dev, + "Could not set up input device keymap: %d\n", err); + goto err_free_dev; + } + + err = input_register_device(inputdev); + if (err) { + dev_err(&priv->platform_device->dev, + "Could not register input device: %d\n", err); + goto err_free_dev; + } + + priv->inputdev = inputdev; + + return 0; + +err_free_dev: + input_free_device(inputdev); + + return err; +} + +static void ideapad_input_exit(struct ideapad_private *priv) +{ + input_unregister_device(priv->inputdev); + priv->inputdev = NULL; +} + +static void ideapad_input_report(struct ideapad_private *priv, + unsigned long scancode) +{ + sparse_keymap_report_event(priv->inputdev, scancode, 1, true); +} + +static void ideapad_input_novokey(struct ideapad_private *priv) +{ + unsigned long long_pressed; + + scoped_guard(mutex, &priv->vpc_mutex) + if (read_ec_data(priv->adev->handle, VPCCMD_R_NOVO, &long_pressed)) + return; + + if (long_pressed) + ideapad_input_report(priv, 17); + else + ideapad_input_report(priv, 16); +} + +static void ideapad_check_special_buttons(struct ideapad_private *priv) +{ + unsigned long bit, value; + + scoped_guard(mutex, &priv->vpc_mutex) + if (read_ec_data(priv->adev->handle, VPCCMD_R_SPECIAL_BUTTONS, &value)) + return; + + for_each_set_bit(bit, &value, 16) { + switch (bit) { + case 6: /* Z570 */ + case 0: /* Z580 */ + /* Thermal Management / Performance Mode button */ + if (priv->dytc) + platform_profile_cycle(); + else + ideapad_input_report(priv, 65); + break; + case 1: + /* OneKey Theater button */ + ideapad_input_report(priv, 64); + break; + default: + dev_info(&priv->platform_device->dev, + "Unknown special button: %lu\n", bit); + break; + } + } +} + +/* + * backlight + */ +static int ideapad_backlight_get_brightness(struct backlight_device *blightdev) +{ + struct ideapad_private *priv = bl_get_data(blightdev); + unsigned long now; + int err; + + guard(mutex)(&priv->vpc_mutex); + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + if (err) + return err; + + return now; +} + +static int ideapad_backlight_update_status(struct backlight_device *blightdev) +{ + struct ideapad_private *priv = bl_get_data(blightdev); + int err; + + guard(mutex)(&priv->vpc_mutex); + + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL, + blightdev->props.brightness); + if (err) + return err; + + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL_POWER, + blightdev->props.power != BACKLIGHT_POWER_OFF); + if (err) + return err; + + return 0; +} + +static const struct backlight_ops ideapad_backlight_ops = { + .get_brightness = ideapad_backlight_get_brightness, + .update_status = ideapad_backlight_update_status, +}; + +static int ideapad_backlight_init(struct ideapad_private *priv) +{ + struct backlight_device *blightdev; + struct backlight_properties props; + unsigned long max, now, power; + int err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &max); + if (err) + return err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + if (err) + return err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power); + if (err) + return err; + + memset(&props, 0, sizeof(props)); + + props.max_brightness = max; + props.type = BACKLIGHT_PLATFORM; + + blightdev = backlight_device_register("ideapad", + &priv->platform_device->dev, + priv, + &ideapad_backlight_ops, + &props); + if (IS_ERR(blightdev)) { + err = PTR_ERR(blightdev); + dev_err(&priv->platform_device->dev, + "Could not register backlight device: %d\n", err); + return err; + } + + priv->blightdev = blightdev; + blightdev->props.brightness = now; + blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; + + backlight_update_status(blightdev); + + return 0; +} + +static void ideapad_backlight_exit(struct ideapad_private *priv) +{ + backlight_device_unregister(priv->blightdev); + priv->blightdev = NULL; +} + +static void ideapad_backlight_notify_power(struct ideapad_private *priv) +{ + struct backlight_device *blightdev = priv->blightdev; + unsigned long power; + + if (!blightdev) + return; + + guard(mutex)(&priv->vpc_mutex); + + if (read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power)) + return; + + blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; +} + +static void ideapad_backlight_notify_brightness(struct ideapad_private *priv) +{ + unsigned long now; + + /* if we control brightness via acpi video driver */ + if (!priv->blightdev) + scoped_guard(mutex, &priv->vpc_mutex) + read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + else + backlight_force_update(priv->blightdev, BACKLIGHT_UPDATE_HOTKEY); +} + +/* + * keyboard backlight + */ +static int ideapad_kbd_bl_check_tristate(int type) +{ + return (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO); +} + +static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv) +{ + unsigned long value; + int err; + + if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) { + err = eval_kblc(priv->adev->handle, + FIELD_PREP(KBD_BL_COMMAND_TYPE, priv->kbd_bl.type) | + KBD_BL_COMMAND_GET, + &value); + + if (err) + return err; + + /* Convert returned value to brightness level */ + value = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value); + + /* Off, low or high */ + if (value <= priv->kbd_bl.led.max_brightness) + return value; + + /* Auto, report as off */ + if (value == priv->kbd_bl.led.max_brightness + 1) + return 0; + + /* Unknown value */ + dev_warn(&priv->platform_device->dev, + "Unknown keyboard backlight value: %lu", value); + return -EINVAL; + } + + err = eval_hals(priv->adev->handle, &value); + if (err) + return err; + + return !!test_bit(HALS_KBD_BL_STATE_BIT, &value); +} + +static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); + + return ideapad_kbd_bl_brightness_get(priv); +} + +static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness) +{ + int err; + unsigned long value; + int type = priv->kbd_bl.type; + + if (ideapad_kbd_bl_check_tristate(type)) { + if (brightness > priv->kbd_bl.led.max_brightness) + return -EINVAL; + + value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) | + FIELD_PREP(KBD_BL_COMMAND_TYPE, type) | + KBD_BL_COMMAND_SET; + err = exec_kblc(priv->adev->handle, value); + } else { + err = exec_sals(priv->adev->handle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF); + } + + if (err) + return err; + + priv->kbd_bl.last_brightness = brightness; + + return 0; +} + +static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); + + return ideapad_kbd_bl_brightness_set(priv, brightness); +} + +static void ideapad_kbd_bl_notify(struct ideapad_private *priv) +{ + int brightness; + + if (!priv->kbd_bl.initialized) + return; + + brightness = ideapad_kbd_bl_brightness_get(priv); + if (brightness < 0) + return; + + if (brightness == priv->kbd_bl.last_brightness) + return; + + priv->kbd_bl.last_brightness = brightness; + + led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness); +} + +static int ideapad_kbd_bl_init(struct ideapad_private *priv) +{ + int brightness, err; + + if (!priv->features.kbd_bl) + return -ENODEV; + + if (WARN_ON(priv->kbd_bl.initialized)) + return -EEXIST; + + if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) + priv->kbd_bl.led.max_brightness = 2; + else + priv->kbd_bl.led.max_brightness = 1; + + brightness = ideapad_kbd_bl_brightness_get(priv); + if (brightness < 0) + return brightness; + + priv->kbd_bl.last_brightness = brightness; + priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT; + priv->kbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get; + priv->kbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set; + priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED | LED_RETAIN_AT_SHUTDOWN; + + err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led); + if (err) + return err; + + priv->kbd_bl.initialized = true; + + return 0; +} + +static void ideapad_kbd_bl_exit(struct ideapad_private *priv) +{ + if (!priv->kbd_bl.initialized) + return; + + priv->kbd_bl.initialized = false; + + led_classdev_unregister(&priv->kbd_bl.led); +} + +/* + * FnLock LED + */ +static enum led_brightness ideapad_fn_lock_led_cdev_get(struct led_classdev *led_cdev) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); + + return ideapad_fn_lock_get(priv); +} + +static int ideapad_fn_lock_led_cdev_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); + + return ideapad_fn_lock_set(priv, brightness); +} + +static int ideapad_fn_lock_led_init(struct ideapad_private *priv) +{ + int brightness, err; + + if (!priv->features.fn_lock) + return -ENODEV; + + if (WARN_ON(priv->fn_lock.initialized)) + return -EEXIST; + + priv->fn_lock.led.max_brightness = 1; + + brightness = ideapad_fn_lock_get(priv); + if (brightness < 0) + return brightness; + + priv->fn_lock.last_brightness = brightness; + priv->fn_lock.led.name = "platform::" LED_FUNCTION_FNLOCK; + priv->fn_lock.led.brightness_get = ideapad_fn_lock_led_cdev_get; + priv->fn_lock.led.brightness_set_blocking = ideapad_fn_lock_led_cdev_set; + priv->fn_lock.led.flags = LED_BRIGHT_HW_CHANGED | LED_RETAIN_AT_SHUTDOWN; + + err = led_classdev_register(&priv->platform_device->dev, &priv->fn_lock.led); + if (err) + return err; + + priv->fn_lock.initialized = true; + + return 0; +} + +static void ideapad_fn_lock_led_exit(struct ideapad_private *priv) +{ + if (!priv->fn_lock.initialized) + return; + + priv->fn_lock.initialized = false; + + led_classdev_unregister(&priv->fn_lock.led); +} + +/* + * module init/exit + */ +static void ideapad_sync_touchpad_state(struct ideapad_private *priv, bool send_events) +{ + unsigned long value; + unsigned char param; + int ret; + + /* Without reading from EC touchpad LED doesn't switch state */ + scoped_guard(mutex, &priv->vpc_mutex) + ret = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value); + if (ret) + return; + + /* + * Some IdeaPads don't really turn off touchpad - they only + * switch the LED state. We (de)activate KBC AUX port to turn + * touchpad off and on. We send KEY_TOUCHPAD_OFF and + * KEY_TOUCHPAD_ON to not to get out of sync with LED + */ + if (priv->features.ctrl_ps2_aux_port) + i8042_command(¶m, value ? I8042_CMD_AUX_ENABLE : I8042_CMD_AUX_DISABLE); + + /* + * On older models the EC controls the touchpad and toggles it on/off + * itself, in this case we report KEY_TOUCHPAD_ON/_OFF. Some models do + * an acpi-notify with VPC bit 5 set on resume, so this function get + * called with send_events=true on every resume. Therefor if the EC did + * not toggle, do nothing to avoid sending spurious KEY_TOUCHPAD_TOGGLE. + */ + if (send_events && value != priv->r_touchpad_val) { + ideapad_input_report(priv, value ? 67 : 66); + sysfs_notify(&priv->platform_device->dev.kobj, NULL, "touchpad"); + } + + priv->r_touchpad_val = value; +} + +static const struct dmi_system_id ymc_ec_trigger_quirk_dmi_table[] = { + { + /* Lenovo Yoga 7 14ARB7 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82QF"), + }, + }, + { + /* Lenovo Yoga 7 14ACN6 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82N7"), + }, + }, + { } +}; + +static void ideapad_laptop_trigger_ec(void) +{ + struct ideapad_private *priv; + int ret; + + guard(mutex)(&ideapad_shared_mutex); + + priv = ideapad_shared; + if (!priv) + return; + + if (!priv->features.ymc_ec_trigger) + return; + + scoped_guard(mutex, &priv->vpc_mutex) + ret = write_ec_cmd(priv->adev->handle, VPCCMD_W_YMC, 1); + if (ret) + dev_warn(&priv->platform_device->dev, "Could not write YMC: %d\n", ret); +} + +static int ideapad_laptop_nb_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + switch (action) { + case IDEAPAD_LAPTOP_YMC_EVENT: + ideapad_laptop_trigger_ec(); + break; + } + + return 0; +} + +static struct notifier_block ideapad_laptop_notifier = { + .notifier_call = ideapad_laptop_nb_notify, +}; + +static BLOCKING_NOTIFIER_HEAD(ideapad_laptop_chain_head); + +int ideapad_laptop_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ideapad_laptop_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_register_notifier, "IDEAPAD_LAPTOP"); + +int ideapad_laptop_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&ideapad_laptop_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_unregister_notifier, "IDEAPAD_LAPTOP"); + +void ideapad_laptop_call_notifier(unsigned long action, void *data) +{ + blocking_notifier_call_chain(&ideapad_laptop_chain_head, action, data); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_call_notifier, "IDEAPAD_LAPTOP"); + +static void ideapad_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct ideapad_private *priv = data; + unsigned long vpc1, vpc2, bit; + + scoped_guard(mutex, &priv->vpc_mutex) { + if (read_ec_data(handle, VPCCMD_R_VPC1, &vpc1)) + return; + + if (read_ec_data(handle, VPCCMD_R_VPC2, &vpc2)) + return; + } + + vpc1 = (vpc2 << 8) | vpc1; + + for_each_set_bit(bit, &vpc1, 16) { + switch (bit) { + case 13: + case 11: + case 8: + case 7: + case 6: + ideapad_input_report(priv, bit); + break; + case 10: + /* + * This event gets send on a Yoga 300-11IBR when the EC + * believes that the device has changed between laptop/ + * tent/stand/tablet mode. The EC relies on getting + * angle info from 2 accelerometers through a special + * windows service calling a DSM on the DUAL250E ACPI- + * device. Linux does not do this, making the laptop/ + * tent/stand/tablet mode info unreliable, so we simply + * ignore these events. + */ + break; + case 9: + ideapad_sync_rfk_state(priv); + break; + case 5: + ideapad_sync_touchpad_state(priv, true); + break; + case 4: + ideapad_backlight_notify_brightness(priv); + break; + case 3: + ideapad_input_novokey(priv); + break; + case 2: + ideapad_backlight_notify_power(priv); + break; + case KBD_BL_KBLC_CHANGED_EVENT: + case 1: + /* + * Some IdeaPads report event 1 every ~20 + * seconds while on battery power; some + * report this when changing to/from tablet + * mode; some report this when the keyboard + * backlight has changed. + */ + ideapad_kbd_bl_notify(priv); + break; + case 0: + ideapad_check_special_buttons(priv); + break; + default: + dev_info(&priv->platform_device->dev, + "Unknown event: %lu\n", bit); + } + } +} + +/* On some models we need to call exec_sals(SALS_FNLOCK_ON/OFF) to set the LED */ +static const struct dmi_system_id set_fn_lock_led_list[] = { + { + /* https://bugzilla.kernel.org/show_bug.cgi?id=212671 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion R7000P2020H"), + } + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion 5 15ARH05"), + } + }, + {} +}; + +/* + * Some ideapads have a hardware rfkill switch, but most do not have one. + * Reading VPCCMD_R_RF always results in 0 on models without a hardware rfkill, + * switch causing ideapad_laptop to wrongly report all radios as hw-blocked. + * There used to be a long list of DMI ids for models without a hw rfkill + * switch here, but that resulted in playing whack a mole. + * More importantly wrongly reporting the wifi radio as hw-blocked, results in + * non working wifi. Whereas not reporting it hw-blocked, when it actually is + * hw-blocked results in an empty SSID list, which is a much more benign + * failure mode. + * So the default now is the much safer option of assuming there is no + * hardware rfkill switch. This default also actually matches most hardware, + * since having a hw rfkill switch is quite rare on modern hardware, so this + * also leads to a much shorter list. + */ +static const struct dmi_system_id hw_rfkill_list[] = { + {} +}; + +/* + * On some models the EC toggles the touchpad muted LED on touchpad toggle + * hotkey presses, but the EC does not actually disable the touchpad itself. + * On these models the driver needs to explicitly enable/disable the i8042 + * (PS/2) aux port. + */ +static const struct dmi_system_id ctrl_ps2_aux_port_list[] = { + { + /* Lenovo Ideapad Z570 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Ideapad Z570"), + }, + }, + {} +}; + +static int ideapad_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct ideapad_private *priv = ext_data; + unsigned long op1, op2; + int err; + + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_FAST: + if (WARN_ON(!priv->features.rapid_charge)) + return -EINVAL; + + op1 = SBMC_CONSERVATION_OFF; + op2 = SBMC_RAPID_CHARGE_ON; + break; + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + op1 = SBMC_RAPID_CHARGE_OFF; + op2 = SBMC_CONSERVATION_ON; + break; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + op1 = SBMC_RAPID_CHARGE_OFF; + op2 = SBMC_CONSERVATION_OFF; + break; + default: + return -EINVAL; + } + + guard(mutex)(&priv->gbmd_sbmc_mutex); + + /* If !rapid_charge, op1 must be SBMC_RAPID_CHARGE_OFF. Skip it. */ + if (priv->features.rapid_charge) { + err = exec_sbmc(priv->adev->handle, op1); + if (err) + return err; + } + + return exec_sbmc(priv->adev->handle, op2); +} + +static int ideapad_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ideapad_private *priv = ext_data; + bool is_rapid_charge, is_conservation; + unsigned long result; + int err; + + scoped_guard(mutex, &priv->gbmd_sbmc_mutex) { + err = eval_gbmd(priv->adev->handle, &result); + if (err) + return err; + } + + is_rapid_charge = (priv->features.rapid_charge && + test_bit(GBMD_RAPID_CHARGE_STATE_BIT, &result)); + is_conservation = test_bit(GBMD_CONSERVATION_STATE_BIT, &result); + + if (unlikely(is_rapid_charge && is_conservation)) { + dev_err(&priv->platform_device->dev, + "unexpected charge_types: both [Fast] and [Long_Life] are enabled\n"); + return -EINVAL; + } + + if (is_rapid_charge) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + else if (is_conservation) + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + + return 0; +} + +static int ideapad_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 ideapad_power_supply_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +#define DEFINE_IDEAPAD_POWER_SUPPLY_EXTENSION(_name, _charge_types) \ + static const struct power_supply_ext _name = { \ + .name = "ideapad_laptop", \ + .properties = ideapad_power_supply_props, \ + .num_properties = ARRAY_SIZE(ideapad_power_supply_props), \ + .charge_types = _charge_types, \ + .get_property = ideapad_psy_ext_get_prop, \ + .set_property = ideapad_psy_ext_set_prop, \ + .property_is_writeable = ideapad_psy_prop_is_writeable, \ + } + +DEFINE_IDEAPAD_POWER_SUPPLY_EXTENSION(ideapad_battery_ext_v1, + (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)) +); + +DEFINE_IDEAPAD_POWER_SUPPLY_EXTENSION(ideapad_battery_ext_v2, + (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_FAST) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)) +); + +static int ideapad_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct ideapad_private *priv = container_of(hook, struct ideapad_private, battery_hook); + + return power_supply_register_extension(battery, priv->battery_ext, + &priv->platform_device->dev, priv); +} + +static int ideapad_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + struct ideapad_private *priv = container_of(hook, struct ideapad_private, battery_hook); + + power_supply_unregister_extension(battery, priv->battery_ext); + + return 0; +} + +static int ideapad_check_features(struct ideapad_private *priv) +{ + acpi_handle handle = priv->adev->handle; + unsigned long val; + int err; + + priv->features.set_fn_lock_led = + set_fn_lock_led || dmi_check_system(set_fn_lock_led_list); + priv->features.hw_rfkill_switch = + hw_rfkill_switch || dmi_check_system(hw_rfkill_list); + priv->features.ctrl_ps2_aux_port = + ctrl_ps2_aux_port || dmi_check_system(ctrl_ps2_aux_port_list); + priv->features.touchpad_ctrl_via_ec = touchpad_ctrl_via_ec; + priv->features.ymc_ec_trigger = + ymc_ec_trigger || dmi_check_system(ymc_ec_trigger_quirk_dmi_table); + + if (!read_ec_data(handle, VPCCMD_R_FAN, &val)) + priv->features.fan_mode = true; + + if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) { + /* Not acquiring gbmd_sbmc_mutex as race condition is impossible on init */ + if (!eval_gbmd(handle, &val)) { + priv->features.conservation_mode = true; + priv->features.rapid_charge = test_bit(GBMD_RAPID_CHARGE_SUPPORTED_BIT, + &val); + + priv->battery_ext = priv->features.rapid_charge + ? &ideapad_battery_ext_v2 + : &ideapad_battery_ext_v1; + + priv->battery_hook.add_battery = ideapad_battery_add; + priv->battery_hook.remove_battery = ideapad_battery_remove; + priv->battery_hook.name = "Ideapad Battery Extension"; + + err = devm_battery_hook_register(&priv->platform_device->dev, + &priv->battery_hook); + if (err) + return err; + } + } + + if (acpi_has_method(handle, "DYTC")) + priv->features.dytc = true; + + if (acpi_has_method(handle, "HALS") && acpi_has_method(handle, "SALS")) { + if (!eval_hals(handle, &val)) { + if (test_bit(HALS_FNLOCK_SUPPORT_BIT, &val)) + priv->features.fn_lock = true; + + if (test_bit(HALS_KBD_BL_SUPPORT_BIT, &val)) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_STANDARD; + } + + if (test_bit(HALS_USB_CHARGING_SUPPORT_BIT, &val)) + priv->features.usb_charging = true; + } + } + + if (acpi_has_method(handle, "KBLC")) { + if (!eval_kblc(priv->adev->handle, KBD_BL_QUERY_TYPE, &val)) { + if (val == KBD_BL_TRISTATE_TYPE) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_TRISTATE; + } else if (val == KBD_BL_TRISTATE_AUTO_TYPE) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_TRISTATE_AUTO; + } else { + dev_warn(&priv->platform_device->dev, + "Unknown keyboard type: %lu", + val); + } + } + } + + return 0; +} + +#if IS_ENABLED(CONFIG_ACPI_WMI) +/* + * WMI driver + */ +enum ideapad_wmi_event_type { + IDEAPAD_WMI_EVENT_ESC, + IDEAPAD_WMI_EVENT_FN_KEYS, +}; + +struct ideapad_wmi_private { + enum ideapad_wmi_event_type event; +}; + +static int ideapad_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct ideapad_wmi_private *wpriv; + + wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL); + if (!wpriv) + return -ENOMEM; + + *wpriv = *(const struct ideapad_wmi_private *)context; + + dev_set_drvdata(&wdev->dev, wpriv); + return 0; +} + +static void ideapad_wmi_notify(struct wmi_device *wdev, union acpi_object *data) +{ + struct ideapad_wmi_private *wpriv = dev_get_drvdata(&wdev->dev); + struct ideapad_private *priv; + + guard(mutex)(&ideapad_shared_mutex); + + priv = ideapad_shared; + if (!priv) + return; + + switch (wpriv->event) { + case IDEAPAD_WMI_EVENT_ESC: + ideapad_input_report(priv, 128); + break; + case IDEAPAD_WMI_EVENT_FN_KEYS: + if (priv->features.set_fn_lock_led) { + int brightness = ideapad_fn_lock_get(priv); + + if (brightness >= 0) { + ideapad_fn_lock_set(priv, brightness); + ideapad_fn_lock_led_notify(priv, brightness); + } + } + + if (data->type != ACPI_TYPE_INTEGER) { + dev_warn(&wdev->dev, + "WMI event data is not an integer\n"); + break; + } + + dev_dbg(&wdev->dev, "WMI fn-key event: 0x%llx\n", + data->integer.value); + + /* performance button triggered by 0x3d */ + if (data->integer.value == 0x3d && priv->dytc) { + platform_profile_cycle(); + break; + } + + /* 0x02 FnLock, 0x03 Esc */ + if (data->integer.value == 0x02 || data->integer.value == 0x03) + ideapad_fn_lock_led_notify(priv, data->integer.value == 0x02); + + ideapad_input_report(priv, + data->integer.value | IDEAPAD_WMI_KEY); + + break; + } +} + +static const struct ideapad_wmi_private ideapad_wmi_context_esc = { + .event = IDEAPAD_WMI_EVENT_ESC +}; + +static const struct ideapad_wmi_private ideapad_wmi_context_fn_keys = { + .event = IDEAPAD_WMI_EVENT_FN_KEYS +}; + +static const struct wmi_device_id ideapad_wmi_ids[] = { + { "26CAB2E5-5CF1-46AE-AAC3-4A12B6BA50E6", &ideapad_wmi_context_esc }, /* Yoga 3 */ + { "56322276-8493-4CE8-A783-98C991274F5E", &ideapad_wmi_context_esc }, /* Yoga 700 */ + { "8FC0DE0C-B4E4-43FD-B0F3-8871711C1294", &ideapad_wmi_context_fn_keys }, /* Legion 5 */ + {}, +}; +MODULE_DEVICE_TABLE(wmi, ideapad_wmi_ids); + +static struct wmi_driver ideapad_wmi_driver = { + .driver = { + .name = "ideapad_wmi", + }, + .id_table = ideapad_wmi_ids, + .min_event_size = sizeof(u32), + .probe = ideapad_wmi_probe, + .notify = ideapad_wmi_notify, +}; + +static int ideapad_wmi_driver_register(void) +{ + return wmi_driver_register(&ideapad_wmi_driver); +} + +static void ideapad_wmi_driver_unregister(void) +{ + return wmi_driver_unregister(&ideapad_wmi_driver); +} + +#else +static inline int ideapad_wmi_driver_register(void) { return 0; } +static inline void ideapad_wmi_driver_unregister(void) { } +#endif + +/* + * ACPI driver + */ +static int ideapad_acpi_add(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct ideapad_private *priv; + acpi_status status; + unsigned long cfg; + int err, i; + + if (!adev || eval_int(adev->handle, "_CFG", &cfg)) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(&pdev->dev, priv); + + priv->cfg = cfg; + priv->adev = adev; + priv->platform_device = pdev; + + err = devm_mutex_init(&pdev->dev, &priv->vpc_mutex); + if (err) + return err; + + err = devm_mutex_init(&pdev->dev, &priv->gbmd_sbmc_mutex); + if (err) + return err; + + err = ideapad_check_features(priv); + if (err) + return err; + + ideapad_debugfs_init(priv); + + err = ideapad_input_init(priv); + if (err) + goto input_failed; + + err = ideapad_kbd_bl_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up keyboard backlight LED: %d\n", err); + else + dev_info(&pdev->dev, "Keyboard backlight control not available\n"); + } + + err = ideapad_fn_lock_led_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up FnLock LED: %d\n", err); + else + dev_info(&pdev->dev, "FnLock control not available\n"); + } + + /* + * On some models without a hw-switch (the yoga 2 13 at least) + * VPCCMD_W_RF must be explicitly set to 1 for the wifi to work. + */ + if (!priv->features.hw_rfkill_switch) + write_ec_cmd(priv->adev->handle, VPCCMD_W_RF, 1); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + if (test_bit(ideapad_rfk_data[i].cfgbit, &priv->cfg)) + ideapad_register_rfkill(priv, i); + + ideapad_sync_rfk_state(priv); + ideapad_sync_touchpad_state(priv, false); + + err = ideapad_dytc_profile_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up DYTC interface: %d\n", err); + else + dev_info(&pdev->dev, "DYTC interface is not available\n"); + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + err = ideapad_backlight_init(priv); + if (err && err != -ENODEV) + goto backlight_failed; + } + + status = acpi_install_notify_handler(adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify, priv); + if (ACPI_FAILURE(status)) { + err = -EIO; + goto notification_failed; + } + + err = ideapad_shared_init(priv); + if (err) + goto shared_init_failed; + + ideapad_laptop_register_notifier(&ideapad_laptop_notifier); + + return 0; + +shared_init_failed: + acpi_remove_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify); + +notification_failed: + ideapad_backlight_exit(priv); + +backlight_failed: + ideapad_dytc_profile_exit(priv); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + ideapad_unregister_rfkill(priv, i); + + ideapad_fn_lock_led_exit(priv); + ideapad_kbd_bl_exit(priv); + ideapad_input_exit(priv); + +input_failed: + ideapad_debugfs_exit(priv); + + return err; +} + +static void ideapad_acpi_remove(struct platform_device *pdev) +{ + struct ideapad_private *priv = dev_get_drvdata(&pdev->dev); + int i; + + ideapad_laptop_unregister_notifier(&ideapad_laptop_notifier); + + ideapad_shared_exit(priv); + + acpi_remove_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify); + + ideapad_backlight_exit(priv); + ideapad_dytc_profile_exit(priv); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + ideapad_unregister_rfkill(priv, i); + + ideapad_fn_lock_led_exit(priv); + ideapad_kbd_bl_exit(priv); + ideapad_input_exit(priv); + ideapad_debugfs_exit(priv); +} + +#ifdef CONFIG_PM_SLEEP +static int ideapad_acpi_resume(struct device *dev) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + + ideapad_sync_rfk_state(priv); + ideapad_sync_touchpad_state(priv, false); + + if (priv->dytc) + dytc_profile_refresh(priv); + + return 0; +} +#endif +static SIMPLE_DEV_PM_OPS(ideapad_pm, NULL, ideapad_acpi_resume); + +static const struct acpi_device_id ideapad_device_ids[] = { + {"VPC2004", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, ideapad_device_ids); + +static struct platform_driver ideapad_acpi_driver = { + .probe = ideapad_acpi_add, + .remove = ideapad_acpi_remove, + .driver = { + .name = "ideapad_acpi", + .pm = &ideapad_pm, + .acpi_match_table = ACPI_PTR(ideapad_device_ids), + .dev_groups = ideapad_attribute_groups, + }, +}; + +static int __init ideapad_laptop_init(void) +{ + int err; + + err = ideapad_wmi_driver_register(); + if (err) + return err; + + err = platform_driver_register(&ideapad_acpi_driver); + if (err) { + ideapad_wmi_driver_unregister(); + return err; + } + + return 0; +} +module_init(ideapad_laptop_init) + +static void __exit ideapad_laptop_exit(void) +{ + ideapad_wmi_driver_unregister(); + platform_driver_unregister(&ideapad_acpi_driver); +} +module_exit(ideapad_laptop_exit) + +MODULE_AUTHOR("David Woodhouse "); +MODULE_DESCRIPTION("IdeaPad ACPI Extras"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.h b/drivers/platform/x86/lenovo/ideapad-laptop.h new file mode 100644 index 000000000..1e52f2aa0 --- /dev/null +++ b/drivers/platform/x86/lenovo/ideapad-laptop.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * ideapad-laptop.h - Lenovo IdeaPad ACPI Extras + * + * Copyright © 2010 Intel Corporation + * Copyright © 2010 David Woodhouse + */ + +#ifndef _IDEAPAD_LAPTOP_H_ +#define _IDEAPAD_LAPTOP_H_ + +#include + +enum ideapad_laptop_notifier_actions { + IDEAPAD_LAPTOP_YMC_EVENT, +}; + +int ideapad_laptop_register_notifier(struct notifier_block *nb); +int ideapad_laptop_unregister_notifier(struct notifier_block *nb); +void ideapad_laptop_call_notifier(unsigned long action, void *data); + +#endif /* !_IDEAPAD_LAPTOP_H_ */ diff --git a/drivers/platform/x86/lenovo/think-lmi.c b/drivers/platform/x86/lenovo/think-lmi.c new file mode 100644 index 000000000..e215e86e3 --- /dev/null +++ b/drivers/platform/x86/lenovo/think-lmi.c @@ -0,0 +1,1859 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Think LMI BIOS configuration driver + * + * Copyright(C) 2019-2021 Lenovo + * + * Original code from Thinkpad-wmi project https://github.com/iksaif/thinkpad-wmi + * Copyright(C) 2017 Corentin Chary + * Distributed under the GPL-2.0 license + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../firmware_attributes_class.h" +#include "think-lmi.h" + +static bool debug_support; +module_param(debug_support, bool, 0444); +MODULE_PARM_DESC(debug_support, "Enable debug command support"); + +/* + * Name: BiosSetting + * Description: Get item name and settings for current LMI instance. + * Type: Query + * Returns: "Item,Value" + * Example: "WakeOnLAN,Enable" + */ +#define LENOVO_BIOS_SETTING_GUID "51F5230E-9677-46CD-A1CF-C0B23EE34DB7" + +/* + * Name: SetBiosSetting + * Description: Change the BIOS setting to the desired value using the SetBiosSetting + * class. To save the settings, use the SaveBiosSetting class. + * BIOS settings and values are case sensitive. + * After making changes to the BIOS settings, you must reboot the computer + * before the changes will take effect. + * Type: Method + * Arguments: "Item,Value,Password,Encoding,KbdLang;" + * Example: "WakeOnLAN,Disable,pa55w0rd,ascii,us;" + */ +#define LENOVO_SET_BIOS_SETTINGS_GUID "98479A64-33F5-4E33-A707-8E251EBBC3A1" + +/* + * Name: SaveBiosSettings + * Description: Save any pending changes in settings. + * Type: Method + * Arguments: "Password,Encoding,KbdLang;" + * Example: "pa55w0rd,ascii,us;" + */ +#define LENOVO_SAVE_BIOS_SETTINGS_GUID "6A4B54EF-A5ED-4D33-9455-B0D9B48DF4B3" + +/* + * Name: BiosPasswordSettings + * Description: Return BIOS Password settings + * Type: Query + * Returns: PasswordMode, PasswordState, MinLength, MaxLength, + * SupportedEncoding, SupportedKeyboard + */ +#define LENOVO_BIOS_PASSWORD_SETTINGS_GUID "8ADB159E-1E32-455C-BC93-308A7ED98246" + +/* + * Name: SetBiosPassword + * Description: Change a specific password. + * - BIOS settings cannot be changed at the same boot as power-on + * passwords (POP) and hard disk passwords (HDP). If you want to change + * BIOS settings and POP or HDP, you must reboot the system after changing + * one of them. + * - A password cannot be set using this method when one does not already + * exist. Passwords can only be updated or cleared. + * Type: Method + * Arguments: "PasswordType,CurrentPassword,NewPassword,Encoding,KbdLang;" + * Example: "pop,pa55w0rd,newpa55w0rd,ascii,us;” + */ +#define LENOVO_SET_BIOS_PASSWORD_GUID "2651D9FD-911C-4B69-B94E-D0DED5963BD7" + +/* + * Name: GetBiosSelections + * Description: Return a list of valid settings for a given item. + * Type: Method + * Arguments: "Item" + * Returns: "Value1,Value2,Value3,..." + * Example: + * -> "FlashOverLAN" + * <- "Enabled,Disabled" + */ +#define LENOVO_GET_BIOS_SELECTIONS_GUID "7364651A-132F-4FE7-ADAA-40C6C7EE2E3B" + +/* + * Name: DebugCmd + * Description: Debug entry method for entering debug commands to the BIOS + */ +#define LENOVO_DEBUG_CMD_GUID "7FF47003-3B6C-4E5E-A227-E979824A85D1" + +/* + * Name: OpcodeIF + * Description: Opcode interface which provides the ability to set multiple + * parameters and then trigger an action with a final command. + * This is particularly useful for simplifying setting passwords. + * With this support comes the ability to set System, HDD and NVMe + * passwords. + * This is currently available on ThinkCenter and ThinkStations platforms + */ +#define LENOVO_OPCODE_IF_GUID "DFDDEF2C-57D4-48ce-B196-0FB787D90836" + +/* + * Name: SetBiosCert + * Description: Install BIOS certificate. + * Type: Method + * Arguments: "Certificate,Password" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_SET_BIOS_CERT_GUID "26861C9F-47E9-44C4-BD8B-DFE7FA2610FE" +#define LENOVO_TC_SET_BIOS_CERT_GUID "955aaf7d-8bc4-4f04-90aa-97469512f167" + +/* + * Name: UpdateBiosCert + * Description: Update BIOS certificate. + * Type: Method + * Format: "Certificate,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_UPDATE_BIOS_CERT_GUID "9AA3180A-9750-41F7-B9F7-D5D3B1BAC3CE" +#define LENOVO_TC_UPDATE_BIOS_CERT_GUID "5f5bbbb2-c72f-4fb8-8129-228eef4fdbed" + +/* + * Name: ClearBiosCert + * Description: Uninstall BIOS certificate. + * Type: Method + * Format: "Serial,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_CLEAR_BIOS_CERT_GUID "B2BC39A7-78DD-4D71-B059-A510DEC44890" +#define LENOVO_TC_CLEAR_BIOS_CERT_GUID "97849cb6-cb44-42d1-a750-26a596a9eec4" + +/* + * Name: CertToPassword + * Description: Switch from certificate to password authentication. + * Type: Method + * Format: "Password,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_CERT_TO_PASSWORD_GUID "0DE8590D-5510-4044-9621-77C227F5A70D" +#define LENOVO_TC_CERT_TO_PASSWORD_GUID "ef65480d-38c9-420d-b700-ab3d6c8ebaca" + +/* + * Name: SetBiosSettingCert + * Description: Set attribute using certificate authentication. + * Type: Method + * Format: "Item,Value,Signature" + */ +#define LENOVO_SET_BIOS_SETTING_CERT_GUID "34A008CC-D205-4B62-9E67-31DFA8B90003" +#define LENOVO_TC_SET_BIOS_SETTING_CERT_GUID "19ecba3b-b318-4192-a89b-43d94bc60cea" + +/* + * Name: SaveBiosSettingCert + * Description: Save any pending changes in settings. + * Type: Method + * Format: "Signature" + */ +#define LENOVO_SAVE_BIOS_SETTING_CERT_GUID "C050FB9D-DF5F-4606-B066-9EFC401B2551" +#define LENOVO_TC_SAVE_BIOS_SETTING_CERT_GUID "0afaf46f-7cca-450a-b455-a826a0bf1af5" + +/* + * Name: CertThumbprint + * Description: Display Certificate thumbprints + * Type: Query + * Returns: MD5, SHA1 & SHA256 thumbprints + */ +#define LENOVO_CERT_THUMBPRINT_GUID "C59119ED-1C0D-4806-A8E9-59AA318176C4" + +#define TLMI_POP_PWD BIT(0) /* Supervisor */ +#define TLMI_PAP_PWD BIT(1) /* Power-on */ +#define TLMI_HDD_PWD BIT(2) /* HDD/NVME */ +#define TLMI_SMP_PWD BIT(6) /* System Management */ +#define TLMI_CERT_SVC BIT(7) /* Admin Certificate Based */ +#define TLMI_CERT_SMC BIT(8) /* System Certificate Based */ + +static const struct tlmi_cert_guids thinkpad_cert_guid = { + .thumbprint = LENOVO_CERT_THUMBPRINT_GUID, + .set_bios_setting = LENOVO_SET_BIOS_SETTING_CERT_GUID, + .save_bios_setting = LENOVO_SAVE_BIOS_SETTING_CERT_GUID, + .cert_to_password = LENOVO_CERT_TO_PASSWORD_GUID, + .clear_bios_cert = LENOVO_CLEAR_BIOS_CERT_GUID, + .update_bios_cert = LENOVO_UPDATE_BIOS_CERT_GUID, + .set_bios_cert = LENOVO_SET_BIOS_CERT_GUID, +}; + +static const struct tlmi_cert_guids thinkcenter_cert_guid = { + .thumbprint = LENOVO_CERT_THUMBPRINT_GUID, /* Same GUID as TP */ + .set_bios_setting = LENOVO_TC_SET_BIOS_SETTING_CERT_GUID, + .save_bios_setting = LENOVO_TC_SAVE_BIOS_SETTING_CERT_GUID, + .cert_to_password = LENOVO_TC_CERT_TO_PASSWORD_GUID, + .clear_bios_cert = LENOVO_TC_CLEAR_BIOS_CERT_GUID, + .update_bios_cert = LENOVO_TC_UPDATE_BIOS_CERT_GUID, + .set_bios_cert = LENOVO_TC_SET_BIOS_CERT_GUID, +}; + +static const struct tlmi_err_codes tlmi_errs[] = { + {"Success", 0}, + {"Set Certificate operation was successful.", 0}, + {"Not Supported", -EOPNOTSUPP}, + {"Invalid Parameter", -EINVAL}, + {"Access Denied", -EACCES}, + {"System Busy", -EBUSY}, + {"Set Certificate operation failed with status:Invalid Parameter.", -EINVAL}, + {"Set Certificate operation failed with status:Invalid certificate type.", -EINVAL}, + {"Set Certificate operation failed with status:Invalid password format.", -EINVAL}, + {"Set Certificate operation failed with status:Password retry count exceeded.", -EACCES}, + {"Set Certificate operation failed with status:Password Invalid.", -EACCES}, + {"Set Certificate operation failed with status:Operation aborted.", -EBUSY}, + {"Set Certificate operation failed with status:No free slots to write.", -ENOSPC}, + {"Set Certificate operation failed with status:Certificate not found.", -EEXIST}, + {"Set Certificate operation failed with status:Internal error.", -EFAULT}, + {"Set Certificate operation failed with status:Certificate too large.", -EFBIG}, +}; + +static const char * const encoding_options[] = { + [TLMI_ENCODING_ASCII] = "ascii", + [TLMI_ENCODING_SCANCODE] = "scancode", +}; +static const char * const level_options[] = { + [TLMI_LEVEL_USER] = "user", + [TLMI_LEVEL_MASTER] = "master", +}; +static struct think_lmi tlmi_priv; +static DEFINE_MUTEX(tlmi_mutex); + +static inline struct tlmi_pwd_setting *to_tlmi_pwd_setting(struct kobject *kobj) +{ + return container_of(kobj, struct tlmi_pwd_setting, kobj); +} + +static inline struct tlmi_attr_setting *to_tlmi_attr_setting(struct kobject *kobj) +{ + return container_of(kobj, struct tlmi_attr_setting, kobj); +} + +/* Convert BIOS WMI error string to suitable error code */ +static int tlmi_errstr_to_err(const char *errstr) +{ + int i; + + for (i = 0; i < sizeof(tlmi_errs)/sizeof(struct tlmi_err_codes); i++) { + if (!strcmp(tlmi_errs[i].err_str, errstr)) + return tlmi_errs[i].err_code; + } + return -EPERM; +} + +/* Extract error string from WMI return buffer */ +static int tlmi_extract_error(const struct acpi_buffer *output) +{ + const union acpi_object *obj; + + obj = output->pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + return tlmi_errstr_to_err(obj->string.pointer); +} + +/* Utility function to execute WMI call to BIOS */ +static int tlmi_simple_call(const char *guid, const char *arg) +{ + const struct acpi_buffer input = { strlen(arg), (char *)arg }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + int i, err; + + /* + * Duplicated call required to match BIOS workaround for behavior + * seen when WMI accessed via scripting on other OS. + */ + for (i = 0; i < 2; i++) { + /* (re)initialize output buffer to default state */ + output.length = ACPI_ALLOCATE_BUFFER; + output.pointer = NULL; + + status = wmi_evaluate_method(guid, 0, 0, &input, &output); + if (ACPI_FAILURE(status)) { + kfree(output.pointer); + return -EIO; + } + err = tlmi_extract_error(&output); + kfree(output.pointer); + if (err) + return err; + } + return 0; +} + +/* Extract output string from WMI return value */ +static int tlmi_extract_output_string(union acpi_object *obj, char **string) +{ + char *s; + + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + s = kstrdup(obj->string.pointer, GFP_KERNEL); + if (!s) + return -ENOMEM; + *string = s; + return 0; +} + +/* ------ Core interface functions ------------*/ + +/* Get password settings from BIOS */ +static int tlmi_get_pwd_settings(struct tlmi_pwdcfg *pwdcfg) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + const union acpi_object *obj; + acpi_status status; + int copy_size; + + if (!tlmi_priv.can_get_password_settings) + return -EOPNOTSUPP; + + status = wmi_query_block(LENOVO_BIOS_PASSWORD_SETTINGS_GUID, 0, + &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_BUFFER || !obj->buffer.pointer) { + kfree(obj); + return -EIO; + } + /* + * The size of thinkpad_wmi_pcfg on ThinkStation is larger than ThinkPad. + * To make the driver compatible on different brands, we permit it to get + * the data in below case. + * Settings must have at minimum the core fields available + */ + if (obj->buffer.length < sizeof(struct tlmi_pwdcfg_core)) { + pr_warn("Unknown pwdcfg buffer length %d\n", obj->buffer.length); + kfree(obj); + return -EIO; + } + + copy_size = min_t(size_t, obj->buffer.length, sizeof(struct tlmi_pwdcfg)); + + memcpy(pwdcfg, obj->buffer.pointer, copy_size); + kfree(obj); + + if (WARN_ON(pwdcfg->core.max_length >= TLMI_PWD_BUFSIZE)) + pwdcfg->core.max_length = TLMI_PWD_BUFSIZE - 1; + return 0; +} + +static int tlmi_save_bios_settings(const char *password) +{ + return tlmi_simple_call(LENOVO_SAVE_BIOS_SETTINGS_GUID, + password); +} + +static int tlmi_opcode_setting(char *setting, const char *value) +{ + char *opcode_str; + int ret; + + opcode_str = kasprintf(GFP_KERNEL, "%s:%s;", setting, value); + if (!opcode_str) + return -ENOMEM; + + ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, opcode_str); + kfree(opcode_str); + return ret; +} + +static int tlmi_setting(struct wmi_device *wdev, int item, char **value) +{ + union acpi_object *obj; + int ret; + + obj = wmidev_block_query(wdev, item); + if (!obj) + return -EIO; + + ret = tlmi_extract_output_string(obj, value); + kfree(obj); + + return ret; +} + +static int tlmi_get_bios_selections(const char *item, char **value) +{ + const struct acpi_buffer input = { strlen(item), (char *)item }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int ret; + + status = wmi_evaluate_method(LENOVO_GET_BIOS_SELECTIONS_GUID, + 0, 0, &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + if (!obj) + return -ENODATA; + + ret = tlmi_extract_output_string(obj, value); + kfree(obj); + + return ret; +} + +/* ---- Authentication sysfs --------------------------------------------------------- */ +static ssize_t is_enabled_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->pwd_enabled || setting->cert_installed); +} + +static struct kobj_attribute auth_is_pass_set = __ATTR_RO(is_enabled); + +static ssize_t current_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + size_t pwdlen; + + pwdlen = strlen(buf); + /* pwdlen == 0 is allowed to clear the password */ + if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) + return -EINVAL; + + strscpy(setting->password, buf, setting->maxlen); + /* Strip out CR if one is present, setting password won't work if it is present */ + strreplace(setting->password, '\n', '\0'); + return count; +} + +static struct kobj_attribute auth_current_password = __ATTR_WO(current_password); + +static ssize_t new_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *auth_str, *new_pwd; + size_t pwdlen; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.can_set_bios_password) + return -EOPNOTSUPP; + + /* Strip out CR if one is present, setting password won't work if it is present */ + new_pwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_pwd) + return -ENOMEM; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + pwdlen = strlen(new_pwd); + /* pwdlen == 0 is allowed to clear the password */ + if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) { + ret = -EINVAL; + goto out; + } + + /* If opcode support is present use that interface */ + if (tlmi_priv.opcode_support) { + char pwd_type[8]; + + /* Special handling required for HDD and NVMe passwords */ + if (setting == tlmi_priv.pwd_hdd) { + if (setting->level == TLMI_LEVEL_USER) + sprintf(pwd_type, "uhdp%d", setting->index); + else + sprintf(pwd_type, "mhdp%d", setting->index); + } else if (setting == tlmi_priv.pwd_nvme) { + if (setting->level == TLMI_LEVEL_USER) + sprintf(pwd_type, "udrp%d", setting->index); + else + sprintf(pwd_type, "adrp%d", setting->index); + } else { + sprintf(pwd_type, "%s", setting->pwd_type); + } + + ret = tlmi_opcode_setting("WmiOpcodePasswordType", pwd_type); + if (ret) + goto out; + + /* + * Note admin password is not always required if SMPControl enabled in BIOS, + * So only set if it's configured. + * Let BIOS figure it out - we'll get an error if operation is not permitted + */ + if (tlmi_priv.pwd_admin->pwd_enabled && strlen(tlmi_priv.pwd_admin->password)) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + ret = tlmi_opcode_setting("WmiOpcodePasswordCurrent01", setting->password); + if (ret) + goto out; + ret = tlmi_opcode_setting("WmiOpcodePasswordNew01", new_pwd); + if (ret) + goto out; + ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, "WmiOpcodePasswordSetUpdate;"); + } else { + /* Format: 'PasswordType,CurrentPw,NewPw,Encoding,KbdLang;' */ + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s,%s,%s;", + setting->pwd_type, setting->password, new_pwd, + encoding_options[setting->encoding], setting->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + ret = tlmi_simple_call(LENOVO_SET_BIOS_PASSWORD_GUID, auth_str); + kfree(auth_str); + } +out: + mutex_unlock(&tlmi_mutex); + kfree(new_pwd); + return ret ?: count; +} + +static struct kobj_attribute auth_new_password = __ATTR_WO(new_password); + +static ssize_t min_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->minlen); +} + +static struct kobj_attribute auth_min_pass_length = __ATTR_RO(min_password_length); + +static ssize_t max_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->maxlen); +} +static struct kobj_attribute auth_max_pass_length = __ATTR_RO(max_password_length); + +static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + if (setting->cert_installed) + return sysfs_emit(buf, "certificate\n"); + return sysfs_emit(buf, "password\n"); +} +static struct kobj_attribute auth_mechanism = __ATTR_RO(mechanism); + +static ssize_t encoding_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", encoding_options[setting->encoding]); +} + +static ssize_t encoding_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int i; + + /* Scan for a matching profile */ + i = sysfs_match_string(encoding_options, buf); + if (i < 0) + return -EINVAL; + + setting->encoding = i; + return count; +} + +static struct kobj_attribute auth_encoding = __ATTR_RW(encoding); + +static ssize_t kbdlang_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->kbdlang); +} + +static ssize_t kbdlang_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int length; + + /* Calculate length till '\n' or terminating 0 */ + length = strchrnul(buf, '\n') - buf; + if (!length || length >= TLMI_LANG_MAXLEN) + return -EINVAL; + + memcpy(setting->kbdlang, buf, length); + setting->kbdlang[length] = '\0'; + return count; +} + +static struct kobj_attribute auth_kbdlang = __ATTR_RW(kbdlang); + +static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->role); +} +static struct kobj_attribute auth_role = __ATTR_RO(role); + +static ssize_t index_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->index); +} + +static ssize_t index_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int err, val; + + err = kstrtoint(buf, 10, &val); + if (err < 0) + return err; + + if (val < 0 || val > TLMI_INDEX_MAX) + return -EINVAL; + + setting->index = val; + return count; +} + +static struct kobj_attribute auth_index = __ATTR_RW(index); + +static ssize_t level_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", level_options[setting->level]); +} + +static ssize_t level_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int i; + + /* Scan for a matching profile */ + i = sysfs_match_string(level_options, buf); + if (i < 0) + return -EINVAL; + + setting->level = i; + return count; +} + +static struct kobj_attribute auth_level = __ATTR_RW(level); + +static char *cert_command(struct tlmi_pwd_setting *setting, const char *arg1, const char *arg2) +{ + /* Prepend with SVC or SMC if multicert supported */ + if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) + return kasprintf(GFP_KERNEL, "%s,%s,%s", + setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", + arg1, arg2); + else + return kasprintf(GFP_KERNEL, "%s,%s", arg1, arg2); +} + +static ssize_t cert_thumbprint(char *buf, const char *arg, int count) +{ + const struct acpi_buffer input = { strlen(arg), (char *)arg }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + const union acpi_object *obj; + acpi_status status; + + if (!tlmi_priv.cert_guid->thumbprint) + return -EOPNOTSUPP; + + /* Older ThinkCenter BIOS may not have support */ + if (!wmi_has_guid(tlmi_priv.cert_guid->thumbprint)) + return -EOPNOTSUPP; + + status = wmi_evaluate_method(tlmi_priv.cert_guid->thumbprint, 0, 0, &input, &output); + if (ACPI_FAILURE(status)) { + kfree(output.pointer); + return -EIO; + } + obj = output.pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) { + kfree(output.pointer); + return -EIO; + } + count += sysfs_emit_at(buf, count, "%s : %s\n", arg, (char *)obj->string.pointer); + kfree(output.pointer); + + return count; +} + +static char *thumbtypes[] = {"Md5", "Sha1", "Sha256"}; + +static ssize_t certificate_thumbprint_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + unsigned int i; + int count = 0; + char *wmistr; + + if (!tlmi_priv.certificate_support || !setting->cert_installed) + return -EOPNOTSUPP; + + for (i = 0; i < ARRAY_SIZE(thumbtypes); i++) { + if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) { + /* Format: 'SVC | SMC, Thumbtype' */ + wmistr = kasprintf(GFP_KERNEL, "%s,%s", + setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", + thumbtypes[i]); + } else { + /* Format: 'Thumbtype' */ + wmistr = kasprintf(GFP_KERNEL, "%s", thumbtypes[i]); + } + if (!wmistr) + return -ENOMEM; + count += cert_thumbprint(buf, wmistr, count); + kfree(wmistr); + } + + return count; +} + +static struct kobj_attribute auth_cert_thumb = __ATTR_RO(certificate_thumbprint); + +static ssize_t cert_to_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *auth_str, *passwd; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + if (!setting->cert_installed) + return -EINVAL; + + if (!setting->signature || !setting->signature[0]) + return -EACCES; + + /* Strip out CR if one is present */ + passwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!passwd) + return -ENOMEM; + + /* Format: 'Password,Signature' */ + auth_str = cert_command(setting, passwd, setting->signature); + if (!auth_str) { + kfree_sensitive(passwd); + return -ENOMEM; + } + ret = tlmi_simple_call(tlmi_priv.cert_guid->cert_to_password, auth_str); + kfree(auth_str); + kfree_sensitive(passwd); + + return ret ?: count; +} + +static struct kobj_attribute auth_cert_to_password = __ATTR_WO(cert_to_password); + +enum cert_install_mode { + TLMI_CERT_INSTALL, + TLMI_CERT_UPDATE, +}; + +static ssize_t certificate_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + enum cert_install_mode install_mode = TLMI_CERT_INSTALL; + char *auth_str, *new_cert; + const char *serial; + char *signature; + const char *guid; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* If empty then clear installed certificate */ + if ((buf[0] == '\0') || (buf[0] == '\n')) { /* Clear installed certificate */ + /* Check that signature is set */ + if (!setting->signature || !setting->signature[0]) + return -EACCES; + + /* Format: 'serial#, signature' */ + serial = dmi_get_system_info(DMI_PRODUCT_SERIAL); + if (!serial) + return -ENODEV; + auth_str = cert_command(setting, serial, setting->signature); + if (!auth_str) + return -ENOMEM; + + ret = tlmi_simple_call(tlmi_priv.cert_guid->clear_bios_cert, auth_str); + kfree(auth_str); + + return ret ?: count; + } + + /* Strip out CR if one is present */ + new_cert = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_cert) + return -ENOMEM; + + if (setting->cert_installed) { + /* Certificate is installed so this is an update */ + install_mode = TLMI_CERT_UPDATE; + /* If admin account enabled - need to use its signature */ + if (tlmi_priv.pwd_admin->pwd_enabled) + signature = tlmi_priv.pwd_admin->signature; + else + signature = setting->signature; + } else { /* Cert install */ + /* Check if SMC and SVC already installed */ + if ((setting == tlmi_priv.pwd_system) && tlmi_priv.pwd_admin->cert_installed) { + /* This gets treated as a cert update */ + install_mode = TLMI_CERT_UPDATE; + signature = tlmi_priv.pwd_admin->signature; + } else { /* Regular cert install */ + install_mode = TLMI_CERT_INSTALL; + signature = setting->signature; + } + } + + if (install_mode == TLMI_CERT_UPDATE) { + /* This is a certificate update */ + if (!signature || !signature[0]) { + kfree(new_cert); + return -EACCES; + } + guid = tlmi_priv.cert_guid->update_bios_cert; + /* Format: 'Certificate,Signature' */ + auth_str = cert_command(setting, new_cert, signature); + } else { + /* This is a fresh install */ + /* To set admin cert, a password must be enabled */ + if ((setting == tlmi_priv.pwd_admin) && + (!setting->pwd_enabled || !setting->password[0])) { + kfree(new_cert); + return -EACCES; + } + guid = tlmi_priv.cert_guid->set_bios_cert; + if (tlmi_priv.thinkcenter_mode) { + /* Format: 'Certificate, password, encoding, kbdlang' */ + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s,%s", new_cert, + setting->password, + encoding_options[setting->encoding], + setting->kbdlang); + } else { + /* Format: 'Certificate, password' */ + auth_str = cert_command(setting, new_cert, setting->password); + } + } + kfree(new_cert); + if (!auth_str) + return -ENOMEM; + + ret = tlmi_simple_call(guid, auth_str); + kfree(auth_str); + + return ret ?: count; +} + +static struct kobj_attribute auth_certificate = __ATTR_WO(certificate); + +static ssize_t signature_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *new_signature; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_signature) + return -ENOMEM; + + /* Free any previous signature */ + kfree(setting->signature); + setting->signature = new_signature; + + return count; +} + +static struct kobj_attribute auth_signature = __ATTR_WO(signature); + +static ssize_t save_signature_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *new_signature; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_signature) + return -ENOMEM; + + /* Free any previous signature */ + kfree(setting->save_signature); + setting->save_signature = new_signature; + + return count; +} + +static struct kobj_attribute auth_save_signature = __ATTR_WO(save_signature); + +static umode_t auth_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + /* We only want to display level and index settings on HDD/NVMe */ + if (attr == &auth_index.attr || attr == &auth_level.attr) { + if ((setting == tlmi_priv.pwd_hdd) || (setting == tlmi_priv.pwd_nvme)) + return attr->mode; + return 0; + } + + /* We only display certificates, if supported */ + if (attr == &auth_certificate.attr || + attr == &auth_signature.attr || + attr == &auth_save_signature.attr || + attr == &auth_cert_thumb.attr || + attr == &auth_cert_to_password.attr) { + if (tlmi_priv.certificate_support) { + if (setting == tlmi_priv.pwd_admin) + return attr->mode; + if ((tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) && + (setting == tlmi_priv.pwd_system)) + return attr->mode; + } + return 0; + } + + /* Don't display un-needed settings if opcode available */ + if ((attr == &auth_encoding.attr || attr == &auth_kbdlang.attr) && + tlmi_priv.opcode_support) + return 0; + + return attr->mode; +} + +static struct attribute *auth_attrs[] = { + &auth_is_pass_set.attr, + &auth_min_pass_length.attr, + &auth_max_pass_length.attr, + &auth_current_password.attr, + &auth_new_password.attr, + &auth_role.attr, + &auth_mechanism.attr, + &auth_encoding.attr, + &auth_kbdlang.attr, + &auth_index.attr, + &auth_level.attr, + &auth_certificate.attr, + &auth_signature.attr, + &auth_save_signature.attr, + &auth_cert_thumb.attr, + &auth_cert_to_password.attr, + NULL +}; + +static const struct attribute_group auth_attr_group = { + .is_visible = auth_attr_is_visible, + .attrs = auth_attrs, +}; +__ATTRIBUTE_GROUPS(auth_attr); + +/* ---- Attributes sysfs --------------------------------------------------------- */ +static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->display_name); +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + char *item, *value; + int ret; + + ret = tlmi_setting(setting->wdev, setting->index, &item); + if (ret) + return ret; + + /* validate and split from `item,value` -> `value` */ + value = strpbrk(item, ","); + if (!value || value == item || !strlen(value + 1)) + ret = -EINVAL; + else { + /* On Workstations remove the Options part after the value */ + strreplace(value, ';', '\0'); + ret = sysfs_emit(buf, "%s\n", value + 1); + } + kfree(item); + + return ret; +} + +static ssize_t possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->possible_values); +} + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + if (setting->possible_values) { + /* Figure out what setting type is as BIOS does not return this */ + if (strchr(setting->possible_values, ';')) + return sysfs_emit(buf, "enumeration\n"); + } + /* Anything else is going to be a string */ + return sysfs_emit(buf, "string\n"); +} + +static ssize_t current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + char *set_str = NULL, *new_setting = NULL; + char *auth_str = NULL; + int ret; + + if (!tlmi_priv.can_set_bios_settings) + return -EOPNOTSUPP; + + /* + * If we are using bulk saves a reboot should be done once save has + * been called + */ + if (tlmi_priv.save_mode == TLMI_SAVE_BULK && tlmi_priv.reboot_required) + return -EPERM; + + /* Strip out CR if one is present */ + new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_setting) + return -ENOMEM; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + /* Check if certificate authentication is enabled and active */ + if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { + if (!tlmi_priv.pwd_admin->signature || !tlmi_priv.pwd_admin->save_signature) { + ret = -EINVAL; + goto out; + } + set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, + new_setting, tlmi_priv.pwd_admin->signature); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(tlmi_priv.cert_guid->set_bios_setting, set_str); + if (ret) + goto out; + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) + tlmi_priv.save_required = true; + else + ret = tlmi_simple_call(tlmi_priv.cert_guid->save_bios_setting, + tlmi_priv.pwd_admin->save_signature); + } else if (tlmi_priv.opcode_support) { + /* + * If opcode support is present use that interface. + * Note - this sets the variable and then the password as separate + * WMI calls. Function tlmi_save_bios_settings will error if the + * password is incorrect. + * Workstation's require the opcode to be set before changing the + * attribute. + */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + + set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, + new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); + if (ret) + goto out; + + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) + tlmi_priv.save_required = true; + else + ret = tlmi_save_bios_settings(""); + } else { /* old non-opcode based authentication method (deprecated) */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, + new_setting, auth_str); + else + set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, + new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); + if (ret) + goto out; + + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) { + tlmi_priv.save_required = true; + } else { + if (auth_str) + ret = tlmi_save_bios_settings(auth_str); + else + ret = tlmi_save_bios_settings(""); + } + } + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } +out: + mutex_unlock(&tlmi_mutex); + kfree(auth_str); + kfree(set_str); + kfree(new_setting); + return ret ?: count; +} + +static struct kobj_attribute attr_displ_name = __ATTR_RO(display_name); + +static struct kobj_attribute attr_possible_values = __ATTR_RO(possible_values); + +static struct kobj_attribute attr_current_val = __ATTR_RW_MODE(current_value, 0600); + +static struct kobj_attribute attr_type = __ATTR_RO(type); + +static umode_t attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + /* We don't want to display possible_values attributes if not available */ + if ((attr == &attr_possible_values.attr) && (!setting->possible_values)) + return 0; + + return attr->mode; +} + +static struct attribute *tlmi_attrs[] = { + &attr_displ_name.attr, + &attr_current_val.attr, + &attr_possible_values.attr, + &attr_type.attr, + NULL +}; + +static const struct attribute_group tlmi_attr_group = { + .is_visible = attr_is_visible, + .attrs = tlmi_attrs, +}; +__ATTRIBUTE_GROUPS(tlmi_attr); + +static void tlmi_attr_setting_release(struct kobject *kobj) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + kfree(setting->possible_values); + kfree(setting); +} + +static void tlmi_pwd_setting_release(struct kobject *kobj) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + kfree(setting); +} + +static const struct kobj_type tlmi_attr_setting_ktype = { + .release = &tlmi_attr_setting_release, + .sysfs_ops = &kobj_sysfs_ops, + .default_groups = tlmi_attr_groups, +}; + +static const struct kobj_type tlmi_pwd_setting_ktype = { + .release = &tlmi_pwd_setting_release, + .sysfs_ops = &kobj_sysfs_ops, + .default_groups = auth_attr_groups, +}; + +static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "%d\n", tlmi_priv.pending_changes); +} + +static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); + +static const char * const save_mode_strings[] = { + [TLMI_SAVE_SINGLE] = "single", + [TLMI_SAVE_BULK] = "bulk", + [TLMI_SAVE_SAVE] = "save" +}; + +static ssize_t save_settings_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + /* Check that setting is valid */ + if (WARN_ON(tlmi_priv.save_mode < TLMI_SAVE_SINGLE || + tlmi_priv.save_mode > TLMI_SAVE_BULK)) + return -EIO; + return sysfs_emit(buf, "%s\n", save_mode_strings[tlmi_priv.save_mode]); +} + +static ssize_t save_settings_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *auth_str = NULL; + int ret = 0; + int cmd; + + cmd = sysfs_match_string(save_mode_strings, buf); + if (cmd < 0) + return cmd; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + switch (cmd) { + case TLMI_SAVE_SINGLE: + case TLMI_SAVE_BULK: + tlmi_priv.save_mode = cmd; + goto out; + case TLMI_SAVE_SAVE: + /* Check if supported*/ + if (!tlmi_priv.can_set_bios_settings || + tlmi_priv.save_mode == TLMI_SAVE_SINGLE) { + ret = -EOPNOTSUPP; + goto out; + } + /* Check there is actually something to save */ + if (!tlmi_priv.save_required) { + ret = -ENOENT; + goto out; + } + /* Check if certificate authentication is enabled and active */ + if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { + if (!tlmi_priv.pwd_admin->signature || + !tlmi_priv.pwd_admin->save_signature) { + ret = -EINVAL; + goto out; + } + ret = tlmi_simple_call(tlmi_priv.cert_guid->save_bios_setting, + tlmi_priv.pwd_admin->save_signature); + if (ret) + goto out; + } else if (tlmi_priv.opcode_support) { + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + ret = tlmi_save_bios_settings(""); + } else { /* old non-opcode based authentication method (deprecated) */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + ret = tlmi_save_bios_settings(auth_str); + else + ret = tlmi_save_bios_settings(""); + } + tlmi_priv.save_required = false; + tlmi_priv.reboot_required = true; + + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } + break; + } +out: + mutex_unlock(&tlmi_mutex); + kfree(auth_str); + return ret ?: count; +} + +static struct kobj_attribute save_settings = __ATTR_RW(save_settings); + +/* ---- Debug interface--------------------------------------------------------- */ +static ssize_t debug_cmd_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *set_str = NULL, *new_setting = NULL; + char *auth_str = NULL; + int ret; + + if (!tlmi_priv.can_debug_cmd) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_setting) + return -ENOMEM; + + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + set_str = kasprintf(GFP_KERNEL, "%s,%s", new_setting, auth_str); + else + set_str = kasprintf(GFP_KERNEL, "%s;", new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_DEBUG_CMD_GUID, set_str); + if (ret) + goto out; + + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } +out: + kfree(auth_str); + kfree(set_str); + kfree(new_setting); + return ret ?: count; +} + +static struct kobj_attribute debug_cmd = __ATTR_WO(debug_cmd); + +/* ---- Initialisation --------------------------------------------------------- */ +static void tlmi_release_attr(void) +{ + struct kobject *pos, *n; + + /* Attribute structures */ + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); + + if (tlmi_priv.can_debug_cmd && debug_support) + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); + + list_for_each_entry_safe(pos, n, &tlmi_priv.attribute_kset->list, entry) + kobject_put(pos); + + kset_unregister(tlmi_priv.attribute_kset); + + /* Free up any saved signatures */ + kfree(tlmi_priv.pwd_admin->signature); + kfree(tlmi_priv.pwd_admin->save_signature); + + /* Authentication structures */ + list_for_each_entry_safe(pos, n, &tlmi_priv.authentication_kset->list, entry) + kobject_put(pos); + + kset_unregister(tlmi_priv.authentication_kset); +} + +static int tlmi_validate_setting_name(struct kset *attribute_kset, char *name) +{ + struct kobject *duplicate; + + if (!strcmp(name, "Reserved")) + return -EINVAL; + + duplicate = kset_find_obj(attribute_kset, name); + if (duplicate) { + pr_debug("Duplicate attribute name found - %s\n", name); + /* kset_find_obj() returns a reference */ + kobject_put(duplicate); + return -EBUSY; + } + + return 0; +} + +static int tlmi_sysfs_init(void) +{ + int i, ret; + + tlmi_priv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", "thinklmi"); + if (IS_ERR(tlmi_priv.class_dev)) { + ret = PTR_ERR(tlmi_priv.class_dev); + goto fail_class_created; + } + + tlmi_priv.attribute_kset = kset_create_and_add("attributes", NULL, + &tlmi_priv.class_dev->kobj); + if (!tlmi_priv.attribute_kset) { + ret = -ENOMEM; + goto fail_device_created; + } + + tlmi_priv.authentication_kset = kset_create_and_add("authentication", NULL, + &tlmi_priv.class_dev->kobj); + if (!tlmi_priv.authentication_kset) { + kset_unregister(tlmi_priv.attribute_kset); + ret = -ENOMEM; + goto fail_device_created; + } + + for (i = 0; i < TLMI_SETTINGS_COUNT; i++) { + /* Check if index is a valid setting - skip if it isn't */ + if (!tlmi_priv.setting[i]) + continue; + + /* check for duplicate or reserved values */ + if (tlmi_validate_setting_name(tlmi_priv.attribute_kset, + tlmi_priv.setting[i]->display_name) < 0) { + kfree(tlmi_priv.setting[i]->possible_values); + kfree(tlmi_priv.setting[i]); + tlmi_priv.setting[i] = NULL; + continue; + } + + /* Build attribute */ + tlmi_priv.setting[i]->kobj.kset = tlmi_priv.attribute_kset; + ret = kobject_init_and_add(&tlmi_priv.setting[i]->kobj, &tlmi_attr_setting_ktype, + NULL, "%s", tlmi_priv.setting[i]->display_name); + if (ret) + goto fail_create_attr; + } + + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); + if (ret) + goto fail_create_attr; + + if (tlmi_priv.can_debug_cmd && debug_support) { + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); + if (ret) + goto fail_create_attr; + } + + /* Create authentication entries */ + tlmi_priv.pwd_admin->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_init_and_add(&tlmi_priv.pwd_admin->kobj, &tlmi_pwd_setting_ktype, + NULL, "%s", "Admin"); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_power->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_init_and_add(&tlmi_priv.pwd_power->kobj, &tlmi_pwd_setting_ktype, + NULL, "%s", "Power-on"); + if (ret) + goto fail_create_attr; + + if (tlmi_priv.opcode_support) { + tlmi_priv.pwd_system->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_init_and_add(&tlmi_priv.pwd_system->kobj, &tlmi_pwd_setting_ktype, + NULL, "%s", "System"); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_hdd->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_init_and_add(&tlmi_priv.pwd_hdd->kobj, &tlmi_pwd_setting_ktype, + NULL, "%s", "HDD"); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_nvme->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_init_and_add(&tlmi_priv.pwd_nvme->kobj, &tlmi_pwd_setting_ktype, + NULL, "%s", "NVMe"); + if (ret) + goto fail_create_attr; + } + + return ret; + +fail_create_attr: + tlmi_release_attr(); +fail_device_created: + device_unregister(tlmi_priv.class_dev); +fail_class_created: + return ret; +} + +/* ---- Base Driver -------------------------------------------------------- */ +static struct tlmi_pwd_setting *tlmi_create_auth(const char *pwd_type, + const char *pwd_role) +{ + struct tlmi_pwd_setting *new_pwd; + + new_pwd = kzalloc_obj(struct tlmi_pwd_setting); + if (!new_pwd) + return NULL; + + strscpy(new_pwd->kbdlang, "us"); + new_pwd->encoding = TLMI_ENCODING_ASCII; + new_pwd->pwd_type = pwd_type; + new_pwd->role = pwd_role; + new_pwd->minlen = tlmi_priv.pwdcfg.core.min_length; + new_pwd->maxlen = tlmi_priv.pwdcfg.core.max_length; + new_pwd->index = 0; + + return new_pwd; +} + +static int tlmi_analyze(struct wmi_device *wdev) +{ + int i, ret; + + if (wmi_has_guid(LENOVO_SET_BIOS_SETTINGS_GUID) && + wmi_has_guid(LENOVO_SAVE_BIOS_SETTINGS_GUID)) + tlmi_priv.can_set_bios_settings = true; + + if (wmi_has_guid(LENOVO_GET_BIOS_SELECTIONS_GUID)) + tlmi_priv.can_get_bios_selections = true; + + if (wmi_has_guid(LENOVO_SET_BIOS_PASSWORD_GUID)) + tlmi_priv.can_set_bios_password = true; + + if (wmi_has_guid(LENOVO_BIOS_PASSWORD_SETTINGS_GUID)) + tlmi_priv.can_get_password_settings = true; + + if (wmi_has_guid(LENOVO_DEBUG_CMD_GUID)) + tlmi_priv.can_debug_cmd = true; + + if (wmi_has_guid(LENOVO_OPCODE_IF_GUID)) + tlmi_priv.opcode_support = true; + + if (wmi_has_guid(LENOVO_SET_BIOS_CERT_GUID) && + wmi_has_guid(LENOVO_SET_BIOS_SETTING_CERT_GUID) && + wmi_has_guid(LENOVO_SAVE_BIOS_SETTING_CERT_GUID)) + tlmi_priv.certificate_support = true; + + /* ThinkCenter uses different GUIDs for certificate support */ + if (wmi_has_guid(LENOVO_TC_SET_BIOS_CERT_GUID) && + wmi_has_guid(LENOVO_TC_SET_BIOS_SETTING_CERT_GUID) && + wmi_has_guid(LENOVO_TC_SAVE_BIOS_SETTING_CERT_GUID)) { + tlmi_priv.certificate_support = true; + tlmi_priv.thinkcenter_mode = true; + pr_info("ThinkCenter modified support being used\n"); + } + + /* + * Try to find the number of valid settings of this machine + * and use it to create sysfs attributes. + */ + for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { + struct tlmi_attr_setting *setting; + char *item = NULL; + + tlmi_priv.setting[i] = NULL; + ret = tlmi_setting(wdev, i, &item); + if (ret) + break; + if (!item) + break; + if (!*item) { + kfree(item); + continue; + } + + /* Remove the value part */ + strreplace(item, ',', '\0'); + + /* Create a setting entry */ + setting = kzalloc_obj(*setting); + if (!setting) { + ret = -ENOMEM; + kfree(item); + goto fail_clear_attr; + } + setting->wdev = wdev; + setting->index = i; + + strscpy(setting->name, item); + /* It is not allowed to have '/' for file name. Convert it into '\'. */ + strreplace(item, '/', '\\'); + strscpy(setting->display_name, item); + + /* If BIOS selections supported, load those */ + if (tlmi_priv.can_get_bios_selections) { + ret = tlmi_get_bios_selections(setting->name, + &setting->possible_values); + if (ret || !setting->possible_values) + pr_info("Error retrieving possible values for %d : %s\n", + i, setting->display_name); + } else { + /* + * Older Thinkstations don't support the bios_selections API. + * Instead they store this as a [Optional:Option1,Option2] section of the + * name string. + * Try and pull that out if it's available. + */ + char *optitem, *optstart, *optend; + + if (!tlmi_setting(setting->wdev, setting->index, &optitem)) { + optstart = strstr(optitem, "[Optional:"); + if (optstart) { + optstart += strlen("[Optional:"); + optend = strstr(optstart, "]"); + if (optend) + setting->possible_values = + kstrndup(optstart, optend - optstart, + GFP_KERNEL); + } + kfree(optitem); + } + } + /* + * firmware-attributes requires that possible_values are separated by ';' but + * Lenovo FW uses ','. Replace appropriately. + */ + if (setting->possible_values) + strreplace(setting->possible_values, ',', ';'); + + tlmi_priv.setting[i] = setting; + kfree(item); + } + + /* Create password setting structure */ + ret = tlmi_get_pwd_settings(&tlmi_priv.pwdcfg); + if (ret) + goto fail_clear_attr; + + /* All failures below boil down to kmalloc failures */ + ret = -ENOMEM; + + tlmi_priv.pwd_admin = tlmi_create_auth("pap", "bios-admin"); + if (!tlmi_priv.pwd_admin) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_PAP_PWD) + tlmi_priv.pwd_admin->pwd_enabled = true; + + tlmi_priv.pwd_power = tlmi_create_auth("pop", "power-on"); + if (!tlmi_priv.pwd_power) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_POP_PWD) + tlmi_priv.pwd_power->pwd_enabled = true; + + if (tlmi_priv.opcode_support) { + tlmi_priv.pwd_system = tlmi_create_auth("smp", "system"); + if (!tlmi_priv.pwd_system) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_SMP_PWD) + tlmi_priv.pwd_system->pwd_enabled = true; + + tlmi_priv.pwd_hdd = tlmi_create_auth("hdd", "hdd"); + if (!tlmi_priv.pwd_hdd) + goto fail_clear_attr; + + tlmi_priv.pwd_nvme = tlmi_create_auth("nvm", "nvme"); + if (!tlmi_priv.pwd_nvme) + goto fail_clear_attr; + + /* Set default hdd/nvme index to 1 as there is no device 0 */ + tlmi_priv.pwd_hdd->index = 1; + tlmi_priv.pwd_nvme->index = 1; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_HDD_PWD) { + /* Check if PWD is configured and set index to first drive found */ + if (tlmi_priv.pwdcfg.ext.hdd_user_password || + tlmi_priv.pwdcfg.ext.hdd_master_password) { + tlmi_priv.pwd_hdd->pwd_enabled = true; + if (tlmi_priv.pwdcfg.ext.hdd_master_password) + tlmi_priv.pwd_hdd->index = + ffs(tlmi_priv.pwdcfg.ext.hdd_master_password) - 1; + else + tlmi_priv.pwd_hdd->index = + ffs(tlmi_priv.pwdcfg.ext.hdd_user_password) - 1; + } + if (tlmi_priv.pwdcfg.ext.nvme_user_password || + tlmi_priv.pwdcfg.ext.nvme_master_password) { + tlmi_priv.pwd_nvme->pwd_enabled = true; + if (tlmi_priv.pwdcfg.ext.nvme_master_password) + tlmi_priv.pwd_nvme->index = + ffs(tlmi_priv.pwdcfg.ext.nvme_master_password) - 1; + else + tlmi_priv.pwd_nvme->index = + ffs(tlmi_priv.pwdcfg.ext.nvme_user_password) - 1; + } + } + } + + if (tlmi_priv.certificate_support) { + if (tlmi_priv.thinkcenter_mode) { + tlmi_priv.cert_guid = &thinkcenter_cert_guid; + tlmi_priv.pwd_admin->cert_installed = tlmi_priv.pwdcfg.core.password_mode; + } else { + tlmi_priv.cert_guid = &thinkpad_cert_guid; + tlmi_priv.pwd_admin->cert_installed = + tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SVC; + tlmi_priv.pwd_system->cert_installed = + tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SMC; + } + } + return 0; + +fail_clear_attr: + for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { + if (tlmi_priv.setting[i]) { + kfree(tlmi_priv.setting[i]->possible_values); + kfree(tlmi_priv.setting[i]); + } + } + kfree(tlmi_priv.pwd_admin); + kfree(tlmi_priv.pwd_power); + kfree(tlmi_priv.pwd_system); + kfree(tlmi_priv.pwd_hdd); + kfree(tlmi_priv.pwd_nvme); + return ret; +} + +static void tlmi_remove(struct wmi_device *wdev) +{ + tlmi_release_attr(); + device_unregister(tlmi_priv.class_dev); +} + +static int tlmi_probe(struct wmi_device *wdev, const void *context) +{ + int ret; + + ret = tlmi_analyze(wdev); + if (ret) + return ret; + + return tlmi_sysfs_init(); +} + +static const struct wmi_device_id tlmi_id_table[] = { + { .guid_string = LENOVO_BIOS_SETTING_GUID }, + { } +}; +MODULE_DEVICE_TABLE(wmi, tlmi_id_table); + +static struct wmi_driver tlmi_driver = { + .driver = { + .name = "think-lmi", + }, + .id_table = tlmi_id_table, + .probe = tlmi_probe, + .remove = tlmi_remove, +}; + +MODULE_AUTHOR("Sugumaran L "); +MODULE_AUTHOR("Mark Pearson "); +MODULE_AUTHOR("Corentin Chary "); +MODULE_DESCRIPTION("ThinkLMI Driver"); +MODULE_LICENSE("GPL"); + +module_wmi_driver(tlmi_driver); diff --git a/drivers/platform/x86/lenovo/think-lmi.h b/drivers/platform/x86/lenovo/think-lmi.h new file mode 100644 index 000000000..017644323 --- /dev/null +++ b/drivers/platform/x86/lenovo/think-lmi.h @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef _THINK_LMI_H_ +#define _THINK_LMI_H_ + +#include +#include + +#define TLMI_SETTINGS_COUNT 256 +#define TLMI_SETTINGS_MAXLEN 512 +#define TLMI_PWD_BUFSIZE 129 +#define TLMI_LANG_MAXLEN 4 +#define TLMI_INDEX_MAX 32 + +/* Possible error values */ +struct tlmi_err_codes { + const char *err_str; + int err_code; +}; + +enum encoding_option { + TLMI_ENCODING_ASCII, + TLMI_ENCODING_SCANCODE, +}; + +enum level_option { + TLMI_LEVEL_USER, + TLMI_LEVEL_MASTER, +}; + +/* + * There are a limit on the number of WMI operations you can do if you use + * the default implementation of saving on every set. This is due to a + * limitation in EFI variable space used. + * Have a 'bulk save' mode where you can manually trigger the save, and can + * therefore set unlimited variables - for users that need it. + */ +enum save_mode { + TLMI_SAVE_SINGLE, + TLMI_SAVE_BULK, + TLMI_SAVE_SAVE, +}; + +/* GUIDs can differ between platforms */ +struct tlmi_cert_guids { + const char *thumbprint; + const char *set_bios_setting; + const char *save_bios_setting; + const char *cert_to_password; + const char *clear_bios_cert; + const char *update_bios_cert; + const char *set_bios_cert; +}; + +/* password configuration details */ +#define TLMI_PWDCFG_MODE_LEGACY 0 +#define TLMI_PWDCFG_MODE_PASSWORD 1 +#define TLMI_PWDCFG_MODE_MULTICERT 3 + +struct tlmi_pwdcfg_core { + uint32_t password_mode; + uint32_t password_state; + uint32_t min_length; + uint32_t max_length; + uint32_t supported_encodings; + uint32_t supported_keyboard; +}; + +struct tlmi_pwdcfg_ext { + uint32_t hdd_user_password; + uint32_t hdd_master_password; + uint32_t nvme_user_password; + uint32_t nvme_master_password; +}; + +struct tlmi_pwdcfg { + struct tlmi_pwdcfg_core core; + struct tlmi_pwdcfg_ext ext; +}; + +/* password setting details */ +struct tlmi_pwd_setting { + struct kobject kobj; + bool pwd_enabled; + char password[TLMI_PWD_BUFSIZE]; + const char *pwd_type; + const char *role; + int minlen; + int maxlen; + enum encoding_option encoding; + char kbdlang[TLMI_LANG_MAXLEN]; + int index; /*Used for HDD and NVME auth */ + enum level_option level; + bool cert_installed; + char *signature; + char *save_signature; +}; + +/* Attribute setting details */ +struct tlmi_attr_setting { + struct kobject kobj; + struct wmi_device *wdev; + int index; + char name[TLMI_SETTINGS_MAXLEN]; + char display_name[TLMI_SETTINGS_MAXLEN]; + char *possible_values; +}; + +struct think_lmi { + struct wmi_device *wmi_device; + + bool can_set_bios_settings; + bool can_get_bios_selections; + bool can_set_bios_password; + bool can_get_password_settings; + bool pending_changes; + bool can_debug_cmd; + bool opcode_support; + bool certificate_support; + enum save_mode save_mode; + bool save_required; + bool reboot_required; + bool thinkcenter_mode; + + struct tlmi_attr_setting *setting[TLMI_SETTINGS_COUNT]; + struct device *class_dev; + struct kset *attribute_kset; + struct kset *authentication_kset; + + struct tlmi_pwdcfg pwdcfg; + struct tlmi_pwd_setting *pwd_admin; + struct tlmi_pwd_setting *pwd_power; + struct tlmi_pwd_setting *pwd_system; + struct tlmi_pwd_setting *pwd_hdd; + struct tlmi_pwd_setting *pwd_nvme; + + const struct tlmi_cert_guids *cert_guid; +}; + +#endif /* !_THINK_LMI_H_ */ diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c new file mode 100644 index 000000000..b528a1e67 --- /dev/null +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c @@ -0,0 +1,12313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * thinkpad_acpi.c - ThinkPad ACPI Extras + * + * Copyright (C) 2004-2005 Borislav Deianov + * Copyright (C) 2006-2009 Henrique de Moraes Holschuh + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#define TPACPI_VERSION "0.26" +#define TPACPI_SYSFS_VERSION 0x030000 + +/* + * Changelog: + * 2007-10-20 changelog trimmed down + * + * 2007-03-27 0.14 renamed to thinkpad_acpi and moved to + * drivers/misc. + * + * 2006-11-22 0.13 new maintainer + * changelog now lives in git commit history, and will + * not be updated further in-file. + * + * 2005-03-17 0.11 support for 600e, 770x + * thanks to Jamie Lentin + * + * 2005-01-16 0.9 use MODULE_VERSION + * thanks to Henrik Brix Andersen + * fix parameter passing on module loading + * thanks to Rusty Russell + * thanks to Jim Radford + * 2004-11-08 0.8 fix init error case, don't return from a macro + * thanks to Chris Wright + */ + +#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 +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include "../dual_accel_detect.h" + +/* ThinkPad CMOS commands */ +#define TP_CMOS_VOLUME_DOWN 0 +#define TP_CMOS_VOLUME_UP 1 +#define TP_CMOS_VOLUME_MUTE 2 +#define TP_CMOS_BRIGHTNESS_UP 4 +#define TP_CMOS_BRIGHTNESS_DOWN 5 +#define TP_CMOS_THINKLIGHT_ON 12 +#define TP_CMOS_THINKLIGHT_OFF 13 + +/* NVRAM Addresses */ +enum tp_nvram_addr { + TP_NVRAM_ADDR_HK2 = 0x57, + TP_NVRAM_ADDR_THINKLIGHT = 0x58, + TP_NVRAM_ADDR_VIDEO = 0x59, + TP_NVRAM_ADDR_BRIGHTNESS = 0x5e, + TP_NVRAM_ADDR_MIXER = 0x60, +}; + +/* NVRAM bit masks */ +enum { + TP_NVRAM_MASK_HKT_THINKPAD = 0x08, + TP_NVRAM_MASK_HKT_ZOOM = 0x20, + TP_NVRAM_MASK_HKT_DISPLAY = 0x40, + TP_NVRAM_MASK_HKT_HIBERNATE = 0x80, + TP_NVRAM_MASK_THINKLIGHT = 0x10, + TP_NVRAM_MASK_HKT_DISPEXPND = 0x30, + TP_NVRAM_MASK_HKT_BRIGHTNESS = 0x20, + TP_NVRAM_MASK_LEVEL_BRIGHTNESS = 0x0f, + TP_NVRAM_POS_LEVEL_BRIGHTNESS = 0, + TP_NVRAM_MASK_MUTE = 0x40, + TP_NVRAM_MASK_HKT_VOLUME = 0x80, + TP_NVRAM_MASK_LEVEL_VOLUME = 0x0f, + TP_NVRAM_POS_LEVEL_VOLUME = 0, +}; + +/* Misc NVRAM-related */ +enum { + TP_NVRAM_LEVEL_VOLUME_MAX = 14, +}; + +/* ACPI HIDs */ +#define TPACPI_ACPI_IBM_HKEY_HID "IBM0068" +#define TPACPI_ACPI_LENOVO_HKEY_HID "LEN0068" +#define TPACPI_ACPI_LENOVO_HKEY_V2_HID "LEN0268" +#define TPACPI_ACPI_EC_HID "PNP0C09" + +/* Input IDs */ +#define TPACPI_HKEY_INPUT_PRODUCT 0x5054 /* "TP" */ +#define TPACPI_HKEY_INPUT_VERSION 0x4101 + +/* ACPI \WGSV commands */ +enum { + TP_ACPI_WGSV_GET_STATE = 0x01, /* Get state information */ + TP_ACPI_WGSV_PWR_ON_ON_RESUME = 0x02, /* Resume WWAN powered on */ + TP_ACPI_WGSV_PWR_OFF_ON_RESUME = 0x03, /* Resume WWAN powered off */ + TP_ACPI_WGSV_SAVE_STATE = 0x04, /* Save state for S4/S5 */ +}; + +/* TP_ACPI_WGSV_GET_STATE bits */ +enum { + TP_ACPI_WGSV_STATE_WWANEXIST = 0x0001, /* WWAN hw available */ + TP_ACPI_WGSV_STATE_WWANPWR = 0x0002, /* WWAN radio enabled */ + TP_ACPI_WGSV_STATE_WWANPWRRES = 0x0004, /* WWAN state at resume */ + TP_ACPI_WGSV_STATE_WWANBIOSOFF = 0x0008, /* WWAN disabled in BIOS */ + TP_ACPI_WGSV_STATE_BLTHEXIST = 0x0001, /* BLTH hw available */ + TP_ACPI_WGSV_STATE_BLTHPWR = 0x0002, /* BLTH radio enabled */ + TP_ACPI_WGSV_STATE_BLTHPWRRES = 0x0004, /* BLTH state at resume */ + TP_ACPI_WGSV_STATE_BLTHBIOSOFF = 0x0008, /* BLTH disabled in BIOS */ + TP_ACPI_WGSV_STATE_UWBEXIST = 0x0010, /* UWB hw available */ + TP_ACPI_WGSV_STATE_UWBPWR = 0x0020, /* UWB radio enabled */ +}; + +/* HKEY events */ +enum tpacpi_hkey_event_t { + /* Original hotkeys */ + TP_HKEY_EV_ORIG_KEY_START = 0x1001, /* First hotkey (FN+F1) */ + TP_HKEY_EV_BRGHT_UP = 0x1010, /* Brightness up */ + TP_HKEY_EV_BRGHT_DOWN = 0x1011, /* Brightness down */ + TP_HKEY_EV_KBD_LIGHT = 0x1012, /* Thinklight/kbd backlight */ + TP_HKEY_EV_VOL_UP = 0x1015, /* Volume up or unmute */ + TP_HKEY_EV_VOL_DOWN = 0x1016, /* Volume down or unmute */ + TP_HKEY_EV_VOL_MUTE = 0x1017, /* Mixer output mute */ + TP_HKEY_EV_ORIG_KEY_END = 0x1020, /* Last original hotkey code */ + + /* Adaptive keyboard (2014 X1 Carbon) */ + TP_HKEY_EV_DFR_CHANGE_ROW = 0x1101, /* Change adaptive kbd Fn row mode */ + TP_HKEY_EV_DFR_S_QUICKVIEW_ROW = 0x1102, /* Set adap. kbd Fn row to function mode */ + TP_HKEY_EV_ADAPTIVE_KEY_START = 0x1103, /* First hotkey code on adaptive kbd */ + TP_HKEY_EV_ADAPTIVE_KEY_END = 0x1116, /* Last hotkey code on adaptive kbd */ + + /* Extended hotkey events in 2017+ models */ + TP_HKEY_EV_EXTENDED_KEY_START = 0x1300, /* First extended hotkey code */ + TP_HKEY_EV_PRIVACYGUARD_TOGGLE = 0x130f, /* Toggle priv.guard on/off */ + TP_HKEY_EV_EXTENDED_KEY_END = 0x1319, /* Last extended hotkey code using + * hkey -> scancode translation for + * compat. Later codes are entered + * directly in the sparse-keymap. + */ + TP_HKEY_EV_AMT_TOGGLE = 0x131a, /* Toggle AMT on/off */ + TP_HKEY_EV_CAMERASHUTTER_TOGGLE = 0x131b, /* Toggle Camera Shutter */ + TP_HKEY_EV_DOUBLETAP_TOGGLE = 0x131c, /* Toggle trackpoint doubletap on/off */ + TP_HKEY_EV_PROFILE_TOGGLE = 0x131f, /* Toggle platform profile in 2024 systems */ + TP_HKEY_EV_PROFILE_TOGGLE2 = 0x1401, /* Toggle platform profile in 2025 + systems */ + + /* Reasons for waking up from S3/S4 */ + TP_HKEY_EV_WKUP_S3_UNDOCK = 0x2304, /* undock requested, S3 */ + TP_HKEY_EV_WKUP_S4_UNDOCK = 0x2404, /* undock requested, S4 */ + TP_HKEY_EV_WKUP_S3_BAYEJ = 0x2305, /* bay ejection req, S3 */ + TP_HKEY_EV_WKUP_S4_BAYEJ = 0x2405, /* bay ejection req, S4 */ + TP_HKEY_EV_WKUP_S3_BATLOW = 0x2313, /* battery empty, S3 */ + TP_HKEY_EV_WKUP_S4_BATLOW = 0x2413, /* battery empty, S4 */ + + /* Auto-sleep after eject request */ + TP_HKEY_EV_BAYEJ_ACK = 0x3003, /* bay ejection complete */ + TP_HKEY_EV_UNDOCK_ACK = 0x4003, /* undock complete */ + + /* Misc bay events */ + TP_HKEY_EV_OPTDRV_EJ = 0x3006, /* opt. drive tray ejected */ + TP_HKEY_EV_HOTPLUG_DOCK = 0x4010, /* docked into hotplug dock + or port replicator */ + TP_HKEY_EV_HOTPLUG_UNDOCK = 0x4011, /* undocked from hotplug + dock or port replicator */ + /* + * Thinkpad X1 Tablet series devices emit 0x4012 and 0x4013 + * when keyboard cover is attached, detached or folded onto the back + */ + TP_HKEY_EV_KBD_COVER_ATTACH = 0x4012, /* keyboard cover attached */ + TP_HKEY_EV_KBD_COVER_DETACH = 0x4013, /* keyboard cover detached or folded back */ + + /* User-interface events */ + TP_HKEY_EV_LID_CLOSE = 0x5001, /* laptop lid closed */ + TP_HKEY_EV_LID_OPEN = 0x5002, /* laptop lid opened */ + TP_HKEY_EV_TABLET_TABLET = 0x5009, /* tablet swivel up */ + TP_HKEY_EV_TABLET_NOTEBOOK = 0x500a, /* tablet swivel down */ + TP_HKEY_EV_TABLET_CHANGED = 0x60c0, /* X1 Yoga (2016): + * enter/leave tablet mode + */ + TP_HKEY_EV_PEN_INSERTED = 0x500b, /* tablet pen inserted */ + TP_HKEY_EV_PEN_REMOVED = 0x500c, /* tablet pen removed */ + TP_HKEY_EV_BRGHT_CHANGED = 0x5010, /* backlight control event */ + + /* Key-related user-interface events */ + TP_HKEY_EV_KEY_NUMLOCK = 0x6000, /* NumLock key pressed */ + TP_HKEY_EV_KEY_FN = 0x6005, /* Fn key pressed? E420 */ + TP_HKEY_EV_KEY_FN_ESC = 0x6060, /* Fn+Esc key pressed X240 */ + + /* Thermal events */ + TP_HKEY_EV_ALARM_BAT_HOT = 0x6011, /* battery too hot */ + TP_HKEY_EV_ALARM_BAT_XHOT = 0x6012, /* battery critically hot */ + TP_HKEY_EV_ALARM_BAT_LIM_CHANGE = 0x6013, /* battery charge limit changed*/ + TP_HKEY_EV_ALARM_SENSOR_HOT = 0x6021, /* sensor too hot */ + TP_HKEY_EV_ALARM_SENSOR_XHOT = 0x6022, /* sensor critically hot */ + TP_HKEY_EV_THM_TABLE_CHANGED = 0x6030, /* windows; thermal table changed */ + TP_HKEY_EV_THM_CSM_COMPLETED = 0x6032, /* windows; thermal control set + * command completed. Related to + * AML DYTC */ + TP_HKEY_EV_THM_TRANSFM_CHANGED = 0x60F0, /* windows; thermal transformation + * changed. Related to AML GMTS */ + + /* AC-related events */ + TP_HKEY_EV_AC_CHANGED = 0x6040, /* AC status changed */ + + /* Further user-interface events */ + TP_HKEY_EV_PALM_DETECTED = 0x60b0, /* palm hoveres keyboard */ + TP_HKEY_EV_PALM_UNDETECTED = 0x60b1, /* palm removed */ + + /* Misc */ + TP_HKEY_EV_RFKILL_CHANGED = 0x7000, /* rfkill switch changed */ + + /* Misc2 */ + TP_HKEY_EV_TRACK_DOUBLETAP = 0x8036, /* trackpoint doubletap */ +}; + +/**************************************************************************** + * Main driver + */ + +#define TPACPI_NAME "thinkpad" +#define TPACPI_DESC "ThinkPad ACPI Extras" +#define TPACPI_FILE TPACPI_NAME "_acpi" +#define TPACPI_URL "http://ibm-acpi.sf.net/" +#define TPACPI_MAIL "ibm-acpi-devel@lists.sourceforge.net" + +#define TPACPI_PROC_DIR "ibm" +#define TPACPI_ACPI_EVENT_PREFIX "ibm" +#define TPACPI_DRVR_NAME TPACPI_FILE +#define TPACPI_DRVR_SHORTNAME "tpacpi" +#define TPACPI_HWMON_DRVR_NAME TPACPI_NAME "_hwmon" + +#define TPACPI_NVRAM_KTHREAD_NAME "ktpacpi_nvramd" +#define TPACPI_WORKQUEUE_NAME "ktpacpid" + +#define TPACPI_MAX_ACPI_ARGS 3 + +/* Debugging printk groups */ +#define TPACPI_DBG_ALL 0xffff +#define TPACPI_DBG_DISCLOSETASK 0x8000 +#define TPACPI_DBG_INIT 0x0001 +#define TPACPI_DBG_EXIT 0x0002 +#define TPACPI_DBG_RFKILL 0x0004 +#define TPACPI_DBG_HKEY 0x0008 +#define TPACPI_DBG_FAN 0x0010 +#define TPACPI_DBG_BRGHT 0x0020 +#define TPACPI_DBG_MIXER 0x0040 + +#define FAN_NOT_PRESENT 65535 + +/**************************************************************************** + * Driver-wide structs and misc. variables + */ + +struct ibm_struct; + +struct tp_acpi_drv_struct { + const struct acpi_device_id *hid; + + void (*notify) (struct ibm_struct *, u32); + acpi_handle *handle; + u32 type; + struct acpi_device *device; +}; + +struct ibm_struct { + char *name; + + int (*read) (struct seq_file *); + int (*write) (char *); + void (*exit) (void); + void (*resume) (void); + void (*suspend) (void); + void (*shutdown) (void); + + struct list_head all_drivers; + + struct tp_acpi_drv_struct *acpi; + + struct { + u8 acpi_notify_installed:1; + u8 proc_created:1; + u8 init_called:1; + u8 experimental:1; + } flags; +}; + +struct ibm_init_struct { + char param[32]; + + int (*init) (struct ibm_init_struct *); + umode_t base_procfs_mode; + struct ibm_struct *data; +}; + +/* DMI Quirks */ +struct quirk_entry { + bool btusb_bug; +}; + +static struct quirk_entry quirk_btusb_bug = { + .btusb_bug = true, +}; + +static struct { + u32 bluetooth:1; + u32 hotkey:1; + u32 hotkey_mask:1; + u32 hotkey_wlsw:1; + enum { + TP_HOTKEY_TABLET_NONE = 0, + TP_HOTKEY_TABLET_USES_MHKG, + TP_HOTKEY_TABLET_USES_GMMS, + } hotkey_tablet; + u32 kbdlight:1; + u32 light:1; + u32 light_status:1; + u32 bright_acpimode:1; + u32 bright_unkfw:1; + u32 wan:1; + u32 uwb:1; + u32 fan_ctrl_status_undef:1; + u32 second_fan:1; + u32 second_fan_ctl:1; + u32 beep_needs_two_args:1; + u32 mixer_no_level_control:1; + u32 battery_force_primary:1; + u32 platform_drv_registered:1; + u32 hotkey_poll_active:1; + u32 has_adaptive_kbd:1; + u32 kbd_lang:1; + u32 trackpoint_doubletap_enable:1; + struct quirk_entry *quirks; +} tp_features; + +static struct { + u16 hotkey_mask_ff:1; + u16 volume_ctrl_forbidden:1; +} tp_warned; + +struct thinkpad_id_data { + unsigned int vendor; /* ThinkPad vendor: + * PCI_VENDOR_ID_IBM/PCI_VENDOR_ID_LENOVO */ + + char *bios_version_str; /* Something like 1ZET51WW (1.03z) */ + char *ec_version_str; /* Something like 1ZHT51WW-1.04a */ + + u32 bios_model; /* 1Y = 0x3159, 0 = unknown */ + u32 ec_model; + u16 bios_release; /* 1ZETK1WW = 0x4b31, 0 = unknown */ + u16 ec_release; + + char *model_str; /* ThinkPad T43 */ + char *nummodel_str; /* 9384A9C for a 9384-A9C model */ +}; +static struct thinkpad_id_data thinkpad_id; + +static enum { + TPACPI_LIFE_INIT = 0, + TPACPI_LIFE_RUNNING, + TPACPI_LIFE_EXITING, +} tpacpi_lifecycle; + +static int experimental; +static u32 dbg_level; + +static struct workqueue_struct *tpacpi_wq; + +enum led_status_t { + TPACPI_LED_OFF = 0, + TPACPI_LED_ON, + TPACPI_LED_BLINK, +}; + +/* tpacpi LED class */ +struct tpacpi_led_classdev { + struct led_classdev led_classdev; + int led; +}; + +/* brightness level capabilities */ +static unsigned int bright_maxlvl; /* 0 = unknown */ + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +static int dbg_wlswemul; +static bool tpacpi_wlsw_emulstate; +static int dbg_bluetoothemul; +static bool tpacpi_bluetooth_emulstate; +static int dbg_wwanemul; +static bool tpacpi_wwan_emulstate; +static int dbg_uwbemul; +static bool tpacpi_uwb_emulstate; +#endif + + +/************************************************************************* + * Debugging helpers + */ + +#define dbg_printk(a_dbg_level, format, arg...) \ +do { \ + if (dbg_level & (a_dbg_level)) \ + printk(KERN_DEBUG pr_fmt("%s: " format), \ + __func__, ##arg); \ +} while (0) + +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +#define vdbg_printk dbg_printk +static const char *str_supported(int is_supported); +#else +static inline const char *str_supported(int is_supported) { return ""; } +#define vdbg_printk(a_dbg_level, format, arg...) \ + do { if (0) no_printk(format, ##arg); } while (0) +#endif + +static void tpacpi_log_usertask(const char * const what) +{ + printk(KERN_DEBUG pr_fmt("%s: access by process with PID %d\n"), + what, task_tgid_vnr(current)); +} + +#define tpacpi_disclose_usertask(what, format, arg...) \ +do { \ + if (unlikely((dbg_level & TPACPI_DBG_DISCLOSETASK) && \ + (tpacpi_lifecycle == TPACPI_LIFE_RUNNING))) { \ + printk(KERN_DEBUG pr_fmt("%s: PID %d: " format), \ + what, task_tgid_vnr(current), ## arg); \ + } \ +} while (0) + +/* + * Quirk handling helpers + * + * ThinkPad IDs and versions seen in the field so far are + * two or three characters from the set [0-9A-Z], i.e. base 36. + * + * We use values well outside that range as specials. + */ + +#define TPACPI_MATCH_ANY 0xffffffffU +#define TPACPI_MATCH_ANY_VERSION 0xffffU +#define TPACPI_MATCH_UNKNOWN 0U + +/* TPID('1', 'Y') == 0x3159 */ +#define TPID(__c1, __c2) (((__c1) << 8) | (__c2)) +#define TPID3(__c1, __c2, __c3) (((__c1) << 16) | ((__c2) << 8) | (__c3)) +#define TPVER TPID + +#define TPACPI_Q_IBM(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_IBM, \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_Q_LNV(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_Q_LNV3(__id1, __id2, __id3, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPID3(__id1, __id2, __id3), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_QEC_IBM(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_IBM, \ + .bios = TPACPI_MATCH_ANY, \ + .ec = TPID(__id1, __id2), \ + .quirks = (__quirk) } + +#define TPACPI_QEC_LNV(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPACPI_MATCH_ANY, \ + .ec = TPID(__id1, __id2), \ + .quirks = (__quirk) } + +struct tpacpi_quirk { + unsigned int vendor; + u32 bios; + u32 ec; + unsigned long quirks; +}; + +/** + * tpacpi_check_quirks() - search BIOS/EC version on a list + * @qlist: array of &struct tpacpi_quirk + * @qlist_size: number of elements in @qlist + * + * Iterates over a quirks list until one is found that matches the + * ThinkPad's vendor, BIOS and EC model. + * + * Returns: %0 if nothing matches, otherwise returns the quirks field of + * the matching &struct tpacpi_quirk entry. + * + * The match criteria is: vendor, ec and bios must match. + */ +static unsigned long __init tpacpi_check_quirks( + const struct tpacpi_quirk *qlist, + unsigned int qlist_size) +{ + while (qlist_size) { + if ((qlist->vendor == thinkpad_id.vendor || + qlist->vendor == TPACPI_MATCH_ANY) && + (qlist->bios == thinkpad_id.bios_model || + qlist->bios == TPACPI_MATCH_ANY) && + (qlist->ec == thinkpad_id.ec_model || + qlist->ec == TPACPI_MATCH_ANY)) + return qlist->quirks; + + qlist_size--; + qlist++; + } + return 0; +} + +static __always_inline bool __pure __init tpacpi_is_lenovo(void) +{ + return thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO; +} + +static __always_inline bool __pure __init tpacpi_is_ibm(void) +{ + return thinkpad_id.vendor == PCI_VENDOR_ID_IBM; +} + +/**************************************************************************** + **************************************************************************** + * + * ACPI Helpers and device model + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * ACPI basic handles + */ + +static acpi_handle root_handle; +static acpi_handle ec_handle; + +#define TPACPI_HANDLE(object, parent, paths...) \ + static acpi_handle object##_handle; \ + static const acpi_handle * const object##_parent __initconst = \ + &parent##_handle; \ + static char *object##_paths[] __initdata = { paths } + +TPACPI_HANDLE(ecrd, ec, "ECRD"); /* 570 */ +TPACPI_HANDLE(ecwr, ec, "ECWR"); /* 570 */ + +TPACPI_HANDLE(cmos, root, "\\UCMS", /* R50, R50e, R50p, R51, */ + /* T4x, X31, X40 */ + "\\CMOS", /* A3x, G4x, R32, T23, T30, X22-24, X30 */ + "\\CMS", /* R40, R40e */ + ); /* all others */ + +TPACPI_HANDLE(hkey, ec, "\\_SB.HKEY", /* 600e/x, 770e, 770x */ + "^HKEY", /* R30, R31 */ + "HKEY", /* all others */ + ); /* 570 */ + +/************************************************************************* + * ACPI helpers + */ + +static int acpi_evalf(acpi_handle handle, + int *res, char *method, char *fmt, ...) +{ + char *fmt0 = fmt; + struct acpi_object_list params; + union acpi_object in_objs[TPACPI_MAX_ACPI_ARGS]; + struct acpi_buffer result, *resultp; + union acpi_object out_obj; + acpi_status status; + va_list ap; + char res_type; + int success; + int quiet; + + if (!*fmt) { + pr_err("acpi_evalf() called with empty format\n"); + return 0; + } + + if (*fmt == 'q') { + quiet = 1; + fmt++; + } else + quiet = 0; + + res_type = *(fmt++); + + params.count = 0; + params.pointer = &in_objs[0]; + + va_start(ap, fmt); + while (*fmt) { + char c = *(fmt++); + switch (c) { + case 'd': /* int */ + in_objs[params.count].integer.value = va_arg(ap, int); + in_objs[params.count++].type = ACPI_TYPE_INTEGER; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", + c); + va_end(ap); + return 0; + } + } + va_end(ap); + + if (res_type != 'v') { + result.length = sizeof(out_obj); + result.pointer = &out_obj; + resultp = &result; + } else + resultp = NULL; + + status = acpi_evaluate_object(handle, method, ¶ms, resultp); + + switch (res_type) { + case 'd': /* int */ + success = (status == AE_OK && + out_obj.type == ACPI_TYPE_INTEGER); + if (success && res) + *res = out_obj.integer.value; + break; + case 'v': /* void */ + success = status == AE_OK; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", + res_type); + return 0; + } + + if (!success && !quiet) + pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", + method, fmt0, acpi_format_exception(status)); + + return success; +} + +static int acpi_ec_read(int i, u8 *p) +{ + int v; + + if (ecrd_handle) { + if (!acpi_evalf(ecrd_handle, &v, NULL, "dd", i)) + return 0; + *p = v; + } else { + if (ec_read(i, p) < 0) + return 0; + } + + return 1; +} + +static int acpi_ec_write(int i, u8 v) +{ + if (ecwr_handle) { + if (!acpi_evalf(ecwr_handle, NULL, NULL, "vdd", i, v)) + return 0; + } else { + if (ec_write(i, v) < 0) + return 0; + } + + return 1; +} + +static int issue_thinkpad_cmos_command(int cmos_cmd) +{ + if (!cmos_handle) + return -ENXIO; + + if (!acpi_evalf(cmos_handle, NULL, NULL, "vd", cmos_cmd)) + return -EIO; + + return 0; +} + +/************************************************************************* + * ACPI device model + */ + +#define TPACPI_ACPIHANDLE_INIT(object) \ + drv_acpi_handle_init(#object, &object##_handle, *object##_parent, \ + object##_paths, ARRAY_SIZE(object##_paths)) + +static void __init drv_acpi_handle_init(const char *name, + acpi_handle *handle, const acpi_handle parent, + char **paths, const int num_paths) +{ + int i; + acpi_status status; + + vdbg_printk(TPACPI_DBG_INIT, "trying to locate ACPI handle for %s\n", + name); + + for (i = 0; i < num_paths; i++) { + status = acpi_get_handle(parent, paths[i], handle); + if (ACPI_SUCCESS(status)) { + dbg_printk(TPACPI_DBG_INIT, + "Found ACPI handle %s for %s\n", + paths[i], name); + return; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "ACPI handle for %s not found\n", + name); + *handle = NULL; +} + +static acpi_status __init tpacpi_acpi_handle_locate_callback(acpi_handle handle, + u32 level, void *context, void **return_value) +{ + if (!strcmp(context, "video")) { + struct acpi_device *dev = acpi_fetch_acpi_dev(handle); + + if (!acpi_dev_is_video_device(dev)) + return AE_OK; + } + + *(acpi_handle *)return_value = handle; + + return AE_CTRL_TERMINATE; +} + +static void __init tpacpi_acpi_handle_locate(const char *name, + const char *hid, + acpi_handle *handle) +{ + acpi_status status; + acpi_handle device_found; + + BUG_ON(!name || !handle); + vdbg_printk(TPACPI_DBG_INIT, + "trying to locate ACPI handle for %s, using HID %s\n", + name, hid ? hid : "NULL"); + + memset(&device_found, 0, sizeof(device_found)); + status = acpi_get_devices(hid, tpacpi_acpi_handle_locate_callback, + (void *)name, &device_found); + + *handle = NULL; + + if (ACPI_SUCCESS(status)) { + *handle = device_found; + dbg_printk(TPACPI_DBG_INIT, + "Found ACPI handle for %s\n", name); + } else { + vdbg_printk(TPACPI_DBG_INIT, + "Could not locate an ACPI handle for %s: %s\n", + name, acpi_format_exception(status)); + } +} + +static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct ibm_struct *ibm = data; + + if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) + return; + + if (!ibm || !ibm->acpi || !ibm->acpi->notify) + return; + + ibm->acpi->notify(ibm, event); +} + +static int __init setup_acpi_notify(struct ibm_struct *ibm) +{ + acpi_status status; + + BUG_ON(!ibm->acpi); + + if (!*ibm->acpi->handle) + return 0; + + vdbg_printk(TPACPI_DBG_INIT, + "setting up ACPI notify for %s\n", ibm->name); + + ibm->acpi->device = acpi_get_acpi_dev(*ibm->acpi->handle); + if (!ibm->acpi->device) { + pr_err("acpi_get_acpi_dev(%s) failed\n", ibm->name); + return -ENODEV; + } + + ibm->acpi->device->driver_data = ibm; + scnprintf(acpi_device_class(ibm->acpi->device), + sizeof(acpi_device_class(ibm->acpi->device)), + "%s/%s", TPACPI_ACPI_EVENT_PREFIX, ibm->name); + + status = acpi_install_notify_handler(*ibm->acpi->handle, + ibm->acpi->type, dispatch_acpi_notify, ibm); + if (ACPI_FAILURE(status)) { + if (status == AE_ALREADY_EXISTS) { + pr_notice("another device driver is already handling %s events\n", + ibm->name); + } else { + pr_err("acpi_install_notify_handler(%s) failed: %s\n", + ibm->name, acpi_format_exception(status)); + } + return -ENODEV; + } + ibm->flags.acpi_notify_installed = 1; + return 0; +} + +/**************************************************************************** + **************************************************************************** + * + * Procfs Helpers + * + **************************************************************************** + ****************************************************************************/ + +static int dispatch_proc_show(struct seq_file *m, void *v) +{ + struct ibm_struct *ibm = m->private; + + if (!ibm || !ibm->read) + return -EINVAL; + return ibm->read(m); +} + +static int dispatch_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, dispatch_proc_show, pde_data(inode)); +} + +static ssize_t dispatch_proc_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *pos) +{ + struct ibm_struct *ibm = pde_data(file_inode(file)); + char *kernbuf; + int ret; + + if (!ibm || !ibm->write) + return -EINVAL; + if (count > PAGE_SIZE - 1) + return -EINVAL; + + kernbuf = memdup_user_nul(userbuf, count); + if (IS_ERR(kernbuf)) + return PTR_ERR(kernbuf); + ret = ibm->write(kernbuf); + if (ret == 0) + ret = count; + + kfree(kernbuf); + + return ret; +} + +static const struct proc_ops dispatch_proc_ops = { + .proc_open = dispatch_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = dispatch_proc_write, +}; + +/**************************************************************************** + **************************************************************************** + * + * Device model: input, hwmon and platform + * + **************************************************************************** + ****************************************************************************/ + +static struct platform_device *tpacpi_pdev; +static struct platform_device *tpacpi_sensors_pdev; +static struct device *tpacpi_hwmon; +static struct device *tpacpi_pprof; +static struct input_dev *tpacpi_inputdev; +static struct mutex tpacpi_inputdev_send_mutex; +static LIST_HEAD(tpacpi_all_drivers); + +#ifdef CONFIG_PM_SLEEP +static int tpacpi_suspend_handler(struct device *dev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->suspend) + (ibm->suspend)(); + } + + return 0; +} + +static int tpacpi_resume_handler(struct device *dev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->resume) + (ibm->resume)(); + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(tpacpi_pm, + tpacpi_suspend_handler, tpacpi_resume_handler); + +static void tpacpi_shutdown_handler(struct platform_device *pdev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->shutdown) + (ibm->shutdown)(); + } +} + +/************************************************************************* + * sysfs support helpers + */ + +static int parse_strtoul(const char *buf, + unsigned long max, unsigned long *value) +{ + char *endp; + + *value = simple_strtoul(skip_spaces(buf), &endp, 0); + endp = skip_spaces(endp); + if (*endp || *value > max) + return -EINVAL; + + return 0; +} + +static void tpacpi_disable_brightness_delay(void) +{ + if (acpi_evalf(hkey_handle, NULL, "PWMS", "qvd", 0)) + pr_notice("ACPI backlight control delay disabled\n"); +} + +static void printk_deprecated_attribute(const char * const what, + const char * const details) +{ + tpacpi_log_usertask("deprecated sysfs attribute"); + pr_warn("WARNING: sysfs attribute %s is deprecated and will be removed. %s\n", + what, details); +} + +/************************************************************************* + * rfkill and radio control support helpers + */ + +/* + * ThinkPad-ACPI firmware handling model: + * + * WLSW (master wireless switch) is event-driven, and is common to all + * firmware-controlled radios. It cannot be controlled, just monitored, + * as expected. It overrides all radio state in firmware + * + * The kernel, a masked-off hotkey, and WLSW can change the radio state + * (TODO: verify how WLSW interacts with the returned radio state). + * + * The only time there are shadow radio state changes, is when + * masked-off hotkeys are used. + */ + +/* + * Internal driver API for radio state: + * + * int: < 0 = error, otherwise enum tpacpi_rfkill_state + * bool: true means radio blocked (off) + */ +enum tpacpi_rfkill_state { + TPACPI_RFK_RADIO_OFF = 0, + TPACPI_RFK_RADIO_ON +}; + +/* rfkill switches */ +enum tpacpi_rfk_id { + TPACPI_RFK_BLUETOOTH_SW_ID = 0, + TPACPI_RFK_WWAN_SW_ID, + TPACPI_RFK_UWB_SW_ID, + TPACPI_RFK_SW_MAX +}; + +static const char *tpacpi_rfkill_names[] = { + [TPACPI_RFK_BLUETOOTH_SW_ID] = "bluetooth", + [TPACPI_RFK_WWAN_SW_ID] = "wwan", + [TPACPI_RFK_UWB_SW_ID] = "uwb", + [TPACPI_RFK_SW_MAX] = NULL +}; + +/* ThinkPad-ACPI rfkill subdriver */ +struct tpacpi_rfk { + struct rfkill *rfkill; + enum tpacpi_rfk_id id; + const struct tpacpi_rfk_ops *ops; +}; + +struct tpacpi_rfk_ops { + /* firmware interface */ + int (*get_status)(void); + int (*set_status)(const enum tpacpi_rfkill_state); +}; + +static struct tpacpi_rfk *tpacpi_rfkill_switches[TPACPI_RFK_SW_MAX]; + +/* Query FW and update rfkill sw state for a given rfkill switch */ +static int tpacpi_rfk_update_swstate(const struct tpacpi_rfk *tp_rfk) +{ + int status; + + if (!tp_rfk) + return -ENODEV; + + status = (tp_rfk->ops->get_status)(); + if (status < 0) + return status; + + rfkill_set_sw_state(tp_rfk->rfkill, + (status == TPACPI_RFK_RADIO_OFF)); + + return status; +} + +/* + * Sync the HW-blocking state of all rfkill switches, + * do notice it causes the rfkill core to schedule uevents + */ +static void tpacpi_rfk_update_hwblock_state(bool blocked) +{ + unsigned int i; + struct tpacpi_rfk *tp_rfk; + + for (i = 0; i < TPACPI_RFK_SW_MAX; i++) { + tp_rfk = tpacpi_rfkill_switches[i]; + if (tp_rfk) { + if (rfkill_set_hw_state(tp_rfk->rfkill, + blocked)) { + /* ignore -- we track sw block */ + } + } + } +} + +/* Call to get the WLSW state from the firmware */ +static int hotkey_get_wlsw(void); + +/* Call to query WLSW state and update all rfkill switches */ +static bool tpacpi_rfk_check_hwblock_state(void) +{ + int res = hotkey_get_wlsw(); + int hw_blocked; + + /* When unknown or unsupported, we have to assume it is unblocked */ + if (res < 0) + return false; + + hw_blocked = (res == TPACPI_RFK_RADIO_OFF); + tpacpi_rfk_update_hwblock_state(hw_blocked); + + return hw_blocked; +} + +static int tpacpi_rfk_hook_set_block(void *data, bool blocked) +{ + struct tpacpi_rfk *tp_rfk = data; + int res; + + dbg_printk(TPACPI_DBG_RFKILL, + "request to change radio state to %s\n", + blocked ? "blocked" : "unblocked"); + + /* try to set radio state */ + res = (tp_rfk->ops->set_status)(blocked ? + TPACPI_RFK_RADIO_OFF : TPACPI_RFK_RADIO_ON); + + /* and update the rfkill core with whatever the FW really did */ + tpacpi_rfk_update_swstate(tp_rfk); + + return (res < 0) ? res : 0; +} + +static const struct rfkill_ops tpacpi_rfk_rfkill_ops = { + .set_block = tpacpi_rfk_hook_set_block, +}; + +static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id, + const struct tpacpi_rfk_ops *tp_rfkops, + const enum rfkill_type rfktype, + const char *name, + const bool set_default) +{ + struct tpacpi_rfk *atp_rfk; + int res; + bool sw_state = false; + bool hw_state; + int sw_status; + + BUG_ON(id >= TPACPI_RFK_SW_MAX || tpacpi_rfkill_switches[id]); + + atp_rfk = kzalloc_obj(struct tpacpi_rfk); + if (atp_rfk) + atp_rfk->rfkill = rfkill_alloc(name, + &tpacpi_pdev->dev, + rfktype, + &tpacpi_rfk_rfkill_ops, + atp_rfk); + if (!atp_rfk || !atp_rfk->rfkill) { + pr_err("failed to allocate memory for rfkill class\n"); + kfree(atp_rfk); + return -ENOMEM; + } + + atp_rfk->id = id; + atp_rfk->ops = tp_rfkops; + + sw_status = (tp_rfkops->get_status)(); + if (sw_status < 0) { + pr_err("failed to read initial state for %s, error %d\n", + name, sw_status); + } else { + sw_state = (sw_status == TPACPI_RFK_RADIO_OFF); + if (set_default) { + /* try to keep the initial state, since we ask the + * firmware to preserve it across S5 in NVRAM */ + rfkill_init_sw_state(atp_rfk->rfkill, sw_state); + } + } + hw_state = tpacpi_rfk_check_hwblock_state(); + rfkill_set_hw_state(atp_rfk->rfkill, hw_state); + + res = rfkill_register(atp_rfk->rfkill); + if (res < 0) { + pr_err("failed to register %s rfkill switch: %d\n", name, res); + rfkill_destroy(atp_rfk->rfkill); + kfree(atp_rfk); + return res; + } + + tpacpi_rfkill_switches[id] = atp_rfk; + + pr_info("rfkill switch %s: radio is %sblocked\n", + name, (sw_state || hw_state) ? "" : "un"); + return 0; +} + +static void tpacpi_destroy_rfkill(const enum tpacpi_rfk_id id) +{ + struct tpacpi_rfk *tp_rfk; + + BUG_ON(id >= TPACPI_RFK_SW_MAX); + + tp_rfk = tpacpi_rfkill_switches[id]; + if (tp_rfk) { + rfkill_unregister(tp_rfk->rfkill); + rfkill_destroy(tp_rfk->rfkill); + tpacpi_rfkill_switches[id] = NULL; + kfree(tp_rfk); + } +} + +static void printk_deprecated_rfkill_attribute(const char * const what) +{ + printk_deprecated_attribute(what, + "Please switch to generic rfkill before year 2010"); +} + +/* sysfs enable ------------------------------------------------ */ +static ssize_t tpacpi_rfk_sysfs_enable_show(const enum tpacpi_rfk_id id, + struct device_attribute *attr, + char *buf) +{ + int status; + + printk_deprecated_rfkill_attribute(attr->attr.name); + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state()) { + status = TPACPI_RFK_RADIO_OFF; + } else { + status = tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + if (status < 0) + return status; + } + + return sysfs_emit(buf, "%d\n", + (status == TPACPI_RFK_RADIO_ON) ? 1 : 0); +} + +static ssize_t tpacpi_rfk_sysfs_enable_store(const enum tpacpi_rfk_id id, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + printk_deprecated_rfkill_attribute(attr->attr.name); + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_disclose_usertask(attr->attr.name, "set to %ld\n", t); + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state() && !!t) + return -EPERM; + + res = tpacpi_rfkill_switches[id]->ops->set_status((!!t) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF); + tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + + return (res < 0) ? res : count; +} + +/* procfs -------------------------------------------------------------- */ +static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, struct seq_file *m) +{ + if (id >= TPACPI_RFK_SW_MAX) + seq_puts(m, "status:\t\tnot supported\n"); + else { + int status; + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state()) { + status = TPACPI_RFK_RADIO_OFF; + } else { + status = tpacpi_rfk_update_swstate( + tpacpi_rfkill_switches[id]); + if (status < 0) + return status; + } + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status == TPACPI_RFK_RADIO_ON)); + seq_puts(m, "commands:\tenable, disable\n"); + } + + return 0; +} + +static int tpacpi_rfk_procfs_write(const enum tpacpi_rfk_id id, char *buf) +{ + char *cmd; + int status = -1; + int res = 0; + + if (id >= TPACPI_RFK_SW_MAX) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "enable")) + status = TPACPI_RFK_RADIO_ON; + else if (strstarts(cmd, "disable")) + status = TPACPI_RFK_RADIO_OFF; + else + return -EINVAL; + } + + if (status != -1) { + tpacpi_disclose_usertask("procfs", "attempt to %s %s\n", + str_enable_disable(status == TPACPI_RFK_RADIO_ON), + tpacpi_rfkill_names[id]); + res = (tpacpi_rfkill_switches[id]->ops->set_status)(status); + tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + } + + return res; +} + +/************************************************************************* + * thinkpad-acpi driver attributes + */ + +/* interface_version --------------------------------------------------- */ +static ssize_t interface_version_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", TPACPI_SYSFS_VERSION); +} +static DRIVER_ATTR_RO(interface_version); + +/* debug_level --------------------------------------------------------- */ +static ssize_t debug_level_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "0x%04x\n", dbg_level); +} + +static ssize_t debug_level_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 0xffff, &t)) + return -EINVAL; + + dbg_level = t; + + return count; +} +static DRIVER_ATTR_RW(debug_level); + +/* version ------------------------------------------------------------- */ +static ssize_t version_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%s v%s\n", + TPACPI_DESC, TPACPI_VERSION); +} +static DRIVER_ATTR_RO(version); + +/* --------------------------------------------------------------------- */ + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + +/* wlsw_emulstate ------------------------------------------------------ */ +static ssize_t wlsw_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_wlsw_emulstate); +} + +static ssize_t wlsw_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + if (tpacpi_wlsw_emulstate != !!t) { + tpacpi_wlsw_emulstate = !!t; + tpacpi_rfk_update_hwblock_state(!t); /* negative logic */ + } + + return count; +} +static DRIVER_ATTR_RW(wlsw_emulstate); + +/* bluetooth_emulstate ------------------------------------------------- */ +static ssize_t bluetooth_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_bluetooth_emulstate); +} + +static ssize_t bluetooth_emulstate_store(struct device_driver *drv, + const char *buf, size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_bluetooth_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(bluetooth_emulstate); + +/* wwan_emulstate ------------------------------------------------- */ +static ssize_t wwan_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_wwan_emulstate); +} + +static ssize_t wwan_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_wwan_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(wwan_emulstate); + +/* uwb_emulstate ------------------------------------------------- */ +static ssize_t uwb_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_uwb_emulstate); +} + +static ssize_t uwb_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_uwb_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(uwb_emulstate); +#endif + +/************************************************************************* + * Firmware Data + */ + +/* + * Table of recommended minimum BIOS versions + * + * Reasons for listing: + * 1. Stable BIOS, listed because the unknown amount of + * bugs and bad ACPI behaviour on older versions + * + * 2. BIOS or EC fw with known bugs that trigger on Linux + * + * 3. BIOS with known reduced functionality in older versions + * + * We recommend the latest BIOS and EC version. + * We only support the latest BIOS and EC fw version as a rule. + * + * Sources: IBM ThinkPad Public Web Documents (update changelogs), + * Information from users in ThinkWiki + * + * WARNING: we use this table also to detect that the machine is + * a ThinkPad in some cases, so don't remove entries lightly. + */ + +#define TPV_Q(__v, __id1, __id2, __bv1, __bv2) \ + { .vendor = (__v), \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = TPACPI_MATCH_ANY_VERSION << 16 \ + | TPVER(__bv1, __bv2) } + +#define TPV_Q_X(__v, __bid1, __bid2, __bv1, __bv2, \ + __eid, __ev1, __ev2) \ + { .vendor = (__v), \ + .bios = TPID(__bid1, __bid2), \ + .ec = __eid, \ + .quirks = TPVER(__ev1, __ev2) << 16 \ + | TPVER(__bv1, __bv2) } + +#define TPV_QI0(__id1, __id2, __bv1, __bv2) \ + TPV_Q(PCI_VENDOR_ID_IBM, __id1, __id2, __bv1, __bv2) + +/* Outdated IBM BIOSes often lack the EC id string */ +#define TPV_QI1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ + __bv1, __bv2, TPID(__id1, __id2), \ + __ev1, __ev2), \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ + __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ + __ev1, __ev2) + +/* Outdated IBM BIOSes often lack the EC id string */ +#define TPV_QI2(__bid1, __bid2, __bv1, __bv2, \ + __eid1, __eid2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ + __bv1, __bv2, TPID(__eid1, __eid2), \ + __ev1, __ev2), \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ + __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ + __ev1, __ev2) + +#define TPV_QL0(__id1, __id2, __bv1, __bv2) \ + TPV_Q(PCI_VENDOR_ID_LENOVO, __id1, __id2, __bv1, __bv2) + +#define TPV_QL1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_LENOVO, __id1, __id2, \ + __bv1, __bv2, TPID(__id1, __id2), \ + __ev1, __ev2) + +#define TPV_QL2(__bid1, __bid2, __bv1, __bv2, \ + __eid1, __eid2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_LENOVO, __bid1, __bid2, \ + __bv1, __bv2, TPID(__eid1, __eid2), \ + __ev1, __ev2) + +static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = { + /* Numeric models ------------------ */ + /* FW MODEL BIOS VERS */ + TPV_QI0('I', 'M', '6', '5'), /* 570 */ + TPV_QI0('I', 'U', '2', '6'), /* 570E */ + TPV_QI0('I', 'B', '5', '4'), /* 600 */ + TPV_QI0('I', 'H', '4', '7'), /* 600E */ + TPV_QI0('I', 'N', '3', '6'), /* 600E */ + TPV_QI0('I', 'T', '5', '5'), /* 600X */ + TPV_QI0('I', 'D', '4', '8'), /* 770, 770E, 770ED */ + TPV_QI0('I', 'I', '4', '2'), /* 770X */ + TPV_QI0('I', 'O', '2', '3'), /* 770Z */ + + /* A-series ------------------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('I', 'W', '5', '9'), /* A20m */ + TPV_QI0('I', 'V', '6', '9'), /* A20p */ + TPV_QI0('1', '0', '2', '6'), /* A21e, A22e */ + TPV_QI0('K', 'U', '3', '6'), /* A21e */ + TPV_QI0('K', 'X', '3', '6'), /* A21m, A22m */ + TPV_QI0('K', 'Y', '3', '8'), /* A21p, A22p */ + TPV_QI0('1', 'B', '1', '7'), /* A22e */ + TPV_QI0('1', '3', '2', '0'), /* A22m */ + TPV_QI0('1', 'E', '7', '3'), /* A30/p (0) */ + TPV_QI1('1', 'G', '4', '1', '1', '7'), /* A31/p (0) */ + TPV_QI1('1', 'N', '1', '6', '0', '7'), /* A31/p (0) */ + + /* G-series ------------------------- */ + /* FW MODEL BIOS VERS */ + TPV_QI0('1', 'T', 'A', '6'), /* G40 */ + TPV_QI0('1', 'X', '5', '7'), /* G41 */ + + /* R-series, T-series --------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('1', 'C', 'F', '0'), /* R30 */ + TPV_QI0('1', 'F', 'F', '1'), /* R31 */ + TPV_QI0('1', 'M', '9', '7'), /* R32 */ + TPV_QI0('1', 'O', '6', '1'), /* R40 */ + TPV_QI0('1', 'P', '6', '5'), /* R40 */ + TPV_QI0('1', 'S', '7', '0'), /* R40e */ + TPV_QI1('1', 'R', 'D', 'R', '7', '1'), /* R50/p, R51, + T40/p, T41/p, T42/p (1) */ + TPV_QI1('1', 'V', '7', '1', '2', '8'), /* R50e, R51 (1) */ + TPV_QI1('7', '8', '7', '1', '0', '6'), /* R51e (1) */ + TPV_QI1('7', '6', '6', '9', '1', '6'), /* R52 (1) */ + TPV_QI1('7', '0', '6', '9', '2', '8'), /* R52, T43 (1) */ + + TPV_QI0('I', 'Y', '6', '1'), /* T20 */ + TPV_QI0('K', 'Z', '3', '4'), /* T21 */ + TPV_QI0('1', '6', '3', '2'), /* T22 */ + TPV_QI1('1', 'A', '6', '4', '2', '3'), /* T23 (0) */ + TPV_QI1('1', 'I', '7', '1', '2', '0'), /* T30 (0) */ + TPV_QI1('1', 'Y', '6', '5', '2', '9'), /* T43/p (1) */ + + TPV_QL1('7', '9', 'E', '3', '5', '0'), /* T60/p */ + TPV_QL1('7', 'C', 'D', '2', '2', '2'), /* R60, R60i */ + TPV_QL1('7', 'E', 'D', '0', '1', '5'), /* R60e, R60i */ + + /* BIOS FW BIOS VERS EC FW EC VERS */ + TPV_QI2('1', 'W', '9', '0', '1', 'V', '2', '8'), /* R50e (1) */ + TPV_QL2('7', 'I', '3', '4', '7', '9', '5', '0'), /* T60/p wide */ + + /* X-series ------------------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('I', 'Z', '9', 'D'), /* X20, X21 */ + TPV_QI0('1', 'D', '7', '0'), /* X22, X23, X24 */ + TPV_QI1('1', 'K', '4', '8', '1', '8'), /* X30 (0) */ + TPV_QI1('1', 'Q', '9', '7', '2', '3'), /* X31, X32 (0) */ + TPV_QI1('1', 'U', 'D', '3', 'B', '2'), /* X40 (0) */ + TPV_QI1('7', '4', '6', '4', '2', '7'), /* X41 (0) */ + TPV_QI1('7', '5', '6', '0', '2', '0'), /* X41t (0) */ + + TPV_QL1('7', 'B', 'D', '7', '4', '0'), /* X60/s */ + TPV_QL1('7', 'J', '3', '0', '1', '3'), /* X60t */ + + /* (0) - older versions lack DMI EC fw string and functionality */ + /* (1) - older versions known to lack functionality */ +}; + +#undef TPV_QL1 +#undef TPV_QL0 +#undef TPV_QI2 +#undef TPV_QI1 +#undef TPV_QI0 +#undef TPV_Q_X +#undef TPV_Q + +static void __init tpacpi_check_outdated_fw(void) +{ + unsigned long fwvers; + u16 ec_version, bios_version; + + fwvers = tpacpi_check_quirks(tpacpi_bios_version_qtable, + ARRAY_SIZE(tpacpi_bios_version_qtable)); + + if (!fwvers) + return; + + bios_version = fwvers & 0xffffU; + ec_version = (fwvers >> 16) & 0xffffU; + + /* note that unknown versions are set to 0x0000 and we use that */ + if ((bios_version > thinkpad_id.bios_release) || + (ec_version > thinkpad_id.ec_release && + ec_version != TPACPI_MATCH_ANY_VERSION)) { + /* + * The changelogs would let us track down the exact + * reason, but it is just too much of a pain to track + * it. We only list BIOSes that are either really + * broken, or really stable to begin with, so it is + * best if the user upgrades the firmware anyway. + */ + pr_warn("WARNING: Outdated ThinkPad BIOS/EC firmware\n"); + pr_warn("WARNING: This firmware may be missing critical bug fixes and/or important features\n"); + } +} + +static bool __init tpacpi_is_fw_known(void) +{ + return tpacpi_check_quirks(tpacpi_bios_version_qtable, + ARRAY_SIZE(tpacpi_bios_version_qtable)) != 0; +} + +/**************************************************************************** + **************************************************************************** + * + * Subdrivers + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * thinkpad-acpi metadata subdriver + */ + +static int thinkpad_acpi_driver_read(struct seq_file *m) +{ + seq_printf(m, "driver:\t\t%s\n", TPACPI_DESC); + seq_printf(m, "version:\t%s\n", TPACPI_VERSION); + return 0; +} + +static struct ibm_struct thinkpad_acpi_driver_data = { + .name = "driver", + .read = thinkpad_acpi_driver_read, +}; + +/************************************************************************* + * Hotkey subdriver + */ + +/* + * ThinkPad firmware event model + * + * The ThinkPad firmware has two main event interfaces: normal ACPI + * notifications (which follow the ACPI standard), and a private event + * interface. + * + * The private event interface also issues events for the hotkeys. As + * the driver gained features, the event handling code ended up being + * built around the hotkey subdriver. This will need to be refactored + * to a more formal event API eventually. + * + * Some "hotkeys" are actually supposed to be used as event reports, + * such as "brightness has changed", "volume has changed", depending on + * the ThinkPad model and how the firmware is operating. + * + * Unlike other classes, hotkey-class events have mask/unmask control on + * non-ancient firmware. However, how it behaves changes a lot with the + * firmware model and version. + */ + +enum { /* hot key scan codes (derived from ACPI DSDT) */ + TP_ACPI_HOTKEYSCAN_FNF1 = 0, + TP_ACPI_HOTKEYSCAN_FNF2, + TP_ACPI_HOTKEYSCAN_FNF3, + TP_ACPI_HOTKEYSCAN_FNF4, + TP_ACPI_HOTKEYSCAN_FNF5, + TP_ACPI_HOTKEYSCAN_FNF6, + TP_ACPI_HOTKEYSCAN_FNF7, + TP_ACPI_HOTKEYSCAN_FNF8, + TP_ACPI_HOTKEYSCAN_FNF9, + TP_ACPI_HOTKEYSCAN_FNF10, + TP_ACPI_HOTKEYSCAN_FNF11, + TP_ACPI_HOTKEYSCAN_FNF12, + TP_ACPI_HOTKEYSCAN_FNBACKSPACE, + TP_ACPI_HOTKEYSCAN_FNINSERT, + TP_ACPI_HOTKEYSCAN_FNDELETE, + TP_ACPI_HOTKEYSCAN_FNHOME, + TP_ACPI_HOTKEYSCAN_FNEND, + TP_ACPI_HOTKEYSCAN_FNPAGEUP, + TP_ACPI_HOTKEYSCAN_FNPAGEDOWN, + TP_ACPI_HOTKEYSCAN_FNSPACE, + TP_ACPI_HOTKEYSCAN_VOLUMEUP, + TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, + TP_ACPI_HOTKEYSCAN_MUTE, + TP_ACPI_HOTKEYSCAN_THINKPAD, + TP_ACPI_HOTKEYSCAN_UNK1, + TP_ACPI_HOTKEYSCAN_UNK2, + TP_ACPI_HOTKEYSCAN_MICMUTE, + TP_ACPI_HOTKEYSCAN_UNK4, + TP_ACPI_HOTKEYSCAN_CONFIG, + TP_ACPI_HOTKEYSCAN_SEARCH, + TP_ACPI_HOTKEYSCAN_SCALE, + TP_ACPI_HOTKEYSCAN_FILE, + + /* Adaptive keyboard keycodes */ + TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, /* 32 / 0x20 */ + TP_ACPI_HOTKEYSCAN_MUTE2 = TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, + TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, + TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, + TP_ACPI_HOTKEYSCAN_CLOUD, + TP_ACPI_HOTKEYSCAN_UNK9, + TP_ACPI_HOTKEYSCAN_VOICE, + TP_ACPI_HOTKEYSCAN_UNK10, + TP_ACPI_HOTKEYSCAN_GESTURES, + TP_ACPI_HOTKEYSCAN_UNK11, + TP_ACPI_HOTKEYSCAN_UNK12, + TP_ACPI_HOTKEYSCAN_UNK13, + TP_ACPI_HOTKEYSCAN_CONFIG2, + TP_ACPI_HOTKEYSCAN_NEW_TAB, + TP_ACPI_HOTKEYSCAN_RELOAD, + TP_ACPI_HOTKEYSCAN_BACK, + TP_ACPI_HOTKEYSCAN_MIC_DOWN, + TP_ACPI_HOTKEYSCAN_MIC_UP, + TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, + TP_ACPI_HOTKEYSCAN_CAMERA_MODE, + TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, + + /* Lenovo extended keymap, starting at 0x1300 */ + TP_ACPI_HOTKEYSCAN_EXTENDED_START, /* 52 / 0x34 */ + /* first new observed key (star, favorites) is 0x1311 */ + TP_ACPI_HOTKEYSCAN_STAR = 69, + TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, + TP_ACPI_HOTKEYSCAN_CALCULATOR, + TP_ACPI_HOTKEYSCAN_BLUETOOTH, + TP_ACPI_HOTKEYSCAN_KEYBOARD, + TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, /* Used by "Lenovo Quick Clean" */ + TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, + TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, + TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, +}; + +enum { /* Keys/events available through NVRAM polling */ + TPACPI_HKEY_NVRAM_KNOWN_MASK = 0x00fb88c0U, + TPACPI_HKEY_NVRAM_GOOD_MASK = 0x00fb8000U, +}; + +enum { /* Positions of some of the keys in hotkey masks */ + TP_ACPI_HKEY_DISPSWTCH_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF7, + TP_ACPI_HKEY_DISPXPAND_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF8, + TP_ACPI_HKEY_HIBERNATE_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF12, + TP_ACPI_HKEY_BRGHTUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNHOME, + TP_ACPI_HKEY_BRGHTDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNEND, + TP_ACPI_HKEY_KBD_LIGHT_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNPAGEUP, + TP_ACPI_HKEY_ZOOM_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNSPACE, + TP_ACPI_HKEY_VOLUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEUP, + TP_ACPI_HKEY_VOLDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, + TP_ACPI_HKEY_MUTE_MASK = 1 << TP_ACPI_HOTKEYSCAN_MUTE, + TP_ACPI_HKEY_THINKPAD_MASK = 1 << TP_ACPI_HOTKEYSCAN_THINKPAD, +}; + +enum { /* NVRAM to ACPI HKEY group map */ + TP_NVRAM_HKEY_GROUP_HK2 = TP_ACPI_HKEY_THINKPAD_MASK | + TP_ACPI_HKEY_ZOOM_MASK | + TP_ACPI_HKEY_DISPSWTCH_MASK | + TP_ACPI_HKEY_HIBERNATE_MASK, + TP_NVRAM_HKEY_GROUP_BRIGHTNESS = TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK, + TP_NVRAM_HKEY_GROUP_VOLUME = TP_ACPI_HKEY_VOLUP_MASK | + TP_ACPI_HKEY_VOLDWN_MASK | + TP_ACPI_HKEY_MUTE_MASK, +}; + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +struct tp_nvram_state { + u16 thinkpad_toggle:1; + u16 zoom_toggle:1; + u16 display_toggle:1; + u16 thinklight_toggle:1; + u16 hibernate_toggle:1; + u16 displayexp_toggle:1; + u16 display_state:1; + u16 brightness_toggle:1; + u16 volume_toggle:1; + u16 mute:1; + + u8 brightness_level; + u8 volume_level; +}; + +/* kthread for the hotkey poller */ +static struct task_struct *tpacpi_hotkey_task; + +/* + * Acquire mutex to write poller control variables as an + * atomic block. + * + * Increment hotkey_config_change when changing them if you + * want the kthread to forget old state. + * + * See HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END + */ +static struct mutex hotkey_thread_data_mutex; +static unsigned int hotkey_config_change; + +/* + * hotkey poller control variables + * + * Must be atomic or readers will also need to acquire mutex + * + * HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END + * should be used only when the changes need to be taken as + * a block, OR when one needs to force the kthread to forget + * old state. + */ +static u32 hotkey_source_mask; /* bit mask 0=ACPI,1=NVRAM */ +static unsigned int hotkey_poll_freq = 10; /* Hz */ + +#define HOTKEY_CONFIG_CRITICAL_START \ + do { \ + mutex_lock(&hotkey_thread_data_mutex); \ + hotkey_config_change++; \ + } while (0); +#define HOTKEY_CONFIG_CRITICAL_END \ + mutex_unlock(&hotkey_thread_data_mutex); + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +#define hotkey_source_mask 0U +#define HOTKEY_CONFIG_CRITICAL_START +#define HOTKEY_CONFIG_CRITICAL_END + +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static struct mutex hotkey_mutex; + +static enum { /* Reasons for waking up */ + TP_ACPI_WAKEUP_NONE = 0, /* None or unknown */ + TP_ACPI_WAKEUP_BAYEJ, /* Bay ejection request */ + TP_ACPI_WAKEUP_UNDOCK, /* Undock request */ +} hotkey_wakeup_reason; + +static int hotkey_autosleep_ack; + +static u32 hotkey_orig_mask; /* events the BIOS had enabled */ +static u32 hotkey_all_mask; /* all events supported in fw */ +static u32 hotkey_adaptive_all_mask; /* all adaptive events supported in fw */ +static u32 hotkey_reserved_mask; /* events better left disabled */ +static u32 hotkey_driver_mask; /* events needed by the driver */ +static u32 hotkey_user_mask; /* events visible to userspace */ +static u32 hotkey_acpi_mask; /* events enabled in firmware */ + +static bool tpacpi_driver_event(const unsigned int hkey_event); +static void hotkey_poll_setup(const bool may_warn); + +/* HKEY.MHKG() return bits */ +#define TP_HOTKEY_TABLET_MASK (1 << 3) +enum { + TP_ACPI_MULTI_MODE_INVALID = 0, + TP_ACPI_MULTI_MODE_UNKNOWN = 1 << 0, + TP_ACPI_MULTI_MODE_LAPTOP = 1 << 1, + TP_ACPI_MULTI_MODE_TABLET = 1 << 2, + TP_ACPI_MULTI_MODE_FLAT = 1 << 3, + TP_ACPI_MULTI_MODE_STAND = 1 << 4, + TP_ACPI_MULTI_MODE_TENT = 1 << 5, + TP_ACPI_MULTI_MODE_STAND_TENT = 1 << 6, +}; + +enum { + /* The following modes are considered tablet mode for the purpose of + * reporting the status to userspace. i.e. in all these modes it makes + * sense to disable the laptop input devices such as touchpad and + * keyboard. + */ + TP_ACPI_MULTI_MODE_TABLET_LIKE = TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT | + TP_ACPI_MULTI_MODE_STAND_TENT, +}; + +static int hotkey_get_wlsw(void) +{ + int status; + + if (!tp_features.hotkey_wlsw) + return -ENODEV; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wlswemul) + return (tpacpi_wlsw_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "WLSW", "d")) + return -EIO; + + return (status) ? TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int hotkey_gmms_get_tablet_mode(int s, int *has_tablet_mode) +{ + int type = (s >> 16) & 0xffff; + int value = s & 0xffff; + int mode = TP_ACPI_MULTI_MODE_INVALID; + int valid_modes = 0; + + if (has_tablet_mode) + *has_tablet_mode = 0; + + switch (type) { + case 1: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND_TENT; + break; + case 2: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT; + break; + case 3: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT; + break; + case 4: + case 5: + /* In mode 4, FLAT is not specified as a valid mode. However, + * it can be seen at least on the X1 Yoga 2nd Generation. + */ + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT; + break; + default: + pr_err("Unknown multi mode status type %d with value 0x%04X, please report this to %s\n", + type, value, TPACPI_MAIL); + return 0; + } + + if (has_tablet_mode && (valid_modes & TP_ACPI_MULTI_MODE_TABLET_LIKE)) + *has_tablet_mode = 1; + + switch (value) { + case 1: + mode = TP_ACPI_MULTI_MODE_LAPTOP; + break; + case 2: + mode = TP_ACPI_MULTI_MODE_FLAT; + break; + case 3: + mode = TP_ACPI_MULTI_MODE_TABLET; + break; + case 4: + if (type == 1) + mode = TP_ACPI_MULTI_MODE_STAND_TENT; + else + mode = TP_ACPI_MULTI_MODE_STAND; + break; + case 5: + mode = TP_ACPI_MULTI_MODE_TENT; + break; + default: + if (type == 5 && value == 0xffff) { + pr_warn("Multi mode status is undetected, assuming laptop\n"); + return 0; + } + } + + if (!(mode & valid_modes)) { + pr_err("Unknown/reserved multi mode value 0x%04X for type %d, please report this to %s\n", + value, type, TPACPI_MAIL); + return 0; + } + + return !!(mode & TP_ACPI_MULTI_MODE_TABLET_LIKE); +} + +static int hotkey_get_tablet_mode(int *status) +{ + int s; + + switch (tp_features.hotkey_tablet) { + case TP_HOTKEY_TABLET_USES_MHKG: + if (!acpi_evalf(hkey_handle, &s, "MHKG", "d")) + return -EIO; + + *status = ((s & TP_HOTKEY_TABLET_MASK) != 0); + break; + case TP_HOTKEY_TABLET_USES_GMMS: + if (!acpi_evalf(hkey_handle, &s, "GMMS", "dd", 0)) + return -EIO; + + *status = hotkey_gmms_get_tablet_mode(s, NULL); + break; + default: + break; + } + + return 0; +} + +/* + * Reads current event mask from firmware, and updates + * hotkey_acpi_mask accordingly. Also resets any bits + * from hotkey_user_mask that are unavailable to be + * delivered (shadow requirement of the userspace ABI). + */ +static int hotkey_mask_get(void) +{ + lockdep_assert_held(&hotkey_mutex); + + if (tp_features.hotkey_mask) { + u32 m = 0; + + if (!acpi_evalf(hkey_handle, &m, "DHKN", "d")) + return -EIO; + + hotkey_acpi_mask = m; + } else { + /* no mask support doesn't mean no event support... */ + hotkey_acpi_mask = hotkey_all_mask; + } + + /* sync userspace-visible mask */ + hotkey_user_mask &= (hotkey_acpi_mask | hotkey_source_mask); + + return 0; +} + +static void hotkey_mask_warn_incomplete_mask(void) +{ + /* log only what the user can fix... */ + const u32 wantedmask = hotkey_driver_mask & + ~(hotkey_acpi_mask | hotkey_source_mask) & + (hotkey_all_mask | TPACPI_HKEY_NVRAM_KNOWN_MASK); + + if (wantedmask) + pr_notice("required events 0x%08x not enabled!\n", wantedmask); +} + +/* + * Set the firmware mask when supported + * + * Also calls hotkey_mask_get to update hotkey_acpi_mask. + * + * NOTE: does not set bits in hotkey_user_mask, but may reset them. + */ +static int hotkey_mask_set(u32 mask) +{ + int i; + int rc = 0; + + const u32 fwmask = mask & ~hotkey_source_mask; + + lockdep_assert_held(&hotkey_mutex); + + if (tp_features.hotkey_mask) { + for (i = 0; i < 32; i++) { + if (!acpi_evalf(hkey_handle, + NULL, "MHKM", "vdd", i + 1, + !!(mask & (1 << i)))) { + rc = -EIO; + break; + } + } + } + + /* + * We *must* make an inconditional call to hotkey_mask_get to + * refresh hotkey_acpi_mask and update hotkey_user_mask + * + * Take the opportunity to also log when we cannot _enable_ + * a given event. + */ + if (!hotkey_mask_get() && !rc && (fwmask & ~hotkey_acpi_mask)) { + pr_notice("asked for hotkey mask 0x%08x, but firmware forced it to 0x%08x\n", + fwmask, hotkey_acpi_mask); + } + + if (tpacpi_lifecycle != TPACPI_LIFE_EXITING) + hotkey_mask_warn_incomplete_mask(); + + return rc; +} + +/* + * Sets hotkey_user_mask and tries to set the firmware mask + */ +static int hotkey_user_mask_set(const u32 mask) +{ + int rc; + + lockdep_assert_held(&hotkey_mutex); + + /* Give people a chance to notice they are doing something that + * is bound to go boom on their users sooner or later */ + if (!tp_warned.hotkey_mask_ff && + (mask == 0xffff || mask == 0xffffff || + mask == 0xffffffff)) { + tp_warned.hotkey_mask_ff = 1; + pr_notice("setting the hotkey mask to 0x%08x is likely not the best way to go about it\n", + mask); + pr_notice("please consider using the driver defaults, and refer to up-to-date thinkpad-acpi documentation\n"); + } + + /* Try to enable what the user asked for, plus whatever we need. + * this syncs everything but won't enable bits in hotkey_user_mask */ + rc = hotkey_mask_set((mask | hotkey_driver_mask) & ~hotkey_source_mask); + + /* Enable the available bits in hotkey_user_mask */ + hotkey_user_mask = mask & (hotkey_acpi_mask | hotkey_source_mask); + + return rc; +} + +/* + * Sets the driver hotkey mask. + * + * Can be called even if the hotkey subdriver is inactive + */ +static int tpacpi_hotkey_driver_mask_set(const u32 mask) +{ + int rc; + + /* Do the right thing if hotkey_init has not been called yet */ + if (!tp_features.hotkey) { + hotkey_driver_mask = mask; + return 0; + } + + mutex_lock(&hotkey_mutex); + + HOTKEY_CONFIG_CRITICAL_START + hotkey_driver_mask = mask; +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_source_mask |= (mask & ~hotkey_all_mask); +#endif + HOTKEY_CONFIG_CRITICAL_END + + rc = hotkey_mask_set((hotkey_acpi_mask | hotkey_driver_mask) & + ~hotkey_source_mask); + hotkey_poll_setup(true); + + mutex_unlock(&hotkey_mutex); + + return rc; +} + +static int hotkey_status_get(int *status) +{ + if (!acpi_evalf(hkey_handle, status, "DHKC", "d")) + return -EIO; + + return 0; +} + +static int hotkey_status_set(bool enable) +{ + if (!acpi_evalf(hkey_handle, NULL, "MHKC", "vd", enable ? 1 : 0)) + return -EIO; + + return 0; +} + +static void tpacpi_input_send_tabletsw(void) +{ + int state; + + if (tp_features.hotkey_tablet && + !hotkey_get_tablet_mode(&state)) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, + SW_TABLET_MODE, !!state); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } +} + +#define GCES_NO_SHUTTER_DEVICE BIT(31) + +static int get_camera_shutter(void) +{ + acpi_handle gces_handle; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GCES", &gces_handle))) + return -ENODEV; + + if (!acpi_evalf(gces_handle, &output, NULL, "dd", 0)) + return -EIO; + + if (output & GCES_NO_SHUTTER_DEVICE) + return -ENODEV; + + return output; +} + +static bool tpacpi_input_send_key(const u32 hkey, bool *send_acpi_ev) +{ + bool known_ev; + u32 scancode; + + if (tpacpi_driver_event(hkey)) + return true; + + /* + * Before the conversion to using the sparse-keymap helpers the driver used to + * map the hkey event codes to 0x00 - 0x4d scancodes so that a straight scancode + * indexed array could be used to map scancodes to keycodes: + * + * 0x1001 - 0x1020 -> 0x00 - 0x1f (Original ThinkPad events) + * 0x1103 - 0x1116 -> 0x20 - 0x33 (Adaptive keyboard, 2014 X1 Carbon) + * 0x1300 - 0x1319 -> 0x34 - 0x4d (Additional keys send in 2017+ models) + * + * The sparse-keymap tables still use these scancodes for these ranges to + * preserve userspace API compatibility (e.g. hwdb keymappings). + */ + if (hkey >= TP_HKEY_EV_ORIG_KEY_START && + hkey <= TP_HKEY_EV_ORIG_KEY_END) { + scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; + if (!(hotkey_user_mask & (1 << scancode))) + return true; /* Not reported but still a known code */ + } else if (hkey >= TP_HKEY_EV_ADAPTIVE_KEY_START && + hkey <= TP_HKEY_EV_ADAPTIVE_KEY_END) { + scancode = hkey - TP_HKEY_EV_ADAPTIVE_KEY_START + + TP_ACPI_HOTKEYSCAN_ADAPTIVE_START; + } else if (hkey >= TP_HKEY_EV_EXTENDED_KEY_START && + hkey <= TP_HKEY_EV_EXTENDED_KEY_END) { + scancode = hkey - TP_HKEY_EV_EXTENDED_KEY_START + + TP_ACPI_HOTKEYSCAN_EXTENDED_START; + } else { + /* + * Do not send ACPI netlink events for unknown hotkeys, to + * avoid userspace starting to rely on them. Instead these + * should be added to the keymap to send evdev events. + */ + if (send_acpi_ev) + *send_acpi_ev = false; + + scancode = hkey; + } + + mutex_lock(&tpacpi_inputdev_send_mutex); + known_ev = sparse_keymap_report_event(tpacpi_inputdev, scancode, 1, true); + mutex_unlock(&tpacpi_inputdev_send_mutex); + + return known_ev; +} + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver; + +/* Do NOT call without validating scancode first */ +static void tpacpi_hotkey_send_key(unsigned int scancode) +{ + tpacpi_input_send_key(TP_HKEY_EV_ORIG_KEY_START + scancode, NULL); +} + +static void hotkey_read_nvram(struct tp_nvram_state *n, const u32 m) +{ + u8 d; + + if (m & TP_NVRAM_HKEY_GROUP_HK2) { + d = nvram_read_byte(TP_NVRAM_ADDR_HK2); + n->thinkpad_toggle = !!(d & TP_NVRAM_MASK_HKT_THINKPAD); + n->zoom_toggle = !!(d & TP_NVRAM_MASK_HKT_ZOOM); + n->display_toggle = !!(d & TP_NVRAM_MASK_HKT_DISPLAY); + n->hibernate_toggle = !!(d & TP_NVRAM_MASK_HKT_HIBERNATE); + } + if (m & TP_ACPI_HKEY_KBD_LIGHT_MASK) { + d = nvram_read_byte(TP_NVRAM_ADDR_THINKLIGHT); + n->thinklight_toggle = !!(d & TP_NVRAM_MASK_THINKLIGHT); + } + if (m & TP_ACPI_HKEY_DISPXPAND_MASK) { + d = nvram_read_byte(TP_NVRAM_ADDR_VIDEO); + n->displayexp_toggle = + !!(d & TP_NVRAM_MASK_HKT_DISPEXPND); + } + if (m & TP_NVRAM_HKEY_GROUP_BRIGHTNESS) { + d = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); + n->brightness_level = (d & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; + n->brightness_toggle = + !!(d & TP_NVRAM_MASK_HKT_BRIGHTNESS); + } + if (m & TP_NVRAM_HKEY_GROUP_VOLUME) { + d = nvram_read_byte(TP_NVRAM_ADDR_MIXER); + n->volume_level = (d & TP_NVRAM_MASK_LEVEL_VOLUME) + >> TP_NVRAM_POS_LEVEL_VOLUME; + n->mute = !!(d & TP_NVRAM_MASK_MUTE); + n->volume_toggle = !!(d & TP_NVRAM_MASK_HKT_VOLUME); + } +} + +#define TPACPI_COMPARE_KEY(__scancode, __member) \ +do { \ + if ((event_mask & (1 << __scancode)) && \ + oldn->__member != newn->__member) \ + tpacpi_hotkey_send_key(__scancode); \ +} while (0) + +#define TPACPI_MAY_SEND_KEY(__scancode) \ +do { \ + if (event_mask & (1 << __scancode)) \ + tpacpi_hotkey_send_key(__scancode); \ +} while (0) + +static void issue_volchange(const unsigned int oldvol, + const unsigned int newvol, + const u32 event_mask) +{ + unsigned int i = oldvol; + + while (i > newvol) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + i--; + } + while (i < newvol) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + i++; + } +} + +static void issue_brightnesschange(const unsigned int oldbrt, + const unsigned int newbrt, + const u32 event_mask) +{ + unsigned int i = oldbrt; + + while (i > newbrt) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + i--; + } + while (i < newbrt) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + i++; + } +} + +static void hotkey_compare_and_issue_event(struct tp_nvram_state *oldn, + struct tp_nvram_state *newn, + const u32 event_mask) +{ + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_THINKPAD, thinkpad_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNSPACE, zoom_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF7, display_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF12, hibernate_toggle); + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNPAGEUP, thinklight_toggle); + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF8, displayexp_toggle); + + /* + * Handle volume + * + * This code is supposed to duplicate the IBM firmware behaviour: + * - Pressing MUTE issues mute hotkey message, even when already mute + * - Pressing Volume up/down issues volume up/down hotkey messages, + * even when already at maximum or minimum volume + * - The act of unmuting issues volume up/down notification, + * depending which key was used to unmute + * + * We are constrained to what the NVRAM can tell us, which is not much + * and certainly not enough if more than one volume hotkey was pressed + * since the last poll cycle. + * + * Just to make our life interesting, some newer Lenovo ThinkPads have + * bugs in the BIOS and may fail to update volume_toggle properly. + */ + if (newn->mute) { + /* muted */ + if (!oldn->mute || + oldn->volume_toggle != newn->volume_toggle || + oldn->volume_level != newn->volume_level) { + /* recently muted, or repeated mute keypress, or + * multiple presses ending in mute */ + issue_volchange(oldn->volume_level, newn->volume_level, + event_mask); + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); + } + } else { + /* unmute */ + if (oldn->mute) { + /* recently unmuted, issue 'unmute' keypress */ + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } + if (oldn->volume_level != newn->volume_level) { + issue_volchange(oldn->volume_level, newn->volume_level, + event_mask); + } else if (oldn->volume_toggle != newn->volume_toggle) { + /* repeated vol up/down keypress at end of scale ? */ + if (newn->volume_level == 0) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + else if (newn->volume_level >= TP_NVRAM_LEVEL_VOLUME_MAX) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } + } + + /* handle brightness */ + if (oldn->brightness_level != newn->brightness_level) { + issue_brightnesschange(oldn->brightness_level, + newn->brightness_level, event_mask); + } else if (oldn->brightness_toggle != newn->brightness_toggle) { + /* repeated key presses that didn't change state */ + if (newn->brightness_level == 0) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + else if (newn->brightness_level >= bright_maxlvl + && !tp_features.bright_unkfw) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + } + +#undef TPACPI_COMPARE_KEY +#undef TPACPI_MAY_SEND_KEY +} + +/* + * Polling driver + * + * We track all events in hotkey_source_mask all the time, since + * most of them are edge-based. We only issue those requested by + * hotkey_user_mask or hotkey_driver_mask, though. + */ +static int hotkey_kthread(void *data) +{ + struct tp_nvram_state s[2] = { 0 }; + u32 poll_mask, event_mask; + unsigned int si, so; + unsigned long t; + unsigned int change_detector; + unsigned int poll_freq; + bool was_frozen; + + if (tpacpi_lifecycle == TPACPI_LIFE_EXITING) + return 0; + + set_freezable(); + + so = 0; + si = 1; + t = 0; + + /* Initial state for compares */ + mutex_lock(&hotkey_thread_data_mutex); + change_detector = hotkey_config_change; + poll_mask = hotkey_source_mask; + event_mask = hotkey_source_mask & + (hotkey_driver_mask | hotkey_user_mask); + poll_freq = hotkey_poll_freq; + mutex_unlock(&hotkey_thread_data_mutex); + hotkey_read_nvram(&s[so], poll_mask); + + while (!kthread_should_stop()) { + if (t == 0) { + if (likely(poll_freq)) + t = 1000/poll_freq; + else + t = 100; /* should never happen... */ + } + t = msleep_interruptible(t); + if (unlikely(kthread_freezable_should_stop(&was_frozen))) + break; + + if (t > 0 && !was_frozen) + continue; + + mutex_lock(&hotkey_thread_data_mutex); + if (was_frozen || hotkey_config_change != change_detector) { + /* forget old state on thaw or config change */ + si = so; + t = 0; + change_detector = hotkey_config_change; + } + poll_mask = hotkey_source_mask; + event_mask = hotkey_source_mask & + (hotkey_driver_mask | hotkey_user_mask); + poll_freq = hotkey_poll_freq; + mutex_unlock(&hotkey_thread_data_mutex); + + if (likely(poll_mask)) { + hotkey_read_nvram(&s[si], poll_mask); + if (likely(si != so)) { + hotkey_compare_and_issue_event(&s[so], &s[si], + event_mask); + } + } + + so = si; + si ^= 1; + } + + return 0; +} + +static void hotkey_poll_stop_sync(void) +{ + lockdep_assert_held(&hotkey_mutex); + + if (tpacpi_hotkey_task) { + kthread_stop(tpacpi_hotkey_task); + tpacpi_hotkey_task = NULL; + } +} + +static void hotkey_poll_setup(const bool may_warn) +{ + const u32 poll_driver_mask = hotkey_driver_mask & hotkey_source_mask; + const u32 poll_user_mask = hotkey_user_mask & hotkey_source_mask; + + lockdep_assert_held(&hotkey_mutex); + + if (hotkey_poll_freq > 0 && + (poll_driver_mask || + (poll_user_mask && tpacpi_inputdev->users > 0))) { + if (!tpacpi_hotkey_task) { + tpacpi_hotkey_task = kthread_run(hotkey_kthread, + NULL, TPACPI_NVRAM_KTHREAD_NAME); + if (IS_ERR(tpacpi_hotkey_task)) { + tpacpi_hotkey_task = NULL; + pr_err("could not create kernel thread for hotkey polling\n"); + } + } + } else { + hotkey_poll_stop_sync(); + if (may_warn && (poll_driver_mask || poll_user_mask) && + hotkey_poll_freq == 0) { + pr_notice("hot keys 0x%08x and/or events 0x%08x require polling, which is currently disabled\n", + poll_user_mask, poll_driver_mask); + } + } +} + +static void hotkey_poll_setup_safe(const bool may_warn) +{ + mutex_lock(&hotkey_mutex); + hotkey_poll_setup(may_warn); + mutex_unlock(&hotkey_mutex); +} + +static void hotkey_poll_set_freq(unsigned int freq) +{ + lockdep_assert_held(&hotkey_mutex); + + if (!freq) + hotkey_poll_stop_sync(); + + hotkey_poll_freq = freq; +} + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static void hotkey_poll_setup(const bool __unused) +{ +} + +static void hotkey_poll_setup_safe(const bool __unused) +{ +} + +static void hotkey_poll_stop_sync(void) +{ +} +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static int hotkey_inputdev_open(struct input_dev *dev) +{ + switch (tpacpi_lifecycle) { + case TPACPI_LIFE_INIT: + case TPACPI_LIFE_RUNNING: + hotkey_poll_setup_safe(false); + return 0; + case TPACPI_LIFE_EXITING: + return -EBUSY; + } + + /* Should only happen if tpacpi_lifecycle is corrupt */ + BUG(); + return -EBUSY; +} + +static void hotkey_inputdev_close(struct input_dev *dev) +{ + /* disable hotkey polling when possible */ + if (tpacpi_lifecycle != TPACPI_LIFE_EXITING && + !(hotkey_source_mask & hotkey_driver_mask)) + hotkey_poll_setup_safe(false); +} + +/* sysfs hotkey enable ------------------------------------------------- */ +static ssize_t hotkey_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, status; + + printk_deprecated_attribute("hotkey_enable", + "Hotkey reporting is always enabled"); + + res = hotkey_status_get(&status); + if (res) + return res; + + return sysfs_emit(buf, "%d\n", status); +} + +static ssize_t hotkey_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + + printk_deprecated_attribute("hotkey_enable", + "Hotkeys can be disabled through hotkey_mask"); + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + if (t == 0) + return -EPERM; + + return count; +} + +static DEVICE_ATTR_RW(hotkey_enable); + +/* sysfs hotkey mask --------------------------------------------------- */ +static ssize_t hotkey_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", hotkey_user_mask); +} + +static ssize_t hotkey_mask_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, 0xffffffffUL, &t)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + res = hotkey_user_mask_set(t); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_poll_setup(true); +#endif + + mutex_unlock(&hotkey_mutex); + + tpacpi_disclose_usertask("hotkey_mask", "set to 0x%08lx\n", t); + + return (res) ? res : count; +} + +static DEVICE_ATTR_RW(hotkey_mask); + +/* sysfs hotkey bios_enabled ------------------------------------------- */ +static ssize_t hotkey_bios_enabled_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static DEVICE_ATTR_RO(hotkey_bios_enabled); + +/* sysfs hotkey bios_mask ---------------------------------------------- */ +static ssize_t hotkey_bios_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + printk_deprecated_attribute("hotkey_bios_mask", + "This attribute is useless."); + return sysfs_emit(buf, "0x%08x\n", hotkey_orig_mask); +} + +static DEVICE_ATTR_RO(hotkey_bios_mask); + +/* sysfs hotkey all_mask ----------------------------------------------- */ +static ssize_t hotkey_all_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + hotkey_all_mask | hotkey_source_mask); +} + +static DEVICE_ATTR_RO(hotkey_all_mask); + +/* sysfs hotkey all_mask ----------------------------------------------- */ +static ssize_t hotkey_adaptive_all_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + hotkey_adaptive_all_mask | hotkey_source_mask); +} + +static DEVICE_ATTR_RO(hotkey_adaptive_all_mask); + +/* sysfs hotkey recommended_mask --------------------------------------- */ +static ssize_t hotkey_recommended_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + (hotkey_all_mask | hotkey_source_mask) + & ~hotkey_reserved_mask); +} + +static DEVICE_ATTR_RO(hotkey_recommended_mask); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + +/* sysfs hotkey hotkey_source_mask ------------------------------------- */ +static ssize_t hotkey_source_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", hotkey_source_mask); +} + +static ssize_t hotkey_source_mask_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + u32 r_ev; + int rc; + + if (parse_strtoul(buf, 0xffffffffUL, &t) || + ((t & ~TPACPI_HKEY_NVRAM_KNOWN_MASK) != 0)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + HOTKEY_CONFIG_CRITICAL_START + hotkey_source_mask = t; + HOTKEY_CONFIG_CRITICAL_END + + rc = hotkey_mask_set((hotkey_user_mask | hotkey_driver_mask) & + ~hotkey_source_mask); + hotkey_poll_setup(true); + + /* check if events needed by the driver got disabled */ + r_ev = hotkey_driver_mask & ~(hotkey_acpi_mask & hotkey_all_mask) + & ~hotkey_source_mask & TPACPI_HKEY_NVRAM_KNOWN_MASK; + + mutex_unlock(&hotkey_mutex); + + if (rc < 0) + pr_err("hotkey_source_mask: failed to update the firmware event mask!\n"); + + if (r_ev) + pr_notice("hotkey_source_mask: some important events were disabled: 0x%04x\n", + r_ev); + + tpacpi_disclose_usertask("hotkey_source_mask", "set to 0x%08lx\n", t); + + return (rc < 0) ? rc : count; +} + +static DEVICE_ATTR_RW(hotkey_source_mask); + +/* sysfs hotkey hotkey_poll_freq --------------------------------------- */ +static ssize_t hotkey_poll_freq_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_poll_freq); +} + +static ssize_t hotkey_poll_freq_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 25, &t)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + hotkey_poll_set_freq(t); + hotkey_poll_setup(true); + + mutex_unlock(&hotkey_mutex); + + tpacpi_disclose_usertask("hotkey_poll_freq", "set to %lu\n", t); + + return count; +} + +static DEVICE_ATTR_RW(hotkey_poll_freq); + +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +/* sysfs hotkey radio_sw (pollable) ------------------------------------ */ +static ssize_t hotkey_radio_sw_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + res = hotkey_get_wlsw(); + if (res < 0) + return res; + + /* Opportunistic update */ + tpacpi_rfk_update_hwblock_state((res == TPACPI_RFK_RADIO_OFF)); + + return sysfs_emit(buf, "%d\n", + (res == TPACPI_RFK_RADIO_OFF) ? 0 : 1); +} + +static DEVICE_ATTR_RO(hotkey_radio_sw); + +static void hotkey_radio_sw_notify_change(void) +{ + if (tp_features.hotkey_wlsw) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "hotkey_radio_sw"); +} + +/* sysfs hotkey tablet mode (pollable) --------------------------------- */ +static ssize_t hotkey_tablet_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, s; + res = hotkey_get_tablet_mode(&s); + if (res < 0) + return res; + + return sysfs_emit(buf, "%d\n", !!s); +} + +static DEVICE_ATTR_RO(hotkey_tablet_mode); + +static void hotkey_tablet_mode_notify_change(void) +{ + if (tp_features.hotkey_tablet) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "hotkey_tablet_mode"); +} + +/* sysfs wakeup reason (pollable) -------------------------------------- */ +static ssize_t hotkey_wakeup_reason_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_wakeup_reason); +} + +static DEVICE_ATTR(wakeup_reason, S_IRUGO, hotkey_wakeup_reason_show, NULL); + +static void hotkey_wakeup_reason_notify_change(void) +{ + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "wakeup_reason"); +} + +/* sysfs wakeup hotunplug_complete (pollable) -------------------------- */ +static ssize_t hotkey_wakeup_hotunplug_complete_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_autosleep_ack); +} + +static DEVICE_ATTR(wakeup_hotunplug_complete, S_IRUGO, + hotkey_wakeup_hotunplug_complete_show, NULL); + +static void hotkey_wakeup_hotunplug_complete_notify_change(void) +{ + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "wakeup_hotunplug_complete"); +} + +/* sysfs adaptive kbd mode --------------------------------------------- */ + +static int adaptive_keyboard_get_mode(void); +static int adaptive_keyboard_set_mode(int new_mode); + +enum ADAPTIVE_KEY_MODE { + HOME_MODE, + WEB_BROWSER_MODE, + WEB_CONFERENCE_MODE, + FUNCTION_MODE, + LAYFLAT_MODE +}; + +static ssize_t adaptive_kbd_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int current_mode; + + current_mode = adaptive_keyboard_get_mode(); + if (current_mode < 0) + return current_mode; + + return sysfs_emit(buf, "%d\n", current_mode); +} + +static ssize_t adaptive_kbd_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, LAYFLAT_MODE, &t)) + return -EINVAL; + + res = adaptive_keyboard_set_mode(t); + return (res < 0) ? res : count; +} + +static DEVICE_ATTR_RW(adaptive_kbd_mode); + +static struct attribute *adaptive_kbd_attributes[] = { + &dev_attr_adaptive_kbd_mode.attr, + NULL +}; + +static umode_t hadaptive_kbd_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.has_adaptive_kbd ? attr->mode : 0; +} + +static const struct attribute_group adaptive_kbd_attr_group = { + .is_visible = hadaptive_kbd_attr_is_visible, + .attrs = adaptive_kbd_attributes, +}; + +/* sysfs doubletap enable --------------------------------------------- */ +static ssize_t doubletap_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", tp_features.trackpoint_doubletap_enable); +} + +static ssize_t doubletap_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + bool enable; + int err; + + err = kstrtobool(buf, &enable); + if (err) + return err; + + tp_features.trackpoint_doubletap_enable = enable; + return count; +} + +static DEVICE_ATTR_RW(doubletap_enable); + +/* --------------------------------------------------------------------- */ + +static struct attribute *hotkey_attributes[] = { + &dev_attr_hotkey_enable.attr, + &dev_attr_hotkey_bios_enabled.attr, + &dev_attr_hotkey_bios_mask.attr, + &dev_attr_wakeup_reason.attr, + &dev_attr_wakeup_hotunplug_complete.attr, + &dev_attr_hotkey_mask.attr, + &dev_attr_hotkey_all_mask.attr, + &dev_attr_hotkey_adaptive_all_mask.attr, + &dev_attr_hotkey_recommended_mask.attr, + &dev_attr_hotkey_tablet_mode.attr, + &dev_attr_hotkey_radio_sw.attr, + &dev_attr_doubletap_enable.attr, +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + &dev_attr_hotkey_source_mask.attr, + &dev_attr_hotkey_poll_freq.attr, +#endif + NULL +}; + +static umode_t hotkey_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &dev_attr_hotkey_tablet_mode.attr) { + if (!tp_features.hotkey_tablet) + return 0; + } else if (attr == &dev_attr_hotkey_radio_sw.attr) { + if (!tp_features.hotkey_wlsw) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group hotkey_attr_group = { + .is_visible = hotkey_attr_is_visible, + .attrs = hotkey_attributes, +}; + +/* + * Sync both the hw and sw blocking state of all switches + */ +static void tpacpi_send_radiosw_update(void) +{ + int wlsw; + + /* + * We must sync all rfkill controllers *before* issuing any + * rfkill input events, or we will race the rfkill core input + * handler. + * + * tpacpi_inputdev_send_mutex works as a synchronization point + * for the above. + * + * We optimize to avoid numerous calls to hotkey_get_wlsw. + */ + + wlsw = hotkey_get_wlsw(); + + /* Sync hw blocking state first if it is hw-blocked */ + if (wlsw == TPACPI_RFK_RADIO_OFF) + tpacpi_rfk_update_hwblock_state(true); + + /* Sync hw blocking state last if it is hw-unblocked */ + if (wlsw == TPACPI_RFK_RADIO_ON) + tpacpi_rfk_update_hwblock_state(false); + + /* Issue rfkill input event for WLSW switch */ + if (!(wlsw < 0)) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, + SW_RFKILL_ALL, (wlsw > 0)); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } + + /* + * this can be unconditional, as we will poll state again + * if userspace uses the notify to read data + */ + hotkey_radio_sw_notify_change(); +} + +static void hotkey_exit(void) +{ + mutex_lock(&hotkey_mutex); + hotkey_poll_stop_sync(); + dbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_HKEY, + "restoring original HKEY status and mask\n"); + /* yes, there is a bitwise or below, we want the + * functions to be called even if one of them fail */ + if (((tp_features.hotkey_mask && + hotkey_mask_set(hotkey_orig_mask)) | + hotkey_status_set(false)) != 0) + pr_err("failed to restore hot key mask to BIOS defaults\n"); + + mutex_unlock(&hotkey_mutex); +} + +/* + * HKEY quirks: + * TPACPI_HK_Q_INIMASK: Supports FN+F3,FN+F4,FN+F12 + */ + +#define TPACPI_HK_Q_INIMASK 0x0001 + +static const struct tpacpi_quirk tpacpi_hotkey_qtable[] __initconst = { + TPACPI_Q_IBM('I', 'H', TPACPI_HK_Q_INIMASK), /* 600E */ + TPACPI_Q_IBM('I', 'N', TPACPI_HK_Q_INIMASK), /* 600E */ + TPACPI_Q_IBM('I', 'D', TPACPI_HK_Q_INIMASK), /* 770, 770E, 770ED */ + TPACPI_Q_IBM('I', 'W', TPACPI_HK_Q_INIMASK), /* A20m */ + TPACPI_Q_IBM('I', 'V', TPACPI_HK_Q_INIMASK), /* A20p */ + TPACPI_Q_IBM('1', '0', TPACPI_HK_Q_INIMASK), /* A21e, A22e */ + TPACPI_Q_IBM('K', 'U', TPACPI_HK_Q_INIMASK), /* A21e */ + TPACPI_Q_IBM('K', 'X', TPACPI_HK_Q_INIMASK), /* A21m, A22m */ + TPACPI_Q_IBM('K', 'Y', TPACPI_HK_Q_INIMASK), /* A21p, A22p */ + TPACPI_Q_IBM('1', 'B', TPACPI_HK_Q_INIMASK), /* A22e */ + TPACPI_Q_IBM('1', '3', TPACPI_HK_Q_INIMASK), /* A22m */ + TPACPI_Q_IBM('1', 'E', TPACPI_HK_Q_INIMASK), /* A30/p (0) */ + TPACPI_Q_IBM('1', 'C', TPACPI_HK_Q_INIMASK), /* R30 */ + TPACPI_Q_IBM('1', 'F', TPACPI_HK_Q_INIMASK), /* R31 */ + TPACPI_Q_IBM('I', 'Y', TPACPI_HK_Q_INIMASK), /* T20 */ + TPACPI_Q_IBM('K', 'Z', TPACPI_HK_Q_INIMASK), /* T21 */ + TPACPI_Q_IBM('1', '6', TPACPI_HK_Q_INIMASK), /* T22 */ + TPACPI_Q_IBM('I', 'Z', TPACPI_HK_Q_INIMASK), /* X20, X21 */ + TPACPI_Q_IBM('1', 'D', TPACPI_HK_Q_INIMASK), /* X22, X23, X24 */ +}; + +static int hotkey_init_tablet_mode(void) +{ + int in_tablet_mode = 0, res; + char *type = NULL; + + if (acpi_evalf(hkey_handle, &res, "GMMS", "qdd", 0)) { + int has_tablet_mode; + + in_tablet_mode = hotkey_gmms_get_tablet_mode(res, + &has_tablet_mode); + /* + * The Yoga 11e series has 2 accelerometers described by a + * BOSC0200 ACPI node. This setup relies on a Windows service + * which calls special ACPI methods on this node to report + * the laptop/tent/tablet mode to the EC. The bmc150 iio driver + * does not support this, so skip the hotkey on these models. + */ + if (has_tablet_mode && !dual_accel_detect()) + tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_GMMS; + type = "GMMS"; + } else if (acpi_evalf(hkey_handle, &res, "MHKG", "qd")) { + /* For X41t, X60t, X61t Tablets... */ + tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_MHKG; + in_tablet_mode = !!(res & TP_HOTKEY_TABLET_MASK); + type = "MHKG"; + } + + if (!tp_features.hotkey_tablet) + return 0; + + pr_info("Tablet mode switch found (type: %s), currently in %s mode\n", + type, in_tablet_mode ? "tablet" : "laptop"); + + return in_tablet_mode; +} + +static const struct key_entry keymap_ibm[] __initconst = { + /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_BATTERY } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_COFFEE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_FN_F6 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, + /* Brightness: firmware always reacts, suppressed through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, + /* Thinklight: firmware always reacts, suppressed through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, + /* + * Volume: firmware always reacts and reprograms the built-in *extra* mixer. + * Suppressed by default through hotkey_reserved_mask. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, + { KE_END } +}; + +static const struct key_entry keymap_lenovo[] __initconst = { + /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_COFFEE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_BATTERY } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_CAMERA, } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, + /* + * These should be enabled --only-- when ACPI video is disabled and + * are handled in a special way by the init code. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, + /* Suppressed by default through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, + /* + * Volume: z60/z61, T60 (BIOS version?): firmware always reacts and + * reprograms the built-in *extra* mixer. + * T60?, T61, R60?, R61: firmware and EC tries to send these over + * the regular keyboard (not through tpacpi). There are still weird bugs + * re. MUTE. May cause the BIOS to interfere with the HDA mixer. + * Suppressed by default through hotkey_reserved_mask. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MICMUTE, { KEY_MICMUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG, { KEY_CONFIG } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_SEARCH, { KEY_SEARCH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_SCALE, { KEY_SCALE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FILE, { KEY_FILE } }, + /* Adaptive keyboard mappings for Carbon X1 2014 translated scancodes 0x20 - 0x33 */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE2, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, { KEY_BRIGHTNESS_MIN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLOUD, { KEY_XFER } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK9, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOICE, { KEY_VOICECOMMAND } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK10, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_GESTURES, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK11, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK12, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK13, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG2, { KEY_CONFIG } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_NEW_TAB, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_RELOAD, { KEY_REFRESH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BACK, { KEY_BACK } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_DOWN, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_UP, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CAMERA_MODE, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, { KEY_RESERVED } }, + /* Extended hotkeys mappings translated scancodes 0x34 - 0x4d */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_STAR, { KEY_BOOKMARKS } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CALCULATOR, { KEY_CALC } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BLUETOOTH, { KEY_BLUETOOTH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_KEYBOARD, { KEY_KEYBOARD } }, + /* Used by "Lenovo Quick Clean" */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, { KEY_FN_RIGHT_SHIFT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, { KEY_NOTIFICATION_CENTER } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, { KEY_PICKUP_PHONE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, { KEY_HANGUP_PHONE } }, + /* + * All mapping below are for raw untranslated hkey event codes mapped directly + * after switching to sparse keymap support. The mappings above use translated + * scancodes to preserve uAPI compatibility, see tpacpi_input_send_key(). + */ + { KE_KEY, 0x131d, { KEY_VENDOR } }, /* System debug info, similar to old ThinkPad key */ + { KE_KEY, 0x1320, { KEY_LINK_PHONE } }, + { KE_KEY, 0x1402, { KEY_LINK_PHONE } }, + { KE_KEY, TP_HKEY_EV_TRACK_DOUBLETAP /* 0x8036 */, { KEY_PROG4 } }, + { KE_END } +}; + +static int __init hotkey_init(struct ibm_init_struct *iibm) +{ + enum keymap_index { + TPACPI_KEYMAP_IBM_GENERIC = 0, + TPACPI_KEYMAP_LENOVO_GENERIC, + }; + + static const struct tpacpi_quirk tpacpi_keymap_qtable[] __initconst = { + /* Generic maps (fallback) */ + { + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_KEYMAP_IBM_GENERIC, + }, + { + .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_KEYMAP_LENOVO_GENERIC, + }, + }; + + unsigned long keymap_id, quirks; + const struct key_entry *keymap; + bool radiosw_state = false; + bool tabletsw_state = false; + int hkeyv, res, status, camera_shutter_state; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "initializing hotkey subdriver\n"); + + BUG_ON(!tpacpi_inputdev); + BUG_ON(tpacpi_inputdev->open != NULL || + tpacpi_inputdev->close != NULL); + + TPACPI_ACPIHANDLE_INIT(hkey); + mutex_init(&hotkey_mutex); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + mutex_init(&hotkey_thread_data_mutex); +#endif + + /* hotkey not supported on 570 */ + tp_features.hotkey = hkey_handle != NULL; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkeys are %s\n", + str_supported(tp_features.hotkey)); + + if (!tp_features.hotkey) + return -ENODEV; + + quirks = tpacpi_check_quirks(tpacpi_hotkey_qtable, + ARRAY_SIZE(tpacpi_hotkey_qtable)); + + tpacpi_disable_brightness_delay(); + + /* mask not supported on 600e/x, 770e, 770x, A21e, A2xm/p, + A30, R30, R31, T20-22, X20-21, X22-24. Detected by checking + for HKEY interface version 0x100 */ + if (acpi_evalf(hkey_handle, &hkeyv, "MHKV", "qd")) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "firmware HKEY interface version: 0x%x\n", + hkeyv); + + switch (hkeyv >> 8) { + case 1: + /* + * MHKV 0x100 in A31, R40, R40e, + * T4x, X31, and later + */ + + /* Paranoia check AND init hotkey_all_mask */ + if (!acpi_evalf(hkey_handle, &hotkey_all_mask, + "MHKA", "qd")) { + pr_err("missing MHKA handler, please report this to %s\n", + TPACPI_MAIL); + /* Fallback: pre-init for FN+F3,F4,F12 */ + hotkey_all_mask = 0x080cU; + } else { + tp_features.hotkey_mask = 1; + } + break; + + case 2: + /* + * MHKV 0x200 in X1, T460s, X260, T560, X1 Tablet (2016) + */ + + /* Paranoia check AND init hotkey_all_mask */ + if (!acpi_evalf(hkey_handle, &hotkey_all_mask, + "MHKA", "dd", 1)) { + pr_err("missing MHKA handler, please report this to %s\n", + TPACPI_MAIL); + /* Fallback: pre-init for FN+F3,F4,F12 */ + hotkey_all_mask = 0x080cU; + } else { + tp_features.hotkey_mask = 1; + } + + /* + * Check if we have an adaptive keyboard, like on the + * Lenovo Carbon X1 2014 (2nd Gen). + */ + if (acpi_evalf(hkey_handle, &hotkey_adaptive_all_mask, + "MHKA", "dd", 2)) { + if (hotkey_adaptive_all_mask != 0) + tp_features.has_adaptive_kbd = true; + } else { + tp_features.has_adaptive_kbd = false; + hotkey_adaptive_all_mask = 0x0U; + } + break; + + default: + pr_err("unknown version of the HKEY interface: 0x%x\n", + hkeyv); + pr_err("please report this to %s\n", TPACPI_MAIL); + break; + } + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkey masks are %s\n", + str_supported(tp_features.hotkey_mask)); + + /* Init hotkey_all_mask if not initialized yet */ + if (!tp_features.hotkey_mask && !hotkey_all_mask && + (quirks & TPACPI_HK_Q_INIMASK)) + hotkey_all_mask = 0x080cU; /* FN+F12, FN+F4, FN+F3 */ + + /* Init hotkey_acpi_mask and hotkey_orig_mask */ + if (tp_features.hotkey_mask) { + /* hotkey_source_mask *must* be zero for + * the first hotkey_mask_get to return hotkey_orig_mask */ + mutex_lock(&hotkey_mutex); + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + if (res) + return res; + + hotkey_orig_mask = hotkey_acpi_mask; + } else { + hotkey_orig_mask = hotkey_all_mask; + hotkey_acpi_mask = hotkey_all_mask; + } + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wlswemul) { + tp_features.hotkey_wlsw = 1; + radiosw_state = !!tpacpi_wlsw_emulstate; + pr_info("radio switch emulation enabled\n"); + } else +#endif + /* Not all thinkpads have a hardware radio switch */ + if (acpi_evalf(hkey_handle, &status, "WLSW", "qd")) { + tp_features.hotkey_wlsw = 1; + radiosw_state = !!status; + pr_info("radio switch found; radios are %s\n", str_enabled_disabled(status & BIT(0))); + } + + tabletsw_state = hotkey_init_tablet_mode(); + + /* Set up key map */ + keymap_id = tpacpi_check_quirks(tpacpi_keymap_qtable, + ARRAY_SIZE(tpacpi_keymap_qtable)); + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "using keymap number %lu\n", keymap_id); + + /* Keys which should be reserved on both IBM and Lenovo models */ + hotkey_reserved_mask = TP_ACPI_HKEY_KBD_LIGHT_MASK | + TP_ACPI_HKEY_VOLUP_MASK | + TP_ACPI_HKEY_VOLDWN_MASK | + TP_ACPI_HKEY_MUTE_MASK; + /* + * Reserve brightness up/down unconditionally on IBM models, on Lenovo + * models these are disabled based on acpi_video_get_backlight_type(). + */ + if (keymap_id == TPACPI_KEYMAP_IBM_GENERIC) { + hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK; + keymap = keymap_ibm; + } else { + keymap = keymap_lenovo; + } + + res = sparse_keymap_setup(tpacpi_inputdev, keymap, NULL); + if (res) + return res; + + camera_shutter_state = get_camera_shutter(); + if (camera_shutter_state >= 0) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_CAMERA_LENS_COVER); + input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); + } + + if (tp_features.hotkey_wlsw) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_RFKILL_ALL); + input_report_switch(tpacpi_inputdev, + SW_RFKILL_ALL, radiosw_state); + } + if (tp_features.hotkey_tablet) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_TABLET_MODE); + input_report_switch(tpacpi_inputdev, + SW_TABLET_MODE, tabletsw_state); + } + + /* Do not issue duplicate brightness change events to + * userspace. tpacpi_detect_brightness_capabilities() must have + * been called before this point */ + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { + pr_info("This ThinkPad has standard ACPI backlight brightness control, supported by the ACPI video driver\n"); + pr_notice("Disabling thinkpad-acpi brightness events by default...\n"); + + /* Disable brightness up/down on Lenovo thinkpads when + * ACPI is handling them, otherwise it is plain impossible + * for userspace to do something even remotely sane */ + hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK; + } + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_source_mask = TPACPI_HKEY_NVRAM_GOOD_MASK + & ~hotkey_all_mask + & ~hotkey_reserved_mask; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkey source mask 0x%08x, polling freq %u\n", + hotkey_source_mask, hotkey_poll_freq); +#endif + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "enabling firmware HKEY event interface...\n"); + res = hotkey_status_set(true); + if (res) { + hotkey_exit(); + return res; + } + mutex_lock(&hotkey_mutex); + res = hotkey_mask_set(((hotkey_all_mask & ~hotkey_reserved_mask) + | hotkey_driver_mask) + & ~hotkey_source_mask); + mutex_unlock(&hotkey_mutex); + if (res < 0 && res != -ENXIO) { + hotkey_exit(); + return res; + } + hotkey_user_mask = (hotkey_acpi_mask | hotkey_source_mask) + & ~hotkey_reserved_mask; + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "initial masks: user=0x%08x, fw=0x%08x, poll=0x%08x\n", + hotkey_user_mask, hotkey_acpi_mask, hotkey_source_mask); + + tpacpi_inputdev->open = &hotkey_inputdev_open; + tpacpi_inputdev->close = &hotkey_inputdev_close; + + hotkey_poll_setup_safe(true); + + /* Enable TrackPoint doubletap event reporting by default. */ + tp_features.trackpoint_doubletap_enable = 1; + + return 0; +} + +/* Thinkpad X1 Carbon support 5 modes including Home mode, Web browser + * mode, Web conference mode, Function mode and Lay-flat mode. + * We support Home mode and Function mode currently. + * + * Will consider support rest of modes in future. + * + */ +static const int adaptive_keyboard_modes[] = { + HOME_MODE, +/* WEB_BROWSER_MODE = 2, + WEB_CONFERENCE_MODE = 3, */ + FUNCTION_MODE +}; + +/* press Fn key a while second, it will switch to Function Mode. Then + * release Fn key, previous mode be restored. + */ +static bool adaptive_keyboard_mode_is_saved; +static int adaptive_keyboard_prev_mode; + +static int adaptive_keyboard_get_mode(void) +{ + int mode = 0; + + if (!acpi_evalf(hkey_handle, &mode, "GTRW", "dd", 0)) { + pr_err("Cannot read adaptive keyboard mode\n"); + return -EIO; + } + + return mode; +} + +static int adaptive_keyboard_set_mode(int new_mode) +{ + if (new_mode < 0 || + new_mode > LAYFLAT_MODE) + return -EINVAL; + + if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", new_mode)) { + pr_err("Cannot set adaptive keyboard mode\n"); + return -EIO; + } + + return 0; +} + +static int adaptive_keyboard_get_next_mode(int mode) +{ + size_t i; + size_t max_mode = ARRAY_SIZE(adaptive_keyboard_modes) - 1; + + for (i = 0; i <= max_mode; i++) { + if (adaptive_keyboard_modes[i] == mode) + break; + } + + if (i >= max_mode) + i = 0; + else + i++; + + return adaptive_keyboard_modes[i]; +} + +static void adaptive_keyboard_change_row(void) +{ + int mode; + + if (adaptive_keyboard_mode_is_saved) { + mode = adaptive_keyboard_prev_mode; + adaptive_keyboard_mode_is_saved = false; + } else { + mode = adaptive_keyboard_get_mode(); + if (mode < 0) + return; + mode = adaptive_keyboard_get_next_mode(mode); + } + + adaptive_keyboard_set_mode(mode); +} + +static void adaptive_keyboard_s_quickview_row(void) +{ + int mode; + + mode = adaptive_keyboard_get_mode(); + if (mode < 0) + return; + + adaptive_keyboard_prev_mode = mode; + adaptive_keyboard_mode_is_saved = true; + + adaptive_keyboard_set_mode(FUNCTION_MODE); +} + +/* 0x1000-0x1FFF: key presses */ +static bool hotkey_notify_hotkey(const u32 hkey, bool *send_acpi_ev) +{ + /* Never send ACPI netlink events for original hotkeys (hkey: 0x1001 - 0x1020) */ + if (hkey >= TP_HKEY_EV_ORIG_KEY_START && hkey <= TP_HKEY_EV_ORIG_KEY_END) { + *send_acpi_ev = false; + + /* Original hotkeys may be polled from NVRAM instead */ + unsigned int scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; + if (hotkey_source_mask & (1 << scancode)) + return true; + } + + return tpacpi_input_send_key(hkey, send_acpi_ev); +} + +/* 0x2000-0x2FFF: Wakeup reason */ +static bool hotkey_notify_wakeup(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_WKUP_S3_UNDOCK: /* suspend, undock */ + case TP_HKEY_EV_WKUP_S4_UNDOCK: /* hibernation, undock */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_UNDOCK; + *send_acpi_ev = false; + break; + + case TP_HKEY_EV_WKUP_S3_BAYEJ: /* suspend, bay eject */ + case TP_HKEY_EV_WKUP_S4_BAYEJ: /* hibernation, bay eject */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_BAYEJ; + *send_acpi_ev = false; + break; + + case TP_HKEY_EV_WKUP_S3_BATLOW: /* Battery on critical low level/S3 */ + case TP_HKEY_EV_WKUP_S4_BATLOW: /* Battery on critical low level/S4 */ + pr_alert("EMERGENCY WAKEUP: battery almost empty\n"); + /* how to auto-heal: */ + /* 2313: woke up from S3, go to S4/S5 */ + /* 2413: woke up from S4, go to S5 */ + break; + + default: + return false; + } + + if (hotkey_wakeup_reason != TP_ACPI_WAKEUP_NONE) { + pr_info("woke up due to a hot-unplug request...\n"); + hotkey_wakeup_reason_notify_change(); + } + return true; +} + +/* 0x4000-0x4FFF: dock-related events */ +static bool hotkey_notify_dockevent(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_UNDOCK_ACK: + /* ACPI undock operation completed after wakeup */ + hotkey_autosleep_ack = 1; + pr_info("undocked\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + return true; + + case TP_HKEY_EV_HOTPLUG_DOCK: /* docked to port replicator */ + pr_info("docked into hotplug port replicator\n"); + return true; + case TP_HKEY_EV_HOTPLUG_UNDOCK: /* undocked from port replicator */ + pr_info("undocked from hotplug port replicator\n"); + return true; + + /* + * Deliberately ignore attaching and detaching the keybord cover to avoid + * duplicates from intel-vbtn, which already emits SW_TABLET_MODE events + * to userspace. + * + * Please refer to the following thread for more information and a preliminary + * implementation using the GTOP ("Get Tablet OPtions") interface that could be + * extended to other attachment options of the ThinkPad X1 Tablet series, such as + * the Pico cartridge dock module: + * https://lore.kernel.org/platform-driver-x86/38cb8265-1e30-d547-9e12-b4ae290be737@a-kobel.de/ + */ + case TP_HKEY_EV_KBD_COVER_ATTACH: + case TP_HKEY_EV_KBD_COVER_DETACH: + *send_acpi_ev = false; + return true; + + default: + return false; + } +} + +/* 0x5000-0x5FFF: human interface helpers */ +static bool hotkey_notify_usrevent(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_PEN_INSERTED: /* X61t: tablet pen inserted into bay */ + case TP_HKEY_EV_PEN_REMOVED: /* X61t: tablet pen removed from bay */ + return true; + + case TP_HKEY_EV_TABLET_TABLET: /* X41t-X61t: tablet mode */ + case TP_HKEY_EV_TABLET_NOTEBOOK: /* X41t-X61t: normal mode */ + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_LID_CLOSE: /* Lid closed */ + case TP_HKEY_EV_LID_OPEN: /* Lid opened */ + case TP_HKEY_EV_BRGHT_CHANGED: /* brightness changed */ + /* do not propagate these events */ + *send_acpi_ev = false; + return true; + + default: + return false; + } +} + +static void thermal_dump_all_sensors(void); +static void palmsensor_refresh(void); + +/* 0x6000-0x6FFF: thermal alarms/notices and keyboard events */ +static bool hotkey_notify_6xxx(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_THM_TABLE_CHANGED: + pr_debug("EC reports: Thermal Table has changed\n"); + /* recommended action: do nothing, we don't have + * Lenovo ATM information */ + return true; + case TP_HKEY_EV_THM_CSM_COMPLETED: + pr_debug("EC reports: Thermal Control Command set completed (DYTC)\n"); + /* Thermal event - pass on to event handler */ + tpacpi_driver_event(hkey); + return true; + case TP_HKEY_EV_THM_TRANSFM_CHANGED: + pr_debug("EC reports: Thermal Transformation changed (GMTS)\n"); + /* recommended action: do nothing, we don't have + * Lenovo ATM information */ + return true; + case TP_HKEY_EV_ALARM_BAT_HOT: + pr_crit("THERMAL ALARM: battery is too hot!\n"); + /* recommended action: warn user through gui */ + break; + case TP_HKEY_EV_ALARM_BAT_XHOT: + pr_alert("THERMAL EMERGENCY: battery is extremely hot!\n"); + /* recommended action: immediate sleep/hibernate */ + break; + case TP_HKEY_EV_ALARM_BAT_LIM_CHANGE: + pr_debug("Battery Info: battery charge threshold changed\n"); + /* User changed charging threshold. No action needed */ + return true; + case TP_HKEY_EV_ALARM_SENSOR_HOT: + pr_crit("THERMAL ALARM: a sensor reports something is too hot!\n"); + /* recommended action: warn user through gui, that */ + /* some internal component is too hot */ + break; + case TP_HKEY_EV_ALARM_SENSOR_XHOT: + pr_alert("THERMAL EMERGENCY: a sensor reports something is extremely hot!\n"); + /* recommended action: immediate sleep/hibernate */ + break; + case TP_HKEY_EV_AC_CHANGED: + /* X120e, X121e, X220, X220i, X220t, X230, T420, T420s, W520: + * AC status changed; can be triggered by plugging or + * unplugging AC adapter, docking or undocking. */ + + fallthrough; + + case TP_HKEY_EV_KEY_NUMLOCK: + case TP_HKEY_EV_KEY_FN: + /* key press events, we just ignore them as long as the EC + * is still reporting them in the normal keyboard stream */ + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_KEY_FN_ESC: + /* Get the media key status to force the status LED to update */ + acpi_evalf(hkey_handle, NULL, "GMKS", "v"); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_TABLET_CHANGED: + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_PALM_DETECTED: + case TP_HKEY_EV_PALM_UNDETECTED: + /* palm detected - pass on to event handler */ + palmsensor_refresh(); + return true; + + default: + /* report simply as unknown, no sensor dump */ + return false; + } + + thermal_dump_all_sensors(); + return true; +} + +static bool hotkey_notify_8xxx(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_TRACK_DOUBLETAP: + /* Only send event if doubletap is enabled */ + if (!tp_features.trackpoint_doubletap_enable) + *send_acpi_ev = false; + return true; + default: + return false; + } +} + +static void hotkey_notify(struct ibm_struct *ibm, u32 event) +{ + u32 hkey; + bool send_acpi_ev; + bool known_ev; + + if (event != 0x80) { + pr_err("unknown HKEY notification event %d\n", event); + /* forward it to userspace, maybe it knows how to handle it */ + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + dev_name(&ibm->acpi->device->dev), + event, 0); + return; + } + + while (1) { + if (!acpi_evalf(hkey_handle, &hkey, "MHKP", "d")) { + pr_err("failed to retrieve HKEY event\n"); + return; + } + + if (hkey == 0) { + /* queue empty */ + return; + } + + send_acpi_ev = true; + known_ev = false; + + switch (hkey >> 12) { + case 1: + /* 0x1000-0x1FFF: key presses */ + known_ev = hotkey_notify_hotkey(hkey, &send_acpi_ev); + break; + case 2: + /* 0x2000-0x2FFF: Wakeup reason */ + known_ev = hotkey_notify_wakeup(hkey, &send_acpi_ev); + break; + case 3: + /* 0x3000-0x3FFF: bay-related wakeups */ + switch (hkey) { + case TP_HKEY_EV_BAYEJ_ACK: + hotkey_autosleep_ack = 1; + pr_info("bay ejected\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + known_ev = true; + break; + case TP_HKEY_EV_OPTDRV_EJ: + /* FIXME: kick libata if SATA link offline */ + known_ev = true; + break; + } + break; + case 4: + /* 0x4000-0x4FFF: dock-related events */ + known_ev = hotkey_notify_dockevent(hkey, &send_acpi_ev); + break; + case 5: + /* 0x5000-0x5FFF: human interface helpers */ + known_ev = hotkey_notify_usrevent(hkey, &send_acpi_ev); + break; + case 6: + /* 0x6000-0x6FFF: thermal alarms/notices and + * keyboard events */ + known_ev = hotkey_notify_6xxx(hkey, &send_acpi_ev); + break; + case 7: + /* 0x7000-0x7FFF: misc */ + if (tp_features.hotkey_wlsw && + hkey == TP_HKEY_EV_RFKILL_CHANGED) { + tpacpi_send_radiosw_update(); + send_acpi_ev = false; + known_ev = true; + } + break; + case 8: + /* 0x8000-0x8FFF: misc2 */ + known_ev = hotkey_notify_8xxx(hkey, &send_acpi_ev); + break; + } + if (!known_ev) { + pr_notice("unhandled HKEY event 0x%04x\n", hkey); + pr_notice("please report the conditions when this event happened to %s\n", + TPACPI_MAIL); + } + + /* netlink events */ + if (send_acpi_ev) { + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + dev_name(&ibm->acpi->device->dev), + event, hkey); + } + } +} + +static void hotkey_suspend(void) +{ + /* Do these on suspend, we get the events on early resume! */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_NONE; + hotkey_autosleep_ack = 0; + + /* save previous mode of adaptive keyboard of X1 Carbon */ + if (tp_features.has_adaptive_kbd) { + if (!acpi_evalf(hkey_handle, &adaptive_keyboard_prev_mode, + "GTRW", "dd", 0)) { + pr_err("Cannot read adaptive keyboard mode.\n"); + } + } +} + +static void hotkey_resume(void) +{ + tpacpi_disable_brightness_delay(); + + mutex_lock(&hotkey_mutex); + if (hotkey_status_set(true) < 0 || + hotkey_mask_set(hotkey_acpi_mask) < 0) + pr_err("error while attempting to reset the event firmware interface\n"); + mutex_unlock(&hotkey_mutex); + + tpacpi_send_radiosw_update(); + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + hotkey_wakeup_reason_notify_change(); + hotkey_wakeup_hotunplug_complete_notify_change(); + hotkey_poll_setup_safe(false); + + /* restore previous mode of adapive keyboard of X1 Carbon */ + if (tp_features.has_adaptive_kbd) { + if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", + adaptive_keyboard_prev_mode)) { + pr_err("Cannot set adaptive keyboard mode.\n"); + } + } +} + +/* procfs -------------------------------------------------------------- */ +static int hotkey_read(struct seq_file *m) +{ + int res, status; + + if (!tp_features.hotkey) { + seq_puts(m, "status:\t\tnot supported\n"); + return 0; + } + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + res = hotkey_status_get(&status); + if (!res) + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + if (res) + return res; + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status & BIT(0))); + if (hotkey_all_mask) { + seq_printf(m, "mask:\t\t0x%08x\n", hotkey_user_mask); + seq_puts(m, "commands:\tenable, disable, reset, \n"); + } else { + seq_puts(m, "mask:\t\tnot supported\n"); + seq_puts(m, "commands:\tenable, disable, reset\n"); + } + + return 0; +} + +static void hotkey_enabledisable_warn(bool enable) +{ + tpacpi_log_usertask("procfs hotkey enable/disable"); + if (!WARN((tpacpi_lifecycle == TPACPI_LIFE_RUNNING || !enable), + pr_fmt("hotkey enable/disable functionality has been removed from the driver. Hotkeys are always enabled.\n"))) + pr_err("Please remove the hotkey=enable module parameter, it is deprecated. Hotkeys are always enabled.\n"); +} + +static int hotkey_write(char *buf) +{ + int res; + u32 mask; + char *cmd; + + if (!tp_features.hotkey) + return -ENODEV; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + mask = hotkey_user_mask; + + res = 0; + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "enable")) { + hotkey_enabledisable_warn(1); + } else if (strstarts(cmd, "disable")) { + hotkey_enabledisable_warn(0); + res = -EPERM; + } else if (strstarts(cmd, "reset")) { + mask = (hotkey_all_mask | hotkey_source_mask) + & ~hotkey_reserved_mask; + } else if (sscanf(cmd, "0x%x", &mask) == 1) { + /* mask set */ + } else if (sscanf(cmd, "%x", &mask) == 1) { + /* mask set */ + } else { + res = -EINVAL; + goto errexit; + } + } + + if (!res) { + tpacpi_disclose_usertask("procfs hotkey", + "set mask to 0x%08x\n", mask); + res = hotkey_user_mask_set(mask); + } + +errexit: + mutex_unlock(&hotkey_mutex); + return res; +} + +static const struct acpi_device_id ibm_htk_device_ids[] = { + {TPACPI_ACPI_IBM_HKEY_HID, 0}, + {TPACPI_ACPI_LENOVO_HKEY_HID, 0}, + {TPACPI_ACPI_LENOVO_HKEY_V2_HID, 0}, + {"", 0}, +}; + +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver = { + .hid = ibm_htk_device_ids, + .notify = hotkey_notify, + .handle = &hkey_handle, + .type = ACPI_DEVICE_NOTIFY, +}; + +static struct ibm_struct hotkey_driver_data = { + .name = "hotkey", + .read = hotkey_read, + .write = hotkey_write, + .exit = hotkey_exit, + .resume = hotkey_resume, + .suspend = hotkey_suspend, + .acpi = &ibm_hotkey_acpidriver, +}; + +/************************************************************************* + * Bluetooth subdriver + */ + +enum { + /* ACPI GBDC/SBDC bits */ + TP_ACPI_BLUETOOTH_HWPRESENT = 0x01, /* Bluetooth hw available */ + TP_ACPI_BLUETOOTH_RADIOSSW = 0x02, /* Bluetooth radio enabled */ + TP_ACPI_BLUETOOTH_RESUMECTRL = 0x04, /* Bluetooth state at resume: + 0 = disable, 1 = enable */ +}; + +enum { + /* ACPI \BLTH commands */ + TP_ACPI_BLTH_GET_ULTRAPORT_ID = 0x00, /* Get Ultraport BT ID */ + TP_ACPI_BLTH_GET_PWR_ON_RESUME = 0x01, /* Get power-on-resume state */ + TP_ACPI_BLTH_PWR_ON_ON_RESUME = 0x02, /* Resume powered on */ + TP_ACPI_BLTH_PWR_OFF_ON_RESUME = 0x03, /* Resume powered off */ + TP_ACPI_BLTH_SAVE_STATE = 0x05, /* Save state for S4/S5 */ +}; + +#define TPACPI_RFK_BLUETOOTH_SW_NAME "tpacpi_bluetooth_sw" + +static int bluetooth_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) + return (tpacpi_bluetooth_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GBDC", "d")) + return -EIO; + + return ((status & TP_ACPI_BLUETOOTH_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int bluetooth_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s bluetooth\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) { + tpacpi_bluetooth_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_BLUETOOTH_RADIOSSW + | TP_ACPI_BLUETOOTH_RESUMECTRL; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SBDC", "vd", status)) + return -EIO; + + return 0; +} + +/* sysfs bluetooth enable ---------------------------------------------- */ +static ssize_t bluetooth_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_BLUETOOTH_SW_ID, + attr, buf); +} + +static ssize_t bluetooth_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_BLUETOOTH_SW_ID, + attr, buf, count); +} + +static DEVICE_ATTR_RW(bluetooth_enable); + +/* --------------------------------------------------------------------- */ + +static struct attribute *bluetooth_attributes[] = { + &dev_attr_bluetooth_enable.attr, + NULL +}; + +static umode_t bluetooth_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.bluetooth ? attr->mode : 0; +} + +static const struct attribute_group bluetooth_attr_group = { + .is_visible = bluetooth_attr_is_visible, + .attrs = bluetooth_attributes, +}; + +static const struct tpacpi_rfk_ops bluetooth_tprfk_ops = { + .get_status = bluetooth_get_status, + .set_status = bluetooth_set_status, +}; + +static void bluetooth_shutdown(void) +{ + /* Order firmware to save current state to NVRAM */ + if (!acpi_evalf(NULL, NULL, "\\BLTH", "vd", + TP_ACPI_BLTH_SAVE_STATE)) + pr_notice("failed to save bluetooth state to NVRAM\n"); + else + vdbg_printk(TPACPI_DBG_RFKILL, + "bluetooth state saved to NVRAM\n"); +} + +static void bluetooth_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID); + bluetooth_shutdown(); +} + +static const struct dmi_system_id fwbug_list[] __initconst = { + { + .ident = "ThinkPad E485", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20KU"), + }, + }, + { + .ident = "ThinkPad E585", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20KV"), + }, + }, + { + .ident = "ThinkPad A285 - 20MW", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MW"), + }, + }, + { + .ident = "ThinkPad A285 - 20MX", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MX"), + }, + }, + { + .ident = "ThinkPad A485 - 20MU", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MU"), + }, + }, + { + .ident = "ThinkPad A485 - 20MV", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MV"), + }, + }, + {} +}; + +static const struct pci_device_id fwbug_cards_ids[] __initconst = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24F3) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24FD) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x2526) }, + {} +}; + + +static int __init have_bt_fwbug(void) +{ + /* + * Some AMD based ThinkPads have a firmware bug that calling + * "GBDC" will cause bluetooth on Intel wireless cards blocked + */ + if (tp_features.quirks && tp_features.quirks->btusb_bug && + pci_dev_present(fwbug_cards_ids)) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + FW_BUG "disable bluetooth subdriver for Intel cards\n"); + return 1; + } else + return 0; +} + +static int __init bluetooth_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing bluetooth subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + /* bluetooth not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + G4x, R30, R31, R40e, R50e, T20-22, X20-21 */ + tp_features.bluetooth = !have_bt_fwbug() && hkey_handle && + acpi_evalf(hkey_handle, &status, "GBDC", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "bluetooth is %s, status 0x%02x\n", + str_supported(tp_features.bluetooth), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) { + tp_features.bluetooth = 1; + pr_info("bluetooth switch emulation enabled\n"); + } else +#endif + if (tp_features.bluetooth && + !(status & TP_ACPI_BLUETOOTH_HWPRESENT)) { + /* no bluetooth hardware present in system */ + tp_features.bluetooth = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "bluetooth hardware not installed\n"); + } + + if (!tp_features.bluetooth) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID, + &bluetooth_tprfk_ops, + RFKILL_TYPE_BLUETOOTH, + TPACPI_RFK_BLUETOOTH_SW_NAME, + true); + return res; +} + +/* procfs -------------------------------------------------------------- */ +static int bluetooth_read(struct seq_file *m) +{ + return tpacpi_rfk_procfs_read(TPACPI_RFK_BLUETOOTH_SW_ID, m); +} + +static int bluetooth_write(char *buf) +{ + return tpacpi_rfk_procfs_write(TPACPI_RFK_BLUETOOTH_SW_ID, buf); +} + +static struct ibm_struct bluetooth_driver_data = { + .name = "bluetooth", + .read = bluetooth_read, + .write = bluetooth_write, + .exit = bluetooth_exit, + .shutdown = bluetooth_shutdown, +}; + +/************************************************************************* + * Wan subdriver + */ + +enum { + /* ACPI GWAN/SWAN bits */ + TP_ACPI_WANCARD_HWPRESENT = 0x01, /* Wan hw available */ + TP_ACPI_WANCARD_RADIOSSW = 0x02, /* Wan radio enabled */ + TP_ACPI_WANCARD_RESUMECTRL = 0x04, /* Wan state at resume: + 0 = disable, 1 = enable */ +}; + +#define TPACPI_RFK_WWAN_SW_NAME "tpacpi_wwan_sw" + +static int wan_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) + return (tpacpi_wwan_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GWAN", "d")) + return -EIO; + + return ((status & TP_ACPI_WANCARD_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int wan_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s wwan\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) { + tpacpi_wwan_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_WANCARD_RADIOSSW + | TP_ACPI_WANCARD_RESUMECTRL; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SWAN", "vd", status)) + return -EIO; + + return 0; +} + +/* sysfs wan enable ---------------------------------------------------- */ +static ssize_t wan_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_WWAN_SW_ID, + attr, buf); +} + +static ssize_t wan_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_WWAN_SW_ID, + attr, buf, count); +} + +static DEVICE_ATTR(wwan_enable, S_IWUSR | S_IRUGO, + wan_enable_show, wan_enable_store); + +/* --------------------------------------------------------------------- */ + +static struct attribute *wan_attributes[] = { + &dev_attr_wwan_enable.attr, + NULL +}; + +static umode_t wan_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + return tp_features.wan ? attr->mode : 0; +} + +static const struct attribute_group wan_attr_group = { + .is_visible = wan_attr_is_visible, + .attrs = wan_attributes, +}; + +static const struct tpacpi_rfk_ops wan_tprfk_ops = { + .get_status = wan_get_status, + .set_status = wan_set_status, +}; + +static void wan_shutdown(void) +{ + /* Order firmware to save current state to NVRAM */ + if (!acpi_evalf(NULL, NULL, "\\WGSV", "vd", + TP_ACPI_WGSV_SAVE_STATE)) + pr_notice("failed to save WWAN state to NVRAM\n"); + else + vdbg_printk(TPACPI_DBG_RFKILL, + "WWAN state saved to NVRAM\n"); +} + +static void wan_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_WWAN_SW_ID); + wan_shutdown(); +} + +static int __init wan_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing wan subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + tp_features.wan = hkey_handle && + acpi_evalf(hkey_handle, &status, "GWAN", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "wan is %s, status 0x%02x\n", + str_supported(tp_features.wan), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) { + tp_features.wan = 1; + pr_info("wwan switch emulation enabled\n"); + } else +#endif + if (tp_features.wan && + !(status & TP_ACPI_WANCARD_HWPRESENT)) { + /* no wan hardware present in system */ + tp_features.wan = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "wan hardware not installed\n"); + } + + if (!tp_features.wan) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_WWAN_SW_ID, + &wan_tprfk_ops, + RFKILL_TYPE_WWAN, + TPACPI_RFK_WWAN_SW_NAME, + true); + return res; +} + +/* procfs -------------------------------------------------------------- */ +static int wan_read(struct seq_file *m) +{ + return tpacpi_rfk_procfs_read(TPACPI_RFK_WWAN_SW_ID, m); +} + +static int wan_write(char *buf) +{ + return tpacpi_rfk_procfs_write(TPACPI_RFK_WWAN_SW_ID, buf); +} + +static struct ibm_struct wan_driver_data = { + .name = "wan", + .read = wan_read, + .write = wan_write, + .exit = wan_exit, + .shutdown = wan_shutdown, +}; + +/************************************************************************* + * UWB subdriver + */ + +enum { + /* ACPI GUWB/SUWB bits */ + TP_ACPI_UWB_HWPRESENT = 0x01, /* UWB hw available */ + TP_ACPI_UWB_RADIOSSW = 0x02, /* UWB radio enabled */ +}; + +#define TPACPI_RFK_UWB_SW_NAME "tpacpi_uwb_sw" + +static int uwb_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) + return (tpacpi_uwb_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GUWB", "d")) + return -EIO; + + return ((status & TP_ACPI_UWB_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int uwb_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s UWB\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) { + tpacpi_uwb_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_UWB_RADIOSSW; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SUWB", "vd", status)) + return -EIO; + + return 0; +} + +/* --------------------------------------------------------------------- */ + +static const struct tpacpi_rfk_ops uwb_tprfk_ops = { + .get_status = uwb_get_status, + .set_status = uwb_set_status, +}; + +static void uwb_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_UWB_SW_ID); +} + +static int __init uwb_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing uwb subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + tp_features.uwb = hkey_handle && + acpi_evalf(hkey_handle, &status, "GUWB", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "uwb is %s, status 0x%02x\n", + str_supported(tp_features.uwb), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) { + tp_features.uwb = 1; + pr_info("uwb switch emulation enabled\n"); + } else +#endif + if (tp_features.uwb && + !(status & TP_ACPI_UWB_HWPRESENT)) { + /* no uwb hardware present in system */ + tp_features.uwb = 0; + dbg_printk(TPACPI_DBG_INIT, + "uwb hardware not installed\n"); + } + + if (!tp_features.uwb) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_UWB_SW_ID, + &uwb_tprfk_ops, + RFKILL_TYPE_UWB, + TPACPI_RFK_UWB_SW_NAME, + false); + return res; +} + +static struct ibm_struct uwb_driver_data = { + .name = "uwb", + .exit = uwb_exit, + .flags.experimental = 1, +}; + +/************************************************************************* + * Video subdriver + */ + +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + +enum video_access_mode { + TPACPI_VIDEO_NONE = 0, + TPACPI_VIDEO_570, /* 570 */ + TPACPI_VIDEO_770, /* 600e/x, 770e, 770x */ + TPACPI_VIDEO_NEW, /* all others */ +}; + +enum { /* video status flags, based on VIDEO_570 */ + TP_ACPI_VIDEO_S_LCD = 0x01, /* LCD output enabled */ + TP_ACPI_VIDEO_S_CRT = 0x02, /* CRT output enabled */ + TP_ACPI_VIDEO_S_DVI = 0x08, /* DVI output enabled */ +}; + +enum { /* TPACPI_VIDEO_570 constants */ + TP_ACPI_VIDEO_570_PHSCMD = 0x87, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHSMASK = 0x03, /* PHS bits that map to + * video_status_flags */ + TP_ACPI_VIDEO_570_PHS2CMD = 0x8b, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHS2SET = 0x80, /* unknown magic constant :( */ +}; + +static enum video_access_mode video_supported; +static int video_orig_autosw; + +static int video_autosw_get(void); +static int video_autosw_set(int enable); + +TPACPI_HANDLE(vid, root, + "\\_SB.PCI.AGP.VGA", /* 570 */ + "\\_SB.PCI0.AGP0.VID0", /* 600e/x, 770x */ + "\\_SB.PCI0.VID0", /* 770e */ + "\\_SB.PCI0.VID", /* A21e, G4x, R50e, X30, X40 */ + "\\_SB.PCI0.AGP.VGA", /* X100e and a few others */ + "\\_SB.PCI0.AGP.VID", /* all others */ + ); /* R30, R31 */ + +TPACPI_HANDLE(vid2, root, "\\_SB.PCI0.AGPB.VID"); /* G41 */ + +static int __init video_init(struct ibm_init_struct *iibm) +{ + int ivga; + + vdbg_printk(TPACPI_DBG_INIT, "initializing video subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(vid); + if (tpacpi_is_ibm()) + TPACPI_ACPIHANDLE_INIT(vid2); + + if (vid2_handle && acpi_evalf(NULL, &ivga, "\\IVGA", "d") && ivga) + /* G41, assume IVGA doesn't change */ + vid_handle = vid2_handle; + + if (!vid_handle) + /* video switching not supported on R30, R31 */ + video_supported = TPACPI_VIDEO_NONE; + else if (tpacpi_is_ibm() && + acpi_evalf(vid_handle, &video_orig_autosw, "SWIT", "qd")) + /* 570 */ + video_supported = TPACPI_VIDEO_570; + else if (tpacpi_is_ibm() && + acpi_evalf(vid_handle, &video_orig_autosw, "^VADL", "qd")) + /* 600e/x, 770e, 770x */ + video_supported = TPACPI_VIDEO_770; + else + /* all others */ + video_supported = TPACPI_VIDEO_NEW; + + vdbg_printk(TPACPI_DBG_INIT, "video is %s, mode %d\n", + str_supported(video_supported != TPACPI_VIDEO_NONE), + video_supported); + + return (video_supported != TPACPI_VIDEO_NONE) ? 0 : -ENODEV; +} + +static void video_exit(void) +{ + dbg_printk(TPACPI_DBG_EXIT, + "restoring original video autoswitch mode\n"); + if (video_autosw_set(video_orig_autosw)) + pr_err("error while trying to restore original video autoswitch mode\n"); +} + +static int video_outputsw_get(void) +{ + int status = 0; + int i; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(NULL, &i, "\\_SB.PHS", "dd", + TP_ACPI_VIDEO_570_PHSCMD)) + return -EIO; + status = i & TP_ACPI_VIDEO_570_PHSMASK; + break; + case TPACPI_VIDEO_770: + if (!acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + break; + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 1) || + !acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0) || + !acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDD", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_DVI; + break; + default: + return -ENOSYS; + } + + return status; +} + +static int video_outputsw_set(int status) +{ + int autosw; + int res = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = acpi_evalf(NULL, NULL, + "\\_SB.PHS2", "vdd", + TP_ACPI_VIDEO_570_PHS2CMD, + status | TP_ACPI_VIDEO_570_PHS2SET); + break; + case TPACPI_VIDEO_770: + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, + "ASWT", "vdd", status * 0x100, 0); + if (!autosw && video_autosw_set(autosw)) { + pr_err("video auto-switch left enabled due to error\n"); + return -EIO; + } + break; + case TPACPI_VIDEO_NEW: + res = acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0x80) && + acpi_evalf(NULL, NULL, "\\VSDS", "vdd", status, 1); + break; + default: + return -ENOSYS; + } + + return (res) ? 0 : -EIO; +} + +static int video_autosw_get(void) +{ + int autosw = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(vid_handle, &autosw, "SWIT", "d")) + return -EIO; + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(vid_handle, &autosw, "^VDEE", "d")) + return -EIO; + break; + default: + return -ENOSYS; + } + + return autosw & 1; +} + +static int video_autosw_set(int enable) +{ + if (!acpi_evalf(vid_handle, NULL, "_DOS", "vd", (enable) ? 1 : 0)) + return -EIO; + return 0; +} + +static int video_outputsw_cycle(void) +{ + int autosw = video_autosw_get(); + int res; + + if (autosw < 0) + return autosw; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(ec_handle, NULL, "_Q16", "v"); + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, "VSWT", "v"); + break; + default: + return -ENOSYS; + } + if (!autosw && video_autosw_set(autosw)) { + pr_err("video auto-switch left enabled due to error\n"); + return -EIO; + } + + return (res) ? 0 : -EIO; +} + +static int video_expand_toggle(void) +{ + switch (video_supported) { + case TPACPI_VIDEO_570: + return acpi_evalf(ec_handle, NULL, "_Q17", "v") ? + 0 : -EIO; + case TPACPI_VIDEO_770: + return acpi_evalf(vid_handle, NULL, "VEXP", "v") ? + 0 : -EIO; + case TPACPI_VIDEO_NEW: + return acpi_evalf(NULL, NULL, "\\VEXP", "v") ? + 0 : -EIO; + default: + return -ENOSYS; + } + /* not reached */ +} + +static int video_read(struct seq_file *m) +{ + int status, autosw; + + if (video_supported == TPACPI_VIDEO_NONE) { + seq_puts(m, "status:\t\tnot supported\n"); + return 0; + } + + /* Even reads can crash X.org, so... */ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + status = video_outputsw_get(); + if (status < 0) + return status; + + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + seq_puts(m, "status:\t\tsupported\n"); + seq_printf(m, "lcd:\t\t%s\n", str_enabled_disabled(status & BIT(0))); + seq_printf(m, "crt:\t\t%s\n", str_enabled_disabled(status & BIT(1))); + if (video_supported == TPACPI_VIDEO_NEW) + seq_printf(m, "dvi:\t\t%s\n", str_enabled_disabled(status & BIT(3))); + seq_printf(m, "auto:\t\t%s\n", str_enabled_disabled(autosw & BIT(0))); + seq_puts(m, "commands:\tlcd_enable, lcd_disable\n"); + seq_puts(m, "commands:\tcrt_enable, crt_disable\n"); + if (video_supported == TPACPI_VIDEO_NEW) + seq_puts(m, "commands:\tdvi_enable, dvi_disable\n"); + seq_puts(m, "commands:\tauto_enable, auto_disable\n"); + seq_puts(m, "commands:\tvideo_switch, expand_toggle\n"); + + return 0; +} + +static int video_write(char *buf) +{ + char *cmd; + int enable, disable, status; + int res; + + if (video_supported == TPACPI_VIDEO_NONE) + return -ENODEV; + + /* Even reads can crash X.org, let alone writes... */ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + enable = 0; + disable = 0; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "lcd_enable")) { + enable |= TP_ACPI_VIDEO_S_LCD; + } else if (strstarts(cmd, "lcd_disable")) { + disable |= TP_ACPI_VIDEO_S_LCD; + } else if (strstarts(cmd, "crt_enable")) { + enable |= TP_ACPI_VIDEO_S_CRT; + } else if (strstarts(cmd, "crt_disable")) { + disable |= TP_ACPI_VIDEO_S_CRT; + } else if (video_supported == TPACPI_VIDEO_NEW && + strstarts(cmd, "dvi_enable")) { + enable |= TP_ACPI_VIDEO_S_DVI; + } else if (video_supported == TPACPI_VIDEO_NEW && + strstarts(cmd, "dvi_disable")) { + disable |= TP_ACPI_VIDEO_S_DVI; + } else if (strstarts(cmd, "auto_enable")) { + res = video_autosw_set(1); + if (res) + return res; + } else if (strstarts(cmd, "auto_disable")) { + res = video_autosw_set(0); + if (res) + return res; + } else if (strstarts(cmd, "video_switch")) { + res = video_outputsw_cycle(); + if (res) + return res; + } else if (strstarts(cmd, "expand_toggle")) { + res = video_expand_toggle(); + if (res) + return res; + } else + return -EINVAL; + } + + if (enable || disable) { + status = video_outputsw_get(); + if (status < 0) + return status; + res = video_outputsw_set((status & ~disable) | enable); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct video_driver_data = { + .name = "video", + .read = video_read, + .write = video_write, + .exit = video_exit, +}; + +#endif /* CONFIG_THINKPAD_ACPI_VIDEO */ + +/************************************************************************* + * Keyboard backlight subdriver + */ + +static enum led_brightness kbdlight_brightness; +static DEFINE_MUTEX(kbdlight_mutex); + +static int kbdlight_set_level(int level) +{ + int ret = 0; + + if (!hkey_handle) + return -ENXIO; + + mutex_lock(&kbdlight_mutex); + + if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level)) + ret = -EIO; + else + kbdlight_brightness = level; + + mutex_unlock(&kbdlight_mutex); + + return ret; +} + +static int kbdlight_get_level(void) +{ + int status = 0; + + if (!hkey_handle) + return -ENXIO; + + if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0)) + return -EIO; + + if (status < 0) + return status; + + return status & 0x3; +} + +static bool kbdlight_is_supported(void) +{ + int status = 0; + + if (!hkey_handle) + return false; + + if (!acpi_has_method(hkey_handle, "MLCG")) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n"); + return false; + } + + if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n"); + return false; + } + + if (status < 0) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status); + return false; + } + + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status); + /* + * Guessed test for keyboard backlight: + * + * Machines with backlight keyboard return: + * b010100000010000000XX - ThinkPad X1 Carbon 3rd + * b110100010010000000XX - ThinkPad x230 + * b010100000010000000XX - ThinkPad x240 + * b010100000010000000XX - ThinkPad W541 + * (XX is current backlight level) + * + * Machines without backlight keyboard return: + * b10100001000000000000 - ThinkPad x230 + * b10110001000000000000 - ThinkPad E430 + * b00000000000000000000 - ThinkPad E450 + * + * Candidate BITs for detection test (XOR): + * b01000000001000000000 + * ^ + */ + return status & BIT(9); +} + +static int kbdlight_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return kbdlight_set_level(brightness); +} + +static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev) +{ + int level; + + level = kbdlight_get_level(); + if (level < 0) + return 0; + + return level; +} + +static struct tpacpi_led_classdev tpacpi_led_kbdlight = { + .led_classdev = { + .name = "tpacpi::kbd_backlight", + .max_brightness = 2, + .flags = LED_BRIGHT_HW_CHANGED, + .brightness_set_blocking = &kbdlight_sysfs_set, + .brightness_get = &kbdlight_sysfs_get, + } +}; + +static int __init kbdlight_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + if (!kbdlight_is_supported()) { + tp_features.kbdlight = 0; + vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n"); + return -ENODEV; + } + + kbdlight_brightness = kbdlight_sysfs_get(NULL); + tp_features.kbdlight = 1; + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_led_kbdlight.led_classdev); + if (rc < 0) { + tp_features.kbdlight = 0; + return rc; + } + + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask | + TP_ACPI_HKEY_KBD_LIGHT_MASK); + return 0; +} + +static void kbdlight_exit(void) +{ + led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev); +} + +static int kbdlight_set_level_and_update(int level) +{ + int ret; + struct led_classdev *led_cdev; + + ret = kbdlight_set_level(level); + led_cdev = &tpacpi_led_kbdlight.led_classdev; + + if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED)) + led_cdev->brightness = level; + + return ret; +} + +static int kbdlight_read(struct seq_file *m) +{ + int level; + + if (!tp_features.kbdlight) { + seq_puts(m, "status:\t\tnot supported\n"); + } else { + level = kbdlight_get_level(); + if (level < 0) + seq_printf(m, "status:\t\terror %d\n", level); + else + seq_printf(m, "status:\t\t%d\n", level); + seq_puts(m, "commands:\t0, 1, 2\n"); + } + + return 0; +} + +static int kbdlight_write(char *buf) +{ + char *cmd; + int res, level = -EINVAL; + + if (!tp_features.kbdlight) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + res = kstrtoint(cmd, 10, &level); + if (res < 0) + return res; + } + + if (level >= 3 || level < 0) + return -EINVAL; + + return kbdlight_set_level_and_update(level); +} + +static void kbdlight_suspend(void) +{ + struct led_classdev *led_cdev; + + if (!tp_features.kbdlight) + return; + + led_cdev = &tpacpi_led_kbdlight.led_classdev; + led_update_brightness(led_cdev); + led_classdev_suspend(led_cdev); +} + +static void kbdlight_resume(void) +{ + if (!tp_features.kbdlight) + return; + + led_classdev_resume(&tpacpi_led_kbdlight.led_classdev); +} + +static struct ibm_struct kbdlight_driver_data = { + .name = "kbdlight", + .read = kbdlight_read, + .write = kbdlight_write, + .suspend = kbdlight_suspend, + .resume = kbdlight_resume, + .exit = kbdlight_exit, +}; + +/************************************************************************* + * Light (thinklight) subdriver + */ + +TPACPI_HANDLE(lght, root, "\\LGHT"); /* A21e, A2xm/p, T20-22, X20-21 */ +TPACPI_HANDLE(ledb, ec, "LEDB"); /* G4x */ + +static int light_get_status(void) +{ + int status = 0; + + if (tp_features.light_status) { + if (!acpi_evalf(ec_handle, &status, "KBLT", "d")) + return -EIO; + return (!!status); + } + + return -ENXIO; +} + +static int light_set_status(int status) +{ + int rc; + + if (tp_features.light) { + if (cmos_handle) { + rc = acpi_evalf(cmos_handle, NULL, NULL, "vd", + (status) ? + TP_CMOS_THINKLIGHT_ON : + TP_CMOS_THINKLIGHT_OFF); + } else { + rc = acpi_evalf(lght_handle, NULL, NULL, "vd", + (status) ? 1 : 0); + } + return (rc) ? 0 : -EIO; + } + + return -ENXIO; +} + +static int light_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return light_set_status((brightness != LED_OFF) ? + TPACPI_LED_ON : TPACPI_LED_OFF); +} + +static enum led_brightness light_sysfs_get(struct led_classdev *led_cdev) +{ + return (light_get_status() == 1) ? LED_ON : LED_OFF; +} + +static struct tpacpi_led_classdev tpacpi_led_thinklight = { + .led_classdev = { + .name = "tpacpi::thinklight", + .max_brightness = 1, + .brightness_set_blocking = &light_sysfs_set, + .brightness_get = &light_sysfs_get, + } +}; + +static int __init light_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing light subdriver\n"); + + if (tpacpi_is_ibm()) { + TPACPI_ACPIHANDLE_INIT(ledb); + TPACPI_ACPIHANDLE_INIT(lght); + } + TPACPI_ACPIHANDLE_INIT(cmos); + + /* light not supported on 570, 600e/x, 770e, 770x, G4x, R30, R31 */ + tp_features.light = (cmos_handle || lght_handle) && !ledb_handle; + + if (tp_features.light) + /* light status not supported on + 570, 600e/x, 770e, 770x, G4x, R30, R31, R32, X20 */ + tp_features.light_status = + acpi_evalf(ec_handle, NULL, "KBLT", "qv"); + + vdbg_printk(TPACPI_DBG_INIT, "light is %s, light status is %s\n", + str_supported(tp_features.light), + str_supported(tp_features.light_status)); + + if (!tp_features.light) + return -ENODEV; + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_led_thinklight.led_classdev); + + if (rc < 0) { + tp_features.light = 0; + tp_features.light_status = 0; + } else { + rc = 0; + } + + return rc; +} + +static void light_exit(void) +{ + led_classdev_unregister(&tpacpi_led_thinklight.led_classdev); +} + +static int light_read(struct seq_file *m) +{ + int status; + + if (!tp_features.light) { + seq_puts(m, "status:\t\tnot supported\n"); + } else if (!tp_features.light_status) { + seq_puts(m, "status:\t\tunknown\n"); + seq_puts(m, "commands:\ton, off\n"); + } else { + status = light_get_status(); + if (status < 0) + return status; + seq_printf(m, "status:\t\t%s\n", str_on_off(status & BIT(0))); + seq_puts(m, "commands:\ton, off\n"); + } + + return 0; +} + +static int light_write(char *buf) +{ + char *cmd; + int newstatus = 0; + + if (!tp_features.light) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "on")) { + newstatus = 1; + } else if (strstarts(cmd, "off")) { + newstatus = 0; + } else + return -EINVAL; + } + + return light_set_status(newstatus); +} + +static struct ibm_struct light_driver_data = { + .name = "light", + .read = light_read, + .write = light_write, + .exit = light_exit, +}; + +/************************************************************************* + * CMOS subdriver + */ + +/* sysfs cmos_command -------------------------------------------------- */ +static ssize_t cmos_command_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long cmos_cmd; + int res; + + if (parse_strtoul(buf, 21, &cmos_cmd)) + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + return (res) ? res : count; +} + +static DEVICE_ATTR_WO(cmos_command); + +static struct attribute *cmos_attributes[] = { + &dev_attr_cmos_command.attr, + NULL +}; + +static umode_t cmos_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return cmos_handle ? attr->mode : 0; +} + +static const struct attribute_group cmos_attr_group = { + .is_visible = cmos_attr_is_visible, + .attrs = cmos_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static int __init cmos_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, + "initializing cmos commands subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(cmos); + + vdbg_printk(TPACPI_DBG_INIT, "cmos commands are %s\n", + str_supported(cmos_handle != NULL)); + + return cmos_handle ? 0 : -ENODEV; +} + +static int cmos_read(struct seq_file *m) +{ + /* cmos not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + R30, R31, T20-22, X20-21 */ + if (!cmos_handle) + seq_puts(m, "status:\t\tnot supported\n"); + else { + seq_puts(m, "status:\t\tsupported\n"); + seq_puts(m, "commands:\t ( is 0-21)\n"); + } + + return 0; +} + +static int cmos_write(char *buf) +{ + char *cmd; + int cmos_cmd, res; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%u", &cmos_cmd) == 1 && + cmos_cmd >= 0 && cmos_cmd <= 21) { + /* cmos_cmd set */ + } else + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct cmos_driver_data = { + .name = "cmos", + .read = cmos_read, + .write = cmos_write, +}; + +/************************************************************************* + * LED subdriver + */ + +enum led_access_mode { + TPACPI_LED_NONE = 0, + TPACPI_LED_570, /* 570 */ + TPACPI_LED_OLD, /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + TPACPI_LED_NEW, /* all others */ +}; + +enum { /* For TPACPI_LED_OLD */ + TPACPI_LED_EC_HLCL = 0x0c, /* EC reg to get led to power on */ + TPACPI_LED_EC_HLBL = 0x0d, /* EC reg to blink a lit led */ + TPACPI_LED_EC_HLMS = 0x0e, /* EC reg to select led to command */ +}; + +static enum led_access_mode led_supported; + +static acpi_handle led_handle; + +#define TPACPI_LED_NUMLEDS 16 +static struct tpacpi_led_classdev *tpacpi_leds; +static enum led_status_t tpacpi_led_state_cache[TPACPI_LED_NUMLEDS]; +static const char * const tpacpi_led_names[TPACPI_LED_NUMLEDS] = { + /* there's a limit of 19 chars + NULL before 2.6.26 */ + "tpacpi::power", + "tpacpi:orange:batt", + "tpacpi:green:batt", + "tpacpi::dock_active", + "tpacpi::bay_active", + "tpacpi::dock_batt", + "tpacpi::unknown_led", + "tpacpi::standby", + "tpacpi::dock_status1", + "tpacpi::dock_status2", + "tpacpi::lid_logo_dot", + "tpacpi::unknown_led3", + "tpacpi::thinkvantage", +}; +#define TPACPI_SAFE_LEDS 0x1481U + +static inline bool tpacpi_is_led_restricted(const unsigned int led) +{ +#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS + return false; +#else + return (1U & (TPACPI_SAFE_LEDS >> led)) == 0; +#endif +} + +static int led_get_status(const unsigned int led) +{ + int status; + enum led_status_t led_s; + + switch (led_supported) { + case TPACPI_LED_570: + if (!acpi_evalf(ec_handle, + &status, "GLED", "dd", 1 << led)) + return -EIO; + led_s = (status == 0) ? + TPACPI_LED_OFF : + ((status == 1) ? + TPACPI_LED_ON : + TPACPI_LED_BLINK); + tpacpi_led_state_cache[led] = led_s; + return led_s; + default: + return -ENXIO; + } + + /* not reached */ +} + +static int led_set_status(const unsigned int led, + const enum led_status_t ledstatus) +{ + /* off, on, blink. Index is led_status_t */ + static const unsigned int led_sled_arg1[] = { 0, 1, 3 }; + static const unsigned int led_led_arg1[] = { 0, 0x80, 0xc0 }; + + int rc = 0; + + switch (led_supported) { + case TPACPI_LED_570: + /* 570 */ + if (unlikely(led > 7)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + (1 << led), led_sled_arg1[ledstatus])) + return -EIO; + break; + case TPACPI_LED_OLD: + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20 */ + if (unlikely(led > 7)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + rc = ec_write(TPACPI_LED_EC_HLMS, (1 << led)); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLBL, + (ledstatus == TPACPI_LED_BLINK) << led); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLCL, + (ledstatus != TPACPI_LED_OFF) << led); + break; + case TPACPI_LED_NEW: + /* all others */ + if (unlikely(led >= TPACPI_LED_NUMLEDS)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + led, led_led_arg1[ledstatus])) + return -EIO; + break; + default: + return -ENXIO; + } + + if (!rc) + tpacpi_led_state_cache[led] = ledstatus; + + return rc; +} + +static int led_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + enum led_status_t new_state; + + if (brightness == LED_OFF) + new_state = TPACPI_LED_OFF; + else if (tpacpi_led_state_cache[data->led] != TPACPI_LED_BLINK) + new_state = TPACPI_LED_ON; + else + new_state = TPACPI_LED_BLINK; + + return led_set_status(data->led, new_state); +} + +static int led_sysfs_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + /* Can we choose the flash rate? */ + if (*delay_on == 0 && *delay_off == 0) { + /* yes. set them to the hardware blink rate (1 Hz) */ + *delay_on = 500; /* ms */ + *delay_off = 500; /* ms */ + } else if ((*delay_on != 500) || (*delay_off != 500)) + return -EINVAL; + + return led_set_status(data->led, TPACPI_LED_BLINK); +} + +static enum led_brightness led_sysfs_get(struct led_classdev *led_cdev) +{ + int rc; + + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + rc = led_get_status(data->led); + + if (rc == TPACPI_LED_OFF || rc < 0) + rc = LED_OFF; /* no error handling in led class :( */ + else + rc = LED_FULL; + + return rc; +} + +static void led_exit(void) +{ + unsigned int i; + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) + led_classdev_unregister(&tpacpi_leds[i].led_classdev); + + kfree(tpacpi_leds); +} + +static int __init tpacpi_init_led(unsigned int led) +{ + /* LEDs with no name don't get registered */ + if (!tpacpi_led_names[led]) + return 0; + + tpacpi_leds[led].led_classdev.brightness_set_blocking = &led_sysfs_set; + tpacpi_leds[led].led_classdev.blink_set = &led_sysfs_blink_set; + if (led_supported == TPACPI_LED_570) + tpacpi_leds[led].led_classdev.brightness_get = &led_sysfs_get; + + tpacpi_leds[led].led_classdev.name = tpacpi_led_names[led]; + tpacpi_leds[led].led_classdev.flags = LED_RETAIN_AT_SHUTDOWN; + tpacpi_leds[led].led = led; + + return led_classdev_register(&tpacpi_pdev->dev, &tpacpi_leds[led].led_classdev); +} + +static const struct tpacpi_quirk led_useful_qtable[] __initconst = { + TPACPI_Q_IBM('1', 'E', 0x009f), /* A30 */ + TPACPI_Q_IBM('1', 'N', 0x009f), /* A31 */ + TPACPI_Q_IBM('1', 'G', 0x009f), /* A31 */ + + TPACPI_Q_IBM('1', 'I', 0x0097), /* T30 */ + TPACPI_Q_IBM('1', 'R', 0x0097), /* T40, T41, T42, R50, R51 */ + TPACPI_Q_IBM('7', '0', 0x0097), /* T43, R52 */ + TPACPI_Q_IBM('1', 'Y', 0x0097), /* T43 */ + TPACPI_Q_IBM('1', 'W', 0x0097), /* R50e */ + TPACPI_Q_IBM('1', 'V', 0x0097), /* R51 */ + TPACPI_Q_IBM('7', '8', 0x0097), /* R51e */ + TPACPI_Q_IBM('7', '6', 0x0097), /* R52 */ + + TPACPI_Q_IBM('1', 'K', 0x00bf), /* X30 */ + TPACPI_Q_IBM('1', 'Q', 0x00bf), /* X31, X32 */ + TPACPI_Q_IBM('1', 'U', 0x00bf), /* X40 */ + TPACPI_Q_IBM('7', '4', 0x00bf), /* X41 */ + TPACPI_Q_IBM('7', '5', 0x00bf), /* X41t */ + + TPACPI_Q_IBM('7', '9', 0x1f97), /* T60 (1) */ + TPACPI_Q_IBM('7', '7', 0x1f97), /* Z60* (1) */ + TPACPI_Q_IBM('7', 'F', 0x1f97), /* Z61* (1) */ + TPACPI_Q_IBM('7', 'B', 0x1fb7), /* X60 (1) */ + + /* (1) - may have excess leds enabled on MSB */ + + /* Defaults (order matters, keep last, don't reorder!) */ + { /* Lenovo */ + .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = 0x1fffU, + }, + { /* IBM ThinkPads with no EC version string */ + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_UNKNOWN, + .quirks = 0x00ffU, + }, + { /* IBM ThinkPads with EC version string */ + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = 0x00bfU, + }, +}; + +static enum led_access_mode __init led_init_detect_mode(void) +{ + acpi_status status; + + if (tpacpi_is_ibm()) { + /* 570 */ + status = acpi_get_handle(ec_handle, "SLED", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_570; + + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + status = acpi_get_handle(ec_handle, "SYSL", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_OLD; + } + + /* most others */ + status = acpi_get_handle(ec_handle, "LED", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_NEW; + + /* R30, R31, and unknown firmwares */ + led_handle = NULL; + return TPACPI_LED_NONE; +} + +static int __init led_init(struct ibm_init_struct *iibm) +{ + unsigned int i; + int rc; + unsigned long useful_leds; + + vdbg_printk(TPACPI_DBG_INIT, "initializing LED subdriver\n"); + + led_supported = led_init_detect_mode(); + + if (led_supported != TPACPI_LED_NONE) { + useful_leds = tpacpi_check_quirks(led_useful_qtable, + ARRAY_SIZE(led_useful_qtable)); + + if (!useful_leds) { + led_handle = NULL; + led_supported = TPACPI_LED_NONE; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "LED commands are %s, mode %d\n", + str_supported(led_supported), led_supported); + + if (led_supported == TPACPI_LED_NONE) + return -ENODEV; + + tpacpi_leds = kzalloc_objs(*tpacpi_leds, TPACPI_LED_NUMLEDS); + if (!tpacpi_leds) { + pr_err("Out of memory for LED data\n"); + return -ENOMEM; + } + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) { + tpacpi_leds[i].led = -1; + + if (!tpacpi_is_led_restricted(i) && test_bit(i, &useful_leds)) { + rc = tpacpi_init_led(i); + if (rc < 0) { + led_exit(); + return rc; + } + } + } + +#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS + pr_notice("warning: userspace override of important firmware LEDs is enabled\n"); +#endif + return 0; +} + +#define str_led_status(s) ((s) >= TPACPI_LED_BLINK ? "blinking" : str_on_off(s)) + +static int led_read(struct seq_file *m) +{ + if (!led_supported) { + seq_puts(m, "status:\t\tnot supported\n"); + return 0; + } + seq_puts(m, "status:\t\tsupported\n"); + + if (led_supported == TPACPI_LED_570) { + /* 570 */ + int i, status; + for (i = 0; i < 8; i++) { + status = led_get_status(i); + if (status < 0) + return -EIO; + seq_printf(m, "%d:\t\t%s\n", i, str_led_status(status)); + } + } + + seq_puts(m, "commands:\t on, off, blink ( is 0-15)\n"); + + return 0; +} + +static int led_write(char *buf) +{ + char *cmd; + int led, rc; + enum led_status_t s; + + if (!led_supported) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%d", &led) != 1) + return -EINVAL; + + if (led < 0 || led > (TPACPI_LED_NUMLEDS - 1)) + return -ENODEV; + + if (tpacpi_leds[led].led < 0) + return -ENODEV; + + if (strstr(cmd, "off")) { + s = TPACPI_LED_OFF; + } else if (strstr(cmd, "on")) { + s = TPACPI_LED_ON; + } else if (strstr(cmd, "blink")) { + s = TPACPI_LED_BLINK; + } else { + return -EINVAL; + } + + rc = led_set_status(led, s); + if (rc < 0) + return rc; + } + + return 0; +} + +static struct ibm_struct led_driver_data = { + .name = "led", + .read = led_read, + .write = led_write, + .exit = led_exit, +}; + +/************************************************************************* + * Beep subdriver + */ + +TPACPI_HANDLE(beep, ec, "BEEP"); /* all except R30, R31 */ + +#define TPACPI_BEEP_Q1 0x0001 + +static const struct tpacpi_quirk beep_quirk_table[] __initconst = { + TPACPI_Q_IBM('I', 'M', TPACPI_BEEP_Q1), /* 570 */ + TPACPI_Q_IBM('I', 'U', TPACPI_BEEP_Q1), /* 570E - unverified */ +}; + +static int __init beep_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT, "initializing beep subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(beep); + + vdbg_printk(TPACPI_DBG_INIT, "beep is %s\n", + str_supported(beep_handle != NULL)); + + quirks = tpacpi_check_quirks(beep_quirk_table, + ARRAY_SIZE(beep_quirk_table)); + + tp_features.beep_needs_two_args = !!(quirks & TPACPI_BEEP_Q1); + + return (beep_handle) ? 0 : -ENODEV; +} + +static int beep_read(struct seq_file *m) +{ + if (!beep_handle) + seq_puts(m, "status:\t\tnot supported\n"); + else { + seq_puts(m, "status:\t\tsupported\n"); + seq_puts(m, "commands:\t ( is 0-17)\n"); + } + + return 0; +} + +static int beep_write(char *buf) +{ + char *cmd; + int beep_cmd; + + if (!beep_handle) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%u", &beep_cmd) == 1 && + beep_cmd >= 0 && beep_cmd <= 17) { + /* beep_cmd set */ + } else + return -EINVAL; + if (tp_features.beep_needs_two_args) { + if (!acpi_evalf(beep_handle, NULL, NULL, "vdd", + beep_cmd, 0)) + return -EIO; + } else { + if (!acpi_evalf(beep_handle, NULL, NULL, "vd", + beep_cmd)) + return -EIO; + } + } + + return 0; +} + +static struct ibm_struct beep_driver_data = { + .name = "beep", + .read = beep_read, + .write = beep_write, +}; + +/************************************************************************* + * Thermal subdriver + */ + +enum thermal_access_mode { + TPACPI_THERMAL_NONE = 0, /* No thermal support */ + TPACPI_THERMAL_ACPI_TMP07, /* Use ACPI TMP0-7 */ + TPACPI_THERMAL_ACPI_UPDT, /* Use ACPI TMP0-7 with UPDT */ + TPACPI_THERMAL_TPEC_8, /* Use ACPI EC regs, 8 sensors */ + TPACPI_THERMAL_TPEC_12, /* Use ACPI EC regs, 12 sensors */ + TPACPI_THERMAL_TPEC_16, /* Use ACPI EC regs, 16 sensors */ +}; + +enum { /* TPACPI_THERMAL_TPEC_* */ + TP_EC_THERMAL_TMP0 = 0x78, /* ACPI EC regs TMP 0..7 */ + TP_EC_THERMAL_TMP8 = 0xC0, /* ACPI EC regs TMP 8..15 */ + TP_EC_THERMAL_TMP0_NS = 0xA8, /* ACPI EC Non-Standard regs TMP 0..7 */ + TP_EC_THERMAL_TMP8_NS = 0xB8, /* ACPI EC Non-standard regs TMP 8..11 */ + TP_EC_FUNCREV = 0xEF, /* ACPI EC Functional revision */ + TP_EC_THERMAL_TMP_NA = -128, /* ACPI EC sensor not available */ + + TPACPI_THERMAL_SENSOR_NA = -128000, /* Sensor not available */ +}; + + +#define TPACPI_MAX_THERMAL_SENSORS 16 /* Max thermal sensors supported */ +struct ibm_thermal_sensors_struct { + s32 temp[TPACPI_MAX_THERMAL_SENSORS]; +}; + +static const struct tpacpi_quirk thermal_quirk_table[] __initconst = { + /* Non-standard address for thermal registers on some ThinkPads */ + TPACPI_Q_LNV3('R', '1', 'F', true), /* L13 Yoga Gen 2 */ + TPACPI_Q_LNV3('N', '2', 'U', true), /* X13 Yoga Gen 2*/ + TPACPI_Q_LNV3('R', '0', 'R', true), /* L380 */ + TPACPI_Q_LNV3('R', '1', '5', true), /* L13 Yoga Gen 1*/ + TPACPI_Q_LNV3('R', '1', '0', true), /* L390 */ + TPACPI_Q_LNV3('N', '2', 'L', true), /* X13 Yoga Gen 1*/ + TPACPI_Q_LNV3('R', '0', 'T', true), /* 11e Gen5 GL*/ + TPACPI_Q_LNV3('R', '1', 'D', true), /* 11e Gen5 GL-R*/ + TPACPI_Q_LNV3('R', '0', 'V', true), /* 11e Gen5 KL-Y*/ +}; + +static enum thermal_access_mode thermal_read_mode; +static bool thermal_use_labels; +static bool thermal_with_ns_address; /* Non-standard thermal reg address */ + +/* Function to check thermal read mode */ +static enum thermal_access_mode __init thermal_read_mode_check(void) +{ + u8 t, ta1, ta2, ver = 0; + int i; + int acpi_tmp7; + + acpi_tmp7 = acpi_evalf(ec_handle, NULL, "TMP7", "qv"); + + if (thinkpad_id.ec_model) { + /* + * Direct EC access mode: sensors at registers 0x78-0x7F, + * 0xC0-0xC7. Registers return 0x00 for non-implemented, + * thermal sensors return 0x80 when not available. + * + * In some special cases (when Power Supply ID is 0xC2) + * above rule causes thermal control issues. Offset 0xEF + * determines EC version. 0xC0-0xC7 are not thermal registers + * in Ver 3. + */ + if (!acpi_ec_read(TP_EC_FUNCREV, &ver)) + pr_warn("Thinkpad ACPI EC unable to access EC version\n"); + + /* Quirks to check non-standard EC */ + thermal_with_ns_address = tpacpi_check_quirks(thermal_quirk_table, + ARRAY_SIZE(thermal_quirk_table)); + + /* Support for Thinkpads with non-standard address */ + if (thermal_with_ns_address) { + pr_info("ECFW with non-standard thermal registers found\n"); + return TPACPI_THERMAL_TPEC_12; + } + + ta1 = ta2 = 0; + for (i = 0; i < 8; i++) { + if (acpi_ec_read(TP_EC_THERMAL_TMP0 + i, &t)) { + ta1 |= t; + } else { + ta1 = 0; + break; + } + if (ver < 3) { + if (acpi_ec_read(TP_EC_THERMAL_TMP8 + i, &t)) { + ta2 |= t; + } else { + ta1 = 0; + break; + } + } + } + + if (ta1 == 0) { + /* This is sheer paranoia, but we handle it anyway */ + if (acpi_tmp7) { + pr_err("ThinkPad ACPI EC access misbehaving, falling back to ACPI TMPx access mode\n"); + return TPACPI_THERMAL_ACPI_TMP07; + } + pr_err("ThinkPad ACPI EC access misbehaving, disabling thermal sensors access\n"); + return TPACPI_THERMAL_NONE; + } + + if (ver >= 3) { + thermal_use_labels = true; + return TPACPI_THERMAL_TPEC_8; + } + + return (ta2 != 0) ? TPACPI_THERMAL_TPEC_16 : TPACPI_THERMAL_TPEC_8; + } + + if (acpi_tmp7) { + if (tpacpi_is_ibm() && acpi_evalf(ec_handle, NULL, "UPDT", "qv")) { + /* 600e/x, 770e, 770x */ + return TPACPI_THERMAL_ACPI_UPDT; + } + /* IBM/LENOVO DSDT EC.TMPx access, max 8 sensors */ + return TPACPI_THERMAL_ACPI_TMP07; + } + + /* temperatures not supported on 570, G4x, R30, R31, R32 */ + return TPACPI_THERMAL_NONE; +} + +/* idx is zero-based */ +static int thermal_get_sensor(int idx, s32 *value) +{ + int t; + s8 tmp; + char tmpi[5]; + + t = TP_EC_THERMAL_TMP0; + + switch (thermal_read_mode) { +#if TPACPI_MAX_THERMAL_SENSORS >= 16 + case TPACPI_THERMAL_TPEC_16: + if (idx >= 8 && idx <= 15) { + t = TP_EC_THERMAL_TMP8; + idx -= 8; + } +#endif + fallthrough; + case TPACPI_THERMAL_TPEC_8: + if (idx <= 7) { + if (!acpi_ec_read(t + idx, &tmp)) + return -EIO; + *value = tmp * 1000; + return 0; + } + break; + + /* The Non-standard EC uses 12 Thermal areas */ + case TPACPI_THERMAL_TPEC_12: + if (idx >= 12) + return -EINVAL; + + t = idx < 8 ? TP_EC_THERMAL_TMP0_NS + idx : + TP_EC_THERMAL_TMP8_NS + (idx - 8); + + if (!acpi_ec_read(t, &tmp)) + return -EIO; + + *value = tmp * MILLIDEGREE_PER_DEGREE; + return 0; + + case TPACPI_THERMAL_ACPI_UPDT: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, NULL, "UPDT", "v")) + return -EIO; + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + *value = (t - 2732) * 100; + return 0; + } + break; + + case TPACPI_THERMAL_ACPI_TMP07: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + if (t > 127 || t < -127) + t = TP_EC_THERMAL_TMP_NA; + *value = t * 1000; + return 0; + } + break; + + case TPACPI_THERMAL_NONE: + default: + return -ENOSYS; + } + + return -EINVAL; +} + +static int thermal_get_sensors(struct ibm_thermal_sensors_struct *s) +{ + int res, i, n; + + if (!s) + return -EINVAL; + + if (thermal_read_mode == TPACPI_THERMAL_TPEC_16) + n = 16; + else if (thermal_read_mode == TPACPI_THERMAL_TPEC_12) + n = 12; + else + n = 8; + + for (i = 0 ; i < n; i++) { + res = thermal_get_sensor(i, &s->temp[i]); + if (res) + return res; + } + + return n; +} + +static void thermal_dump_all_sensors(void) +{ + int n, i; + struct ibm_thermal_sensors_struct t; + + n = thermal_get_sensors(&t); + if (n <= 0) + return; + + pr_notice("temperatures (Celsius):"); + + for (i = 0; i < n; i++) { + if (t.temp[i] != TPACPI_THERMAL_SENSOR_NA) + pr_cont(" %d", (int)(t.temp[i] / 1000)); + else + pr_cont(" N/A"); + } + + pr_cont("\n"); +} + +/* sysfs temp##_input -------------------------------------------------- */ + +static ssize_t thermal_temp_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute *sensor_attr = + to_sensor_dev_attr(attr); + int idx = sensor_attr->index; + s32 value; + int res; + + res = thermal_get_sensor(idx, &value); + if (res) + return res; + if (value == TPACPI_THERMAL_SENSOR_NA) + return -ENXIO; + + return sysfs_emit(buf, "%d\n", value); +} + +#define THERMAL_SENSOR_ATTR_TEMP(_idxA, _idxB) \ + SENSOR_ATTR(temp##_idxA##_input, S_IRUGO, \ + thermal_temp_input_show, NULL, _idxB) + +static struct sensor_device_attribute sensor_dev_attr_thermal_temp_input[] = { + THERMAL_SENSOR_ATTR_TEMP(1, 0), + THERMAL_SENSOR_ATTR_TEMP(2, 1), + THERMAL_SENSOR_ATTR_TEMP(3, 2), + THERMAL_SENSOR_ATTR_TEMP(4, 3), + THERMAL_SENSOR_ATTR_TEMP(5, 4), + THERMAL_SENSOR_ATTR_TEMP(6, 5), + THERMAL_SENSOR_ATTR_TEMP(7, 6), + THERMAL_SENSOR_ATTR_TEMP(8, 7), + THERMAL_SENSOR_ATTR_TEMP(9, 8), + THERMAL_SENSOR_ATTR_TEMP(10, 9), + THERMAL_SENSOR_ATTR_TEMP(11, 10), + THERMAL_SENSOR_ATTR_TEMP(12, 11), + THERMAL_SENSOR_ATTR_TEMP(13, 12), + THERMAL_SENSOR_ATTR_TEMP(14, 13), + THERMAL_SENSOR_ATTR_TEMP(15, 14), + THERMAL_SENSOR_ATTR_TEMP(16, 15), +}; + +#define THERMAL_ATTRS(X) \ + &sensor_dev_attr_thermal_temp_input[X].dev_attr.attr + +static struct attribute *thermal_temp_input_attr[] = { + THERMAL_ATTRS(0), + THERMAL_ATTRS(1), + THERMAL_ATTRS(2), + THERMAL_ATTRS(3), + THERMAL_ATTRS(4), + THERMAL_ATTRS(5), + THERMAL_ATTRS(6), + THERMAL_ATTRS(7), + THERMAL_ATTRS(8), + THERMAL_ATTRS(9), + THERMAL_ATTRS(10), + THERMAL_ATTRS(11), + THERMAL_ATTRS(12), + THERMAL_ATTRS(13), + THERMAL_ATTRS(14), + THERMAL_ATTRS(15), + NULL +}; + +#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr) + +static umode_t thermal_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device_attribute *dev_attr = to_dev_attr(attr); + struct sensor_device_attribute *sensor_attr = + to_sensor_dev_attr(dev_attr); + + int idx = sensor_attr->index; + + switch (thermal_read_mode) { + case TPACPI_THERMAL_NONE: + return 0; + + case TPACPI_THERMAL_ACPI_TMP07: + case TPACPI_THERMAL_ACPI_UPDT: + case TPACPI_THERMAL_TPEC_8: + if (idx >= 8) + return 0; + break; + + case TPACPI_THERMAL_TPEC_12: + if (idx >= 12) + return 0; + break; + + default: + break; + + } + + return attr->mode; +} + +static const struct attribute_group thermal_attr_group = { + .is_visible = thermal_attr_is_visible, + .attrs = thermal_temp_input_attr, +}; + +#undef THERMAL_SENSOR_ATTR_TEMP +#undef THERMAL_ATTRS + +static ssize_t temp1_label_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "CPU\n"); +} +static DEVICE_ATTR_RO(temp1_label); + +static ssize_t temp2_label_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "GPU\n"); +} +static DEVICE_ATTR_RO(temp2_label); + +static struct attribute *temp_label_attributes[] = { + &dev_attr_temp1_label.attr, + &dev_attr_temp2_label.attr, + NULL +}; + +static umode_t temp_label_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return thermal_use_labels ? attr->mode : 0; +} + +static const struct attribute_group temp_label_attr_group = { + .is_visible = temp_label_attr_is_visible, + .attrs = temp_label_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static int __init thermal_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, "initializing thermal subdriver\n"); + + thermal_read_mode = thermal_read_mode_check(); + + vdbg_printk(TPACPI_DBG_INIT, "thermal is %s, mode %d\n", + str_supported(thermal_read_mode != TPACPI_THERMAL_NONE), + thermal_read_mode); + + return thermal_read_mode != TPACPI_THERMAL_NONE ? 0 : -ENODEV; +} + +static int thermal_read(struct seq_file *m) +{ + int n, i; + struct ibm_thermal_sensors_struct t; + + n = thermal_get_sensors(&t); + if (unlikely(n < 0)) + return n; + + seq_puts(m, "temperatures:\t"); + + if (n > 0) { + for (i = 0; i < (n - 1); i++) + seq_printf(m, "%d ", t.temp[i] / 1000); + seq_printf(m, "%d\n", t.temp[i] / 1000); + } else + seq_puts(m, "not supported\n"); + + return 0; +} + +static struct ibm_struct thermal_driver_data = { + .name = "thermal", + .read = thermal_read, +}; + +/************************************************************************* + * Backlight/brightness subdriver + */ + +#define TPACPI_BACKLIGHT_DEV_NAME "thinkpad_screen" + +/* + * ThinkPads can read brightness from two places: EC HBRV (0x31), or + * CMOS NVRAM byte 0x5E, bits 0-3. + * + * EC HBRV (0x31) has the following layout + * Bit 7: unknown function + * Bit 6: unknown function + * Bit 5: Z: honour scale changes, NZ: ignore scale changes + * Bit 4: must be set to zero to avoid problems + * Bit 3-0: backlight brightness level + * + * brightness_get_raw returns status data in the HBRV layout + * + * WARNING: The X61 has been verified to use HBRV for something else, so + * this should be used _only_ on IBM ThinkPads, and maybe with some careful + * testing on the very early *60 Lenovo models... + */ + +enum { + TP_EC_BACKLIGHT = 0x31, + + /* TP_EC_BACKLIGHT bitmasks */ + TP_EC_BACKLIGHT_LVLMSK = 0x1F, + TP_EC_BACKLIGHT_CMDMSK = 0xE0, + TP_EC_BACKLIGHT_MAPSW = 0x20, +}; + +enum tpacpi_brightness_access_mode { + TPACPI_BRGHT_MODE_AUTO = 0, /* Not implemented yet */ + TPACPI_BRGHT_MODE_EC, /* EC control */ + TPACPI_BRGHT_MODE_UCMS_STEP, /* UCMS step-based control */ + TPACPI_BRGHT_MODE_ECNVRAM, /* EC control w/ NVRAM store */ + TPACPI_BRGHT_MODE_MAX +}; + +static struct backlight_device *ibm_backlight_device; + +static enum tpacpi_brightness_access_mode brightness_mode = + TPACPI_BRGHT_MODE_MAX; + +static unsigned int brightness_enable = 2; /* 2 = auto, 0 = no, 1 = yes */ + +static struct mutex brightness_mutex; + +/* NVRAM brightness access */ +static unsigned int tpacpi_brightness_nvram_get(void) +{ + u8 lnvram; + + lockdep_assert_held(&brightness_mutex); + + lnvram = (nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS) + & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; + lnvram &= bright_maxlvl; + + return lnvram; +} + +static void tpacpi_brightness_checkpoint_nvram(void) +{ + u8 lec = 0; + u8 b_nvram; + + if (brightness_mode != TPACPI_BRGHT_MODE_ECNVRAM) + return; + + vdbg_printk(TPACPI_DBG_BRGHT, + "trying to checkpoint backlight level to NVRAM...\n"); + + if (mutex_lock_killable(&brightness_mutex) < 0) + return; + + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + goto unlock; + lec &= TP_EC_BACKLIGHT_LVLMSK; + b_nvram = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); + + if (lec != ((b_nvram & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS)) { + /* NVRAM needs update */ + b_nvram &= ~(TP_NVRAM_MASK_LEVEL_BRIGHTNESS << + TP_NVRAM_POS_LEVEL_BRIGHTNESS); + b_nvram |= lec; + nvram_write_byte(b_nvram, TP_NVRAM_ADDR_BRIGHTNESS); + dbg_printk(TPACPI_DBG_BRGHT, + "updated NVRAM backlight level to %u (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } else + vdbg_printk(TPACPI_DBG_BRGHT, + "NVRAM backlight level already is %u (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + +unlock: + mutex_unlock(&brightness_mutex); +} + + +static int tpacpi_brightness_get_raw(int *status) +{ + u8 lec = 0; + + lockdep_assert_held(&brightness_mutex); + + switch (brightness_mode) { + case TPACPI_BRGHT_MODE_UCMS_STEP: + *status = tpacpi_brightness_nvram_get(); + return 0; + case TPACPI_BRGHT_MODE_EC: + case TPACPI_BRGHT_MODE_ECNVRAM: + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + return -EIO; + *status = lec; + return 0; + default: + return -ENXIO; + } +} + +/* do NOT call with illegal backlight level value */ +static int tpacpi_brightness_set_ec(unsigned int value) +{ + u8 lec = 0; + + lockdep_assert_held(&brightness_mutex); + + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + return -EIO; + + if (unlikely(!acpi_ec_write(TP_EC_BACKLIGHT, + (lec & TP_EC_BACKLIGHT_CMDMSK) | + (value & TP_EC_BACKLIGHT_LVLMSK)))) + return -EIO; + + return 0; +} + +static int tpacpi_brightness_set_ucmsstep(unsigned int value) +{ + int cmos_cmd, inc; + unsigned int current_value, i; + + lockdep_assert_held(&brightness_mutex); + + current_value = tpacpi_brightness_nvram_get(); + + if (value == current_value) + return 0; + + cmos_cmd = (value > current_value) ? + TP_CMOS_BRIGHTNESS_UP : + TP_CMOS_BRIGHTNESS_DOWN; + inc = (value > current_value) ? 1 : -1; + + for (i = current_value; i != value; i += inc) + if (issue_thinkpad_cmos_command(cmos_cmd)) + return -EIO; + + return 0; +} + +/* May return EINTR which can always be mapped to ERESTARTSYS */ +static int brightness_set(unsigned int value) +{ + int res; + + if (value > bright_maxlvl) + return -EINVAL; + + vdbg_printk(TPACPI_DBG_BRGHT, + "set backlight level to %d\n", value); + + res = mutex_lock_killable(&brightness_mutex); + if (res < 0) + return res; + + switch (brightness_mode) { + case TPACPI_BRGHT_MODE_EC: + case TPACPI_BRGHT_MODE_ECNVRAM: + res = tpacpi_brightness_set_ec(value); + break; + case TPACPI_BRGHT_MODE_UCMS_STEP: + res = tpacpi_brightness_set_ucmsstep(value); + break; + default: + res = -ENXIO; + } + + mutex_unlock(&brightness_mutex); + return res; +} + +/* sysfs backlight class ----------------------------------------------- */ + +static int brightness_update_status(struct backlight_device *bd) +{ + int level = backlight_get_brightness(bd); + + dbg_printk(TPACPI_DBG_BRGHT, + "backlight: attempt to set level to %d\n", + level); + + /* it is the backlight class's job (caller) to handle + * EINTR and other errors properly */ + return brightness_set(level); +} + +static int brightness_get(struct backlight_device *bd) +{ + int status, res; + + res = mutex_lock_killable(&brightness_mutex); + if (res < 0) + return 0; + + res = tpacpi_brightness_get_raw(&status); + + mutex_unlock(&brightness_mutex); + + if (res < 0) + return 0; + + return status & TP_EC_BACKLIGHT_LVLMSK; +} + +static void tpacpi_brightness_notify_change(void) +{ + backlight_force_update(ibm_backlight_device, + BACKLIGHT_UPDATE_HOTKEY); +} + +static const struct backlight_ops ibm_backlight_data = { + .get_brightness = brightness_get, + .update_status = brightness_update_status, +}; + +/* --------------------------------------------------------------------- */ + +static int __init tpacpi_evaluate_bcl(struct acpi_device *adev, void *not_used) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int rc; + + status = acpi_evaluate_object(adev->handle, "_BCL", NULL, &buffer); + if (ACPI_FAILURE(status)) + return 0; + + obj = buffer.pointer; + if (!obj || obj->type != ACPI_TYPE_PACKAGE) { + acpi_handle_info(adev->handle, + "Unknown _BCL data, please report this to %s\n", + TPACPI_MAIL); + rc = 0; + } else { + rc = obj->package.count; + } + kfree(obj); + + return rc; +} + +/* + * Call _BCL method of video device. On some ThinkPads this will + * switch the firmware to the ACPI brightness control mode. + */ + +static int __init tpacpi_query_bcl_levels(acpi_handle handle) +{ + struct acpi_device *device; + + device = acpi_fetch_acpi_dev(handle); + if (!device) + return 0; + + return acpi_dev_for_each_child(device, tpacpi_evaluate_bcl, NULL); +} + + +/* + * Returns 0 (no ACPI _BCL or _BCL invalid), or size of brightness map + */ +static unsigned int __init tpacpi_check_std_acpi_brightness_support(void) +{ + acpi_handle video_device; + int bcl_levels = 0; + + tpacpi_acpi_handle_locate("video", NULL, &video_device); + if (video_device) + bcl_levels = tpacpi_query_bcl_levels(video_device); + + tp_features.bright_acpimode = (bcl_levels > 0); + + return (bcl_levels > 2) ? (bcl_levels - 2) : 0; +} + +/* + * These are only useful for models that have only one possibility + * of GPU. If the BIOS model handles both ATI and Intel, don't use + * these quirks. + */ +#define TPACPI_BRGHT_Q_NOEC 0x0001 /* Must NOT use EC HBRV */ +#define TPACPI_BRGHT_Q_EC 0x0002 /* Should or must use EC HBRV */ +#define TPACPI_BRGHT_Q_ASK 0x8000 /* Ask for user report */ + +static const struct tpacpi_quirk brightness_quirk_table[] __initconst = { + /* Models with ATI GPUs known to require ECNVRAM mode */ + TPACPI_Q_IBM('1', 'Y', TPACPI_BRGHT_Q_EC), /* T43/p ATI */ + + /* Models with ATI GPUs that can use ECNVRAM */ + TPACPI_Q_IBM('1', 'R', TPACPI_BRGHT_Q_EC), /* R50,51 T40-42 */ + TPACPI_Q_IBM('1', 'Q', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + TPACPI_Q_IBM('7', '6', TPACPI_BRGHT_Q_EC), /* R52 */ + TPACPI_Q_IBM('7', '8', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + + /* Models with Intel Extreme Graphics 2 */ + TPACPI_Q_IBM('1', 'U', TPACPI_BRGHT_Q_NOEC), /* X40 */ + TPACPI_Q_IBM('1', 'V', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + TPACPI_Q_IBM('1', 'W', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + + /* Models with Intel GMA900 */ + TPACPI_Q_IBM('7', '0', TPACPI_BRGHT_Q_NOEC), /* T43, R52 */ + TPACPI_Q_IBM('7', '4', TPACPI_BRGHT_Q_NOEC), /* X41 */ + TPACPI_Q_IBM('7', '5', TPACPI_BRGHT_Q_NOEC), /* X41 Tablet */ +}; + +/* + * Returns < 0 for error, otherwise sets tp_features.bright_* + * and bright_maxlvl. + */ +static void __init tpacpi_detect_brightness_capabilities(void) +{ + unsigned int b; + + vdbg_printk(TPACPI_DBG_INIT, + "detecting firmware brightness interface capabilities\n"); + + /* we could run a quirks check here (same table used by + * brightness_init) if needed */ + + /* + * We always attempt to detect acpi support, so as to switch + * Lenovo Vista BIOS to ACPI brightness mode even if we are not + * going to publish a backlight interface + */ + b = tpacpi_check_std_acpi_brightness_support(); + switch (b) { + case 16: + bright_maxlvl = 15; + break; + case 8: + case 0: + bright_maxlvl = 7; + break; + default: + tp_features.bright_unkfw = 1; + bright_maxlvl = b - 1; + } + pr_debug("detected %u brightness levels\n", bright_maxlvl + 1); +} + +static int __init brightness_init(struct ibm_init_struct *iibm) +{ + struct backlight_properties props; + int b; + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT, "initializing brightness subdriver\n"); + + mutex_init(&brightness_mutex); + + quirks = tpacpi_check_quirks(brightness_quirk_table, + ARRAY_SIZE(brightness_quirk_table)); + + /* tpacpi_detect_brightness_capabilities() must have run already */ + + /* if it is unknown, we don't handle it: it wouldn't be safe */ + if (tp_features.bright_unkfw) + return -ENODEV; + + if (!brightness_enable) { + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness support disabled by module parameter\n"); + return -ENODEV; + } + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { + if (brightness_enable > 1) { + pr_info("Standard ACPI backlight interface available, not loading native one\n"); + return -ENODEV; + } else if (brightness_enable == 1) { + pr_warn("Cannot enable backlight brightness support, ACPI is already handling it. Refer to the acpi_backlight kernel parameter.\n"); + return -ENODEV; + } + } else if (!tp_features.bright_acpimode) { + pr_notice("ACPI backlight interface not available\n"); + return -ENODEV; + } + + pr_notice("ACPI native brightness control enabled\n"); + + /* + * Check for module parameter bogosity, note that we + * init brightness_mode to TPACPI_BRGHT_MODE_MAX in order to be + * able to detect "unspecified" + */ + if (brightness_mode > TPACPI_BRGHT_MODE_MAX) + return -EINVAL; + + /* TPACPI_BRGHT_MODE_AUTO not implemented yet, just use default */ + if (brightness_mode == TPACPI_BRGHT_MODE_AUTO || + brightness_mode == TPACPI_BRGHT_MODE_MAX) { + if (quirks & TPACPI_BRGHT_Q_EC) + brightness_mode = TPACPI_BRGHT_MODE_ECNVRAM; + else + brightness_mode = TPACPI_BRGHT_MODE_UCMS_STEP; + + dbg_printk(TPACPI_DBG_BRGHT, + "driver auto-selected brightness_mode=%d\n", + brightness_mode); + } + + /* Safety */ + if (!tpacpi_is_ibm() && + (brightness_mode == TPACPI_BRGHT_MODE_ECNVRAM || + brightness_mode == TPACPI_BRGHT_MODE_EC)) + return -EINVAL; + + if (tpacpi_brightness_get_raw(&b) < 0) + return -ENODEV; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = bright_maxlvl; + props.brightness = b & TP_EC_BACKLIGHT_LVLMSK; + ibm_backlight_device = backlight_device_register(TPACPI_BACKLIGHT_DEV_NAME, + NULL, NULL, + &ibm_backlight_data, + &props); + if (IS_ERR(ibm_backlight_device)) { + int rc = PTR_ERR(ibm_backlight_device); + ibm_backlight_device = NULL; + pr_err("Could not register backlight device\n"); + return rc; + } + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness is supported\n"); + + if (quirks & TPACPI_BRGHT_Q_ASK) { + pr_notice("brightness: will use unverified default: brightness_mode=%d\n", + brightness_mode); + pr_notice("brightness: please report to %s whether it works well or not on your ThinkPad\n", + TPACPI_MAIL); + } + + /* Added by mistake in early 2007. Probably useless, but it could + * be working around some unknown firmware problem where the value + * read at startup doesn't match the real hardware state... so leave + * it in place just in case */ + backlight_update_status(ibm_backlight_device); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness: registering brightness hotkeys as change notification\n"); + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask + | TP_ACPI_HKEY_BRGHTUP_MASK + | TP_ACPI_HKEY_BRGHTDWN_MASK); + return 0; +} + +static void brightness_suspend(void) +{ + tpacpi_brightness_checkpoint_nvram(); +} + +static void brightness_shutdown(void) +{ + tpacpi_brightness_checkpoint_nvram(); +} + +static void brightness_exit(void) +{ + if (ibm_backlight_device) { + vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_BRGHT, + "calling backlight_device_unregister()\n"); + backlight_device_unregister(ibm_backlight_device); + } + + tpacpi_brightness_checkpoint_nvram(); +} + +static int brightness_read(struct seq_file *m) +{ + int level; + + level = brightness_get(NULL); + if (level < 0) { + seq_puts(m, "level:\t\tunreadable\n"); + } else { + seq_printf(m, "level:\t\t%d\n", level); + seq_puts(m, "commands:\tup, down\n"); + seq_printf(m, "commands:\tlevel ( is 0-%d)\n", + bright_maxlvl); + } + + return 0; +} + +static int brightness_write(char *buf) +{ + int level; + int rc; + char *cmd; + + level = brightness_get(NULL); + if (level < 0) + return level; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "up")) { + if (level < bright_maxlvl) + level++; + } else if (strstarts(cmd, "down")) { + if (level > 0) + level--; + } else if (sscanf(cmd, "level %d", &level) == 1 && + level >= 0 && level <= bright_maxlvl) { + /* new level set */ + } else + return -EINVAL; + } + + tpacpi_disclose_usertask("procfs brightness", + "set level to %d\n", level); + + /* + * Now we know what the final level should be, so we try to set it. + * Doing it this way makes the syscall restartable in case of EINTR + */ + rc = brightness_set(level); + if (!rc && ibm_backlight_device) + backlight_force_update(ibm_backlight_device, + BACKLIGHT_UPDATE_SYSFS); + return (rc == -EINTR) ? -ERESTARTSYS : rc; +} + +static struct ibm_struct brightness_driver_data = { + .name = "brightness", + .read = brightness_read, + .write = brightness_write, + .exit = brightness_exit, + .suspend = brightness_suspend, + .shutdown = brightness_shutdown, +}; + +/************************************************************************* + * Volume subdriver + */ + +/* + * IBM ThinkPads have a simple volume controller with MUTE gating. + * Very early Lenovo ThinkPads follow the IBM ThinkPad spec. + * + * Since the *61 series (and probably also the later *60 series), Lenovo + * ThinkPads only implement the MUTE gate. + * + * EC register 0x30 + * Bit 6: MUTE (1 mutes sound) + * Bit 3-0: Volume + * Other bits should be zero as far as we know. + * + * This is also stored in CMOS NVRAM, byte 0x60, bit 6 (MUTE), and + * bits 3-0 (volume). Other bits in NVRAM may have other functions, + * such as bit 7 which is used to detect repeated presses of MUTE, + * and we leave them unchanged. + * + * On newer Lenovo ThinkPads, the EC can automatically change the volume + * in response to user input. Unfortunately, this rarely works well. + * The laptop changes the state of its internal MUTE gate and, on some + * models, sends KEY_MUTE, causing any user code that responds to the + * mute button to get confused. The hardware MUTE gate is also + * unnecessary, since user code can handle the mute button without + * kernel or EC help. + * + * To avoid confusing userspace, we simply disable all EC-based mute + * and volume controls when possible. + */ + +#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT + +#define TPACPI_ALSA_DRVNAME "ThinkPad EC" +#define TPACPI_ALSA_SHRTNAME "ThinkPad Console Audio Control" +#define TPACPI_ALSA_MIXERNAME TPACPI_ALSA_SHRTNAME + +#if SNDRV_CARDS <= 32 +#define DEFAULT_ALSA_IDX ~((1 << (SNDRV_CARDS - 3)) - 1) +#else +#define DEFAULT_ALSA_IDX ~((1 << (32 - 3)) - 1) +#endif +static int alsa_index = DEFAULT_ALSA_IDX; /* last three slots */ +static char *alsa_id = "ThinkPadEC"; +static bool alsa_enable = SNDRV_DEFAULT_ENABLE1; + +struct tpacpi_alsa_data { + struct snd_card *card; + struct snd_ctl_elem_id *ctl_mute_id; + struct snd_ctl_elem_id *ctl_vol_id; +}; + +static struct snd_card *alsa_card; + +enum { + TP_EC_AUDIO = 0x30, + + /* TP_EC_AUDIO bits */ + TP_EC_AUDIO_MUTESW = 6, + + /* TP_EC_AUDIO bitmasks */ + TP_EC_AUDIO_LVL_MSK = 0x0F, + TP_EC_AUDIO_MUTESW_MSK = (1 << TP_EC_AUDIO_MUTESW), + + /* Maximum volume */ + TP_EC_VOLUME_MAX = 14, +}; + +enum tpacpi_volume_access_mode { + TPACPI_VOL_MODE_AUTO = 0, /* Not implemented yet */ + TPACPI_VOL_MODE_EC, /* Pure EC control */ + TPACPI_VOL_MODE_UCMS_STEP, /* UCMS step-based control: N/A */ + TPACPI_VOL_MODE_ECNVRAM, /* EC control w/ NVRAM store */ + TPACPI_VOL_MODE_MAX +}; + +enum tpacpi_volume_capabilities { + TPACPI_VOL_CAP_AUTO = 0, /* Use white/blacklist */ + TPACPI_VOL_CAP_VOLMUTE, /* Output vol and mute */ + TPACPI_VOL_CAP_MUTEONLY, /* Output mute only */ + TPACPI_VOL_CAP_MAX +}; + +enum tpacpi_mute_btn_mode { + TP_EC_MUTE_BTN_LATCH = 0, /* Mute mutes; up/down unmutes */ + /* We don't know what mode 1 is. */ + TP_EC_MUTE_BTN_NONE = 2, /* Mute and up/down are just keys */ + TP_EC_MUTE_BTN_TOGGLE = 3, /* Mute toggles; up/down unmutes */ +}; + +static enum tpacpi_volume_access_mode volume_mode = + TPACPI_VOL_MODE_MAX; + +static enum tpacpi_volume_capabilities volume_capabilities; +static bool volume_control_allowed; +static bool software_mute_requested = true; +static bool software_mute_active; +static int software_mute_orig_mode; + +/* + * Used to syncronize writers to TP_EC_AUDIO and + * TP_NVRAM_ADDR_MIXER, as we need to do read-modify-write + */ +static struct mutex volume_mutex; + +static void tpacpi_volume_checkpoint_nvram(void) +{ + u8 lec = 0; + u8 b_nvram; + u8 ec_mask; + + if (volume_mode != TPACPI_VOL_MODE_ECNVRAM) + return; + if (!volume_control_allowed) + return; + if (software_mute_active) + return; + + vdbg_printk(TPACPI_DBG_MIXER, + "trying to checkpoint mixer state to NVRAM...\n"); + + if (tp_features.mixer_no_level_control) + ec_mask = TP_EC_AUDIO_MUTESW_MSK; + else + ec_mask = TP_EC_AUDIO_MUTESW_MSK | TP_EC_AUDIO_LVL_MSK; + + if (mutex_lock_killable(&volume_mutex) < 0) + return; + + if (unlikely(!acpi_ec_read(TP_EC_AUDIO, &lec))) + goto unlock; + lec &= ec_mask; + b_nvram = nvram_read_byte(TP_NVRAM_ADDR_MIXER); + + if (lec != (b_nvram & ec_mask)) { + /* NVRAM needs update */ + b_nvram &= ~ec_mask; + b_nvram |= lec; + nvram_write_byte(b_nvram, TP_NVRAM_ADDR_MIXER); + dbg_printk(TPACPI_DBG_MIXER, + "updated NVRAM mixer status to 0x%02x (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } else { + vdbg_printk(TPACPI_DBG_MIXER, + "NVRAM mixer status already is 0x%02x (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } + +unlock: + mutex_unlock(&volume_mutex); +} + +static int volume_get_status_ec(u8 *status) +{ + u8 s; + + if (!acpi_ec_read(TP_EC_AUDIO, &s)) + return -EIO; + + *status = s; + + dbg_printk(TPACPI_DBG_MIXER, "status 0x%02x\n", s); + + return 0; +} + +static int volume_get_status(u8 *status) +{ + return volume_get_status_ec(status); +} + +static int volume_set_status_ec(const u8 status) +{ + if (!acpi_ec_write(TP_EC_AUDIO, status)) + return -EIO; + + dbg_printk(TPACPI_DBG_MIXER, "set EC mixer to 0x%02x\n", status); + + /* + * On X200s, and possibly on others, it can take a while for + * reads to become correct. + */ + msleep(1); + + return 0; +} + +static int volume_set_status(const u8 status) +{ + return volume_set_status_ec(status); +} + +/* returns < 0 on error, 0 on no change, 1 on change */ +static int __volume_set_mute_ec(const bool mute) +{ + int rc; + u8 s, n; + + if (mutex_lock_killable(&volume_mutex) < 0) + return -EINTR; + + rc = volume_get_status_ec(&s); + if (rc) + goto unlock; + + n = (mute) ? s | TP_EC_AUDIO_MUTESW_MSK : + s & ~TP_EC_AUDIO_MUTESW_MSK; + + if (n != s) { + rc = volume_set_status_ec(n); + if (!rc) + rc = 1; + } + +unlock: + mutex_unlock(&volume_mutex); + return rc; +} + +static int volume_alsa_set_mute(const bool mute) +{ + dbg_printk(TPACPI_DBG_MIXER, "ALSA: trying to %smute\n", + (mute) ? "" : "un"); + return __volume_set_mute_ec(mute); +} + +static int volume_set_mute(const bool mute) +{ + int rc; + + dbg_printk(TPACPI_DBG_MIXER, "trying to %smute\n", + (mute) ? "" : "un"); + + rc = __volume_set_mute_ec(mute); + return (rc < 0) ? rc : 0; +} + +/* returns < 0 on error, 0 on no change, 1 on change */ +static int __volume_set_volume_ec(const u8 vol) +{ + int rc; + u8 s, n; + + if (vol > TP_EC_VOLUME_MAX) + return -EINVAL; + + if (mutex_lock_killable(&volume_mutex) < 0) + return -EINTR; + + rc = volume_get_status_ec(&s); + if (rc) + goto unlock; + + n = (s & ~TP_EC_AUDIO_LVL_MSK) | vol; + + if (n != s) { + rc = volume_set_status_ec(n); + if (!rc) + rc = 1; + } + +unlock: + mutex_unlock(&volume_mutex); + return rc; +} + +static int volume_set_software_mute(bool startup) +{ + int result; + + if (!tpacpi_is_lenovo()) + return -ENODEV; + + if (startup) { + if (!acpi_evalf(ec_handle, &software_mute_orig_mode, + "HAUM", "qd")) + return -EIO; + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "Initial HAUM setting was %d\n", + software_mute_orig_mode); + } + + if (!acpi_evalf(ec_handle, &result, "SAUM", "qdd", + (int)TP_EC_MUTE_BTN_NONE)) + return -EIO; + + if (result != TP_EC_MUTE_BTN_NONE) + pr_warn("Unexpected SAUM result %d\n", + result); + + /* + * In software mute mode, the standard codec controls take + * precendence, so we unmute the ThinkPad HW switch at + * startup. Just on case there are SAUM-capable ThinkPads + * with level controls, set max HW volume as well. + */ + if (tp_features.mixer_no_level_control) + result = volume_set_mute(false); + else + result = volume_set_status(TP_EC_VOLUME_MAX); + + if (result != 0) + pr_warn("Failed to unmute the HW mute switch\n"); + + return 0; +} + +static void volume_exit_software_mute(void) +{ + int r; + + if (!acpi_evalf(ec_handle, &r, "SAUM", "qdd", software_mute_orig_mode) + || r != software_mute_orig_mode) + pr_warn("Failed to restore mute mode\n"); +} + +static int volume_alsa_set_volume(const u8 vol) +{ + dbg_printk(TPACPI_DBG_MIXER, + "ALSA: trying to set volume level to %hu\n", vol); + return __volume_set_volume_ec(vol); +} + +static void volume_alsa_notify_change(void) +{ + struct tpacpi_alsa_data *d; + + if (alsa_card && alsa_card->private_data) { + d = alsa_card->private_data; + if (d->ctl_mute_id) + snd_ctl_notify(alsa_card, + SNDRV_CTL_EVENT_MASK_VALUE, + d->ctl_mute_id); + if (d->ctl_vol_id) + snd_ctl_notify(alsa_card, + SNDRV_CTL_EVENT_MASK_VALUE, + d->ctl_vol_id); + } +} + +static int volume_alsa_vol_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 = 0; + uinfo->value.integer.max = TP_EC_VOLUME_MAX; + return 0; +} + +static int volume_alsa_vol_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + u8 s; + int rc; + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + ucontrol->value.integer.value[0] = s & TP_EC_AUDIO_LVL_MSK; + return 0; +} + +static int volume_alsa_vol_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + tpacpi_disclose_usertask("ALSA", "set volume to %ld\n", + ucontrol->value.integer.value[0]); + return volume_alsa_set_volume(ucontrol->value.integer.value[0]); +} + +#define volume_alsa_mute_info snd_ctl_boolean_mono_info + +static int volume_alsa_mute_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + u8 s; + int rc; + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + ucontrol->value.integer.value[0] = + (s & TP_EC_AUDIO_MUTESW_MSK) ? 0 : 1; + return 0; +} + +static int volume_alsa_mute_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + tpacpi_disclose_usertask("ALSA", "%smute\n", + ucontrol->value.integer.value[0] ? + "un" : ""); + return volume_alsa_set_mute(!ucontrol->value.integer.value[0]); +} + +static struct snd_kcontrol_new volume_alsa_control_vol __initdata = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Console Playback Volume", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READ, + .info = volume_alsa_vol_info, + .get = volume_alsa_vol_get, +}; + +static struct snd_kcontrol_new volume_alsa_control_mute __initdata = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Console Playback Switch", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READ, + .info = volume_alsa_mute_info, + .get = volume_alsa_mute_get, +}; + +static void volume_suspend(void) +{ + tpacpi_volume_checkpoint_nvram(); +} + +static void volume_resume(void) +{ + if (software_mute_active) { + if (volume_set_software_mute(false) < 0) + pr_warn("Failed to restore software mute\n"); + } else { + volume_alsa_notify_change(); + } +} + +static void volume_shutdown(void) +{ + tpacpi_volume_checkpoint_nvram(); +} + +static void volume_exit(void) +{ + if (alsa_card) { + snd_card_free(alsa_card); + alsa_card = NULL; + } + + tpacpi_volume_checkpoint_nvram(); + + if (software_mute_active) + volume_exit_software_mute(); +} + +static int __init volume_create_alsa_mixer(void) +{ + struct snd_card *card; + struct tpacpi_alsa_data *data; + struct snd_kcontrol *ctl_vol; + struct snd_kcontrol *ctl_mute; + int rc; + + rc = snd_card_new(&tpacpi_pdev->dev, + alsa_index, alsa_id, THIS_MODULE, + sizeof(struct tpacpi_alsa_data), &card); + if (rc < 0 || !card) { + pr_err("Failed to create ALSA card structures: %d\n", rc); + return -ENODEV; + } + + BUG_ON(!card->private_data); + data = card->private_data; + data->card = card; + + strscpy(card->driver, TPACPI_ALSA_DRVNAME); + strscpy(card->shortname, TPACPI_ALSA_SHRTNAME); + snprintf(card->mixername, sizeof(card->mixername), "ThinkPad EC %s", + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "(unknown)"); + snprintf(card->longname, sizeof(card->longname), + "%s at EC reg 0x%02x, fw %s", card->shortname, TP_EC_AUDIO, + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "unknown"); + + if (volume_control_allowed) { + volume_alsa_control_vol.put = volume_alsa_vol_put; + volume_alsa_control_vol.access = + SNDRV_CTL_ELEM_ACCESS_READWRITE; + + volume_alsa_control_mute.put = volume_alsa_mute_put; + volume_alsa_control_mute.access = + SNDRV_CTL_ELEM_ACCESS_READWRITE; + } + + if (!tp_features.mixer_no_level_control) { + ctl_vol = snd_ctl_new1(&volume_alsa_control_vol, NULL); + rc = snd_ctl_add(card, ctl_vol); + if (rc < 0) { + pr_err("Failed to create ALSA volume control: %d\n", + rc); + goto err_exit; + } + data->ctl_vol_id = &ctl_vol->id; + } + + ctl_mute = snd_ctl_new1(&volume_alsa_control_mute, NULL); + rc = snd_ctl_add(card, ctl_mute); + if (rc < 0) { + pr_err("Failed to create ALSA mute control: %d\n", rc); + goto err_exit; + } + data->ctl_mute_id = &ctl_mute->id; + + rc = snd_card_register(card); + if (rc < 0) { + pr_err("Failed to register ALSA card: %d\n", rc); + goto err_exit; + } + + alsa_card = card; + return 0; + +err_exit: + snd_card_free(card); + return -ENODEV; +} + +#define TPACPI_VOL_Q_MUTEONLY 0x0001 /* Mute-only control available */ +#define TPACPI_VOL_Q_LEVEL 0x0002 /* Volume control available */ + +static const struct tpacpi_quirk volume_quirk_table[] __initconst = { + /* Whitelist volume level on all IBM by default */ + { .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, + .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_VOL_Q_LEVEL }, + + /* Lenovo models with volume control (needs confirmation) */ + TPACPI_QEC_LNV('7', 'C', TPACPI_VOL_Q_LEVEL), /* R60/i */ + TPACPI_QEC_LNV('7', 'E', TPACPI_VOL_Q_LEVEL), /* R60e/i */ + TPACPI_QEC_LNV('7', '9', TPACPI_VOL_Q_LEVEL), /* T60/p */ + TPACPI_QEC_LNV('7', 'B', TPACPI_VOL_Q_LEVEL), /* X60/s */ + TPACPI_QEC_LNV('7', 'J', TPACPI_VOL_Q_LEVEL), /* X60t */ + TPACPI_QEC_LNV('7', '7', TPACPI_VOL_Q_LEVEL), /* Z60 */ + TPACPI_QEC_LNV('7', 'F', TPACPI_VOL_Q_LEVEL), /* Z61 */ + + /* Whitelist mute-only on all Lenovo by default */ + { .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, + .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_VOL_Q_MUTEONLY } +}; + +static int __init volume_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing volume subdriver\n"); + + mutex_init(&volume_mutex); + + /* + * Check for module parameter bogosity, note that we + * init volume_mode to TPACPI_VOL_MODE_MAX in order to be + * able to detect "unspecified" + */ + if (volume_mode > TPACPI_VOL_MODE_MAX) + return -EINVAL; + + if (volume_mode == TPACPI_VOL_MODE_UCMS_STEP) { + pr_err("UCMS step volume mode not implemented, please contact %s\n", + TPACPI_MAIL); + return -ENODEV; + } + + if (volume_capabilities >= TPACPI_VOL_CAP_MAX) + return -EINVAL; + + /* + * The ALSA mixer is our primary interface. + * When disabled, don't install the subdriver at all + */ + if (!alsa_enable) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "ALSA mixer disabled by parameter, not loading volume subdriver...\n"); + return -ENODEV; + } + + quirks = tpacpi_check_quirks(volume_quirk_table, + ARRAY_SIZE(volume_quirk_table)); + + switch (volume_capabilities) { + case TPACPI_VOL_CAP_AUTO: + if (quirks & TPACPI_VOL_Q_MUTEONLY) + tp_features.mixer_no_level_control = 1; + else if (quirks & TPACPI_VOL_Q_LEVEL) + tp_features.mixer_no_level_control = 0; + else + return -ENODEV; /* no mixer */ + break; + case TPACPI_VOL_CAP_VOLMUTE: + tp_features.mixer_no_level_control = 0; + break; + case TPACPI_VOL_CAP_MUTEONLY: + tp_features.mixer_no_level_control = 1; + break; + default: + return -ENODEV; + } + + if (volume_capabilities != TPACPI_VOL_CAP_AUTO) + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "using user-supplied volume_capabilities=%d\n", + volume_capabilities); + + if (volume_mode == TPACPI_VOL_MODE_AUTO || + volume_mode == TPACPI_VOL_MODE_MAX) { + volume_mode = TPACPI_VOL_MODE_ECNVRAM; + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "driver auto-selected volume_mode=%d\n", + volume_mode); + } else { + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "using user-supplied volume_mode=%d\n", + volume_mode); + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "mute is supported, volume control is %s\n", + str_supported(!tp_features.mixer_no_level_control)); + + if (software_mute_requested && volume_set_software_mute(true) == 0) { + software_mute_active = true; + } else { + rc = volume_create_alsa_mixer(); + if (rc) { + pr_err("Could not create the ALSA mixer interface\n"); + return rc; + } + + pr_info("Console audio control enabled, mode: %s\n", + (volume_control_allowed) ? + "override (read/write)" : + "monitor (read only)"); + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "registering volume hotkeys as change notification\n"); + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask + | TP_ACPI_HKEY_VOLUP_MASK + | TP_ACPI_HKEY_VOLDWN_MASK + | TP_ACPI_HKEY_MUTE_MASK); + + return 0; +} + +static int volume_read(struct seq_file *m) +{ + u8 status; + + if (volume_get_status(&status) < 0) { + seq_puts(m, "level:\t\tunreadable\n"); + } else { + if (tp_features.mixer_no_level_control) + seq_puts(m, "level:\t\tunsupported\n"); + else + seq_printf(m, "level:\t\t%d\n", + status & TP_EC_AUDIO_LVL_MSK); + + seq_printf(m, "mute:\t\t%s\n", str_on_off(status & BIT(TP_EC_AUDIO_MUTESW))); + + if (volume_control_allowed) { + seq_puts(m, "commands:\tunmute, mute\n"); + if (!tp_features.mixer_no_level_control) { + seq_puts(m, "commands:\tup, down\n"); + seq_printf(m, "commands:\tlevel ( is 0-%d)\n", + TP_EC_VOLUME_MAX); + } + } + } + + return 0; +} + +static int volume_write(char *buf) +{ + u8 s; + u8 new_level, new_mute; + int l; + char *cmd; + int rc; + + /* + * We do allow volume control at driver startup, so that the + * user can set initial state through the volume=... parameter hack. + */ + if (!volume_control_allowed && tpacpi_lifecycle != TPACPI_LIFE_INIT) { + if (unlikely(!tp_warned.volume_ctrl_forbidden)) { + tp_warned.volume_ctrl_forbidden = 1; + pr_notice("Console audio control in monitor mode, changes are not allowed\n"); + pr_notice("Use the volume_control=1 module parameter to enable volume control\n"); + } + return -EPERM; + } + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + new_level = s & TP_EC_AUDIO_LVL_MSK; + new_mute = s & TP_EC_AUDIO_MUTESW_MSK; + + while ((cmd = strsep(&buf, ","))) { + if (!tp_features.mixer_no_level_control) { + if (strstarts(cmd, "up")) { + if (new_mute) + new_mute = 0; + else if (new_level < TP_EC_VOLUME_MAX) + new_level++; + continue; + } else if (strstarts(cmd, "down")) { + if (new_mute) + new_mute = 0; + else if (new_level > 0) + new_level--; + continue; + } else if (sscanf(cmd, "level %u", &l) == 1 && + l >= 0 && l <= TP_EC_VOLUME_MAX) { + new_level = l; + continue; + } + } + if (strstarts(cmd, "mute")) + new_mute = TP_EC_AUDIO_MUTESW_MSK; + else if (strstarts(cmd, "unmute")) + new_mute = 0; + else + return -EINVAL; + } + + if (tp_features.mixer_no_level_control) { + tpacpi_disclose_usertask("procfs volume", "%smute\n", + new_mute ? "" : "un"); + rc = volume_set_mute(!!new_mute); + } else { + tpacpi_disclose_usertask("procfs volume", + "%smute and set level to %d\n", + new_mute ? "" : "un", new_level); + rc = volume_set_status(new_mute | new_level); + } + volume_alsa_notify_change(); + + return (rc == -EINTR) ? -ERESTARTSYS : rc; +} + +static struct ibm_struct volume_driver_data = { + .name = "volume", + .read = volume_read, + .write = volume_write, + .exit = volume_exit, + .suspend = volume_suspend, + .resume = volume_resume, + .shutdown = volume_shutdown, +}; + +#else /* !CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +#define alsa_card NULL + +static inline void volume_alsa_notify_change(void) +{ +} + +static int __init volume_init(struct ibm_init_struct *iibm) +{ + pr_info("volume: disabled as there is no ALSA support in this kernel\n"); + + return -ENODEV; +} + +static struct ibm_struct volume_driver_data = { + .name = "volume", +}; + +#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +/************************************************************************* + * Fan subdriver + */ + +/* + * FAN ACCESS MODES + * + * TPACPI_FAN_RD_ACPI_GFAN: + * ACPI GFAN method: returns fan level + * + * see TPACPI_FAN_WR_ACPI_SFAN + * EC 0x2f (HFSP) not available if GFAN exists + * + * TPACPI_FAN_WR_ACPI_SFAN: + * ACPI SFAN method: sets fan level, 0 (stop) to 7 (max) + * + * EC 0x2f (HFSP) might be available *for reading*, but do not use + * it for writing. + * + * TPACPI_FAN_RD_ACPI_FANG: + * ACPI FANG method: returns fan control register + * + * Takes one parameter which is 0x8100 plus the offset to EC memory + * address 0xf500 and returns the byte at this address. + * + * 0xf500: + * When the value is less than 9 automatic mode is enabled + * 0xf502: + * Contains the current fan speed from 0-100% + * 0xf506: + * Bit 7 has to be set in order to enable manual control by + * writing a value >= 9 to 0xf500 + * + * TPACPI_FAN_WR_ACPI_FANW: + * ACPI FANW method: sets fan control registers + * + * Takes 0x8100 plus the offset to EC memory address 0xf500 and the + * value to be written there as parameters. + * + * see TPACPI_FAN_RD_ACPI_FANG + * + * TPACPI_FAN_WR_TPEC: + * ThinkPad EC register 0x2f (HFSP): fan control loop mode + * Supported on almost all ThinkPads + * + * Fan speed changes of any sort (including those caused by the + * disengaged mode) are usually done slowly by the firmware as the + * maximum amount of fan duty cycle change per second seems to be + * limited. + * + * Reading is not available if GFAN exists. + * Writing is not available if SFAN exists. + * + * Bits + * 7 automatic mode engaged; + * (default operation mode of the ThinkPad) + * fan level is ignored in this mode. + * 6 full speed mode (takes precedence over bit 7); + * not available on all thinkpads. May disable + * the tachometer while the fan controller ramps up + * the speed (which can take up to a few *minutes*). + * Speeds up fan to 100% duty-cycle, which is far above + * the standard RPM levels. It is not impossible that + * it could cause hardware damage. + * 5-3 unused in some models. Extra bits for fan level + * in others, but still useless as all values above + * 7 map to the same speed as level 7 in these models. + * 2-0 fan level (0..7 usually) + * 0x00 = stop + * 0x07 = max (set when temperatures critical) + * Some ThinkPads may have other levels, see + * TPACPI_FAN_WR_ACPI_FANS (X31/X40/X41) + * + * FIRMWARE BUG: on some models, EC 0x2f might not be initialized at + * boot. Apparently the EC does not initialize it, so unless ACPI DSDT + * does so, its initial value is meaningless (0x07). + * + * For firmware bugs, refer to: + * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * ---- + * + * ThinkPad EC register 0x84 (LSB), 0x85 (MSB): + * Main fan tachometer reading (in RPM) + * + * This register is present on all ThinkPads with a new-style EC, and + * it is known not to be present on the A21m/e, and T22, as there is + * something else in offset 0x84 according to the ACPI DSDT. Other + * ThinkPads from this same time period (and earlier) probably lack the + * tachometer as well. + * + * Unfortunately a lot of ThinkPads with new-style ECs but whose firmware + * was never fixed by IBM to report the EC firmware version string + * probably support the tachometer (like the early X models), so + * detecting it is quite hard. We need more data to know for sure. + * + * FIRMWARE BUG: always read 0x84 first, otherwise incorrect readings + * might result. + * + * FIRMWARE BUG: may go stale while the EC is switching to full speed + * mode. + * + * For firmware bugs, refer to: + * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * ---- + * + * ThinkPad EC register 0x31 bit 0 (only on select models) + * + * When bit 0 of EC register 0x31 is zero, the tachometer registers + * show the speed of the main fan. When bit 0 of EC register 0x31 + * is one, the tachometer registers show the speed of the auxiliary + * fan. + * + * Fan control seems to affect both fans, regardless of the state + * of this bit. + * + * So far, only the firmware for the X60/X61 non-tablet versions + * seem to support this (firmware TP-7M). + * + * TPACPI_FAN_WR_ACPI_FANS: + * ThinkPad X31, X40, X41. Not available in the X60. + * + * FANS ACPI handle: takes three arguments: low speed, medium speed, + * high speed. ACPI DSDT seems to map these three speeds to levels + * as follows: STOP LOW LOW MED MED HIGH HIGH HIGH HIGH + * (this map is stored on FAN0..FAN8 as "0,1,1,2,2,3,3,3,3") + * + * The speeds are stored on handles + * (FANA:FAN9), (FANC:FANB), (FANE:FAND). + * + * There are three default speed sets, accessible as handles: + * FS1L,FS1M,FS1H; FS2L,FS2M,FS2H; FS3L,FS3M,FS3H + * + * ACPI DSDT switches which set is in use depending on various + * factors. + * + * TPACPI_FAN_WR_TPEC is also available and should be used to + * command the fan. The X31/X40/X41 seems to have 8 fan levels, + * but the ACPI tables just mention level 7. + * + * TPACPI_FAN_RD_TPEC_NS: + * This mode is used for a few ThinkPads (L13 Yoga Gen2, X13 Yoga Gen2 etc.) + * that are using non-standard EC locations for reporting fan speeds. + * Currently these platforms only provide fan rpm reporting. + * + */ + +#define FAN_RPM_CAL_CONST 491520 /* FAN RPM calculation offset for some non-standard ECFW */ + +#define FAN_NS_CTRL_STATUS BIT(2) /* Bit which determines control is enabled or not */ +#define FAN_NS_CTRL BIT(4) /* Bit which determines control is by host or EC */ +#define FAN_CLOCK_TPM (22500*60) /* Ticks per minute for a 22.5 kHz clock */ + +enum { /* Fan control constants */ + fan_status_offset = 0x2f, /* EC register 0x2f */ + fan_rpm_offset = 0x84, /* EC register 0x84: LSB, 0x85 MSB (RPM) + * 0x84 must be read before 0x85 */ + fan_select_offset = 0x31, /* EC register 0x31 (Firmware 7M) + bit 0 selects which fan is active */ + + fan_status_offset_ns = 0x93, /* Special status/control offset for non-standard EC Fan1 */ + fan2_status_offset_ns = 0x96, /* Special status/control offset for non-standard EC Fan2 */ + fan_rpm_status_ns = 0x95, /* Special offset for Fan1 RPM status for non-standard EC */ + fan2_rpm_status_ns = 0x98, /* Special offset for Fan2 RPM status for non-standard EC */ + + TP_EC_FAN_FULLSPEED = 0x40, /* EC fan mode: full speed */ + TP_EC_FAN_AUTO = 0x80, /* EC fan mode: auto fan control */ + + TPACPI_FAN_LAST_LEVEL = 0x100, /* Use cached last-seen fan level */ +}; + +enum fan_status_access_mode { + TPACPI_FAN_NONE = 0, /* No fan status or control */ + TPACPI_FAN_RD_ACPI_GFAN, /* Use ACPI GFAN */ + TPACPI_FAN_RD_ACPI_FANG, /* Use ACPI FANG */ + TPACPI_FAN_RD_TPEC, /* Use ACPI EC regs 0x2f, 0x84-0x85 */ + TPACPI_FAN_RD_TPEC_NS, /* Use non-standard ACPI EC regs (eg: L13 Yoga gen2 etc.) */ +}; + +enum fan_control_access_mode { + TPACPI_FAN_WR_NONE = 0, /* No fan control */ + TPACPI_FAN_WR_ACPI_SFAN, /* Use ACPI SFAN */ + TPACPI_FAN_WR_ACPI_FANW, /* Use ACPI FANW */ + TPACPI_FAN_WR_TPEC, /* Use ACPI EC reg 0x2f */ + TPACPI_FAN_WR_ACPI_FANS, /* Use ACPI FANS and EC reg 0x2f */ +}; + +enum fan_control_commands { + TPACPI_FAN_CMD_SPEED = 0x0001, /* speed command */ + TPACPI_FAN_CMD_LEVEL = 0x0002, /* level command */ + TPACPI_FAN_CMD_ENABLE = 0x0004, /* enable/disable cmd, + * and also watchdog cmd */ +}; + +static bool fan_control_allowed; + +static enum fan_status_access_mode fan_status_access_mode; +static enum fan_control_access_mode fan_control_access_mode; +static enum fan_control_commands fan_control_commands; + +static u8 fan_control_initial_status; +static u8 fan_control_desired_level; +static u8 fan_control_resume_level; +static int fan_watchdog_maxinterval; + +static bool fan_with_ns_addr; +static bool ecfw_with_fan_dec_rpm; +static bool fan_speed_in_tpr; + +static struct mutex fan_mutex; + +static void fan_watchdog_fire(struct work_struct *ignored); +static DECLARE_DELAYED_WORK(fan_watchdog_task, fan_watchdog_fire); + +TPACPI_HANDLE(fans, ec, "FANS"); /* X31, X40, X41 */ +TPACPI_HANDLE(gfan, ec, "GFAN", /* 570 */ + "\\FSPD", /* 600e/x, 770e, 770x */ + ); /* all others */ +TPACPI_HANDLE(fang, ec, "FANG", /* E531 */ + ); /* all others */ +TPACPI_HANDLE(sfan, ec, "SFAN", /* 570 */ + "JFNS", /* 770x-JL */ + ); /* all others */ +TPACPI_HANDLE(fanw, ec, "FANW", /* E531 */ + ); /* all others */ + +/* + * Unitialized HFSP quirk: ACPI DSDT and EC fail to initialize the + * HFSP register at boot, so it contains 0x07 but the Thinkpad could + * be in auto mode (0x80). + * + * This is corrected by any write to HFSP either by the driver, or + * by the firmware. + * + * We assume 0x07 really means auto mode while this quirk is active, + * as this is far more likely than the ThinkPad being in level 7, + * which is only used by the firmware during thermal emergencies. + * + * Enable for TP-1Y (T43), TP-78 (R51e), TP-76 (R52), + * TP-70 (T43, R52), which are known to be buggy. + */ + +static void fan_quirk1_setup(void) +{ + if (fan_control_initial_status == 0x07) { + pr_notice("fan_init: initial fan status is unknown, assuming it is in auto mode\n"); + tp_features.fan_ctrl_status_undef = 1; + } +} + +static void fan_quirk1_handle(u8 *fan_status) +{ + if (unlikely(tp_features.fan_ctrl_status_undef)) { + if (*fan_status != fan_control_initial_status) { + /* something changed the HFSP regisnter since + * driver init time, so it is not undefined + * anymore */ + tp_features.fan_ctrl_status_undef = 0; + } else { + /* Return most likely status. In fact, it + * might be the only possible status */ + *fan_status = TP_EC_FAN_AUTO; + } + } +} + +/* Select main fan on X60/X61, NOOP on others */ +static bool fan_select_fan1(void) +{ + if (tp_features.second_fan) { + u8 val; + + if (ec_read(fan_select_offset, &val) < 0) + return false; + val &= 0xFEU; + if (ec_write(fan_select_offset, val) < 0) + return false; + } + return true; +} + +/* Select secondary fan on X60/X61 */ +static bool fan_select_fan2(void) +{ + u8 val; + + if (!tp_features.second_fan) + return false; + + if (ec_read(fan_select_offset, &val) < 0) + return false; + val |= 0x01U; + if (ec_write(fan_select_offset, val) < 0) + return false; + + return true; +} + +static void fan_update_desired_level(u8 status) +{ + lockdep_assert_held(&fan_mutex); + + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + if (status > 7) + fan_control_desired_level = 7; + else + fan_control_desired_level = status; + } +} + +static int fan_get_status(u8 *status) +{ + u8 s; + + /* TODO: + * Add TPACPI_FAN_RD_ACPI_FANS ? */ + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: { + /* 570, 600e/x, 770e, 770x */ + int res; + + if (unlikely(!acpi_evalf(gfan_handle, &res, NULL, "d"))) + return -EIO; + + if (likely(status)) + *status = res & 0x07; + + break; + } + case TPACPI_FAN_RD_ACPI_FANG: { + /* E531 */ + int mode, speed; + + if (unlikely(!acpi_evalf(fang_handle, &mode, NULL, "dd", 0x8100))) + return -EIO; + if (unlikely(!acpi_evalf(fang_handle, &speed, NULL, "dd", 0x8102))) + return -EIO; + + if (likely(status)) { + *status = speed * 7 / 100; + if (mode < 9) + *status |= TP_EC_FAN_AUTO; + } + + break; + } + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!acpi_ec_read(fan_status_offset, &s))) + return -EIO; + + if (likely(status)) { + *status = s; + fan_quirk1_handle(status); + } + + break; + case TPACPI_FAN_RD_TPEC_NS: + /* Default mode is AUTO which means controlled by EC */ + if (!acpi_ec_read(fan_status_offset_ns, &s)) + return -EIO; + + if (status) + *status = s; + + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_get_status_safe(u8 *status) +{ + int rc; + u8 s; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + rc = fan_get_status(&s); + /* NS EC doesn't have register with level settings */ + if (!rc && !fan_with_ns_addr) + fan_update_desired_level(s); + mutex_unlock(&fan_mutex); + + if (rc) + return rc; + if (status) + *status = s; + + return 0; +} + +static int fan_get_speed(unsigned int *speed) +{ + u8 hi, lo; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!fan_select_fan1())) + return -EIO; + if (unlikely(!acpi_ec_read(fan_rpm_offset, &lo) || + !acpi_ec_read(fan_rpm_offset + 1, &hi))) + return -EIO; + + if (likely(speed)) { + *speed = (hi << 8) | lo; + if (fan_speed_in_tpr && *speed != 0) + *speed = FAN_CLOCK_TPM / *speed; + } + break; + case TPACPI_FAN_RD_TPEC_NS: + if (!acpi_ec_read(fan_rpm_status_ns, &lo)) + return -EIO; + + if (speed) + *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan2_get_speed(unsigned int *speed) +{ + u8 hi, lo, status; + bool rc; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!fan_select_fan2())) + return -EIO; + rc = !acpi_ec_read(fan_rpm_offset, &lo) || + !acpi_ec_read(fan_rpm_offset + 1, &hi); + fan_select_fan1(); /* play it safe */ + if (rc) + return -EIO; + + if (likely(speed)) { + *speed = (hi << 8) | lo; + if (fan_speed_in_tpr && *speed != 0) + *speed = FAN_CLOCK_TPM / *speed; + } + break; + + case TPACPI_FAN_RD_TPEC_NS: + rc = !acpi_ec_read(fan2_status_offset_ns, &status); + if (rc) + return -EIO; + if (!(status & FAN_NS_CTRL_STATUS)) { + pr_info("secondary fan control not supported\n"); + return -EIO; + } + rc = !acpi_ec_read(fan2_rpm_status_ns, &lo); + if (rc) + return -EIO; + if (speed) + *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; + break; + case TPACPI_FAN_RD_ACPI_FANG: { + /* E531 */ + int speed_tmp; + + if (unlikely(!acpi_evalf(fang_handle, &speed_tmp, NULL, "dd", 0x8102))) + return -EIO; + + if (likely(speed)) + *speed = speed_tmp * 65535 / 100; + break; + } + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_set_level(int level) +{ + if (!fan_control_allowed) + return -EPERM; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + if ((level < 0) || (level > 7)) + return -EINVAL; + + if (tp_features.second_fan_ctl) { + if (!fan_select_fan2() || + !acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) { + pr_warn("Couldn't set 2nd fan level, disabling support\n"); + tp_features.second_fan_ctl = 0; + } + fan_select_fan1(); + } + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) + return -EIO; + break; + + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!(level & TP_EC_FAN_AUTO) && + !(level & TP_EC_FAN_FULLSPEED) && + ((level < 0) || (level > 7))) + return -EINVAL; + + /* safety net should the EC not support AUTO + * or FULLSPEED mode bits and just ignore them */ + if (level & TP_EC_FAN_FULLSPEED) + level |= 7; /* safety min speed 7 */ + else if (level & TP_EC_FAN_AUTO) + level |= 4; /* safety min speed 4 */ + + if (tp_features.second_fan_ctl) { + if (!fan_select_fan2() || + !acpi_ec_write(fan_status_offset, level)) { + pr_warn("Couldn't set 2nd fan level, disabling support\n"); + tp_features.second_fan_ctl = 0; + } + fan_select_fan1(); + + } + if (!acpi_ec_write(fan_status_offset, level)) + return -EIO; + else + tp_features.fan_ctrl_status_undef = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!(level & TP_EC_FAN_AUTO) && (level < 0 || level > 7)) + return -EINVAL; + if (level & TP_EC_FAN_FULLSPEED) + return -EINVAL; + + if (level & TP_EC_FAN_AUTO) { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { + return -EIO; + } + } else { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, level * 100 / 7)) { + return -EIO; + } + } + break; + + default: + return -ENXIO; + } + + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0x%02x\n", level); + return 0; +} + +static int fan_set_level_safe(int level) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + if (level == TPACPI_FAN_LAST_LEVEL) + level = fan_control_desired_level; + + rc = fan_set_level(level); + if (!rc) + fan_update_desired_level(level); + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_enable(void) +{ + u8 s = 0; + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + rc = fan_get_status(&s); + if (rc) + break; + + /* Don't go out of emergency fan mode */ + if (s != 7) { + s &= 0x07; + s |= TP_EC_FAN_AUTO | 4; /* min fan speed 4 */ + } + + if (!acpi_ec_write(fan_status_offset, s)) + rc = -EIO; + else { + tp_features.fan_ctrl_status_undef = 0; + rc = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + rc = fan_get_status(&s); + if (rc) + break; + + s &= 0x07; + + /* Set fan to at least level 4 */ + s |= 4; + + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", s)) + rc = -EIO; + else + rc = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { + rc = -EIO; + break; + } + + rc = 0; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + + if (!rc) + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0x%02x\n", + s); + return rc; +} + +static int fan_set_disable(void) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!acpi_ec_write(fan_status_offset, 0x00)) + rc = -EIO; + else { + fan_control_desired_level = 0; + tp_features.fan_ctrl_status_undef = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", 0x00)) + rc = -EIO; + else + fan_control_desired_level = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, 0x00)) { + rc = -EIO; + break; + } + rc = 0; + break; + + default: + rc = -ENXIO; + } + + if (!rc) + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0\n"); + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_speed(int speed) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + if (speed >= 0 && speed <= 65535) { + if (!acpi_evalf(fans_handle, NULL, NULL, "vddd", + speed, speed, speed)) + rc = -EIO; + } else + rc = -EINVAL; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (speed >= 0 && speed <= 65535) { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", + 0x8102, speed * 100 / 65535)) + rc = -EIO; + } else + rc = -EINVAL; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + return rc; +} + +static void fan_watchdog_reset(void) +{ + if (fan_control_access_mode == TPACPI_FAN_WR_NONE) + return; + + if (fan_watchdog_maxinterval > 0 && + tpacpi_lifecycle != TPACPI_LIFE_EXITING) + mod_delayed_work(tpacpi_wq, &fan_watchdog_task, + secs_to_jiffies(fan_watchdog_maxinterval)); + else + cancel_delayed_work(&fan_watchdog_task); +} + +static void fan_watchdog_fire(struct work_struct *ignored) +{ + int rc; + + if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) + return; + + pr_notice("fan watchdog: enabling fan\n"); + rc = fan_set_enable(); + if (rc < 0) { + pr_err("fan watchdog: error %d while enabling fan, will try again later...\n", + rc); + /* reschedule for later */ + fan_watchdog_reset(); + } +} + +/* + * SYSFS fan layout: hwmon compatible (device) + * + * pwm*_enable: + * 0: "disengaged" mode + * 1: manual mode + * 2: native EC "auto" mode (recommended, hardware default) + * + * pwm*: set speed in manual mode, ignored otherwise. + * 0 is level 0; 255 is level 7. Intermediate points done with linear + * interpolation. + * + * fan*_input: tachometer reading, RPM + * + * + * SYSFS fan layout: extensions + * + * fan_watchdog (driver): + * fan watchdog interval in seconds, 0 disables (default), max 120 + */ + +/* sysfs fan pwm1_enable ----------------------------------------------- */ +static ssize_t fan_pwm1_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, mode; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if (status & TP_EC_FAN_FULLSPEED) { + mode = 0; + } else if (status & TP_EC_FAN_AUTO) { + mode = 2; + } else + mode = 1; + + return sysfs_emit(buf, "%d\n", mode); +} + +static ssize_t fan_pwm1_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res, level; + + if (parse_strtoul(buf, 2, &t)) + return -EINVAL; + + tpacpi_disclose_usertask("hwmon pwm1_enable", + "set fan mode to %lu\n", t); + + switch (t) { + case 0: + level = TP_EC_FAN_FULLSPEED; + break; + case 1: + level = TPACPI_FAN_LAST_LEVEL; + break; + case 2: + level = TP_EC_FAN_AUTO; + break; + case 3: + /* reserved for software-controlled auto mode */ + return -ENOSYS; + default: + return -EINVAL; + } + + res = fan_set_level_safe(level); + if (res == -ENXIO) + return -EINVAL; + else if (res < 0) + return res; + + fan_watchdog_reset(); + + return count; +} + +static DEVICE_ATTR(pwm1_enable, S_IWUSR | S_IRUGO, + fan_pwm1_enable_show, fan_pwm1_enable_store); + +/* sysfs fan pwm1 ------------------------------------------------------ */ +static ssize_t fan_pwm1_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) != 0) + status = fan_control_desired_level; + + if (status > 7) + status = 7; + + return sysfs_emit(buf, "%u\n", (status * 255) / 7); +} + +static ssize_t fan_pwm1_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long s; + int rc; + u8 status, newlevel; + + if (parse_strtoul(buf, 255, &s)) + return -EINVAL; + + tpacpi_disclose_usertask("hwmon pwm1", + "set fan speed to %lu\n", s); + + /* scale down from 0-255 to 0-7 */ + newlevel = (s >> 5) & 0x07; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = fan_get_status(&status); + if (!rc && (status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + rc = fan_set_level(newlevel); + if (rc == -ENXIO) + rc = -EINVAL; + else if (!rc) { + fan_update_desired_level(newlevel); + fan_watchdog_reset(); + } + } + + mutex_unlock(&fan_mutex); + return (rc) ? rc : count; +} + +static DEVICE_ATTR(pwm1, S_IWUSR | S_IRUGO, fan_pwm1_show, fan_pwm1_store); + +/* sysfs fan fan1_input ------------------------------------------------ */ +static ssize_t fan_fan1_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + unsigned int speed; + + res = fan_get_speed(&speed); + if (res < 0) + return res; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + return sysfs_emit(buf, "%u\n", speed); + else + return sysfs_emit(buf, "%x\n", speed); +} + +static DEVICE_ATTR(fan1_input, S_IRUGO, fan_fan1_input_show, NULL); + +/* sysfs fan fan2_input ------------------------------------------------ */ +static ssize_t fan_fan2_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + unsigned int speed; + + res = fan2_get_speed(&speed); + if (res < 0) + return res; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + return sysfs_emit(buf, "%u\n", speed); + else + return sysfs_emit(buf, "%x\n", speed); +} + +static DEVICE_ATTR(fan2_input, S_IRUGO, fan_fan2_input_show, NULL); + +/* sysfs fan fan_watchdog (hwmon driver) ------------------------------- */ +static ssize_t fan_watchdog_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%u\n", fan_watchdog_maxinterval); +} + +static ssize_t fan_watchdog_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 120, &t)) + return -EINVAL; + + if (!fan_control_allowed) + return -EPERM; + + fan_watchdog_maxinterval = t; + fan_watchdog_reset(); + + tpacpi_disclose_usertask("fan_watchdog", "set to %lu\n", t); + + return count; +} +static DRIVER_ATTR_RW(fan_watchdog); + +/* --------------------------------------------------------------------- */ + +static struct attribute *fan_attributes[] = { + &dev_attr_pwm1_enable.attr, + &dev_attr_pwm1.attr, + &dev_attr_fan1_input.attr, + &dev_attr_fan2_input.attr, + NULL +}; + +static umode_t fan_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + if (fan_status_access_mode == TPACPI_FAN_NONE && + fan_control_access_mode == TPACPI_FAN_WR_NONE) + return 0; + + if (attr == &dev_attr_fan2_input.attr) { + if (!tp_features.second_fan) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group fan_attr_group = { + .is_visible = fan_attr_is_visible, + .attrs = fan_attributes, +}; + +static struct attribute *fan_driver_attributes[] = { + &driver_attr_fan_watchdog.attr, + NULL +}; + +static const struct attribute_group fan_driver_attr_group = { + .is_visible = fan_attr_is_visible, + .attrs = fan_driver_attributes, +}; + +#define TPACPI_FAN_Q1 0x0001 /* Uninitialized HFSP */ +#define TPACPI_FAN_2FAN 0x0002 /* EC 0x31 bit 0 selects fan2 */ +#define TPACPI_FAN_2CTL 0x0004 /* selects fan2 control */ +#define TPACPI_FAN_NOFAN 0x0008 /* no fan available */ +#define TPACPI_FAN_NS 0x0010 /* For EC with non-Standard register addresses */ +#define TPACPI_FAN_DECRPM 0x0020 /* For ECFW's with RPM in register as decimal */ +#define TPACPI_FAN_TPR 0x0040 /* Fan speed is in Ticks Per Revolution */ +#define TPACPI_FAN_NOACPI 0x0080 /* Don't use ACPI methods even if detected */ + +static const struct tpacpi_quirk fan_quirk_table[] __initconst = { + TPACPI_QEC_IBM('1', 'Y', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '8', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '6', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '0', TPACPI_FAN_Q1), + TPACPI_QEC_LNV('7', 'M', TPACPI_FAN_2FAN), + TPACPI_Q_LNV('N', '1', TPACPI_FAN_2FAN), + TPACPI_Q_LNV3('N', '1', 'D', TPACPI_FAN_2CTL), /* P70 */ + TPACPI_Q_LNV3('N', '1', 'E', TPACPI_FAN_2CTL), /* P50 */ + TPACPI_Q_LNV3('N', '1', 'T', TPACPI_FAN_2CTL), /* P71 */ + TPACPI_Q_LNV3('N', '1', 'U', TPACPI_FAN_2CTL), /* P51 */ + TPACPI_Q_LNV3('N', '2', 'C', TPACPI_FAN_2CTL), /* P52 / P72 */ + TPACPI_Q_LNV3('N', '2', 'N', TPACPI_FAN_2CTL), /* P53 / P73 */ + TPACPI_Q_LNV3('N', '2', 'E', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (1st gen) */ + TPACPI_Q_LNV3('N', '2', 'O', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (2nd gen) */ + TPACPI_Q_LNV3('N', '3', '0', TPACPI_FAN_2CTL), /* P15 (1st gen) / P15v (1st gen) */ + TPACPI_Q_LNV3('N', '3', '7', TPACPI_FAN_2CTL), /* T15g (2nd gen) */ + TPACPI_Q_LNV3('R', '1', 'F', TPACPI_FAN_NS), /* L13 Yoga Gen 2 */ + TPACPI_Q_LNV3('N', '2', 'U', TPACPI_FAN_NS), /* X13 Yoga Gen 2*/ + TPACPI_Q_LNV3('R', '0', 'R', TPACPI_FAN_NS), /* L380 */ + TPACPI_Q_LNV3('R', '1', '5', TPACPI_FAN_NS), /* L13 Yoga Gen 1 */ + TPACPI_Q_LNV3('R', '1', '0', TPACPI_FAN_NS), /* L390 */ + TPACPI_Q_LNV3('N', '2', 'L', TPACPI_FAN_NS), /* X13 Yoga Gen 1 */ + TPACPI_Q_LNV3('R', '0', 'T', TPACPI_FAN_NS), /* 11e Gen5 GL */ + TPACPI_Q_LNV3('R', '1', 'D', TPACPI_FAN_NS), /* 11e Gen5 GL-R */ + TPACPI_Q_LNV3('R', '0', 'V', TPACPI_FAN_NS), /* 11e Gen5 KL-Y */ + TPACPI_Q_LNV3('N', '1', 'O', TPACPI_FAN_NOFAN), /* X1 Tablet (2nd gen) */ + TPACPI_Q_LNV3('R', '0', 'Q', TPACPI_FAN_DECRPM),/* L480 */ + TPACPI_Q_LNV('8', 'F', TPACPI_FAN_TPR), /* ThinkPad x120e */ + TPACPI_Q_LNV3('R', '0', '0', TPACPI_FAN_NOACPI),/* E560 */ + TPACPI_Q_LNV3('R', '1', '2', TPACPI_FAN_NOACPI),/* T495 */ + TPACPI_Q_LNV3('R', '1', '3', TPACPI_FAN_NOACPI),/* T495s */ +}; + +static int __init fan_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "initializing fan subdriver\n"); + + mutex_init(&fan_mutex); + fan_status_access_mode = TPACPI_FAN_NONE; + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + fan_watchdog_maxinterval = 0; + tp_features.fan_ctrl_status_undef = 0; + tp_features.second_fan = 0; + tp_features.second_fan_ctl = 0; + fan_control_desired_level = 7; + + if (tpacpi_is_ibm()) { + TPACPI_ACPIHANDLE_INIT(fans); + TPACPI_ACPIHANDLE_INIT(gfan); + TPACPI_ACPIHANDLE_INIT(sfan); + } + if (tpacpi_is_lenovo()) { + TPACPI_ACPIHANDLE_INIT(fang); + TPACPI_ACPIHANDLE_INIT(fanw); + } + + quirks = tpacpi_check_quirks(fan_quirk_table, + ARRAY_SIZE(fan_quirk_table)); + + if (quirks & TPACPI_FAN_NOFAN) { + pr_info("No integrated ThinkPad fan available\n"); + return -ENODEV; + } + + if (quirks & TPACPI_FAN_NS) { + pr_info("ECFW with non-standard fan reg control found\n"); + fan_with_ns_addr = 1; + /* Fan ctrl support from host is undefined for now */ + tp_features.fan_ctrl_status_undef = 1; + } + + /* Check for the EC/BIOS with RPM reported in decimal*/ + if (quirks & TPACPI_FAN_DECRPM) { + pr_info("ECFW with fan RPM as decimal in EC register\n"); + ecfw_with_fan_dec_rpm = 1; + tp_features.fan_ctrl_status_undef = 1; + } + + if (quirks & TPACPI_FAN_NOACPI) { + /* E560, T495, T495s */ + pr_info("Ignoring buggy ACPI fan access method\n"); + fang_handle = NULL; + fanw_handle = NULL; + } + + if (gfan_handle) { + /* 570, 600e/x, 770e, 770x */ + fan_status_access_mode = TPACPI_FAN_RD_ACPI_GFAN; + } else if (fang_handle) { + /* E531 */ + fan_status_access_mode = TPACPI_FAN_RD_ACPI_FANG; + } else { + /* all other ThinkPads: note that even old-style + * ThinkPad ECs supports the fan control register */ + if (fan_with_ns_addr || + likely(acpi_ec_read(fan_status_offset, &fan_control_initial_status))) { + int res; + unsigned int speed; + + fan_status_access_mode = fan_with_ns_addr ? + TPACPI_FAN_RD_TPEC_NS : TPACPI_FAN_RD_TPEC; + + if (quirks & TPACPI_FAN_Q1) + fan_quirk1_setup(); + if (quirks & TPACPI_FAN_TPR) + fan_speed_in_tpr = true; + /* Try and probe the 2nd fan */ + tp_features.second_fan = 1; /* needed for get_speed to work */ + res = fan2_get_speed(&speed); + if (res >= 0 && speed != FAN_NOT_PRESENT) { + /* It responded - so let's assume it's there */ + tp_features.second_fan = 1; + /* fan control not currently available for ns ECFW */ + tp_features.second_fan_ctl = !fan_with_ns_addr; + pr_info("secondary fan control detected & enabled\n"); + } else { + /* Fan not auto-detected */ + tp_features.second_fan = 0; + if (quirks & TPACPI_FAN_2FAN) { + tp_features.second_fan = 1; + pr_info("secondary fan support enabled\n"); + } + if (quirks & TPACPI_FAN_2CTL) { + tp_features.second_fan = 1; + tp_features.second_fan_ctl = 1; + pr_info("secondary fan control enabled\n"); + } + } + } else { + pr_err("ThinkPad ACPI EC access misbehaving, fan status and control unavailable\n"); + return -ENODEV; + } + } + + if (sfan_handle) { + /* 570, 770x-JL */ + fan_control_access_mode = TPACPI_FAN_WR_ACPI_SFAN; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_ENABLE; + } else if (fanw_handle) { + /* E531 */ + fan_control_access_mode = TPACPI_FAN_WR_ACPI_FANW; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_SPEED | TPACPI_FAN_CMD_ENABLE; + } else { + if (!gfan_handle) { + /* gfan without sfan means no fan control */ + /* all other models implement TP EC 0x2f control */ + + if (fans_handle) { + /* X31, X40, X41 */ + fan_control_access_mode = + TPACPI_FAN_WR_ACPI_FANS; + fan_control_commands |= + TPACPI_FAN_CMD_SPEED | + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } else { + fan_control_access_mode = TPACPI_FAN_WR_TPEC; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } + } + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "fan is %s, modes %d, %d\n", + str_supported(fan_status_access_mode != TPACPI_FAN_NONE || + fan_control_access_mode != TPACPI_FAN_WR_NONE), + fan_status_access_mode, fan_control_access_mode); + + /* fan control master switch */ + if (!fan_control_allowed) { + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "fan control features disabled by parameter\n"); + } + + /* update fan_control_desired_level */ + if (fan_status_access_mode != TPACPI_FAN_NONE) + fan_get_status_safe(NULL); + + if (fan_status_access_mode == TPACPI_FAN_NONE && + fan_control_access_mode == TPACPI_FAN_WR_NONE) + return -ENODEV; + + return 0; +} + +static void fan_exit(void) +{ + vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_FAN, + "cancelling any pending fan watchdog tasks\n"); + + cancel_delayed_work(&fan_watchdog_task); + flush_workqueue(tpacpi_wq); +} + +static void fan_suspend(void) +{ + int rc; + + if (!fan_control_allowed) + return; + + /* Store fan status in cache */ + fan_control_resume_level = 0; + rc = fan_get_status_safe(&fan_control_resume_level); + if (rc) + pr_notice("failed to read fan level for later restore during resume: %d\n", + rc); + + /* if it is undefined, don't attempt to restore it. + * KEEP THIS LAST */ + if (tp_features.fan_ctrl_status_undef) + fan_control_resume_level = 0; +} + +static void fan_resume(void) +{ + u8 current_level = 7; + bool do_set = false; + int rc; + + /* DSDT *always* updates status on resume */ + tp_features.fan_ctrl_status_undef = 0; + + if (!fan_control_allowed || + !fan_control_resume_level || + fan_get_status_safe(¤t_level)) + return; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + /* never decrease fan level */ + do_set = (fan_control_resume_level > current_level); + break; + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + /* never decrease fan level, scale is: + * TP_EC_FAN_FULLSPEED > 7 >= TP_EC_FAN_AUTO + * + * We expect the firmware to set either 7 or AUTO, but we + * handle FULLSPEED out of paranoia. + * + * So, we can safely only restore FULLSPEED or 7, anything + * else could slow the fan. Restoring AUTO is useless, at + * best that's exactly what the DSDT already set (it is the + * slower it uses). + * + * Always keep in mind that the DSDT *will* have set the + * fans to what the vendor supposes is the best level. We + * muck with it only to speed the fan up. + */ + if (fan_control_resume_level != 7 && + !(fan_control_resume_level & TP_EC_FAN_FULLSPEED)) + return; + else + do_set = !(current_level & TP_EC_FAN_FULLSPEED) && + (current_level != fan_control_resume_level); + break; + default: + return; + } + if (do_set) { + pr_notice("restoring fan level to 0x%02x\n", + fan_control_resume_level); + rc = fan_set_level_safe(fan_control_resume_level); + if (rc < 0) + pr_notice("failed to restore fan level: %d\n", rc); + } +} + +static int fan_read(struct seq_file *m) +{ + int rc; + u8 status; + unsigned int speed = 0; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: + /* 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc) + return rc; + + seq_printf(m, "status:\t\t%s\n" + "level:\t\t%d\n", + str_enabled_disabled(status), status); + break; + + case TPACPI_FAN_RD_TPEC_NS: + case TPACPI_FAN_RD_TPEC: + case TPACPI_FAN_RD_ACPI_FANG: + /* all except 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc) + return rc; + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status)); + + rc = fan_get_speed(&speed); + if (rc < 0) + return rc; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + seq_printf(m, "speed:\t\t%d\n", speed); + else + seq_printf(m, "speed:\t\t%x\n", speed); + + if (fan_status_access_mode == TPACPI_FAN_RD_TPEC_NS) { + /* + * No full speed bit in NS EC + * EC Auto mode is set by default. + * No other levels settings available + */ + seq_printf(m, "level:\t\t%s\n", status & FAN_NS_CTRL ? "unknown" : "auto"); + } else if (fan_status_access_mode == TPACPI_FAN_RD_TPEC) { + if (status & TP_EC_FAN_FULLSPEED) + /* Disengaged mode takes precedence */ + seq_puts(m, "level:\t\tdisengaged\n"); + else if (status & TP_EC_FAN_AUTO) + seq_puts(m, "level:\t\tauto\n"); + else + seq_printf(m, "level:\t\t%d\n", status); + } + break; + + case TPACPI_FAN_NONE: + default: + seq_puts(m, "status:\t\tnot supported\n"); + } + + if (fan_control_commands & TPACPI_FAN_CMD_LEVEL) { + seq_puts(m, "commands:\tlevel "); + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + seq_puts(m, " ( is 0-7)\n"); + break; + + default: + seq_puts(m, " ( is 0-7, auto, disengaged, full-speed)\n"); + break; + } + } + + if (fan_control_commands & TPACPI_FAN_CMD_ENABLE) + seq_printf(m, "commands:\tenable, disable\n" + "commands:\twatchdog ( is 0 (off), 1-120 (seconds))\n"); + + if (fan_control_commands & TPACPI_FAN_CMD_SPEED) + seq_puts(m, "commands:\tspeed ( is 0-65535)\n"); + + return 0; +} + +static int fan_write_cmd_level(const char *cmd, int *rc) +{ + int level; + + if (strstarts(cmd, "level auto")) + level = TP_EC_FAN_AUTO; + else if (strstarts(cmd, "level disengaged") || strstarts(cmd, "level full-speed")) + level = TP_EC_FAN_FULLSPEED; + else if (sscanf(cmd, "level %d", &level) != 1) + return 0; + + *rc = fan_set_level_safe(level); + if (*rc == -ENXIO) + pr_err("level command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", + "set level to %d\n", level); + + return 1; +} + +static int fan_write_cmd_enable(const char *cmd, int *rc) +{ + if (!strstarts(cmd, "enable")) + return 0; + + *rc = fan_set_enable(); + if (*rc == -ENXIO) + pr_err("enable command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", "enable\n"); + + return 1; +} + +static int fan_write_cmd_disable(const char *cmd, int *rc) +{ + if (!strstarts(cmd, "disable")) + return 0; + + *rc = fan_set_disable(); + if (*rc == -ENXIO) + pr_err("disable command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", "disable\n"); + + return 1; +} + +static int fan_write_cmd_speed(const char *cmd, int *rc) +{ + int speed; + + if (sscanf(cmd, "speed %d", &speed) != 1) + return 0; + + *rc = fan_set_speed(speed); + if (*rc == -ENXIO) + pr_err("speed command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", + "set speed to %d\n", speed); + + return 1; +} + +static int fan_write_cmd_watchdog(const char *cmd, int *rc) +{ + int interval; + + if (sscanf(cmd, "watchdog %d", &interval) != 1) + return 0; + + if (interval < 0 || interval > 120) + *rc = -EINVAL; + else { + fan_watchdog_maxinterval = interval; + tpacpi_disclose_usertask("procfs fan", + "set watchdog timer to %d\n", + interval); + } + + return 1; +} + +static int fan_write(char *buf) +{ + char *cmd; + int rc = 0; + + while (!rc && (cmd = strsep(&buf, ","))) { + if (!((fan_control_commands & TPACPI_FAN_CMD_LEVEL) && + fan_write_cmd_level(cmd, &rc)) && + !((fan_control_commands & TPACPI_FAN_CMD_ENABLE) && + (fan_write_cmd_enable(cmd, &rc) || + fan_write_cmd_disable(cmd, &rc) || + fan_write_cmd_watchdog(cmd, &rc))) && + !((fan_control_commands & TPACPI_FAN_CMD_SPEED) && + fan_write_cmd_speed(cmd, &rc)) + ) + rc = -EINVAL; + else if (!rc) + fan_watchdog_reset(); + } + + return rc; +} + +static struct ibm_struct fan_driver_data = { + .name = "fan", + .read = fan_read, + .write = fan_write, + .exit = fan_exit, + .suspend = fan_suspend, + .resume = fan_resume, +}; + +/************************************************************************* + * Mute LED subdriver + */ + +#define TPACPI_LED_MAX 2 + +struct tp_led_table { + acpi_string name; + int on_value; + int off_value; + int state; +}; + +static struct tp_led_table led_tables[TPACPI_LED_MAX] = { + [LED_AUDIO_MUTE] = { + .name = "SSMS", + .on_value = 1, + .off_value = 0, + }, + [LED_AUDIO_MICMUTE] = { + .name = "MMTS", + .on_value = 2, + .off_value = 0, + }, +}; + +static int mute_led_on_off(struct tp_led_table *t, bool state) +{ + acpi_handle temp; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { + pr_warn("Thinkpad ACPI has no %s interface.\n", t->name); + return -EIO; + } + + if (!acpi_evalf(hkey_handle, &output, t->name, "dd", + state ? t->on_value : t->off_value)) + return -EIO; + + t->state = state; + return state; +} + +static int tpacpi_led_set(int whichled, bool on) +{ + struct tp_led_table *t; + + t = &led_tables[whichled]; + if (t->state < 0 || t->state == on) + return t->state; + return mute_led_on_off(t, on); +} + +static int tpacpi_led_mute_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return tpacpi_led_set(LED_AUDIO_MUTE, brightness != LED_OFF); +} + +static int tpacpi_led_micmute_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return tpacpi_led_set(LED_AUDIO_MICMUTE, brightness != LED_OFF); +} + +static struct led_classdev mute_led_cdev[TPACPI_LED_MAX] = { + [LED_AUDIO_MUTE] = { + .name = "platform::mute", + .max_brightness = 1, + .brightness_set_blocking = tpacpi_led_mute_set, + .default_trigger = "audio-mute", + }, + [LED_AUDIO_MICMUTE] = { + .name = "platform::micmute", + .max_brightness = 1, + .brightness_set_blocking = tpacpi_led_micmute_set, + .default_trigger = "audio-micmute", + }, +}; + +static int mute_led_init(struct ibm_init_struct *iibm) +{ + acpi_handle temp; + int i, err; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + struct tp_led_table *t = &led_tables[i]; + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { + t->state = -ENODEV; + continue; + } + + err = led_classdev_register(&tpacpi_pdev->dev, &mute_led_cdev[i]); + if (err < 0) { + while (i--) + led_classdev_unregister(&mute_led_cdev[i]); + return err; + } + } + return 0; +} + +static void mute_led_exit(void) +{ + int i; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + led_classdev_unregister(&mute_led_cdev[i]); + tpacpi_led_set(i, false); + } +} + +static void mute_led_resume(void) +{ + int i; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + struct tp_led_table *t = &led_tables[i]; + if (t->state >= 0) + mute_led_on_off(t, t->state); + } +} + +static struct ibm_struct mute_led_driver_data = { + .name = "mute_led", + .exit = mute_led_exit, + .resume = mute_led_resume, +}; + +/* + * Battery Wear Control Driver + * Contact: Ognjen Galic + */ + +/* Metadata */ + +#define GET_START "BCTG" +#define SET_START "BCCS" +#define GET_STOP "BCSG" +#define SET_STOP "BCSS" +#define GET_DISCHARGE "BDSG" +#define SET_DISCHARGE "BDSS" +#define GET_INHIBIT "BICG" +#define SET_INHIBIT "BICS" + +enum { + BAT_ANY = 0, + BAT_PRIMARY = 1, + BAT_SECONDARY = 2 +}; + +enum { + /* Error condition bit */ + METHOD_ERR = BIT(31), +}; + +enum { + /* This is used in the get/set helpers */ + THRESHOLD_START, + THRESHOLD_STOP, + FORCE_DISCHARGE, + INHIBIT_CHARGE, +}; + +struct tpacpi_battery_data { + int charge_start; + int start_support; + int charge_stop; + int stop_support; + unsigned int charge_behaviours; +}; + +struct tpacpi_battery_driver_data { + struct tpacpi_battery_data batteries[3]; + int individual_addressing; +}; + +static struct tpacpi_battery_driver_data battery_info; + +/* ACPI helpers/functions/probes */ + +/* + * This evaluates a ACPI method call specific to the battery + * ACPI extension. The specifics are that an error is marked + * in the 32rd bit of the response, so we just check that here. + */ +static acpi_status tpacpi_battery_acpi_eval(char *method, int *ret, int param) +{ + int response; + + if (!acpi_evalf(hkey_handle, &response, method, "dd", param)) { + acpi_handle_err(hkey_handle, "%s: evaluate failed", method); + return AE_ERROR; + } + if (response & METHOD_ERR) { + acpi_handle_err(hkey_handle, + "%s evaluated but flagged as error", method); + return AE_ERROR; + } + *ret = response; + return AE_OK; +} + +static int tpacpi_battery_get(int what, int battery, int *ret) +{ + switch (what) { + case THRESHOLD_START: + if (!battery_info.batteries[battery].start_support || + ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, ret, battery))) + return -ENODEV; + + /* The value is in the low 8 bits of the response */ + *ret = *ret & 0xFF; + return 0; + case THRESHOLD_STOP: + if (!battery_info.batteries[battery].stop_support || + ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, ret, battery))) + return -ENODEV; + /* Value is in lower 8 bits */ + *ret = *ret & 0xFF; + /* + * On the stop value, if we return 0 that + * does not make any sense. 0 means Default, which + * means that charging stops at 100%, so we return + * that. + */ + if (*ret == 0) + *ret = 100; + return 0; + case FORCE_DISCHARGE: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, ret, battery)) + return -ENODEV; + /* The force discharge status is in bit 0 */ + *ret = *ret & 0x01; + return 0; + case INHIBIT_CHARGE: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, ret, battery)) + return -ENODEV; + /* The inhibit charge status is in bit 0 */ + *ret = *ret & 0x01; + return 0; + default: + pr_crit("wrong parameter: %d", what); + return -EINVAL; + } +} + +static int tpacpi_battery_set(int what, int battery, int value) +{ + int param, ret; + /* The first 8 bits are the value of the threshold */ + param = value; + /* The battery ID is in bits 8-9, 2 bits */ + param |= battery << 8; + + switch (what) { + case THRESHOLD_START: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_START, &ret, param)) { + pr_err("failed to set charge threshold on battery %d", + battery); + return -ENODEV; + } + return 0; + case THRESHOLD_STOP: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_STOP, &ret, param)) { + pr_err("failed to set stop threshold: %d", battery); + return -ENODEV; + } + return 0; + case FORCE_DISCHARGE: + /* Force discharge is in bit 0, + * break on AC attach is in bit 1 (won't work on some ThinkPads), + * battery ID is in bits 8-9, 2 bits. + */ + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_DISCHARGE, &ret, param))) { + pr_err("failed to set force discharge on %d", battery); + return -ENODEV; + } + return 0; + case INHIBIT_CHARGE: + /* When setting inhibit charge, we set a default value of + * always breaking on AC detach and the effective time is set to + * be permanent. + * The battery ID is in bits 4-5, 2 bits, + * the effective time is in bits 8-23, 2 bytes. + * A time of FFFF indicates forever. + */ + param = value; + param |= battery << 4; + param |= 0xFFFF << 8; + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_INHIBIT, &ret, param))) { + pr_err("failed to set inhibit charge on %d", battery); + return -ENODEV; + } + return 0; + default: + pr_crit("wrong parameter: %d", what); + return -EINVAL; + } +} + +static int tpacpi_battery_set_validate(int what, int battery, int value) +{ + int ret, v; + + ret = tpacpi_battery_set(what, battery, value); + if (ret < 0) + return ret; + + ret = tpacpi_battery_get(what, battery, &v); + if (ret < 0) + return ret; + + if (v == value) + return 0; + + msleep(500); + + ret = tpacpi_battery_get(what, battery, &v); + if (ret < 0) + return ret; + + if (v == value) + return 0; + + return -EIO; +} + +static int tpacpi_battery_probe(int battery) +{ + int ret = 0; + + memset(&battery_info.batteries[battery], 0, + sizeof(battery_info.batteries[battery])); + + /* + * 1) Get the current start threshold + * 2) Check for support + * 3) Get the current stop threshold + * 4) Check for support + * 5) Get the current force discharge status + * 6) Check for support + * 7) Get the current inhibit charge status + * 8) Check for support + */ + if (acpi_has_method(hkey_handle, GET_START)) { + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, &ret, battery)) { + pr_err("Error probing battery %d\n", battery); + return -ENODEV; + } + /* Individual addressing is in bit 9 */ + if (ret & BIT(9)) + battery_info.individual_addressing = true; + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].start_support = 1; + else + return -ENODEV; + if (tpacpi_battery_get(THRESHOLD_START, battery, + &battery_info.batteries[battery].charge_start)) { + pr_err("Error probing battery %d\n", battery); + return -ENODEV; + } + } + if (acpi_has_method(hkey_handle, GET_STOP)) { + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, &ret, battery)) { + pr_err("Error probing battery stop; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].stop_support = 1; + else + return -ENODEV; + if (tpacpi_battery_get(THRESHOLD_STOP, battery, + &battery_info.batteries[battery].charge_stop)) { + pr_err("Error probing battery stop: %d\n", battery); + return -ENODEV; + } + } + if (acpi_has_method(hkey_handle, GET_DISCHARGE)) { + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, &ret, battery))) { + pr_err("Error probing battery discharge; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE); + } + if (acpi_has_method(hkey_handle, GET_INHIBIT)) { + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, &ret, battery))) { + pr_err("Error probing battery inhibit charge; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 5 */ + if (ret & BIT(5)) + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE); + } + + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO); + + pr_info("battery %d registered (start %d, stop %d, behaviours: 0x%x)\n", + battery, + battery_info.batteries[battery].charge_start, + battery_info.batteries[battery].charge_stop, + battery_info.batteries[battery].charge_behaviours); + + return 0; +} + +/* General helper functions */ + +static int tpacpi_battery_get_id(const char *battery_name) +{ + + if (strcmp(battery_name, "BAT0") == 0 || + tp_features.battery_force_primary) + return BAT_PRIMARY; + if (strcmp(battery_name, "BAT1") == 0) + return BAT_SECONDARY; + /* + * If for some reason the battery is not BAT0 nor is it + * BAT1, we will assume it's the default, first battery, + * AKA primary. + */ + pr_warn("unknown battery %s, assuming primary", battery_name); + return BAT_PRIMARY; +} + +/* sysfs interface */ + +static ssize_t tpacpi_battery_store(int what, + struct device *dev, + const char *buf, size_t count) +{ + struct power_supply *supply = to_power_supply(dev); + unsigned long value; + int battery, rval; + /* + * Some systems have support for more than + * one battery. If that is the case, + * tpacpi_battery_probe marked that addressing + * them individually is supported, so we do that + * based on the device struct. + * + * On systems that are not supported, we assume + * the primary as most of the ACPI calls fail + * with "Any Battery" as the parameter. + */ + if (battery_info.individual_addressing) + /* BAT_PRIMARY or BAT_SECONDARY */ + battery = tpacpi_battery_get_id(supply->desc->name); + else + battery = BAT_PRIMARY; + + rval = kstrtoul(buf, 10, &value); + if (rval) + return rval; + + switch (what) { + case THRESHOLD_START: + if (!battery_info.batteries[battery].start_support) + return -ENODEV; + /* valid values are [0, 99] */ + if (value > 99) + return -EINVAL; + if (value > battery_info.batteries[battery].charge_stop) + return -EINVAL; + if (tpacpi_battery_set(THRESHOLD_START, battery, value)) + return -ENODEV; + battery_info.batteries[battery].charge_start = value; + return count; + + case THRESHOLD_STOP: + if (!battery_info.batteries[battery].stop_support) + return -ENODEV; + /* valid values are [1, 100] */ + if (value < 1 || value > 100) + return -EINVAL; + if (value < battery_info.batteries[battery].charge_start) + return -EINVAL; + battery_info.batteries[battery].charge_stop = value; + /* + * When 100 is passed to stop, we need to flip + * it to 0 as that the EC understands that as + * "Default", which will charge to 100% + */ + if (value == 100) + value = 0; + if (tpacpi_battery_set(THRESHOLD_STOP, battery, value)) + return -EINVAL; + return count; + default: + pr_crit("Wrong parameter: %d", what); + return -EINVAL; + } + return count; +} + +static ssize_t tpacpi_battery_show(int what, + struct device *dev, + char *buf) +{ + struct power_supply *supply = to_power_supply(dev); + int ret, battery; + /* + * Some systems have support for more than + * one battery. If that is the case, + * tpacpi_battery_probe marked that addressing + * them individually is supported, so we; + * based on the device struct. + * + * On systems that are not supported, we assume + * the primary as most of the ACPI calls fail + * with "Any Battery" as the parameter. + */ + if (battery_info.individual_addressing) + /* BAT_PRIMARY or BAT_SECONDARY */ + battery = tpacpi_battery_get_id(supply->desc->name); + else + battery = BAT_PRIMARY; + if (tpacpi_battery_get(what, battery, &ret)) + return -ENODEV; + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t charge_control_start_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_battery_show(THRESHOLD_START, device, buf); +} + +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_battery_show(THRESHOLD_STOP, device, buf); +} + +static ssize_t charge_behaviour_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + enum power_supply_charge_behaviour active = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + struct power_supply *supply = to_power_supply(dev); + unsigned int available; + int ret, battery; + + battery = tpacpi_battery_get_id(supply->desc->name); + available = battery_info.batteries[battery].charge_behaviours; + + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) { + if (tpacpi_battery_get(FORCE_DISCHARGE, battery, &ret)) + return -ENODEV; + if (ret) { + active = POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE; + goto out; + } + } + + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) { + if (tpacpi_battery_get(INHIBIT_CHARGE, battery, &ret)) + return -ENODEV; + if (ret) { + active = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + goto out; + } + } + +out: + return power_supply_charge_behaviour_show(dev, available, active, buf); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_battery_store(THRESHOLD_START, dev, buf, count); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_battery_store(THRESHOLD_STOP, dev, buf, count); +} + +static ssize_t charge_behaviour_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct power_supply *supply = to_power_supply(dev); + int selected, battery, ret = 0; + unsigned int available; + + battery = tpacpi_battery_get_id(supply->desc->name); + available = battery_info.batteries[battery].charge_behaviours; + selected = power_supply_charge_behaviour_parse(available, buf); + + if (selected < 0) + return selected; + + switch (selected) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) + ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) + ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0)); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) + ret = tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0); + ret = min(ret, tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 1)); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) + ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); + ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 1)); + if (ret < 0) + return ret; + break; + default: + dev_err(dev, "Unexpected charge behaviour: %d\n", selected); + return -EINVAL; + } + + return count; +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); +static DEVICE_ATTR_RW(charge_control_end_threshold); +static DEVICE_ATTR_RW(charge_behaviour); +static struct device_attribute dev_attr_charge_start_threshold = __ATTR( + charge_start_threshold, + 0644, + charge_control_start_threshold_show, + charge_control_start_threshold_store +); +static struct device_attribute dev_attr_charge_stop_threshold = __ATTR( + charge_stop_threshold, + 0644, + charge_control_end_threshold_show, + charge_control_end_threshold_store +); + +static struct attribute *tpacpi_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + &dev_attr_charge_start_threshold.attr, + &dev_attr_charge_stop_threshold.attr, + &dev_attr_charge_behaviour.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(tpacpi_battery); + +/* ACPI battery hooking */ + +static int tpacpi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + int batteryid = tpacpi_battery_get_id(battery->desc->name); + + if (tpacpi_battery_probe(batteryid)) + return -ENODEV; + if (device_add_groups(&battery->dev, tpacpi_battery_groups)) + return -ENODEV; + return 0; +} + +static int tpacpi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_groups(&battery->dev, tpacpi_battery_groups); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = tpacpi_battery_add, + .remove_battery = tpacpi_battery_remove, + .name = "ThinkPad Battery Extension", +}; + +/* Subdriver init/exit */ + +static const struct tpacpi_quirk battery_quirk_table[] __initconst = { + /* + * Individual addressing is broken on models that expose the + * primary battery as BAT1. + */ + TPACPI_Q_LNV('G', '8', true), /* ThinkPad X131e */ + TPACPI_Q_LNV('8', 'F', true), /* Thinkpad X120e */ + TPACPI_Q_LNV('J', '7', true), /* B5400 */ + TPACPI_Q_LNV('J', 'I', true), /* Thinkpad 11e */ + TPACPI_Q_LNV3('R', '0', 'B', true), /* Thinkpad 11e gen 3 */ + TPACPI_Q_LNV3('R', '0', 'C', true), /* Thinkpad 13 */ + TPACPI_Q_LNV3('R', '0', 'J', true), /* Thinkpad 13 gen 2 */ + TPACPI_Q_LNV3('R', '0', 'K', true), /* Thinkpad 11e gen 4 celeron BIOS */ +}; + +static int __init tpacpi_battery_init(struct ibm_init_struct *ibm) +{ + memset(&battery_info, 0, sizeof(battery_info)); + + tp_features.battery_force_primary = tpacpi_check_quirks( + battery_quirk_table, + ARRAY_SIZE(battery_quirk_table)); + + battery_hook_register(&battery_hook); + return 0; +} + +static void tpacpi_battery_exit(void) +{ + battery_hook_unregister(&battery_hook); +} + +static struct ibm_struct battery_driver_data = { + .name = "battery", + .exit = tpacpi_battery_exit, +}; + +/************************************************************************* + * LCD Shadow subdriver, for the Lenovo PrivacyGuard feature + */ + +static struct drm_privacy_screen *lcdshadow_dev; +static acpi_handle lcdshadow_get_handle; +static acpi_handle lcdshadow_set_handle; + +static int lcdshadow_set_sw_state(struct drm_privacy_screen *priv, + enum drm_privacy_screen_status state) +{ + int output; + + if (WARN_ON(!mutex_is_locked(&priv->lock))) + return -EIO; + + if (!acpi_evalf(lcdshadow_set_handle, &output, NULL, "dd", (int)state)) + return -EIO; + + priv->hw_state = priv->sw_state = state; + return 0; +} + +static void lcdshadow_get_hw_state(struct drm_privacy_screen *priv) +{ + int output; + + if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) + return; + + priv->hw_state = priv->sw_state = output & 0x1; +} + +static const struct drm_privacy_screen_ops lcdshadow_ops = { + .set_sw_state = lcdshadow_set_sw_state, + .get_hw_state = lcdshadow_get_hw_state, +}; + +static int tpacpi_lcdshadow_init(struct ibm_init_struct *iibm) +{ + acpi_status status1, status2; + int output; + + status1 = acpi_get_handle(hkey_handle, "GSSS", &lcdshadow_get_handle); + status2 = acpi_get_handle(hkey_handle, "SSSS", &lcdshadow_set_handle); + if (ACPI_FAILURE(status1) || ACPI_FAILURE(status2)) + return 0; + + if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) + return -EIO; + + if (!(output & 0x10000)) + return 0; + + lcdshadow_dev = drm_privacy_screen_register(&tpacpi_pdev->dev, + &lcdshadow_ops, NULL); + if (IS_ERR(lcdshadow_dev)) + return PTR_ERR(lcdshadow_dev); + + return 0; +} + +static void lcdshadow_exit(void) +{ + drm_privacy_screen_unregister(lcdshadow_dev); +} + +static void lcdshadow_resume(void) +{ + if (!lcdshadow_dev) + return; + + mutex_lock(&lcdshadow_dev->lock); + lcdshadow_set_sw_state(lcdshadow_dev, lcdshadow_dev->sw_state); + mutex_unlock(&lcdshadow_dev->lock); +} + +static int lcdshadow_read(struct seq_file *m) +{ + if (!lcdshadow_dev) { + seq_puts(m, "status:\t\tnot supported\n"); + } else { + seq_printf(m, "status:\t\t%d\n", lcdshadow_dev->hw_state); + seq_puts(m, "commands:\t0, 1\n"); + } + + return 0; +} + +static int lcdshadow_write(char *buf) +{ + char *cmd; + int res, state = -EINVAL; + + if (!lcdshadow_dev) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + res = kstrtoint(cmd, 10, &state); + if (res < 0) + return res; + } + + if (state >= 2 || state < 0) + return -EINVAL; + + mutex_lock(&lcdshadow_dev->lock); + res = lcdshadow_set_sw_state(lcdshadow_dev, state); + mutex_unlock(&lcdshadow_dev->lock); + + drm_privacy_screen_call_notifier_chain(lcdshadow_dev); + + return res; +} + +static struct ibm_struct lcdshadow_driver_data = { + .name = "lcdshadow", + .exit = lcdshadow_exit, + .resume = lcdshadow_resume, + .read = lcdshadow_read, + .write = lcdshadow_write, +}; + +/************************************************************************* + * Thinkpad sensor interfaces + */ + +#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ +#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ +#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ +#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ + +#define DYTC_CMD_GET 2 /* To get current IC function and mode */ +#define DYTC_GET_LAPMODE_BIT 17 /* Set when in lapmode */ + +#define PALMSENSOR_PRESENT_BIT 0 /* Determine if psensor present */ +#define PALMSENSOR_ON_BIT 1 /* psensor status */ + +static bool has_palmsensor; +static bool has_lapsensor; +static bool palm_state; +static bool lap_state; +static int dytc_version; + +static int dytc_command(int command, int *output) +{ + acpi_handle dytc_handle; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DYTC", &dytc_handle))) { + /* Platform doesn't support DYTC */ + return -ENODEV; + } + if (!acpi_evalf(dytc_handle, output, NULL, "dd", command)) + return -EIO; + return 0; +} + +static int lapsensor_get(bool *present, bool *state) +{ + int output, err; + + *present = false; + err = dytc_command(DYTC_CMD_GET, &output); + if (err) + return err; + + *present = true; /*If we get his far, we have lapmode support*/ + *state = output & BIT(DYTC_GET_LAPMODE_BIT) ? true : false; + return 0; +} + +static int palmsensor_get(bool *present, bool *state) +{ + acpi_handle psensor_handle; + int output; + + *present = false; + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GPSS", &psensor_handle))) + return -ENODEV; + if (!acpi_evalf(psensor_handle, &output, NULL, "d")) + return -EIO; + + *present = output & BIT(PALMSENSOR_PRESENT_BIT) ? true : false; + *state = output & BIT(PALMSENSOR_ON_BIT) ? true : false; + return 0; +} + +static void lapsensor_refresh(void) +{ + bool state; + int err; + + if (has_lapsensor) { + err = lapsensor_get(&has_lapsensor, &state); + if (err) + return; + if (lap_state != state) { + lap_state = state; + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "dytc_lapmode"); + } + } +} + +static void palmsensor_refresh(void) +{ + bool state; + int err; + + if (has_palmsensor) { + err = palmsensor_get(&has_palmsensor, &state); + if (err) + return; + if (palm_state != state) { + palm_state = state; + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "palmsensor"); + } + } +} + +static ssize_t dytc_lapmode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + if (has_lapsensor) + return sysfs_emit(buf, "%d\n", lap_state); + return sysfs_emit(buf, "\n"); +} +static DEVICE_ATTR_RO(dytc_lapmode); + +static ssize_t palmsensor_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + if (has_palmsensor) + return sysfs_emit(buf, "%d\n", palm_state); + return sysfs_emit(buf, "\n"); +} +static DEVICE_ATTR_RO(palmsensor); + +static struct attribute *proxsensor_attributes[] = { + &dev_attr_dytc_lapmode.attr, + &dev_attr_palmsensor.attr, + NULL +}; + +static umode_t proxsensor_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &dev_attr_dytc_lapmode.attr) { + /* + * Platforms before DYTC version 5 claim to have a lap sensor, + * but it doesn't work, so we ignore them. + */ + if (!has_lapsensor || dytc_version < 5) + return 0; + } else if (attr == &dev_attr_palmsensor.attr) { + if (!has_palmsensor) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group proxsensor_attr_group = { + .is_visible = proxsensor_attr_is_visible, + .attrs = proxsensor_attributes, +}; + +static int tpacpi_proxsensor_init(struct ibm_init_struct *iibm) +{ + int palm_err, lap_err; + + palm_err = palmsensor_get(&has_palmsensor, &palm_state); + lap_err = lapsensor_get(&has_lapsensor, &lap_state); + /* If support isn't available for both devices return -ENODEV */ + if ((palm_err == -ENODEV) && (lap_err == -ENODEV)) + return -ENODEV; + /* Otherwise, if there was an error return it */ + if (palm_err && (palm_err != -ENODEV)) + return palm_err; + if (lap_err && (lap_err != -ENODEV)) + return lap_err; + + return 0; +} + +static struct ibm_struct proxsensor_driver_data = { + .name = "proximity-sensor", +}; + +/************************************************************************* + * DYTC Platform Profile interface + */ + +#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ +#define DYTC_CMD_MMC_GET 8 /* To get current MMC function and mode */ +#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ + +#define DYTC_CMD_FUNC_CAP 3 /* To get DYTC capabilities */ +#define DYTC_FC_MMC 27 /* MMC Mode supported */ +#define DYTC_FC_PSC 29 /* PSC Mode supported */ +#define DYTC_FC_AMT 31 /* AMT mode supported */ + +#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ +#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ + +#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ +#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ +#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ + +#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ +#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ +#define DYTC_FUNCTION_MMC 11 /* Function = 11, MMC mode */ +#define DYTC_FUNCTION_PSC 13 /* Function = 13, PSC mode */ +#define DYTC_FUNCTION_AMT 15 /* Function = 15, AMT mode */ + +#define DYTC_MODE_AMT_ENABLE 0x1 /* Enable AMT (in balanced mode) */ +#define DYTC_MODE_AMT_DISABLE 0xF /* Disable AMT (in other modes) */ + +#define DYTC_MODE_MMC_PERFORM 2 /* High power mode aka performance */ +#define DYTC_MODE_MMC_LOWPOWER 3 /* Low power mode */ +#define DYTC_MODE_MMC_BALANCE 0xF /* Default mode aka balanced */ +#define DYTC_MODE_MMC_DEFAULT 0 /* Default mode from MMC_GET, aka balanced */ + +#define DYTC_MODE_PSC_LOWPOWER 3 /* Low power mode */ +#define DYTC_MODE_PSC_BALANCE 5 /* Default mode aka balanced */ +#define DYTC_MODE_PSC_PERFORM 7 /* High power mode aka performance */ + +#define DYTC_MODE_PSCV9_LOWPOWER 1 /* Low power mode */ +#define DYTC_MODE_PSCV9_BALANCE 3 /* Default mode aka balanced */ +#define DYTC_MODE_PSCV9_PERFORM 4 /* High power mode aka performance */ + +#define DYTC_ERR_MASK 0xF /* Bits 0-3 in cmd result are the error result */ +#define DYTC_ERR_SUCCESS 1 /* CMD completed successful */ + +#define DYTC_SET_COMMAND(function, mode, on) \ + (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ + (mode) << DYTC_SET_MODE_BIT | \ + (on) << DYTC_SET_VALID_BIT) + +#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 0) +#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 1) +static int dytc_control_amt(bool enable); +static bool dytc_amt_active; + +static enum platform_profile_option dytc_current_profile; +static atomic_t dytc_ignore_event = ATOMIC_INIT(0); +static DEFINE_MUTEX(dytc_mutex); +static int dytc_capabilities; +static bool dytc_mmc_get_available; +static int profile_force; + +static int platform_psc_profile_lowpower = DYTC_MODE_PSC_LOWPOWER; +static int platform_psc_profile_balanced = DYTC_MODE_PSC_BALANCE; +static int platform_psc_profile_performance = DYTC_MODE_PSC_PERFORM; + +static int convert_dytc_to_profile(int funcmode, int dytcmode, + enum platform_profile_option *profile) +{ + switch (funcmode) { + case DYTC_FUNCTION_MMC: + switch (dytcmode) { + case DYTC_MODE_MMC_LOWPOWER: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case DYTC_MODE_MMC_DEFAULT: + case DYTC_MODE_MMC_BALANCE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case DYTC_MODE_MMC_PERFORM: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + default: /* Unknown mode */ + return -EINVAL; + } + return 0; + case DYTC_FUNCTION_PSC: + if (dytcmode == platform_psc_profile_lowpower) + *profile = PLATFORM_PROFILE_LOW_POWER; + else if (dytcmode == platform_psc_profile_balanced) + *profile = PLATFORM_PROFILE_BALANCED; + else if (dytcmode == platform_psc_profile_performance) + *profile = PLATFORM_PROFILE_PERFORMANCE; + else + return -EINVAL; + + return 0; + case DYTC_FUNCTION_AMT: + /* For now return balanced. It's the closest we have to 'auto' */ + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + default: + /* Unknown function */ + pr_debug("unknown function 0x%x\n", funcmode); + return -EOPNOTSUPP; + } + return 0; +} + +static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) +{ + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_LOWPOWER; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_lowpower; + break; + case PLATFORM_PROFILE_BALANCED: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_BALANCE; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_balanced; + break; + case PLATFORM_PROFILE_PERFORMANCE: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_PERFORM; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_performance; + break; + default: /* Unknown profile */ + return -EOPNOTSUPP; + } + return 0; +} + +/* + * dytc_profile_get: Function to register with platform_profile + * handler. Returns current platform profile. + */ +static int dytc_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + *profile = dytc_current_profile; + return 0; +} + +static int dytc_control_amt(bool enable) +{ + int dummy; + int err; + int cmd; + + if (!(dytc_capabilities & BIT(DYTC_FC_AMT))) { + pr_warn("Attempting to toggle AMT on a system that doesn't advertise support\n"); + return -ENODEV; + } + + if (enable) + cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_ENABLE, enable); + else + cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_DISABLE, enable); + + pr_debug("%sabling AMT (cmd 0x%x)", enable ? "en":"dis", cmd); + err = dytc_command(cmd, &dummy); + if (err) + return err; + dytc_amt_active = enable; + return 0; +} + +/* + * Helper function - check if we are in CQL mode and if we are + * - disable CQL, + * - run the command + * - enable CQL + * If not in CQL mode, just run the command + */ +static int dytc_cql_command(int command, int *output) +{ + int err, cmd_err, dummy; + int cur_funcmode; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_command(DYTC_CMD_GET, output); + if (err) + return err; + + cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; + /* Check if we're OK to return immediately */ + if ((command == DYTC_CMD_GET) && (cur_funcmode != DYTC_FUNCTION_CQL)) + return 0; + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + atomic_inc(&dytc_ignore_event); + err = dytc_command(DYTC_DISABLE_CQL, &dummy); + if (err) + return err; + } + + cmd_err = dytc_command(command, output); + /* Check return condition after we've restored CQL state */ + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = dytc_command(DYTC_ENABLE_CQL, &dummy); + if (err) + return err; + } + return cmd_err; +} + +/* + * dytc_profile_set: Function to register with platform_profile + * handler. Sets current platform profile. + */ +static int dytc_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + int perfmode; + int output; + int err; + + err = mutex_lock_interruptible(&dytc_mutex); + if (err) + return err; + + err = convert_profile_to_dytc(profile, &perfmode); + if (err) + goto unlock; + + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { + if (profile == PLATFORM_PROFILE_BALANCED) { + /* + * To get back to balanced mode we need to issue a reset command. + * Note we still need to disable CQL mode before hand and re-enable + * it afterwards, otherwise dytc_lapmode gets reset to 0 and stays + * stuck at 0 for aprox. 30 minutes. + */ + err = dytc_cql_command(DYTC_CMD_RESET, &output); + if (err) + goto unlock; + } else { + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_cql_command(DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), + &output); + if (err) + goto unlock; + } + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { + err = dytc_command(DYTC_SET_COMMAND(DYTC_FUNCTION_PSC, perfmode, 1), &output); + if (err) + goto unlock; + + /* system supports AMT, activate it when on balanced */ + if (dytc_capabilities & BIT(DYTC_FC_AMT)) + dytc_control_amt(profile == PLATFORM_PROFILE_BALANCED); + } + /* Success - update current profile */ + dytc_current_profile = profile; +unlock: + mutex_unlock(&dytc_mutex); + return err; +} + +static int dytc_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); + + return 0; +} + +static const struct platform_profile_ops dytc_profile_ops = { + .probe = dytc_profile_probe, + .profile_get = dytc_profile_get, + .profile_set = dytc_profile_set, +}; + +static void dytc_profile_refresh(void) +{ + enum platform_profile_option profile; + int output = 0, err = 0; + int perfmode, funcmode = 0; + + mutex_lock(&dytc_mutex); + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { + if (dytc_mmc_get_available) + err = dytc_command(DYTC_CMD_MMC_GET, &output); + else + err = dytc_cql_command(DYTC_CMD_GET, &output); + funcmode = DYTC_FUNCTION_MMC; + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { + err = dytc_command(DYTC_CMD_GET, &output); + /* Check if we are PSC mode, or have AMT enabled */ + funcmode = (output >> DYTC_GET_FUNCTION_BIT) & 0xF; + } else { /* Unknown profile mode */ + err = -ENODEV; + } + mutex_unlock(&dytc_mutex); + if (err) + return; + + perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; + err = convert_dytc_to_profile(funcmode, perfmode, &profile); + if (!err && profile != dytc_current_profile) { + dytc_current_profile = profile; + platform_profile_notify(tpacpi_pprof); + } +} + +static int tpacpi_dytc_profile_init(struct ibm_init_struct *iibm) +{ + int err, output; + + err = dytc_command(DYTC_CMD_QUERY, &output); + if (err) + return err; + + if (output & BIT(DYTC_QUERY_ENABLE_BIT)) + dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; + + dbg_printk(TPACPI_DBG_INIT, "DYTC version %d\n", dytc_version); + /* Check DYTC is enabled and supports mode setting */ + if (dytc_version < 5) + return -ENODEV; + + /* Check what capabilities are supported */ + err = dytc_command(DYTC_CMD_FUNC_CAP, &dytc_capabilities); + if (err) + return err; + + /* Check if user wants to override the profile selection */ + if (profile_force) { + switch (profile_force) { + case -1: + dytc_capabilities = 0; + break; + case 1: + dytc_capabilities = BIT(DYTC_FC_MMC); + break; + case 2: + dytc_capabilities = BIT(DYTC_FC_PSC); + break; + } + pr_debug("Profile selection forced: 0x%x\n", dytc_capabilities); + } + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { /* MMC MODE */ + pr_debug("MMC is supported\n"); + /* + * Check if MMC_GET functionality available + * Version > 6 and return success from MMC_GET command + */ + dytc_mmc_get_available = false; + if (dytc_version >= 6) { + err = dytc_command(DYTC_CMD_MMC_GET, &output); + if (!err && ((output & DYTC_ERR_MASK) == DYTC_ERR_SUCCESS)) + dytc_mmc_get_available = true; + } + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { /* PSC MODE */ + pr_debug("PSC is supported\n"); + if (dytc_version >= 9) { /* update profiles for DYTC 9 and up */ + platform_psc_profile_lowpower = DYTC_MODE_PSCV9_LOWPOWER; + platform_psc_profile_balanced = DYTC_MODE_PSCV9_BALANCE; + platform_psc_profile_performance = DYTC_MODE_PSCV9_PERFORM; + } + } else { + dbg_printk(TPACPI_DBG_INIT, "No DYTC support available\n"); + return -ENODEV; + } + + dbg_printk(TPACPI_DBG_INIT, + "DYTC version %d: thermal mode available\n", dytc_version); + + /* Create platform_profile structure and register */ + tpacpi_pprof = platform_profile_register(&tpacpi_pdev->dev, "thinkpad-acpi-profile", + NULL, &dytc_profile_ops); + /* + * If for some reason platform_profiles aren't enabled + * don't quit terminally. + */ + if (IS_ERR(tpacpi_pprof)) + return -ENODEV; + + /* Ensure initial values are correct */ + dytc_profile_refresh(); + + /* Workaround for https://bugzilla.kernel.org/show_bug.cgi?id=216347 */ + if (dytc_capabilities & BIT(DYTC_FC_PSC)) + dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); + + return 0; +} + +static void dytc_profile_exit(void) +{ + if (!IS_ERR_OR_NULL(tpacpi_pprof)) + platform_profile_remove(tpacpi_pprof); +} + +static struct ibm_struct dytc_profile_driver_data = { + .name = "dytc-profile", + .exit = dytc_profile_exit, +}; + +/************************************************************************* + * Keyboard language interface + */ + +struct keyboard_lang_data { + const char *lang_str; + int lang_code; +}; + +static const struct keyboard_lang_data keyboard_lang_data[] = { + {"be", 0x080c}, + {"cz", 0x0405}, + {"da", 0x0406}, + {"de", 0x0c07}, + {"en", 0x0000}, + {"es", 0x2c0a}, + {"et", 0x0425}, + {"fr", 0x040c}, + {"fr-ch", 0x100c}, + {"hu", 0x040e}, + {"it", 0x0410}, + {"jp", 0x0411}, + {"nl", 0x0413}, + {"nn", 0x0414}, + {"pl", 0x0415}, + {"pt", 0x0816}, + {"sl", 0x041b}, + {"sv", 0x081d}, + {"tr", 0x041f}, +}; + +static int set_keyboard_lang_command(int command) +{ + acpi_handle sskl_handle; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "SSKL", &sskl_handle))) { + /* Platform doesn't support SSKL */ + return -ENODEV; + } + + if (!acpi_evalf(sskl_handle, &output, NULL, "dd", command)) + return -EIO; + + return 0; +} + +static int get_keyboard_lang(int *output) +{ + acpi_handle gskl_handle; + int kbd_lang; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GSKL", &gskl_handle))) { + /* Platform doesn't support GSKL */ + return -ENODEV; + } + + if (!acpi_evalf(gskl_handle, &kbd_lang, NULL, "dd", 0x02000000)) + return -EIO; + + /* + * METHOD_ERR gets returned on devices where there are no special (e.g. '=', + * '(' and ')') keys which use layout dependent key-press emulation. + */ + if (kbd_lang & METHOD_ERR) + return -ENODEV; + + *output = kbd_lang; + + return 0; +} + +/* sysfs keyboard language entry */ +static ssize_t keyboard_lang_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int output, err, i, len = 0; + + err = get_keyboard_lang(&output); + if (err) + return err; + + for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { + if (i) + len += sysfs_emit_at(buf, len, "%s", " "); + + if (output == keyboard_lang_data[i].lang_code) { + len += sysfs_emit_at(buf, len, "[%s]", keyboard_lang_data[i].lang_str); + } else { + len += sysfs_emit_at(buf, len, "%s", keyboard_lang_data[i].lang_str); + } + } + len += sysfs_emit_at(buf, len, "\n"); + + return len; +} + +static ssize_t keyboard_lang_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int err, i; + bool lang_found = false; + int lang_code = 0; + + for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { + if (sysfs_streq(buf, keyboard_lang_data[i].lang_str)) { + lang_code = keyboard_lang_data[i].lang_code; + lang_found = true; + break; + } + } + + if (lang_found) { + lang_code = lang_code | 1 << 24; + + /* Set language code */ + err = set_keyboard_lang_command(lang_code); + if (err) + return err; + } else { + dev_err(&tpacpi_pdev->dev, "Unknown Keyboard language. Ignoring\n"); + return -EINVAL; + } + + tpacpi_disclose_usertask(attr->attr.name, + "keyboard language is set to %s\n", buf); + + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "keyboard_lang"); + + return count; +} +static DEVICE_ATTR_RW(keyboard_lang); + +static struct attribute *kbdlang_attributes[] = { + &dev_attr_keyboard_lang.attr, + NULL +}; + +static umode_t kbdlang_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.kbd_lang ? attr->mode : 0; +} + +static const struct attribute_group kbdlang_attr_group = { + .is_visible = kbdlang_attr_is_visible, + .attrs = kbdlang_attributes, +}; + +static int tpacpi_kbdlang_init(struct ibm_init_struct *iibm) +{ + int err, output; + + err = get_keyboard_lang(&output); + tp_features.kbd_lang = !err; + return err; +} + +static struct ibm_struct kbdlang_driver_data = { + .name = "kbdlang", +}; + +/************************************************************************* + * DPRC(Dynamic Power Reduction Control) subdriver, for the Lenovo WWAN + * and WLAN feature. + */ +#define DPRC_GET_WWAN_ANTENNA_TYPE 0x40000 +#define DPRC_WWAN_ANTENNA_TYPE_A_BIT BIT(4) +#define DPRC_WWAN_ANTENNA_TYPE_B_BIT BIT(8) +static bool has_antennatype; +static int wwan_antennatype; + +static int dprc_command(int command, int *output) +{ + acpi_handle dprc_handle; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DPRC", &dprc_handle))) { + /* Platform doesn't support DPRC */ + return -ENODEV; + } + + if (!acpi_evalf(dprc_handle, output, NULL, "dd", command)) + return -EIO; + + /* + * METHOD_ERR gets returned on devices where few commands are not supported + * for example command to get WWAN Antenna type command is not supported on + * some devices. + */ + if (*output & METHOD_ERR) + return -ENODEV; + + return 0; +} + +static int get_wwan_antenna(int *wwan_antennatype) +{ + int output, err; + + /* Get current Antenna type */ + err = dprc_command(DPRC_GET_WWAN_ANTENNA_TYPE, &output); + if (err) + return err; + + if (output & DPRC_WWAN_ANTENNA_TYPE_A_BIT) + *wwan_antennatype = 1; + else if (output & DPRC_WWAN_ANTENNA_TYPE_B_BIT) + *wwan_antennatype = 2; + else + return -ENODEV; + + return 0; +} + +/* sysfs wwan antenna type entry */ +static ssize_t wwan_antenna_type_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + switch (wwan_antennatype) { + case 1: + return sysfs_emit(buf, "type a\n"); + case 2: + return sysfs_emit(buf, "type b\n"); + default: + return -ENODATA; + } +} +static DEVICE_ATTR_RO(wwan_antenna_type); + +static struct attribute *dprc_attributes[] = { + &dev_attr_wwan_antenna_type.attr, + NULL +}; + +static umode_t dprc_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return has_antennatype ? attr->mode : 0; +} + +static const struct attribute_group dprc_attr_group = { + .is_visible = dprc_attr_is_visible, + .attrs = dprc_attributes, +}; + +static int tpacpi_dprc_init(struct ibm_init_struct *iibm) +{ + int err; + + err = get_wwan_antenna(&wwan_antennatype); + if (err) + return err; + + has_antennatype = true; + return 0; +} + +static struct ibm_struct dprc_driver_data = { + .name = "dprc", +}; + +/* + * Auxmac + * + * This auxiliary mac address is enabled in the bios through the + * MAC Address Pass-through feature. In most cases, there are three + * possibilities: Internal Mac, Second Mac, and disabled. + * + */ + +#define AUXMAC_LEN 12 +#define AUXMAC_START 9 +#define AUXMAC_STRLEN 22 +#define AUXMAC_BEGIN_MARKER 8 +#define AUXMAC_END_MARKER 21 + +static char auxmac[AUXMAC_LEN + 1]; + +static int auxmac_init(struct ibm_init_struct *iibm) +{ + acpi_status status; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + status = acpi_evaluate_object(NULL, "\\MACA", NULL, &buffer); + + if (ACPI_FAILURE(status)) + return -ENODEV; + + obj = buffer.pointer; + + if (obj->type != ACPI_TYPE_STRING || obj->string.length != AUXMAC_STRLEN) { + pr_info("Invalid buffer for MAC address pass-through.\n"); + goto auxmacinvalid; + } + + if (obj->string.pointer[AUXMAC_BEGIN_MARKER] != '#' || + obj->string.pointer[AUXMAC_END_MARKER] != '#') { + pr_info("Invalid header for MAC address pass-through.\n"); + goto auxmacinvalid; + } + + if (strncmp(obj->string.pointer + AUXMAC_START, "XXXXXXXXXXXX", AUXMAC_LEN) != 0) + strscpy(auxmac, obj->string.pointer + AUXMAC_START, sizeof(auxmac)); + else + strscpy(auxmac, "disabled", sizeof(auxmac)); + +free: + kfree(obj); + return 0; + +auxmacinvalid: + strscpy(auxmac, "unavailable", sizeof(auxmac)); + goto free; +} + +static struct ibm_struct auxmac_data = { + .name = "auxmac", +}; + +static DEVICE_STRING_ATTR_RO(auxmac, 0444, auxmac); + +static umode_t auxmac_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return auxmac[0] == 0 ? 0 : attr->mode; +} + +static struct attribute *auxmac_attributes[] = { + &dev_attr_auxmac.attr.attr, + NULL +}; + +static const struct attribute_group auxmac_attr_group = { + .is_visible = auxmac_attr_is_visible, + .attrs = auxmac_attributes, +}; + +/************************************************************************* + * HWDD subdriver, for the Lenovo Hardware Damage Detection feature. + */ + +#define HWDD_GET_DMG_USBC 0x80000001 +#define HWDD_GET_CAP 0 +#define HWDD_NOT_SUPPORTED BIT(31) +#define HWDD_SUPPORT_USBC BIT(0) + +#define PORT_STATUS GENMASK(7, 4) +#define LID_STATUS GENMASK(11, 8) +#define BASE_STATUS GENMASK(15, 12) +#define POS_STATUS GENMASK(3, 2) +#define PANEL_STATUS GENMASK(1, 0) + +#define PORT_DETAIL_OFFSET 16 + +#define PANEL_TOP 0 +#define PANEL_BASE 1 +#define PANEL_LEFT 2 +#define PANEL_RIGHT 3 + +#define POS_LEFT 0 +#define POS_CENTER 1 +#define POS_RIGHT 2 + +#define NUM_PORTS 4 + +static bool hwdd_support_available; +static bool ucdd_supported; + +static int hwdd_command(int command, int *output) +{ + acpi_handle hwdd_handle; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "HWDD", &hwdd_handle))) + return -ENODEV; + + if (!acpi_evalf(hwdd_handle, output, NULL, "dd", command)) + return -EIO; + + return 0; +} + +static bool display_damage(char *buf, int *count, char *type, unsigned int dmg_status) +{ + unsigned char lid_status, base_status, port_status; + unsigned char loc_status, pos_status, panel_status; + bool damage_detected = false; + int i; + + port_status = FIELD_GET(PORT_STATUS, dmg_status); + lid_status = FIELD_GET(LID_STATUS, dmg_status); + base_status = FIELD_GET(BASE_STATUS, dmg_status); + for (i = 0; i < NUM_PORTS; i++) { + if (!(dmg_status & BIT(i)) || !(port_status & BIT(i))) + continue; + + *count += sysfs_emit_at(buf, *count, "%s: ", type); + loc_status = (dmg_status >> (PORT_DETAIL_OFFSET + (4 * i))) & 0xF; + pos_status = FIELD_GET(POS_STATUS, loc_status); + panel_status = FIELD_GET(PANEL_STATUS, loc_status); + + if (lid_status & BIT(i)) + *count += sysfs_emit_at(buf, *count, "Lid, "); + if (base_status & BIT(i)) + *count += sysfs_emit_at(buf, *count, "Base, "); + + switch (pos_status) { + case PANEL_TOP: + *count += sysfs_emit_at(buf, *count, "Top, "); + break; + case PANEL_BASE: + *count += sysfs_emit_at(buf, *count, "Bottom, "); + break; + case PANEL_LEFT: + *count += sysfs_emit_at(buf, *count, "Left, "); + break; + case PANEL_RIGHT: + *count += sysfs_emit_at(buf, *count, "Right, "); + break; + default: + pr_err("Unexpected value %d in switch statement\n", pos_status); + } + + switch (panel_status) { + case POS_LEFT: + *count += sysfs_emit_at(buf, *count, "Left port\n"); + break; + case POS_CENTER: + *count += sysfs_emit_at(buf, *count, "Center port\n"); + break; + case POS_RIGHT: + *count += sysfs_emit_at(buf, *count, "Right port\n"); + break; + default: + *count += sysfs_emit_at(buf, *count, "Undefined\n"); + break; + } + damage_detected = true; + } + return damage_detected; +} + +/* sysfs type-c damage detection detail */ +static ssize_t hwdd_detail_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + unsigned int damage_status; + int err, count = 0; + + if (!ucdd_supported) + return -ENODEV; + + /* Get USB TYPE-C damage status */ + err = hwdd_command(HWDD_GET_DMG_USBC, &damage_status); + if (err) + return err; + + if (!display_damage(buf, &count, "Type-C", damage_status)) + count += sysfs_emit_at(buf, count, "No damage detected\n"); + + return count; +} + +/* sysfs type-c damage detection capability */ +static ssize_t hwdd_status_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + unsigned int damage_status, port_status; + int err, i; + + if (!ucdd_supported) + return -ENODEV; + + /* Get USB TYPE-C damage status */ + err = hwdd_command(HWDD_GET_DMG_USBC, &damage_status); + if (err) + return err; + + port_status = FIELD_GET(PORT_STATUS, damage_status); + for (i = 0; i < NUM_PORTS; i++) { + if (!(damage_status & BIT(i))) + continue; + if (port_status & BIT(i)) + return sysfs_emit(buf, "1\n"); + } + + return sysfs_emit(buf, "0\n"); +} + +/* sysfs type-c damage detection raw data - accessed via debugfs*/ +static int hwdd_raw_show(struct seq_file *m, void *v) +{ + unsigned int damage_status; + int err; + + if (!ucdd_supported) + return -ENODEV; + + /* Get USB TYPE-C damage status */ + err = hwdd_command(HWDD_GET_DMG_USBC, &damage_status); + if (err) + return err; + + seq_printf(m, "HWDD: 0x%x\n", damage_status); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(hwdd_raw); + +static DEVICE_ATTR_RO(hwdd_status); +static DEVICE_ATTR_RO(hwdd_detail); + +static struct attribute *hwdd_attributes[] = { + &dev_attr_hwdd_status.attr, + &dev_attr_hwdd_detail.attr, + NULL +}; + +static umode_t hwdd_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return hwdd_support_available ? attr->mode : 0; +} + +static const struct attribute_group hwdd_attr_group = { + .is_visible = hwdd_attr_is_visible, + .attrs = hwdd_attributes, +}; + +static int tpacpi_hwdd_init(struct ibm_init_struct *iibm) +{ + int err, output; + + /* Below command checks the HWDD damage capability */ + err = hwdd_command(HWDD_GET_CAP, &output); + if (err) + return err; + + if (!(output & HWDD_NOT_SUPPORTED)) + return -ENODEV; + + hwdd_support_available = true; + + /* + * BIT(0) is assigned to check capability of damage detection is + * supported for USB Type-C port or not. + */ + if (output & HWDD_SUPPORT_USBC) + ucdd_supported = true; + + return err; +} + +static struct ibm_struct hwdd_driver_data = { + .name = "hwdd", +}; + +/* --------------------------------------------------------------------- */ + +static struct attribute *tpacpi_driver_attributes[] = { + &driver_attr_debug_level.attr, + &driver_attr_version.attr, + &driver_attr_interface_version.attr, +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + &driver_attr_wlsw_emulstate.attr, + &driver_attr_bluetooth_emulstate.attr, + &driver_attr_wwan_emulstate.attr, + &driver_attr_uwb_emulstate.attr, +#endif + NULL +}; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +static umode_t tpacpi_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &driver_attr_wlsw_emulstate.attr) { + if (!dbg_wlswemul) + return 0; + } else if (attr == &driver_attr_bluetooth_emulstate.attr) { + if (!dbg_bluetoothemul) + return 0; + } else if (attr == &driver_attr_wwan_emulstate.attr) { + if (!dbg_wwanemul) + return 0; + } else if (attr == &driver_attr_uwb_emulstate.attr) { + if (!dbg_uwbemul) + return 0; + } + + return attr->mode; +} +#endif + +static const struct attribute_group tpacpi_driver_attr_group = { +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + .is_visible = tpacpi_attr_is_visible, +#endif + .attrs = tpacpi_driver_attributes, +}; + +static const struct attribute_group *tpacpi_driver_groups[] = { + &tpacpi_driver_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_groups[] = { + &adaptive_kbd_attr_group, + &hotkey_attr_group, + &bluetooth_attr_group, + &wan_attr_group, + &cmos_attr_group, + &proxsensor_attr_group, + &kbdlang_attr_group, + &dprc_attr_group, + &auxmac_attr_group, + &hwdd_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_hwmon_groups[] = { + &thermal_attr_group, + &temp_label_attr_group, + &fan_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_hwmon_driver_groups[] = { + &fan_driver_attr_group, + NULL, +}; + +/**************************************************************************** + **************************************************************************** + * + * Platform drivers + * + **************************************************************************** + ****************************************************************************/ + +static struct platform_driver tpacpi_pdriver = { + .driver = { + .name = TPACPI_DRVR_NAME, + .pm = &tpacpi_pm, + .groups = tpacpi_driver_groups, + .dev_groups = tpacpi_groups, + }, + .shutdown = tpacpi_shutdown_handler, +}; + +static struct platform_driver tpacpi_hwmon_pdriver = { + .driver = { + .name = TPACPI_HWMON_DRVR_NAME, + .groups = tpacpi_hwmon_driver_groups, + }, +}; + +/**************************************************************************** + **************************************************************************** + * + * Infrastructure + * + **************************************************************************** + ****************************************************************************/ + +/* + * HKEY event callout for other subdrivers go here + * (yes, it is ugly, but it is quick, safe, and gets the job done + */ +static bool tpacpi_driver_event(const unsigned int hkey_event) +{ + int camera_shutter_state; + + switch (hkey_event) { + case TP_HKEY_EV_BRGHT_UP: + case TP_HKEY_EV_BRGHT_DOWN: + if (ibm_backlight_device) + tpacpi_brightness_notify_change(); + /* + * Key press events are suppressed by default hotkey_user_mask + * and should still be reported if explicitly requested. + */ + return false; + case TP_HKEY_EV_VOL_UP: + case TP_HKEY_EV_VOL_DOWN: + case TP_HKEY_EV_VOL_MUTE: + if (alsa_card) + volume_alsa_notify_change(); + + /* Key events are suppressed by default hotkey_user_mask */ + return false; + case TP_HKEY_EV_KBD_LIGHT: + if (tp_features.kbdlight) { + enum led_brightness brightness; + + mutex_lock(&kbdlight_mutex); + + /* + * Check the brightness actually changed, setting the brightness + * through kbdlight_set_level() also triggers this event. + */ + brightness = kbdlight_sysfs_get(NULL); + if (kbdlight_brightness != brightness) { + kbdlight_brightness = brightness; + led_classdev_notify_brightness_hw_changed( + &tpacpi_led_kbdlight.led_classdev, brightness); + } + + mutex_unlock(&kbdlight_mutex); + } + /* Key events are suppressed by default hotkey_user_mask */ + return false; + case TP_HKEY_EV_DFR_CHANGE_ROW: + adaptive_keyboard_change_row(); + return true; + case TP_HKEY_EV_DFR_S_QUICKVIEW_ROW: + adaptive_keyboard_s_quickview_row(); + return true; + case TP_HKEY_EV_THM_CSM_COMPLETED: + lapsensor_refresh(); + /* If we are already accessing DYTC then skip dytc update */ + if (!atomic_add_unless(&dytc_ignore_event, -1, 0)) + dytc_profile_refresh(); + + return true; + case TP_HKEY_EV_PRIVACYGUARD_TOGGLE: + if (lcdshadow_dev) { + enum drm_privacy_screen_status old_hw_state; + bool changed; + + mutex_lock(&lcdshadow_dev->lock); + old_hw_state = lcdshadow_dev->hw_state; + lcdshadow_get_hw_state(lcdshadow_dev); + changed = lcdshadow_dev->hw_state != old_hw_state; + mutex_unlock(&lcdshadow_dev->lock); + + if (changed) + drm_privacy_screen_call_notifier_chain(lcdshadow_dev); + } + return true; + case TP_HKEY_EV_AMT_TOGGLE: + /* If we're enabling AMT we need to force balanced mode */ + if (!dytc_amt_active) + /* This will also set AMT mode enabled */ + dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); + else + dytc_control_amt(!dytc_amt_active); + + return true; + case TP_HKEY_EV_CAMERASHUTTER_TOGGLE: + camera_shutter_state = get_camera_shutter(); + if (camera_shutter_state < 0) { + pr_err("Error retrieving camera shutter state after shutter event\n"); + return true; + } + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + return true; + case TP_HKEY_EV_DOUBLETAP_TOGGLE: + /* Toggle kernel-level doubletap event filtering */ + tp_features.trackpoint_doubletap_enable = + !tp_features.trackpoint_doubletap_enable; + return true; + case TP_HKEY_EV_PROFILE_TOGGLE: + case TP_HKEY_EV_PROFILE_TOGGLE2: + platform_profile_cycle(); + return true; + } + + return false; +} + +/* --------------------------------------------------------------------- */ + +/* /proc support */ +static struct proc_dir_entry *proc_dir; + +/* + * Module and infrastructure proble, init and exit handling + */ + +static bool force_load; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +static const char * __init str_supported(int is_supported) +{ + static char text_unsupported[] __initdata = "not supported"; + + return (is_supported) ? &text_unsupported[4] : &text_unsupported[0]; +} +#endif /* CONFIG_THINKPAD_ACPI_DEBUG */ + +static struct dentry *tpacpi_dbg; +static void tpacpi_debugfs_init(void) +{ + tpacpi_dbg = debugfs_create_dir("tpacpi", NULL); + + /* HWDD raw data */ + debugfs_create_file("hwdd-raw", 0444, tpacpi_dbg, NULL, &hwdd_raw_fops); +} + +static void tpacpi_debugfs_remove(void) +{ + debugfs_remove_recursive(tpacpi_dbg); +} + +static void ibm_exit(struct ibm_struct *ibm) +{ + dbg_printk(TPACPI_DBG_EXIT, "removing %s\n", ibm->name); + + list_del_init(&ibm->all_drivers); + + if (ibm->flags.acpi_notify_installed) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: acpi_remove_notify_handler\n", ibm->name); + BUG_ON(!ibm->acpi); + acpi_remove_notify_handler(*ibm->acpi->handle, + ibm->acpi->type, + dispatch_acpi_notify); + ibm->acpi->device->driver_data = NULL; + acpi_dev_put(ibm->acpi->device); + ibm->flags.acpi_notify_installed = 0; + } + + if (ibm->flags.proc_created) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: remove_proc_entry\n", ibm->name); + remove_proc_entry(ibm->name, proc_dir); + ibm->flags.proc_created = 0; + } + + if (ibm->flags.init_called && ibm->exit) { + ibm->exit(); + ibm->flags.init_called = 0; + } + + dbg_printk(TPACPI_DBG_INIT, "finished removing %s\n", ibm->name); +} + +static int __init ibm_init(struct ibm_init_struct *iibm) +{ + int ret; + struct ibm_struct *ibm = iibm->data; + struct proc_dir_entry *entry; + + BUG_ON(ibm == NULL); + + INIT_LIST_HEAD(&ibm->all_drivers); + + if (ibm->flags.experimental && !experimental) + return 0; + + dbg_printk(TPACPI_DBG_INIT, + "probing for %s\n", ibm->name); + + if (iibm->init) { + ret = iibm->init(iibm); + if (ret > 0 || ret == -ENODEV) + return 0; /* subdriver functionality not available */ + if (ret) + return ret; + + ibm->flags.init_called = 1; + } + + if (ibm->acpi) { + if (ibm->acpi->notify) { + ret = setup_acpi_notify(ibm); + if (ret == -ENODEV) { + pr_notice("disabling subdriver %s\n", + ibm->name); + ret = 0; + goto err_out; + } + if (ret < 0) + goto err_out; + } + } + + dbg_printk(TPACPI_DBG_INIT, + "%s installed\n", ibm->name); + + if (ibm->read) { + umode_t mode = iibm->base_procfs_mode; + + if (!mode) + mode = S_IRUGO; + if (ibm->write) + mode |= S_IWUSR; + entry = proc_create_data(ibm->name, mode, proc_dir, + &dispatch_proc_ops, ibm); + if (!entry) { + pr_err("unable to create proc entry %s\n", ibm->name); + ret = -ENODEV; + goto err_out; + } + ibm->flags.proc_created = 1; + } + + list_add_tail(&ibm->all_drivers, &tpacpi_all_drivers); + + return 0; + +err_out: + dbg_printk(TPACPI_DBG_INIT, + "%s: at error exit path with result %d\n", + ibm->name, ret); + + ibm_exit(ibm); + return (ret < 0) ? ret : 0; +} + +/* Probing */ + +static char __init tpacpi_parse_fw_id(const char * const s, + u32 *model, u16 *release) +{ + int i; + + if (!s || strlen(s) < 8) + goto invalid; + + for (i = 0; i < 8; i++) + if (!((s[i] >= '0' && s[i] <= '9') || + (s[i] >= 'A' && s[i] <= 'Z'))) + goto invalid; + + /* + * Most models: xxyTkkWW (#.##c) + * Ancient 570/600 and -SL lacks (#.##c) + */ + if (s[3] == 'T' || s[3] == 'N') { + *model = TPID(s[0], s[1]); + *release = TPVER(s[4], s[5]); + return s[2]; + + /* New models: xxxyTkkW (#.##c); T550 and some others */ + } else if (s[4] == 'T' || s[4] == 'N') { + *model = TPID3(s[0], s[1], s[2]); + *release = TPVER(s[5], s[6]); + return s[3]; + } + +invalid: + return '\0'; +} + +#define EC_FW_STRING_LEN 18 + +static void find_new_ec_fwstr(const struct dmi_header *dm, void *private) +{ + char *ec_fw_string = (char *) private; + const char *dmi_data = (const char *)dm; + /* + * ThinkPad Embedded Controller Program Table on newer models + * + * Offset | Name | Width | Description + * ---------------------------------------------------- + * 0x00 | Type | BYTE | 0x8C + * 0x01 | Length | BYTE | + * 0x02 | Handle | WORD | Varies + * 0x04 | Signature | BYTEx6 | ASCII for "LENOVO" + * 0x0A | OEM struct offset | BYTE | 0x0B + * 0x0B | OEM struct number | BYTE | 0x07, for this structure + * 0x0C | OEM struct revision | BYTE | 0x01, for this format + * 0x0D | ECP version ID | STR ID | + * 0x0E | ECP release date | STR ID | + */ + + /* Return if data structure not match */ + if (dm->type != 140 || dm->length < 0x0F || + memcmp(dmi_data + 4, "LENOVO", 6) != 0 || + dmi_data[0x0A] != 0x0B || dmi_data[0x0B] != 0x07 || + dmi_data[0x0C] != 0x01) + return; + + /* fwstr is the first 8byte string */ + BUILD_BUG_ON(EC_FW_STRING_LEN <= 8); + memcpy(ec_fw_string, dmi_data + 0x0F, 8); +} + +/* returns 0 - probe ok, or < 0 - probe error. + * Probe ok doesn't mean thinkpad found. + * On error, kfree() cleanup on tp->* is not performed, caller must do it */ +static int __must_check __init get_thinkpad_model_data( + struct thinkpad_id_data *tp) +{ + const struct dmi_device *dev = NULL; + char ec_fw_string[EC_FW_STRING_LEN] = {0}; + char const *s; + char t; + + if (!tp) + return -EINVAL; + + memset(tp, 0, sizeof(*tp)); + + if (dmi_name_in_vendors("IBM")) + tp->vendor = PCI_VENDOR_ID_IBM; + else if (dmi_name_in_vendors("LENOVO")) + tp->vendor = PCI_VENDOR_ID_LENOVO; + else if (dmi_name_in_vendors("NEC")) + tp->vendor = PCI_VENDOR_ID_LENOVO; + else + return 0; + + s = dmi_get_system_info(DMI_BIOS_VERSION); + tp->bios_version_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->bios_version_str) + return -ENOMEM; + + /* Really ancient ThinkPad 240X will fail this, which is fine */ + t = tpacpi_parse_fw_id(tp->bios_version_str, + &tp->bios_model, &tp->bios_release); + if (t != 'E' && t != 'C') + return 0; + + /* + * ThinkPad T23 or newer, A31 or newer, R50e or newer, + * X32 or newer, all Z series; Some models must have an + * up-to-date BIOS or they will not be detected. + * + * See https://thinkwiki.org/wiki/List_of_DMI_IDs + */ + while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) { + if (sscanf(dev->name, + "IBM ThinkPad Embedded Controller -[%17c", + ec_fw_string) == 1) { + ec_fw_string[sizeof(ec_fw_string) - 1] = 0; + ec_fw_string[strcspn(ec_fw_string, " ]")] = 0; + break; + } + } + + /* Newer ThinkPads have different EC program info table */ + if (!ec_fw_string[0]) + dmi_walk(find_new_ec_fwstr, &ec_fw_string); + + if (ec_fw_string[0]) { + tp->ec_version_str = kstrdup(ec_fw_string, GFP_KERNEL); + if (!tp->ec_version_str) + return -ENOMEM; + + t = tpacpi_parse_fw_id(ec_fw_string, + &tp->ec_model, &tp->ec_release); + if (t != 'H') { + pr_notice("ThinkPad firmware release %s doesn't match the known patterns\n", + ec_fw_string); + pr_notice("please report this to %s\n", TPACPI_MAIL); + } + } + + s = dmi_get_system_info(DMI_PRODUCT_VERSION); + if (s && !(strncasecmp(s, "ThinkPad", 8) && strncasecmp(s, "Lenovo", 6))) { + tp->model_str = kstrdup(s, GFP_KERNEL); + if (!tp->model_str) + return -ENOMEM; + } else { + s = dmi_get_system_info(DMI_BIOS_VENDOR); + if (s && !(strncasecmp(s, "Lenovo", 6))) { + tp->model_str = kstrdup(s, GFP_KERNEL); + if (!tp->model_str) + return -ENOMEM; + } + } + + s = dmi_get_system_info(DMI_PRODUCT_NAME); + tp->nummodel_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->nummodel_str) + return -ENOMEM; + + return 0; +} + +static int __init probe_for_thinkpad(void) +{ + int is_thinkpad; + + if (acpi_disabled) + return -ENODEV; + + /* It would be dangerous to run the driver in this case */ + if (!tpacpi_is_ibm() && !tpacpi_is_lenovo()) + return -ENODEV; + + /* + * Non-ancient models have better DMI tagging, but very old models + * don't. tpacpi_is_fw_known() is a cheat to help in that case. + */ + is_thinkpad = (thinkpad_id.model_str != NULL) || + (thinkpad_id.ec_model != 0) || + tpacpi_is_fw_known(); + + /* The EC handler is required */ + tpacpi_acpi_handle_locate("ec", TPACPI_ACPI_EC_HID, &ec_handle); + if (!ec_handle) { + if (is_thinkpad) + pr_err("Not yet supported ThinkPad detected!\n"); + return -ENODEV; + } + + if (!is_thinkpad && !force_load) + return -ENODEV; + + return 0; +} + +static void __init thinkpad_acpi_init_banner(void) +{ + pr_info("%s v%s\n", TPACPI_DESC, TPACPI_VERSION); + pr_info("%s\n", TPACPI_URL); + + pr_info("ThinkPad BIOS %s, EC %s\n", + (thinkpad_id.bios_version_str) ? + thinkpad_id.bios_version_str : "unknown", + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "unknown"); + + BUG_ON(!thinkpad_id.vendor); + + if (thinkpad_id.model_str) + pr_info("%s %s, model %s\n", + (thinkpad_id.vendor == PCI_VENDOR_ID_IBM) ? + "IBM" : ((thinkpad_id.vendor == + PCI_VENDOR_ID_LENOVO) ? + "Lenovo" : "Unknown vendor"), + thinkpad_id.model_str, + (thinkpad_id.nummodel_str) ? + thinkpad_id.nummodel_str : "unknown"); +} + +/* Module init, exit, parameters */ + +static struct ibm_init_struct ibms_init[] __initdata = { + { + .data = &thinkpad_acpi_driver_data, + }, + { + .init = hotkey_init, + .data = &hotkey_driver_data, + }, + { + .init = bluetooth_init, + .data = &bluetooth_driver_data, + }, + { + .init = wan_init, + .data = &wan_driver_data, + }, + { + .init = uwb_init, + .data = &uwb_driver_data, + }, +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + { + .init = video_init, + .base_procfs_mode = S_IRUSR, + .data = &video_driver_data, + }, +#endif + { + .init = kbdlight_init, + .data = &kbdlight_driver_data, + }, + { + .init = light_init, + .data = &light_driver_data, + }, + { + .init = cmos_init, + .data = &cmos_driver_data, + }, + { + .init = led_init, + .data = &led_driver_data, + }, + { + .init = beep_init, + .data = &beep_driver_data, + }, + { + .init = thermal_init, + .data = &thermal_driver_data, + }, + { + .init = brightness_init, + .data = &brightness_driver_data, + }, + { + .init = volume_init, + .data = &volume_driver_data, + }, + { + .init = fan_init, + .data = &fan_driver_data, + }, + { + .init = mute_led_init, + .data = &mute_led_driver_data, + }, + { + .init = tpacpi_battery_init, + .data = &battery_driver_data, + }, + { + .init = tpacpi_lcdshadow_init, + .data = &lcdshadow_driver_data, + }, + { + .init = tpacpi_proxsensor_init, + .data = &proxsensor_driver_data, + }, + { + .init = tpacpi_dytc_profile_init, + .data = &dytc_profile_driver_data, + }, + { + .init = tpacpi_kbdlang_init, + .data = &kbdlang_driver_data, + }, + { + .init = tpacpi_dprc_init, + .data = &dprc_driver_data, + }, + { + .init = auxmac_init, + .data = &auxmac_data, + }, + { + .init = tpacpi_hwdd_init, + .data = &hwdd_driver_data, + }, +}; + +static int __init set_ibm_param(const char *val, const struct kernel_param *kp) +{ + unsigned int i; + struct ibm_struct *ibm; + + if (!kp || !kp->name || !val) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ibm = ibms_init[i].data; + if (!ibm || !ibm->name) + continue; + + if (strcmp(ibm->name, kp->name) == 0 && ibm->write) { + if (strlen(val) > sizeof(ibms_init[i].param) - 1) + return -ENOSPC; + strscpy(ibms_init[i].param, val); + return 0; + } + } + + return -EINVAL; +} + +module_param(experimental, int, 0444); +MODULE_PARM_DESC(experimental, + "Enables experimental features when non-zero"); + +module_param_named(debug, dbg_level, uint, 0); +MODULE_PARM_DESC(debug, "Sets debug level bit-mask"); + +module_param(force_load, bool, 0444); +MODULE_PARM_DESC(force_load, + "Attempts to load the driver even on a mis-identified ThinkPad when true"); + +module_param_named(fan_control, fan_control_allowed, bool, 0444); +MODULE_PARM_DESC(fan_control, + "Enables setting fan parameters features when true"); + +module_param_named(brightness_mode, brightness_mode, uint, 0444); +MODULE_PARM_DESC(brightness_mode, + "Selects brightness control strategy: 0=auto, 1=EC, 2=UCMS, 3=EC+NVRAM"); + +module_param(brightness_enable, uint, 0444); +MODULE_PARM_DESC(brightness_enable, + "Enables backlight control when 1, disables when 0"); + +#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT +module_param_named(volume_mode, volume_mode, uint, 0444); +MODULE_PARM_DESC(volume_mode, + "Selects volume control strategy: 0=auto, 1=EC, 2=N/A, 3=EC+NVRAM"); + +module_param_named(volume_capabilities, volume_capabilities, uint, 0444); +MODULE_PARM_DESC(volume_capabilities, + "Selects the mixer capabilities: 0=auto, 1=volume and mute, 2=mute only"); + +module_param_named(volume_control, volume_control_allowed, bool, 0444); +MODULE_PARM_DESC(volume_control, + "Enables software override for the console audio control when true"); + +module_param_named(software_mute, software_mute_requested, bool, 0444); +MODULE_PARM_DESC(software_mute, + "Request full software mute control"); + +/* ALSA module API parameters */ +module_param_named(index, alsa_index, int, 0444); +MODULE_PARM_DESC(index, "ALSA index for the ACPI EC Mixer"); +module_param_named(id, alsa_id, charp, 0444); +MODULE_PARM_DESC(id, "ALSA id for the ACPI EC Mixer"); +module_param_named(enable, alsa_enable, bool, 0444); +MODULE_PARM_DESC(enable, "Enable the ALSA interface for the ACPI EC Mixer"); +#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +/* The module parameter can't be read back, that's why 0 is used here */ +#define TPACPI_PARAM(feature) \ + module_param_call(feature, set_ibm_param, NULL, NULL, 0); \ + MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command at module load, see documentation") + +TPACPI_PARAM(hotkey); +TPACPI_PARAM(bluetooth); +TPACPI_PARAM(video); +TPACPI_PARAM(light); +TPACPI_PARAM(cmos); +TPACPI_PARAM(led); +TPACPI_PARAM(beep); +TPACPI_PARAM(brightness); +TPACPI_PARAM(volume); +TPACPI_PARAM(fan); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +module_param(dbg_wlswemul, uint, 0444); +MODULE_PARM_DESC(dbg_wlswemul, "Enables WLSW emulation"); +module_param_named(wlsw_state, tpacpi_wlsw_emulstate, bool, 0); +MODULE_PARM_DESC(wlsw_state, + "Initial state of the emulated WLSW switch"); + +module_param(dbg_bluetoothemul, uint, 0444); +MODULE_PARM_DESC(dbg_bluetoothemul, "Enables bluetooth switch emulation"); +module_param_named(bluetooth_state, tpacpi_bluetooth_emulstate, bool, 0); +MODULE_PARM_DESC(bluetooth_state, + "Initial state of the emulated bluetooth switch"); + +module_param(dbg_wwanemul, uint, 0444); +MODULE_PARM_DESC(dbg_wwanemul, "Enables WWAN switch emulation"); +module_param_named(wwan_state, tpacpi_wwan_emulstate, bool, 0); +MODULE_PARM_DESC(wwan_state, + "Initial state of the emulated WWAN switch"); + +module_param(dbg_uwbemul, uint, 0444); +MODULE_PARM_DESC(dbg_uwbemul, "Enables UWB switch emulation"); +module_param_named(uwb_state, tpacpi_uwb_emulstate, bool, 0); +MODULE_PARM_DESC(uwb_state, + "Initial state of the emulated UWB switch"); +#endif + +module_param(profile_force, int, 0444); +MODULE_PARM_DESC(profile_force, "Force profile mode. -1=off, 1=MMC, 2=PSC"); + +static void thinkpad_acpi_module_exit(void) +{ + tpacpi_lifecycle = TPACPI_LIFE_EXITING; + + if (tpacpi_sensors_pdev) { + platform_driver_unregister(&tpacpi_hwmon_pdriver); + platform_device_unregister(tpacpi_sensors_pdev); + } + + if (tp_features.platform_drv_registered) + platform_driver_unregister(&tpacpi_pdriver); + if (tpacpi_pdev) + platform_device_unregister(tpacpi_pdev); + + if (proc_dir) + remove_proc_entry(TPACPI_PROC_DIR, acpi_root_dir); + if (tpacpi_wq) + destroy_workqueue(tpacpi_wq); + if (tpacpi_dbg) + tpacpi_debugfs_remove(); + + kfree(thinkpad_id.bios_version_str); + kfree(thinkpad_id.ec_version_str); + kfree(thinkpad_id.model_str); + kfree(thinkpad_id.nummodel_str); +} + +static void tpacpi_subdrivers_release(void *data) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe_reverse(ibm, itmp, &tpacpi_all_drivers, all_drivers) + ibm_exit(ibm); + + dbg_printk(TPACPI_DBG_INIT, "finished subdriver exit path...\n"); +} + +static int __init tpacpi_pdriver_probe(struct platform_device *pdev) +{ + int ret; + + ret = devm_mutex_init(&pdev->dev, &tpacpi_inputdev_send_mutex); + if (ret) + return ret; + + tpacpi_inputdev = devm_input_allocate_device(&pdev->dev); + if (!tpacpi_inputdev) + return -ENOMEM; + + tpacpi_inputdev->name = "ThinkPad Extra Buttons"; + tpacpi_inputdev->phys = TPACPI_DRVR_NAME "/input0"; + tpacpi_inputdev->id.bustype = BUS_HOST; + tpacpi_inputdev->id.vendor = thinkpad_id.vendor; + tpacpi_inputdev->id.product = TPACPI_HKEY_INPUT_PRODUCT; + tpacpi_inputdev->id.version = TPACPI_HKEY_INPUT_VERSION; + tpacpi_inputdev->dev.parent = &tpacpi_pdev->dev; + + /* Init subdriver dependencies */ + tpacpi_detect_brightness_capabilities(); + + /* Init subdrivers */ + for (unsigned int i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ret = ibm_init(&ibms_init[i]); + if (ret >= 0 && *ibms_init[i].param) + ret = ibms_init[i].data->write(ibms_init[i].param); + if (ret < 0) { + tpacpi_subdrivers_release(NULL); + return ret; + } + } + + ret = devm_add_action_or_reset(&pdev->dev, tpacpi_subdrivers_release, NULL); + if (ret) + return ret; + + ret = input_register_device(tpacpi_inputdev); + if (ret < 0) + pr_err("unable to register input device\n"); + + return ret; +} + +static int __init tpacpi_hwmon_pdriver_probe(struct platform_device *pdev) +{ + tpacpi_hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, TPACPI_NAME, + NULL, tpacpi_hwmon_groups); + if (IS_ERR(tpacpi_hwmon)) + pr_err("unable to register hwmon device\n"); + + return PTR_ERR_OR_ZERO(tpacpi_hwmon); +} + +static int __init thinkpad_acpi_module_init(void) +{ + const struct dmi_system_id *dmi_id; + int ret; + acpi_object_type obj_type; + + tpacpi_lifecycle = TPACPI_LIFE_INIT; + + /* Driver-level probe */ + + ret = get_thinkpad_model_data(&thinkpad_id); + if (ret) { + pr_err("unable to get DMI data: %d\n", ret); + thinkpad_acpi_module_exit(); + return ret; + } + ret = probe_for_thinkpad(); + if (ret) { + thinkpad_acpi_module_exit(); + return ret; + } + + /* Driver initialization */ + + thinkpad_acpi_init_banner(); + tpacpi_check_outdated_fw(); + + TPACPI_ACPIHANDLE_INIT(ecrd); + TPACPI_ACPIHANDLE_INIT(ecwr); + + /* + * Quirk: in some models (e.g. X380 Yoga), an object named ECRD + * exists, but it is a register, not a method. + */ + if (ecrd_handle) { + acpi_get_type(ecrd_handle, &obj_type); + if (obj_type != ACPI_TYPE_METHOD) + ecrd_handle = NULL; + } + if (ecwr_handle) { + acpi_get_type(ecwr_handle, &obj_type); + if (obj_type != ACPI_TYPE_METHOD) + ecwr_handle = NULL; + } + + tpacpi_wq = create_singlethread_workqueue(TPACPI_WORKQUEUE_NAME); + if (!tpacpi_wq) { + thinkpad_acpi_module_exit(); + return -ENOMEM; + } + + proc_dir = proc_mkdir(TPACPI_PROC_DIR, acpi_root_dir); + if (!proc_dir) { + pr_err("unable to create proc dir " TPACPI_PROC_DIR "\n"); + thinkpad_acpi_module_exit(); + return -ENODEV; + } + + tpacpi_debugfs_init(); + + dmi_id = dmi_first_match(fwbug_list); + if (dmi_id) + tp_features.quirks = dmi_id->driver_data; + + /* Device initialization */ + tpacpi_pdev = platform_device_register_simple(TPACPI_DRVR_NAME, PLATFORM_DEVID_NONE, + NULL, 0); + if (IS_ERR(tpacpi_pdev)) { + ret = PTR_ERR(tpacpi_pdev); + tpacpi_pdev = NULL; + pr_err("unable to register platform device\n"); + thinkpad_acpi_module_exit(); + return ret; + } + + ret = platform_driver_probe(&tpacpi_pdriver, tpacpi_pdriver_probe); + if (ret) { + pr_err("unable to register main platform driver\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.platform_drv_registered = 1; + + tpacpi_sensors_pdev = platform_create_bundle(&tpacpi_hwmon_pdriver, + tpacpi_hwmon_pdriver_probe, + NULL, 0, NULL, 0); + if (IS_ERR(tpacpi_sensors_pdev)) { + ret = PTR_ERR(tpacpi_sensors_pdev); + tpacpi_sensors_pdev = NULL; + pr_err("unable to register hwmon platform device/driver bundle\n"); + thinkpad_acpi_module_exit(); + return ret; + } + + tpacpi_lifecycle = TPACPI_LIFE_RUNNING; + + return 0; +} + +MODULE_ALIAS(TPACPI_DRVR_SHORTNAME); + +/* + * This will autoload the driver in almost every ThinkPad + * in widespread use. + * + * Only _VERY_ old models, like the 240, 240x and 570 lack + * the HKEY event interface. + */ +MODULE_DEVICE_TABLE(acpi, ibm_htk_device_ids); + +/* + * DMI matching for module autoloading + * + * See https://thinkwiki.org/wiki/List_of_DMI_IDs + * See https://thinkwiki.org/wiki/BIOS_Upgrade_Downloads + * + * Only models listed in thinkwiki will be supported, so add yours + * if it is not there yet. + */ +#define IBM_BIOS_MODULE_ALIAS(__type) \ + MODULE_ALIAS("dmi:bvnIBM:bvr" __type "ET??WW*") + +/* Ancient thinkpad BIOSes have to be identified by + * BIOS type or model number, and there are far less + * BIOS types than model numbers... */ +IBM_BIOS_MODULE_ALIAS("I[MU]"); /* 570, 570e */ + +MODULE_AUTHOR("Borislav Deianov "); +MODULE_AUTHOR("Henrique de Moraes Holschuh "); +MODULE_DESCRIPTION(TPACPI_DESC); +MODULE_VERSION(TPACPI_VERSION); +MODULE_LICENSE("GPL"); + +module_init(thinkpad_acpi_module_init); +module_exit(thinkpad_acpi_module_exit); diff --git a/drivers/platform/x86/lenovo/wmi-camera.c b/drivers/platform/x86/lenovo/wmi-camera.c new file mode 100644 index 000000000..89ecbce60 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-camera.c @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Lenovo WMI Camera Button Driver + * + * Author: Ai Chao + * Copyright (C) 2024 KylinSoft Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define WMI_LENOVO_CAMERABUTTON_EVENT_GUID "50C76F1F-D8E4-D895-0A3D-62F4EA400013" + +struct lenovo_wmi_priv { + struct input_dev *idev; + struct mutex notify_lock; /* lenovo WMI camera button notify lock */ +}; + +enum { + SW_CAMERA_OFF = 0, + SW_CAMERA_ON = 1, +}; + +static int camera_shutter_input_setup(struct wmi_device *wdev, u8 camera_mode) +{ + struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + int err; + + priv->idev = input_allocate_device(); + if (!priv->idev) + return -ENOMEM; + + priv->idev->name = "Lenovo WMI Camera Button"; + priv->idev->phys = "wmi/input0"; + priv->idev->id.bustype = BUS_HOST; + priv->idev->dev.parent = &wdev->dev; + + input_set_capability(priv->idev, EV_SW, SW_CAMERA_LENS_COVER); + + input_report_switch(priv->idev, SW_CAMERA_LENS_COVER, + camera_mode == SW_CAMERA_ON ? 0 : 1); + input_sync(priv->idev); + + err = input_register_device(priv->idev); + if (err) { + input_free_device(priv->idev); + priv->idev = NULL; + } + + return err; +} + +static void lenovo_wmi_notify(struct wmi_device *wdev, union acpi_object *obj) +{ + struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + u8 camera_mode; + + if (obj->type != ACPI_TYPE_BUFFER) { + dev_err(&wdev->dev, "Bad response type %u\n", obj->type); + return; + } + + if (obj->buffer.length != 1) { + dev_err(&wdev->dev, "Invalid buffer length %u\n", obj->buffer.length); + return; + } + + /* + * obj->buffer.pointer[0] is camera mode: + * 0 camera close + * 1 camera open + */ + camera_mode = obj->buffer.pointer[0]; + if (camera_mode > SW_CAMERA_ON) { + dev_err(&wdev->dev, "Unknown camera mode %u\n", camera_mode); + return; + } + + guard(mutex)(&priv->notify_lock); + + if (!priv->idev) { + if (camera_shutter_input_setup(wdev, camera_mode)) + dev_warn(&wdev->dev, "Failed to register input device\n"); + return; + } + + if (camera_mode == SW_CAMERA_ON) + input_report_switch(priv->idev, SW_CAMERA_LENS_COVER, 0); + else + input_report_switch(priv->idev, SW_CAMERA_LENS_COVER, 1); + input_sync(priv->idev); +} + +static int lenovo_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct lenovo_wmi_priv *priv; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, priv); + + mutex_init(&priv->notify_lock); + + return 0; +} + +static void lenovo_wmi_remove(struct wmi_device *wdev) +{ + struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + + if (priv->idev) + input_unregister_device(priv->idev); + + mutex_destroy(&priv->notify_lock); +} + +static const struct wmi_device_id lenovo_wmi_id_table[] = { + { .guid_string = WMI_LENOVO_CAMERABUTTON_EVENT_GUID }, + { } +}; +MODULE_DEVICE_TABLE(wmi, lenovo_wmi_id_table); + +static struct wmi_driver lenovo_wmi_driver = { + .driver = { + .name = "lenovo-wmi-camera", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lenovo_wmi_id_table, + .min_event_size = sizeof(u8), + .no_singleton = true, + .probe = lenovo_wmi_probe, + .notify = lenovo_wmi_notify, + .remove = lenovo_wmi_remove, +}; +module_wmi_driver(lenovo_wmi_driver); + +MODULE_AUTHOR("Ai Chao "); +MODULE_DESCRIPTION("Lenovo WMI Camera Button Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c new file mode 100644 index 000000000..d5e961566 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata.c @@ -0,0 +1,948 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo Capability Data WMI Data Block driver. + * + * Lenovo Capability Data provides information on tunable attributes used by + * the "Other Mode" WMI interface. + * + * Capability Data 00 includes if the attribute is supported by the hardware, + * and the default_value. All attributes are independent of thermal modes. + * + * Capability Data 01 includes if the attribute is supported by the hardware, + * and the default_value, max_value, min_value, and step increment. Each + * attribute has multiple pages, one for each of the thermal modes managed by + * the Gamezone interface. + * + * Fan Test Data includes the max/min fan speed RPM for each fan. This is + * reference data for self-test. If the fan is in good condition, it is capable + * to spin faster than max RPM or slower than min RPM. + * + * Copyright (C) 2025 Derek J. Clark + * - Initial implementation (formerly named lenovo-wmi-capdata01) + * + * Copyright (C) 2025 Rong Zhang + * - Unified implementation + */ + +#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 +#include + +#include "wmi-capdata.h" +#include "wmi-helpers.h" + +#define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E" +#define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154" +#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21" + +#define ACPI_AC_NOTIFY_STATUS 0x80 + +#define LWMI_FEATURE_ID_FAN_TEST 0x05 + +#define LWMI_ATTR_ID_FAN_TEST \ + lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_TEST, \ + LWMI_GZ_THERMAL_MODE_NONE, LWMI_TYPE_ID_NONE) + +enum lwmi_cd_type { + LENOVO_CAPABILITY_DATA_00, + LENOVO_CAPABILITY_DATA_01, + LENOVO_FAN_TEST_DATA, + CD_TYPE_NONE = -1, +}; + +#define LWMI_CD_TABLE_ITEM(_type) \ + [_type] = { \ + .name = #_type, \ + .type = _type, \ + } + +static const struct lwmi_cd_info { + const char *name; + enum lwmi_cd_type type; +} lwmi_cd_table[] = { + LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00), + LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01), + LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA), +}; + +struct lwmi_cd_priv { + struct notifier_block acpi_nb; /* ACPI events */ + struct wmi_device *wdev; + struct cd_list *list; + struct dentry *debugfs_dir; + + /* + * A capdata device may be a component master of another capdata device. + * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan + * |- master |- component + * |- sub-master + * |- sub-component + */ + struct lwmi_cd_sub_master_priv { + struct device *master_dev; + cd_list_cb_t master_cb; + struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */ + bool registered; /* Has the sub-master been registered? */ + } *sub_master; +}; + +struct cd_list { + struct mutex list_mutex; /* list R/W mutex */ + enum lwmi_cd_type type; + u8 count; + + union { + DECLARE_FLEX_ARRAY(struct capdata00, cd00); + DECLARE_FLEX_ARRAY(struct capdata01, cd01); + DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan); + }; +}; + +static struct wmi_driver lwmi_cd_driver; + +/* ======== Device components ======== */ + +/** + * lwmi_cd_match() - Match rule for the master driver. + * @dev: Pointer to the capability data parent device. + * @type: Pointer to capability data type (enum lwmi_cd_type *) to match. + * + * Return: int. + */ +static int lwmi_cd_match(struct device *dev, void *type) +{ + struct lwmi_cd_priv *priv; + + if (dev->driver != &lwmi_cd_driver.driver) + return false; + + priv = dev_get_drvdata(dev); + return priv->list->type == *(enum lwmi_cd_type *)type; +} + +/** + * lwmi_cd_match_add_all() - Add all match rule for the master driver. + * @master: Pointer to the master device. + * @matchptr: Pointer to the returned component_match pointer. + * + * Adds all component matches to the list stored in @matchptr for the @master + * device. @matchptr must be initialized to NULL. + */ +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr) +{ + int i; + + if (WARN_ON(*matchptr)) + return; + + for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) { + /* Skip sub-components. */ + if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA) + continue; + + component_match_add(master, matchptr, lwmi_cd_match, + (void *)&lwmi_cd_table[i].type); + if (IS_ERR(*matchptr)) + return; + } +} +EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CAPDATA"); + +/** + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component. + * @priv: Pointer to the capability data private data. + * + * Call the master callback and pass the sub-component list to it if the + * dependency chain (master <-> sub-master <-> sub-component) is complete. + */ +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv) +{ + struct cd_list *sub_component_list = priv->sub_master->sub_component_list; + + /* + * Call the callback only if the dependency chain is ready: + * - Binding between master and sub-master: fills master_dev and master_cb + * - Binding between sub-master and sub-component: fills sub_component_list + * + * If a binding has been unbound before the other binding is bound, the + * corresponding members filled by the former are guaranteed to be cleared. + * + * This function is only called in bind callbacks, and the component + * framework guarantees bind/unbind callbacks may never execute + * simultaneously, which implies that it's impossible to have a race + * condition. + * + * Hence, this check is sufficient to ensure that the callback is called + * at most once and with the correct state, without relying on a specific + * sequence of binding establishment. + */ + if (!sub_component_list || + !priv->sub_master->master_dev || + !priv->sub_master->master_cb) + return; + + if (PTR_ERR(sub_component_list) == -ENODEV) + sub_component_list = NULL; + else if (WARN_ON(IS_ERR(sub_component_list))) + return; + + priv->sub_master->master_cb(priv->sub_master->master_dev, + sub_component_list); + + /* + * Userspace may unbind a device from its driver and bind it again + * through sysfs. Let's call this operation "reprobe" to distinguish it + * from component "rebind". + * + * When reprobing capdata00/01 or the master device, the master device + * is unbound from us with appropriate cleanup before we bind to it and + * call master_cb. Everything is fine in this case. + * + * When reprobing capdata_fan, the master device has never been unbound + * from us (hence no cleanup is done)[1], but we call master_cb the + * second time. To solve this issue, we clear master_cb and master_dev + * so we won't call master_cb twice while a binding is still complete. + * + * Note that we can't clear sub_component_list, otherwise reprobing + * capdata01 or the master device causes master_cb to be never called + * after we rebind to the master device. + * + * [1]: The master device does not need capdata_fan in run time, so + * losing capdata_fan will not break the binding to the master device. + */ + priv->sub_master->master_cb = NULL; + priv->sub_master->master_dev = NULL; +} + +/** + * lwmi_cd_component_bind() - Bind component to master device. + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device. + * @om_dev: Pointer to the lenovo-wmi-other driver parent device. + * @data: lwmi_cd_binder object pointer used to return the capability data. + * + * On lenovo-wmi-other's master bind, provide a pointer to the local capdata + * list. This is used to call lwmi_cd*_get_data to look up attribute data + * from the lenovo-wmi-other driver. + * + * If cd_dev is a sub-master, try to call the master callback. + * + * Return: 0 + */ +static int lwmi_cd_component_bind(struct device *cd_dev, + struct device *om_dev, void *data) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev); + struct lwmi_cd_binder *binder = data; + + switch (priv->list->type) { + case LENOVO_CAPABILITY_DATA_00: + binder->cd00_list = priv->list; + + priv->sub_master->master_dev = om_dev; + priv->sub_master->master_cb = binder->cd_fan_list_cb; + lwmi_cd_call_master_cb(priv); + + break; + case LENOVO_CAPABILITY_DATA_01: + binder->cd01_list = priv->list; + break; + default: + return -EINVAL; + } + + return 0; +} + +/** + * lwmi_cd_component_unbind() - Unbind component to master device. + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device. + * @om_dev: Pointer to the lenovo-wmi-other driver parent device. + * @data: Unused. + * + * If cd_dev is a sub-master, clear the collected data from the master device to + * prevent the binding establishment between the sub-master and the sub- + * component (if it's about to happen) from calling the master callback. + */ +static void lwmi_cd_component_unbind(struct device *cd_dev, + struct device *om_dev, void *data) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev); + + switch (priv->list->type) { + case LENOVO_CAPABILITY_DATA_00: + priv->sub_master->master_dev = NULL; + priv->sub_master->master_cb = NULL; + return; + default: + return; + } +} + +static const struct component_ops lwmi_cd_component_ops = { + .bind = lwmi_cd_component_bind, + .unbind = lwmi_cd_component_unbind, +}; + +/** + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device + * @dev: The sub-master capdata basic device. + * + * Call component_bind_all to bind the sub-component device to the sub-master + * device. On success, collect the pointer to the sub-component list and try + * to call the master callback. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_cd_sub_master_bind(struct device *dev) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(dev); + struct cd_list *sub_component_list; + int ret; + + ret = component_bind_all(dev, &sub_component_list); + if (ret) + return ret; + + priv->sub_master->sub_component_list = sub_component_list; + lwmi_cd_call_master_cb(priv); + + return 0; +} + +/** + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device + * @dev: The sub-master capdata basic device + * + * Clear the collected pointer to the sub-component list to prevent the binding + * establishment between the sub-master and the sub-component (if it's about to + * happen) from calling the master callback. Then, call component_unbind_all to + * unbind the sub-component device from the sub-master device. + */ +static void lwmi_cd_sub_master_unbind(struct device *dev) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(dev); + + priv->sub_master->sub_component_list = NULL; + + component_unbind_all(dev, NULL); +} + +static const struct component_master_ops lwmi_cd_sub_master_ops = { + .bind = lwmi_cd_sub_master_bind, + .unbind = lwmi_cd_sub_master_unbind, +}; + +/** + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component + * @priv: Pointer to the sub-master capdata device private data. + * @sub_component_type: Type of the sub-component. + * + * Match the sub-component type and register the current capdata device as a + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub- + * component as non-existent without registering sub-master. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv, + enum lwmi_cd_type sub_component_type) +{ + struct component_match *master_match = NULL; + int ret; + + priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL); + if (!priv->sub_master) + return -ENOMEM; + + if (sub_component_type == CD_TYPE_NONE) { + /* The master callback will be called with NULL on bind. */ + priv->sub_master->sub_component_list = ERR_PTR(-ENODEV); + priv->sub_master->registered = false; + return 0; + } + + /* + * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack + * data cannot be used here. Steal one from lwmi_cd_table. + */ + component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match, + (void *)&lwmi_cd_table[sub_component_type].type); + if (IS_ERR(master_match)) + return PTR_ERR(master_match); + + ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops, + master_match); + if (ret) + return ret; + + priv->sub_master->registered = true; + return 0; +} + +/** + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered + * @priv: Pointer to the sub-master capdata device private data. + */ +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv) +{ + if (!priv->sub_master->registered) + return; + + component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops); + priv->sub_master->registered = false; +} + +/** + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device. + * @sc_dev: Pointer to the sub-component capdata parent device. + * @sm_dev: Pointer to the sub-master capdata parent device. + * @data: Pointer used to return the capability data list pointer. + * + * On sub-master's bind, provide a pointer to the local capdata list. + * This is used by the sub-master to call the master callback. + * + * Return: 0 + */ +static int lwmi_cd_sub_component_bind(struct device *sc_dev, + struct device *sm_dev, void *data) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev); + struct cd_list **listp = data; + + *listp = priv->list; + + return 0; +} + +static const struct component_ops lwmi_cd_sub_component_ops = { + .bind = lwmi_cd_sub_component_bind, +}; + +/* + * lwmi_cd*_get_data - Get the data of the specified attribute + * @list: The lenovo-wmi-capdata pointer to its cd_list struct. + * @attribute_id: The capdata attribute ID to be found. + * @output: Pointer to a capdata* struct to return the data. + * + * Retrieves the capability data struct pointer for the given + * attribute. + * + * Return: 0 on success, or -EINVAL. + */ +#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t) \ + int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output) \ + { \ + u8 idx; \ + \ + if (WARN_ON(list->type != _cd_type)) \ + return -EINVAL; \ + \ + guard(mutex)(&list->list_mutex); \ + for (idx = 0; idx < list->count; idx++) { \ + if (list->_cdxx[idx].id != attribute_id) \ + continue; \ + memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx])); \ + return 0; \ + } \ + return -EINVAL; \ + } + +DEF_LWMI_CDXX_GET_DATA(cd00, LENOVO_CAPABILITY_DATA_00, struct capdata00); +EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CAPDATA"); + +DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01); +EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CAPDATA"); + +DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan); +EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CAPDATA"); + +/* ======== debugfs ======== */ + +/** + * lwmi_cd00_show() - Dump capdata00 + * @s: Pointer to seq_file where the capdata00 is dumped. + * @cd00: Pointer to a capdata00 struct to be dumped. + */ +static void lwmi_cd00_show(struct seq_file *s, struct capdata00 *cd00) +{ + u8 dev = FIELD_GET(LWMI_ATTR_DEV_ID_MASK, cd00->id); + u8 feat = FIELD_GET(LWMI_ATTR_FEAT_ID_MASK, cd00->id); + u8 mode = FIELD_GET(LWMI_ATTR_MODE_ID_MASK, cd00->id); + u8 type = FIELD_GET(LWMI_ATTR_TYPE_ID_MASK, cd00->id); + bool extra = cd00->supported & ~(LWMI_SUPP_GET | LWMI_SUPP_SET | LWMI_SUPP_VALID); + bool get = cd00->supported & LWMI_SUPP_GET; + bool set = cd00->supported & LWMI_SUPP_SET; + bool valid = cd00->supported & LWMI_SUPP_VALID; + + seq_printf(s, " id: 0x%08x [dev: %2u, feat: %2u, mode: %2u, type: %2u]\n", + cd00->id, dev, feat, mode, type); + + seq_printf(s, " supported: 0x%08x [%c%c%c%c]\n", cd00->supported, + extra ? '+' : ' ', + get ? 'R' : ' ', + set ? 'W' : ' ', + valid ? 'V' : ' '); + + seq_printf(s, " default_value: %u\n", cd00->default_value); +} + +/** + * lwmi_cd01_show() - Dump capdata01 + * @s: Pointer to seq_file where the capdata01 is dumped. + * @cd01: Pointer to a capdata01 struct to be dumped. + */ +static void lwmi_cd01_show(struct seq_file *s, struct capdata01 *cd01) +{ + /* capdata01 is an extension to capdata00. */ + lwmi_cd00_show(s, &cd01->cd00); + + seq_printf(s, " step: %u\n", cd01->step); + seq_printf(s, " min_value: %u\n", cd01->min_value); + seq_printf(s, " max_value: %u\n", cd01->max_value); +} + +/** + * lwmi_cd_fan_show() - Dump capdata_fan + * @s: Pointer to seq_file where the capdata_fan is dumped. + * @cd_fan: Pointer to a capdata_fan struct to be dumped. + */ +static void lwmi_cd_fan_show(struct seq_file *s, struct capdata_fan *cd_fan) +{ + seq_printf(s, " id: %u\n", cd_fan->id); + seq_printf(s, " min_rpm: %u\n", cd_fan->min_rpm); + seq_printf(s, " max_rpm: %u\n", cd_fan->max_rpm); +} + +/** + * lwmi_cd_debugfs_show() - Dump capability data to debugfs + * @s: Pointer to seq_file where the capability data is dumped. + * @data: unused. + * + * Return: 0 + */ +static int lwmi_cd_debugfs_show(struct seq_file *s, void *data) +{ + struct lwmi_cd_priv *priv = s->private; + u8 idx; + + guard(mutex)(&priv->list->list_mutex); + + /* lwmi_cd_alloc() ensured priv->list->type must be a valid type. */ + for (idx = 0; idx < priv->list->count; idx++) { + seq_printf(s, "%s[%u]:\n", lwmi_cd_table[priv->list->type].name, idx); + + if (priv->list->type == LENOVO_CAPABILITY_DATA_00) + lwmi_cd00_show(s, &priv->list->cd00[idx]); + else if (priv->list->type == LENOVO_CAPABILITY_DATA_01) + lwmi_cd01_show(s, &priv->list->cd01[idx]); + else if (priv->list->type == LENOVO_FAN_TEST_DATA) + lwmi_cd_fan_show(s, &priv->list->cd_fan[idx]); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(lwmi_cd_debugfs); + +/** + * lwmi_cd_debugfs_add() - Create debugfs directory and files for a device + * @priv: lenovo-wmi-capdata driver data. + */ +static void lwmi_cd_debugfs_add(struct lwmi_cd_priv *priv) +{ + priv->debugfs_dir = lwmi_debugfs_create_dir(priv->wdev); + + debugfs_create_file("capdata", 0444, priv->debugfs_dir, priv, &lwmi_cd_debugfs_fops); +} + +/** + * lwmi_cd_debugfs_remove() - Remove debugfs directory for a device + * @priv: lenovo-wmi-capdata driver data. + */ +static void lwmi_cd_debugfs_remove(struct lwmi_cd_priv *priv) +{ + debugfs_remove_recursive(priv->debugfs_dir); + priv->debugfs_dir = NULL; +} + +/* ======== WMI interface ======== */ + +/** + * lwmi_cd_cache() - Cache all WMI data block information + * @priv: lenovo-wmi-capdata driver data. + * + * Loop through each WMI data block and cache the data. + * + * Return: 0 on success, or an error. + */ +static int lwmi_cd_cache(struct lwmi_cd_priv *priv) +{ + size_t size; + int idx; + void *p; + + switch (priv->list->type) { + case LENOVO_CAPABILITY_DATA_00: + p = &priv->list->cd00[0]; + size = sizeof(priv->list->cd00[0]); + break; + case LENOVO_CAPABILITY_DATA_01: + p = &priv->list->cd01[0]; + size = sizeof(priv->list->cd01[0]); + break; + case LENOVO_FAN_TEST_DATA: + /* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */ + return 0; + default: + return -EINVAL; + } + + guard(mutex)(&priv->list->list_mutex); + for (idx = 0; idx < priv->list->count; idx++, p += size) { + union acpi_object *ret_obj __free(kfree) = NULL; + + ret_obj = wmidev_block_query(priv->wdev, idx); + if (!ret_obj) + return -ENODEV; + + if (ret_obj->type != ACPI_TYPE_BUFFER || + ret_obj->buffer.length < size) + continue; + + memcpy(p, ret_obj->buffer.pointer, size); + } + + return 0; +} + +/** + * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list + * @priv: lenovo-wmi-capdata driver data. + * @listptr: Pointer to returned cd_list pointer. + * + * Return: count of fans found, or an error. + */ +static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr) +{ + struct cd_list *list; + size_t size; + u32 count; + int idx; + + /* Emit unaligned access to u8 buffer with __packed. */ + struct cd_fan_block { + u32 nr; + u32 data[]; /* id[nr], max_rpm[nr], min_rpm[nr] */ + } __packed * block; + + union acpi_object *ret_obj __free(kfree) = wmidev_block_query(priv->wdev, 0); + if (!ret_obj) + return -ENODEV; + + if (ret_obj->type == ACPI_TYPE_BUFFER) { + block = (struct cd_fan_block *)ret_obj->buffer.pointer; + size = ret_obj->buffer.length; + + count = size >= sizeof(*block) ? block->nr : 0; + if (size < struct_size(block, data, count * 3)) { + dev_warn(&priv->wdev->dev, + "incomplete fan test data block: %zu < %zu, ignoring\n", + size, struct_size(block, data, count * 3)); + count = 0; + } else if (count > U8_MAX) { + dev_warn(&priv->wdev->dev, + "too many fans reported: %u > %u, truncating\n", + count, U8_MAX); + count = U8_MAX; + } + } else { + /* + * This is usually caused by a dummy ACPI method. Do not return an error + * as failing to probe this device will result in sub-master device being + * unbound. This behavior aligns with lwmi_cd_cache(). + */ + count = 0; + } + + list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL); + if (!list) + return -ENOMEM; + + for (idx = 0; idx < count; idx++) { + /* Do not calculate array index using count, as it may be truncated. */ + list->cd_fan[idx] = (struct capdata_fan) { + .id = block->data[idx], + .max_rpm = block->data[idx + block->nr], + .min_rpm = block->data[idx + (2 * block->nr)], + }; + } + + *listptr = list; + return count; +} + +/** + * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata + * @priv: lenovo-wmi-capdata driver data. + * @type: The type of capability data. + * + * Allocate a cd_list struct large enough to contain data from all WMI data + * blocks provided by the interface. + * + * Return: 0 on success, or an error. + */ +static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type) +{ + struct cd_list *list; + size_t list_size; + int count, ret; + + count = wmidev_instance_count(priv->wdev); + + switch (type) { + case LENOVO_CAPABILITY_DATA_00: + list_size = struct_size(list, cd00, count); + break; + case LENOVO_CAPABILITY_DATA_01: + list_size = struct_size(list, cd01, count); + break; + case LENOVO_FAN_TEST_DATA: + count = lwmi_cd_fan_list_alloc_cache(priv, &list); + if (count < 0) + return count; + + goto got_list; + default: + return -EINVAL; + } + + list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL); + if (!list) + return -ENOMEM; + +got_list: + ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex); + if (ret) + return ret; + + list->type = type; + list->count = count; + priv->list = list; + + return 0; +} + +/** + * lwmi_cd_setup() - Cache all WMI data block information + * @priv: lenovo-wmi-capdata driver data. + * @type: The type of capability data. + * + * Allocate a cd_list struct large enough to contain data from all WMI data + * blocks provided by the interface. Then loop through each data block and + * cache the data. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type) +{ + int ret; + + ret = lwmi_cd_alloc(priv, type); + if (ret) + return ret; + + return lwmi_cd_cache(priv); +} + +/** + * lwmi_cd01_notifier_call() - Call method for cd01 notifier. + * block call chain. + * @nb: The notifier_block registered to lenovo-wmi-events driver. + * @action: Unused. + * @data: The ACPI event. + * + * For LWMI_EVENT_THERMAL_MODE, set current_mode and notify platform_profile + * of a change. + * + * Return: notifier_block status. + */ +static int lwmi_cd01_notifier_call(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct acpi_bus_event *event = data; + struct lwmi_cd_priv *priv; + int ret; + + if (strcmp(event->device_class, ACPI_AC_CLASS) != 0) + return NOTIFY_DONE; + + priv = container_of(nb, struct lwmi_cd_priv, acpi_nb); + + switch (event->type) { + case ACPI_AC_NOTIFY_STATUS: + ret = lwmi_cd_cache(priv); + if (ret) + return NOTIFY_BAD; + + return NOTIFY_OK; + default: + return NOTIFY_DONE; + } +} + +/** + * lwmi_cd01_unregister() - Unregister the cd01 ACPI notifier_block. + * @data: The ACPI event notifier_block to unregister. + */ +static void lwmi_cd01_unregister(void *data) +{ + struct notifier_block *acpi_nb = data; + + unregister_acpi_notifier(acpi_nb); +} + +static int lwmi_cd_probe(struct wmi_device *wdev, const void *context) +{ + const struct lwmi_cd_info *info = context; + struct lwmi_cd_priv *priv; + int ret; + + if (!info) + return -EINVAL; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + ret = lwmi_cd_setup(priv, info->type); + if (ret) + goto out; + + switch (info->type) { + case LENOVO_CAPABILITY_DATA_00: { + enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA; + struct capdata00 capdata00; + + ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00); + if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) { + dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n"); + sub_component_type = CD_TYPE_NONE; + } + + /* Sub-master (capdata00) <-> sub-component (capdata_fan) */ + ret = lwmi_cd_sub_master_add(priv, sub_component_type); + if (ret) + goto out; + + /* Master (lenovo-wmi-other) <-> sub-master (capdata00) */ + ret = component_add(&wdev->dev, &lwmi_cd_component_ops); + if (ret) + lwmi_cd_sub_master_del(priv); + + goto out; + } + case LENOVO_CAPABILITY_DATA_01: + priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call; + + ret = register_acpi_notifier(&priv->acpi_nb); + if (ret) + goto out; + + ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister, + &priv->acpi_nb); + if (ret) + goto out; + + ret = component_add(&wdev->dev, &lwmi_cd_component_ops); + goto out; + case LENOVO_FAN_TEST_DATA: + ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops); + goto out; + default: + return -EINVAL; + } +out: + if (ret) { + dev_err(&wdev->dev, "failed to register %s: %d\n", + info->name, ret); + } else { + lwmi_cd_debugfs_add(priv); + + dev_dbg(&wdev->dev, "registered %s with %u items\n", + info->name, priv->list->count); + } + return ret; +} + +static void lwmi_cd_remove(struct wmi_device *wdev) +{ + struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev); + + lwmi_cd_debugfs_remove(priv); + + switch (priv->list->type) { + case LENOVO_CAPABILITY_DATA_00: + lwmi_cd_sub_master_del(priv); + fallthrough; + case LENOVO_CAPABILITY_DATA_01: + component_del(&wdev->dev, &lwmi_cd_component_ops); + break; + case LENOVO_FAN_TEST_DATA: + component_del(&wdev->dev, &lwmi_cd_sub_component_ops); + break; + default: + WARN_ON(1); + } +} + +#define LWMI_CD_WDEV_ID(_type) \ + .guid_string = _type##_GUID, \ + .context = &lwmi_cd_table[_type], + +static const struct wmi_device_id lwmi_cd_id_table[] = { + { LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) }, + { LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) }, + { LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) }, + {} +}; + +static struct wmi_driver lwmi_cd_driver = { + .driver = { + .name = "lenovo_wmi_capdata", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lwmi_cd_id_table, + .probe = lwmi_cd_probe, + .remove = lwmi_cd_remove, + .no_singleton = true, +}; + +module_wmi_driver(lwmi_cd_driver); + +MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); +MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_AUTHOR("Rong Zhang "); +MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h new file mode 100644 index 000000000..92098aeee --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_CAPDATA_H_ +#define _LENOVO_WMI_CAPDATA_H_ + +#include +#include +#include + +#define LWMI_SUPP_VALID BIT(0) +#define LWMI_SUPP_GET BIT(1) +#define LWMI_SUPP_SET BIT(2) + +#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24) +#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16) +#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8) +#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0) + +enum lwmi_device_id { + LWMI_DEVICE_ID_CPU = 0x01, + LWMI_DEVICE_ID_GPU = 0x02, + LWMI_DEVICE_ID_PSU = 0x03, + LWMI_DEVICE_ID_FAN = 0x04, +}; + +#define LWMI_TYPE_ID_NONE 0x00 + +struct component_match; +struct device; +struct cd_list; + +struct capdata00 { + u32 id; + u32 supported; + u32 default_value; +}; + +struct capdata01 { + union { + struct capdata00; + struct capdata00 cd00; + }; + u32 step; + u32 min_value; + u32 max_value; +}; + +struct capdata_fan { + u32 id; + u32 min_rpm; + u32 max_rpm; +}; + +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list); + +struct lwmi_cd_binder { + struct cd_list *cd00_list; + struct cd_list *cd01_list; + /* + * May be called during or after the bind callback. + * Will be called with NULL if capdata_fan does not exist. + * The pointer is only valid in the callback; never keep it for later use! + */ + cd_list_cb_t cd_fan_list_cb; +}; + +/** + * lwmi_attr_id() - Formats a capability data attribute ID + * @dev_id: The u8 corresponding to the device ID. + * @feat_id: The u8 corresponding to the feature ID on the device. + * @mode_id: The u8 corresponding to the wmi-gamezone mode for set/get. + * @type_id: The u8 corresponding to the sub-device. + * + * Return: encoded capability data attribute ID. + */ +static inline u32 lwmi_attr_id(u8 dev_id, u8 feat_id, u8 mode_id, u8 type_id) +{ + return (FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, dev_id) | + FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, feat_id) | + FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode_id) | + FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, type_id)); +} + +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr); +int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output); +int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output); +int lwmi_cd_fan_get_data(struct cd_list *list, u32 attribute_id, struct capdata_fan *output); + +#endif /* !_LENOVO_WMI_CAPDATA_H_ */ diff --git a/drivers/platform/x86/lenovo/wmi-events.c b/drivers/platform/x86/lenovo/wmi-events.c new file mode 100644 index 000000000..fc25bba68 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-events.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo WMI Events driver. Lenovo WMI interfaces provide various + * hardware triggered events that many drivers need to have propagated. + * This driver provides a uniform entrypoint for these events so that + * any driver that needs to respond to these events can subscribe to a + * notifier chain. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include + +#include "wmi-events.h" +#include "wmi-helpers.h" + +#define THERMAL_MODE_EVENT_GUID "D320289E-8FEA-41E0-86F9-911D83151B5F" + +#define LWMI_EVENT_DEVICE(guid, type) \ + .guid_string = (guid), .context = &(enum lwmi_events_type) \ + { \ + type \ + } + +static BLOCKING_NOTIFIER_HEAD(events_chain_head); + +struct lwmi_events_priv { + struct wmi_device *wdev; + enum lwmi_events_type type; +}; + +/** + * lwmi_events_register_notifier() - Add a notifier to the notifier chain. + * @nb: The notifier_block struct to register + * + * Call blocking_notifier_chain_register to register the notifier block to the + * lenovo-wmi-events driver blocking notifier chain. + * + * Return: 0 on success, %-EEXIST on error. + */ +int lwmi_events_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&events_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_events_register_notifier, "LENOVO_WMI_EVENTS"); + +/** + * lwmi_events_unregister_notifier() - Remove a notifier from the notifier + * chain. + * @nb: The notifier_block struct to unregister + * + * Call blocking_notifier_chain_unregister to unregister the notifier block + * from the lenovo-wmi-events driver blocking notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +int lwmi_events_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&events_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_events_unregister_notifier, "LENOVO_WMI_EVENTS"); + +/** + * devm_lwmi_events_unregister_notifier() - Remove a notifier from the notifier + * chain. + * @data: Void pointer to the notifier_block struct to unregister. + * + * Call lwmi_events_unregister_notifier to unregister the notifier block from + * the lenovo-wmi-events driver blocking notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +static void devm_lwmi_events_unregister_notifier(void *data) +{ + struct notifier_block *nb = data; + + lwmi_events_unregister_notifier(nb); +} + +/** + * devm_lwmi_events_register_notifier() - Add a notifier to the notifier chain. + * @dev: The parent device of the notifier_block struct. + * @nb: The notifier_block struct to register + * + * Call lwmi_events_register_notifier to register the notifier block to the + * lenovo-wmi-events driver blocking notifier chain. Then add, as a device + * managed action, unregister_notifier to automatically unregister the + * notifier block upon its parent device removal. + * + * Return: 0 on success, or an error code. + */ +int devm_lwmi_events_register_notifier(struct device *dev, + struct notifier_block *nb) +{ + int ret; + + ret = lwmi_events_register_notifier(nb); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(dev, devm_lwmi_events_unregister_notifier, nb); +} +EXPORT_SYMBOL_NS_GPL(devm_lwmi_events_register_notifier, "LENOVO_WMI_EVENTS"); + +/** + * lwmi_events_notify() - Call functions for the notifier call chain. + * @wdev: The parent WMI device of the driver. + * @obj: ACPI object passed by the registered WMI Event. + * + * Validate WMI event data and notify all registered drivers of the event and + * its output. + * + * Return: 0 on success, or an error code. + */ +static void lwmi_events_notify(struct wmi_device *wdev, union acpi_object *obj) +{ + struct lwmi_events_priv *priv = dev_get_drvdata(&wdev->dev); + int sel_prof; + int ret; + + switch (priv->type) { + case LWMI_EVENT_THERMAL_MODE: + if (obj->type != ACPI_TYPE_INTEGER) + return; + + sel_prof = obj->integer.value; + + switch (sel_prof) { + case LWMI_GZ_THERMAL_MODE_QUIET: + case LWMI_GZ_THERMAL_MODE_BALANCED: + case LWMI_GZ_THERMAL_MODE_PERFORMANCE: + case LWMI_GZ_THERMAL_MODE_EXTREME: + case LWMI_GZ_THERMAL_MODE_CUSTOM: + ret = blocking_notifier_call_chain(&events_chain_head, + LWMI_EVENT_THERMAL_MODE, + &sel_prof); + if (ret == NOTIFY_BAD) + dev_err(&wdev->dev, + "Failed to send notification to call chain for WMI Events\n"); + return; + default: + dev_err(&wdev->dev, "Got invalid thermal mode: %x", + sel_prof); + return; + } + break; + default: + return; + } +} + +static int lwmi_events_probe(struct wmi_device *wdev, const void *context) +{ + struct lwmi_events_priv *priv; + + if (!context) + return -EINVAL; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + priv->type = *(enum lwmi_events_type *)context; + dev_set_drvdata(&wdev->dev, priv); + + return 0; +} + +static const struct wmi_device_id lwmi_events_id_table[] = { + { LWMI_EVENT_DEVICE(THERMAL_MODE_EVENT_GUID, LWMI_EVENT_THERMAL_MODE) }, + {} +}; + +static struct wmi_driver lwmi_events_driver = { + .driver = { + .name = "lenovo_wmi_events", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lwmi_events_id_table, + .min_event_size = sizeof(u32), + .probe = lwmi_events_probe, + .notify = lwmi_events_notify, + .no_singleton = true, +}; + +module_wmi_driver(lwmi_events_driver); + +MODULE_DEVICE_TABLE(wmi, lwmi_events_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo WMI Events Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-events.h b/drivers/platform/x86/lenovo/wmi-events.h new file mode 100644 index 000000000..cd34e8869 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-events.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_EVENTS_H_ +#define _LENOVO_WMI_EVENTS_H_ + +struct device; +struct notifier_block; + +enum lwmi_events_type { + LWMI_EVENT_THERMAL_MODE = 1, +}; + +int lwmi_events_register_notifier(struct notifier_block *nb); +int lwmi_events_unregister_notifier(struct notifier_block *nb); +int devm_lwmi_events_register_notifier(struct device *dev, + struct notifier_block *nb); + +#endif /* !_LENOVO_WMI_EVENTS_H_ */ diff --git a/drivers/platform/x86/lenovo/wmi-gamezone.c b/drivers/platform/x86/lenovo/wmi-gamezone.c new file mode 100644 index 000000000..109c0b564 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-gamezone.c @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo GameZone WMI interface driver. + * + * The GameZone WMI interface provides platform profile and fan curve settings + * for devices that fall under the "Gaming Series" of Lenovo Legion devices. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wmi-events.h" +#include "wmi-helpers.h" + +#define LENOVO_GAMEZONE_GUID "887B54E3-DDDC-4B2C-8B88-68A26A8835D0" + +#define LWMI_GZ_METHOD_ID_SMARTFAN_SUP 43 +#define LWMI_GZ_METHOD_ID_SMARTFAN_SET 44 +#define LWMI_GZ_METHOD_ID_SMARTFAN_GET 45 + +struct lwmi_gz_priv { + enum thermal_mode current_mode; + struct notifier_block event_nb; + struct notifier_block mode_nb; + spinlock_t gz_mode_lock; /* current_mode lock */ + struct wmi_device *wdev; + int extreme_supported; + struct device *ppdev; +}; + +struct quirk_entry { + bool extreme_supported; +}; + +static struct quirk_entry quirk_no_extreme_bug = { + .extreme_supported = false, +}; + +/** + * lwmi_gz_mode_call() - Call method for lenovo-wmi-other driver notifier. + * + * @nb: The notifier_block registered to lenovo-wmi-other driver. + * @cmd: The event type. + * @data: Thermal mode enum pointer pointer for returning the thermal mode. + * + * For LWMI_GZ_GET_THERMAL_MODE, retrieve the current thermal mode. + * + * Return: Notifier_block status. + */ +static int lwmi_gz_mode_call(struct notifier_block *nb, unsigned long cmd, + void *data) +{ + enum thermal_mode **mode = data; + struct lwmi_gz_priv *priv; + + priv = container_of(nb, struct lwmi_gz_priv, mode_nb); + + switch (cmd) { + case LWMI_GZ_GET_THERMAL_MODE: + scoped_guard(spinlock, &priv->gz_mode_lock) { + **mode = priv->current_mode; + } + return NOTIFY_OK; + default: + return NOTIFY_DONE; + } +} + +/** + * lwmi_gz_event_call() - Call method for lenovo-wmi-events driver notifier. + * block call chain. + * @nb: The notifier_block registered to lenovo-wmi-events driver. + * @cmd: The event type. + * @data: The data to be updated by the event. + * + * For LWMI_EVENT_THERMAL_MODE, set current_mode and notify platform_profile + * of a change. + * + * Return: notifier_block status. + */ +static int lwmi_gz_event_call(struct notifier_block *nb, unsigned long cmd, + void *data) +{ + enum thermal_mode *mode = data; + struct lwmi_gz_priv *priv; + + priv = container_of(nb, struct lwmi_gz_priv, event_nb); + + switch (cmd) { + case LWMI_EVENT_THERMAL_MODE: + scoped_guard(spinlock, &priv->gz_mode_lock) { + priv->current_mode = *mode; + } + platform_profile_notify(priv->ppdev); + return NOTIFY_STOP; + default: + return NOTIFY_DONE; + } +} + +/** + * lwmi_gz_thermal_mode_supported() - Get the version of the WMI + * interface to determine the support level. + * @wdev: The Gamezone WMI device. + * @supported: Pointer to return the support level with. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_gz_thermal_mode_supported(struct wmi_device *wdev, + int *supported) +{ + return lwmi_dev_evaluate_int(wdev, 0x0, LWMI_GZ_METHOD_ID_SMARTFAN_SUP, + NULL, 0, supported); +} + +/** + * lwmi_gz_thermal_mode_get() - Get the current thermal mode. + * @wdev: The Gamezone interface WMI device. + * @mode: Pointer to return the thermal mode with. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_gz_thermal_mode_get(struct wmi_device *wdev, + enum thermal_mode *mode) +{ + return lwmi_dev_evaluate_int(wdev, 0x0, LWMI_GZ_METHOD_ID_SMARTFAN_GET, + NULL, 0, mode); +} + +/** + * lwmi_gz_profile_get() - Get the current platform profile. + * @dev: the Gamezone interface parent device. + * @profile: Pointer to provide the current platform profile with. + * + * Call lwmi_gz_thermal_mode_get and convert the thermal mode into a platform + * profile based on the support level of the interface. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_gz_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct lwmi_gz_priv *priv = dev_get_drvdata(dev); + enum thermal_mode mode; + int ret; + + ret = lwmi_gz_thermal_mode_get(priv->wdev, &mode); + if (ret) + return ret; + + switch (mode) { + case LWMI_GZ_THERMAL_MODE_QUIET: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case LWMI_GZ_THERMAL_MODE_BALANCED: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case LWMI_GZ_THERMAL_MODE_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case LWMI_GZ_THERMAL_MODE_EXTREME: + *profile = PLATFORM_PROFILE_MAX_POWER; + break; + case LWMI_GZ_THERMAL_MODE_CUSTOM: + *profile = PLATFORM_PROFILE_CUSTOM; + break; + default: + return -EINVAL; + } + + guard(spinlock)(&priv->gz_mode_lock); + priv->current_mode = mode; + + return 0; +} + +/** + * lwmi_gz_profile_set() - Set the current platform profile. + * @dev: The Gamezone interface parent device. + * @profile: Pointer to the desired platform profile. + * + * Convert the given platform profile into a thermal mode based on the support + * level of the interface, then call the WMI method to set the thermal mode. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_gz_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct lwmi_gz_priv *priv = dev_get_drvdata(dev); + struct wmi_method_args_32 args = {}; + enum thermal_mode mode; + int ret; + + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + mode = LWMI_GZ_THERMAL_MODE_QUIET; + break; + case PLATFORM_PROFILE_BALANCED: + mode = LWMI_GZ_THERMAL_MODE_BALANCED; + break; + case PLATFORM_PROFILE_PERFORMANCE: + mode = LWMI_GZ_THERMAL_MODE_PERFORMANCE; + break; + case PLATFORM_PROFILE_MAX_POWER: + mode = LWMI_GZ_THERMAL_MODE_EXTREME; + break; + case PLATFORM_PROFILE_CUSTOM: + mode = LWMI_GZ_THERMAL_MODE_CUSTOM; + break; + default: + return -EOPNOTSUPP; + } + + args.arg0 = mode; + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, + LWMI_GZ_METHOD_ID_SMARTFAN_SET, + (u8 *)&args, sizeof(args), NULL); + if (ret) + return ret; + + guard(spinlock)(&priv->gz_mode_lock); + priv->current_mode = mode; + + return 0; +} + +static const struct dmi_system_id fwbug_list[] = { + { + .ident = "Legion Go 8APU1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8APU1"), + }, + .driver_data = &quirk_no_extreme_bug, + }, + { + .ident = "Legion Go S 8APU1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8APU1"), + }, + .driver_data = &quirk_no_extreme_bug, + }, + { + .ident = "Legion Go S 8ARP1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8ARP1"), + }, + .driver_data = &quirk_no_extreme_bug, + }, + { + .ident = "Legion Go 8ASP2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8ASP2"), + }, + .driver_data = &quirk_no_extreme_bug, + }, + { + .ident = "Legion Go 8AHP2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8AHP2"), + }, + .driver_data = &quirk_no_extreme_bug, + }, + {}, +}; + +/** + * lwmi_gz_extreme_supported() - Evaluate if a device supports extreme thermal mode. + * @profile_support_ver: Version of the WMI interface. + * + * Determine if the extreme thermal mode is supported by the hardware. + * Anything version 5 or lower does not. For devices with a version 6 or + * greater do a DMI check, as some devices report a version that supports + * extreme mode but have an incomplete entry in the BIOS. To ensure this + * cannot be set, quirk them to prevent assignment. + * + * Return: bool. + */ +static bool lwmi_gz_extreme_supported(int profile_support_ver) +{ + const struct dmi_system_id *dmi_id; + struct quirk_entry *quirks; + + if (profile_support_ver < 6) + return false; + + dmi_id = dmi_first_match(fwbug_list); + if (!dmi_id) + return true; + + quirks = dmi_id->driver_data; + + return quirks->extreme_supported; +} + +/** + * lwmi_gz_platform_profile_probe - Enable and set up the platform profile + * device. + * @drvdata: Driver data for the interface. + * @choices: Container for enabled platform profiles. + * + * Determine if thermal mode is supported, and if so to what feature level. + * Then enable all supported platform profiles. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_gz_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + struct lwmi_gz_priv *priv = drvdata; + int profile_support_ver; + int ret; + + ret = lwmi_gz_thermal_mode_supported(priv->wdev, &profile_support_ver); + if (ret) + return ret; + + if (profile_support_ver < 1) + return -ENODEV; + + 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); + + priv->extreme_supported = lwmi_gz_extreme_supported(profile_support_ver); + if (priv->extreme_supported) + set_bit(PLATFORM_PROFILE_MAX_POWER, choices); + + return 0; +} + +static const struct platform_profile_ops lwmi_gz_platform_profile_ops = { + .probe = lwmi_gz_platform_profile_probe, + .profile_get = lwmi_gz_profile_get, + .profile_set = lwmi_gz_profile_set, +}; + +static int lwmi_gz_probe(struct wmi_device *wdev, const void *context) +{ + struct lwmi_gz_priv *priv; + int ret; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + priv->ppdev = devm_platform_profile_register(&wdev->dev, "lenovo-wmi-gamezone", + priv, &lwmi_gz_platform_profile_ops); + if (IS_ERR(priv->ppdev)) + return -ENODEV; + + spin_lock_init(&priv->gz_mode_lock); + + ret = lwmi_gz_thermal_mode_get(wdev, &priv->current_mode); + if (ret) + return ret; + + priv->event_nb.notifier_call = lwmi_gz_event_call; + ret = devm_lwmi_events_register_notifier(&wdev->dev, &priv->event_nb); + if (ret) + return ret; + + priv->mode_nb.notifier_call = lwmi_gz_mode_call; + return devm_lwmi_tm_register_notifier(&wdev->dev, &priv->mode_nb); +} + +static const struct wmi_device_id lwmi_gz_id_table[] = { + { LENOVO_GAMEZONE_GUID, NULL }, + {} +}; + +static struct wmi_driver lwmi_gz_driver = { + .driver = { + .name = "lenovo_wmi_gamezone", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lwmi_gz_id_table, + .probe = lwmi_gz_probe, + .no_singleton = true, +}; + +module_wmi_driver(lwmi_gz_driver); + +MODULE_IMPORT_NS("LENOVO_WMI_EVENTS"); +MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); +MODULE_DEVICE_TABLE(wmi, lwmi_gz_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo GameZone WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-helpers.c b/drivers/platform/x86/lenovo/wmi-helpers.c new file mode 100644 index 000000000..8f5766c39 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-helpers.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo Legion WMI helpers driver. + * + * The Lenovo Legion WMI interface is broken up into multiple GUID interfaces + * that require cross-references between GUID's for some functionality. The + * "Custom Mode" interface is a legacy interface for managing and displaying + * CPU & GPU power and hwmon settings and readings. The "Other Mode" interface + * is a modern interface that replaces or extends the "Custom Mode" interface + * methods. The "Gamezone" interface adds advanced features such as fan + * profiles and overclocking. The "Lighting" interface adds control of various + * status lights related to different hardware components. Each of these + * drivers uses a common procedure to get data from the WMI interface, + * enumerated here. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wmi-helpers.h" + +/* Thermal mode notifier chain. */ +static BLOCKING_NOTIFIER_HEAD(tm_chain_head); + +/** + * lwmi_dev_evaluate_int() - Helper function for calling WMI methods that + * return an integer. + * @wdev: Pointer to the WMI device to be called. + * @instance: Instance of the called method. + * @method_id: WMI Method ID for the method to be called. + * @buf: Buffer of all arguments for the given method_id. + * @size: Length of the buffer. + * @retval: Pointer for the return value to be assigned. + * + * Calls wmidev_evaluate_method for Lenovo WMI devices that return an ACPI + * integer. Validates the return value type and assigns the value to the + * retval pointer. + * + * Return: 0 on success, or an error code. + */ +int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id, + unsigned char *buf, size_t size, u32 *retval) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer input = { size, buf }; + acpi_status status; + + status = wmidev_evaluate_method(wdev, instance, method_id, &input, + &output); + if (ACPI_FAILURE(status)) + return -EIO; + + union acpi_object *ret_obj __free(kfree) = output.pointer; + + if (retval) { + if (!ret_obj) + return -ENODATA; + + switch (ret_obj->type) { + /* + * The ACPI method may simply return a buffer when a u32 + * is expected. This is valid on Windows as its WMI-ACPI + * driver converts everything to a common buffer. + */ + case ACPI_TYPE_BUFFER: + if (ret_obj->buffer.length < sizeof(u32)) + return -ENXIO; + + *retval = get_unaligned_le32(ret_obj->buffer.pointer); + return 0; + case ACPI_TYPE_INTEGER: + *retval = (u32)ret_obj->integer.value; + return 0; + default: + return -ENXIO; + } + } + + return 0; +}; +EXPORT_SYMBOL_NS_GPL(lwmi_dev_evaluate_int, "LENOVO_WMI_HELPERS"); + +/** + * lwmi_tm_register_notifier() - Add a notifier to the blocking notifier chain + * @nb: The notifier_block struct to register + * + * Call blocking_notifier_chain_register to register the notifier block to the + * thermal mode notifier chain. + * + * Return: 0 on success, %-EEXIST on error. + */ +int lwmi_tm_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&tm_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_tm_register_notifier, "LENOVO_WMI_HELPERS"); + +/** + * lwmi_tm_unregister_notifier() - Remove a notifier from the blocking notifier + * chain. + * @nb: The notifier_block struct to register + * + * Call blocking_notifier_chain_unregister to unregister the notifier block from the + * thermal mode notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +int lwmi_tm_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&tm_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_tm_unregister_notifier, "LENOVO_WMI_HELPERS"); + +/** + * devm_lwmi_tm_unregister_notifier() - Remove a notifier from the blocking + * notifier chain. + * @data: Void pointer to the notifier_block struct to register. + * + * Call lwmi_tm_unregister_notifier to unregister the notifier block from the + * thermal mode notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +static void devm_lwmi_tm_unregister_notifier(void *data) +{ + struct notifier_block *nb = data; + + lwmi_tm_unregister_notifier(nb); +} + +/** + * devm_lwmi_tm_register_notifier() - Add a notifier to the blocking notifier + * chain. + * @dev: The parent device of the notifier_block struct. + * @nb: The notifier_block struct to register + * + * Call lwmi_tm_register_notifier to register the notifier block to the + * thermal mode notifier chain. Then add devm_lwmi_tm_unregister_notifier + * as a device managed action to automatically unregister the notifier block + * upon parent device removal. + * + * Return: 0 on success, or an error code. + */ +int devm_lwmi_tm_register_notifier(struct device *dev, + struct notifier_block *nb) +{ + int ret; + + ret = lwmi_tm_register_notifier(nb); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(dev, devm_lwmi_tm_unregister_notifier, + nb); +} +EXPORT_SYMBOL_NS_GPL(devm_lwmi_tm_register_notifier, "LENOVO_WMI_HELPERS"); + +/** + * lwmi_tm_notifier_call() - Call functions for the notifier call chain. + * @mode: Pointer to a thermal mode enum to retrieve the data from. + * + * Call blocking_notifier_call_chain to retrieve the thermal mode from the + * lenovo-wmi-gamezone driver. + * + * Return: 0 on success, or an error code. + */ +int lwmi_tm_notifier_call(enum thermal_mode *mode) +{ + int ret; + + ret = blocking_notifier_call_chain(&tm_chain_head, + LWMI_GZ_GET_THERMAL_MODE, &mode); + if ((ret & ~NOTIFY_STOP_MASK) != NOTIFY_OK) + return -EINVAL; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(lwmi_tm_notifier_call, "LENOVO_WMI_HELPERS"); + +static struct dentry *lwmi_debugfs_dir; + +/** + * lwmi_debugfs_create_dir() - Helper function for creating a debugfs directory + * for a device. + * @wdev: Pointer to the WMI device to be called. + * + * Caller must remove the directory with debugfs_remove_recursive() on device + * removal. + * + * Return: Pointer to the created directory. + */ +struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev) +{ + return debugfs_create_dir(dev_name(&wdev->dev), lwmi_debugfs_dir); +} +EXPORT_SYMBOL_NS_GPL(lwmi_debugfs_create_dir, "LENOVO_WMI_HELPERS"); + +static int __init lwmi_helpers_init(void) +{ + lwmi_debugfs_dir = debugfs_create_dir("lenovo_wmi", NULL); + + return 0; +} +subsys_initcall(lwmi_helpers_init) + +static void __exit lwmi_helpers_exit(void) +{ + debugfs_remove_recursive(lwmi_debugfs_dir); +} +module_exit(lwmi_helpers_exit) + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo WMI Helpers Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-helpers.h b/drivers/platform/x86/lenovo/wmi-helpers.h new file mode 100644 index 000000000..039fe6100 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-helpers.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_HELPERS_H_ +#define _LENOVO_WMI_HELPERS_H_ + +#include + +struct device; +struct notifier_block; +struct wmi_device; + +struct wmi_method_args_32 { + u32 arg0; + u32 arg1; +}; + +struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev); + +enum lwmi_event_type { + LWMI_GZ_GET_THERMAL_MODE = 0x01, +}; + +enum thermal_mode { + LWMI_GZ_THERMAL_MODE_NONE = 0x00, + LWMI_GZ_THERMAL_MODE_QUIET = 0x01, + LWMI_GZ_THERMAL_MODE_BALANCED = 0x02, + LWMI_GZ_THERMAL_MODE_PERFORMANCE = 0x03, + LWMI_GZ_THERMAL_MODE_EXTREME = 0xE0, /* Ver 6+ */ + LWMI_GZ_THERMAL_MODE_CUSTOM = 0xFF, +}; + +int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id, + unsigned char *buf, size_t size, u32 *retval); + +int lwmi_tm_register_notifier(struct notifier_block *nb); +int lwmi_tm_unregister_notifier(struct notifier_block *nb); +int devm_lwmi_tm_register_notifier(struct device *dev, + struct notifier_block *nb); +int lwmi_tm_notifier_call(enum thermal_mode *mode); + +#endif /* !_LENOVO_WMI_HELPERS_H_ */ diff --git a/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c b/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c new file mode 100644 index 000000000..7b9bad197 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Lenovo Super Hotkey Utility WMI extras driver for Ideapad laptop + * + * Copyright (C) 2025 Lenovo + */ + +#include +#include +#include +#include +#include +#include + +/* Lenovo Super Hotkey WMI GUIDs */ +#define LUD_WMI_METHOD_GUID "CE6C0974-0407-4F50-88BA-4FC3B6559AD8" + +/* Lenovo Utility Data WMI method_id */ +#define WMI_LUD_GET_SUPPORT 1 +#define WMI_LUD_SET_FEATURE 2 + +#define WMI_LUD_GET_MICMUTE_LED_VER 20 +#define WMI_LUD_GET_AUDIOMUTE_LED_VER 26 + +#define WMI_LUD_SUPPORT_MICMUTE_LED_VER 25 +#define WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER 27 + +/* Input parameters to mute/unmute audio LED and Mic LED */ +struct wmi_led_args { + u8 id; + u8 subid; + u16 value; +}; + +/* Values of input parameters to SetFeature of audio LED and Mic LED */ +enum hotkey_set_feature { + MIC_MUTE_LED_ON = 1, + MIC_MUTE_LED_OFF = 2, + AUDIO_MUTE_LED_ON = 4, + AUDIO_MUTE_LED_OFF = 5, +}; + +#define LSH_ACPI_LED_MAX 2 + +struct lenovo_super_hotkey_wmi_private { + struct led_classdev cdev[LSH_ACPI_LED_MAX]; + struct wmi_device *led_wdev; +}; + +enum mute_led_type { + MIC_MUTE, + AUDIO_MUTE, +}; + +static int lsh_wmi_mute_led_set(enum mute_led_type led_type, struct led_classdev *led_cdev, + enum led_brightness brightness) + +{ + struct lenovo_super_hotkey_wmi_private *wpriv = container_of(led_cdev, + struct lenovo_super_hotkey_wmi_private, cdev[led_type]); + struct wmi_led_args led_arg = {0, 0, 0}; + struct acpi_buffer input; + acpi_status status; + + switch (led_type) { + case MIC_MUTE: + led_arg.id = brightness == LED_ON ? MIC_MUTE_LED_ON : MIC_MUTE_LED_OFF; + break; + case AUDIO_MUTE: + led_arg.id = brightness == LED_ON ? AUDIO_MUTE_LED_ON : AUDIO_MUTE_LED_OFF; + break; + default: + return -EINVAL; + } + + input.length = sizeof(led_arg); + input.pointer = &led_arg; + status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_SET_FEATURE, &input, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +static int lsh_wmi_audiomute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) + +{ + return lsh_wmi_mute_led_set(AUDIO_MUTE, led_cdev, brightness); +} + +static int lsh_wmi_micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return lsh_wmi_mute_led_set(MIC_MUTE, led_cdev, brightness); +} + +static int lenovo_super_hotkey_wmi_led_init(enum mute_led_type led_type, struct device *dev) +{ + struct lenovo_super_hotkey_wmi_private *wpriv = dev_get_drvdata(dev); + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer input; + int led_version, err = 0; + unsigned int wmiarg; + acpi_status status; + + switch (led_type) { + case MIC_MUTE: + wmiarg = WMI_LUD_GET_MICMUTE_LED_VER; + break; + case AUDIO_MUTE: + wmiarg = WMI_LUD_GET_AUDIOMUTE_LED_VER; + break; + default: + return -EINVAL; + } + + input.length = sizeof(wmiarg); + input.pointer = &wmiarg; + status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_GET_SUPPORT, &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + union acpi_object *obj __free(kfree) = output.pointer; + if (!obj || obj->type != ACPI_TYPE_INTEGER) + return -EIO; + + led_version = obj->integer.value; + + /* + * Output parameters define: 0 means mute LED is not supported, Non-zero means + * mute LED can be supported. + */ + if (led_version == 0) + return 0; + + + switch (led_type) { + case MIC_MUTE: + if (led_version != WMI_LUD_SUPPORT_MICMUTE_LED_VER) { + pr_warn("The MIC_MUTE LED of this device isn't supported.\n"); + return 0; + } + + wpriv->cdev[led_type].name = "platform::micmute"; + wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_micmute_led_set; + wpriv->cdev[led_type].default_trigger = "audio-micmute"; + break; + case AUDIO_MUTE: + if (led_version != WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER) { + pr_warn("The AUDIO_MUTE LED of this device isn't supported.\n"); + return 0; + } + + wpriv->cdev[led_type].name = "platform::mute"; + wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_audiomute_led_set; + wpriv->cdev[led_type].default_trigger = "audio-mute"; + break; + default: + dev_err(dev, "Unknown LED type %d\n", led_type); + return -EINVAL; + } + + wpriv->cdev[led_type].max_brightness = LED_ON; + wpriv->cdev[led_type].flags = LED_CORE_SUSPENDRESUME; + + err = devm_led_classdev_register(dev, &wpriv->cdev[led_type]); + if (err < 0) { + dev_err(dev, "Could not register mute LED %d : %d\n", led_type, err); + return err; + } + return 0; +} + +static int lenovo_super_hotkey_wmi_leds_setup(struct device *dev) +{ + int err; + + err = lenovo_super_hotkey_wmi_led_init(MIC_MUTE, dev); + if (err) + return err; + + err = lenovo_super_hotkey_wmi_led_init(AUDIO_MUTE, dev); + if (err) + return err; + + return 0; +} + +static int lenovo_super_hotkey_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct lenovo_super_hotkey_wmi_private *wpriv; + + wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL); + if (!wpriv) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, wpriv); + wpriv->led_wdev = wdev; + return lenovo_super_hotkey_wmi_leds_setup(&wdev->dev); +} + +static const struct wmi_device_id lenovo_super_hotkey_wmi_id_table[] = { + { LUD_WMI_METHOD_GUID, NULL }, /* Utility data */ + { } +}; + +MODULE_DEVICE_TABLE(wmi, lenovo_super_hotkey_wmi_id_table); + +static struct wmi_driver lenovo_wmi_hotkey_utilities_driver = { + .driver = { + .name = "lenovo_wmi_hotkey_utilities", + .probe_type = PROBE_PREFER_ASYNCHRONOUS + }, + .id_table = lenovo_super_hotkey_wmi_id_table, + .probe = lenovo_super_hotkey_wmi_probe, + .no_singleton = true, +}; + +module_wmi_driver(lenovo_wmi_hotkey_utilities_driver); + +MODULE_AUTHOR("Jackie Dong "); +MODULE_DESCRIPTION("Lenovo Super Hotkey Utility WMI extras driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c new file mode 100644 index 000000000..fbb32bf40 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -0,0 +1,1738 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo Other Mode WMI interface driver. + * + * This driver uses the fw_attributes class to expose the various WMI functions + * provided by the "Other Mode" WMI interface. This enables CPU and GPU power + * limit as well as various other attributes for devices that fall under the + * "Gaming Series" of Lenovo laptop devices. Each attribute exposed by the + * "Other Mode" interface has a corresponding Capability Data struct that + * allows the driver to probe details about the attribute such as if it is + * supported by the hardware, the default_value, max_value, min_value, and step + * increment. + * + * These attributes typically don't fit anywhere else in the sysfs and are set + * in Windows using one of Lenovo's multiple user applications. + * + * Additionally, this driver also exports tunable fan speed RPM to HWMON. + * Min/max RPM are also provided for reference. + * + * Copyright (C) 2025 Derek J. Clark + * - fw_attributes + * - binding to Capability Data 01 + * + * Copyright (C) 2025 Rong Zhang + * - HWMON + * - binding to Capability Data 00 and Fan + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "wmi-capdata.h" +#include "wmi-events.h" +#include "wmi-helpers.h" +#include "../firmware_attributes_class.h" + +#define LENOVO_OTHER_MODE_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B" + +enum lwmi_feature_id_cpu { + LWMI_FEATURE_ID_CPU_SPPT = 0x01, + LWMI_FEATURE_ID_CPU_SPL = 0x02, + LWMI_FEATURE_ID_CPU_FPPT = 0x03, + LWMI_FEATURE_ID_CPU_TEMP = 0x04, + LWMI_FEATURE_ID_CPU_APU = 0x05, + LWMI_FEATURE_ID_CPU_CL = 0x06, + LWMI_FEATURE_ID_CPU_TAU = 0x07, + LWMI_FEATURE_ID_CPU_IPL = 0x09, +}; + +enum lwmi_feature_id_gpu { + LWMI_FEATURE_ID_GPU_NV_PPAB = 0x01, + LWMI_FEATURE_ID_GPU_NV_CTGP = 0x02, + LWMI_FEATURE_ID_GPU_TEMP = 0x03, + LWMI_FEATURE_ID_GPU_AC_OFFSET = 0x04, + LWMI_FEATURE_ID_DGPU_BOOST_CLK = 0x06, + LWMI_FEATURE_ID_DGPU_EN = 0x07, + LWMI_FEATURE_ID_GPU_MODE = 0x08, + LWMI_FEATURE_ID_DGPU_DIDVID = 0x09, + LWMI_FEATURE_ID_GPU_NV_BPL = 0x0a, + LWMI_FEATURE_ID_GPU_NV_CPU_BOOST = 0x0b, +}; + +enum lwmi_feature_id_psu { + LWMI_FEATURE_ID_PSU_CHARGE_TYPES = 0x01, + LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOUR = 0x02, +}; + +#define LWMI_FEATURE_ID_FAN_RPM 0x03 + +#define LWMI_TYPE_ID_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 + +#define LWMI_FEATURE_VALUE_GET 17 +#define LWMI_FEATURE_VALUE_SET 18 + +#define LWMI_FAN_ID_BASE 1 +#define LWMI_FAN_NR 4 +#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE) + +#define LWMI_FAN_DIV 100 + +#define LWMI_CHARGE_BEHAVIOR_DISCHARGE 0x00 +#define LWMI_CHARGE_BEHAVIOR_AUTO 0x01 +#define LWMI_CHARGE_TYPE_STANDARD 0x00 +#define LWMI_CHARGE_TYPE_LONGLIFE 0x01 + +#define LWMI_ATTR_ID_FAN_RPM(x) \ + lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ + LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) + +#define LWMI_ATTR_ID_PSU(feat, type) \ + lwmi_attr_id(LWMI_DEVICE_ID_PSU, feat, \ + LWMI_GZ_THERMAL_MODE_NONE, type) + +#define LWMI_OM_SYSFS_NAME "lenovo-wmi-other" +#define LWMI_OM_HWMON_NAME "lenovo_wmi_other" + +static DEFINE_IDA(lwmi_om_ida); + +enum attribute_property { + DEFAULT_VAL, + MAX_VAL, + MIN_VAL, + STEP_VAL, + SUPPORTED, +}; + +struct lwmi_fan_info { + u32 supported; + u32 last_target; + long min_rpm; + long max_rpm; +}; + +struct lwmi_om_priv { + struct component_master_ops *ops; + + /* only valid after capdata bind */ + struct cd_list *cd00_list; + struct cd_list *cd01_list; + + struct device *hwmon_dev; + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + struct wmi_device *wdev; + int ida_id; + + struct lwmi_fan_info fan_info[LWMI_FAN_NR]; + + struct { + bool capdata00_collected : 1; + bool capdata_fan_collected : 1; + } fan_flags; + + enum power_supply_charge_behaviour charge_behaviour; + const struct power_supply_ext *battery_ext; + struct acpi_battery_hook battery_hook; + bool bh_registered; +}; + +/* + * Visibility of fan channels: + * + * +-------------------+---------+------------------+-----------------------+------------+ + * | | default | +expose_all_fans | +relax_fan_constraint | +both | + * +-------------------+---------+------------------+-----------------------+------------+ + * | canonical | RW | RW | RW+relaxed | RW+relaxed | + * +-------------------+---------+------------------+-----------------------+------------+ + * | -capdata_fan[idx] | N | RO | N | RW+relaxed | + * +-------------------+---------+------------------+-----------------------+------------+ + * + * Note: + * 1. LWMI_ATTR_ID_FAN_RPM[idx].supported is always checked before exposing a channel. + * 2. -capdata_fan implies -capdata_fan[idx]. + */ +static bool expose_all_fans; +module_param(expose_all_fans, bool, 0444); +MODULE_PARM_DESC(expose_all_fans, + "This option skips some capability checks and solely relies on per-channel ones " + "to expose fan attributes. Use with caution."); + +static bool relax_fan_constraint; +module_param(relax_fan_constraint, bool, 0444); +MODULE_PARM_DESC(relax_fan_constraint, + "Do not enforce fan RPM constraint (div/min/max) " + "and enables fan tuning when such data is missing. " + "Enabling this may results in HWMON attributes being out-of-sync, " + "and setting a too low RPM stops the fan. Use with caution."); + +/* Visibility of power supply extensions */ +static bool force_load_psy_ext; +module_param(force_load_psy_ext, bool, 0444); +MODULE_PARM_DESC(force_load_psy_ext, + "This option will skip checking if the ideapad_laptop driver will conflict " + "with adding an extension to set the battery charge behavior and battery charge " + "control end threshold. It will also skip checking if the BIOS reports that " + "those features are fully supported. It is recommended to blacklist the ideapad " + "driver before using this option."); + +/* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */ + +/** + * lwmi_om_fan_get_set() - Get or set fan RPM value of specified fan + * @priv: Driver private data structure + * @channel: Fan channel index (0-based) + * @val: Pointer to value (input for set, output for get) + * @set: True to set value, false to get value + * + * Communicates with WMI interface to either retrieve current fan RPM + * or set target fan RPM. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_fan_get_set(struct lwmi_om_priv *priv, int channel, u32 *val, bool set) +{ + struct wmi_method_args_32 args = {}; + u32 method_id, retval; + int err; + + method_id = set ? LWMI_FEATURE_VALUE_SET : LWMI_FEATURE_VALUE_GET; + args.arg0 = LWMI_ATTR_ID_FAN_RPM(channel); + args.arg1 = set ? *val : 0; + + err = lwmi_dev_evaluate_int(priv->wdev, 0x0, method_id, + (unsigned char *)&args, sizeof(args), &retval); + if (err) + return err; + + if (!set) { + *val = retval; + return 0; + } + + /* + * It seems that 0 means "no error" and 1 means "done". Apparently + * different firmware teams have different thoughts on indicating + * success, so we accepts both. + */ + return (retval == 0 || retval == 1) ? 0 : -EIO; +} + +/** + * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes + * @drvdata: Driver private data + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * + * Determines whether an HWMON attribute should be visible in sysfs + * based on hardware capabilities and current configuration. + * + * Return: permission mode, or 0 if invisible. + */ +static umode_t lwmi_om_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + struct lwmi_om_priv *priv = (struct lwmi_om_priv *)drvdata; + bool visible = false; + + if (type == hwmon_fan) { + if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID)) + return 0; + + switch (attr) { + case hwmon_fan_target: + if (!(priv->fan_info[channel].supported & LWMI_SUPP_SET)) + return 0; + + if (relax_fan_constraint || + (priv->fan_info[channel].min_rpm >= 0 && + priv->fan_info[channel].max_rpm >= 0)) + return 0644; + + /* + * Reaching here implies expose_all_fans is set. + * See lwmi_om_hwmon_add(). + */ + dev_warn_once(&priv->wdev->dev, + "fan tuning disabled due to missing RPM constraint\n"); + return 0; + case hwmon_fan_div: + case hwmon_fan_input: + visible = priv->fan_info[channel].supported & LWMI_SUPP_GET; + break; + case hwmon_fan_min: + visible = priv->fan_info[channel].min_rpm >= 0; + break; + case hwmon_fan_max: + visible = priv->fan_info[channel].max_rpm >= 0; + break; + } + } + + return visible ? 0444 : 0; +} + +/** + * lwmi_om_hwmon_read() - Read HWMON sensor data + * @dev: Device pointer + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * @val: Pointer to store value + * + * Reads current sensor values from hardware through WMI interface. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + u32 retval = 0; + int err; + + if (type == hwmon_fan) { + switch (attr) { + /* + * The EC has an internal RPM divisor (i.e., the raw register value is + * RPM / fanY_div). For fanY_input, the WMI method reads the register + * value and returns raw * fanY_div. For fanY_target, the WMI method + * divides the written value by fanY_div before writing it to the EC. + * + * As a result, reading fanY_input always returns a multiple of fanY_div, + * while writing to fanY_target loses the remainder. + */ + case hwmon_fan_div: + *val = LWMI_FAN_DIV; + return 0; + case hwmon_fan_input: + err = lwmi_om_fan_get_set(priv, channel, &retval, false); + if (err) + return err; + + *val = retval; + return 0; + case hwmon_fan_target: + *val = priv->fan_info[channel].last_target; + return 0; + case hwmon_fan_min: + *val = priv->fan_info[channel].min_rpm; + return 0; + case hwmon_fan_max: + *val = priv->fan_info[channel].max_rpm; + return 0; + } + } + + return -EOPNOTSUPP; +} + +/** + * lwmi_om_hwmon_write() - Write HWMON sensor data + * @dev: Device pointer + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * @val: Value to write + * + * Writes configuration values to hardware through WMI interface. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + u32 raw, min_rpm, max_rpm; + int err; + + if (type == hwmon_fan) { + switch (attr) { + case hwmon_fan_target: + if (relax_fan_constraint) { + min_rpm = 1; + max_rpm = U16_MAX; + } else { + min_rpm = priv->fan_info[channel].min_rpm; + max_rpm = priv->fan_info[channel].max_rpm; + } + + /* 0 means "auto". */ + if (val != 0 && (val < min_rpm || val > max_rpm)) + return -EINVAL; + + /* + * The effective fanY_target is always a multiple of fanY_div + * due to the EC's internal RPM divisor (see lwmi_om_hwmon_read). + * + * Round down the written value to the nearest multiple of fanY_div + * to prevent mismatch between the effective value and last_target. + * + * For relax_fan_constraint, skip this conversion as setting a + * sub-fanY_div value is necessary to completely stop the fan on + * some devices. + */ + if (!relax_fan_constraint) + raw = val / LWMI_FAN_DIV * LWMI_FAN_DIV; + else + raw = val; + + err = lwmi_om_fan_get_set(priv, channel, &raw, true); + if (err) + return err; + + priv->fan_info[channel].last_target = raw; + return 0; + } + } + + return -EOPNOTSUPP; +} + +static const struct hwmon_channel_info * const lwmi_om_hwmon_info[] = { + /* Must match LWMI_FAN_NR. */ + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX), + NULL +}; + +static const struct hwmon_ops lwmi_om_hwmon_ops = { + .is_visible = lwmi_om_hwmon_is_visible, + .read = lwmi_om_hwmon_read, + .write = lwmi_om_hwmon_write, +}; + +static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = { + .ops = &lwmi_om_hwmon_ops, + .info = lwmi_om_hwmon_info, +}; + +/** + * lwmi_om_hwmon_add() - Register HWMON device if all info is collected + * @priv: Driver private data + */ +static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) +{ + int i, valid; + + if (WARN_ON(priv->hwmon_dev)) + return; + + if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) { + dev_dbg(&priv->wdev->dev, "HWMON registration pending (00: %d, fan: %d)\n", + priv->fan_flags.capdata00_collected, + priv->fan_flags.capdata_fan_collected); + return; + } + + if (expose_all_fans) + dev_warn(&priv->wdev->dev, "all fans exposed. Use with caution\n"); + + if (relax_fan_constraint) + dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution\n"); + + valid = 0; + for (i = 0; i < LWMI_FAN_NR; i++) { + if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID)) + continue; + + valid++; + + if (!expose_all_fans && + (priv->fan_info[i].min_rpm < 0 || priv->fan_info[i].max_rpm < 0)) { + dev_dbg(&priv->wdev->dev, "missing RPM constraint for fan%d, hiding\n", + LWMI_FAN_ID(i)); + priv->fan_info[i].supported = 0; + valid--; + } + } + + if (valid == 0) { + dev_warn(&priv->wdev->dev, + "fan reporting/tuning is unsupported on this device\n"); + return; + } + + priv->hwmon_dev = hwmon_device_register_with_info(&priv->wdev->dev, + LWMI_OM_HWMON_NAME, priv, + &lwmi_om_hwmon_chip_info, + NULL); + if (IS_ERR(priv->hwmon_dev)) { + dev_warn(&priv->wdev->dev, "failed to register HWMON device: %ld\n", + PTR_ERR(priv->hwmon_dev)); + priv->hwmon_dev = NULL; + return; + } + + dev_dbg(&priv->wdev->dev, "registered HWMON device\n"); +} + +/** + * lwmi_om_hwmon_remove() - Unregister HWMON device + * @priv: Driver private data + * + * Unregisters the HWMON device if applicable. + */ +static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv) +{ + if (!priv->hwmon_dev) + return; + + hwmon_device_unregister(priv->hwmon_dev); + priv->hwmon_dev = NULL; +} + +/** + * lwmi_om_fan_info_init() - Initialzie fan info + * @priv: Driver private data + * + * lwmi_om_fan_info_collect_cd00() and lwmi_om_fan_info_collect_cd_fan() may be + * called in an arbitrary order. Hence, initializion must be done before. + */ +static void lwmi_om_fan_info_init(struct lwmi_om_priv *priv) +{ + int i; + + for (i = 0; i < LWMI_FAN_NR; i++) { + priv->fan_info[i] = (struct lwmi_fan_info) { + .supported = 0, + /* + * Assume 0 on probe as the EC resets all fans to auto mode on (re)boot. + * + * Note that S0ix (s2idle) preserves the RPM target, so we don't need + * suspend/resume callbacks. This behavior has not been tested on S3- + * capable devices, but I doubt if such devices even have this interface. + */ + .last_target = 0, + .min_rpm = -ENODATA, + .max_rpm = -ENODATA, + }; + } + + priv->fan_flags.capdata00_collected = false; + priv->fan_flags.capdata_fan_collected = false; +} + +/** + * lwmi_om_fan_info_collect_cd00() - Collect fan info from capdata 00 + * @priv: Driver private data + */ +static void lwmi_om_fan_info_collect_cd00(struct lwmi_om_priv *priv) +{ + struct capdata00 capdata00; + int i, err; + + dev_dbg(&priv->wdev->dev, "Collecting fan info from capdata00\n"); + + for (i = 0; i < LWMI_FAN_NR; i++) { + err = lwmi_cd00_get_data(priv->cd00_list, LWMI_ATTR_ID_FAN_RPM(i), &capdata00); + priv->fan_info[i].supported = err ? 0 : capdata00.supported; + } + + priv->fan_flags.capdata00_collected = true; + lwmi_om_hwmon_add(priv); +} + +/** + * lwmi_om_fan_info_collect_cd_fan() - Collect fan info from capdata fan + * @dev: Pointer to the lenovo-wmi-other device + * @cd_fan_list: Pointer to the capdata fan list + */ +static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *cd_fan_list) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + struct capdata_fan capdata_fan; + int i, err; + + dev_dbg(dev, "Collecting fan info from capdata_fan\n"); + + if (!cd_fan_list) + goto out; + + for (i = 0; i < LWMI_FAN_NR; i++) { + err = lwmi_cd_fan_get_data(cd_fan_list, LWMI_FAN_ID(i), &capdata_fan); + if (err) + continue; + + priv->fan_info[i].min_rpm = capdata_fan.min_rpm; + priv->fan_info[i].max_rpm = capdata_fan.max_rpm; + } + +out: + priv->fan_flags.capdata_fan_collected = true; + lwmi_om_hwmon_add(priv); +} + +/* ======== Power Supply Extension (component: lenovo-wmi-capdata 00) ======== */ + +/** + * lwmi_psy_ext_get_prop() - Get a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer to the lwmi_om_priv drvdata + * @prop: The property to read + * @val: The value to return + * + * Reads the given value from the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_get_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = ext_data; + struct wmi_method_args_32 args = {}; + u32 retval; + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + /* Reading from BIOS reads the wrong bit. Use cached value */ + val->intval = priv->charge_behaviour; + return 0; + case POWER_SUPPLY_PROP_CHARGE_TYPES: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPES, + LWMI_TYPE_ID_PSU_AC); + break; + default: + return -EINVAL; + } + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (u8 *)&args, sizeof(args), + &retval); + if (ret) + return ret; + + dev_dbg(&priv->wdev->dev, "Got return value %#x for property %#x\n", retval, prop); + + switch (retval) { + case LWMI_CHARGE_TYPE_LONGLIFE: + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + break; + case LWMI_CHARGE_TYPE_STANDARD: + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge types value: %#x\n", retval); + return -EINVAL; + } + + return 0; +} + +/** + * lwmi_psy_ext_set_prop() - Set a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer to the lwmi_om_priv drvdata + * @prop: The property to write + * @val: The value to write + * + * Writes the given value to the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_set_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = ext_data; + struct wmi_method_args_32 args = {}; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOUR, + LWMI_TYPE_ID_NONE); + switch (val->intval) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + args.arg1 = LWMI_CHARGE_BEHAVIOR_AUTO; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: + args.arg1 = LWMI_CHARGE_BEHAVIOR_DISCHARGE; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge behavior value: %#x\n", + val->intval); + return -EINVAL; + } + priv->charge_behaviour = val->intval; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPES: + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPES, + LWMI_TYPE_ID_PSU_AC); + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + args.arg1 = LWMI_CHARGE_TYPE_LONGLIFE; + break; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + args.arg1 = LWMI_CHARGE_TYPE_STANDARD; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge types value: %#x\n", + val->intval); + return -EINVAL; + } + break; + default: + return -EINVAL; + } + + dev_dbg(&priv->wdev->dev, "Attempting to set %#010x for property %#x to %#x\n", + args.arg0, prop, args.arg1); + + return lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, + (u8 *)&args, sizeof(args), NULL); +} + +/** lwmi_psy_prop_get_supported() - Gets the support level from capdata for a given property + * @priv: Pointer to the lwmi_om_priv drvdata + * @prop: The power supply property to be evaluated + * + * Return: bitmapped capability data support level + */ +static u32 lwmi_psy_prop_get_supported(struct lwmi_om_priv *priv, enum power_supply_property prop) +{ + struct capdata00 capdata; + u32 attribute_id; + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOUR, + LWMI_TYPE_ID_NONE); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPES: + attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPES, + LWMI_TYPE_ID_PSU_AC); + break; + default: + return 0; + } + + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return 0; + + dev_dbg(&priv->wdev->dev, "Battery charge feature (%#010x) support level: %#x\n", + attribute_id, capdata.supported); + + return capdata.supported; +} + +/** + * lwmi_psy_prop_is_supported() - Determine if the property is supported + * @priv: Pointer to the lwmi_om_priv drvdata + * @prop: The power supply property to be evaluated + * + * Checks capdata 00 to determine if the property is supported. + * + * Return: true if readable, or false + */ +static bool lwmi_psy_prop_is_supported(struct lwmi_om_priv *priv, enum power_supply_property prop) +{ + int ret; + + if (force_load_psy_ext) + return true; + + ret = lwmi_psy_prop_get_supported(priv, prop); + if (ret < 0) + return false; + + return (ret & LWMI_SUPP_VALID) && (ret & LWMI_SUPP_GET); +} + +/** + * lwmi_psy_prop_is_writeable() - Determine if the property is writeable + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer the lwmi_om_priv drvdata + * @prop: The property to check + * + * Checks capdata 00 to determine if the property is writable. + * + * Return: true if writable, or false + */ +static int lwmi_psy_prop_is_writeable(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop) +{ + struct lwmi_om_priv *priv = ext_data; + int ret; + + if (force_load_psy_ext) + return true; + + ret = lwmi_psy_prop_get_supported(priv, prop); + if (ret < 0) + return false; + + return !!(ret & LWMI_SUPP_SET); +} + +#define DEFINE_LWMI_POWER_SUPPLY_EXTENSION(_name, _props, _behaviours, _types) \ + static const struct power_supply_ext _name = { \ + .name = LWMI_OM_SYSFS_NAME, \ + .properties = _props, \ + .num_properties = ARRAY_SIZE(_props), \ + .charge_behaviours = _behaviours, \ + .charge_types = _types, \ + .get_property = lwmi_psy_ext_get_prop, \ + .set_property = lwmi_psy_ext_set_prop, \ + .property_is_writeable = lwmi_psy_prop_is_writeable, \ + } + +static const enum power_supply_property lwmi_psy_ext_props_all[] = { + POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const enum power_supply_property lwmi_psy_ext_props_types[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const enum power_supply_property lwmi_psy_ext_props_behaviour[] = { + POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, +}; + +#define LWMI_CHARGE_BEHAVIOURS (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \ + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) + +#define LWMI_CHARGE_TYPES (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | \ + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)) + +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_all, lwmi_psy_ext_props_all, + LWMI_CHARGE_BEHAVIOURS, LWMI_CHARGE_TYPES); +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_types, lwmi_psy_ext_props_types, + 0, LWMI_CHARGE_TYPES); +DEFINE_LWMI_POWER_SUPPLY_EXTENSION(lwmi_psy_ext_behaviour, lwmi_psy_ext_props_behaviour, + LWMI_CHARGE_BEHAVIOURS, 0); + +#define LWMI_PSY_PROP_BEHAVIOUR BIT(0) +#define LWMI_PSY_PROP_TYPES BIT(1) + +static const struct power_supply_ext *lwmi_psy_exts[] = { + [LWMI_PSY_PROP_BEHAVIOUR] = &lwmi_psy_ext_behaviour, + [LWMI_PSY_PROP_TYPES] = &lwmi_psy_ext_types, + [LWMI_PSY_PROP_BEHAVIOUR | LWMI_PSY_PROP_TYPES] = &lwmi_psy_ext_all, +}; + +/** + * lwmi_add_battery() - Connect the power_supply_ext + * @battery: The battery to extend + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook); + + return power_supply_register_extension(battery, priv->battery_ext, &priv->wdev->dev, priv); +} + +/** + * lwmi_remove_battery() - Disconnect the power_supply_ext + * @battery: The battery that was extended + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook); + + power_supply_unregister_extension(battery, priv->battery_ext); + return 0; +} + +/** + * lwmi_acpi_match() - Attempts to return the ideapad acpi handle + * @handle: The ACPI handle that manages battery charging + * @lvl: Unused + * @context: Void pointer to the acpi_handle object to return + * @retval: Unused + * + * Checks if the ideapad_laptop driver is going to manage charge_type first, + * then if not, hooks the battery to our WMI methods. + * + * Return: AE_CTRL_TERMINATE if found, AE_OK if not found. + */ +static acpi_status lwmi_acpi_match(acpi_handle handle, u32 lvl, + void *context, void **retval) +{ + acpi_handle *ahand = context; + + if (!handle) + return AE_OK; + + *ahand = handle; + + return AE_CTRL_TERMINATE; +} + +/** + * lwmi_om_psy_ext_init() - Hooks power supply extension to device battery + * @priv: Pointer to the lwmi_om_priv drvdata. + * + * Checks if the ideapad_laptop driver is going to manage charge attributes first, + * then if not, hooks the battery to our WMI methods if they are supported. + */ +static void lwmi_om_psy_ext_init(struct lwmi_om_priv *priv) +{ + static const char * const ideapad_hid = "VPC2004"; + acpi_handle handle = NULL; + unsigned int props = 0; + int ret; + + if (lwmi_psy_prop_is_supported(priv, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)) + props |= LWMI_PSY_PROP_BEHAVIOUR; + if (lwmi_psy_prop_is_supported(priv, POWER_SUPPLY_PROP_CHARGE_TYPES)) + props |= LWMI_PSY_PROP_TYPES; + if (!props) + return; + if (force_load_psy_ext) + goto load_psy_ext; + + /* Deconflict ideapad_laptop driver */ + ret = acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); + if (ret) + return; + + if (handle && acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) { + dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for device\n"); + return; + } + +load_psy_ext: + /* Add battery hooks */ + priv->battery_ext = lwmi_psy_exts[props]; + priv->battery_hook.add_battery = lwmi_add_battery; + priv->battery_hook.remove_battery = lwmi_remove_battery; + priv->battery_hook.name = "Lenovo WMI Other Battery Extension"; + priv->bh_registered = true; + + battery_hook_register(&priv->battery_hook); +} + +/** + * lwmi_om_psy_remove() - Unregister battery hook + * @priv: Driver private data + * + * Unregisters the battery hook if applicable. + */ +static void lwmi_om_psy_remove(struct lwmi_om_priv *priv) +{ + if (!priv->bh_registered) + return; + + battery_hook_unregister(&priv->battery_hook); + priv->bh_registered = false; +} + +/* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */ + +struct tunable_attr_01 { + struct device *dev; + u8 feature_id; + u8 device_id; + u8 type_id; + u8 cd_mode_id; /* mode arg for searching capdata */ + u8 cv_mode_id; /* mode arg for set/get current_value */ +}; + +/** + * tunable_attr_01_id() - Formats a tunable_attr_01 to a capdata attribute ID + * @attr: The tunable_attr_01 to format. + * @mode: The u8 corresponding to the wmi-gamezone mode for set/get. + * + * Return: encoded capability data attribute ID. + */ +static u32 tunable_attr_01_id(struct tunable_attr_01 *attr, u8 mode) +{ + return lwmi_attr_id(attr->device_id, attr->feature_id, mode, attr->type_id); +} + +static struct tunable_attr_01 ppt_pl1_spl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_spl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 ppt_pl2_sppt = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPPT, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl2_sppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 ppt_pl3_fppt = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_FPPT, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl3_fppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_FPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 cpu_temp = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_apu_spl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_APU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_cpu_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_CL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_tau = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TAU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 gpu_nv_ppab = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_PPAB, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ctgp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CTGP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_temp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ac_offset = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_AC_OFFSET, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_boost_clk = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_BOOST_CLK, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_enable = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_EN, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_mode = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_MODE, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_didvid = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_DIDVID, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_bpl = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_BPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_cpu_boost = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CPU_BOOST, + .type_id = LWMI_TYPE_ID_NONE, +}; + +struct capdata01_attr_group { + const struct attribute_group *attr_group; + struct tunable_attr_01 *tunable_attr; +}; + +/* Attribute Methods */ + +/** + * int_type_show() - Emit the data type for an integer attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * + * Return: Number of characters written to buf. + */ +static ssize_t int_type_show(struct kobject *kobj, struct kobj_attribute *kattr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +/** + * attr_capdata01_show() - Get the value of the specified attribute property + * + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * @tunable_attr: The attribute to be read. + * @prop: The property of this attribute to be read. + * + * Retrieves the given property from the capability data 01 struct for the + * specified attribute's "custom" thermal mode. This function is intended + * to be generic so it can be called from any integer attributes "_show" + * function. + * + * If the WMI is success the sysfs attribute is notified. + * + * Return: Either number of characters written to buf, or an error code. + */ +static ssize_t attr_capdata01_show(struct kobject *kobj, + struct kobj_attribute *kattr, char *buf, + struct tunable_attr_01 *tunable_attr, + enum attribute_property prop) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct capdata01 capdata; + u32 attribute_id; + int value, ret; + + attribute_id = tunable_attr_01_id(tunable_attr, tunable_attr->cd_mode_id); + + ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata); + if (ret) + return ret; + + switch (prop) { + case DEFAULT_VAL: + value = capdata.default_value; + break; + case MAX_VAL: + value = capdata.max_value; + break; + case MIN_VAL: + value = capdata.min_value; + break; + case STEP_VAL: + value = capdata.step; + break; + default: + return -EINVAL; + } + + return sysfs_emit(buf, "%d\n", value); +} + +/** + * attr_current_value_store() - Set the current value of the given attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to read from, this is parsed to `int` type. + * @count: Required by sysfs attribute macros, pass in from the callee attr. + * @tunable_attr: The attribute to be stored. + * + * Sets the value of the given attribute when operating under the "custom" + * smartfan profile. The current smartfan profile is retrieved from the + * lenovo-wmi-gamezone driver and error is returned if the result is not + * "custom". This function is intended to be generic so it can be called from + * any integer attribute's "_store" function. The integer to be sent to the WMI + * method is range checked and an error code is returned if out of range. + * + * If the value is valid and WMI is success, then the sysfs attribute is + * notified. + * + * Return: Either count, or an error code. + */ +static ssize_t attr_current_value_store(struct kobject *kobj, + struct kobj_attribute *kattr, + const char *buf, size_t count, + struct tunable_attr_01 *tunable_attr) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct wmi_method_args_32 args = {}; + struct capdata01 capdata; + enum thermal_mode mode; + u32 value; + int ret; + + ret = lwmi_tm_notifier_call(&mode); + if (ret) + return ret; + + if (mode != LWMI_GZ_THERMAL_MODE_CUSTOM) + return -EBUSY; + + args.arg0 = tunable_attr_01_id(tunable_attr, tunable_attr->cd_mode_id); + + ret = lwmi_cd01_get_data(priv->cd01_list, args.arg0, &capdata); + if (ret) + return ret; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value < capdata.min_value || value > capdata.max_value) + return -EINVAL; + + args.arg0 = tunable_attr_01_id(tunable_attr, tunable_attr->cv_mode_id); + args.arg1 = value; + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, + (unsigned char *)&args, sizeof(args), NULL); + if (ret) + return ret; + + return count; +}; + +/** + * attr_current_value_show() - Get the current value of the given attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * @tunable_attr: The attribute to be read. + * + * Retrieves the value of the given attribute for the current smartfan profile. + * The current smartfan profile is retrieved from the lenovo-wmi-gamezone driver. + * This function is intended to be generic so it can be called from any integer + * attribute's "_show" function. + * + * If the WMI is success the sysfs attribute is notified. + * + * Return: Either number of characters written to buf, or an error code. + */ +static ssize_t attr_current_value_show(struct kobject *kobj, + struct kobj_attribute *kattr, char *buf, + struct tunable_attr_01 *tunable_attr) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct wmi_method_args_32 args = {}; + enum thermal_mode mode; + int retval; + int ret; + + ret = lwmi_tm_notifier_call(&mode); + if (ret) + return ret; + + /* If "no-mode" is the supported mode, ensure we never send current mode */ + if (tunable_attr->cv_mode_id == LWMI_GZ_THERMAL_MODE_NONE) + mode = tunable_attr->cv_mode_id; + + args.arg0 = tunable_attr_01_id(tunable_attr, mode); + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (unsigned char *)&args, sizeof(args), + &retval); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", retval); +} + +/** + * lwmi_attr_01_is_supported() - Determine if the given attribute is supported. + * @tunable_attr: The attribute to verify. + * + * For an attribute to be supported it must have a functional get/set method, + * as well as associated capability data stored in the capdata01 table. + * + * First check if the attribute has a corresponding data table under custom mode + * (0xff), then under no mode (0x00). If either of those passes, check if the + * supported field of the capdata struct is > 0. If it is supported, store the + * successful mode in the cd_mode_id field of tunable_attr. + * + * If the attribute capdata shows it is supported, attempt to determine the mode + * for the current value property get/set methods using a similar pattern to the + * capdata table check. If the value returned by either mode is 0 or an error, + * assume that mode is not supported. Otherwise, store the successful mode in the + * cv_mode_id field of tunable_attr. + * + * If any of the above checks fail then the attribute is not fully supported. + * + * Return: true if capdata and set/get modes are found, otherwise false. + */ +static bool lwmi_attr_01_is_supported(struct tunable_attr_01 *tunable_attr) +{ + u8 modes[2] = { LWMI_GZ_THERMAL_MODE_CUSTOM, LWMI_GZ_THERMAL_MODE_NONE }; + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct wmi_method_args_32 args = {}; + bool cd_mode_found = false; + bool cv_mode_found = false; + struct capdata01 capdata; + int retval, ret, i; + + /* Determine tunable_attr->cd_mode_id */ + for (i = 0; i < ARRAY_SIZE(modes); i++) { + args.arg0 = tunable_attr_01_id(tunable_attr, modes[i]); + + ret = lwmi_cd01_get_data(priv->cd01_list, args.arg0, &capdata); + if (ret || !capdata.supported) + continue; + + tunable_attr->cd_mode_id = modes[i]; + cd_mode_found = true; + break; + } + + if (!cd_mode_found) + return cd_mode_found; + + dev_dbg(tunable_attr->dev, + "cd_mode_id: %#010x\n", args.arg0); + + /* Determine tunable_attr->cv_mode_id, returns 1 if supported */ + for (i = 0; i < ARRAY_SIZE(modes); i++) { + args.arg0 = tunable_attr_01_id(tunable_attr, modes[i]); + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (u8 *)&args, sizeof(args), + &retval); + if (ret || !retval) + continue; + + tunable_attr->cv_mode_id = modes[i]; + cv_mode_found = true; + break; + } + + if (!cv_mode_found) + return cv_mode_found; + + dev_dbg(tunable_attr->dev, "cv_mode_id: %#010x, attribute support level: %#010x\n", + args.arg0, capdata.supported); + + return capdata.supported > 0; +} + +/* Lenovo WMI Other Mode Attribute macros */ +#define __LWMI_ATTR_RO(_func, _name) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _func##_##_name##_show, \ + } + +#define __LWMI_ATTR_RO_AS(_name, _show) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _show, \ + } + +#define __LWMI_ATTR_RW(_func, _name) \ + __ATTR(_name, 0644, _func##_##_name##_show, _func##_##_name##_store) + +/* Shows a formatted static variable */ +#define __LWMI_ATTR_SHOW_FMT(_prop, _attrname, _fmt, _val) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return sysfs_emit(buf, _fmt, _val); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __LWMI_ATTR_RO(_attrname, _prop) + +/* Attribute current value read/write */ +#define __LWMI_TUNABLE_CURRENT_VALUE_CAP01(_attrname) \ + static ssize_t _attrname##_current_value_store( \ + struct kobject *kobj, struct kobj_attribute *kattr, \ + const char *buf, size_t count) \ + { \ + return attr_current_value_store(kobj, kattr, buf, count, \ + &_attrname); \ + } \ + static ssize_t _attrname##_current_value_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return attr_current_value_show(kobj, kattr, buf, &_attrname); \ + } \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __LWMI_ATTR_RW(_attrname, current_value) + +/* Attribute property read only */ +#define __LWMI_TUNABLE_RO_CAP01(_prop, _attrname, _prop_type) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return attr_capdata01_show(kobj, kattr, buf, &_attrname, \ + _prop_type); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __LWMI_ATTR_RO(_attrname, _prop) + +#define LWMI_ATTR_GROUP_TUNABLE_CAP01(_attrname, _fsname, _dispname) \ + __LWMI_TUNABLE_CURRENT_VALUE_CAP01(_attrname); \ + __LWMI_TUNABLE_RO_CAP01(default_value, _attrname, DEFAULT_VAL); \ + __LWMI_ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + __LWMI_TUNABLE_RO_CAP01(max_value, _attrname, MAX_VAL); \ + __LWMI_TUNABLE_RO_CAP01(min_value, _attrname, MIN_VAL); \ + __LWMI_TUNABLE_RO_CAP01(scalar_increment, _attrname, STEP_VAL); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __LWMI_ATTR_RO_AS(type, int_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_default_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_max_value.attr, \ + &attr_##_attrname##_min_value.attr, \ + &attr_##_attrname##_scalar_increment.attr, \ + &attr_##_attrname##_type.attr, \ + NULL, \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +/* CPU tunable attributes */ +LWMI_ATTR_GROUP_TUNABLE_CAP01(cpu_temp, "cpu_temp", + "Set the CPU thermal load limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_cpu_cl, "ppt_cpu_cl", + "Set the CPU cross loading power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_apu_spl, "ppt_pl1_apu_spl", + "Set the APU sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl, "ppt_pl1_spl", + "Set the CPU sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl_cl, "ppt_pl1_spl_cl", + "Set the CPU cross loading sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt, "ppt_pl2_sppt", + "Set the CPU slow package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt_cl, "ppt_pl2_sppt_cl", + "Set the CPU cross loading slow package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt, "ppt_pl3_fppt", + "Set the CPU fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt_cl, "ppt_pl3_fppt_cl", + "Set the CPU cross loading fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_tau, "ppt_pl1_tau", + "Set the CPU sustained power limit exceed duration"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl, "ppt_pl4_ipl", + "Set the CPU instantaneous power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl_cl, "ppt_pl4_ipl_cl", + "Set the CPU cross loading instantaneous power limit"); + +/* GPU tunable attributes */ +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_boost_clk, "dgpu_boost_clk", + "Set the dedicated GPU boost clock"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_didvid, "dgpu_didvid", + "Get the GPU device identifier and vendor identifier"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_enable, "dgpu_enable", + "Set the dedicated Nvidia GPU enabled status"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_mode, "gpu_mode", + "Set the GPU mode by power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ac_offset, "gpu_nv_ac_offset", + "Set the Nvidia GPU AC total processing power baseline offset"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_bpl, "gpu_nv_bpl", + "Set the Nvidia GPU base power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_cpu_boost, "gpu_nv_cpu_boost", + "Set the Nvidia GPU to CPU dynamic boost limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ctgp, "gpu_nv_ctgp", + "Set the GPU configurable total graphics power"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ppab, "gpu_nv_ppab", + "Set the Nvidia GPU power performance aware boost limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_temp, "gpu_temp", + "Set the GPU thermal load limit"); + +static struct capdata01_attr_group cd01_attr_groups[] = { + { &cpu_temp_attr_group, &cpu_temp }, + { &dgpu_boost_clk_attr_group, &dgpu_boost_clk }, + { &dgpu_didvid_attr_group, &dgpu_didvid }, + { &dgpu_enable_attr_group, &dgpu_enable }, + { &gpu_mode_attr_group, &gpu_mode }, + { &gpu_nv_ac_offset_attr_group, &gpu_nv_ac_offset }, + { &gpu_nv_bpl_attr_group, &gpu_nv_bpl }, + { &gpu_nv_cpu_boost_attr_group, &gpu_nv_cpu_boost }, + { &gpu_nv_ctgp_attr_group, &gpu_nv_ctgp }, + { &gpu_nv_ppab_attr_group, &gpu_nv_ppab }, + { &gpu_temp_attr_group, &gpu_temp }, + { &ppt_cpu_cl_attr_group, &ppt_cpu_cl }, + { &ppt_pl1_apu_spl_attr_group, &ppt_pl1_apu_spl }, + { &ppt_pl1_spl_attr_group, &ppt_pl1_spl }, + { &ppt_pl1_spl_cl_attr_group, &ppt_pl1_spl_cl }, + { &ppt_pl1_tau_attr_group, &ppt_pl1_tau }, + { &ppt_pl2_sppt_attr_group, &ppt_pl2_sppt }, + { &ppt_pl2_sppt_cl_attr_group, &ppt_pl2_sppt_cl }, + { &ppt_pl3_fppt_attr_group, &ppt_pl3_fppt }, + { &ppt_pl3_fppt_cl_attr_group, &ppt_pl3_fppt_cl }, + { &ppt_pl4_ipl_attr_group, &ppt_pl4_ipl }, + { &ppt_pl4_ipl_cl_attr_group, &ppt_pl4_ipl_cl }, + {}, +}; + +/** + * lwmi_om_fw_attr_add() - Register all firmware_attributes_class members + * @priv: The Other Mode driver data. + */ +static void lwmi_om_fw_attr_add(struct lwmi_om_priv *priv) +{ + unsigned int i; + int err; + + err = ida_alloc(&lwmi_om_ida, GFP_KERNEL); + if (err < 0) + goto err_no_ida; + + priv->ida_id = err; + + priv->fw_attr_dev = device_create(&firmware_attributes_class, NULL, + MKDEV(0, 0), NULL, "%s-%u", + LWMI_OM_SYSFS_NAME, priv->ida_id); + if (IS_ERR(priv->fw_attr_dev)) { + err = PTR_ERR(priv->fw_attr_dev); + goto err_free_ida; + } + + priv->fw_attr_kset = kset_create_and_add("attributes", NULL, + &priv->fw_attr_dev->kobj); + if (!priv->fw_attr_kset) { + err = -ENOMEM; + goto err_destroy_classdev; + } + + for (i = 0; i < ARRAY_SIZE(cd01_attr_groups) - 1; i++) { + cd01_attr_groups[i].tunable_attr->dev = &priv->wdev->dev; + if (!lwmi_attr_01_is_supported(cd01_attr_groups[i].tunable_attr)) + continue; + + err = sysfs_create_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + if (err) + goto err_remove_groups; + } + return; + +err_remove_groups: + while (i--) + sysfs_remove_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + + kset_unregister(priv->fw_attr_kset); + +err_destroy_classdev: + device_unregister(priv->fw_attr_dev); + +err_free_ida: + ida_free(&lwmi_om_ida, priv->ida_id); + +err_no_ida: + priv->ida_id = -EIDRM; + + dev_warn(&priv->wdev->dev, + "failed to register firmware-attributes device: %d\n", err); +} + +/** + * lwmi_om_fw_attr_remove() - Unregister all capability data attribute groups + * @priv: the lenovo-wmi-other driver data. + */ +static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv) +{ + if (priv->ida_id < 0) + return; + + for (unsigned int i = 0; i < ARRAY_SIZE(cd01_attr_groups) - 1; i++) + sysfs_remove_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + + kset_unregister(priv->fw_attr_kset); + device_unregister(priv->fw_attr_dev); + ida_free(&lwmi_om_ida, priv->ida_id); + priv->ida_id = -EIDRM; +} + +/* ======== Self (master: lenovo-wmi-other) ======== */ + +/** + * lwmi_om_master_bind() - Bind all components of the other mode driver + * @dev: The lenovo-wmi-other driver basic device. + * + * Call component_bind_all to bind the lenovo-wmi-capdata devices to the + * lenovo-wmi-other master driver, with a callback to collect fan info from + * capdata_fan. On success, assign the capability data list pointers to the + * driver data struct for later access. These pointers are only valid while the + * capdata interfaces exist. Finally, collect fan info from capdata00 and + * register all firmware attribute groups. Note that the HWMON device is + * registered only if all fan info is collected. Hence, it is not registered + * here. See lwmi_om_fan_info_collect_cd00() and + * lwmi_om_fan_info_collect_cd_fan(). + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_master_bind(struct device *dev) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + struct lwmi_cd_binder binder = { + .cd_fan_list_cb = lwmi_om_fan_info_collect_cd_fan, + }; + int ret; + + lwmi_om_fan_info_init(priv); + + ret = component_bind_all(dev, &binder); + if (ret) + return ret; + + priv->cd00_list = binder.cd00_list; + priv->cd01_list = binder.cd01_list; + if (!priv->cd00_list || !priv->cd01_list) { + component_unbind_all(dev, NULL); + + return -ENODEV; + } + + lwmi_om_fan_info_collect_cd00(priv); + lwmi_om_psy_ext_init(priv); + + lwmi_om_fw_attr_add(priv); + + return 0; +} + +/** + * lwmi_om_master_unbind() - Unbind all components of the other mode driver + * @dev: The lenovo-wmi-other driver basic device + * + * Unregister all firmware attribute groups and the HWMON device. Then call + * component_unbind_all to unbind lenovo-wmi-capdata devices from the + * lenovo-wmi-other master driver. + */ +static void lwmi_om_master_unbind(struct device *dev) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + + lwmi_om_fw_attr_remove(priv); + + lwmi_om_hwmon_remove(priv); + + lwmi_om_psy_remove(priv); + + component_unbind_all(dev, NULL); +} + +static const struct component_master_ops lwmi_om_master_ops = { + .bind = lwmi_om_master_bind, + .unbind = lwmi_om_master_unbind, +}; + +static int lwmi_other_probe(struct wmi_device *wdev, const void *context) +{ + struct component_match *master_match = NULL; + struct lwmi_om_priv *priv; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + /* Sentinel for on-demand ida_free(). */ + priv->ida_id = -EIDRM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + lwmi_cd_match_add_all(&wdev->dev, &master_match); + if (IS_ERR(master_match)) + return PTR_ERR(master_match); + + return component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops, + master_match); +} + +static void lwmi_other_remove(struct wmi_device *wdev) +{ + component_master_del(&wdev->dev, &lwmi_om_master_ops); +} + +static const struct wmi_device_id lwmi_other_id_table[] = { + { LENOVO_OTHER_MODE_GUID, NULL }, + {} +}; + +static struct wmi_driver lwmi_other_driver = { + .driver = { + .name = "lenovo_wmi_other", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lwmi_other_id_table, + .probe = lwmi_other_probe, + .remove = lwmi_other_remove, + .no_singleton = true, +}; + +module_wmi_driver(lwmi_other_driver); + +MODULE_IMPORT_NS("LENOVO_WMI_CAPDATA"); +MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); +MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_AUTHOR("Rong Zhang "); +MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/ymc.c b/drivers/platform/x86/lenovo/ymc.c new file mode 100644 index 000000000..1b73a55f1 --- /dev/null +++ b/drivers/platform/x86/lenovo/ymc.c @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * lenovo-ymc.c - Lenovo Yoga Mode Control driver + * + * Copyright © 2022 Gergo Koteles + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include "ideapad-laptop.h" + +#define LENOVO_YMC_EVENT_GUID "06129D99-6083-4164-81AD-F092F9D773A6" +#define LENOVO_YMC_QUERY_GUID "09B0EE6E-C3FD-4243-8DA1-7911FF80BB8C" + +#define LENOVO_YMC_QUERY_INSTANCE 0 +#define LENOVO_YMC_QUERY_METHOD 0x01 + +static bool force; +module_param(force, bool, 0444); +MODULE_PARM_DESC(force, "Force loading on boards without a convertible DMI chassis-type"); + +static const struct dmi_system_id allowed_chasis_types_dmi_table[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */), + }, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */), + }, + }, + { } +}; + +struct lenovo_ymc_private { + struct input_dev *input_dev; +}; + +static const struct key_entry lenovo_ymc_keymap[] = { + /* Ignore the uninitialized state */ + { KE_IGNORE, 0x00 }, + /* Laptop */ + { KE_SW, 0x01, { .sw = { SW_TABLET_MODE, 0 } } }, + /* Tablet */ + { KE_SW, 0x02, { .sw = { SW_TABLET_MODE, 1 } } }, + /* Drawing Board */ + { KE_SW, 0x03, { .sw = { SW_TABLET_MODE, 1 } } }, + /* Tent */ + { KE_SW, 0x04, { .sw = { SW_TABLET_MODE, 1 } } }, + { KE_END }, +}; + +static void lenovo_ymc_notify(struct wmi_device *wdev, union acpi_object *data) +{ + struct lenovo_ymc_private *priv = dev_get_drvdata(&wdev->dev); + u32 input_val = 0; + struct acpi_buffer input = { sizeof(input_val), &input_val }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int code; + + status = wmi_evaluate_method(LENOVO_YMC_QUERY_GUID, + LENOVO_YMC_QUERY_INSTANCE, + LENOVO_YMC_QUERY_METHOD, + &input, &output); + + if (ACPI_FAILURE(status)) { + dev_warn(&wdev->dev, + "Failed to evaluate query method: %s\n", + acpi_format_exception(status)); + return; + } + + obj = output.pointer; + + if (obj->type != ACPI_TYPE_INTEGER) { + dev_warn(&wdev->dev, + "WMI event data is not an integer\n"); + goto free_obj; + } + code = obj->integer.value; + + if (!sparse_keymap_report_event(priv->input_dev, code, 1, true)) + dev_warn(&wdev->dev, "Unknown key %d pressed\n", code); + +free_obj: + kfree(obj); + ideapad_laptop_call_notifier(IDEAPAD_LAPTOP_YMC_EVENT, &code); +} + +static int lenovo_ymc_probe(struct wmi_device *wdev, const void *ctx) +{ + struct lenovo_ymc_private *priv; + struct input_dev *input_dev; + int err; + + if (!dmi_check_system(allowed_chasis_types_dmi_table)) { + if (force) + dev_info(&wdev->dev, "Force loading Lenovo YMC support\n"); + else + return -ENODEV; + } + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + input_dev = devm_input_allocate_device(&wdev->dev); + if (!input_dev) + return -ENOMEM; + + input_dev->name = "Lenovo Yoga Tablet Mode Control switch"; + input_dev->phys = LENOVO_YMC_EVENT_GUID "/input0"; + input_dev->id.bustype = BUS_HOST; + input_dev->dev.parent = &wdev->dev; + err = sparse_keymap_setup(input_dev, lenovo_ymc_keymap, NULL); + if (err) { + dev_err(&wdev->dev, + "Could not set up input device keymap: %d\n", err); + return err; + } + + err = input_register_device(input_dev); + if (err) { + dev_err(&wdev->dev, + "Could not register input device: %d\n", err); + return err; + } + + priv->input_dev = input_dev; + dev_set_drvdata(&wdev->dev, priv); + + /* Report the state for the first time on probe */ + lenovo_ymc_notify(wdev, NULL); + return 0; +} + +static const struct wmi_device_id lenovo_ymc_wmi_id_table[] = { + { .guid_string = LENOVO_YMC_EVENT_GUID }, + { } +}; +MODULE_DEVICE_TABLE(wmi, lenovo_ymc_wmi_id_table); + +static struct wmi_driver lenovo_ymc_driver = { + .driver = { + .name = "lenovo-ymc", + }, + .id_table = lenovo_ymc_wmi_id_table, + .min_event_size = sizeof(u32), + .probe = lenovo_ymc_probe, + .notify = lenovo_ymc_notify, +}; + +module_wmi_driver(lenovo_ymc_driver); + +MODULE_AUTHOR("Gergo Koteles "); +MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("IDEAPAD_LAPTOP"); diff --git a/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c new file mode 100644 index 000000000..8551ab4d2 --- /dev/null +++ b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Support for the custom fast charging protocol found on the Lenovo Yoga + * Tablet 2 1380F / 1380L models. + * + * Copyright (C) 2024 Hans de Goede + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../serdev_helpers.h" + +#define YT2_1380_FC_PDEV_NAME "lenovo-yoga-tab2-pro-1380-fastcharger" +#define YT2_1380_FC_SERDEV_CTRL "serial0" +#define YT2_1380_FC_SERDEV_NAME "serial0-0" +#define YT2_1380_FC_EXTCON_NAME "i2c-lc824206xa" + +#define YT2_1380_FC_MAX_TRIES 5 +#define YT2_1380_FC_PIN_SW_DELAY_US (10 * USEC_PER_MSEC) +#define YT2_1380_FC_UART_DRAIN_DELAY_US (50 * USEC_PER_MSEC) +#define YT2_1380_FC_VOLT_SW_DELAY_US (1000 * USEC_PER_MSEC) + +struct yt2_1380_fc { + struct device *dev; + struct pinctrl *pinctrl; + struct pinctrl_state *gpio_state; + struct pinctrl_state *uart_state; + struct gpio_desc *uart3_txd; + struct gpio_desc *uart3_rxd; + struct extcon_dev *extcon; + struct notifier_block nb; + struct work_struct work; + bool fast_charging; +}; + +static int yt2_1380_fc_set_gpio_mode(struct yt2_1380_fc *fc, bool enable) +{ + struct pinctrl_state *state = enable ? fc->gpio_state : fc->uart_state; + int ret; + + ret = pinctrl_select_state(fc->pinctrl, state); + if (ret) { + dev_err(fc->dev, "Error %d setting pinctrl state\n", ret); + return ret; + } + + fsleep(YT2_1380_FC_PIN_SW_DELAY_US); + return 0; +} + +static bool yt2_1380_fc_dedicated_charger_connected(struct yt2_1380_fc *fc) +{ + return extcon_get_state(fc->extcon, EXTCON_CHG_USB_DCP) > 0; +} + +static bool yt2_1380_fc_fast_charger_connected(struct yt2_1380_fc *fc) +{ + return extcon_get_state(fc->extcon, EXTCON_CHG_USB_FAST) > 0; +} + +static void yt2_1380_fc_worker(struct work_struct *work) +{ + struct yt2_1380_fc *fc = container_of(work, struct yt2_1380_fc, work); + int i, ret; + + /* Do nothing if already fast charging */ + if (yt2_1380_fc_fast_charger_connected(fc)) + return; + + for (i = 0; i < YT2_1380_FC_MAX_TRIES; i++) { + /* Set pins to UART mode (for charger disconnect and retries) */ + ret = yt2_1380_fc_set_gpio_mode(fc, false); + if (ret) + return; + + /* Only try 12V charging if a dedicated charger is detected */ + if (!yt2_1380_fc_dedicated_charger_connected(fc)) + return; + + /* Send the command to switch to 12V charging */ + ret = serdev_device_write_buf(to_serdev_device(fc->dev), "SC", strlen("SC")); + if (ret != strlen("SC")) { + dev_err(fc->dev, "Error %d writing to uart\n", ret); + return; + } + + fsleep(YT2_1380_FC_UART_DRAIN_DELAY_US); + + /* Re-check a charger is still connected */ + if (!yt2_1380_fc_dedicated_charger_connected(fc)) + return; + + /* + * Now switch the lines to GPIO (output, high). The charger + * expects the lines being driven high after the command. + * Presumably this is used to detect the tablet getting + * unplugged (to switch back to 5V output on unplug). + */ + ret = yt2_1380_fc_set_gpio_mode(fc, true); + if (ret) + return; + + fsleep(YT2_1380_FC_VOLT_SW_DELAY_US); + + if (yt2_1380_fc_fast_charger_connected(fc)) + return; /* Success */ + } + + dev_dbg(fc->dev, "Failed to switch to 12V charging (not the original charger?)\n"); + /* Failed to enable 12V fast charging, reset pins to default UART mode */ + yt2_1380_fc_set_gpio_mode(fc, false); +} + +static int yt2_1380_fc_extcon_evt(struct notifier_block *nb, + unsigned long event, void *param) +{ + struct yt2_1380_fc *fc = container_of(nb, struct yt2_1380_fc, nb); + + schedule_work(&fc->work); + return NOTIFY_OK; +} + +static size_t yt2_1380_fc_receive(struct serdev_device *serdev, const u8 *data, size_t len) +{ + /* + * Since the USB data lines are shorted for DCP detection, echos of + * the "SC" command send in yt2_1380_fc_worker() will be received. + */ + dev_dbg(&serdev->dev, "recv: %*ph\n", (int)len, data); + return len; +} + +static const struct serdev_device_ops yt2_1380_fc_serdev_ops = { + .receive_buf = yt2_1380_fc_receive, + .write_wakeup = serdev_device_write_wakeup, +}; + +static int yt2_1380_fc_serdev_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + struct yt2_1380_fc *fc; + int ret; + + fc = devm_kzalloc(dev, sizeof(*fc), GFP_KERNEL); + if (!fc) + return -ENOMEM; + + fc->dev = dev; + fc->nb.notifier_call = yt2_1380_fc_extcon_evt; + INIT_WORK(&fc->work, yt2_1380_fc_worker); + + /* + * Do this first since it may return -EPROBE_DEFER. + * There is no extcon_put(), so there is no need to free this. + */ + fc->extcon = extcon_get_extcon_dev(YT2_1380_FC_EXTCON_NAME); + if (IS_ERR(fc->extcon)) + return dev_err_probe(dev, PTR_ERR(fc->extcon), "getting extcon\n"); + + fc->pinctrl = devm_pinctrl_get(dev); + if (IS_ERR(fc->pinctrl)) + return dev_err_probe(dev, PTR_ERR(fc->pinctrl), "getting pinctrl\n"); + + /* + * To switch the UART3 pins connected to the USB data lines between + * UART and GPIO modes. + */ + fc->gpio_state = pinctrl_lookup_state(fc->pinctrl, "uart3_gpio"); + fc->uart_state = pinctrl_lookup_state(fc->pinctrl, "uart3_uart"); + if (IS_ERR(fc->gpio_state) || IS_ERR(fc->uart_state)) + return dev_err_probe(dev, -EINVAL, "getting pinctrl states\n"); + + ret = yt2_1380_fc_set_gpio_mode(fc, true); + if (ret) + return ret; + + fc->uart3_txd = devm_gpiod_get(dev, "uart3_txd", GPIOD_OUT_HIGH); + if (IS_ERR(fc->uart3_txd)) + return dev_err_probe(dev, PTR_ERR(fc->uart3_txd), "getting uart3_txd gpio\n"); + + fc->uart3_rxd = devm_gpiod_get(dev, "uart3_rxd", GPIOD_OUT_HIGH); + if (IS_ERR(fc->uart3_rxd)) + return dev_err_probe(dev, PTR_ERR(fc->uart3_rxd), "getting uart3_rxd gpio\n"); + + ret = yt2_1380_fc_set_gpio_mode(fc, false); + if (ret) + return ret; + + serdev_device_set_drvdata(serdev, fc); + serdev_device_set_client_ops(serdev, &yt2_1380_fc_serdev_ops); + + ret = devm_serdev_device_open(dev, serdev); + if (ret) + return dev_err_probe(dev, ret, "opening UART device\n"); + + serdev_device_set_baudrate(serdev, 600); + serdev_device_set_flow_control(serdev, false); + + ret = devm_extcon_register_notifier_all(dev, fc->extcon, &fc->nb); + if (ret) + return dev_err_probe(dev, ret, "registering extcon notifier\n"); + + /* In case the extcon already has detected a DCP charger */ + schedule_work(&fc->work); + + return 0; +} + +static struct serdev_device_driver yt2_1380_fc_serdev_driver = { + .probe = yt2_1380_fc_serdev_probe, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +static const struct pinctrl_map yt2_1380_fc_pinctrl_map[] = { + PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_uart", + "INT33FC:00", "uart3_grp", "uart"), + PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_gpio", + "INT33FC:00", "uart3_grp_gpio", "gpio"), +}; + +static int yt2_1380_fc_pdev_probe(struct platform_device *pdev) +{ + struct serdev_device *serdev; + struct device *ctrl_dev; + int ret; + + /* Register pinctrl mappings for setting the UART3 pins mode */ + ret = devm_pinctrl_register_mappings(&pdev->dev, yt2_1380_fc_pinctrl_map, + ARRAY_SIZE(yt2_1380_fc_pinctrl_map)); + if (ret) + return ret; + + /* And create the serdev to talk to the charger over the UART3 pins */ + ctrl_dev = get_serdev_controller("PNP0501", "1", 0, YT2_1380_FC_SERDEV_CTRL); + if (IS_ERR(ctrl_dev)) + return PTR_ERR(ctrl_dev); + + serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); + put_device(ctrl_dev); + if (!serdev) + return -ENOMEM; + + /* Propagate pdev-fwnode set by x86-android-tablets to serdev */ + device_set_node(&serdev->dev, dev_fwnode(&pdev->dev)); + /* The fwnode is a managed node, so it will be auto-put on serdev_device_put() */ + fwnode_handle_get(dev_fwnode(&serdev->dev)); + + ret = serdev_device_add(serdev); + if (ret) { + serdev_device_put(serdev); + return dev_err_probe(&pdev->dev, ret, "adding serdev\n"); + } + + /* + * serdev device <-> driver matching relies on OF or ACPI matches and + * neither is available here, manually bind the driver. + */ + ret = device_driver_attach(&yt2_1380_fc_serdev_driver.driver, &serdev->dev); + if (ret) { + /* device_driver_attach() maps EPROBE_DEFER to EAGAIN, map it back */ + serdev_device_remove(serdev); + return dev_err_probe(&pdev->dev, + (ret == -EAGAIN) ? -EPROBE_DEFER : ret, + "attaching serdev driver\n"); + } + + /* So that yt2_1380_fc_pdev_remove() can remove the serdev */ + platform_set_drvdata(pdev, serdev); + return 0; +} + +static void yt2_1380_fc_pdev_remove(struct platform_device *pdev) +{ + struct serdev_device *serdev = platform_get_drvdata(pdev); + + serdev_device_remove(serdev); +} + +static struct platform_driver yt2_1380_fc_pdev_driver = { + .probe = yt2_1380_fc_pdev_probe, + .remove = yt2_1380_fc_pdev_remove, + .driver = { + .name = YT2_1380_FC_PDEV_NAME, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static int __init yt2_1380_fc_module_init(void) +{ + int ret; + + /* + * serdev driver MUST be registered first because pdev driver calls + * device_driver_attach() on the serdev, serdev-driver pair. + */ + ret = serdev_device_driver_register(&yt2_1380_fc_serdev_driver); + if (ret) + return ret; + + ret = platform_driver_register(&yt2_1380_fc_pdev_driver); + if (ret) + serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver); + + return ret; +} +module_init(yt2_1380_fc_module_init); + +static void __exit yt2_1380_fc_module_exit(void) +{ + platform_driver_unregister(&yt2_1380_fc_pdev_driver); + serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver); +} +module_exit(yt2_1380_fc_module_exit); + +MODULE_ALIAS("platform:" YT2_1380_FC_PDEV_NAME); +MODULE_DESCRIPTION("Lenovo Yoga Tablet 2 1380 fast charge driver"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/yogabook.c b/drivers/platform/x86/lenovo/yogabook.c new file mode 100644 index 000000000..1a4b2ab1f --- /dev/null +++ b/drivers/platform/x86/lenovo/yogabook.c @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Platform driver for Lenovo Yoga Book YB1-X90F/L tablets (Android model) + * WMI driver for Lenovo Yoga Book YB1-X91F/L tablets (Windows model) + * + * The keyboard half of the YB1 models can function as both a capacitive + * touch keyboard or as a Wacom digitizer, but not at the same time. + * + * This driver takes care of switching between the 2 functions. + * + * Copyright 2023 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define YB_MBTN_EVENT_GUID "243FEC1D-1963-41C1-8100-06A9D82A94B4" + +#define YB_KBD_BL_DEFAULT 128 +#define YB_KBD_BL_MAX 255 +#define YB_KBD_BL_PWM_PERIOD 13333 + +#define YB_PDEV_NAME "yogabook-touch-kbd-digitizer-switch" + +/* flags */ +enum { + YB_KBD_IS_ON, + YB_DIGITIZER_IS_ON, + YB_DIGITIZER_MODE, + YB_TABLET_MODE, + YB_SUSPENDED, +}; + +struct yogabook_data { + struct device *dev; + struct acpi_device *kbd_adev; + struct acpi_device *dig_adev; + struct device *kbd_dev; + struct device *dig_dev; + struct led_classdev *pen_led; + struct gpio_desc *pen_touch_event; + struct gpio_desc *kbd_bl_led_enable; + struct gpio_desc *backside_hall_gpio; + struct pwm_device *kbd_bl_pwm; + int (*set_kbd_backlight)(struct yogabook_data *data, uint8_t level); + int pen_touch_irq; + int backside_hall_irq; + struct work_struct work; + struct led_classdev kbd_bl_led; + unsigned long flags; + u8 brightness; +}; + +static void yogabook_work(struct work_struct *work) +{ + struct yogabook_data *data = container_of(work, struct yogabook_data, work); + bool kbd_on, digitizer_on; + int r; + + if (test_bit(YB_SUSPENDED, &data->flags)) + return; + + if (test_bit(YB_TABLET_MODE, &data->flags)) { + kbd_on = false; + digitizer_on = false; + } else if (test_bit(YB_DIGITIZER_MODE, &data->flags)) { + digitizer_on = true; + kbd_on = false; + } else { + kbd_on = true; + digitizer_on = false; + } + + if (!kbd_on && test_bit(YB_KBD_IS_ON, &data->flags)) { + /* + * Must be done before releasing the keyboard touchscreen driver, + * so that the keyboard touchscreen dev is still in D0. + */ + data->set_kbd_backlight(data, 0); + device_release_driver(data->kbd_dev); + clear_bit(YB_KBD_IS_ON, &data->flags); + } + + if (!digitizer_on && test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { + led_set_brightness(data->pen_led, LED_OFF); + device_release_driver(data->dig_dev); + clear_bit(YB_DIGITIZER_IS_ON, &data->flags); + } + + if (kbd_on && !test_bit(YB_KBD_IS_ON, &data->flags)) { + r = device_reprobe(data->kbd_dev); + if (r) + dev_warn(data->dev, "Reprobe of keyboard touchscreen failed: %d\n", r); + + data->set_kbd_backlight(data, data->brightness); + set_bit(YB_KBD_IS_ON, &data->flags); + } + + if (digitizer_on && !test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { + r = device_reprobe(data->dig_dev); + if (r) + dev_warn(data->dev, "Reprobe of digitizer failed: %d\n", r); + + led_set_brightness(data->pen_led, LED_FULL); + set_bit(YB_DIGITIZER_IS_ON, &data->flags); + } +} + +static void yogabook_toggle_digitizer_mode(struct yogabook_data *data) +{ + if (test_bit(YB_SUSPENDED, &data->flags)) + return; + + if (test_bit(YB_DIGITIZER_MODE, &data->flags)) + clear_bit(YB_DIGITIZER_MODE, &data->flags); + else + set_bit(YB_DIGITIZER_MODE, &data->flags); + + /* + * We are called from the ACPI core and the driver [un]binding which is + * done also needs ACPI functions, use a workqueue to avoid deadlocking. + */ + schedule_work(&data->work); +} + +static irqreturn_t yogabook_backside_hall_irq(int irq, void *_data) +{ + struct yogabook_data *data = _data; + + if (gpiod_get_value(data->backside_hall_gpio)) + set_bit(YB_TABLET_MODE, &data->flags); + else + clear_bit(YB_TABLET_MODE, &data->flags); + + schedule_work(&data->work); + + return IRQ_HANDLED; +} + +#define kbd_led_to_yogabook(cdev) container_of(cdev, struct yogabook_data, kbd_bl_led) + +static enum led_brightness kbd_brightness_get(struct led_classdev *cdev) +{ + struct yogabook_data *data = kbd_led_to_yogabook(cdev); + + return data->brightness; +} + +static int kbd_brightness_set(struct led_classdev *cdev, + enum led_brightness value) +{ + struct yogabook_data *data = kbd_led_to_yogabook(cdev); + + if ((value < 0) || (value > YB_KBD_BL_MAX)) + return -EINVAL; + + data->brightness = value; + + if (!test_bit(YB_KBD_IS_ON, &data->flags)) + return 0; + + return data->set_kbd_backlight(data, data->brightness); +} + +static struct gpiod_lookup_table yogabook_gpios = { + .table = { + GPIO_LOOKUP("INT33FF:02", 18, "backside_hall_sw", GPIO_ACTIVE_LOW), + {} + }, +}; + +static struct led_lookup_data yogabook_pen_led = { + .provider = "platform::indicator", + .con_id = "pen-icon-led", +}; + +static int yogabook_probe(struct device *dev, struct yogabook_data *data, + const char *kbd_bl_led_name) +{ + int r; + + data->dev = dev; + data->brightness = YB_KBD_BL_DEFAULT; + set_bit(YB_KBD_IS_ON, &data->flags); + set_bit(YB_DIGITIZER_IS_ON, &data->flags); + INIT_WORK(&data->work, yogabook_work); + + yogabook_pen_led.dev_id = dev_name(dev); + led_add_lookup(&yogabook_pen_led); + data->pen_led = devm_led_get(dev, "pen-icon-led"); + led_remove_lookup(&yogabook_pen_led); + + if (IS_ERR(data->pen_led)) + return dev_err_probe(dev, PTR_ERR(data->pen_led), "Getting pen icon LED\n"); + + yogabook_gpios.dev_id = dev_name(dev); + gpiod_add_lookup_table(&yogabook_gpios); + data->backside_hall_gpio = devm_gpiod_get(dev, "backside_hall_sw", GPIOD_IN); + gpiod_remove_lookup_table(&yogabook_gpios); + + if (IS_ERR(data->backside_hall_gpio)) + return dev_err_probe(dev, PTR_ERR(data->backside_hall_gpio), + "Getting backside_hall_sw GPIO\n"); + + r = gpiod_to_irq(data->backside_hall_gpio); + if (r < 0) + return dev_err_probe(dev, r, "Getting backside_hall_sw IRQ\n"); + + data->backside_hall_irq = r; + + /* Set default brightness before enabling the IRQ */ + data->set_kbd_backlight(data, YB_KBD_BL_DEFAULT); + + r = request_irq(data->backside_hall_irq, yogabook_backside_hall_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "backside_hall_sw", data); + if (r) + return dev_err_probe(dev, r, "Requesting backside_hall_sw IRQ\n"); + + schedule_work(&data->work); + + data->kbd_bl_led.name = kbd_bl_led_name; + data->kbd_bl_led.brightness_set_blocking = kbd_brightness_set; + data->kbd_bl_led.brightness_get = kbd_brightness_get; + data->kbd_bl_led.max_brightness = YB_KBD_BL_MAX; + + r = devm_led_classdev_register(dev, &data->kbd_bl_led); + if (r < 0) { + dev_err_probe(dev, r, "Registering backlight LED device\n"); + goto error_free_irq; + } + + dev_set_drvdata(dev, data); + return 0; + +error_free_irq: + free_irq(data->backside_hall_irq, data); + cancel_work_sync(&data->work); + return r; +} + +static void yogabook_remove(struct yogabook_data *data) +{ + int r = 0; + + free_irq(data->backside_hall_irq, data); + cancel_work_sync(&data->work); + + if (!test_bit(YB_KBD_IS_ON, &data->flags)) + r |= device_reprobe(data->kbd_dev); + + if (!test_bit(YB_DIGITIZER_IS_ON, &data->flags)) + r |= device_reprobe(data->dig_dev); + + if (r) + dev_warn(data->dev, "Reprobe of devices failed\n"); +} + +static int yogabook_suspend(struct device *dev) +{ + struct yogabook_data *data = dev_get_drvdata(dev); + + set_bit(YB_SUSPENDED, &data->flags); + flush_work(&data->work); + + if (test_bit(YB_KBD_IS_ON, &data->flags)) + data->set_kbd_backlight(data, 0); + + return 0; +} + +static int yogabook_resume(struct device *dev) +{ + struct yogabook_data *data = dev_get_drvdata(dev); + + if (test_bit(YB_KBD_IS_ON, &data->flags)) + data->set_kbd_backlight(data, data->brightness); + + clear_bit(YB_SUSPENDED, &data->flags); + + /* Check for YB_TABLET_MODE changes made during suspend */ + schedule_work(&data->work); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(yogabook_pm_ops, yogabook_suspend, yogabook_resume); + +/********** WMI driver code **********/ + +/* + * To control keyboard backlight, call the method KBLC() of the TCS1 ACPI + * device (Goodix touchpad acts as virtual sensor keyboard). + */ +static int yogabook_wmi_set_kbd_backlight(struct yogabook_data *data, + uint8_t level) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_object_list input; + union acpi_object param; + acpi_status status; + + dev_dbg(data->dev, "Set KBLC level to %u\n", level); + + /* Ensure keyboard touchpad is on before we call KBLC() */ + acpi_device_set_power(data->kbd_adev, ACPI_STATE_D0); + + input.count = 1; + input.pointer = ¶m; + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = YB_KBD_BL_MAX - level; + + status = acpi_evaluate_object(acpi_device_handle(data->kbd_adev), "KBLC", + &input, &output); + if (ACPI_FAILURE(status)) { + dev_err(data->dev, "Failed to call KBLC method: 0x%x\n", status); + return status; + } + + kfree(output.pointer); + return 0; +} + +static int yogabook_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct device *dev = &wdev->dev; + struct yogabook_data *data; + int r; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->kbd_adev = acpi_dev_get_first_match_dev("GDIX1001", NULL, -1); + if (!data->kbd_adev) + return dev_err_probe(dev, -ENODEV, + "Cannot find the touchpad device in ACPI tables\n"); + + data->dig_adev = acpi_dev_get_first_match_dev("WCOM0019", NULL, -1); + if (!data->dig_adev) { + r = dev_err_probe(dev, -ENODEV, + "Cannot find the digitizer device in ACPI tables\n"); + goto error_put_devs; + } + + data->kbd_dev = get_device(acpi_get_first_physical_node(data->kbd_adev)); + if (!data->kbd_dev || !data->kbd_dev->driver) { + r = -EPROBE_DEFER; + goto error_put_devs; + } + + data->dig_dev = get_device(acpi_get_first_physical_node(data->dig_adev)); + if (!data->dig_dev || !data->dig_dev->driver) { + r = -EPROBE_DEFER; + goto error_put_devs; + } + + data->set_kbd_backlight = yogabook_wmi_set_kbd_backlight; + + r = yogabook_probe(dev, data, "ybwmi::kbd_backlight"); + if (r) + goto error_put_devs; + + return 0; + +error_put_devs: + put_device(data->dig_dev); + put_device(data->kbd_dev); + acpi_dev_put(data->dig_adev); + acpi_dev_put(data->kbd_adev); + return r; +} + +static void yogabook_wmi_remove(struct wmi_device *wdev) +{ + struct yogabook_data *data = dev_get_drvdata(&wdev->dev); + + yogabook_remove(data); + + put_device(data->dig_dev); + put_device(data->kbd_dev); + acpi_dev_put(data->dig_adev); + acpi_dev_put(data->kbd_adev); +} + +static void yogabook_wmi_notify(struct wmi_device *wdev, union acpi_object *dummy) +{ + yogabook_toggle_digitizer_mode(dev_get_drvdata(&wdev->dev)); +} + +static const struct wmi_device_id yogabook_wmi_id_table[] = { + { + .guid_string = YB_MBTN_EVENT_GUID, + }, + { } /* Terminating entry */ +}; +MODULE_DEVICE_TABLE(wmi, yogabook_wmi_id_table); + +static struct wmi_driver yogabook_wmi_driver = { + .driver = { + .name = "yogabook-wmi", + .pm = pm_sleep_ptr(&yogabook_pm_ops), + }, + .id_table = yogabook_wmi_id_table, + .min_event_size = 0, + .probe = yogabook_wmi_probe, + .remove = yogabook_wmi_remove, + .notify = yogabook_wmi_notify, +}; + +/********** platform driver code **********/ + +static struct gpiod_lookup_table yogabook_pdev_gpios = { + .dev_id = YB_PDEV_NAME, + .table = { + GPIO_LOOKUP("INT33FF:00", 95, "pen_touch_event", GPIO_ACTIVE_HIGH), + GPIO_LOOKUP("INT33FF:03", 52, "enable_keyboard_led", GPIO_ACTIVE_HIGH), + {} + }, +}; + +static int yogabook_pdev_set_kbd_backlight(struct yogabook_data *data, u8 level) +{ + struct pwm_state state = { + .period = YB_KBD_BL_PWM_PERIOD, + .duty_cycle = YB_KBD_BL_PWM_PERIOD * level / YB_KBD_BL_MAX, + .enabled = level, + }; + + pwm_apply_might_sleep(data->kbd_bl_pwm, &state); + gpiod_set_value(data->kbd_bl_led_enable, level ? 1 : 0); + return 0; +} + +static irqreturn_t yogabook_pen_touch_irq(int irq, void *data) +{ + yogabook_toggle_digitizer_mode(data); + return IRQ_HANDLED; +} + +static int yogabook_pdev_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct yogabook_data *data; + int r; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->kbd_dev = bus_find_device_by_name(&i2c_bus_type, NULL, "i2c-goodix_ts"); + if (!data->kbd_dev || !data->kbd_dev->driver) { + r = -EPROBE_DEFER; + goto error_put_devs; + } + + data->dig_dev = bus_find_device_by_name(&i2c_bus_type, NULL, "i2c-wacom"); + if (!data->dig_dev || !data->dig_dev->driver) { + r = -EPROBE_DEFER; + goto error_put_devs; + } + + gpiod_add_lookup_table(&yogabook_pdev_gpios); + data->pen_touch_event = devm_gpiod_get(dev, "pen_touch_event", GPIOD_IN); + data->kbd_bl_led_enable = devm_gpiod_get(dev, "enable_keyboard_led", GPIOD_OUT_HIGH); + gpiod_remove_lookup_table(&yogabook_pdev_gpios); + + if (IS_ERR(data->pen_touch_event)) { + r = dev_err_probe(dev, PTR_ERR(data->pen_touch_event), + "Getting pen_touch_event GPIO\n"); + goto error_put_devs; + } + + if (IS_ERR(data->kbd_bl_led_enable)) { + r = dev_err_probe(dev, PTR_ERR(data->kbd_bl_led_enable), + "Getting enable_keyboard_led GPIO\n"); + goto error_put_devs; + } + + data->kbd_bl_pwm = devm_pwm_get(dev, "pwm_soc_lpss_2"); + if (IS_ERR(data->kbd_bl_pwm)) { + r = dev_err_probe(dev, PTR_ERR(data->kbd_bl_pwm), + "Getting keyboard backlight PWM\n"); + goto error_put_devs; + } + + r = gpiod_to_irq(data->pen_touch_event); + if (r < 0) { + dev_err_probe(dev, r, "Getting pen_touch_event IRQ\n"); + goto error_put_devs; + } + data->pen_touch_irq = r; + + r = request_irq(data->pen_touch_irq, yogabook_pen_touch_irq, IRQF_TRIGGER_FALLING, + "pen_touch_event", data); + if (r) { + dev_err_probe(dev, r, "Requesting pen_touch_event IRQ\n"); + goto error_put_devs; + } + + data->set_kbd_backlight = yogabook_pdev_set_kbd_backlight; + + r = yogabook_probe(dev, data, "yogabook::kbd_backlight"); + if (r) + goto error_free_irq; + + return 0; + +error_free_irq: + free_irq(data->pen_touch_irq, data); + cancel_work_sync(&data->work); +error_put_devs: + put_device(data->dig_dev); + put_device(data->kbd_dev); + return r; +} + +static void yogabook_pdev_remove(struct platform_device *pdev) +{ + struct yogabook_data *data = platform_get_drvdata(pdev); + + yogabook_remove(data); + free_irq(data->pen_touch_irq, data); + cancel_work_sync(&data->work); + put_device(data->dig_dev); + put_device(data->kbd_dev); +} + +static struct platform_driver yogabook_pdev_driver = { + .probe = yogabook_pdev_probe, + .remove = yogabook_pdev_remove, + .driver = { + .name = YB_PDEV_NAME, + .pm = pm_sleep_ptr(&yogabook_pm_ops), + }, +}; + +static int __init yogabook_module_init(void) +{ + int r; + + r = wmi_driver_register(&yogabook_wmi_driver); + if (r) + return r; + + r = platform_driver_register(&yogabook_pdev_driver); + if (r) + wmi_driver_unregister(&yogabook_wmi_driver); + + return r; +} + +static void __exit yogabook_module_exit(void) +{ + platform_driver_unregister(&yogabook_pdev_driver); + wmi_driver_unregister(&yogabook_wmi_driver); +} + +module_init(yogabook_module_init); +module_exit(yogabook_module_exit); + +MODULE_ALIAS("platform:" YB_PDEV_NAME); +MODULE_AUTHOR("Yauhen Kharuzhy"); +MODULE_DESCRIPTION("Lenovo Yoga Book driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/lg-laptop.c b/drivers/platform/x86/lg-laptop.c new file mode 100644 index 000000000..a8f2f465e --- /dev/null +++ b/drivers/platform/x86/lg-laptop.c @@ -0,0 +1,912 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * lg-laptop.c - LG Gram ACPI features and hotkeys Driver + * + * Copyright (C) 2018 Matan Ziv-Av + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LED_DEVICE(_name, max, flag) struct led_classdev _name = { \ + .name = __stringify(_name), \ + .max_brightness = max, \ + .brightness_set = _name##_set, \ + .brightness_get = _name##_get, \ + .flags = flag, \ +} + +MODULE_AUTHOR("Matan Ziv-Av"); +MODULE_DESCRIPTION("LG WMI Hotkey Driver"); +MODULE_LICENSE("GPL"); + +static bool fw_debug; +module_param(fw_debug, bool, 0); +MODULE_PARM_DESC(fw_debug, "Enable printing of firmware debug messages"); + +#define LG_ADDRESS_SPACE_ID 0x8F + +#define LG_ADDRESS_SPACE_DEBUG_FLAG_ADR 0x00 +#define LG_ADDRESS_SPACE_HD_AUDIO_POWER_ADDR 0x01 +#define LG_ADDRESS_SPACE_FAN_MODE_ADR 0x03 + +#define LG_ADDRESS_SPACE_DTTM_FLAG_ADR 0x20 +#define LG_ADDRESS_SPACE_CPU_TEMP_ADR 0x21 +#define LG_ADDRESS_SPACE_CPU_TRIP_LOW_ADR 0x22 +#define LG_ADDRESS_SPACE_CPU_TRIP_HIGH_ADR 0x23 +#define LG_ADDRESS_SPACE_MB_TEMP_ADR 0x24 +#define LG_ADDRESS_SPACE_MB_TRIP_LOW_ADR 0x25 +#define LG_ADDRESS_SPACE_MB_TRIP_HIGH_ADR 0x26 + +#define LG_ADDRESS_SPACE_DEBUG_MSG_START_ADR 0x3E8 +#define LG_ADDRESS_SPACE_DEBUG_MSG_END_ADR 0x5E8 + +#define WMI_EVENT_GUID0 "E4FB94F9-7F2B-4173-AD1A-CD1D95086248" +#define WMI_EVENT_GUID1 "023B133E-49D1-4E10-B313-698220140DC2" +#define WMI_EVENT_GUID2 "37BE1AC0-C3F2-4B1F-BFBE-8FDEAF2814D6" +#define WMI_EVENT_GUID3 "911BAD44-7DF8-4FBB-9319-BABA1C4B293B" +#define WMI_METHOD_WMAB "C3A72B38-D3EF-42D3-8CBB-D5A57049F66D" +#define WMI_METHOD_WMBB "2B4F501A-BD3C-4394-8DCF-00A7D2BC8210" +#define WMI_EVENT_GUID WMI_EVENT_GUID0 + +#define SB_GGOV_METHOD "\\_SB.GGOV" +#define GOV_TLED 0x2020008 +#define WM_GET 1 +#define WM_SET 2 +#define WM_KEY_LIGHT 0x400 +#define WM_TLED 0x404 +#define WM_FN_LOCK 0x407 +#define WM_BATT_LIMIT 0x61 +#define WM_READER_MODE 0xBF +#define WM_FAN_MODE 0x33 +#define WMBB_USB_CHARGE 0x10B +#define WMBB_BATT_LIMIT 0x10C + +#define FAN_MODE_LOWER GENMASK(1, 0) +#define FAN_MODE_UPPER GENMASK(5, 4) + +#define PLATFORM_NAME "lg-laptop" + +MODULE_ALIAS("wmi:" WMI_EVENT_GUID0); +MODULE_ALIAS("wmi:" WMI_EVENT_GUID1); +MODULE_ALIAS("wmi:" WMI_EVENT_GUID2); +MODULE_ALIAS("wmi:" WMI_EVENT_GUID3); +MODULE_ALIAS("wmi:" WMI_METHOD_WMAB); +MODULE_ALIAS("wmi:" WMI_METHOD_WMBB); + +static struct platform_device *pf_device; +static struct input_dev *wmi_input_dev; + +static u32 inited; +#define INIT_INPUT_WMI_0 0x01 +#define INIT_INPUT_WMI_2 0x02 +#define INIT_INPUT_ACPI 0x04 +#define INIT_SPARSE_KEYMAP 0x80 + +static int battery_limit_use_wmbb; +static struct led_classdev kbd_backlight; +static enum led_brightness get_kbd_backlight_level(struct device *dev); + +static const struct key_entry wmi_keymap[] = { + {KE_KEY, 0x70, {KEY_F15} }, /* LG control panel (F1) */ + {KE_KEY, 0x74, {KEY_F21} }, /* Touchpad toggle (F5) */ + {KE_KEY, 0xf020000, {KEY_F14} }, /* Read mode (F9) */ + {KE_KEY, 0x10000000, {KEY_F16} },/* Keyboard backlight (F8) - pressing + * this key both sends an event and + * changes backlight level. + */ + {KE_END, 0} +}; + +static int ggov(u32 arg0) +{ + union acpi_object args[1]; + union acpi_object *r; + acpi_status status; + acpi_handle handle; + struct acpi_object_list arg; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + int res; + + args[0].type = ACPI_TYPE_INTEGER; + args[0].integer.value = arg0; + + status = acpi_get_handle(NULL, (acpi_string) SB_GGOV_METHOD, &handle); + if (ACPI_FAILURE(status)) { + pr_err("Cannot get handle"); + return -ENODEV; + } + + arg.count = 1; + arg.pointer = args; + + status = acpi_evaluate_object(handle, NULL, &arg, &buffer); + if (ACPI_FAILURE(status)) { + acpi_handle_err(handle, "GGOV: call failed.\n"); + return -EINVAL; + } + + r = buffer.pointer; + if (r->type != ACPI_TYPE_INTEGER) { + kfree(r); + return -EINVAL; + } + + res = r->integer.value; + kfree(r); + + return res; +} + +static union acpi_object *lg_wmab(struct device *dev, u32 method, u32 arg1, u32 arg2) +{ + union acpi_object args[3]; + acpi_status status; + struct acpi_object_list arg; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + + args[0].type = ACPI_TYPE_INTEGER; + args[0].integer.value = method; + args[1].type = ACPI_TYPE_INTEGER; + args[1].integer.value = arg1; + args[2].type = ACPI_TYPE_INTEGER; + args[2].integer.value = arg2; + + arg.count = 3; + arg.pointer = args; + + status = acpi_evaluate_object(ACPI_HANDLE(dev), "WMAB", &arg, &buffer); + if (ACPI_FAILURE(status)) { + dev_err(dev, "WMAB: call failed.\n"); + return NULL; + } + + return buffer.pointer; +} + +static union acpi_object *lg_wmbb(struct device *dev, u32 method_id, u32 arg1, u32 arg2) +{ + union acpi_object args[3]; + acpi_status status; + struct acpi_object_list arg; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + u8 buf[32]; + + *(u32 *)buf = method_id; + *(u32 *)(buf + 4) = arg1; + *(u32 *)(buf + 16) = arg2; + args[0].type = ACPI_TYPE_INTEGER; + args[0].integer.value = 0; /* ignored */ + args[1].type = ACPI_TYPE_INTEGER; + args[1].integer.value = 1; /* Must be 1 or 2. Does not matter which */ + args[2].type = ACPI_TYPE_BUFFER; + args[2].buffer.length = 32; + args[2].buffer.pointer = buf; + + arg.count = 3; + arg.pointer = args; + + status = acpi_evaluate_object(ACPI_HANDLE(dev), "WMBB", &arg, &buffer); + if (ACPI_FAILURE(status)) { + dev_err(dev, "WMBB: call failed.\n"); + return NULL; + } + + return (union acpi_object *)buffer.pointer; +} + +static void wmi_notify(union acpi_object *obj, void *context) +{ + long data = (long)context; + + pr_debug("event guid %li\n", data); + if (!obj) + return; + + if (obj->type == ACPI_TYPE_INTEGER) { + int eventcode = obj->integer.value; + struct key_entry *key; + + if (eventcode == 0x10000000) { + led_classdev_notify_brightness_hw_changed( + &kbd_backlight, get_kbd_backlight_level(kbd_backlight.dev->parent)); + } else { + key = sparse_keymap_entry_from_scancode( + wmi_input_dev, eventcode); + if (key && key->type == KE_KEY) + sparse_keymap_report_entry(wmi_input_dev, + key, 1, true); + } + } + + pr_debug("Type: %i Eventcode: 0x%llx\n", obj->type, + obj->integer.value); +} + +static void wmi_input_setup(void) +{ + acpi_status status; + + wmi_input_dev = input_allocate_device(); + if (wmi_input_dev) { + wmi_input_dev->name = "LG WMI hotkeys"; + wmi_input_dev->phys = "wmi/input0"; + wmi_input_dev->id.bustype = BUS_HOST; + + if (sparse_keymap_setup(wmi_input_dev, wmi_keymap, NULL) || + input_register_device(wmi_input_dev)) { + pr_info("Cannot initialize input device"); + input_free_device(wmi_input_dev); + return; + } + + inited |= INIT_SPARSE_KEYMAP; + status = wmi_install_notify_handler(WMI_EVENT_GUID0, wmi_notify, + (void *)0); + if (ACPI_SUCCESS(status)) + inited |= INIT_INPUT_WMI_0; + + status = wmi_install_notify_handler(WMI_EVENT_GUID2, wmi_notify, + (void *)2); + if (ACPI_SUCCESS(status)) + inited |= INIT_INPUT_WMI_2; + } else { + pr_info("Cannot allocate input device"); + } +} + +static ssize_t fan_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + union acpi_object *r; + int ret; + + ret = kstrtoul(buffer, 10, &value); + if (ret) + return ret; + if (value >= 3) + return -EINVAL; + + r = lg_wmab(dev, WM_FAN_MODE, WM_SET, + FIELD_PREP(FAN_MODE_LOWER, value) | + FIELD_PREP(FAN_MODE_UPPER, value)); + kfree(r); + + return count; +} + +static ssize_t fan_mode_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int mode; + union acpi_object *r; + + r = lg_wmab(dev, WM_FAN_MODE, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_INTEGER) { + kfree(r); + return -EIO; + } + + mode = FIELD_GET(FAN_MODE_LOWER, r->integer.value); + kfree(r); + + return sysfs_emit(buffer, "%d\n", mode); +} + +static ssize_t usb_charge_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + bool value; + union acpi_object *r; + int ret; + + ret = kstrtobool(buffer, &value); + if (ret) + return ret; + + r = lg_wmbb(dev, WMBB_USB_CHARGE, WM_SET, value); + if (!r) + return -EIO; + + kfree(r); + return count; +} + +static ssize_t usb_charge_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int status; + union acpi_object *r; + + r = lg_wmbb(dev, WMBB_USB_CHARGE, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_BUFFER) { + kfree(r); + return -EIO; + } + + status = !!r->buffer.pointer[0x10]; + + kfree(r); + + return sysfs_emit(buffer, "%d\n", status); +} + +static ssize_t reader_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + bool value; + union acpi_object *r; + int ret; + + ret = kstrtobool(buffer, &value); + if (ret) + return ret; + + r = lg_wmab(dev, WM_READER_MODE, WM_SET, value); + if (!r) + return -EIO; + + kfree(r); + return count; +} + +static ssize_t reader_mode_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int status; + union acpi_object *r; + + r = lg_wmab(dev, WM_READER_MODE, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_INTEGER) { + kfree(r); + return -EIO; + } + + status = !!r->integer.value; + + kfree(r); + + return sysfs_emit(buffer, "%d\n", status); +} + +static ssize_t fn_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + bool value; + union acpi_object *r; + int ret; + + ret = kstrtobool(buffer, &value); + if (ret) + return ret; + + r = lg_wmab(dev, WM_FN_LOCK, WM_SET, value); + if (!r) + return -EIO; + + kfree(r); + return count; +} + +static ssize_t fn_lock_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int status; + union acpi_object *r; + + r = lg_wmab(dev, WM_FN_LOCK, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_BUFFER) { + kfree(r); + return -EIO; + } + + status = !!r->buffer.pointer[0]; + kfree(r); + + return sysfs_emit(buffer, "%d\n", status); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long value; + int ret; + + ret = kstrtoul(buf, 10, &value); + if (ret) + return ret; + + if (value == 100 || value == 80) { + union acpi_object *r; + + if (battery_limit_use_wmbb) + r = lg_wmbb(&pf_device->dev, WMBB_BATT_LIMIT, WM_SET, value); + else + r = lg_wmab(&pf_device->dev, WM_BATT_LIMIT, WM_SET, value); + if (!r) + return -EIO; + + kfree(r); + return count; + } + + return -EINVAL; +} + +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + unsigned int status; + union acpi_object *r; + + if (battery_limit_use_wmbb) { + r = lg_wmbb(&pf_device->dev, WMBB_BATT_LIMIT, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_BUFFER) { + kfree(r); + return -EIO; + } + + status = r->buffer.pointer[0x10]; + } else { + r = lg_wmab(&pf_device->dev, WM_BATT_LIMIT, WM_GET, 0); + if (!r) + return -EIO; + + if (r->type != ACPI_TYPE_INTEGER) { + kfree(r); + return -EIO; + } + + status = r->integer.value; + } + kfree(r); + if (status != 80 && status != 100) + status = 0; + + return sysfs_emit(buf, "%d\n", status); +} + +static ssize_t battery_care_limit_show(struct device *dev, + struct device_attribute *attr, + char *buffer) +{ + return charge_control_end_threshold_show(dev, attr, buffer); +} + +static ssize_t battery_care_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + return charge_control_end_threshold_store(dev, attr, buffer, count); +} + +static DEVICE_ATTR_RW(fan_mode); +static DEVICE_ATTR_RW(usb_charge); +static DEVICE_ATTR_RW(reader_mode); +static DEVICE_ATTR_RW(fn_lock); +static DEVICE_ATTR_RW(charge_control_end_threshold); +static DEVICE_ATTR_RW(battery_care_limit); + +static int lg_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + if (device_create_file(&battery->dev, + &dev_attr_charge_control_end_threshold)) + return -ENODEV; + + return 0; +} + +static int lg_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_file(&battery->dev, + &dev_attr_charge_control_end_threshold); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = lg_battery_add, + .remove_battery = lg_battery_remove, + .name = "LG Battery Extension", +}; + +static struct attribute *dev_attributes[] = { + &dev_attr_fan_mode.attr, + &dev_attr_usb_charge.attr, + &dev_attr_reader_mode.attr, + &dev_attr_fn_lock.attr, + &dev_attr_battery_care_limit.attr, + NULL +}; + +static const struct attribute_group dev_attribute_group = { + .attrs = dev_attributes, +}; + +static void tpad_led_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + union acpi_object *r; + + r = lg_wmab(cdev->dev->parent, WM_TLED, WM_SET, brightness > LED_OFF); + kfree(r); +} + +static enum led_brightness tpad_led_get(struct led_classdev *cdev) +{ + return ggov(GOV_TLED) > 0 ? LED_ON : LED_OFF; +} + +static LED_DEVICE(tpad_led, 1, 0); + +static void kbd_backlight_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + u32 val; + union acpi_object *r; + + val = 0x22; + if (brightness <= LED_OFF) + val = 0; + if (brightness >= LED_FULL) + val = 0x24; + r = lg_wmab(cdev->dev->parent, WM_KEY_LIGHT, WM_SET, val); + kfree(r); +} + +static enum led_brightness get_kbd_backlight_level(struct device *dev) +{ + union acpi_object *r; + int val; + + r = lg_wmab(dev, WM_KEY_LIGHT, WM_GET, 0); + + if (!r) + return LED_OFF; + + if (r->type != ACPI_TYPE_BUFFER || r->buffer.pointer[1] != 0x05) { + kfree(r); + return LED_OFF; + } + + switch (r->buffer.pointer[0] & 0x27) { + case 0x24: + val = LED_FULL; + break; + case 0x22: + val = LED_HALF; + break; + default: + val = LED_OFF; + } + + kfree(r); + + return val; +} + +static enum led_brightness kbd_backlight_get(struct led_classdev *cdev) +{ + return get_kbd_backlight_level(cdev->dev->parent); +} + +static LED_DEVICE(kbd_backlight, 255, LED_BRIGHT_HW_CHANGED); + +static void wmi_input_destroy(void) +{ + if (inited & INIT_INPUT_WMI_2) + wmi_remove_notify_handler(WMI_EVENT_GUID2); + + if (inited & INIT_INPUT_WMI_0) + wmi_remove_notify_handler(WMI_EVENT_GUID0); + + if (inited & INIT_SPARSE_KEYMAP) + input_unregister_device(wmi_input_dev); + + inited &= ~(INIT_INPUT_WMI_0 | INIT_INPUT_WMI_2 | INIT_SPARSE_KEYMAP); +} + +static struct platform_driver pf_driver = { + .driver = { + .name = PLATFORM_NAME, + } +}; + +static acpi_status lg_laptop_address_space_write(struct device *dev, acpi_physical_address address, + size_t size, u64 value) +{ + u8 byte; + + /* Ignore any debug messages */ + if (address >= LG_ADDRESS_SPACE_DEBUG_MSG_START_ADR && + address <= LG_ADDRESS_SPACE_DEBUG_MSG_END_ADR) + return AE_OK; + + if (size != sizeof(byte)) + return AE_BAD_PARAMETER; + + byte = value & 0xFF; + + switch (address) { + case LG_ADDRESS_SPACE_HD_AUDIO_POWER_ADDR: + /* + * The HD audio power field is not affected by the DTTM flag, + * so we have to manually check fw_debug. + */ + if (fw_debug) + dev_dbg(dev, "HD audio power %s\n", str_enabled_disabled(byte)); + + return AE_OK; + case LG_ADDRESS_SPACE_FAN_MODE_ADR: + /* + * The fan mode field is not affected by the DTTM flag, so we + * have to manually check fw_debug. + */ + if (fw_debug) + dev_dbg(dev, "Fan mode set to mode %u\n", byte); + + return AE_OK; + case LG_ADDRESS_SPACE_CPU_TEMP_ADR: + dev_dbg(dev, "CPU temperature is %u °C\n", byte); + return AE_OK; + case LG_ADDRESS_SPACE_CPU_TRIP_LOW_ADR: + dev_dbg(dev, "CPU lower trip point set to %u °C\n", byte); + return AE_OK; + case LG_ADDRESS_SPACE_CPU_TRIP_HIGH_ADR: + dev_dbg(dev, "CPU higher trip point set to %u °C\n", byte); + return AE_OK; + case LG_ADDRESS_SPACE_MB_TEMP_ADR: + dev_dbg(dev, "Motherboard temperature is %u °C\n", byte); + return AE_OK; + case LG_ADDRESS_SPACE_MB_TRIP_LOW_ADR: + dev_dbg(dev, "Motherboard lower trip point set to %u °C\n", byte); + return AE_OK; + case LG_ADDRESS_SPACE_MB_TRIP_HIGH_ADR: + dev_dbg(dev, "Motherboard higher trip point set to %u °C\n", byte); + return AE_OK; + default: + dev_notice_ratelimited(dev, "Ignoring write to unknown opregion address %llu\n", + address); + return AE_OK; + } +} + +static acpi_status lg_laptop_address_space_read(struct device *dev, acpi_physical_address address, + size_t size, u64 *value) +{ + if (size != 1) + return AE_BAD_PARAMETER; + + switch (address) { + case LG_ADDRESS_SPACE_DEBUG_FLAG_ADR: + /* Debug messages are already printed using the standard ACPI Debug object */ + *value = 0x00; + return AE_OK; + case LG_ADDRESS_SPACE_DTTM_FLAG_ADR: + *value = fw_debug; + return AE_OK; + default: + dev_notice_ratelimited(dev, "Attempt to read unknown opregion address %llu\n", + address); + return AE_BAD_PARAMETER; + } +} + +static acpi_status lg_laptop_address_space_handler(u32 function, acpi_physical_address address, + u32 bits, u64 *value, void *handler_context, + void *region_context) +{ + struct device *dev = handler_context; + size_t size; + + if (bits % BITS_PER_BYTE) + return AE_BAD_PARAMETER; + + size = bits / BITS_PER_BYTE; + + switch (function) { + case ACPI_READ: + return lg_laptop_address_space_read(dev, address, size, value); + case ACPI_WRITE: + return lg_laptop_address_space_write(dev, address, size, *value); + default: + return AE_BAD_PARAMETER; + } +} + +static void lg_laptop_remove_address_space_handler(void *data) +{ + struct acpi_device *device = data; + + acpi_remove_address_space_handler(device->handle, LG_ADDRESS_SPACE_ID, + &lg_laptop_address_space_handler); +} + +static int acpi_probe(struct platform_device *pdev) +{ + struct platform_device_info pdev_info = { + .name = PLATFORM_NAME, + .id = PLATFORM_DEVID_NONE, + }; + struct acpi_device *device; + acpi_status status; + int ret; + const char *product; + int year = 2017; + + if (pf_device) + return 0; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + pdev_info.fwnode = acpi_fwnode_handle(device), + + status = acpi_install_address_space_handler(device->handle, LG_ADDRESS_SPACE_ID, + &lg_laptop_address_space_handler, + NULL, &pdev->dev); + if (ACPI_FAILURE(status)) + return -ENODEV; + + ret = devm_add_action_or_reset(&pdev->dev, lg_laptop_remove_address_space_handler, + device); + if (ret < 0) + return ret; + + ret = platform_driver_register(&pf_driver); + if (ret) + return ret; + + pf_device = platform_device_register_full(&pdev_info); + if (IS_ERR(pf_device)) { + ret = PTR_ERR(pf_device); + pf_device = NULL; + pr_err("unable to register platform device\n"); + goto out_platform_registered; + } + product = dmi_get_system_info(DMI_PRODUCT_NAME); + if (product && strlen(product) > 4) + switch (product[4]) { + case '5': + if (strlen(product) > 5) + switch (product[5]) { + case 'N': + year = 2021; + break; + case '0': + year = 2016; + break; + default: + year = 2022; + } + break; + case '6': + year = 2016; + break; + case '7': + year = 2017; + break; + case '8': + year = 2018; + break; + case '9': + year = 2019; + break; + case '0': + if (strlen(product) > 5) + switch (product[5]) { + case 'N': + year = 2020; + break; + case 'P': + year = 2021; + break; + case 'Q': + year = 2022; + break; + case 'R': + year = 2023; + break; + case 'S': + year = 2024; + break; + default: + year = 2025; + } + break; + default: + year = 2019; + } + pr_info("product: %s year: %d\n", product ?: "unknown", year); + + if (year >= 2019) + battery_limit_use_wmbb = 1; + + ret = sysfs_create_group(&pf_device->dev.kobj, &dev_attribute_group); + if (ret) + goto out_platform_device; + + /* LEDs are optional */ + led_classdev_register(&pf_device->dev, &kbd_backlight); + led_classdev_register(&pf_device->dev, &tpad_led); + + wmi_input_setup(); + battery_hook_register(&battery_hook); + + return 0; + +out_platform_device: + platform_device_unregister(pf_device); +out_platform_registered: + platform_driver_unregister(&pf_driver); + return ret; +} + +static void acpi_remove(struct platform_device *pdev) +{ + sysfs_remove_group(&pf_device->dev.kobj, &dev_attribute_group); + + led_classdev_unregister(&tpad_led); + led_classdev_unregister(&kbd_backlight); + + battery_hook_unregister(&battery_hook); + wmi_input_destroy(); + platform_device_unregister(pf_device); + pf_device = NULL; + platform_driver_unregister(&pf_driver); +} + +static const struct acpi_device_id device_ids[] = { + {"LGEX0820", 0}, + {"", 0} +}; +MODULE_DEVICE_TABLE(acpi, device_ids); + +static struct platform_driver acpi_driver = { + .probe = acpi_probe, + .remove = acpi_remove, + .driver = { + .name = "LG Gram Laptop Support", + .acpi_match_table = device_ids, + }, +}; + +module_platform_driver(acpi_driver); diff --git a/drivers/platform/x86/meegopad_anx7428.c b/drivers/platform/x86/meegopad_anx7428.c new file mode 100644 index 000000000..b2c4d4f52 --- /dev/null +++ b/drivers/platform/x86/meegopad_anx7428.c @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver to power on the Analogix ANX7428 USB Type-C crosspoint switch + * on MeeGoPad top-set boxes. + * + * The MeeGoPad T8 and T9 are Cherry Trail top-set boxes which + * use an ANX7428 to provide a Type-C port with USB3.1 Gen 1 and + * DisplayPort over Type-C alternate mode support. + * + * The ANX7428 has a microcontroller which takes care of the PD + * negotiation and automatically sets the builtin Crosspoint Switch + * to send the right signal to the 4 highspeed pairs of the Type-C + * connector. It also takes care of HPD and AUX channel routing for + * DP alternate mode. + * + * IOW the ANX7428 operates fully autonomous and to the x5-Z8350 SoC + * things look like there simply is a USB-3 Type-A connector and a + * separate DisplayPort connector. Except that the BIOS does not + * power on the ANX7428 at boot. This driver takes care of powering + * on the ANX7428. + * + * It should be possible to tell the micro-controller which data- and/or + * power-role to negotiate and to swap the role(s) after negotiation + * but the MeeGoPad top-set boxes always draw their power from a separate + * power-connector and they only support USB host-mode. So this functionality + * is unnecessary and due to lack of documentation this is tricky to support. + * + * For a more complete ANX7428 driver see drivers/usb/misc/anx7418/ of + * the LineageOS kernel for the LG G5 (International) aka the LG H850: + * https://github.com/LineageOS/android_kernel_lge_msm8996/ + * + * (C) Copyright 2024 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Register addresses and fields */ +#define VENDOR_ID 0x00 +#define DEVICE_ID 0x02 + +#define TX_STATUS 0x16 +#define STATUS_SUCCESS BIT(0) +#define STATUS_ERROR BIT(1) +#define OCM_STARTUP BIT(7) + +static bool force; +module_param(force, bool, 0444); +MODULE_PARM_DESC(force, "Force the driver to probe on unknown boards"); + +static const struct acpi_gpio_params enable_gpio = { 0, 0, false }; +static const struct acpi_gpio_params reset_gpio = { 1, 0, true }; + +static const struct acpi_gpio_mapping meegopad_anx7428_gpios[] = { + { "enable-gpios", &enable_gpio, 1 }, + { "reset-gpios", &reset_gpio, 1 }, + { } +}; + +static const struct dmi_system_id meegopad_anx7428_ids[] = { + { + /* Meegopad T08 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Default string"), + DMI_MATCH(DMI_PRODUCT_NAME, "Default string"), + DMI_MATCH(DMI_BOARD_NAME, "T3 MRD"), + DMI_MATCH(DMI_BOARD_VERSION, "V1.1"), + }, + }, + { } +}; + +static int anx7428_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct gpio_desc *gpio; + int ret, val; + + if (!dmi_check_system(meegopad_anx7428_ids) && !force) { + dev_warn(dev, "Not probing unknown board, pass meegopad_anx7428.force=1 to probe"); + return -ENODEV; + } + + ret = devm_acpi_dev_add_driver_gpios(dev, meegopad_anx7428_gpios); + if (ret) + return ret; + + /* + * Set GPIOs to desired values while getting them, they are not needed + * afterwards. Ordering and delays come from android_kernel_lge_msm8996. + */ + gpio = devm_gpiod_get(dev, "enable", GPIOD_OUT_HIGH); + if (IS_ERR(gpio)) + return dev_err_probe(dev, PTR_ERR(gpio), "getting enable GPIO\n"); + + fsleep(10000); + + gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(gpio)) + return dev_err_probe(dev, PTR_ERR(gpio), "getting reset GPIO\n"); + + /* Wait for the OCM (On Chip Microcontroller) to start */ + ret = read_poll_timeout(i2c_smbus_read_byte_data, val, + val >= 0 && (val & OCM_STARTUP), + 5000, 50000, true, client, TX_STATUS); + if (ret) + return dev_err_probe(dev, ret, + "On Chip Microcontroller did not start, status: 0x%02x\n", + val); + + ret = i2c_smbus_read_word_data(client, VENDOR_ID); + if (ret < 0) + return dev_err_probe(dev, ret, "reading vendor-id register\n"); + val = ret; + + ret = i2c_smbus_read_word_data(client, DEVICE_ID); + if (ret < 0) + return dev_err_probe(dev, ret, "reading device-id register\n"); + + dev_dbg(dev, "Powered on ANX7428 id %04x:%04x\n", val, ret); + return 0; +} + +static const struct acpi_device_id anx7428_acpi_match[] = { + { "ANXO7418" }, /* ACPI says 7418 (max 2 DP lanes version) but HW is 7428 */ + { } +}; +MODULE_DEVICE_TABLE(acpi, anx7428_acpi_match); + +static struct i2c_driver anx7428_driver = { + .driver = { + .name = "meegopad_anx7428", + .acpi_match_table = anx7428_acpi_match, + }, + .probe = anx7428_probe, +}; +module_i2c_driver(anx7428_driver); + +MODULE_AUTHOR("Hans de Goede "); +MODULE_DESCRIPTION("MeeGoPad ANX7428 driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/meraki-mx100.c b/drivers/platform/x86/meraki-mx100.c new file mode 100644 index 000000000..9f4caa1f3 --- /dev/null +++ b/drivers/platform/x86/meraki-mx100.c @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * Cisco Meraki MX100 (Tinkerbell) board platform driver + * + * Based off of arch/x86/platform/meraki/tink.c from the + * Meraki GPL release meraki-firmware-sources-r23-20150601 + * + * Format inspired by platform/x86/pcengines-apuv2.c + * + * Copyright (C) 2021 Chris Blake + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* LEDs */ +static const struct software_node tink_gpio_leds_node = { + .name = "meraki-mx100-leds", +}; + +static const struct property_entry tink_internet_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:internet"), + PROPERTY_ENTRY_STRING("linux,default-trigger", "default-on"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 11, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node tink_internet_led_node = { + .name = "internet-led", + .parent = &tink_gpio_leds_node, + .properties = tink_internet_led_props, +}; + +static const struct property_entry tink_lan2_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan2"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 18, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan2_led_node = { + .name = "lan2-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan2_led_props, +}; + +static const struct property_entry tink_lan3_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan3"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 20, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan3_led_node = { + .name = "lan3-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan3_led_props, +}; + +static const struct property_entry tink_lan4_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan4"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 22, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan4_led_node = { + .name = "lan4-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan4_led_props, +}; + +static const struct property_entry tink_lan5_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan5"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 23, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan5_led_node = { + .name = "lan5-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan5_led_props, +}; + +static const struct property_entry tink_lan6_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan6"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 32, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan6_led_node = { + .name = "lan6-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan6_led_props, +}; + +static const struct property_entry tink_lan7_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan7"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 34, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan7_led_node = { + .name = "lan7-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan7_led_props, +}; + +static const struct property_entry tink_lan8_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan8"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 35, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan8_led_node = { + .name = "lan8-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan8_led_props, +}; + +static const struct property_entry tink_lan9_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan9"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 36, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan9_led_node = { + .name = "lan9-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan9_led_props, +}; + +static const struct property_entry tink_lan10_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan10"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 37, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan10_led_node = { + .name = "lan10-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan10_led_props, +}; + +static const struct property_entry tink_lan11_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:lan11"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 48, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node tink_lan11_led_node = { + .name = "lan11-led", + .parent = &tink_gpio_leds_node, + .properties = tink_lan11_led_props, +}; + +static const struct property_entry tink_ha_green_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:ha"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 16, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node tink_ha_green_led_node = { + .name = "ha-green-led", + .parent = &tink_gpio_leds_node, + .properties = tink_ha_green_led_props, +}; + +static const struct property_entry tink_ha_orange_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:orange:ha"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 7, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node tink_ha_orange_led_node = { + .name = "ha-orange-led", + .parent = &tink_gpio_leds_node, + .properties = tink_ha_orange_led_props, +}; + +static const struct property_entry tink_usb_green_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:green:usb"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 21, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node tink_usb_green_led_node = { + .name = "usb-green-led", + .parent = &tink_gpio_leds_node, + .properties = tink_usb_green_led_props, +}; + +static const struct property_entry tink_usb_orange_led_props[] = { + PROPERTY_ENTRY_STRING("label", "mx100:orange:usb"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 19, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node tink_usb_orange_led_node = { + .name = "usb-orange-led", + .parent = &tink_gpio_leds_node, + .properties = tink_usb_orange_led_props, +}; + +/* Reset Button */ +static const struct property_entry tink_gpio_keys_props[] = { + PROPERTY_ENTRY_U32("poll-interval", 20), + { } +}; + +static const struct software_node tink_gpio_keys_node = { + .name = "mx100-keys", + .properties = tink_gpio_keys_props, +}; + +static const struct property_entry tink_reset_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_RESTART), + PROPERTY_ENTRY_STRING("label", "Reset"), + PROPERTY_ENTRY_GPIO("gpios", &lpc_ich_gpio_swnode, 60, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("linux,input-type", EV_KEY), + PROPERTY_ENTRY_U32("debounce-interval", 100), + { } +}; + +static const struct software_node tink_reset_key_node = { + .name = "reset", + .parent = &tink_gpio_keys_node, + .properties = tink_reset_key_props, +}; + +static const struct software_node *tink_swnodes[] = { + /* LEDs nodes */ + &tink_gpio_leds_node, + &tink_internet_led_node, + &tink_lan2_led_node, + &tink_lan3_led_node, + &tink_lan4_led_node, + &tink_lan5_led_node, + &tink_lan6_led_node, + &tink_lan7_led_node, + &tink_lan8_led_node, + &tink_lan9_led_node, + &tink_lan10_led_node, + &tink_lan11_led_node, + &tink_ha_green_led_node, + &tink_ha_orange_led_node, + &tink_usb_green_led_node, + &tink_usb_orange_led_node, + /* Keys nodes */ + &tink_gpio_keys_node, + &tink_reset_key_node, + NULL +}; + +/* Board setup */ +static const struct dmi_system_id tink_systems[] __initconst = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Cisco"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "MX100-HW"), + }, + }, + {} /* Terminating entry */ +}; +MODULE_DEVICE_TABLE(dmi, tink_systems); + +static struct platform_device *tink_leds_pdev; +static struct platform_device *tink_keys_pdev; + +static int __init tink_board_init(void) +{ + struct platform_device_info keys_info = { + .name = "gpio-keys-polled", + .id = PLATFORM_DEVID_NONE, + }; + struct platform_device_info leds_info = { + .name = "leds-gpio", + .id = PLATFORM_DEVID_NONE, + }; + int err; + + if (!dmi_first_match(tink_systems)) + return -ENODEV; + + /* + * We need to make sure that GPIO60 isn't set to native mode as is default since it's our + * Reset Button. To do this, write to GPIO_USE_SEL2 to have GPIO60 set to GPIO mode. + * This is documented on page 1609 of the PCH datasheet, order number 327879-005US + */ + outl(inl(0x530) | BIT(28), 0x530); + + err = software_node_register_node_group(tink_swnodes); + if (err) { + pr_err("failed to register software nodes: %d\n", err); + return err; + } + + leds_info.fwnode = software_node_fwnode(&tink_gpio_leds_node); + tink_leds_pdev = platform_device_register_full(&leds_info); + if (IS_ERR(tink_leds_pdev)) { + err = PTR_ERR(tink_leds_pdev); + pr_err("failed to create LED device: %d\n", err); + goto err_unregister_swnodes; + } + + keys_info.fwnode = software_node_fwnode(&tink_gpio_keys_node); + tink_keys_pdev = platform_device_register_full(&keys_info); + if (IS_ERR(tink_keys_pdev)) { + err = PTR_ERR(tink_keys_pdev); + pr_err("failed to create key device: %d\n", err); + goto err_unregister_leds; + } + + return 0; + +err_unregister_leds: + platform_device_unregister(tink_leds_pdev); +err_unregister_swnodes: + software_node_unregister_node_group(tink_swnodes); + return err; +} +module_init(tink_board_init); + +static void __exit tink_board_exit(void) +{ + platform_device_unregister(tink_keys_pdev); + platform_device_unregister(tink_leds_pdev); + software_node_unregister_node_group(tink_swnodes); +} +module_exit(tink_board_exit); + +MODULE_AUTHOR("Chris Blake "); +MODULE_DESCRIPTION("Cisco Meraki MX100 Platform Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:meraki-mx100"); +MODULE_IMPORT_NS("LPC_ICH"); diff --git a/drivers/platform/x86/msi-ec.c b/drivers/platform/x86/msi-ec.c new file mode 100644 index 000000000..dfe4532eb --- /dev/null +++ b/drivers/platform/x86/msi-ec.c @@ -0,0 +1,1365 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * msi-ec: MSI laptops' embedded controller driver. + * + * This driver allows various MSI laptops' functionalities to be + * controlled from userspace. + * + * It contains EC memory configurations for different firmware versions + * and exports battery charge thresholds to userspace. + * + * Copyright (C) 2023 Jose Angel Pastrana + * Copyright (C) 2023 Aakash Singh + * Copyright (C) 2023 Nikita Kravets + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include "msi-ec.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SM_ECO_NAME "eco" +#define SM_COMFORT_NAME "comfort" +#define SM_SPORT_NAME "sport" +#define SM_TURBO_NAME "turbo" + +#define FM_AUTO_NAME "auto" +#define FM_SILENT_NAME "silent" +#define FM_BASIC_NAME "basic" +#define FM_ADVANCED_NAME "advanced" + +static const char * const ALLOWED_FW_0[] __initconst = { + "14C1EMS1.012", + "14C1EMS1.101", + "14C1EMS1.102", + NULL +}; + +static struct msi_ec_conf CONF0 __initdata = { + .allowed_fw = ALLOWED_FW_0, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xbf, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNKNOWN, // 0xd5 needs testing + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_BASIC_NAME, 0x4d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = 0x89, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2b, + .mute_led_address = 0x2c, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = 0xf3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_1[] __initconst = { + "17F2EMS1.103", + "17F2EMS1.104", + "17F2EMS1.106", + "17F2EMS1.107", + NULL +}; + +static struct msi_ec_conf CONF1 __initdata = { + .allowed_fw = ALLOWED_FW_1, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xbf, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNKNOWN, + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_BASIC_NAME, 0x4d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = 0x89, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2b, + .mute_led_address = 0x2c, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = 0xf3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_2[] __initconst = { + "1552EMS1.118", + NULL +}; + +static struct msi_ec_conf CONF2 __initdata = { + .allowed_fw = ALLOWED_FW_2, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xeb, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_BASIC_NAME, 0x4d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = 0x89, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2c, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_3[] __initconst = { + "1592EMS1.111", + NULL +}; + +static struct msi_ec_conf CONF3 __initdata = { + .allowed_fw = ALLOWED_FW_3, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xeb, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_BASIC_NAME, 0x4d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0xc9, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = 0x89, // ? + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2b, + .mute_led_address = 0x2c, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_4[] __initconst = { + "16V4EMS1.114", + NULL +}; + +static struct msi_ec_conf CONF4 __initdata = { + .allowed_fw = ALLOWED_FW_4, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = MSI_EC_ADDR_UNKNOWN, // supported, but unknown + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { // may be supported, but address is unknown + .address = MSI_EC_ADDR_UNKNOWN, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, // needs testing + .rt_fan_speed_address = 0x71, // needs testing + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + }, + .leds = { + .micmute_led_address = MSI_EC_ADDR_UNKNOWN, + .mute_led_address = MSI_EC_ADDR_UNKNOWN, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xd3, not functional + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_5[] __initconst = { + "158LEMS1.103", + "158LEMS1.105", + "158LEMS1.106", + NULL +}; + +static struct msi_ec_conf CONF5 __initdata = { + .allowed_fw = ALLOWED_FW_5, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { // todo: reverse + .address = 0xbf, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { // unsupported? + .address = MSI_EC_ADDR_UNKNOWN, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, // needs testing + .rt_fan_speed_address = 0x71, // needs testing + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNKNOWN, + .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + }, + .leds = { + .micmute_led_address = 0x2b, + .mute_led_address = 0x2c, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xf3, not functional + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_6[] __initconst = { + "1542EMS1.102", + "1542EMS1.104", + NULL +}; + +static struct msi_ec_conf CONF6 __initdata = { + .allowed_fw = ALLOWED_FW_6, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = MSI_EC_ADDR_UNSUPP, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xbf, // todo: reverse + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xd5, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0xc9, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + }, + .leds = { + .micmute_led_address = MSI_EC_ADDR_UNSUPP, + .mute_led_address = MSI_EC_ADDR_UNSUPP, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = MSI_EC_ADDR_UNSUPP, // 0xf3, not functional + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_7[] __initconst = { + "17FKEMS1.108", + "17FKEMS1.109", + "17FKEMS1.10A", + NULL +}; + +static struct msi_ec_conf CONF7 __initdata = { + .allowed_fw = ALLOWED_FW_7, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = MSI_EC_ADDR_UNSUPP, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xbf, // needs testing + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNKNOWN, // 0xd5 but has its own wet of modes + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, // d may not be relevant + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0xc9, // needs testing + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNKNOWN, + .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + }, + .leds = { + .micmute_led_address = MSI_EC_ADDR_UNSUPP, + .mute_led_address = 0x2c, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = 0xf3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_8[] __initconst = { + "14F1EMS1.115", + NULL +}; + +static struct msi_ec_conf CONF8 __initdata = { + .allowed_fw = ALLOWED_FW_8, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = MSI_EC_ADDR_UNSUPP, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xeb, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_BASIC_NAME, 0x4d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNKNOWN, + .rt_fan_speed_address = MSI_EC_ADDR_UNKNOWN, + }, + .leds = { + .micmute_led_address = MSI_EC_ADDR_UNSUPP, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, // ? + .bl_modes = { 0x00, 0x08 }, // ? + .max_mode = 1, // ? + .bl_state_address = MSI_EC_ADDR_UNSUPP, // not functional + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_9[] __initconst = { + "14JKEMS1.104", + NULL +}; + +static struct msi_ec_conf CONF9 __initdata = { + .allowed_fw = ALLOWED_FW_9, + .charge_control = { + .address = 0xef, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xbf, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xf2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNSUPP, // unsupported or enabled by ECO shift + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xf4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x00, + .rt_fan_speed_base_max = 0x96, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNSUPP, + .rt_fan_speed_address = MSI_EC_ADDR_UNSUPP, + }, + .leds = { + .micmute_led_address = 0x2b, + .mute_led_address = 0x2c, + .bit = 2, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNSUPP, // not presented in MSI app + .bl_modes = { 0x00, 0x08 }, + .max_mode = 1, + .bl_state_address = 0xf3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_10[] __initconst = { + "1582EMS1.107", // GF66 11UC + "1583EMS1.109", // Pulse GL66 12UEK + NULL +}; + +static struct msi_ec_conf CONF10 __initdata = { + .allowed_fw = ALLOWED_FW_10, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = MSI_EC_ADDR_UNSUPP, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xe5, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, // ? + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNKNOWN, // ? + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2c, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, + .bl_modes = { 0x00, 0x08 }, + .max_mode = 1, + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_11[] __initconst = { + "16S6EMS1.111", // Prestige 15 a11scx + "1552EMS1.115", // Modern 15 a11m + NULL +}; + +static struct msi_ec_conf CONF11 __initdata = { + .allowed_fw = ALLOWED_FW_11, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = MSI_EC_ADDR_UNKNOWN, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = 0xeb, + .mask = 0x0f, + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x4d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNSUPP, + .rt_fan_speed_address = MSI_EC_ADDR_UNSUPP, + }, + .leds = { + .micmute_led_address = 0x2c, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, + .bl_modes = {}, // ? + .max_mode = 1, // ? + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_12[] __initconst = { + "16R6EMS1.104", // GF63 Thin 11UC + NULL +}; + +static struct msi_ec_conf CONF12 __initdata = { + .allowed_fw = ALLOWED_FW_12, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, + { SM_COMFORT_NAME, 0xc1 }, + { SM_SPORT_NAME, 0xc0 }, + { SM_TURBO_NAME, 0xc4 }, + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNSUPP, // 0xeb + .mask = 0x0f, // 00, 0f + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, + .rt_fan_speed_base_min = 0x19, + .rt_fan_speed_base_max = 0x37, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = MSI_EC_ADDR_UNSUPP, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = MSI_EC_ADDR_UNSUPP, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = MSI_EC_ADDR_UNKNOWN, + .bl_modes = { 0x00, 0x08 }, + .max_mode = 1, + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static const char * const ALLOWED_FW_13[] __initconst = { + "1594EMS1.109", // MSI Prestige 16 Studio A13VE + NULL +}; + +static struct msi_ec_conf CONF13 __initdata = { + .allowed_fw = ALLOWED_FW_13, + .charge_control = { + .address = 0xd7, + .offset_start = 0x8a, + .offset_end = 0x80, + .range_min = 0x8a, + .range_max = 0xe4, + }, + .webcam = { + .address = 0x2e, + .block_address = 0x2f, + .bit = 1, + }, + .fn_win_swap = { + .address = 0xe8, + .bit = 4, // 0x00-0x10 + }, + .cooler_boost = { + .address = 0x98, + .bit = 7, + }, + .shift_mode = { + .address = 0xd2, + .modes = { + { SM_ECO_NAME, 0xc2 }, // super battery + { SM_COMFORT_NAME, 0xc1 }, // balanced + { SM_TURBO_NAME, 0xc4 }, // extreme + MSI_EC_MODE_NULL + }, + }, + .super_battery = { + .address = MSI_EC_ADDR_UNSUPP, + .mask = 0x0f, // 00, 0f + }, + .fan_mode = { + .address = 0xd4, + .modes = { + { FM_AUTO_NAME, 0x0d }, + { FM_SILENT_NAME, 0x1d }, + { FM_ADVANCED_NAME, 0x8d }, + MSI_EC_MODE_NULL + }, + }, + .cpu = { + .rt_temp_address = 0x68, + .rt_fan_speed_address = 0x71, // 0x0-0x96 + .rt_fan_speed_base_min = 0x00, + .rt_fan_speed_base_max = 0x96, + .bs_fan_speed_address = MSI_EC_ADDR_UNSUPP, + .bs_fan_speed_base_min = 0x00, + .bs_fan_speed_base_max = 0x0f, + }, + .gpu = { + .rt_temp_address = 0x80, + .rt_fan_speed_address = 0x89, + }, + .leds = { + .micmute_led_address = 0x2c, + .mute_led_address = 0x2d, + .bit = 1, + }, + .kbd_bl = { + .bl_mode_address = 0x2c, // KB auto turn off + .bl_modes = { 0x00, 0x08 }, // always on; off after 10 sec + .max_mode = 1, + .bl_state_address = 0xd3, + .state_base_value = 0x80, + .max_state = 3, + }, +}; + +static struct msi_ec_conf *CONFIGS[] __initdata = { + &CONF0, + &CONF1, + &CONF2, + &CONF3, + &CONF4, + &CONF5, + &CONF6, + &CONF7, + &CONF8, + &CONF9, + &CONF10, + &CONF11, + &CONF12, + &CONF13, + NULL +}; + +static struct msi_ec_conf conf; // current configuration + +/* + * Helper functions + */ + +static int ec_read_seq(u8 addr, u8 *buf, u8 len) +{ + int result; + + for (u8 i = 0; i < len; i++) { + result = ec_read(addr + i, buf + i); + if (result < 0) + return result; + } + + return 0; +} + +static int ec_get_firmware_version(u8 buf[MSI_EC_FW_VERSION_LENGTH + 1]) +{ + int result; + + memset(buf, 0, MSI_EC_FW_VERSION_LENGTH + 1); + result = ec_read_seq(MSI_EC_FW_VERSION_ADDRESS, + buf, + MSI_EC_FW_VERSION_LENGTH); + if (result < 0) + return result; + + return MSI_EC_FW_VERSION_LENGTH + 1; +} + +/* + * Sysfs power_supply subsystem + */ + +static ssize_t charge_control_threshold_show(u8 offset, + struct device *device, + struct device_attribute *attr, + char *buf) +{ + u8 rdata; + int result; + + result = ec_read(conf.charge_control.address, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", rdata - offset); +} + +static ssize_t charge_control_threshold_store(u8 offset, + struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 wdata; + int result; + + result = kstrtou8(buf, 10, &wdata); + if (result < 0) + return result; + + wdata += offset; + if (wdata < conf.charge_control.range_min || + wdata > conf.charge_control.range_max) + return -EINVAL; + + result = ec_write(conf.charge_control.address, wdata); + if (result < 0) + return result; + + return count; +} + +static ssize_t charge_control_start_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return charge_control_threshold_show(conf.charge_control.offset_start, + device, attr, buf); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return charge_control_threshold_store(conf.charge_control.offset_start, + dev, attr, buf, count); +} + +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return charge_control_threshold_show(conf.charge_control.offset_end, + device, attr, buf); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return charge_control_threshold_store(conf.charge_control.offset_end, + dev, attr, buf, count); +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); +static DEVICE_ATTR_RW(charge_control_end_threshold); + +static struct attribute *msi_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + NULL +}; + +ATTRIBUTE_GROUPS(msi_battery); + +static int msi_battery_add(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + return device_add_groups(&battery->dev, msi_battery_groups); +} + +static int msi_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + device_remove_groups(&battery->dev, msi_battery_groups); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = msi_battery_add, + .remove_battery = msi_battery_remove, + .name = MSI_EC_DRIVER_NAME, +}; + +/* + * Module load/unload + */ + +static const struct dmi_system_id msi_dmi_table[] __initconst __maybe_unused = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + }, + }, + {} +}; +MODULE_DEVICE_TABLE(dmi, msi_dmi_table); + +static int __init load_configuration(void) +{ + int result; + + u8 fw_version[MSI_EC_FW_VERSION_LENGTH + 1]; + + /* get firmware version */ + result = ec_get_firmware_version(fw_version); + if (result < 0) + return result; + + /* load the suitable configuration, if exists */ + for (int i = 0; CONFIGS[i]; i++) { + if (match_string(CONFIGS[i]->allowed_fw, -1, fw_version) != -EINVAL) { + conf = *CONFIGS[i]; + conf.allowed_fw = NULL; + return 0; + } + } + + /* config not found */ + + for (int i = 0; i < MSI_EC_FW_VERSION_LENGTH; i++) { + if (!isgraph(fw_version[i])) { + pr_warn("Unable to find a valid firmware version!\n"); + return -EOPNOTSUPP; + } + } + + pr_warn("Firmware version is not supported: '%s'\n", fw_version); + return -EOPNOTSUPP; +} + +static int __init msi_ec_init(void) +{ + int result; + + result = load_configuration(); + if (result < 0) + return result; + + battery_hook_register(&battery_hook); + return 0; +} + +static void __exit msi_ec_exit(void) +{ + battery_hook_unregister(&battery_hook); +} + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Jose Angel Pastrana "); +MODULE_AUTHOR("Aakash Singh "); +MODULE_AUTHOR("Nikita Kravets "); +MODULE_DESCRIPTION("MSI Embedded Controller"); + +module_init(msi_ec_init); +module_exit(msi_ec_exit); diff --git a/drivers/platform/x86/msi-ec.h b/drivers/platform/x86/msi-ec.h new file mode 100644 index 000000000..086351217 --- /dev/null +++ b/drivers/platform/x86/msi-ec.h @@ -0,0 +1,122 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* + * msi-ec: MSI laptops' embedded controller driver. + * + * Copyright (C) 2023 Jose Angel Pastrana + * Copyright (C) 2023 Aakash Singh + * Copyright (C) 2023 Nikita Kravets + */ + +#ifndef _MSI_EC_H_ +#define _MSI_EC_H_ + +#include + +#define MSI_EC_DRIVER_NAME "msi-ec" + +#define MSI_EC_ADDR_UNKNOWN 0xff01 // unknown address +#define MSI_EC_ADDR_UNSUPP 0xff01 // unsupported parameter + +// Firmware info addresses are universal +#define MSI_EC_FW_VERSION_ADDRESS 0xa0 +#define MSI_EC_FW_DATE_ADDRESS 0xac +#define MSI_EC_FW_TIME_ADDRESS 0xb4 +#define MSI_EC_FW_VERSION_LENGTH 12 +#define MSI_EC_FW_DATE_LENGTH 8 +#define MSI_EC_FW_TIME_LENGTH 8 + +struct msi_ec_charge_control_conf { + int address; + int offset_start; + int offset_end; + int range_min; + int range_max; +}; + +struct msi_ec_webcam_conf { + int address; + int block_address; + int bit; +}; + +struct msi_ec_fn_win_swap_conf { + int address; + int bit; +}; + +struct msi_ec_cooler_boost_conf { + int address; + int bit; +}; + +#define MSI_EC_MODE_NULL { NULL, 0 } +struct msi_ec_mode { + const char *name; + int value; +}; + +struct msi_ec_shift_mode_conf { + int address; + struct msi_ec_mode modes[5]; // fixed size for easier hard coding +}; + +struct msi_ec_super_battery_conf { + int address; + int mask; +}; + +struct msi_ec_fan_mode_conf { + int address; + struct msi_ec_mode modes[5]; // fixed size for easier hard coding +}; + +struct msi_ec_cpu_conf { + int rt_temp_address; + int rt_fan_speed_address; // realtime + int rt_fan_speed_base_min; + int rt_fan_speed_base_max; + int bs_fan_speed_address; // basic + int bs_fan_speed_base_min; + int bs_fan_speed_base_max; +}; + +struct msi_ec_gpu_conf { + int rt_temp_address; + int rt_fan_speed_address; // realtime +}; + +struct msi_ec_led_conf { + int micmute_led_address; + int mute_led_address; + int bit; +}; + +#define MSI_EC_KBD_BL_STATE_MASK 0x3 +struct msi_ec_kbd_bl_conf { + int bl_mode_address; + int bl_modes[2]; + int max_mode; + + int bl_state_address; + int state_base_value; + int max_state; +}; + +struct msi_ec_conf { + const char * const *allowed_fw; + + struct msi_ec_charge_control_conf charge_control; + struct msi_ec_webcam_conf webcam; + struct msi_ec_fn_win_swap_conf fn_win_swap; + struct msi_ec_cooler_boost_conf cooler_boost; + struct msi_ec_shift_mode_conf shift_mode; + struct msi_ec_super_battery_conf super_battery; + struct msi_ec_fan_mode_conf fan_mode; + struct msi_ec_cpu_conf cpu; + struct msi_ec_gpu_conf gpu; + struct msi_ec_led_conf leds; + struct msi_ec_kbd_bl_conf kbd_bl; +}; + +#endif // _MSI_EC_H_ diff --git a/drivers/platform/x86/msi-laptop.c b/drivers/platform/x86/msi-laptop.c new file mode 100644 index 000000000..ddef6b78d --- /dev/null +++ b/drivers/platform/x86/msi-laptop.c @@ -0,0 +1,1152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/*-*-linux-c-*-*/ + +/* + Copyright (C) 2006 Lennart Poettering + + */ + +/* + * msi-laptop.c - MSI S270 laptop support. This laptop is sold under + * various brands, including "Cytron/TCM/Medion/Tchibo MD96100". + * + * Driver also supports S271, S420 models. + * + * This driver exports a few files in /sys/devices/platform/msi-laptop-pf/: + * + * lcd_level - Screen brightness: contains a single integer in the + * range 0..8. (rw) + * + * auto_brightness - Enable automatic brightness control: contains + * either 0 or 1. If set to 1 the hardware adjusts the screen + * brightness automatically when the power cord is + * plugged/unplugged. (rw) + * + * wlan - WLAN subsystem enabled: contains either 0 or 1. (ro) + * + * bluetooth - Bluetooth subsystem enabled: contains either 0 or 1 + * Please note that this file is constantly 0 if no Bluetooth + * hardware is available. (ro) + * + * In addition to these platform device attributes the driver + * registers itself in the Linux backlight control subsystem and is + * available to userspace under /sys/class/backlight/msi-laptop-bl/. + * + * This driver might work on other laptops produced by MSI. If you + * want to try it you can pass force=1 as argument to the module which + * will force it to load even when the DMI data doesn't identify the + * laptop as MSI S270. YMMV. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MSI_LCD_LEVEL_MAX 9 + +#define MSI_EC_COMMAND_WIRELESS 0x10 +#define MSI_EC_COMMAND_LCD_LEVEL 0x11 + +#define MSI_STANDARD_EC_COMMAND_ADDRESS 0x2e +#define MSI_STANDARD_EC_BLUETOOTH_MASK (1 << 0) +#define MSI_STANDARD_EC_WEBCAM_MASK (1 << 1) +#define MSI_STANDARD_EC_WLAN_MASK (1 << 3) +#define MSI_STANDARD_EC_3G_MASK (1 << 4) + +/* For set SCM load flag to disable BIOS fn key */ +#define MSI_STANDARD_EC_SCM_LOAD_ADDRESS 0x2d +#define MSI_STANDARD_EC_SCM_LOAD_MASK (1 << 0) + +#define MSI_STANDARD_EC_FUNCTIONS_ADDRESS 0xe4 +/* Power LED is orange - Turbo mode */ +#define MSI_STANDARD_EC_TURBO_MASK (1 << 1) +/* Power LED is green - ECO mode */ +#define MSI_STANDARD_EC_ECO_MASK (1 << 3) +/* Touchpad is turned on */ +#define MSI_STANDARD_EC_TOUCHPAD_MASK (1 << 4) +/* If this bit != bit 1, turbo mode can't be toggled */ +#define MSI_STANDARD_EC_TURBO_COOLDOWN_MASK (1 << 7) + +#define MSI_STANDARD_EC_FAN_ADDRESS 0x33 +/* If zero, fan rotates at maximal speed */ +#define MSI_STANDARD_EC_AUTOFAN_MASK (1 << 0) + +#ifdef CONFIG_PM_SLEEP +static int msi_laptop_resume(struct device *device); +#endif +static SIMPLE_DEV_PM_OPS(msi_laptop_pm, NULL, msi_laptop_resume); + +#define MSI_STANDARD_EC_DEVICES_EXISTS_ADDRESS 0x2f + +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, "Force driver load, ignore DMI data"); + +static int auto_brightness; +module_param(auto_brightness, int, 0); +MODULE_PARM_DESC(auto_brightness, "Enable automatic brightness control (0: disabled; 1: enabled; 2: don't touch)"); + +static const struct key_entry msi_laptop_keymap[] = { + {KE_KEY, KEY_TOUCHPAD_ON, {KEY_TOUCHPAD_ON} }, /* Touch Pad On */ + {KE_KEY, KEY_TOUCHPAD_OFF, {KEY_TOUCHPAD_OFF} },/* Touch Pad On */ + {KE_END, 0} +}; + +static struct input_dev *msi_laptop_input_dev; + +static int wlan_s, bluetooth_s, threeg_s; +static int threeg_exists; +static struct rfkill *rfk_wlan, *rfk_bluetooth, *rfk_threeg; + +/* MSI laptop quirks */ +struct quirk_entry { + bool old_ec_model; + + /* Some MSI 3G netbook only have one fn key to control + * Wlan/Bluetooth/3G, those netbook will load the SCM (windows app) to + * disable the original Wlan/Bluetooth control by BIOS when user press + * fn key, then control Wlan/Bluetooth/3G by SCM (software control by + * OS). Without SCM, user cann't on/off 3G module on those 3G netbook. + * On Linux, msi-laptop driver will do the same thing to disable the + * original BIOS control, then might need use HAL or other userland + * application to do the software control that simulate with SCM. + * e.g. MSI N034 netbook + */ + bool load_scm_model; + + /* Some MSI laptops need delay before reading from EC */ + bool ec_delay; + + /* Some MSI Wind netbooks (e.g. MSI Wind U100) need loading SCM to get + * some features working (e.g. ECO mode), but we cannot change + * Wlan/Bluetooth state in software and we can only read its state. + */ + bool ec_read_only; +}; + +static struct quirk_entry *quirks; + +/* Hardware access */ + +static int set_lcd_level(int level) +{ + u8 buf[2]; + + if (level < 0 || level >= MSI_LCD_LEVEL_MAX) + return -EINVAL; + + buf[0] = 0x80; + buf[1] = (u8) (level*31); + + return ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, buf, sizeof(buf), + NULL, 0); +} + +static int get_lcd_level(void) +{ + u8 wdata = 0, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, &wdata, 1, + &rdata, 1); + if (result < 0) + return result; + + return (int) rdata / 31; +} + +static int get_auto_brightness(void) +{ + u8 wdata = 4, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, &wdata, 1, + &rdata, 1); + if (result < 0) + return result; + + return !!(rdata & 8); +} + +static int set_auto_brightness(int enable) +{ + u8 wdata[2], rdata; + int result; + + wdata[0] = 4; + + result = ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, wdata, 1, + &rdata, 1); + if (result < 0) + return result; + + wdata[0] = 0x84; + wdata[1] = (rdata & 0xF7) | (enable ? 8 : 0); + + return ec_transaction(MSI_EC_COMMAND_LCD_LEVEL, wdata, 2, + NULL, 0); +} + +static ssize_t set_device_state(const char *buf, size_t count, u8 mask) +{ + int status; + u8 wdata = 0, rdata; + int result; + + if (sscanf(buf, "%i", &status) != 1 || (status < 0 || status > 1)) + return -EINVAL; + + if (quirks->ec_read_only) + return 0; + + /* read current device state */ + result = ec_read(MSI_STANDARD_EC_COMMAND_ADDRESS, &rdata); + if (result < 0) + return result; + + if (!!(rdata & mask) != status) { + /* reverse device bit */ + if (rdata & mask) + wdata = rdata & ~mask; + else + wdata = rdata | mask; + + result = ec_write(MSI_STANDARD_EC_COMMAND_ADDRESS, wdata); + if (result < 0) + return result; + } + + return count; +} + +static int get_wireless_state(int *wlan, int *bluetooth) +{ + u8 wdata = 0, rdata; + int result; + + result = ec_transaction(MSI_EC_COMMAND_WIRELESS, &wdata, 1, &rdata, 1); + if (result < 0) + return result; + + if (wlan) + *wlan = !!(rdata & 8); + + if (bluetooth) + *bluetooth = !!(rdata & 128); + + return 0; +} + +static int get_wireless_state_ec_standard(void) +{ + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_COMMAND_ADDRESS, &rdata); + if (result < 0) + return result; + + wlan_s = !!(rdata & MSI_STANDARD_EC_WLAN_MASK); + + bluetooth_s = !!(rdata & MSI_STANDARD_EC_BLUETOOTH_MASK); + + threeg_s = !!(rdata & MSI_STANDARD_EC_3G_MASK); + + return 0; +} + +static int get_threeg_exists(void) +{ + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_DEVICES_EXISTS_ADDRESS, &rdata); + if (result < 0) + return result; + + threeg_exists = !!(rdata & MSI_STANDARD_EC_3G_MASK); + + return 0; +} + +/* Backlight device stuff */ + +static int bl_get_brightness(struct backlight_device *b) +{ + return get_lcd_level(); +} + + +static int bl_update_status(struct backlight_device *b) +{ + return set_lcd_level(b->props.brightness); +} + +static const struct backlight_ops msibl_ops = { + .get_brightness = bl_get_brightness, + .update_status = bl_update_status, +}; + +static struct backlight_device *msibl_device; + +/* Platform device */ + +static ssize_t show_wlan(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret, enabled = 0; + + if (quirks->old_ec_model) { + ret = get_wireless_state(&enabled, NULL); + } else { + ret = get_wireless_state_ec_standard(); + enabled = wlan_s; + } + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%i\n", enabled); +} + +static ssize_t store_wlan(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return set_device_state(buf, count, MSI_STANDARD_EC_WLAN_MASK); +} + +static ssize_t show_bluetooth(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret, enabled = 0; + + if (quirks->old_ec_model) { + ret = get_wireless_state(NULL, &enabled); + } else { + ret = get_wireless_state_ec_standard(); + enabled = bluetooth_s; + } + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%i\n", enabled); +} + +static ssize_t store_bluetooth(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return set_device_state(buf, count, MSI_STANDARD_EC_BLUETOOTH_MASK); +} + +static ssize_t show_threeg(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + /* old msi ec not support 3G */ + if (quirks->old_ec_model) + return -ENODEV; + + ret = get_wireless_state_ec_standard(); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%i\n", threeg_s); +} + +static ssize_t store_threeg(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return set_device_state(buf, count, MSI_STANDARD_EC_3G_MASK); +} + +static ssize_t show_lcd_level(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_lcd_level(); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%i\n", ret); +} + +static ssize_t store_lcd_level(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + + int level, ret; + + if (sscanf(buf, "%i", &level) != 1 || + (level < 0 || level >= MSI_LCD_LEVEL_MAX)) + return -EINVAL; + + ret = set_lcd_level(level); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t show_auto_brightness(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + int ret; + + ret = get_auto_brightness(); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%i\n", ret); +} + +static ssize_t store_auto_brightness(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + + int enable, ret; + + if (sscanf(buf, "%i", &enable) != 1 || (enable != (enable & 1))) + return -EINVAL; + + ret = set_auto_brightness(enable); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t show_touchpad(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_TOUCHPAD_MASK)); +} + +static ssize_t show_turbo(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_TURBO_MASK)); +} + +static ssize_t show_eco(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_ECO_MASK)); +} + +static ssize_t show_turbo_cooldown(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", (!!(rdata & MSI_STANDARD_EC_TURBO_MASK)) | + (!!(rdata & MSI_STANDARD_EC_TURBO_COOLDOWN_MASK) << 1)); +} + +static ssize_t show_auto_fan(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FAN_ADDRESS, &rdata); + if (result < 0) + return result; + + return sysfs_emit(buf, "%i\n", !!(rdata & MSI_STANDARD_EC_AUTOFAN_MASK)); +} + +static ssize_t store_auto_fan(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + + int enable, result; + + if (sscanf(buf, "%i", &enable) != 1 || (enable != (enable & 1))) + return -EINVAL; + + result = ec_write(MSI_STANDARD_EC_FAN_ADDRESS, enable); + if (result < 0) + return result; + + return count; +} + +static DEVICE_ATTR(lcd_level, 0644, show_lcd_level, store_lcd_level); +static DEVICE_ATTR(auto_brightness, 0644, show_auto_brightness, + store_auto_brightness); +static DEVICE_ATTR(bluetooth, 0444, show_bluetooth, NULL); +static DEVICE_ATTR(wlan, 0444, show_wlan, NULL); +static DEVICE_ATTR(threeg, 0444, show_threeg, NULL); +static DEVICE_ATTR(touchpad, 0444, show_touchpad, NULL); +static DEVICE_ATTR(turbo_mode, 0444, show_turbo, NULL); +static DEVICE_ATTR(eco_mode, 0444, show_eco, NULL); +static DEVICE_ATTR(turbo_cooldown, 0444, show_turbo_cooldown, NULL); +static DEVICE_ATTR(auto_fan, 0644, show_auto_fan, store_auto_fan); + +static struct attribute *msipf_attributes[] = { + &dev_attr_bluetooth.attr, + &dev_attr_wlan.attr, + &dev_attr_touchpad.attr, + &dev_attr_turbo_mode.attr, + &dev_attr_eco_mode.attr, + &dev_attr_turbo_cooldown.attr, + &dev_attr_auto_fan.attr, + NULL +}; + +static struct attribute *msipf_old_attributes[] = { + &dev_attr_lcd_level.attr, + &dev_attr_auto_brightness.attr, + NULL +}; + +static const struct attribute_group msipf_attribute_group = { + .attrs = msipf_attributes +}; + +static const struct attribute_group msipf_old_attribute_group = { + .attrs = msipf_old_attributes +}; + +static struct platform_driver msipf_driver = { + .driver = { + .name = "msi-laptop-pf", + .pm = &msi_laptop_pm, + }, +}; + +static struct platform_device *msipf_device; + +/* Initialization */ + +static struct quirk_entry quirk_old_ec_model = { + .old_ec_model = true, +}; + +static struct quirk_entry quirk_load_scm_model = { + .load_scm_model = true, + .ec_delay = true, +}; + +static struct quirk_entry quirk_load_scm_ro_model = { + .load_scm_model = true, + .ec_read_only = true, +}; + +static int dmi_check_cb(const struct dmi_system_id *dmi) +{ + pr_info("Identified laptop model '%s'\n", dmi->ident); + + quirks = dmi->driver_data; + + return 1; +} + +static unsigned long msi_work_delay(int msecs) +{ + if (quirks->ec_delay) + return msecs_to_jiffies(msecs); + + return 0; +} + +static const struct dmi_system_id msi_dmi_table[] __initconst = { + { + .ident = "MSI S270", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1013"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0131"), + DMI_MATCH(DMI_CHASSIS_VENDOR, "MICRO-STAR INT") + }, + .driver_data = &quirk_old_ec_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI S271", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1058"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0581"), + DMI_MATCH(DMI_BOARD_NAME, "MS-1058") + }, + .driver_data = &quirk_old_ec_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI S420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-1412"), + DMI_MATCH(DMI_BOARD_VENDOR, "MSI"), + DMI_MATCH(DMI_BOARD_NAME, "MS-1412") + }, + .driver_data = &quirk_old_ec_model, + .callback = dmi_check_cb + }, + { + .ident = "Medion MD96100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "NOTEBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "SAM2000"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0131"), + DMI_MATCH(DMI_CHASSIS_VENDOR, "MICRO-STAR INT") + }, + .driver_data = &quirk_old_ec_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI N034", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-N034"), + DMI_MATCH(DMI_CHASSIS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD") + }, + .driver_data = &quirk_load_scm_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI N051", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-N051"), + DMI_MATCH(DMI_CHASSIS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD") + }, + .driver_data = &quirk_load_scm_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI N014", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD"), + DMI_MATCH(DMI_PRODUCT_NAME, "MS-N014"), + }, + .driver_data = &quirk_load_scm_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI CR620", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "Micro-Star International"), + DMI_MATCH(DMI_PRODUCT_NAME, "CR620"), + }, + .driver_data = &quirk_load_scm_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI U270", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_PRODUCT_NAME, "U270 series"), + }, + .driver_data = &quirk_load_scm_model, + .callback = dmi_check_cb + }, + { + .ident = "MSI U90/U100", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "MICRO-STAR INTERNATIONAL CO., LTD"), + DMI_MATCH(DMI_PRODUCT_NAME, "U90/U100"), + }, + .driver_data = &quirk_load_scm_ro_model, + .callback = dmi_check_cb + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, msi_dmi_table); + +static int rfkill_bluetooth_set(void *data, bool blocked) +{ + /* Do something with blocked...*/ + /* + * blocked == false is on + * blocked == true is off + */ + int result = set_device_state(blocked ? "0" : "1", 0, + MSI_STANDARD_EC_BLUETOOTH_MASK); + + return min(result, 0); +} + +static int rfkill_wlan_set(void *data, bool blocked) +{ + int result = set_device_state(blocked ? "0" : "1", 0, + MSI_STANDARD_EC_WLAN_MASK); + + return min(result, 0); +} + +static int rfkill_threeg_set(void *data, bool blocked) +{ + int result = set_device_state(blocked ? "0" : "1", 0, + MSI_STANDARD_EC_3G_MASK); + + return min(result, 0); +} + +static const struct rfkill_ops rfkill_bluetooth_ops = { + .set_block = rfkill_bluetooth_set +}; + +static const struct rfkill_ops rfkill_wlan_ops = { + .set_block = rfkill_wlan_set +}; + +static const struct rfkill_ops rfkill_threeg_ops = { + .set_block = rfkill_threeg_set +}; + +static void rfkill_cleanup(void) +{ + if (rfk_bluetooth) { + rfkill_unregister(rfk_bluetooth); + rfkill_destroy(rfk_bluetooth); + } + + if (rfk_threeg) { + rfkill_unregister(rfk_threeg); + rfkill_destroy(rfk_threeg); + } + + if (rfk_wlan) { + rfkill_unregister(rfk_wlan); + rfkill_destroy(rfk_wlan); + } +} + +static bool msi_rfkill_set_state(struct rfkill *rfkill, bool blocked) +{ + if (quirks->ec_read_only) + return rfkill_set_hw_state(rfkill, blocked); + else + return rfkill_set_sw_state(rfkill, blocked); +} + +static void msi_update_rfkill(struct work_struct *ignored) +{ + get_wireless_state_ec_standard(); + + if (rfk_wlan) + msi_rfkill_set_state(rfk_wlan, !wlan_s); + if (rfk_bluetooth) + msi_rfkill_set_state(rfk_bluetooth, !bluetooth_s); + if (rfk_threeg) + msi_rfkill_set_state(rfk_threeg, !threeg_s); +} +static DECLARE_DELAYED_WORK(msi_rfkill_dwork, msi_update_rfkill); + +static void msi_send_touchpad_key(struct work_struct *ignored) +{ + u8 rdata; + int result; + + result = ec_read(MSI_STANDARD_EC_FUNCTIONS_ADDRESS, &rdata); + if (result < 0) + return; + + sparse_keymap_report_event(msi_laptop_input_dev, + (rdata & MSI_STANDARD_EC_TOUCHPAD_MASK) ? + KEY_TOUCHPAD_ON : KEY_TOUCHPAD_OFF, 1, true); +} +static DECLARE_DELAYED_WORK(msi_touchpad_dwork, msi_send_touchpad_key); + +static bool msi_laptop_i8042_filter(unsigned char data, unsigned char str, struct serio *port, + void *context) +{ + static bool extended; + + if (str & I8042_STR_AUXDATA) + return false; + + /* 0x54 wwan, 0x62 bluetooth, 0x76 wlan, 0xE4 touchpad toggle*/ + if (unlikely(data == 0xe0)) { + extended = true; + return false; + } else if (unlikely(extended)) { + extended = false; + switch (data) { + case 0xE4: + schedule_delayed_work(&msi_touchpad_dwork, msi_work_delay(500)); + break; + case 0x54: + case 0x62: + case 0x76: + schedule_delayed_work(&msi_rfkill_dwork, msi_work_delay(500)); + break; + } + } + + return false; +} + +static void msi_init_rfkill(struct work_struct *ignored) +{ + if (rfk_wlan) { + msi_rfkill_set_state(rfk_wlan, !wlan_s); + rfkill_wlan_set(NULL, !wlan_s); + } + if (rfk_bluetooth) { + msi_rfkill_set_state(rfk_bluetooth, !bluetooth_s); + rfkill_bluetooth_set(NULL, !bluetooth_s); + } + if (rfk_threeg) { + msi_rfkill_set_state(rfk_threeg, !threeg_s); + rfkill_threeg_set(NULL, !threeg_s); + } +} +static DECLARE_DELAYED_WORK(msi_rfkill_init, msi_init_rfkill); + +static int rfkill_init(struct platform_device *sdev) +{ + /* add rfkill */ + int retval; + + /* keep the hardware wireless state */ + get_wireless_state_ec_standard(); + + rfk_bluetooth = rfkill_alloc("msi-bluetooth", &sdev->dev, + RFKILL_TYPE_BLUETOOTH, + &rfkill_bluetooth_ops, NULL); + if (!rfk_bluetooth) { + retval = -ENOMEM; + goto err_bluetooth; + } + retval = rfkill_register(rfk_bluetooth); + if (retval) + goto err_bluetooth; + + rfk_wlan = rfkill_alloc("msi-wlan", &sdev->dev, RFKILL_TYPE_WLAN, + &rfkill_wlan_ops, NULL); + if (!rfk_wlan) { + retval = -ENOMEM; + goto err_wlan; + } + retval = rfkill_register(rfk_wlan); + if (retval) + goto err_wlan; + + if (threeg_exists) { + rfk_threeg = rfkill_alloc("msi-threeg", &sdev->dev, + RFKILL_TYPE_WWAN, &rfkill_threeg_ops, NULL); + if (!rfk_threeg) { + retval = -ENOMEM; + goto err_threeg; + } + retval = rfkill_register(rfk_threeg); + if (retval) + goto err_threeg; + } + + /* schedule to run rfkill state initial */ + schedule_delayed_work(&msi_rfkill_init, msi_work_delay(1000)); + return 0; + +err_threeg: + rfkill_destroy(rfk_threeg); + if (rfk_wlan) + rfkill_unregister(rfk_wlan); +err_wlan: + rfkill_destroy(rfk_wlan); + if (rfk_bluetooth) + rfkill_unregister(rfk_bluetooth); +err_bluetooth: + rfkill_destroy(rfk_bluetooth); + + return retval; +} + +static int msi_scm_disable_hw_fn_handling(void) +{ + u8 data; + int result; + + if (!quirks->load_scm_model) + return 0; + + /* set load SCM to disable hardware control by fn key */ + result = ec_read(MSI_STANDARD_EC_SCM_LOAD_ADDRESS, &data); + if (result < 0) + return result; + + result = ec_write(MSI_STANDARD_EC_SCM_LOAD_ADDRESS, + data | MSI_STANDARD_EC_SCM_LOAD_MASK); + if (result < 0) + return result; + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int msi_laptop_resume(struct device *device) +{ + return msi_scm_disable_hw_fn_handling(); +} +#endif + +static int __init msi_laptop_input_setup(void) +{ + int err; + + msi_laptop_input_dev = input_allocate_device(); + if (!msi_laptop_input_dev) + return -ENOMEM; + + msi_laptop_input_dev->name = "MSI Laptop hotkeys"; + msi_laptop_input_dev->phys = "msi-laptop/input0"; + msi_laptop_input_dev->id.bustype = BUS_HOST; + + err = sparse_keymap_setup(msi_laptop_input_dev, + msi_laptop_keymap, NULL); + if (err) + goto err_free_dev; + + err = input_register_device(msi_laptop_input_dev); + if (err) + goto err_free_dev; + + return 0; + +err_free_dev: + input_free_device(msi_laptop_input_dev); + return err; +} + +static int __init load_scm_model_init(struct platform_device *sdev) +{ + int result; + + if (!quirks->ec_read_only) { + /* allow userland write sysfs file */ + dev_attr_bluetooth.store = store_bluetooth; + dev_attr_wlan.store = store_wlan; + dev_attr_threeg.store = store_threeg; + dev_attr_bluetooth.attr.mode |= S_IWUSR; + dev_attr_wlan.attr.mode |= S_IWUSR; + dev_attr_threeg.attr.mode |= S_IWUSR; + } + + /* disable hardware control by fn key */ + result = msi_scm_disable_hw_fn_handling(); + if (result < 0) + return result; + + /* initial rfkill */ + result = rfkill_init(sdev); + if (result < 0) + goto fail_rfkill; + + /* setup input device */ + result = msi_laptop_input_setup(); + if (result) + goto fail_input; + + result = i8042_install_filter(msi_laptop_i8042_filter, NULL); + if (result) { + pr_err("Unable to install key filter\n"); + goto fail_filter; + } + + return 0; + +fail_filter: + input_unregister_device(msi_laptop_input_dev); + +fail_input: + rfkill_cleanup(); + +fail_rfkill: + return result; +} + +static void msi_scm_model_exit(void) +{ + if (!quirks->load_scm_model) + return; + + i8042_remove_filter(msi_laptop_i8042_filter); + cancel_delayed_work_sync(&msi_touchpad_dwork); + input_unregister_device(msi_laptop_input_dev); + cancel_delayed_work_sync(&msi_rfkill_dwork); + rfkill_cleanup(); +} + +static int __init msi_init(void) +{ + int ret; + + if (acpi_disabled) + return -ENODEV; + + dmi_check_system(msi_dmi_table); + if (!quirks) + /* quirks may be NULL if no match in DMI table */ + quirks = &quirk_load_scm_model; + if (force) + quirks = &quirk_old_ec_model; + + if (!quirks->old_ec_model) + get_threeg_exists(); + + if (auto_brightness < 0 || auto_brightness > 2) + return -EINVAL; + + /* Register backlight stuff */ + if (quirks->old_ec_model && + acpi_video_get_backlight_type() == acpi_backlight_vendor) { + struct backlight_properties props; + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = MSI_LCD_LEVEL_MAX - 1; + msibl_device = backlight_device_register("msi-laptop-bl", NULL, + NULL, &msibl_ops, + &props); + if (IS_ERR(msibl_device)) + return PTR_ERR(msibl_device); + } + + ret = platform_driver_register(&msipf_driver); + if (ret) + goto fail_backlight; + + /* Register platform stuff */ + + msipf_device = platform_device_alloc("msi-laptop-pf", PLATFORM_DEVID_NONE); + if (!msipf_device) { + ret = -ENOMEM; + goto fail_platform_driver; + } + + ret = platform_device_add(msipf_device); + if (ret) + goto fail_device_add; + + if (quirks->load_scm_model && (load_scm_model_init(msipf_device) < 0)) { + ret = -EINVAL; + goto fail_scm_model_init; + } + + ret = sysfs_create_group(&msipf_device->dev.kobj, + &msipf_attribute_group); + if (ret) + goto fail_create_group; + + if (!quirks->old_ec_model) { + if (threeg_exists) + ret = device_create_file(&msipf_device->dev, + &dev_attr_threeg); + if (ret) + goto fail_create_attr; + } else { + ret = sysfs_create_group(&msipf_device->dev.kobj, + &msipf_old_attribute_group); + if (ret) + goto fail_create_attr; + + /* Disable automatic brightness control by default because + * this module was probably loaded to do brightness control in + * software. */ + + if (auto_brightness != 2) + set_auto_brightness(auto_brightness); + } + + return 0; + +fail_create_attr: + sysfs_remove_group(&msipf_device->dev.kobj, &msipf_attribute_group); +fail_create_group: + msi_scm_model_exit(); +fail_scm_model_init: + platform_device_del(msipf_device); +fail_device_add: + platform_device_put(msipf_device); +fail_platform_driver: + platform_driver_unregister(&msipf_driver); +fail_backlight: + backlight_device_unregister(msibl_device); + + return ret; +} + +static void __exit msi_cleanup(void) +{ + msi_scm_model_exit(); + sysfs_remove_group(&msipf_device->dev.kobj, &msipf_attribute_group); + if (!quirks->old_ec_model && threeg_exists) + device_remove_file(&msipf_device->dev, &dev_attr_threeg); + if (quirks->old_ec_model) + sysfs_remove_group(&msipf_device->dev.kobj, + &msipf_old_attribute_group); + platform_device_unregister(msipf_device); + platform_driver_unregister(&msipf_driver); + backlight_device_unregister(msibl_device); + + if (quirks->old_ec_model) { + /* Enable automatic brightness control again */ + if (auto_brightness != 2) + set_auto_brightness(1); + } +} + +module_init(msi_init); +module_exit(msi_cleanup); + +MODULE_AUTHOR("Lennart Poettering"); +MODULE_DESCRIPTION("MSI Laptop Support"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c new file mode 100644 index 000000000..e912fcc12 --- /dev/null +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Linux driver for WMI platform features on MSI notebooks. + * + * Copyright (C) 2024 Armin Wolf + */ + +#define pr_format(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DRIVER_NAME "msi-wmi-platform" + +#define MSI_PLATFORM_GUID "ABBC0F6E-8EA1-11D1-00A0-C90629100000" + +#define MSI_WMI_PLATFORM_INTERFACE_VERSION 2 + +#define MSI_PLATFORM_WMI_MAJOR_OFFSET 1 +#define MSI_PLATFORM_WMI_MINOR_OFFSET 2 + +#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) +#define MSI_PLATFORM_EC_CHANGED_PAGE BIT(6) +#define MSI_PLATFORM_EC_IS_TIGERLAKE BIT(7) +#define MSI_PLATFORM_EC_VERSION_OFFSET 2 + +static bool force; +module_param_unsafe(force, bool, 0); +MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); + +enum msi_wmi_platform_method { + MSI_PLATFORM_GET_PACKAGE = 0x01, + MSI_PLATFORM_SET_PACKAGE = 0x02, + MSI_PLATFORM_GET_EC = 0x03, + MSI_PLATFORM_SET_EC = 0x04, + MSI_PLATFORM_GET_BIOS = 0x05, + MSI_PLATFORM_SET_BIOS = 0x06, + MSI_PLATFORM_GET_SMBUS = 0x07, + MSI_PLATFORM_SET_SMBUS = 0x08, + MSI_PLATFORM_GET_MASTER_BATTERY = 0x09, + MSI_PLATFORM_SET_MASTER_BATTERY = 0x0a, + MSI_PLATFORM_GET_SLAVE_BATTERY = 0x0b, + MSI_PLATFORM_SET_SLAVE_BATTERY = 0x0c, + MSI_PLATFORM_GET_TEMPERATURE = 0x0d, + MSI_PLATFORM_SET_TEMPERATURE = 0x0e, + MSI_PLATFORM_GET_THERMAL = 0x0f, + MSI_PLATFORM_SET_THERMAL = 0x10, + MSI_PLATFORM_GET_FAN = 0x11, + MSI_PLATFORM_SET_FAN = 0x12, + MSI_PLATFORM_GET_DEVICE = 0x13, + MSI_PLATFORM_SET_DEVICE = 0x14, + MSI_PLATFORM_GET_POWER = 0x15, + MSI_PLATFORM_SET_POWER = 0x16, + MSI_PLATFORM_GET_DEBUG = 0x17, + MSI_PLATFORM_SET_DEBUG = 0x18, + MSI_PLATFORM_GET_AP = 0x19, + MSI_PLATFORM_SET_AP = 0x1a, + MSI_PLATFORM_GET_DATA = 0x1b, + MSI_PLATFORM_SET_DATA = 0x1c, + MSI_PLATFORM_GET_WMI = 0x1d, +}; + +struct msi_wmi_platform_data { + struct wmi_device *wdev; + struct mutex wmi_lock; /* Necessary when calling WMI methods */ +}; + +struct msi_wmi_platform_debugfs_data { + struct msi_wmi_platform_data *data; + enum msi_wmi_platform_method method; + struct rw_semaphore buffer_lock; /* Protects debugfs buffer */ + size_t length; + u8 buffer[32]; +}; + +static const char * const msi_wmi_platform_debugfs_names[] = { + "get_package", + "set_package", + "get_ec", + "set_ec", + "get_bios", + "set_bios", + "get_smbus", + "set_smbus", + "get_master_battery", + "set_master_battery", + "get_slave_battery", + "set_slave_battery", + "get_temperature", + "set_temperature", + "get_thermal", + "set_thermal", + "get_fan", + "set_fan", + "get_device", + "set_device", + "get_power", + "set_power", + "get_debug", + "set_debug", + "get_ap", + "set_ap", + "get_data", + "set_data", + "get_wmi" +}; + +static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, size_t length) +{ + if (obj->type != ACPI_TYPE_BUFFER) + return -ENOMSG; + + if (obj->buffer.length != length) + return -EPROTO; + + if (!obj->buffer.pointer[0]) + return -EIO; + + memcpy(output, obj->buffer.pointer, obj->buffer.length); + + 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) +{ + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer in = { + .length = input_length, + .pointer = input + }; + union acpi_object *obj; + acpi_status status; + int ret; + + if (!input_length || !output_length) + return -EINVAL; + + /* + * 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; + } + + obj = out.pointer; + if (!obj) + return -ENODATA; + + ret = msi_wmi_platform_parse_buffer(obj, output, output_length); + kfree(obj); + + return ret; +} + +static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return 0444; +} + +static int msi_wmi_platform_read(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 input[32] = { 0 }; + u8 output[32]; + u16 value; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, input, sizeof(input), output, + sizeof(output)); + if (ret < 0) + return ret; + + value = get_unaligned_be16(&output[channel * 2 + 1]); + if (!value) + *val = 0; + else + *val = 480000 / value; + + return 0; +} + +static const struct hwmon_ops msi_wmi_platform_ops = { + .is_visible = msi_wmi_platform_is_visible, + .read = msi_wmi_platform_read, +}; + +static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT, + HWMON_F_INPUT, + HWMON_F_INPUT, + HWMON_F_INPUT + ), + NULL +}; + +static const struct hwmon_chip_info msi_wmi_platform_chip_info = { + .ops = &msi_wmi_platform_ops, + .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) +{ + struct seq_file *seq = fp->private_data; + struct msi_wmi_platform_debugfs_data *data = seq->private; + u8 payload[32] = { }; + ssize_t ret; + + /* Do not allow partial writes */ + if (*offset != 0) + return -EINVAL; + + /* Do not allow incomplete command buffers */ + if (length != data->length) + return -EINVAL; + + ret = simple_write_to_buffer(payload, sizeof(payload), offset, input, length); + if (ret < 0) + return ret; + + down_write(&data->buffer_lock); + ret = msi_wmi_platform_query(data->data, data->method, payload, data->length, data->buffer, + data->length); + up_write(&data->buffer_lock); + + if (ret < 0) + return ret; + + return length; +} + +static int msi_wmi_platform_show(struct seq_file *seq, void *p) +{ + struct msi_wmi_platform_debugfs_data *data = seq->private; + int ret; + + down_read(&data->buffer_lock); + ret = seq_write(seq, data->buffer, data->length); + up_read(&data->buffer_lock); + + return ret; +} + +static int msi_wmi_platform_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); +} + +static const struct file_operations msi_wmi_platform_debugfs_fops = { + .owner = THIS_MODULE, + .open = msi_wmi_platform_open, + .read = seq_read, + .write = msi_wmi_platform_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static void msi_wmi_platform_debugfs_remove(void *data) +{ + struct dentry *dir = data; + + debugfs_remove_recursive(dir); +} + +static void msi_wmi_platform_debugfs_add(struct msi_wmi_platform_data *drvdata, struct dentry *dir, + const char *name, enum msi_wmi_platform_method method) +{ + struct msi_wmi_platform_debugfs_data *data; + struct dentry *entry; + + data = devm_kzalloc(&drvdata->wdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return; + + data->data = drvdata; + data->method = method; + init_rwsem(&data->buffer_lock); + + /* The ACPI firmware for now always requires a 32 byte input buffer due to + * a peculiarity in how Windows handles the CreateByteField() ACPI operator. + */ + data->length = 32; + + entry = debugfs_create_file(name, 0600, dir, data, &msi_wmi_platform_debugfs_fops); + if (IS_ERR(entry)) + devm_kfree(&drvdata->wdev->dev, data); +} + +static void msi_wmi_platform_debugfs_init(struct msi_wmi_platform_data *data) +{ + struct dentry *dir; + char dir_name[64]; + int ret, method; + + scnprintf(dir_name, ARRAY_SIZE(dir_name), "%s-%s", DRIVER_NAME, dev_name(&data->wdev->dev)); + + dir = debugfs_create_dir(dir_name, NULL); + if (IS_ERR(dir)) + return; + + ret = devm_add_action_or_reset(&data->wdev->dev, msi_wmi_platform_debugfs_remove, dir); + if (ret < 0) + return; + + for (method = MSI_PLATFORM_GET_PACKAGE; method <= MSI_PLATFORM_GET_WMI; method++) + msi_wmi_platform_debugfs_add(data, dir, msi_wmi_platform_debugfs_names[method - 1], + method); +} + +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); + + 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 flags; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, input, sizeof(input), output, + sizeof(output)); + if (ret < 0) + return ret; + + flags = output[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]); + + if (!(flags & MSI_PLATFORM_EC_IS_TIGERLAKE)) { + if (!force) + return -ENODEV; + + dev_warn(&data->wdev->dev, "Loading on a non-Tigerlake platform\n"); + } + + return 0; +} + +static int msi_wmi_platform_init(struct msi_wmi_platform_data *data) +{ + u8 input[32] = { 0 }; + u8 output[32]; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, input, sizeof(input), output, + sizeof(output)); + 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]); + + if (output[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]); + } + + return 0; +} + +static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) +{ + struct msi_wmi_platform_data *data; + int ret; + + data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->wdev = wdev; + dev_set_drvdata(&wdev->dev, data); + + ret = devm_mutex_init(&wdev->dev, &data->wmi_lock); + if (ret < 0) + return ret; + + ret = msi_wmi_platform_init(data); + if (ret < 0) + return ret; + + ret = msi_wmi_platform_ec_init(data); + if (ret < 0) + return ret; + + msi_wmi_platform_debugfs_init(data); + + return msi_wmi_platform_hwmon_init(data); +} + +static const struct wmi_device_id msi_wmi_platform_id_table[] = { + { MSI_PLATFORM_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, msi_wmi_platform_id_table); + +static struct wmi_driver msi_wmi_platform_driver = { + .driver = { + .name = DRIVER_NAME, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = msi_wmi_platform_id_table, + .probe = msi_wmi_platform_probe, + .no_singleton = true, +}; + +/* + * MSI reused the WMI GUID from the WMI-ACPI sample code provided by Microsoft, + * so other manufacturers might use it as well for their WMI-ACPI implementations. + */ +static const struct dmi_system_id msi_wmi_platform_whitelist[] __initconst = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"), + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"), + }, + }, + { } +}; + +static int __init msi_wmi_platform_module_init(void) +{ + if (!dmi_check_system(msi_wmi_platform_whitelist)) { + if (!force) + return -ENODEV; + + pr_warn("Ignoring DMI whitelist\n"); + } + + return wmi_driver_register(&msi_wmi_platform_driver); +} + +static void __exit msi_wmi_platform_module_exit(void) +{ + wmi_driver_unregister(&msi_wmi_platform_driver); +} + +module_init(msi_wmi_platform_module_init); +module_exit(msi_wmi_platform_module_exit); + + +MODULE_AUTHOR("Armin Wolf "); +MODULE_DESCRIPTION("MSI WMI platform features"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/msi-wmi.c b/drivers/platform/x86/msi-wmi.c new file mode 100644 index 000000000..4a7ac85c4 --- /dev/null +++ b/drivers/platform/x86/msi-wmi.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MSI WMI hotkeys + * + * Copyright (C) 2009 Novell + * + * Most stuff taken over from hp-wmi + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Thomas Renninger "); +MODULE_DESCRIPTION("MSI laptop WMI hotkeys driver"); +MODULE_LICENSE("GPL"); + +#define DRV_NAME "msi-wmi" + +#define MSIWMI_BIOS_GUID "551A1F84-FBDD-4125-91DB-3EA8F44F1D45" +#define MSIWMI_MSI_EVENT_GUID "B6F3EEF2-3D2F-49DC-9DE3-85BCE18C62F2" +#define MSIWMI_WIND_EVENT_GUID "5B3CC38A-40D9-7245-8AE6-1145B751BE3F" + +MODULE_ALIAS("wmi:" MSIWMI_BIOS_GUID); +MODULE_ALIAS("wmi:" MSIWMI_MSI_EVENT_GUID); +MODULE_ALIAS("wmi:" MSIWMI_WIND_EVENT_GUID); + +enum msi_scancodes { + /* Generic MSI keys (not present on MSI Wind) */ + MSI_KEY_BRIGHTNESSUP = 0xD0, + MSI_KEY_BRIGHTNESSDOWN, + MSI_KEY_VOLUMEUP, + MSI_KEY_VOLUMEDOWN, + MSI_KEY_MUTE, + /* MSI Wind keys */ + WIND_KEY_TOUCHPAD = 0x08, /* Fn+F3 touchpad toggle */ + WIND_KEY_BLUETOOTH = 0x56, /* Fn+F11 Bluetooth toggle */ + WIND_KEY_CAMERA, /* Fn+F6 webcam toggle */ + WIND_KEY_WLAN = 0x5f, /* Fn+F11 Wi-Fi toggle */ + WIND_KEY_TURBO, /* Fn+F10 turbo mode toggle */ + WIND_KEY_ECO = 0x69, /* Fn+F10 ECO mode toggle */ +}; +static struct key_entry msi_wmi_keymap[] = { + { KE_KEY, MSI_KEY_BRIGHTNESSUP, {KEY_BRIGHTNESSUP} }, + { KE_KEY, MSI_KEY_BRIGHTNESSDOWN, {KEY_BRIGHTNESSDOWN} }, + { KE_KEY, MSI_KEY_VOLUMEUP, {KEY_VOLUMEUP} }, + { KE_KEY, MSI_KEY_VOLUMEDOWN, {KEY_VOLUMEDOWN} }, + { KE_KEY, MSI_KEY_MUTE, {KEY_MUTE} }, + + /* These keys work without WMI. Ignore them to avoid double keycodes */ + { KE_IGNORE, WIND_KEY_TOUCHPAD, {KEY_TOUCHPAD_TOGGLE} }, + { KE_IGNORE, WIND_KEY_BLUETOOTH, {KEY_BLUETOOTH} }, + { KE_IGNORE, WIND_KEY_CAMERA, {KEY_CAMERA} }, + { KE_IGNORE, WIND_KEY_WLAN, {KEY_WLAN} }, + + /* These are unknown WMI events found on MSI Wind */ + { KE_IGNORE, 0x00 }, + { KE_IGNORE, 0x62 }, + { KE_IGNORE, 0x63 }, + + /* These are MSI Wind keys that should be handled via WMI */ + { KE_KEY, WIND_KEY_TURBO, {KEY_PROG1} }, + { KE_KEY, WIND_KEY_ECO, {KEY_PROG2} }, + + { KE_END, 0 } +}; + +static ktime_t last_pressed; + +static const struct { + const char *guid; + bool quirk_last_pressed; +} *event_wmi, event_wmis[] = { + { MSIWMI_MSI_EVENT_GUID, true }, + { MSIWMI_WIND_EVENT_GUID, false }, +}; + +static struct backlight_device *backlight; + +static int backlight_map[] = { 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF }; + +static struct input_dev *msi_wmi_input_dev; + +static int msi_wmi_query_block(int instance, int *ret) +{ + acpi_status status; + union acpi_object *obj; + + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + + status = wmi_query_block(MSIWMI_BIOS_GUID, instance, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + + if (!obj || obj->type != ACPI_TYPE_INTEGER) { + if (obj) { + pr_err("query block returned object " + "type: %d - buffer length:%d\n", obj->type, + obj->type == ACPI_TYPE_BUFFER ? + obj->buffer.length : 0); + } + kfree(obj); + return -EINVAL; + } + *ret = obj->integer.value; + kfree(obj); + return 0; +} + +static int msi_wmi_set_block(int instance, int value) +{ + acpi_status status; + + struct acpi_buffer input = { sizeof(int), &value }; + + pr_debug("Going to set block of instance: %d - value: %d\n", + instance, value); + + status = wmi_set_block(MSIWMI_BIOS_GUID, instance, &input); + + return ACPI_SUCCESS(status) ? 0 : 1; +} + +static int bl_get(struct backlight_device *bd) +{ + int level, err, ret; + + /* Instance 1 is "get backlight", cmp with DSDT */ + err = msi_wmi_query_block(1, &ret); + if (err) { + pr_err("Could not query backlight: %d\n", err); + return -EINVAL; + } + pr_debug("Get: Query block returned: %d\n", ret); + for (level = 0; level < ARRAY_SIZE(backlight_map); level++) { + if (backlight_map[level] == ret) { + pr_debug("Current backlight level: 0x%X - index: %d\n", + backlight_map[level], level); + break; + } + } + if (level == ARRAY_SIZE(backlight_map)) { + pr_err("get: Invalid brightness value: 0x%X\n", ret); + return -EINVAL; + } + return level; +} + +static int bl_set_status(struct backlight_device *bd) +{ + int bright = bd->props.brightness; + if (bright >= ARRAY_SIZE(backlight_map) || bright < 0) + return -EINVAL; + + /* Instance 0 is "set backlight" */ + return msi_wmi_set_block(0, backlight_map[bright]); +} + +static const struct backlight_ops msi_backlight_ops = { + .get_brightness = bl_get, + .update_status = bl_set_status, +}; + +static void msi_wmi_notify(union acpi_object *obj, void *context) +{ + struct key_entry *key; + + if (obj && obj->type == ACPI_TYPE_INTEGER) { + int eventcode = obj->integer.value; + pr_debug("Eventcode: 0x%x\n", eventcode); + key = sparse_keymap_entry_from_scancode(msi_wmi_input_dev, + eventcode); + if (!key) { + pr_info("Unknown key pressed - %x\n", eventcode); + return; + } + + if (event_wmi->quirk_last_pressed) { + ktime_t cur = ktime_get_real(); + ktime_t diff = ktime_sub(cur, last_pressed); + /* Ignore event if any event happened in a 50 ms + timeframe -> Key press may result in 10-20 GPEs */ + if (ktime_to_us(diff) < 1000 * 50) { + pr_debug("Suppressed key event 0x%X - " + "Last press was %lld us ago\n", + key->code, ktime_to_us(diff)); + return; + } + last_pressed = cur; + } + + if (key->type == KE_KEY && + /* Brightness is served via acpi video driver */ + (backlight || + (key->code != MSI_KEY_BRIGHTNESSUP && + key->code != MSI_KEY_BRIGHTNESSDOWN))) { + pr_debug("Send key: 0x%X - Input layer keycode: %d\n", + key->code, key->keycode); + sparse_keymap_report_entry(msi_wmi_input_dev, key, 1, + true); + } + } else + pr_info("Unknown event received\n"); +} + +static int __init msi_wmi_backlight_setup(void) +{ + int err; + struct backlight_properties props; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = ARRAY_SIZE(backlight_map) - 1; + backlight = backlight_device_register(DRV_NAME, NULL, NULL, + &msi_backlight_ops, + &props); + if (IS_ERR(backlight)) + return PTR_ERR(backlight); + + err = bl_get(NULL); + if (err < 0) { + backlight_device_unregister(backlight); + return err; + } + + backlight->props.brightness = err; + + return 0; +} + +static int __init msi_wmi_input_setup(void) +{ + int err; + + msi_wmi_input_dev = input_allocate_device(); + if (!msi_wmi_input_dev) + return -ENOMEM; + + msi_wmi_input_dev->name = "MSI WMI hotkeys"; + msi_wmi_input_dev->phys = "wmi/input0"; + msi_wmi_input_dev->id.bustype = BUS_HOST; + + err = sparse_keymap_setup(msi_wmi_input_dev, msi_wmi_keymap, NULL); + if (err) + goto err_free_dev; + + err = input_register_device(msi_wmi_input_dev); + + if (err) + goto err_free_dev; + + last_pressed = 0; + + return 0; + +err_free_dev: + input_free_device(msi_wmi_input_dev); + return err; +} + +static int __init msi_wmi_init(void) +{ + int err; + int i; + + for (i = 0; i < ARRAY_SIZE(event_wmis); i++) { + if (!wmi_has_guid(event_wmis[i].guid)) + continue; + + err = msi_wmi_input_setup(); + if (err) { + pr_err("Unable to setup input device\n"); + return err; + } + + err = wmi_install_notify_handler(event_wmis[i].guid, + msi_wmi_notify, NULL); + if (ACPI_FAILURE(err)) { + pr_err("Unable to setup WMI notify handler\n"); + goto err_free_input; + } + + pr_debug("Event handler installed\n"); + event_wmi = &event_wmis[i]; + break; + } + + if (wmi_has_guid(MSIWMI_BIOS_GUID) && + acpi_video_get_backlight_type() == acpi_backlight_vendor) { + err = msi_wmi_backlight_setup(); + if (err) { + pr_err("Unable to setup backlight device\n"); + goto err_uninstall_handler; + } + pr_debug("Backlight device created\n"); + } + + if (!event_wmi && !backlight) { + pr_err("This machine doesn't have neither MSI-hotkeys nor backlight through WMI\n"); + return -ENODEV; + } + + return 0; + +err_uninstall_handler: + if (event_wmi) + wmi_remove_notify_handler(event_wmi->guid); +err_free_input: + if (event_wmi) + input_unregister_device(msi_wmi_input_dev); + return err; +} + +static void __exit msi_wmi_exit(void) +{ + if (event_wmi) { + wmi_remove_notify_handler(event_wmi->guid); + input_unregister_device(msi_wmi_input_dev); + } + backlight_device_unregister(backlight); +} + +module_init(msi_wmi_init); +module_exit(msi_wmi_exit); diff --git a/drivers/platform/x86/mxm-wmi.c b/drivers/platform/x86/mxm-wmi.c new file mode 100644 index 000000000..dbc5e35ec --- /dev/null +++ b/drivers/platform/x86/mxm-wmi.c @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MXM WMI driver + * + * Copyright(C) 2010 Red Hat. + */ +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Dave Airlie"); +MODULE_DESCRIPTION("MXM WMI Driver"); +MODULE_LICENSE("GPL"); + +#define MXM_WMMX_GUID "F6CB5C3C-9CAE-4EBD-B577-931EA32A2CC0" + +MODULE_ALIAS("wmi:"MXM_WMMX_GUID); + +#define MXM_WMMX_FUNC_MXDS 0x5344584D /* "MXDS" */ +#define MXM_WMMX_FUNC_MXMX 0x53445344 /* "MXMX" */ + +struct mxds_args { + u32 func; + u32 args; + u32 xarg; +}; + +int mxm_wmi_call_mxds(int adapter) +{ + struct mxds_args args = { + .func = MXM_WMMX_FUNC_MXDS, + .args = 0, + .xarg = 1, + }; + struct acpi_buffer input = { (acpi_size)sizeof(args), &args }; + acpi_status status; + + printk("calling mux switch %d\n", adapter); + + status = wmi_evaluate_method(MXM_WMMX_GUID, 0x0, adapter, &input, NULL); + + if (ACPI_FAILURE(status)) + return status; + + printk("mux switched %d\n", status); + return 0; + +} +EXPORT_SYMBOL_GPL(mxm_wmi_call_mxds); + +int mxm_wmi_call_mxmx(int adapter) +{ + struct mxds_args args = { + .func = MXM_WMMX_FUNC_MXMX, + .args = 0, + .xarg = 1, + }; + struct acpi_buffer input = { (acpi_size)sizeof(args), &args }; + acpi_status status; + + printk("calling mux switch %d\n", adapter); + + status = wmi_evaluate_method(MXM_WMMX_GUID, 0x0, adapter, &input, NULL); + + if (ACPI_FAILURE(status)) + return status; + + printk("mux mutex set switched %d\n", status); + return 0; + +} +EXPORT_SYMBOL_GPL(mxm_wmi_call_mxmx); + +bool mxm_wmi_supported(void) +{ + bool guid_valid; + guid_valid = wmi_has_guid(MXM_WMMX_GUID); + return guid_valid; +} +EXPORT_SYMBOL_GPL(mxm_wmi_supported); diff --git a/drivers/platform/x86/nvidia-wmi-ec-backlight.c b/drivers/platform/x86/nvidia-wmi-ec-backlight.c new file mode 100644 index 000000000..b4eebfb96 --- /dev/null +++ b/drivers/platform/x86/nvidia-wmi-ec-backlight.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include + +static bool force; +module_param(force, bool, 0444); +MODULE_PARM_DESC(force, "Force loading (disable acpi_backlight=xxx checks"); + +/** + * wmi_brightness_notify() - helper function for calling WMI-wrapped ACPI method + * @w: Pointer to the struct wmi_device identified by %WMI_BRIGHTNESS_GUID + * @id: The WMI method ID to call (e.g. %WMI_BRIGHTNESS_METHOD_LEVEL or + * %WMI_BRIGHTNESS_METHOD_SOURCE) + * @mode: The operation to perform on the method (e.g. %WMI_BRIGHTNESS_MODE_SET + * or %WMI_BRIGHTNESS_MODE_GET) + * @val: Pointer to a value passed in by the caller when @mode is + * %WMI_BRIGHTNESS_MODE_SET, or a value passed out to caller when @mode + * is %WMI_BRIGHTNESS_MODE_GET or %WMI_BRIGHTNESS_MODE_GET_MAX_LEVEL. + * + * Returns 0 on success, or a negative error number on failure. + */ +static int wmi_brightness_notify(struct wmi_device *w, enum wmi_brightness_method id, enum wmi_brightness_mode mode, u32 *val) +{ + struct wmi_brightness_args args = { + .mode = mode, + .val = 0, + .ret = 0, + }; + struct acpi_buffer buf = { (acpi_size)sizeof(args), &args }; + acpi_status status; + + if (id < WMI_BRIGHTNESS_METHOD_LEVEL || + id >= WMI_BRIGHTNESS_METHOD_MAX || + mode < WMI_BRIGHTNESS_MODE_GET || mode >= WMI_BRIGHTNESS_MODE_MAX) + return -EINVAL; + + if (mode == WMI_BRIGHTNESS_MODE_SET) + args.val = *val; + + status = wmidev_evaluate_method(w, 0, id, &buf, &buf); + if (ACPI_FAILURE(status)) { + dev_err(&w->dev, "EC backlight control failed: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + if (mode != WMI_BRIGHTNESS_MODE_SET) + *val = args.ret; + + return 0; +} + +static int nvidia_wmi_ec_backlight_update_status(struct backlight_device *bd) +{ + struct wmi_device *wdev = bl_get_data(bd); + + return wmi_brightness_notify(wdev, WMI_BRIGHTNESS_METHOD_LEVEL, + WMI_BRIGHTNESS_MODE_SET, + &bd->props.brightness); +} + +static int nvidia_wmi_ec_backlight_get_brightness(struct backlight_device *bd) +{ + struct wmi_device *wdev = bl_get_data(bd); + u32 level; + int ret; + + ret = wmi_brightness_notify(wdev, WMI_BRIGHTNESS_METHOD_LEVEL, + WMI_BRIGHTNESS_MODE_GET, &level); + if (ret < 0) + return ret; + + return level; +} + +static const struct backlight_ops nvidia_wmi_ec_backlight_ops = { + .update_status = nvidia_wmi_ec_backlight_update_status, + .get_brightness = nvidia_wmi_ec_backlight_get_brightness, +}; + +static int nvidia_wmi_ec_backlight_probe(struct wmi_device *wdev, const void *ctx) +{ + struct backlight_properties props = {}; + struct backlight_device *bdev; + int ret; + + /* drivers/acpi/video_detect.c also checks that SOURCE == EC */ + if (!force && acpi_video_get_backlight_type() != acpi_backlight_nvidia_wmi_ec) + return -ENODEV; + + /* + * Identify this backlight device as a firmware device so that it can + * be prioritized over any exposed GPU-driven raw device(s). + */ + props.type = BACKLIGHT_FIRMWARE; + + ret = wmi_brightness_notify(wdev, WMI_BRIGHTNESS_METHOD_LEVEL, + WMI_BRIGHTNESS_MODE_GET_MAX_LEVEL, + &props.max_brightness); + if (ret) + return ret; + + ret = wmi_brightness_notify(wdev, WMI_BRIGHTNESS_METHOD_LEVEL, + WMI_BRIGHTNESS_MODE_GET, &props.brightness); + if (ret) + return ret; + + bdev = devm_backlight_device_register(&wdev->dev, + "nvidia_wmi_ec_backlight", + &wdev->dev, wdev, + &nvidia_wmi_ec_backlight_ops, + &props); + return PTR_ERR_OR_ZERO(bdev); +} + +static const struct wmi_device_id nvidia_wmi_ec_backlight_id_table[] = { + { .guid_string = WMI_BRIGHTNESS_GUID }, + { } +}; +MODULE_DEVICE_TABLE(wmi, nvidia_wmi_ec_backlight_id_table); + +static struct wmi_driver nvidia_wmi_ec_backlight_driver = { + .driver = { + .name = "nvidia-wmi-ec-backlight", + }, + .probe = nvidia_wmi_ec_backlight_probe, + .id_table = nvidia_wmi_ec_backlight_id_table, +}; +module_wmi_driver(nvidia_wmi_ec_backlight_driver); + +MODULE_AUTHOR("Daniel Dadap "); +MODULE_DESCRIPTION("NVIDIA WMI EC Backlight driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/oxpec.c b/drivers/platform/x86/oxpec.c new file mode 100644 index 000000000..99c0dfcf3 --- /dev/null +++ b/drivers/platform/x86/oxpec.c @@ -0,0 +1,1008 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Platform driver for OneXPlayer and AOKZOE devices. + * + * Fan control is provided via pwm interface in the range [0-255]. + * Old AMD boards use [0-100] as range in the EC, the written value is + * scaled to accommodate for that. Newer boards like the mini PRO and + * AOKZOE are not scaled but have the same EC layout. Newer models + * like the 2 and X1 are [0-184] and are scaled to 0-255. OrangePi + * are [1-244] and scaled to 0-255. + * + * Copyright (C) 2022 Joaquín I. Aramendía + * Copyright (C) 2024 Derek J. Clark + * Copyright (C) 2025-2026 Antheas Kapenekakis + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Handle ACPI lock mechanism */ +static u32 oxp_mutex; + +#define ACPI_LOCK_DELAY_MS 500 + +static bool lock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_acquire_global_lock(ACPI_LOCK_DELAY_MS, &oxp_mutex)); +} + +static bool unlock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_release_global_lock(oxp_mutex)); +} + +enum oxp_board { + aok_zoe_a1 = 1, + orange_pi_neo, + oxp_2, + oxp_fly, + oxp_mini_amd, + oxp_mini_amd_a07, + oxp_mini_amd_pro, + oxp_x1, + oxp_g1_i, + oxp_g1_a, +}; + +static enum oxp_board board; +static struct device *oxp_dev; + +/* Fan reading and PWM */ +#define OXP_SENSOR_FAN_REG 0x76 /* Fan reading is 2 registers long */ +#define OXP_2_SENSOR_FAN_REG 0x58 /* Fan reading is 2 registers long */ +#define OXP_SENSOR_PWM_ENABLE_REG 0x4A /* PWM enable is 1 register long */ +#define OXP_SENSOR_PWM_REG 0x4B /* PWM reading is 1 register long */ +#define PWM_MODE_AUTO 0x00 +#define PWM_MODE_MANUAL 0x01 + +/* OrangePi fan reading and PWM */ +#define ORANGEPI_SENSOR_FAN_REG 0x78 /* Fan reading is 2 registers long */ +#define ORANGEPI_SENSOR_PWM_ENABLE_REG 0x40 /* PWM enable is 1 register long */ +#define ORANGEPI_SENSOR_PWM_REG 0x38 /* PWM reading is 1 register long */ + +/* Turbo button takeover function + * Different boards have different values and EC registers + * for the same function + */ +#define OXP_TURBO_SWITCH_REG 0xF1 /* Mini Pro, OneXFly, AOKZOE */ +#define OXP_2_TURBO_SWITCH_REG 0xEB /* OXP2 and X1 */ +#define OXP_MINI_TURBO_SWITCH_REG 0x1E /* Mini AO7 */ + +#define OXP_MINI_TURBO_TAKE_VAL 0x01 /* Mini AO7 */ +#define OXP_TURBO_TAKE_VAL 0x40 /* All other models */ + +/* X1 Turbo LED */ +#define OXP_X1_TURBO_LED_REG 0x57 + +#define OXP_X1_TURBO_LED_OFF 0x01 +#define OXP_X1_TURBO_LED_ON 0x02 + +/* Battery extension settings */ +#define EC_CHARGE_CONTROL_BEHAVIOURS (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \ + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE) | \ + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE_AWAKE)) + +#define OXP_X1_CHARGE_LIMIT_REG 0xA3 /* X1 charge limit (%) */ +#define OXP_X1_CHARGE_INHIBIT_REG 0xA4 /* X1 bypass charging */ + +#define OXP_X1_CHARGE_INHIBIT_MASK_AWAKE 0x01 +/* X1 Mask is 0x0A, F1Pro is 0x02 but the extra bit on the X1 does nothing. */ +#define OXP_X1_CHARGE_INHIBIT_MASK_OFF 0x02 +#define OXP_X1_CHARGE_INHIBIT_MASK_ALWAYS (OXP_X1_CHARGE_INHIBIT_MASK_AWAKE | \ + OXP_X1_CHARGE_INHIBIT_MASK_OFF) + +static const struct dmi_system_id dmi_table[] = { + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), + }, + .driver_data = (void *)aok_zoe_a1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), + }, + .driver_data = (void *)aok_zoe_a1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), + }, + .driver_data = (void *)aok_zoe_a1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), + }, + .driver_data = (void *)orange_pi_neo, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONE XPLAYER"), + }, + .driver_data = (void *)oxp_mini_amd, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), + }, + .driver_data = (void *)oxp_2, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER APEX"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-01"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 OLED"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1L"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), + }, + .driver_data = (void *)oxp_fly, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), + }, + .driver_data = (void *)oxp_g1_a, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER SUPER X"), + }, + .driver_data = (void *)oxp_g1_a, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 i"), + }, + .driver_data = (void *)oxp_g1_i, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER mini A07"), + }, + .driver_data = (void *)oxp_mini_amd_a07, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER Mini Pro"), + }, + .driver_data = (void *)oxp_mini_amd_pro, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 i"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Air"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 mini"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Mini Pro"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), + }, + .driver_data = (void *)oxp_x1, + }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro EVA-02"), + }, + .driver_data = (void *)oxp_x1, + }, + {}, +}; + +/* Helper functions to handle EC read/write */ +static int read_from_ec(u8 reg, int size, long *val) +{ + u8 buffer; + int ret; + int i; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + *val = 0; + for (i = 0; i < size; i++) { + ret = ec_read(reg + i, &buffer); + if (ret) + return ret; + *val <<= i * 8; + *val += buffer; + } + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return 0; +} + +static int write_to_ec(u8 reg, u8 value) +{ + int ret; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + ret = ec_write(reg, value); + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return ret; +} + +/* Callbacks for turbo toggle attribute */ +static umode_t tt_toggle_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + switch (board) { + case aok_zoe_a1: + case oxp_2: + case oxp_fly: + case oxp_mini_amd_a07: + case oxp_mini_amd_pro: + case oxp_x1: + case oxp_g1_i: + case oxp_g1_a: + return attr->mode; + default: + break; + } + return 0; +} + +static ssize_t tt_toggle_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + u8 reg, mask, val; + long raw_val; + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret) + return ret; + + switch (board) { + case oxp_mini_amd_a07: + reg = OXP_MINI_TURBO_SWITCH_REG; + mask = OXP_MINI_TURBO_TAKE_VAL; + break; + case aok_zoe_a1: + case oxp_fly: + case oxp_mini_amd_pro: + case oxp_g1_a: + reg = OXP_TURBO_SWITCH_REG; + mask = OXP_TURBO_TAKE_VAL; + break; + case oxp_2: + case oxp_x1: + case oxp_g1_i: + reg = OXP_2_TURBO_SWITCH_REG; + mask = OXP_TURBO_TAKE_VAL; + break; + default: + return -EINVAL; + } + + ret = read_from_ec(reg, 1, &raw_val); + if (ret) + return ret; + + val = raw_val; + if (enable) + val |= mask; + else + val &= ~mask; + + ret = write_to_ec(reg, val); + if (ret) + return ret; + + return count; +} + +static ssize_t tt_toggle_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u8 reg, mask; + int retval; + long val; + + switch (board) { + case oxp_mini_amd_a07: + reg = OXP_MINI_TURBO_SWITCH_REG; + mask = OXP_MINI_TURBO_TAKE_VAL; + break; + case aok_zoe_a1: + case oxp_fly: + case oxp_mini_amd_pro: + case oxp_g1_a: + reg = OXP_TURBO_SWITCH_REG; + mask = OXP_TURBO_TAKE_VAL; + break; + case oxp_2: + case oxp_x1: + case oxp_g1_i: + reg = OXP_2_TURBO_SWITCH_REG; + mask = OXP_TURBO_TAKE_VAL; + break; + default: + return -EINVAL; + } + + retval = read_from_ec(reg, 1, &val); + if (retval) + return retval; + + return sysfs_emit(buf, "%d\n", (val & mask) == mask); +} + +static DEVICE_ATTR_RW(tt_toggle); + +/* Callbacks for turbo LED attribute */ +static umode_t tt_led_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + switch (board) { + case oxp_x1: + return attr->mode; + default: + break; + } + return 0; +} + +static ssize_t tt_led_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + u8 reg, val; + bool value; + int ret; + + ret = kstrtobool(buf, &value); + if (ret) + return ret; + + switch (board) { + case oxp_x1: + reg = OXP_X1_TURBO_LED_REG; + val = value ? OXP_X1_TURBO_LED_ON : OXP_X1_TURBO_LED_OFF; + break; + default: + return -EINVAL; + } + + ret = write_to_ec(reg, val); + if (ret) + return ret; + + return count; +} + +static ssize_t tt_led_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + long enval; + long val; + int ret; + u8 reg; + + switch (board) { + case oxp_x1: + reg = OXP_X1_TURBO_LED_REG; + enval = OXP_X1_TURBO_LED_ON; + break; + default: + return -EINVAL; + } + + ret = read_from_ec(reg, 1, &val); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", val == enval); +} + +static DEVICE_ATTR_RW(tt_led); + +/* Callbacks for charge behaviour attributes */ +static bool oxp_psy_ext_supported(void) +{ + switch (board) { + case oxp_x1: + case oxp_g1_i: + case oxp_g1_a: + case oxp_fly: + return true; + default: + break; + } + return false; +} + +static int oxp_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) +{ + long raw_val; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + ret = read_from_ec(OXP_X1_CHARGE_LIMIT_REG, 1, &raw_val); + if (ret) + return ret; + if (raw_val < 0 || raw_val > 100) + return -EINVAL; + val->intval = raw_val; + return 0; + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + ret = read_from_ec(OXP_X1_CHARGE_INHIBIT_REG, 1, &raw_val); + if (ret) + return ret; + if ((raw_val & OXP_X1_CHARGE_INHIBIT_MASK_ALWAYS) == + OXP_X1_CHARGE_INHIBIT_MASK_ALWAYS) + val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + else if ((raw_val & OXP_X1_CHARGE_INHIBIT_MASK_AWAKE) == + OXP_X1_CHARGE_INHIBIT_MASK_AWAKE) + val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE_AWAKE; + else + val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + return 0; + default: + return -EINVAL; + } +} + +static int oxp_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) +{ + long raw_val; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval < 0 || val->intval > 100) + return -EINVAL; + return write_to_ec(OXP_X1_CHARGE_LIMIT_REG, val->intval); + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + switch (val->intval) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + raw_val = 0; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE_AWAKE: + raw_val = OXP_X1_CHARGE_INHIBIT_MASK_AWAKE; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: + raw_val = OXP_X1_CHARGE_INHIBIT_MASK_ALWAYS; + break; + default: + return -EINVAL; + } + + return write_to_ec(OXP_X1_CHARGE_INHIBIT_REG, raw_val); + default: + return -EINVAL; + } +} + +static int oxp_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_BEHAVIOUR, + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const struct power_supply_ext oxp_psy_ext = { + .name = "oxp-charge-control", + .properties = oxp_psy_ext_props, + .num_properties = ARRAY_SIZE(oxp_psy_ext_props), + .charge_behaviours = EC_CHARGE_CONTROL_BEHAVIOURS, + .get_property = oxp_psy_ext_get_prop, + .set_property = oxp_psy_ext_set_prop, + .property_is_writeable = oxp_psy_prop_is_writeable, +}; + +static int oxp_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + return power_supply_register_extension(battery, &oxp_psy_ext, oxp_dev, NULL); +} + +static int oxp_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &oxp_psy_ext); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = oxp_add_battery, + .remove_battery = oxp_remove_battery, + .name = "OneXPlayer Battery", +}; + +/* PWM enable/disable functions */ +static int oxp_pwm_enable(void) +{ + switch (board) { + case orange_pi_neo: + return write_to_ec(ORANGEPI_SENSOR_PWM_ENABLE_REG, PWM_MODE_MANUAL); + case aok_zoe_a1: + case oxp_2: + case oxp_fly: + case oxp_mini_amd: + case oxp_mini_amd_a07: + case oxp_mini_amd_pro: + case oxp_x1: + case oxp_g1_i: + case oxp_g1_a: + return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, PWM_MODE_MANUAL); + default: + return -EINVAL; + } +} + +static int oxp_pwm_disable(void) +{ + switch (board) { + case orange_pi_neo: + return write_to_ec(ORANGEPI_SENSOR_PWM_ENABLE_REG, PWM_MODE_AUTO); + case aok_zoe_a1: + case oxp_2: + case oxp_fly: + case oxp_mini_amd: + case oxp_mini_amd_a07: + case oxp_mini_amd_pro: + case oxp_x1: + case oxp_g1_i: + case oxp_g1_a: + return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, PWM_MODE_AUTO); + default: + return -EINVAL; + } +} + +static int oxp_pwm_read(long *val) +{ + switch (board) { + case orange_pi_neo: + return read_from_ec(ORANGEPI_SENSOR_PWM_ENABLE_REG, 1, val); + case aok_zoe_a1: + case oxp_2: + case oxp_fly: + case oxp_mini_amd: + case oxp_mini_amd_a07: + case oxp_mini_amd_pro: + case oxp_x1: + case oxp_g1_i: + case oxp_g1_a: + return read_from_ec(OXP_SENSOR_PWM_ENABLE_REG, 1, val); + default: + return -EOPNOTSUPP; + } +} + +/* Callbacks for hwmon interface */ +static umode_t oxp_ec_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, u32 attr, int channel) +{ + switch (type) { + case hwmon_fan: + return 0444; + case hwmon_pwm: + return 0644; + default: + return 0; + } +} + +/* Fan speed read function */ +static int oxp_pwm_fan_speed(long *val) +{ + switch (board) { + case orange_pi_neo: + return read_from_ec(ORANGEPI_SENSOR_FAN_REG, 2, val); + case oxp_2: + case oxp_x1: + case oxp_g1_i: + return read_from_ec(OXP_2_SENSOR_FAN_REG, 2, val); + case aok_zoe_a1: + case oxp_fly: + case oxp_mini_amd: + case oxp_mini_amd_a07: + case oxp_mini_amd_pro: + case oxp_g1_a: + return read_from_ec(OXP_SENSOR_FAN_REG, 2, val); + default: + return -EOPNOTSUPP; + } +} + +/* PWM input read/write functions */ +static int oxp_pwm_input_write(long val) +{ + if (val < 0 || val > 255) + return -EINVAL; + + switch (board) { + case orange_pi_neo: + /* scale to range [1-244] */ + val = ((val - 1) * 243 / 254) + 1; + return write_to_ec(ORANGEPI_SENSOR_PWM_REG, val); + case oxp_2: + case oxp_x1: + case oxp_g1_i: + /* scale to range [0-184] */ + val = (val * 184) / 255; + return write_to_ec(OXP_SENSOR_PWM_REG, val); + case oxp_mini_amd: + case oxp_mini_amd_a07: + /* scale to range [0-100] */ + val = (val * 100) / 255; + return write_to_ec(OXP_SENSOR_PWM_REG, val); + case aok_zoe_a1: + case oxp_fly: + case oxp_mini_amd_pro: + case oxp_g1_a: + return write_to_ec(OXP_SENSOR_PWM_REG, val); + default: + return -EOPNOTSUPP; + } +} + +static int oxp_pwm_input_read(long *val) +{ + int ret; + + switch (board) { + case orange_pi_neo: + ret = read_from_ec(ORANGEPI_SENSOR_PWM_REG, 1, val); + if (ret) + return ret; + /* scale from range [1-244] */ + *val = ((*val - 1) * 254 / 243) + 1; + break; + case oxp_2: + case oxp_x1: + case oxp_g1_i: + ret = read_from_ec(OXP_SENSOR_PWM_REG, 1, val); + if (ret) + return ret; + /* scale from range [0-184] */ + *val = (*val * 255) / 184; + break; + case oxp_mini_amd: + case oxp_mini_amd_a07: + ret = read_from_ec(OXP_SENSOR_PWM_REG, 1, val); + if (ret) + return ret; + /* scale from range [0-100] */ + *val = (*val * 255) / 100; + break; + case aok_zoe_a1: + case oxp_fly: + case oxp_mini_amd_pro: + case oxp_g1_a: + default: + ret = read_from_ec(OXP_SENSOR_PWM_REG, 1, val); + if (ret) + return ret; + break; + } + return 0; +} + +static int oxp_platform_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + int ret; + + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + return oxp_pwm_fan_speed(val); + default: + break; + } + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_input: + return oxp_pwm_input_read(val); + case hwmon_pwm_enable: + ret = oxp_pwm_read(val); + if (ret) + return ret; + + /* Check for auto and return 2 */ + if (!*val) { + *val = 2; + return 0; + } + + /* Return 0 if at full fan speed, 1 otherwise */ + ret = oxp_pwm_fan_speed(val); + if (ret) + return ret; + + if (*val == 255) + *val = 0; + else + *val = 1; + + return 0; + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +static int oxp_platform_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + int ret; + + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + if (val == 1) + return oxp_pwm_enable(); + else if (val == 2) + return oxp_pwm_disable(); + else if (val != 0) + return -EINVAL; + + /* Enable PWM and set to max speed */ + ret = oxp_pwm_enable(); + if (ret) + return ret; + return oxp_pwm_input_write(255); + case hwmon_pwm_input: + return oxp_pwm_input_write(val); + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +/* Known sensors in the OXP EC controllers */ +static const struct hwmon_channel_info * const oxp_platform_sensors[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL, +}; + +static struct attribute *oxp_tt_toggle_attrs[] = { + &dev_attr_tt_toggle.attr, + NULL +}; + +static const struct attribute_group oxp_tt_toggle_attribute_group = { + .is_visible = tt_toggle_is_visible, + .attrs = oxp_tt_toggle_attrs, +}; + +static struct attribute *oxp_tt_led_attrs[] = { + &dev_attr_tt_led.attr, + NULL +}; + +static const struct attribute_group oxp_tt_led_attribute_group = { + .is_visible = tt_led_is_visible, + .attrs = oxp_tt_led_attrs, +}; + +static const struct attribute_group *oxp_ec_groups[] = { + &oxp_tt_toggle_attribute_group, + &oxp_tt_led_attribute_group, + NULL +}; + +static const struct hwmon_ops oxp_ec_hwmon_ops = { + .is_visible = oxp_ec_hwmon_is_visible, + .read = oxp_platform_read, + .write = oxp_platform_write, +}; + +static const struct hwmon_chip_info oxp_ec_chip_info = { + .ops = &oxp_ec_hwmon_ops, + .info = oxp_platform_sensors, +}; + +/* Initialization logic */ +static int oxp_platform_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device *hwdev; + int ret; + + oxp_dev = dev; + hwdev = devm_hwmon_device_register_with_info(dev, "oxp_ec", NULL, + &oxp_ec_chip_info, NULL); + + if (IS_ERR(hwdev)) + return PTR_ERR(hwdev); + + if (oxp_psy_ext_supported()) { + ret = devm_battery_hook_register(dev, &battery_hook); + if (ret) + return ret; + } + + return 0; +} + +static struct platform_driver oxp_platform_driver = { + .driver = { + .name = "oxp-platform", + .dev_groups = oxp_ec_groups, + }, + .probe = oxp_platform_probe, +}; + +static struct platform_device *oxp_platform_device; + +static int __init oxp_platform_init(void) +{ + const struct dmi_system_id *dmi_entry; + + dmi_entry = dmi_first_match(dmi_table); + if (!dmi_entry) + return -ENODEV; + + board = (enum oxp_board)(unsigned long)dmi_entry->driver_data; + + /* + * Have to check for AMD processor here because DMI strings are the same + * between Intel and AMD boards on older OneXPlayer devices, the only way + * to tell them apart is the CPU. Old Intel boards have an unsupported EC. + */ + if (board == oxp_mini_amd && boot_cpu_data.x86_vendor != X86_VENDOR_AMD) + return -ENODEV; + + oxp_platform_device = + platform_create_bundle(&oxp_platform_driver, + oxp_platform_probe, NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(oxp_platform_device); +} + +static void __exit oxp_platform_exit(void) +{ + platform_device_unregister(oxp_platform_device); + platform_driver_unregister(&oxp_platform_driver); +} + +MODULE_DEVICE_TABLE(dmi, dmi_table); + +module_init(oxp_platform_init); +module_exit(oxp_platform_exit); + +MODULE_AUTHOR("Joaquín Ignacio Aramendía "); +MODULE_DESCRIPTION("Platform driver that handles EC sensors of OneXPlayer devices"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/p2sb.c b/drivers/platform/x86/p2sb.c new file mode 100644 index 000000000..cbbb0f809 --- /dev/null +++ b/drivers/platform/x86/p2sb.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Primary to Sideband (P2SB) bridge access support + * + * Copyright (c) 2017, 2021-2022 Intel Corporation. + * + * Authors: Andy Shevchenko + * Jonathan Yong + */ + +#include +#include +#include +#include + +#include +#include + +#define P2SBC 0xe0 +#define P2SBC_HIDE BIT(8) + +#define P2SB_DEVFN_DEFAULT PCI_DEVFN(31, 1) +#define P2SB_DEVFN_GOLDMONT PCI_DEVFN(13, 0) +#define SPI_DEVFN_GOLDMONT PCI_DEVFN(13, 2) + +static const struct x86_cpu_id p2sb_cpu_ids[] = { + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT, P2SB_DEVFN_GOLDMONT), + X86_MATCH_VFM(INTEL_ATOM_GOLDMONT_PLUS, P2SB_DEVFN_GOLDMONT), + {} +}; + +/* + * Cache BAR0 of P2SB device functions 0 to 7. + * TODO: The constant 8 is the number of functions that PCI specification + * defines. Same definitions exist tree-wide. Unify this definition and + * the other definitions then move to include/uapi/linux/pci.h. + */ +#define NR_P2SB_RES_CACHE 8 + +struct p2sb_res_cache { + u32 bus_dev_id; + struct resource res; +}; + +static struct p2sb_res_cache p2sb_resources[NR_P2SB_RES_CACHE]; +static bool p2sb_hidden_by_bios; + +static void p2sb_get_devfn(unsigned int *devfn) +{ + unsigned int fn = P2SB_DEVFN_DEFAULT; + const struct x86_cpu_id *id; + + id = x86_match_cpu(p2sb_cpu_ids); + if (id) + fn = (unsigned int)id->driver_data; + + *devfn = fn; +} + +static bool p2sb_valid_resource(const struct resource *res) +{ + return res->flags & ~IORESOURCE_UNSET; +} + +/* Copy resource from the first BAR of the device in question */ +static void p2sb_read_bar0(struct pci_dev *pdev, struct resource *mem) +{ + struct resource *bar0 = pci_resource_n(pdev, 0); + + /* Make sure we have no dangling pointers in the output */ + memset(mem, 0, sizeof(*mem)); + + /* + * We copy only selected fields from the original resource. + * Because a PCI device will be removed soon, we may not use + * any allocated data, hence we may not copy any pointers. + */ + mem->start = bar0->start; + mem->end = bar0->end; + mem->flags = bar0->flags; + mem->desc = bar0->desc; +} + +static void p2sb_scan_and_cache_devfn(struct pci_bus *bus, unsigned int devfn) +{ + struct p2sb_res_cache *cache = &p2sb_resources[PCI_FUNC(devfn)]; + struct pci_dev *pdev; + + pdev = pci_scan_single_device(bus, devfn); + if (!pdev) + return; + + p2sb_read_bar0(pdev, &cache->res); + cache->bus_dev_id = bus->dev.id; + + pci_stop_and_remove_bus_device(pdev); +} + +static int p2sb_scan_and_cache(struct pci_bus *bus, unsigned int devfn) +{ + /* + * The BIOS prevents the P2SB device from being enumerated by the PCI + * subsystem, so we need to unhide and hide it back to lookup the BAR. + */ + pci_bus_write_config_dword(bus, devfn, P2SBC, 0); + + /* Scan the P2SB device and cache its BAR0 */ + p2sb_scan_and_cache_devfn(bus, devfn); + + /* On Goldmont p2sb_bar() also gets called for the SPI controller */ + if (devfn == P2SB_DEVFN_GOLDMONT) + p2sb_scan_and_cache_devfn(bus, SPI_DEVFN_GOLDMONT); + + pci_bus_write_config_dword(bus, devfn, P2SBC, P2SBC_HIDE); + + if (!p2sb_valid_resource(&p2sb_resources[PCI_FUNC(devfn)].res)) + return -ENOENT; + + return 0; +} + +static struct pci_bus *p2sb_get_bus(struct pci_bus *bus) +{ + static struct pci_bus *p2sb_bus; + + bus = bus ?: p2sb_bus; + if (bus) + return bus; + + /* Assume P2SB is on the bus 0 in domain 0 */ + p2sb_bus = pci_find_bus(0, 0); + return p2sb_bus; +} + +static int p2sb_cache_resources(void) +{ + unsigned int devfn_p2sb; + u32 value = P2SBC_HIDE; + struct pci_bus *bus; + u16 class; + int ret = 0; + + /* Get devfn for P2SB device itself */ + p2sb_get_devfn(&devfn_p2sb); + + bus = p2sb_get_bus(NULL); + if (!bus) + return -ENODEV; + + /* + * When a device with same devfn exists and its device class is not + * PCI_CLASS_MEMORY_OTHER for P2SB, do not touch it. + */ + pci_bus_read_config_word(bus, devfn_p2sb, PCI_CLASS_DEVICE, &class); + if (!PCI_POSSIBLE_ERROR(class) && class != PCI_CLASS_MEMORY_OTHER) + return -ENODEV; + + /* + * Prevent concurrent PCI bus scan from seeing the P2SB device and + * removing via sysfs while it is temporarily exposed. + */ + pci_lock_rescan_remove(); + + pci_bus_read_config_dword(bus, devfn_p2sb, P2SBC, &value); + p2sb_hidden_by_bios = value & P2SBC_HIDE; + + /* + * If the BIOS does not hide the P2SB device then its resources + * are accesilble. Cache them only if the P2SB device is hidden. + */ + if (p2sb_hidden_by_bios) + ret = p2sb_scan_and_cache(bus, devfn_p2sb); + + pci_unlock_rescan_remove(); + + return ret; +} + +static int p2sb_read_from_cache(struct pci_bus *bus, unsigned int devfn, + struct resource *mem) +{ + struct p2sb_res_cache *cache = &p2sb_resources[PCI_FUNC(devfn)]; + + if (cache->bus_dev_id != bus->dev.id) + return -ENODEV; + + if (!p2sb_valid_resource(&cache->res)) + return -ENOENT; + + memcpy(mem, &cache->res, sizeof(*mem)); + + return 0; +} + +static int p2sb_read_from_dev(struct pci_bus *bus, unsigned int devfn, + struct resource *mem) +{ + struct pci_dev *pdev; + int ret = 0; + + pdev = pci_get_slot(bus, devfn); + if (!pdev) + return -ENODEV; + + if (p2sb_valid_resource(pci_resource_n(pdev, 0))) + p2sb_read_bar0(pdev, mem); + else + ret = -ENOENT; + + pci_dev_put(pdev); + + return ret; +} + +/** + * p2sb_bar - Get Primary to Sideband (P2SB) bridge device BAR + * @bus: PCI bus to communicate with + * @devfn: PCI slot and function to communicate with + * @mem: memory resource to be filled in + * + * If @bus is NULL, the bus 0 in domain 0 will be used. + * If @devfn is 0, it will be replaced by devfn of the P2SB device. + * + * Caller must provide a valid pointer to @mem. + * + * Return: + * 0 on success or appropriate errno value on error. + */ +int p2sb_bar(struct pci_bus *bus, unsigned int devfn, struct resource *mem) +{ + bus = p2sb_get_bus(bus); + if (!bus) + return -ENODEV; + + if (!devfn) + p2sb_get_devfn(&devfn); + + if (p2sb_hidden_by_bios) + return p2sb_read_from_cache(bus, devfn, mem); + + return p2sb_read_from_dev(bus, devfn, mem); +} +EXPORT_SYMBOL_GPL(p2sb_bar); + +static int __init p2sb_fs_init(void) +{ + return p2sb_cache_resources(); +} + +/* + * pci_rescan_remove_lock() can not be locked in sysfs PCI bus rescan path + * because of deadlock. To avoid the deadlock, access P2SB devices with the lock + * at an early step in kernel initialization and cache required resources. + * + * We want to run as early as possible. If the P2SB was assigned a bad BAR, + * we'll need to wait on pcibios_assign_resources() to fix it. So, our list of + * initcall dependencies looks something like this: + * + * ... + * subsys_initcall (pci_subsys_init) + * fs_initcall (pcibios_assign_resources) + */ +fs_initcall_sync(p2sb_fs_init); diff --git a/drivers/platform/x86/panasonic-laptop.c b/drivers/platform/x86/panasonic-laptop.c new file mode 100644 index 000000000..719add753 --- /dev/null +++ b/drivers/platform/x86/panasonic-laptop.c @@ -0,0 +1,1141 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Panasonic HotKey and LCD brightness control driver + * (C) 2004 Hiroshi Miura + * (C) 2004 NTT DATA Intellilink Co. http://www.intellilink.co.jp/ + * (C) YOKOTA Hiroshi + * (C) 2004 David Bronaugh + * (C) 2006-2008 Harald Welte + * + * derived from toshiba_acpi.c, Copyright (C) 2002-2004 John Belmonte + * + *--------------------------------------------------------------------------- + * + * ChangeLog: + * Aug.18, 2020 Kenneth Chan + * -v0.98 add platform devices for firmware brightness registers + * add support for battery charging threshold (eco mode) + * resolve hotkey double trigger + * add write support to mute + * fix sticky_key init bug + * fix naming of platform files for consistency with other + * modules + * split MODULE_AUTHOR() by one author per macro call + * replace ACPI prints with pr_*() macros + * -v0.97 add support for cdpower hardware switch + * -v0.96 merge Lucina's enhancement + * Jan.13, 2009 Martin Lucina + * - add support for optical driver power in + * Y and W series + * + * Sep.23, 2008 Harald Welte + * -v0.95 rename driver from drivers/acpi/pcc_acpi.c to + * drivers/misc/panasonic-laptop.c + * + * Jul.04, 2008 Harald Welte + * -v0.94 replace /proc interface with device attributes + * support {set,get}keycode on th input device + * + * Jun.27, 2008 Harald Welte + * -v0.92 merge with 2.6.26-rc6 input API changes + * remove broken <= 2.6.15 kernel support + * resolve all compiler warnings + * various coding style fixes (checkpatch.pl) + * add support for backlight api + * major code restructuring + * + * Dac.28, 2007 Harald Welte + * -v0.91 merge with 2.6.24-rc6 ACPI changes + * + * Nov.04, 2006 Hiroshi Miura + * -v0.9 remove warning about section reference. + * remove acpi_os_free + * add /proc/acpi/pcc/brightness interface for HAL access + * merge dbronaugh's enhancement + * Aug.17, 2004 David Bronaugh (dbronaugh) + * - Added screen brightness setting interface + * Thanks to FreeBSD crew (acpi_panasonic.c) + * for the ideas I needed to accomplish it + * + * May.29, 2006 Hiroshi Miura + * -v0.8.4 follow to change keyinput structure + * thanks Fabian Yamaguchi , + * Jacob Bower and + * Hiroshi Yokota for providing solutions. + * + * Oct.02, 2004 Hiroshi Miura + * -v0.8.2 merge code of YOKOTA Hiroshi + * . + * Add sticky key mode interface. + * Refactoring acpi_pcc_generate_keyinput(). + * + * Sep.15, 2004 Hiroshi Miura + * -v0.8 Generate key input event on input subsystem. + * This is based on yet another driver written by + * Ryuta Nakanishi. + * + * Sep.10, 2004 Hiroshi Miura + * -v0.7 Change proc interface functions using seq_file + * facility as same as other ACPI drivers. + * + * Aug.28, 2004 Hiroshi Miura + * -v0.6.4 Fix a silly error with status checking + * + * Aug.25, 2004 Hiroshi Miura + * -v0.6.3 replace read_acpi_int by standard function + * acpi_evaluate_integer + * some clean up and make smart copyright notice. + * fix return value of pcc_acpi_get_key() + * fix checking return value of acpi_bus_register_driver() + * + * Aug.22, 2004 David Bronaugh + * -v0.6.2 Add check on ACPI data (num_sifr) + * Coding style cleanups, better error messages/handling + * Fixed an off-by-one error in memory allocation + * + * Aug.21, 2004 David Bronaugh + * -v0.6.1 Fix a silly error with status checking + * + * Aug.20, 2004 David Bronaugh + * - v0.6 Correct brightness controls to reflect reality + * based on information gleaned by Hiroshi Miura + * and discussions with Hiroshi Miura + * + * Aug.10, 2004 Hiroshi Miura + * - v0.5 support LCD brightness control + * based on the disclosed information by MEI. + * + * Jul.25, 2004 Hiroshi Miura + * - v0.4 first post version + * add function to retrive SIFR + * + * Jul.24, 2004 Hiroshi Miura + * - v0.3 get proper status of hotkey + * + * Jul.22, 2004 Hiroshi Miura + * - v0.2 add HotKey handler + * + * Jul.17, 2004 Hiroshi Miura + * - v0.1 start from toshiba_acpi driver written by John Belmonte + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Hiroshi Miura "); +MODULE_AUTHOR("David Bronaugh "); +MODULE_AUTHOR("Harald Welte "); +MODULE_AUTHOR("Martin Lucina "); +MODULE_AUTHOR("Kenneth Chan "); +MODULE_DESCRIPTION("ACPI HotKey driver for Panasonic Let's Note laptops"); +MODULE_LICENSE("GPL"); + +#define LOGPREFIX "pcc_acpi: " + +/* Define ACPI PATHs */ +/* Lets note hotkeys */ +#define METHOD_HKEY_QUERY "HINF" +#define METHOD_HKEY_SQTY "SQTY" +#define METHOD_HKEY_SINF "SINF" +#define METHOD_HKEY_SSET "SSET" +#define METHOD_ECWR "\\_SB.ECWR" +#define HKEY_NOTIFY 0x80 +#define ECO_MODE_OFF 0x00 +#define ECO_MODE_ON 0x80 + +#define ACPI_PCC_DRIVER_NAME "Panasonic Laptop Support" +#define ACPI_PCC_DEVICE_NAME "Hotkey" +#define ACPI_PCC_CLASS "pcc" + +#define ACPI_PCC_INPUT_PHYS "panasonic/hkey0" + +/* LCD_TYPEs: 0 = Normal, 1 = Semi-transparent + ECO_MODEs: 0x03 = off, 0x83 = on +*/ +enum SINF_BITS { SINF_NUM_BATTERIES = 0, + SINF_LCD_TYPE, + SINF_AC_MAX_BRIGHT, + SINF_AC_MIN_BRIGHT, + SINF_AC_CUR_BRIGHT, + SINF_DC_MAX_BRIGHT, + SINF_DC_MIN_BRIGHT, + SINF_DC_CUR_BRIGHT, + SINF_MUTE, + SINF_RESERVED, + SINF_ECO_MODE = 0x0A, + SINF_CUR_BRIGHT = 0x0D, + SINF_STICKY_KEY = 0x80, + }; +/* R1 handles SINF_AC_CUR_BRIGHT as SINF_CUR_BRIGHT, doesn't know AC state */ + +static int acpi_pcc_hotkey_probe(struct platform_device *pdev); +static void acpi_pcc_hotkey_remove(struct platform_device *pdev); +static void acpi_pcc_hotkey_notify(acpi_handle handle, u32 event, void *data); + +static const struct acpi_device_id pcc_device_ids[] = { + { "MAT0012", 0}, + { "MAT0013", 0}, + { "MAT0018", 0}, + { "MAT0019", 0}, + { "", 0}, +}; +MODULE_DEVICE_TABLE(acpi, pcc_device_ids); + +#ifdef CONFIG_PM_SLEEP +static int acpi_pcc_hotkey_resume(struct device *dev); +#endif +static SIMPLE_DEV_PM_OPS(acpi_pcc_hotkey_pm, NULL, acpi_pcc_hotkey_resume); + +static struct platform_driver acpi_pcc_driver = { + .probe = acpi_pcc_hotkey_probe, + .remove = acpi_pcc_hotkey_remove, + .driver = { + .name = ACPI_PCC_DRIVER_NAME, + .acpi_match_table = pcc_device_ids, + .pm = &acpi_pcc_hotkey_pm, + }, +}; + +static const struct key_entry panasonic_keymap[] = { + { KE_KEY, 0, { KEY_RESERVED } }, + { KE_KEY, 1, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 2, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 3, { KEY_DISPLAYTOGGLE } }, + { KE_KEY, 4, { KEY_MUTE } }, + { KE_KEY, 5, { KEY_VOLUMEDOWN } }, + { KE_KEY, 6, { KEY_VOLUMEUP } }, + { KE_KEY, 7, { KEY_SLEEP } }, + { KE_KEY, 8, { KEY_PROG1 } }, /* Change CPU boost */ + { KE_KEY, 9, { KEY_BATTERY } }, + { KE_KEY, 10, { KEY_SUSPEND } }, + { KE_KEY, 21, { KEY_MACRO1 } }, + { KE_KEY, 22, { KEY_MACRO2 } }, + { KE_KEY, 24, { KEY_MACRO3 } }, + { KE_KEY, 25, { KEY_MACRO4 } }, + { KE_KEY, 34, { KEY_MACRO5 } }, + { KE_KEY, 35, { KEY_MACRO6 } }, + { KE_KEY, 36, { KEY_MACRO7 } }, + { KE_KEY, 37, { KEY_MACRO8 } }, + { KE_KEY, 41, { KEY_MACRO9 } }, + { KE_KEY, 42, { KEY_MACRO10 } }, + { KE_KEY, 43, { KEY_MACRO11 } }, + { KE_END, 0 } +}; + +struct pcc_acpi { + acpi_handle handle; + unsigned long num_sifr; + int sticky_key; + int eco_mode; + int mute; + int ac_brightness; + int dc_brightness; + int current_brightness; + struct acpi_device *device; + struct input_dev *input_dev; + struct backlight_device *backlight; + struct platform_device *platform; + u32 sinf[] __counted_by(num_sifr); +}; + +/* + * On some Panasonic models the volume up / down / mute keys send duplicate + * keypress events over the PS/2 kbd interface, filter these out. + */ +static bool panasonic_i8042_filter(unsigned char data, unsigned char str, + struct serio *port, void *context) +{ + static bool extended; + + if (str & I8042_STR_AUXDATA) + return false; + + if (data == 0xe0) { + extended = true; + return true; + } else if (extended) { + extended = false; + + switch (data & 0x7f) { + case 0x20: /* e0 20 / e0 a0, Volume Mute press / release */ + case 0x2e: /* e0 2e / e0 ae, Volume Down press / release */ + case 0x30: /* e0 30 / e0 b0, Volume Up press / release */ + return true; + default: + /* + * Report the previously filtered e0 before continuing + * with the next non-filtered byte. + */ + serio_interrupt(port, 0xe0, 0); + return false; + } + } + + return false; +} + +/* method access functions */ +static int acpi_pcc_write_sset(struct pcc_acpi *pcc, int func, int val) +{ + union acpi_object in_objs[] = { + { .integer.type = ACPI_TYPE_INTEGER, + .integer.value = func, }, + { .integer.type = ACPI_TYPE_INTEGER, + .integer.value = val, }, + }; + struct acpi_object_list params = { + .count = ARRAY_SIZE(in_objs), + .pointer = in_objs, + }; + acpi_status status = AE_OK; + + status = acpi_evaluate_object(pcc->handle, METHOD_HKEY_SSET, + ¶ms, NULL); + + return (status == AE_OK) ? 0 : -EIO; +} + +static inline int acpi_pcc_get_sqty(struct acpi_device *device) +{ + unsigned long long s; + acpi_status status; + + status = acpi_evaluate_integer(device->handle, METHOD_HKEY_SQTY, + NULL, &s); + if (ACPI_SUCCESS(status)) + return s; + else { + pr_err("evaluation error HKEY.SQTY\n"); + return -EINVAL; + } +} + +static int acpi_pcc_retrieve_biosdata(struct pcc_acpi *pcc) +{ + acpi_status status; + struct acpi_buffer buffer = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *hkey = NULL; + int i; + + status = acpi_evaluate_object(pcc->handle, METHOD_HKEY_SINF, NULL, + &buffer); + if (ACPI_FAILURE(status)) { + pr_err("evaluation error HKEY.SINF\n"); + return 0; + } + + hkey = buffer.pointer; + if (!hkey || (hkey->type != ACPI_TYPE_PACKAGE)) { + pr_err("Invalid HKEY.SINF\n"); + status = AE_ERROR; + goto end; + } + + if (pcc->num_sifr < hkey->package.count) { + pr_err("SQTY reports bad SINF length SQTY: %lu SINF-pkg-count: %u\n", + pcc->num_sifr, hkey->package.count); + status = AE_ERROR; + goto end; + } + + for (i = 0; i < hkey->package.count; i++) { + union acpi_object *element = &(hkey->package.elements[i]); + if (likely(element->type == ACPI_TYPE_INTEGER)) { + pcc->sinf[i] = element->integer.value; + } else + pr_err("Invalid HKEY.SINF data\n"); + } + pcc->sinf[hkey->package.count] = -1; + +end: + kfree(buffer.pointer); + return status == AE_OK; +} + +/* backlight API interface functions */ + +/* This driver currently treats AC and DC brightness identical, + * since we don't need to invent an interface to the core ACPI + * logic to receive events in case a power supply is plugged in + * or removed */ + +static int bl_get(struct backlight_device *bd) +{ + struct pcc_acpi *pcc = bl_get_data(bd); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return pcc->sinf[SINF_AC_CUR_BRIGHT]; +} + +static int bl_set_status(struct backlight_device *bd) +{ + struct pcc_acpi *pcc = bl_get_data(bd); + int bright = bd->props.brightness; + int rc; + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + if (bright < pcc->sinf[SINF_AC_MIN_BRIGHT]) + bright = pcc->sinf[SINF_AC_MIN_BRIGHT]; + + if (bright < pcc->sinf[SINF_DC_MIN_BRIGHT]) + bright = pcc->sinf[SINF_DC_MIN_BRIGHT]; + + if (bright < pcc->sinf[SINF_AC_MIN_BRIGHT] || + bright > pcc->sinf[SINF_AC_MAX_BRIGHT]) + return -EINVAL; + + rc = acpi_pcc_write_sset(pcc, SINF_AC_CUR_BRIGHT, bright); + if (rc < 0) + return rc; + + return acpi_pcc_write_sset(pcc, SINF_DC_CUR_BRIGHT, bright); +} + +static const struct backlight_ops pcc_backlight_ops = { + .get_brightness = bl_get, + .update_status = bl_set_status, +}; + + +/* returns ACPI_SUCCESS if methods to control optical drive are present */ + +static acpi_status check_optd_present(void) +{ + acpi_status status = AE_OK; + acpi_handle handle; + + status = acpi_get_handle(NULL, "\\_SB.STAT", &handle); + if (ACPI_FAILURE(status)) + goto out; + status = acpi_get_handle(NULL, "\\_SB.FBAY", &handle); + if (ACPI_FAILURE(status)) + goto out; + status = acpi_get_handle(NULL, "\\_SB.CDDI", &handle); + if (ACPI_FAILURE(status)) + goto out; + +out: + return status; +} + +/* get optical driver power state */ + +static int get_optd_power_state(void) +{ + acpi_status status; + unsigned long long state; + int result; + + status = acpi_evaluate_integer(NULL, "\\_SB.STAT", NULL, &state); + if (ACPI_FAILURE(status)) { + pr_err("evaluation error _SB.STAT\n"); + result = -EIO; + goto out; + } + switch (state) { + case 0: /* power off */ + result = 0; + break; + case 0x0f: /* power on */ + result = 1; + break; + default: + result = -EIO; + break; + } + +out: + return result; +} + +/* set optical drive power state */ + +static int set_optd_power_state(int new_state) +{ + int result; + acpi_status status; + + result = get_optd_power_state(); + if (result < 0) + goto out; + if (new_state == result) + goto out; + + switch (new_state) { + case 0: /* power off */ + /* Call CDDR instead, since they both call the same method + * while CDDI takes 1 arg and we are not quite sure what it is. + */ + status = acpi_evaluate_object(NULL, "\\_SB.CDDR", NULL, NULL); + if (ACPI_FAILURE(status)) { + pr_err("evaluation error _SB.CDDR\n"); + result = -EIO; + } + break; + case 1: /* power on */ + status = acpi_evaluate_object(NULL, "\\_SB.FBAY", NULL, NULL); + if (ACPI_FAILURE(status)) { + pr_err("evaluation error _SB.FBAY\n"); + result = -EIO; + } + break; + default: + result = -EINVAL; + break; + } + +out: + return result; +} + + +/* sysfs user interface functions */ + +static ssize_t numbatt_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_NUM_BATTERIES]); +} + +static ssize_t lcdtype_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_LCD_TYPE]); +} + +static ssize_t mute_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_MUTE]); +} + +static ssize_t mute_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, val; + + err = kstrtoint(buf, 0, &val); + if (err) + return err; + if (val == 0 || val == 1) { + acpi_pcc_write_sset(pcc, SINF_MUTE, val); + pcc->mute = val; + } + + return count; +} + +static ssize_t sticky_key_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sticky_key); +} + +static ssize_t sticky_key_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, val; + + err = kstrtoint(buf, 0, &val); + if (err) + return err; + if (val == 0 || val == 1) { + acpi_pcc_write_sset(pcc, SINF_STICKY_KEY, val); + pcc->sticky_key = val; + } + + return count; +} + +static ssize_t eco_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int result; + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + switch (pcc->sinf[SINF_ECO_MODE]) { + case (ECO_MODE_OFF + 3): + result = 0; + break; + case (ECO_MODE_ON + 3): + result = 1; + break; + default: + return -EIO; + } + return sysfs_emit(buf, "%u\n", result); +} + +static ssize_t eco_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, state; + + union acpi_object param[2]; + struct acpi_object_list input; + acpi_status status; + + param[0].type = ACPI_TYPE_INTEGER; + param[0].integer.value = 0x15; + param[1].type = ACPI_TYPE_INTEGER; + input.count = 2; + input.pointer = param; + + err = kstrtoint(buf, 0, &state); + if (err) + return err; + + switch (state) { + case 0: + param[1].integer.value = ECO_MODE_OFF; + pcc->sinf[SINF_ECO_MODE] = 0; + pcc->eco_mode = 0; + break; + case 1: + param[1].integer.value = ECO_MODE_ON; + pcc->sinf[SINF_ECO_MODE] = 1; + pcc->eco_mode = 1; + break; + default: + /* nothing to do */ + return count; + } + + status = acpi_evaluate_object(NULL, METHOD_ECWR, + &input, NULL); + if (ACPI_FAILURE(status)) { + pr_err("%s evaluation failed\n", METHOD_ECWR); + return -EINVAL; + } + + return count; +} + +static ssize_t ac_brightness_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_AC_CUR_BRIGHT]); +} + +static ssize_t ac_brightness_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, val; + + err = kstrtoint(buf, 0, &val); + if (err) + return err; + if (val >= 0 && val <= 255) { + acpi_pcc_write_sset(pcc, SINF_AC_CUR_BRIGHT, val); + pcc->ac_brightness = val; + } + + return count; +} + +static ssize_t dc_brightness_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_DC_CUR_BRIGHT]); +} + +static ssize_t dc_brightness_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, val; + + err = kstrtoint(buf, 0, &val); + if (err) + return err; + if (val >= 0 && val <= 255) { + acpi_pcc_write_sset(pcc, SINF_DC_CUR_BRIGHT, val); + pcc->dc_brightness = val; + } + + return count; +} + +static ssize_t current_brightness_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (!acpi_pcc_retrieve_biosdata(pcc)) + return -EIO; + + return sysfs_emit(buf, "%u\n", pcc->sinf[SINF_CUR_BRIGHT]); +} + +static ssize_t current_brightness_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + int err, val; + + err = kstrtoint(buf, 0, &val); + if (err) + return err; + + if (val >= 0 && val <= 255) { + err = acpi_pcc_write_sset(pcc, SINF_CUR_BRIGHT, val); + pcc->current_brightness = val; + } + + return count; +} + +static ssize_t cdpower_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int state = get_optd_power_state(); + + if (state < 0) + return state; + + return sysfs_emit(buf, "%d\n", state); +} + +static ssize_t cdpower_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int err, val; + + err = kstrtoint(buf, 10, &val); + if (err) + return err; + set_optd_power_state(val); + return count; +} + +static DEVICE_ATTR_RO(numbatt); +static DEVICE_ATTR_RO(lcdtype); +static DEVICE_ATTR_RW(mute); +static DEVICE_ATTR_RW(sticky_key); +static DEVICE_ATTR_RW(eco_mode); +static DEVICE_ATTR_RW(ac_brightness); +static DEVICE_ATTR_RW(dc_brightness); +static DEVICE_ATTR_RW(current_brightness); +static DEVICE_ATTR_RW(cdpower); + +static umode_t pcc_sysfs_is_visible(struct kobject *kobj, struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct acpi_device *acpi = to_acpi_device(dev); + struct pcc_acpi *pcc = acpi_driver_data(acpi); + + if (attr == &dev_attr_mute.attr) + return (pcc->num_sifr > SINF_MUTE) ? attr->mode : 0; + + if (attr == &dev_attr_eco_mode.attr) + return (pcc->num_sifr > SINF_ECO_MODE) ? attr->mode : 0; + + if (attr == &dev_attr_current_brightness.attr) + return (pcc->num_sifr > SINF_CUR_BRIGHT) ? attr->mode : 0; + + return attr->mode; +} + +static struct attribute *pcc_sysfs_entries[] = { + &dev_attr_numbatt.attr, + &dev_attr_lcdtype.attr, + &dev_attr_mute.attr, + &dev_attr_sticky_key.attr, + &dev_attr_eco_mode.attr, + &dev_attr_ac_brightness.attr, + &dev_attr_dc_brightness.attr, + &dev_attr_current_brightness.attr, + &dev_attr_cdpower.attr, + NULL, +}; + +static const struct attribute_group pcc_attr_group = { + .name = NULL, /* put in device directory */ + .attrs = pcc_sysfs_entries, + .is_visible = pcc_sysfs_is_visible, +}; + + +/* hotkey input device driver */ + +static int sleep_keydown_seen; +static void acpi_pcc_generate_keyinput(struct pcc_acpi *pcc) +{ + struct input_dev *hotk_input_dev = pcc->input_dev; + int rc; + unsigned long long result; + unsigned int key; + unsigned int updown; + + rc = acpi_evaluate_integer(pcc->handle, METHOD_HKEY_QUERY, + NULL, &result); + if (ACPI_FAILURE(rc)) { + pr_err("error getting hotkey status\n"); + return; + } + + key = result & GENMASK(6, 0); + updown = result & BIT(7); /* 0x80 == key down; 0x00 = key up */ + + /* hack: some firmware sends no key down for sleep / hibernate */ + if (key == 7 || key == 10) { + if (updown) + sleep_keydown_seen = 1; + if (!sleep_keydown_seen) + sparse_keymap_report_event(hotk_input_dev, + key, 0x80, false); + } + + /* + * Don't report brightness key-presses if they are also reported + * by the ACPI video bus. + */ + if ((key == 1 || key == 2) && acpi_video_handles_brightness_key_presses()) + return; + + if (!sparse_keymap_report_event(hotk_input_dev, key, updown, false)) + pr_err("Unknown hotkey event: 0x%04llx\n", result); +} + +static void acpi_pcc_hotkey_notify(acpi_handle handle, u32 event, void *data) +{ + struct pcc_acpi *pcc = data; + + switch (event) { + case HKEY_NOTIFY: + acpi_pcc_generate_keyinput(pcc); + break; + default: + /* nothing to do */ + break; + } +} + +static void pcc_optd_notify(acpi_handle handle, u32 event, void *data) +{ + if (event != ACPI_NOTIFY_EJECT_REQUEST) + return; + + set_optd_power_state(0); +} + +static void pcc_register_optd_notifier(struct pcc_acpi *pcc, char *node) +{ + acpi_status status; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + + if (ACPI_SUCCESS(status)) { + status = acpi_install_notify_handler(handle, + ACPI_SYSTEM_NOTIFY, + pcc_optd_notify, pcc); + if (ACPI_FAILURE(status)) + pr_err("Failed to register notify on %s\n", node); + } +} + +static void pcc_unregister_optd_notifier(struct pcc_acpi *pcc, char *node) +{ + acpi_status status = AE_OK; + acpi_handle handle; + + status = acpi_get_handle(NULL, node, &handle); + + if (ACPI_SUCCESS(status)) { + status = acpi_remove_notify_handler(handle, + ACPI_SYSTEM_NOTIFY, + pcc_optd_notify); + if (ACPI_FAILURE(status)) + pr_err("Error removing optd notify handler %s\n", + node); + } +} + +static int acpi_pcc_init_input(struct pcc_acpi *pcc) +{ + struct input_dev *input_dev; + int error; + + input_dev = input_allocate_device(); + if (!input_dev) + return -ENOMEM; + + input_dev->name = ACPI_PCC_DRIVER_NAME; + input_dev->phys = ACPI_PCC_INPUT_PHYS; + input_dev->id.bustype = BUS_HOST; + input_dev->id.vendor = 0x0001; + input_dev->id.product = 0x0001; + input_dev->id.version = 0x0100; + + error = sparse_keymap_setup(input_dev, panasonic_keymap, NULL); + if (error) { + pr_err("Unable to setup input device keymap\n"); + goto err_free_dev; + } + + error = input_register_device(input_dev); + if (error) { + pr_err("Unable to register input device\n"); + goto err_free_dev; + } + + pcc->input_dev = input_dev; + return 0; + + err_free_dev: + input_free_device(input_dev); + return error; +} + +/* kernel module interface */ + +#ifdef CONFIG_PM_SLEEP +static int acpi_pcc_hotkey_resume(struct device *dev) +{ + struct pcc_acpi *pcc = acpi_driver_data(ACPI_COMPANION(dev)); + + if (pcc->num_sifr > SINF_MUTE) + acpi_pcc_write_sset(pcc, SINF_MUTE, pcc->mute); + if (pcc->num_sifr > SINF_ECO_MODE) + acpi_pcc_write_sset(pcc, SINF_ECO_MODE, pcc->eco_mode); + acpi_pcc_write_sset(pcc, SINF_STICKY_KEY, pcc->sticky_key); + acpi_pcc_write_sset(pcc, SINF_AC_CUR_BRIGHT, pcc->ac_brightness); + acpi_pcc_write_sset(pcc, SINF_DC_CUR_BRIGHT, pcc->dc_brightness); + if (pcc->num_sifr > SINF_CUR_BRIGHT) + acpi_pcc_write_sset(pcc, SINF_CUR_BRIGHT, pcc->current_brightness); + + return 0; +} +#endif + +static int acpi_pcc_hotkey_probe(struct platform_device *pdev) +{ + struct backlight_properties props; + struct acpi_device *device; + struct pcc_acpi *pcc; + int num_sifr, result; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + num_sifr = acpi_pcc_get_sqty(device); + + /* + * pcc->sinf is expected to at least have the AC+DC brightness entries. + * Accesses to higher SINF entries are checked against num_sifr. + */ + if (num_sifr <= SINF_DC_CUR_BRIGHT || num_sifr > 255) { + pr_err("num_sifr %d out of range %d - 255\n", num_sifr, SINF_DC_CUR_BRIGHT + 1); + return -ENODEV; + } + + /* + * Some DSDT-s have an off-by-one bug where the SINF package count is + * one higher than the SQTY reported value, allocate 1 entry extra. + */ + num_sifr++; + + pcc = kzalloc_flex(*pcc, sinf, num_sifr); + if (!pcc) { + pr_err("Couldn't allocate mem for pcc"); + return -ENOMEM; + } + + pcc->num_sifr = num_sifr; + pcc->device = device; + pcc->handle = device->handle; + device->driver_data = pcc; + strscpy(acpi_device_name(device), ACPI_PCC_DEVICE_NAME); + strscpy(acpi_device_class(device), ACPI_PCC_CLASS); + + result = acpi_pcc_init_input(pcc); + if (result) { + pr_err("Error installing keyinput handler\n"); + goto out_hotkey; + } + + if (!acpi_pcc_retrieve_biosdata(pcc)) { + result = -EIO; + pr_err("Couldn't retrieve BIOS data\n"); + goto out_input; + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + /* initialize backlight */ + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = pcc->sinf[SINF_AC_MAX_BRIGHT]; + + pcc->backlight = backlight_device_register("panasonic", NULL, pcc, + &pcc_backlight_ops, &props); + if (IS_ERR(pcc->backlight)) { + result = PTR_ERR(pcc->backlight); + goto out_input; + } + + /* read the initial brightness setting from the hardware */ + pcc->backlight->props.brightness = pcc->sinf[SINF_AC_CUR_BRIGHT]; + } + + /* Reset initial sticky key mode since the hardware register state is not consistent */ + acpi_pcc_write_sset(pcc, SINF_STICKY_KEY, 0); + pcc->sticky_key = 0; + + pcc->ac_brightness = pcc->sinf[SINF_AC_CUR_BRIGHT]; + pcc->dc_brightness = pcc->sinf[SINF_DC_CUR_BRIGHT]; + if (pcc->num_sifr > SINF_MUTE) + pcc->mute = pcc->sinf[SINF_MUTE]; + if (pcc->num_sifr > SINF_ECO_MODE) + pcc->eco_mode = pcc->sinf[SINF_ECO_MODE]; + if (pcc->num_sifr > SINF_CUR_BRIGHT) + pcc->current_brightness = pcc->sinf[SINF_CUR_BRIGHT]; + + /* add sysfs attributes */ + result = sysfs_create_group(&device->dev.kobj, &pcc_attr_group); + if (result) + goto out_backlight; + + result = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify, pcc); + if (result) + goto out_sysfs; + + /* optical drive initialization */ + if (ACPI_SUCCESS(check_optd_present())) { + pcc->platform = platform_device_register_simple("panasonic", + PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(pcc->platform)) { + result = PTR_ERR(pcc->platform); + goto out_notify_handler; + } + result = device_create_file(&pcc->platform->dev, + &dev_attr_cdpower); + if (result) + goto out_platform; + + pcc_register_optd_notifier(pcc, "\\_SB.PCI0.EHCI.ERHB.OPTD"); + } else { + pcc->platform = NULL; + } + + i8042_install_filter(panasonic_i8042_filter, NULL); + return 0; + +out_platform: + platform_device_unregister(pcc->platform); +out_notify_handler: + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify); +out_sysfs: + sysfs_remove_group(&device->dev.kobj, &pcc_attr_group); +out_backlight: + backlight_device_unregister(pcc->backlight); +out_input: + input_unregister_device(pcc->input_dev); +out_hotkey: + device->driver_data = NULL; + kfree(pcc); + + return result; +} + +static void acpi_pcc_hotkey_remove(struct platform_device *pdev) +{ + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + struct pcc_acpi *pcc = acpi_driver_data(device); + + i8042_remove_filter(panasonic_i8042_filter); + + if (pcc->platform) { + pcc_unregister_optd_notifier(pcc, "\\_SB.PCI0.EHCI.ERHB.OPTD"); + device_remove_file(&pcc->platform->dev, &dev_attr_cdpower); + platform_device_unregister(pcc->platform); + } + + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, + acpi_pcc_hotkey_notify); + + sysfs_remove_group(&device->dev.kobj, &pcc_attr_group); + + backlight_device_unregister(pcc->backlight); + + input_unregister_device(pcc->input_dev); + + device->driver_data = NULL; + + kfree(pcc); +} + +module_platform_driver(acpi_pcc_driver); diff --git a/drivers/platform/x86/pcengines-apuv2.c b/drivers/platform/x86/pcengines-apuv2.c new file mode 100644 index 000000000..8ac5f719c --- /dev/null +++ b/drivers/platform/x86/pcengines-apuv2.c @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * PC-Engines APUv2/APUv3 board platform driver + * for GPIO buttons and LEDs + * + * Copyright (C) 2018 metux IT consult + * Author: Enrico Weigelt + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * NOTE: this driver only supports APUv2/3 - not APUv1, as this one + * has completely different register layouts. + */ + +/* Register mappings */ +#define APU2_GPIO_REG_LED1 AMD_FCH_GPIO_REG_GPIO57 +#define APU2_GPIO_REG_LED2 AMD_FCH_GPIO_REG_GPIO58 +#define APU2_GPIO_REG_LED3 AMD_FCH_GPIO_REG_GPIO59_DEVSLP1 +#define APU2_GPIO_REG_MODESW AMD_FCH_GPIO_REG_GPIO32_GE1 +#define APU2_GPIO_REG_SIMSWAP AMD_FCH_GPIO_REG_GPIO33_GE2 +#define APU2_GPIO_REG_MPCIE2 AMD_FCH_GPIO_REG_GPIO55_DEVSLP0 +#define APU2_GPIO_REG_MPCIE3 AMD_FCH_GPIO_REG_GPIO51 + +/* Order in which the GPIO lines are defined in the register list */ +#define APU2_GPIO_LINE_LED1 0 +#define APU2_GPIO_LINE_LED2 1 +#define APU2_GPIO_LINE_LED3 2 +#define APU2_GPIO_LINE_MODESW 3 +#define APU2_GPIO_LINE_SIMSWAP 4 +#define APU2_GPIO_LINE_MPCIE2 5 +#define APU2_GPIO_LINE_MPCIE3 6 + +/* GPIO device */ + +static int apu2_gpio_regs[] = { + [APU2_GPIO_LINE_LED1] = APU2_GPIO_REG_LED1, + [APU2_GPIO_LINE_LED2] = APU2_GPIO_REG_LED2, + [APU2_GPIO_LINE_LED3] = APU2_GPIO_REG_LED3, + [APU2_GPIO_LINE_MODESW] = APU2_GPIO_REG_MODESW, + [APU2_GPIO_LINE_SIMSWAP] = APU2_GPIO_REG_SIMSWAP, + [APU2_GPIO_LINE_MPCIE2] = APU2_GPIO_REG_MPCIE2, + [APU2_GPIO_LINE_MPCIE3] = APU2_GPIO_REG_MPCIE3, +}; + +static const char * const apu2_gpio_names[] = { + [APU2_GPIO_LINE_LED1] = "front-led1", + [APU2_GPIO_LINE_LED2] = "front-led2", + [APU2_GPIO_LINE_LED3] = "front-led3", + [APU2_GPIO_LINE_MODESW] = "front-button", + [APU2_GPIO_LINE_SIMSWAP] = "simswap", + [APU2_GPIO_LINE_MPCIE2] = "mpcie2_reset", + [APU2_GPIO_LINE_MPCIE3] = "mpcie3_reset", +}; + +static const struct amd_fch_gpio_pdata board_apu2 = { + .gpio_num = ARRAY_SIZE(apu2_gpio_regs), + .gpio_reg = apu2_gpio_regs, + .gpio_names = apu2_gpio_names, +}; + +static const struct software_node apu2_gpiochip_node = { + .name = AMD_FCH_GPIO_DRIVER_NAME, +}; + +/* GPIO LEDs device */ +static const struct software_node apu2_leds_node = { + .name = "apu2-leds", +}; + +static const struct property_entry apu2_led1_props[] = { + PROPERTY_ENTRY_STRING("label", "apu:green:1"), + PROPERTY_ENTRY_GPIO("gpios", &apu2_gpiochip_node, + APU2_GPIO_LINE_LED1, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node apu2_led1_swnode = { + .name = "led-1", + .parent = &apu2_leds_node, + .properties = apu2_led1_props, +}; + +static const struct property_entry apu2_led2_props[] = { + PROPERTY_ENTRY_STRING("label", "apu:green:2"), + PROPERTY_ENTRY_GPIO("gpios", &apu2_gpiochip_node, + APU2_GPIO_LINE_LED2, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node apu2_led2_swnode = { + .name = "led-2", + .parent = &apu2_leds_node, + .properties = apu2_led2_props, +}; + +static const struct property_entry apu2_led3_props[] = { + PROPERTY_ENTRY_STRING("label", "apu:green:3"), + PROPERTY_ENTRY_GPIO("gpios", &apu2_gpiochip_node, + APU2_GPIO_LINE_LED3, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node apu2_led3_swnode = { + .name = "led-3", + .parent = &apu2_leds_node, + .properties = apu2_led3_props, +}; + +/* GPIO keyboard device */ +static const struct property_entry apu2_keys_props[] = { + PROPERTY_ENTRY_U32("poll-interval", 100), + { } +}; + +static const struct software_node apu2_keys_node = { + .name = "apu2-keys", + .properties = apu2_keys_props, +}; + +static const struct property_entry apu2_front_button_props[] = { + PROPERTY_ENTRY_STRING("label", "front button"), + PROPERTY_ENTRY_U32("linux,code", KEY_RESTART), + PROPERTY_ENTRY_GPIO("gpios", &apu2_gpiochip_node, + APU2_GPIO_LINE_MODESW, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 10), + { } +}; + +static const struct software_node apu2_front_button_swnode = { + .name = "front-button", + .parent = &apu2_keys_node, + .properties = apu2_front_button_props, +}; + +static const struct software_node *apu2_swnodes[] = { + &apu2_gpiochip_node, + /* LEDs nodes */ + &apu2_leds_node, + &apu2_led1_swnode, + &apu2_led2_swnode, + &apu2_led3_swnode, + /* Keys nodes */ + &apu2_keys_node, + &apu2_front_button_swnode, + NULL +}; + +/* Board setup */ + +/* Note: matching works on string prefix, so "apu2" must come before "apu" */ +static const struct dmi_system_id apu_gpio_dmi_table[] __initconst = { + + /* APU2 w/ legacy BIOS < 4.0.8 */ + { + .ident = "apu2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "APU2") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU2 w/ legacy BIOS >= 4.0.8 */ + { + .ident = "apu2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "apu2") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU2 w/ mainline BIOS */ + { + .ident = "apu2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "PC Engines apu2") + }, + .driver_data = (void *)&board_apu2, + }, + + /* APU3 w/ legacy BIOS < 4.0.8 */ + { + .ident = "apu3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "APU3") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU3 w/ legacy BIOS >= 4.0.8 */ + { + .ident = "apu3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "apu3") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU3 w/ mainline BIOS */ + { + .ident = "apu3", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "PC Engines apu3") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU4 w/ legacy BIOS < 4.0.8 */ + { + .ident = "apu4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "APU4") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU4 w/ legacy BIOS >= 4.0.8 */ + { + .ident = "apu4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "apu4") + }, + .driver_data = (void *)&board_apu2, + }, + /* APU4 w/ mainline BIOS */ + { + .ident = "apu4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_BOARD_NAME, "PC Engines apu4") + }, + .driver_data = (void *)&board_apu2, + }, + {} +}; + +static struct platform_device *apu_gpio_pdev; +static struct platform_device *apu_leds_pdev; +static struct platform_device *apu_keys_pdev; + +static struct platform_device * __init apu_create_pdev(const char *name, + const void *data, size_t size, + const struct software_node *swnode) +{ + struct platform_device_info pdev_info = { + .name = name, + .id = PLATFORM_DEVID_NONE, + .data = data, + .size_data = size, + .swnode = swnode, + }; + struct platform_device *pdev; + int err; + + pdev = platform_device_register_full(&pdev_info); + + err = PTR_ERR_OR_ZERO(pdev); + if (err) + pr_err("failed registering %s: %d\n", name, err); + + return pdev; +} + +static int __init apu_board_init(void) +{ + const struct dmi_system_id *id; + int err; + + id = dmi_first_match(apu_gpio_dmi_table); + if (!id) { + pr_err("failed to detect APU board via DMI\n"); + return -ENODEV; + } + + err = software_node_register_node_group(apu2_swnodes); + if (err) { + pr_err("failed to register software nodes: %d\n", err); + return err; + } + + apu_gpio_pdev = apu_create_pdev(AMD_FCH_GPIO_DRIVER_NAME, + id->driver_data, sizeof(struct amd_fch_gpio_pdata), + &apu2_gpiochip_node); + err = PTR_ERR_OR_ZERO(apu_gpio_pdev); + if (err) + goto err_unregister_swnodes; + + apu_leds_pdev = apu_create_pdev("leds-gpio", NULL, 0, &apu2_leds_node); + err = PTR_ERR_OR_ZERO(apu_leds_pdev); + if (err) + goto err_unregister_gpio; + + apu_keys_pdev = apu_create_pdev("gpio-keys-polled", NULL, 0, &apu2_keys_node); + err = PTR_ERR_OR_ZERO(apu_keys_pdev); + if (err) + goto err_unregister_leds; + + return 0; + +err_unregister_leds: + platform_device_unregister(apu_leds_pdev); +err_unregister_gpio: + platform_device_unregister(apu_gpio_pdev); +err_unregister_swnodes: + software_node_unregister_node_group(apu2_swnodes); + return err; +} + +static void __exit apu_board_exit(void) +{ + platform_device_unregister(apu_keys_pdev); + platform_device_unregister(apu_leds_pdev); + platform_device_unregister(apu_gpio_pdev); + software_node_unregister_node_group(apu2_swnodes); +} + +module_init(apu_board_init); +module_exit(apu_board_exit); + +MODULE_AUTHOR("Enrico Weigelt, metux IT consult "); +MODULE_DESCRIPTION("PC Engines APUv2/APUv3 board GPIO/LEDs/keys driver"); +MODULE_LICENSE("GPL"); +MODULE_DEVICE_TABLE(dmi, apu_gpio_dmi_table); +MODULE_SOFTDEP("pre: platform:" AMD_FCH_GPIO_DRIVER_NAME " platform:leds-gpio platform:gpio_keys_polled"); diff --git a/drivers/platform/x86/pmc_atom.c b/drivers/platform/x86/pmc_atom.c new file mode 100644 index 000000000..046933f6d --- /dev/null +++ b/drivers/platform/x86/pmc_atom.c @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Intel Atom SoC Power Management Controller Driver + * Copyright (c) 2014-2015,2017,2022 Intel Corporation. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct pmc_bit_map { + const char *name; + u32 bit_mask; +}; + +struct pmc_reg_map { + const struct pmc_bit_map *d3_sts_0; + const struct pmc_bit_map *d3_sts_1; + const struct pmc_bit_map *func_dis; + const struct pmc_bit_map *func_dis_2; + const struct pmc_bit_map *pss; +}; + +struct pmc_data { + const struct pmc_reg_map *map; + const struct pmc_clk *clks; +}; + +struct pmc_dev { + u32 base_addr; + void __iomem *regmap; + const struct pmc_reg_map *map; +#ifdef CONFIG_DEBUG_FS + struct dentry *dbgfs_dir; +#endif /* CONFIG_DEBUG_FS */ + bool init; +}; + +static struct pmc_dev pmc_device; +static u32 acpi_base_addr; + +static const struct pmc_clk byt_clks[] = { + { + .name = "xtal", + .freq = 25000000, + .parent_name = NULL, + }, + { + .name = "pll", + .freq = 19200000, + .parent_name = "xtal", + }, + {} +}; + +static const struct pmc_clk cht_clks[] = { + { + .name = "xtal", + .freq = 19200000, + .parent_name = NULL, + }, + {} +}; + +static const struct pmc_bit_map d3_sts_0_map[] = { + {"LPSS1_F0_DMA", BIT_LPSS1_F0_DMA}, + {"LPSS1_F1_PWM1", BIT_LPSS1_F1_PWM1}, + {"LPSS1_F2_PWM2", BIT_LPSS1_F2_PWM2}, + {"LPSS1_F3_HSUART1", BIT_LPSS1_F3_HSUART1}, + {"LPSS1_F4_HSUART2", BIT_LPSS1_F4_HSUART2}, + {"LPSS1_F5_SPI", BIT_LPSS1_F5_SPI}, + {"LPSS1_F6_Reserved", BIT_LPSS1_F6_XXX}, + {"LPSS1_F7_Reserved", BIT_LPSS1_F7_XXX}, + {"SCC_EMMC", BIT_SCC_EMMC}, + {"SCC_SDIO", BIT_SCC_SDIO}, + {"SCC_SDCARD", BIT_SCC_SDCARD}, + {"SCC_MIPI", BIT_SCC_MIPI}, + {"HDA", BIT_HDA}, + {"LPE", BIT_LPE}, + {"OTG", BIT_OTG}, + {"USH", BIT_USH}, + {"GBE", BIT_GBE}, + {"SATA", BIT_SATA}, + {"USB_EHCI", BIT_USB_EHCI}, + {"SEC", BIT_SEC}, + {"PCIE_PORT0", BIT_PCIE_PORT0}, + {"PCIE_PORT1", BIT_PCIE_PORT1}, + {"PCIE_PORT2", BIT_PCIE_PORT2}, + {"PCIE_PORT3", BIT_PCIE_PORT3}, + {"LPSS2_F0_DMA", BIT_LPSS2_F0_DMA}, + {"LPSS2_F1_I2C1", BIT_LPSS2_F1_I2C1}, + {"LPSS2_F2_I2C2", BIT_LPSS2_F2_I2C2}, + {"LPSS2_F3_I2C3", BIT_LPSS2_F3_I2C3}, + {"LPSS2_F3_I2C4", BIT_LPSS2_F4_I2C4}, + {"LPSS2_F5_I2C5", BIT_LPSS2_F5_I2C5}, + {"LPSS2_F6_I2C6", BIT_LPSS2_F6_I2C6}, + {"LPSS2_F7_I2C7", BIT_LPSS2_F7_I2C7}, + {} +}; + +static struct pmc_bit_map byt_d3_sts_1_map[] = { + {"SMB", BIT_SMB}, + {"OTG_SS_PHY", BIT_OTG_SS_PHY}, + {"USH_SS_PHY", BIT_USH_SS_PHY}, + {"DFX", BIT_DFX}, + {} +}; + +static struct pmc_bit_map cht_d3_sts_1_map[] = { + {"SMB", BIT_SMB}, + {"GMM", BIT_STS_GMM}, + {"ISH", BIT_STS_ISH}, + {} +}; + +static struct pmc_bit_map cht_func_dis_2_map[] = { + {"SMB", BIT_SMB}, + {"GMM", BIT_FD_GMM}, + {"ISH", BIT_FD_ISH}, + {} +}; + +static const struct pmc_bit_map byt_pss_map[] = { + {"GBE", PMC_PSS_BIT_GBE}, + {"SATA", PMC_PSS_BIT_SATA}, + {"HDA", PMC_PSS_BIT_HDA}, + {"SEC", PMC_PSS_BIT_SEC}, + {"PCIE", PMC_PSS_BIT_PCIE}, + {"LPSS", PMC_PSS_BIT_LPSS}, + {"LPE", PMC_PSS_BIT_LPE}, + {"DFX", PMC_PSS_BIT_DFX}, + {"USH_CTRL", PMC_PSS_BIT_USH_CTRL}, + {"USH_SUS", PMC_PSS_BIT_USH_SUS}, + {"USH_VCCS", PMC_PSS_BIT_USH_VCCS}, + {"USH_VCCA", PMC_PSS_BIT_USH_VCCA}, + {"OTG_CTRL", PMC_PSS_BIT_OTG_CTRL}, + {"OTG_VCCS", PMC_PSS_BIT_OTG_VCCS}, + {"OTG_VCCA_CLK", PMC_PSS_BIT_OTG_VCCA_CLK}, + {"OTG_VCCA", PMC_PSS_BIT_OTG_VCCA}, + {"USB", PMC_PSS_BIT_USB}, + {"USB_SUS", PMC_PSS_BIT_USB_SUS}, + {} +}; + +static const struct pmc_bit_map cht_pss_map[] = { + {"SATA", PMC_PSS_BIT_SATA}, + {"HDA", PMC_PSS_BIT_HDA}, + {"SEC", PMC_PSS_BIT_SEC}, + {"PCIE", PMC_PSS_BIT_PCIE}, + {"LPSS", PMC_PSS_BIT_LPSS}, + {"LPE", PMC_PSS_BIT_LPE}, + {"UFS", PMC_PSS_BIT_CHT_UFS}, + {"UXD", PMC_PSS_BIT_CHT_UXD}, + {"UXD_FD", PMC_PSS_BIT_CHT_UXD_FD}, + {"UX_ENG", PMC_PSS_BIT_CHT_UX_ENG}, + {"USB_SUS", PMC_PSS_BIT_CHT_USB_SUS}, + {"GMM", PMC_PSS_BIT_CHT_GMM}, + {"ISH", PMC_PSS_BIT_CHT_ISH}, + {"DFX_MASTER", PMC_PSS_BIT_CHT_DFX_MASTER}, + {"DFX_CLUSTER1", PMC_PSS_BIT_CHT_DFX_CLUSTER1}, + {"DFX_CLUSTER2", PMC_PSS_BIT_CHT_DFX_CLUSTER2}, + {"DFX_CLUSTER3", PMC_PSS_BIT_CHT_DFX_CLUSTER3}, + {"DFX_CLUSTER4", PMC_PSS_BIT_CHT_DFX_CLUSTER4}, + {"DFX_CLUSTER5", PMC_PSS_BIT_CHT_DFX_CLUSTER5}, + {} +}; + +static const struct pmc_reg_map byt_reg_map = { + .d3_sts_0 = d3_sts_0_map, + .d3_sts_1 = byt_d3_sts_1_map, + .func_dis = d3_sts_0_map, + .func_dis_2 = byt_d3_sts_1_map, + .pss = byt_pss_map, +}; + +static const struct pmc_reg_map cht_reg_map = { + .d3_sts_0 = d3_sts_0_map, + .d3_sts_1 = cht_d3_sts_1_map, + .func_dis = d3_sts_0_map, + .func_dis_2 = cht_func_dis_2_map, + .pss = cht_pss_map, +}; + +static const struct pmc_data byt_data = { + .map = &byt_reg_map, + .clks = byt_clks, +}; + +static const struct pmc_data cht_data = { + .map = &cht_reg_map, + .clks = cht_clks, +}; + +static inline u32 pmc_reg_read(struct pmc_dev *pmc, int reg_offset) +{ + return readl(pmc->regmap + reg_offset); +} + +static inline void pmc_reg_write(struct pmc_dev *pmc, int reg_offset, u32 val) +{ + writel(val, pmc->regmap + reg_offset); +} + +int pmc_atom_read(int offset, u32 *value) +{ + struct pmc_dev *pmc = &pmc_device; + + if (!pmc->init) + return -ENODEV; + + *value = pmc_reg_read(pmc, offset); + return 0; +} + +static void pmc_power_off(void) +{ + u16 pm1_cnt_port; + u32 pm1_cnt_value; + + pr_info("Preparing to enter system sleep state S5\n"); + + pm1_cnt_port = acpi_base_addr + PM1_CNT; + + pm1_cnt_value = inl(pm1_cnt_port); + pm1_cnt_value &= ~SLEEP_TYPE_MASK; + pm1_cnt_value |= SLEEP_TYPE_S5; + pm1_cnt_value |= SLEEP_ENABLE; + + outl(pm1_cnt_value, pm1_cnt_port); +} + +static void pmc_hw_reg_setup(struct pmc_dev *pmc) +{ + /* + * Disable PMC S0IX_WAKE_EN events coming from: + * - LPC clock run + * - GPIO_SUS ored dedicated IRQs + * - GPIO_SCORE ored dedicated IRQs + * - GPIO_SUS shared IRQ + * - GPIO_SCORE shared IRQ + */ + pmc_reg_write(pmc, PMC_S0IX_WAKE_EN, (u32)PMC_WAKE_EN_SETTING); +} + +#ifdef CONFIG_DEBUG_FS +static void pmc_dev_state_print(struct seq_file *s, int reg_index, + u32 sts, const struct pmc_bit_map *sts_map, + u32 fd, const struct pmc_bit_map *fd_map) +{ + int offset = PMC_REG_BIT_WIDTH * reg_index; + int index; + + for (index = 0; sts_map[index].name; index++) { + seq_printf(s, "Dev: %-2d - %-32s\tState: %s [%s]\n", + offset + index, sts_map[index].name, + fd_map[index].bit_mask & fd ? "Disabled" : "Enabled ", + sts_map[index].bit_mask & sts ? "D3" : "D0"); + } +} + +static int pmc_dev_state_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmc = s->private; + const struct pmc_reg_map *m = pmc->map; + u32 func_dis, func_dis_2; + u32 d3_sts_0, d3_sts_1; + + func_dis = pmc_reg_read(pmc, PMC_FUNC_DIS); + func_dis_2 = pmc_reg_read(pmc, PMC_FUNC_DIS_2); + d3_sts_0 = pmc_reg_read(pmc, PMC_D3_STS_0); + d3_sts_1 = pmc_reg_read(pmc, PMC_D3_STS_1); + + /* Low part */ + pmc_dev_state_print(s, 0, d3_sts_0, m->d3_sts_0, func_dis, m->func_dis); + + /* High part */ + pmc_dev_state_print(s, 1, d3_sts_1, m->d3_sts_1, func_dis_2, m->func_dis_2); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(pmc_dev_state); + +static int pmc_pss_state_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmc = s->private; + const struct pmc_bit_map *map = pmc->map->pss; + u32 pss = pmc_reg_read(pmc, PMC_PSS); + int index; + + for (index = 0; map[index].name; index++) { + seq_printf(s, "Island: %-2d - %-32s\tState: %s\n", + index, map[index].name, + map[index].bit_mask & pss ? "Off" : "On"); + } + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(pmc_pss_state); + +static int pmc_sleep_tmr_show(struct seq_file *s, void *unused) +{ + struct pmc_dev *pmc = s->private; + u64 s0ir_tmr, s0i1_tmr, s0i2_tmr, s0i3_tmr, s0_tmr; + + s0ir_tmr = (u64)pmc_reg_read(pmc, PMC_S0IR_TMR) << PMC_TMR_SHIFT; + s0i1_tmr = (u64)pmc_reg_read(pmc, PMC_S0I1_TMR) << PMC_TMR_SHIFT; + s0i2_tmr = (u64)pmc_reg_read(pmc, PMC_S0I2_TMR) << PMC_TMR_SHIFT; + s0i3_tmr = (u64)pmc_reg_read(pmc, PMC_S0I3_TMR) << PMC_TMR_SHIFT; + s0_tmr = (u64)pmc_reg_read(pmc, PMC_S0_TMR) << PMC_TMR_SHIFT; + + seq_printf(s, "S0IR Residency:\t%lldus\n", s0ir_tmr); + seq_printf(s, "S0I1 Residency:\t%lldus\n", s0i1_tmr); + seq_printf(s, "S0I2 Residency:\t%lldus\n", s0i2_tmr); + seq_printf(s, "S0I3 Residency:\t%lldus\n", s0i3_tmr); + seq_printf(s, "S0 Residency:\t%lldus\n", s0_tmr); + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(pmc_sleep_tmr); + +static void pmc_dbgfs_register(struct pmc_dev *pmc) +{ + struct dentry *dir; + + dir = debugfs_create_dir("pmc_atom", NULL); + + pmc->dbgfs_dir = dir; + + debugfs_create_file("dev_state", S_IFREG | S_IRUGO, dir, pmc, + &pmc_dev_state_fops); + debugfs_create_file("pss_state", S_IFREG | S_IRUGO, dir, pmc, + &pmc_pss_state_fops); + debugfs_create_file("sleep_state", S_IFREG | S_IRUGO, dir, pmc, + &pmc_sleep_tmr_fops); +} +#else +static void pmc_dbgfs_register(struct pmc_dev *pmc) +{ +} +#endif /* CONFIG_DEBUG_FS */ + +static bool pmc_clk_is_critical = true; + +static int dmi_callback(const struct dmi_system_id *d) +{ + pr_info("%s: PMC critical clocks quirk enabled\n", d->ident); + + return 1; +} + +static int dmi_callback_siemens(const struct dmi_system_id *d) +{ + u32 st_id; + + if (dmi_walk(simatic_ipc_find_dmi_entry_helper, &st_id)) + goto out; + + if (st_id == SIMATIC_IPC_IPC227E || st_id == SIMATIC_IPC_IPC277E) + return dmi_callback(d); + +out: + pmc_clk_is_critical = false; + return 1; +} + +/* + * Some systems need one or more of their pmc_plt_clks to be + * marked as critical. + */ +static const struct dmi_system_id critclk_systems[] = { + { + /* pmc_plt_clk0 is used for an external HSIC USB HUB */ + .ident = "MPL CEC1x", + .callback = dmi_callback, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MPL AG"), + DMI_MATCH(DMI_PRODUCT_NAME, "CEC10 Family"), + }, + }, + { + /* + * Lex System / Lex Computech Co. makes a lot of Bay Trail + * based embedded boards which often come with multiple + * ethernet controllers using multiple pmc_plt_clks. See: + * https://www.lex.com.tw/products/embedded-ipc-board/ + */ + .ident = "Lex BayTrail", + .callback = dmi_callback, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Lex BayTrail"), + }, + }, + { + /* pmc_plt_clk* - are used for ethernet controllers */ + .ident = "Beckhoff Baytrail", + .callback = dmi_callback, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Beckhoff Automation"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "CBxx63"), + }, + }, + { + .ident = "SIEMENS AG", + .callback = dmi_callback_siemens, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SIEMENS AG"), + }, + }, + {} +}; + +static int pmc_setup_clks(struct pci_dev *pdev, void __iomem *pmc_regmap, + const struct pmc_data *pmc_data) +{ + struct platform_device *clkdev; + struct pmc_clk_data *clk_data; + + clk_data = kzalloc_obj(*clk_data); + if (!clk_data) + return -ENOMEM; + + clk_data->base = pmc_regmap; /* offset is added by client */ + clk_data->clks = pmc_data->clks; + if (dmi_check_system(critclk_systems)) + clk_data->critical = pmc_clk_is_critical; + + clkdev = platform_device_register_data(&pdev->dev, "clk-pmc-atom", + PLATFORM_DEVID_NONE, + clk_data, sizeof(*clk_data)); + if (IS_ERR(clkdev)) { + kfree(clk_data); + return PTR_ERR(clkdev); + } + + kfree(clk_data); + + return 0; +} + +#ifdef CONFIG_SUSPEND +static void pmc_dev_state_check(u32 sts, const struct pmc_bit_map *sts_map, + u32 fd, const struct pmc_bit_map *fd_map, + u32 sts_possible_false_pos) +{ + int index; + + for (index = 0; sts_map[index].name; index++) { + if (!(fd_map[index].bit_mask & fd) && + !(sts_map[index].bit_mask & sts)) { + if (sts_map[index].bit_mask & sts_possible_false_pos) + pm_pr_dbg("%s is in D0 prior to s2idle\n", + sts_map[index].name); + else + pr_err("%s is in D0 prior to s2idle\n", + sts_map[index].name); + } + } +} + +static void pmc_s2idle_check(void) +{ + struct pmc_dev *pmc = &pmc_device; + const struct pmc_reg_map *m = pmc->map; + u32 func_dis, func_dis_2; + u32 d3_sts_0, d3_sts_1; + u32 false_pos_sts_0, false_pos_sts_1; + int i; + + func_dis = pmc_reg_read(pmc, PMC_FUNC_DIS); + func_dis_2 = pmc_reg_read(pmc, PMC_FUNC_DIS_2); + d3_sts_0 = pmc_reg_read(pmc, PMC_D3_STS_0); + d3_sts_1 = pmc_reg_read(pmc, PMC_D3_STS_1); + + /* + * Some blocks are not used on lower-featured versions of the SoC and + * always report D0, add these to false_pos mask to log at debug level. + */ + if (m->d3_sts_1 == byt_d3_sts_1_map) { + /* Bay Trail */ + false_pos_sts_0 = BIT_GBE | BIT_SATA | BIT_PCIE_PORT0 | + BIT_PCIE_PORT1 | BIT_PCIE_PORT2 | BIT_PCIE_PORT3 | + BIT_LPSS2_F5_I2C5; + false_pos_sts_1 = BIT_SMB | BIT_USH_SS_PHY | BIT_DFX; + } else { + /* Cherry Trail */ + false_pos_sts_0 = BIT_GBE | BIT_SATA | BIT_LPSS2_F7_I2C7; + false_pos_sts_1 = BIT_SMB | BIT_STS_ISH; + } + + pmc_dev_state_check(d3_sts_0, m->d3_sts_0, func_dis, m->func_dis, false_pos_sts_0); + pmc_dev_state_check(d3_sts_1, m->d3_sts_1, func_dis_2, m->func_dis_2, false_pos_sts_1); + + /* Forced-on PMC clocks prevent S0i3 */ + for (i = 0; i < PMC_CLK_NUM; i++) { + u32 ctl = pmc_reg_read(pmc, PMC_CLK_CTL_OFFSET + 4 * i); + + if ((ctl & PMC_MASK_CLK_CTL) != PMC_CLK_CTL_FORCE_ON) + continue; + + pr_err("clock %d is ON prior to freeze (ctl 0x%08x)\n", i, ctl); + } +} + +static struct acpi_s2idle_dev_ops pmc_s2idle_ops = { + .check = pmc_s2idle_check, +}; + +static void pmc_s2idle_check_register(void) +{ + acpi_register_lps0_dev(&pmc_s2idle_ops); +} +#else +static void pmc_s2idle_check_register(void) {} +#endif + +static int pmc_setup_dev(struct pci_dev *pdev, const struct pci_device_id *ent) +{ + struct pmc_dev *pmc = &pmc_device; + const struct pmc_data *data = (struct pmc_data *)ent->driver_data; + const struct pmc_reg_map *map = data->map; + int ret; + + /* Obtain ACPI base address */ + pci_read_config_dword(pdev, ACPI_BASE_ADDR_OFFSET, &acpi_base_addr); + acpi_base_addr &= ACPI_BASE_ADDR_MASK; + + /* Install power off function */ + if (acpi_base_addr != 0 && pm_power_off == NULL) + pm_power_off = pmc_power_off; + + pci_read_config_dword(pdev, PMC_BASE_ADDR_OFFSET, &pmc->base_addr); + pmc->base_addr &= PMC_BASE_ADDR_MASK; + + pmc->regmap = ioremap(pmc->base_addr, PMC_MMIO_REG_LEN); + if (!pmc->regmap) { + dev_err(&pdev->dev, "error: ioremap failed\n"); + return -ENOMEM; + } + + pmc->map = map; + + /* PMC hardware registers setup */ + pmc_hw_reg_setup(pmc); + + pmc_dbgfs_register(pmc); + + /* Register platform clocks - PMC_PLT_CLK [0..5] */ + ret = pmc_setup_clks(pdev, pmc->regmap, data); + if (ret) + dev_warn(&pdev->dev, "platform clocks register failed: %d\n", + ret); + + pmc_s2idle_check_register(); + pmc->init = true; + return ret; +} + +/* Data for PCI driver interface used by pci_match_id() call below */ +static const struct pci_device_id pmc_pci_ids[] = { + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_VLV_PMC), .driver_data = (kernel_ulong_t)&byt_data }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_CHT_PMC), .driver_data = (kernel_ulong_t)&cht_data }, + {} +}; + +static int __init pmc_atom_init(void) +{ + struct pci_dev *pdev = NULL; + const struct pci_device_id *ent; + + /* + * We look for our device - PCU PMC. + * We assume that there is maximum one device. + * + * We can't use plain pci_driver mechanism, + * as the device is really a multiple function device, + * main driver that binds to the pci_device is lpc_ich + * and have to find & bind to the device this way. + */ + for_each_pci_dev(pdev) { + ent = pci_match_id(pmc_pci_ids, pdev); + if (ent) + return pmc_setup_dev(pdev, ent); + } + /* Device not found */ + return -ENODEV; +} + +device_initcall(pmc_atom_init); + +/* +MODULE_AUTHOR("Aubrey Li "); +MODULE_DESCRIPTION("Intel Atom SoC Power Management Controller Interface"); +MODULE_LICENSE("GPL v2"); +*/ diff --git a/drivers/platform/x86/portwell-ec.c b/drivers/platform/x86/portwell-ec.c new file mode 100644 index 000000000..ac506ea40 --- /dev/null +++ b/drivers/platform/x86/portwell-ec.c @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * portwell-ec.c: Portwell embedded controller driver. + * + * Tested on: + * - Portwell NANO-6064 + * + * This driver supports Portwell boards with an ITE embedded controller (EC). + * The EC is accessed through I/O ports and provides: + * - Temperature and voltage readings (hwmon) + * - 8 GPIO pins for control and monitoring + * - Hardware watchdog with 1-15300 second timeout range + * + * It integrates with the Linux hwmon, GPIO and Watchdog subsystems. + * + * (C) Copyright 2025 Portwell, Inc. + * Author: Yen-Chi Huang (jesse.huang@portwell.com.tw) + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PORTWELL_EC_IOSPACE 0xe300 +#define PORTWELL_EC_IOSPACE_LEN SZ_256 + +#define PORTWELL_GPIO_PINS 8 +#define PORTWELL_GPIO_DIR_REG 0x2b +#define PORTWELL_GPIO_VAL_REG 0x2c + +#define PORTWELL_HWMON_TEMP_NUM 3 +#define PORTWELL_HWMON_VOLT_NUM 5 + +#define PORTWELL_WDT_EC_CONFIG_ADDR 0x06 +#define PORTWELL_WDT_CONFIG_ENABLE 0x1 +#define PORTWELL_WDT_CONFIG_DISABLE 0x0 +#define PORTWELL_WDT_EC_COUNT_MIN_ADDR 0x07 +#define PORTWELL_WDT_EC_COUNT_SEC_ADDR 0x08 +#define PORTWELL_WDT_EC_MAX_COUNT_SECOND (255 * 60) + +#define PORTWELL_EC_FW_VENDOR_ADDRESS 0x4d +#define PORTWELL_EC_FW_VENDOR_LENGTH 3 +#define PORTWELL_EC_FW_VENDOR_NAME "PWG" + +#define PORTWELL_EC_ADC_MAX 1023 + +static bool force; +module_param(force, bool, 0444); +MODULE_PARM_DESC(force, "Force loading EC driver without checking DMI boardname"); + +/* A sensor's metadata (label, scale, and register) */ +struct pwec_sensor_prop { + const char *label; + u8 reg; + u32 scale; +}; + +/* Master configuration with properties for all possible sensors */ +static const struct { + const struct pwec_sensor_prop temp_props[PORTWELL_HWMON_TEMP_NUM]; + const struct pwec_sensor_prop in_props[PORTWELL_HWMON_VOLT_NUM]; +} pwec_master_data = { + .temp_props = { + { "CPU Temperature", 0x00, 0 }, + { "System Temperature", 0x02, 0 }, + { "Aux Temperature", 0x04, 0 }, + }, + .in_props = { + { "Vcore", 0x20, 3000 }, + { "3.3V", 0x22, 6000 }, + { "5V", 0x24, 9600 }, + { "12V", 0x30, 19800 }, + { "VDIMM", 0x32, 3000 }, + }, +}; + +struct pwec_board_info { + u32 temp_mask; /* bit N = temperature channel N */ + u32 in_mask; /* bit N = voltage channel N */ +}; + +static const struct pwec_board_info pwec_board_info_default = { + .temp_mask = GENMASK(PORTWELL_HWMON_TEMP_NUM - 1, 0), + .in_mask = GENMASK(PORTWELL_HWMON_VOLT_NUM - 1, 0), +}; + +static const struct pwec_board_info pwec_board_info_nano = { + .temp_mask = BIT(0) | BIT(1), + .in_mask = GENMASK(4, 0), +}; + +static const struct dmi_system_id pwec_dmi_table[] = { + { + .ident = "NANO-6064 series", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "NANO-6064"), + }, + .driver_data = (void *)&pwec_board_info_nano, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, pwec_dmi_table); + +/* Functions for access EC via IOSPACE */ + +static void pwec_write(u8 index, u8 data) +{ + outb(data, PORTWELL_EC_IOSPACE + index); +} + +static u8 pwec_read(u8 address) +{ + return inb(PORTWELL_EC_IOSPACE + address); +} + +/* Ensure consistent 16-bit read across potential MSB rollover. */ +static u16 pwec_read16_stable(u8 lsb_reg) +{ + u8 lsb, msb, old_msb; + + do { + old_msb = pwec_read(lsb_reg + 1); + lsb = pwec_read(lsb_reg); + msb = pwec_read(lsb_reg + 1); + } while (msb != old_msb); + + return (msb << 8) | lsb; +} + +/* GPIO functions */ + +static int pwec_gpio_get(struct gpio_chip *chip, unsigned int offset) +{ + return pwec_read(PORTWELL_GPIO_VAL_REG) & BIT(offset) ? 1 : 0; +} + +static int pwec_gpio_set(struct gpio_chip *chip, unsigned int offset, int val) +{ + u8 tmp = pwec_read(PORTWELL_GPIO_VAL_REG); + + if (val) + tmp |= BIT(offset); + else + tmp &= ~BIT(offset); + pwec_write(PORTWELL_GPIO_VAL_REG, tmp); + + return 0; +} + +static int pwec_gpio_get_direction(struct gpio_chip *chip, unsigned int offset) +{ + u8 direction = pwec_read(PORTWELL_GPIO_DIR_REG) & BIT(offset); + + if (direction) + return GPIO_LINE_DIRECTION_IN; + + return GPIO_LINE_DIRECTION_OUT; +} + +/* + * Changing direction causes issues on some boards, + * so direction_input and direction_output are disabled for now. + */ + +static int pwec_gpio_direction_input(struct gpio_chip *gc, unsigned int offset) +{ + return -EOPNOTSUPP; +} + +static int pwec_gpio_direction_output(struct gpio_chip *gc, unsigned int offset, int value) +{ + return -EOPNOTSUPP; +} + +static struct gpio_chip pwec_gpio_chip = { + .label = "portwell-ec-gpio", + .get_direction = pwec_gpio_get_direction, + .direction_input = pwec_gpio_direction_input, + .direction_output = pwec_gpio_direction_output, + .get = pwec_gpio_get, + .set = pwec_gpio_set, + .base = -1, + .ngpio = PORTWELL_GPIO_PINS, +}; + +/* Watchdog functions */ + +static void pwec_wdt_write_timeout(unsigned int timeout) +{ + pwec_write(PORTWELL_WDT_EC_COUNT_MIN_ADDR, timeout / 60); + pwec_write(PORTWELL_WDT_EC_COUNT_SEC_ADDR, timeout % 60); +} + +static int pwec_wdt_trigger(struct watchdog_device *wdd) +{ + pwec_wdt_write_timeout(wdd->timeout); + pwec_write(PORTWELL_WDT_EC_CONFIG_ADDR, PORTWELL_WDT_CONFIG_ENABLE); + + return 0; +} + +static int pwec_wdt_start(struct watchdog_device *wdd) +{ + return pwec_wdt_trigger(wdd); +} + +static int pwec_wdt_stop(struct watchdog_device *wdd) +{ + pwec_write(PORTWELL_WDT_EC_CONFIG_ADDR, PORTWELL_WDT_CONFIG_DISABLE); + return 0; +} + +static int pwec_wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout) +{ + wdd->timeout = timeout; + pwec_wdt_write_timeout(wdd->timeout); + + return 0; +} + +/* Ensure consistent min/sec read in case of second rollover. */ +static unsigned int pwec_wdt_get_timeleft(struct watchdog_device *wdd) +{ + u8 sec, min, old_min; + + do { + old_min = pwec_read(PORTWELL_WDT_EC_COUNT_MIN_ADDR); + sec = pwec_read(PORTWELL_WDT_EC_COUNT_SEC_ADDR); + min = pwec_read(PORTWELL_WDT_EC_COUNT_MIN_ADDR); + } while (min != old_min); + + return min * 60 + sec; +} + +static const struct watchdog_ops pwec_wdt_ops = { + .owner = THIS_MODULE, + .start = pwec_wdt_start, + .stop = pwec_wdt_stop, + .ping = pwec_wdt_trigger, + .set_timeout = pwec_wdt_set_timeout, + .get_timeleft = pwec_wdt_get_timeleft, +}; + +static struct watchdog_device ec_wdt_dev = { + .info = &(struct watchdog_info){ + .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE, + .identity = "Portwell EC watchdog", + }, + .ops = &pwec_wdt_ops, + .timeout = 60, + .min_timeout = 1, + .max_timeout = PORTWELL_WDT_EC_MAX_COUNT_SECOND, +}; + +/* HWMON functions */ + +static umode_t pwec_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct pwec_board_info *info = drvdata; + + switch (type) { + case hwmon_temp: + return (info->temp_mask & BIT(channel)) ? 0444 : 0; + case hwmon_in: + return (info->in_mask & BIT(channel)) ? 0444 : 0; + default: + return 0; + } +} + +static int pwec_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + u16 tmp16; + + switch (type) { + case hwmon_temp: + *val = pwec_read(pwec_master_data.temp_props[channel].reg) * MILLIDEGREE_PER_DEGREE; + return 0; + case hwmon_in: + tmp16 = pwec_read16_stable(pwec_master_data.in_props[channel].reg); + *val = (tmp16 * pwec_master_data.in_props[channel].scale) / PORTWELL_EC_ADC_MAX; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int pwec_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + switch (type) { + case hwmon_temp: + *str = pwec_master_data.temp_props[channel].label; + return 0; + case hwmon_in: + *str = pwec_master_data.in_props[channel].label; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_channel_info *pwec_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(in, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL), + NULL +}; + +static const struct hwmon_ops pwec_hwmon_ops = { + .is_visible = pwec_hwmon_is_visible, + .read = pwec_hwmon_read, + .read_string = pwec_hwmon_read_string, +}; + +static const struct hwmon_chip_info pwec_chip_info = { + .ops = &pwec_hwmon_ops, + .info = pwec_hwmon_info, +}; + +static int pwec_firmware_vendor_check(void) +{ + u8 buf[PORTWELL_EC_FW_VENDOR_LENGTH + 1]; + u8 i; + + for (i = 0; i < PORTWELL_EC_FW_VENDOR_LENGTH; i++) + buf[i] = pwec_read(PORTWELL_EC_FW_VENDOR_ADDRESS + i); + buf[PORTWELL_EC_FW_VENDOR_LENGTH] = '\0'; + + return !strcmp(PORTWELL_EC_FW_VENDOR_NAME, buf) ? 0 : -ENODEV; +} + +static int pwec_probe(struct platform_device *pdev) +{ + struct device *hwmon_dev; + void *drvdata = dev_get_platdata(&pdev->dev); + int ret; + + if (!devm_request_region(&pdev->dev, PORTWELL_EC_IOSPACE, + PORTWELL_EC_IOSPACE_LEN, dev_name(&pdev->dev))) { + dev_err(&pdev->dev, "failed to get IO region\n"); + return -EBUSY; + } + + ret = pwec_firmware_vendor_check(); + if (ret < 0) + return ret; + + ret = devm_gpiochip_add_data(&pdev->dev, &pwec_gpio_chip, NULL); + if (ret < 0) { + dev_err(&pdev->dev, "failed to register Portwell EC GPIO\n"); + return ret; + } + + if (IS_REACHABLE(CONFIG_HWMON)) { + hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, + "portwell_ec", drvdata, &pwec_chip_info, NULL); + ret = PTR_ERR_OR_ZERO(hwmon_dev); + if (ret) + return ret; + } + + ec_wdt_dev.parent = &pdev->dev; + return devm_watchdog_register_device(&pdev->dev, &ec_wdt_dev); +} + +static int pwec_suspend(struct device *dev) +{ + if (watchdog_active(&ec_wdt_dev)) + return pwec_wdt_stop(&ec_wdt_dev); + + return 0; +} + +static int pwec_resume(struct device *dev) +{ + if (watchdog_active(&ec_wdt_dev)) + return pwec_wdt_start(&ec_wdt_dev); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(pwec_dev_pm_ops, pwec_suspend, pwec_resume); + +static struct platform_driver pwec_driver = { + .driver = { + .name = "portwell-ec", + .pm = pm_sleep_ptr(&pwec_dev_pm_ops), + }, + .probe = pwec_probe, +}; + +static struct platform_device *pwec_dev; + +static int __init pwec_init(void) +{ + const struct dmi_system_id *match; + const struct pwec_board_info *hwmon_data; + int ret; + + match = dmi_first_match(pwec_dmi_table); + if (!match) { + if (!force) + return -ENODEV; + hwmon_data = &pwec_board_info_default; + pr_warn("force load portwell-ec without DMI check, using full display config\n"); + } else { + hwmon_data = match->driver_data; + } + + ret = platform_driver_register(&pwec_driver); + if (ret) + return ret; + + pwec_dev = platform_device_register_data(NULL, "portwell-ec", PLATFORM_DEVID_NONE, + hwmon_data, sizeof(*hwmon_data)); + if (IS_ERR(pwec_dev)) { + platform_driver_unregister(&pwec_driver); + return PTR_ERR(pwec_dev); + } + + return 0; +} + +static void __exit pwec_exit(void) +{ + platform_device_unregister(pwec_dev); + platform_driver_unregister(&pwec_driver); +} + +module_init(pwec_init); +module_exit(pwec_exit); + +MODULE_AUTHOR("Yen-Chi Huang "); +MODULE_DESCRIPTION("Portwell EC Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/quickstart.c b/drivers/platform/x86/quickstart.c new file mode 100644 index 000000000..186a243a0 --- /dev/null +++ b/drivers/platform/x86/quickstart.c @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ACPI Direct App Launch driver + * + * Copyright (C) 2024 Armin Wolf + * Copyright (C) 2022 Arvid Norlander + * Copyright (C) 2007-2010 Angelo Arrifano + * + * Information gathered from disassembled dsdt and from here: + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DRIVER_NAME "quickstart" + +/* + * There will be two events: + * 0x02 - Button was pressed while device was off/sleeping. + * 0x80 - Button was pressed while device was up. + */ +#define QUICKSTART_EVENT_RUNTIME 0x80 + +struct quickstart_data { + struct device *dev; + struct mutex input_lock; /* Protects input sequence during notify */ + struct input_dev *input_device; + char input_name[32]; + char phys[32]; + u32 id; +}; + +/* + * Knowing what these buttons do require system specific knowledge. + * This could be done by matching on DMI data in a long quirk table. + * However, it is easier to leave it up to user space to figure this out. + * + * Using for example udev hwdb the scancode 0x1 can be remapped suitably. + */ +static const struct key_entry quickstart_keymap[] = { + { KE_KEY, 0x1, { KEY_UNKNOWN } }, + { KE_END, 0 }, +}; + +static ssize_t button_id_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct quickstart_data *data = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", data->id); +} +static DEVICE_ATTR_RO(button_id); + +static struct attribute *quickstart_attrs[] = { + &dev_attr_button_id.attr, + NULL +}; +ATTRIBUTE_GROUPS(quickstart); + +static void quickstart_notify(acpi_handle handle, u32 event, void *context) +{ + struct quickstart_data *data = context; + + switch (event) { + case QUICKSTART_EVENT_RUNTIME: + mutex_lock(&data->input_lock); + sparse_keymap_report_event(data->input_device, 0x1, 1, true); + mutex_unlock(&data->input_lock); + + acpi_bus_generate_netlink_event(DRIVER_NAME, dev_name(data->dev), event, 0); + break; + default: + dev_err(data->dev, FW_INFO "Unexpected ACPI notify event (%u)\n", event); + break; + } +} + +/* + * The GHID ACPI method is used to indicate the "role" of the button. + * However, all the meanings of these values are vendor defined. + * + * We do however expose this value to user space. + */ +static int quickstart_get_ghid(struct quickstart_data *data) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_handle handle = ACPI_HANDLE(data->dev); + union acpi_object *obj; + acpi_status status; + int ret = 0; + + /* + * This returns a buffer telling the button usage ID, + * and triggers pending notify events (The ones before booting). + */ + status = acpi_evaluate_object_typed(handle, "GHID", NULL, &buffer, ACPI_TYPE_BUFFER); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = buffer.pointer; + if (!obj) + return -ENODATA; + + /* + * Quoting the specification: + * "The GHID method can return a BYTE, WORD, or DWORD. + * The value must be encoded in little-endian byte + * order (least significant byte first)." + */ + switch (obj->buffer.length) { + case 1: + data->id = obj->buffer.pointer[0]; + break; + case 2: + data->id = get_unaligned_le16(obj->buffer.pointer); + break; + case 4: + data->id = get_unaligned_le32(obj->buffer.pointer); + break; + default: + dev_err(data->dev, + FW_BUG "GHID method returned buffer of unexpected length %u\n", + obj->buffer.length); + ret = -EIO; + break; + } + + kfree(obj); + + return ret; +} + +static void quickstart_notify_remove(void *context) +{ + struct quickstart_data *data = context; + acpi_handle handle; + + handle = ACPI_HANDLE(data->dev); + + acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, quickstart_notify); +} + +static int quickstart_probe(struct platform_device *pdev) +{ + struct quickstart_data *data; + acpi_handle handle; + acpi_status status; + int ret; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = &pdev->dev; + dev_set_drvdata(&pdev->dev, data); + + ret = devm_mutex_init(&pdev->dev, &data->input_lock); + if (ret < 0) + return ret; + + /* + * We have to initialize the device wakeup before evaluating GHID because + * doing so will notify the device if the button was used to wake the machine + * from S5. + */ + device_init_wakeup(&pdev->dev, true); + + ret = quickstart_get_ghid(data); + if (ret < 0) + return ret; + + data->input_device = devm_input_allocate_device(&pdev->dev); + if (!data->input_device) + return -ENOMEM; + + ret = sparse_keymap_setup(data->input_device, quickstart_keymap, NULL); + if (ret < 0) + return ret; + + snprintf(data->input_name, sizeof(data->input_name), "Quickstart Button %u", data->id); + snprintf(data->phys, sizeof(data->phys), DRIVER_NAME "/input%u", data->id); + + data->input_device->name = data->input_name; + data->input_device->phys = data->phys; + data->input_device->id.bustype = BUS_HOST; + + ret = input_register_device(data->input_device); + if (ret < 0) + return ret; + + status = acpi_install_notify_handler(handle, ACPI_DEVICE_NOTIFY, quickstart_notify, data); + if (ACPI_FAILURE(status)) + return -EIO; + + return devm_add_action_or_reset(&pdev->dev, quickstart_notify_remove, data); +} + +static const struct acpi_device_id quickstart_device_ids[] = { + { "PNP0C32" }, + { } +}; +MODULE_DEVICE_TABLE(acpi, quickstart_device_ids); + +static struct platform_driver quickstart_platform_driver = { + .driver = { + .name = DRIVER_NAME, + .dev_groups = quickstart_groups, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .acpi_match_table = quickstart_device_ids, + }, + .probe = quickstart_probe, +}; +module_platform_driver(quickstart_platform_driver); + +MODULE_AUTHOR("Armin Wolf "); +MODULE_AUTHOR("Arvid Norlander "); +MODULE_AUTHOR("Angelo Arrifano"); +MODULE_DESCRIPTION("ACPI Direct App Launch driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/redmi-wmi.c b/drivers/platform/x86/redmi-wmi.c new file mode 100644 index 000000000..58898630e --- /dev/null +++ b/drivers/platform/x86/redmi-wmi.c @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0 +/* WMI driver for Xiaomi Redmibooks */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define WMI_REDMIBOOK_KEYBOARD_EVENT_GUID "46C93E13-EE9B-4262-8488-563BCA757FEF" + +#define AI_KEY_VALUE_MASK BIT(8) + +static const struct key_entry redmi_wmi_keymap[] = { + {KE_KEY, 0x00000201, {KEY_SELECTIVE_SCREENSHOT}}, + {KE_KEY, 0x00000301, {KEY_ALL_APPLICATIONS}}, + {KE_KEY, 0x00001b01, {KEY_CONFIG}}, + {KE_KEY, 0x00011b01, {KEY_CONFIG}}, + {KE_KEY, 0x00010101, {KEY_SWITCHVIDEOMODE}}, + {KE_KEY, 0x00001a01, {KEY_REFRESH_RATE_TOGGLE}}, + + /* AI button has code for each position */ + {KE_KEY, 0x00011801, {KEY_ASSISTANT}}, + {KE_KEY, 0x00011901, {KEY_ASSISTANT}}, + + /* Keyboard backlight */ + {KE_IGNORE, 0x00000501, {}}, + {KE_IGNORE, 0x00800501, {}}, + {KE_IGNORE, 0x00050501, {}}, + {KE_IGNORE, 0x000a0501, {}}, + + /* Xiaomi G Command Center */ + {KE_KEY, 0x00010a01, {KEY_VENDOR}}, + + /* OEM preset power mode */ + {KE_IGNORE, 0x00011601, {}}, + {KE_IGNORE, 0x00021601, {}}, + {KE_IGNORE, 0x00031601, {}}, + {KE_IGNORE, 0x00041601, {}}, + + /* Fn Lock state */ + {KE_IGNORE, 0x00000701, {}}, + {KE_IGNORE, 0x00010701, {}}, + + /* Fn+`/1/2/3/4 */ + {KE_KEY, 0x00011101, {KEY_F13}}, + {KE_KEY, 0x00011201, {KEY_F14}}, + {KE_KEY, 0x00011301, {KEY_F15}}, + {KE_KEY, 0x00011401, {KEY_F16}}, + {KE_KEY, 0x00011501, {KEY_F17}}, + + {KE_END} +}; + +struct redmi_wmi { + struct input_dev *input_dev; + /* Protects the key event sequence */ + struct mutex key_lock; +}; + +static int redmi_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct redmi_wmi *data; + int err; + + /* Init dev */ + data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, data); + + err = devm_mutex_init(&wdev->dev, &data->key_lock); + if (err) + return err; + + data->input_dev = devm_input_allocate_device(&wdev->dev); + if (!data->input_dev) + return -ENOMEM; + + data->input_dev->name = "Redmibook WMI keys"; + data->input_dev->phys = "wmi/input0"; + + err = sparse_keymap_setup(data->input_dev, redmi_wmi_keymap, NULL); + if (err) + return err; + + return input_register_device(data->input_dev); +} + +static void redmi_wmi_notify(struct wmi_device *wdev, union acpi_object *obj) +{ + struct key_entry *entry; + struct redmi_wmi *data = dev_get_drvdata(&wdev->dev); + bool autorelease = true; + u32 payload; + int value = 1; + + if (obj->type != ACPI_TYPE_BUFFER) { + dev_err(&wdev->dev, "Bad response type %u\n", obj->type); + return; + } + + if (obj->buffer.length < 32) { + dev_err(&wdev->dev, "Invalid buffer length %u\n", obj->buffer.length); + return; + } + + payload = get_unaligned_le32(obj->buffer.pointer); + entry = sparse_keymap_entry_from_scancode(data->input_dev, payload); + + if (!entry) { + dev_dbg(&wdev->dev, "Unknown WMI event with payload %u", payload); + return; + } + + /* AI key quirk */ + if (entry->keycode == KEY_ASSISTANT) { + value = !(payload & AI_KEY_VALUE_MASK); + autorelease = false; + } + + guard(mutex)(&data->key_lock); + sparse_keymap_report_entry(data->input_dev, entry, value, autorelease); +} + +static const struct wmi_device_id redmi_wmi_id_table[] = { + { WMI_REDMIBOOK_KEYBOARD_EVENT_GUID, NULL }, + { } +}; + +static struct wmi_driver redmi_wmi_driver = { + .driver = { + .name = "redmi-wmi", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = redmi_wmi_id_table, + .min_event_size = 32, + .probe = redmi_wmi_probe, + .notify = redmi_wmi_notify, + .no_singleton = true, +}; +module_wmi_driver(redmi_wmi_driver); + +MODULE_DEVICE_TABLE(wmi, redmi_wmi_id_table); +MODULE_AUTHOR("Gladyshev Ilya "); +MODULE_DESCRIPTION("Redmibook WMI driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/samsung-galaxybook.c b/drivers/platform/x86/samsung-galaxybook.c new file mode 100644 index 000000000..6382af0b1 --- /dev/null +++ b/drivers/platform/x86/samsung-galaxybook.c @@ -0,0 +1,1456 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Samsung Galaxy Book driver + * + * Copyright (c) 2025 Joshua Grisham + * + * With contributions to the SCAI ACPI device interface: + * Copyright (c) 2024 Giulio Girardi + * + * Implementation inspired by existing x86 platform drivers. + * Thank you to the authors! + */ + +#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 "samsung-galaxybook" + +struct samsung_galaxybook { + struct platform_device *platform; + struct acpi_device *acpi; + + struct device *fw_attrs_dev; + struct kset *fw_attrs_kset; + /* block in case firmware attributes are updated in multiple threads */ + struct mutex fw_attr_lock; + + bool has_kbd_backlight; + bool has_block_recording; + bool has_performance_mode; + + struct led_classdev kbd_backlight; + struct work_struct kbd_backlight_hotkey_work; + /* block in case brightness updated using hotkey and another thread */ + struct mutex kbd_backlight_lock; + + void *i8042_filter_ptr; + + struct work_struct block_recording_hotkey_work; + struct input_dev *input; + + struct acpi_battery_hook battery_hook; + + u8 profile_performance_modes[PLATFORM_PROFILE_LAST]; +}; + +enum galaxybook_fw_attr_id { + GB_ATTR_POWER_ON_LID_OPEN, + GB_ATTR_USB_CHARGING, + GB_ATTR_BLOCK_RECORDING, +}; + +static const char * const galaxybook_fw_attr_name[] = { + [GB_ATTR_POWER_ON_LID_OPEN] = "power_on_lid_open", + [GB_ATTR_USB_CHARGING] = "usb_charging", + [GB_ATTR_BLOCK_RECORDING] = "block_recording", +}; + +static const char * const galaxybook_fw_attr_desc[] = { + [GB_ATTR_POWER_ON_LID_OPEN] = "Power On Lid Open", + [GB_ATTR_USB_CHARGING] = "USB Charging", + [GB_ATTR_BLOCK_RECORDING] = "Block Recording", +}; + +#define GB_ATTR_LANGUAGE_CODE "en_US.UTF-8" + +struct galaxybook_fw_attr { + struct samsung_galaxybook *galaxybook; + enum galaxybook_fw_attr_id fw_attr_id; + struct attribute_group attr_group; + struct kobj_attribute display_name; + struct kobj_attribute current_value; + int (*get_value)(struct samsung_galaxybook *galaxybook, bool *value); + int (*set_value)(struct samsung_galaxybook *galaxybook, const bool value); +}; + +struct sawb { + u16 safn; + u16 sasb; + u8 rflg; + union { + struct { + u8 gunm; + u8 guds[250]; + } __packed; + struct { + u8 caid[16]; + u8 fncn; + u8 subn; + u8 iob0; + u8 iob1; + u8 iob2; + u8 iob3; + u8 iob4; + u8 iob5; + u8 iob6; + u8 iob7; + u8 iob8; + u8 iob9; + } __packed; + struct { + u8 iob_prefix[18]; + u8 iobs[10]; + } __packed; + } __packed; +} __packed; + +#define GB_SAWB_LEN_SETTINGS 0x15 +#define GB_SAWB_LEN_PERFORMANCE_MODE 0x100 + +#define GB_SAFN 0x5843 + +#define GB_SASB_KBD_BACKLIGHT 0x78 +#define GB_SASB_POWER_MANAGEMENT 0x7a +#define GB_SASB_USB_CHARGING_GET 0x67 +#define GB_SASB_USB_CHARGING_SET 0x68 +#define GB_SASB_NOTIFICATIONS 0x86 +#define GB_SASB_BLOCK_RECORDING 0x8a +#define GB_SASB_PERFORMANCE_MODE 0x91 + +#define GB_SAWB_RFLG_POS 4 +#define GB_SAWB_GB_GUNM_POS 5 + +#define GB_RFLG_SUCCESS 0xaa +#define GB_GUNM_FAIL 0xff + +#define GB_GUNM_FEATURE_ENABLE 0xbb +#define GB_GUNM_FEATURE_ENABLE_SUCCESS 0xdd +#define GB_GUDS_FEATURE_ENABLE 0xaa +#define GB_GUDS_FEATURE_ENABLE_SUCCESS 0xcc + +#define GB_GUNM_GET 0x81 +#define GB_GUNM_SET 0x82 + +#define GB_GUNM_POWER_MANAGEMENT 0x82 + +#define GB_GUNM_USB_CHARGING_GET 0x80 +#define GB_GUNM_USB_CHARGING_ON 0x81 +#define GB_GUNM_USB_CHARGING_OFF 0x80 +#define GB_GUDS_POWER_ON_LID_OPEN 0xa3 +#define GB_GUDS_POWER_ON_LID_OPEN_GET 0x81 +#define GB_GUDS_POWER_ON_LID_OPEN_SET 0x80 +#define GB_GUDS_BATTERY_CHARGE_CONTROL 0xe9 +#define GB_GUDS_BATTERY_CHARGE_CONTROL_GET 0x91 +#define GB_GUDS_BATTERY_CHARGE_CONTROL_SET 0x90 +#define GB_GUNM_ACPI_NOTIFY_ENABLE 0x80 +#define GB_GUDS_ACPI_NOTIFY_ENABLE 0x02 + +#define GB_BLOCK_RECORDING_ON 0x0 +#define GB_BLOCK_RECORDING_OFF 0x1 + +#define GB_FNCN_PERFORMANCE_MODE 0x51 +#define GB_SUBN_PERFORMANCE_MODE_LIST 0x01 +#define GB_SUBN_PERFORMANCE_MODE_GET 0x02 +#define GB_SUBN_PERFORMANCE_MODE_SET 0x03 + +/* guid 8246028d-8bca-4a55-ba0f-6f1e6b921b8f */ +static const guid_t performance_mode_guid = + GUID_INIT(0x8246028d, 0x8bca, 0x4a55, 0xba, 0x0f, 0x6f, 0x1e, 0x6b, 0x92, 0x1b, 0x8f); +#define GB_PERFORMANCE_MODE_GUID performance_mode_guid + +#define GB_PERFORMANCE_MODE_FANOFF 0xb +#define GB_PERFORMANCE_MODE_LOWNOISE 0xa +#define GB_PERFORMANCE_MODE_OPTIMIZED 0x0 +#define GB_PERFORMANCE_MODE_OPTIMIZED_V2 0x2 +#define GB_PERFORMANCE_MODE_PERFORMANCE 0x1 +#define GB_PERFORMANCE_MODE_PERFORMANCE_V2 0x15 +#define GB_PERFORMANCE_MODE_ULTRA 0x16 +#define GB_PERFORMANCE_MODE_IGNORE1 0x14 +#define GB_PERFORMANCE_MODE_IGNORE2 0xc + +#define GB_ACPI_METHOD_ENABLE "SDLS" +#define GB_ACPI_METHOD_ENABLE_ON 1 +#define GB_ACPI_METHOD_ENABLE_OFF 0 +#define GB_ACPI_METHOD_SETTINGS "CSFI" +#define GB_ACPI_METHOD_PERFORMANCE_MODE "CSXI" + +#define GB_KBD_BACKLIGHT_MAX_BRIGHTNESS 3 + +#define GB_ACPI_NOTIFY_BATTERY_STATE_CHANGED 0x61 +#define GB_ACPI_NOTIFY_DEVICE_ON_TABLE 0x6c +#define GB_ACPI_NOTIFY_DEVICE_OFF_TABLE 0x6d +#define GB_ACPI_NOTIFY_HOTKEY_PERFORMANCE_MODE 0x70 +#define GB_ACPI_NOTIFY_HOTKEY_KBD_BACKLIGHT 0x7d +#define GB_ACPI_NOTIFY_HOTKEY_MICMUTE 0x6e +#define GB_ACPI_NOTIFY_HOTKEY_CAMERA 0x6f + +#define GB_KEY_KBD_BACKLIGHT_KEYDOWN 0x2c +#define GB_KEY_KBD_BACKLIGHT_KEYUP 0xac +#define GB_KEY_BLOCK_RECORDING_KEYDOWN 0x1f +#define GB_KEY_BLOCK_RECORDING_KEYUP 0x9f +#define GB_KEY_BATTERY_NOTIFY_KEYUP 0xf +#define GB_KEY_BATTERY_NOTIFY_KEYDOWN 0x8f + +/* + * Optional features which have been determined as not supported on a particular + * device will return GB_NOT_SUPPORTED from their init function. Positive + * EOPNOTSUPP is used as the underlying value instead of negative to + * differentiate this return code from valid upstream failures. + */ +#define GB_NOT_SUPPORTED EOPNOTSUPP /* Galaxy Book feature not supported */ + +/* + * ACPI method handling + */ + +static int galaxybook_acpi_method(struct samsung_galaxybook *galaxybook, acpi_string method, + struct sawb *buf, size_t len) +{ + struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object in_obj, *out_obj; + struct acpi_object_list input; + acpi_status status; + int err; + + in_obj.type = ACPI_TYPE_BUFFER; + in_obj.buffer.length = len; + in_obj.buffer.pointer = (u8 *)buf; + + input.count = 1; + input.pointer = &in_obj; + + status = acpi_evaluate_object_typed(galaxybook->acpi->handle, method, &input, &output, + ACPI_TYPE_BUFFER); + + if (ACPI_FAILURE(status)) { + dev_err(&galaxybook->acpi->dev, "failed to execute method %s; got %s\n", + method, acpi_format_exception(status)); + return -EIO; + } + + out_obj = output.pointer; + + if (out_obj->buffer.length != len || out_obj->buffer.length < GB_SAWB_GB_GUNM_POS + 1) { + dev_err(&galaxybook->acpi->dev, + "failed to execute %s; response length mismatch\n", + method); + err = -EPROTO; + goto out_free; + } + if (out_obj->buffer.pointer[GB_SAWB_RFLG_POS] != GB_RFLG_SUCCESS) { + dev_err(&galaxybook->acpi->dev, + "failed to execute %s; device did not respond with success code 0x%x\n", + method, GB_RFLG_SUCCESS); + err = -ENXIO; + goto out_free; + } + if (out_obj->buffer.pointer[GB_SAWB_GB_GUNM_POS] == GB_GUNM_FAIL) { + dev_err(&galaxybook->acpi->dev, + "failed to execute %s; device responded with failure code 0x%x\n", + method, GB_GUNM_FAIL); + err = -ENXIO; + goto out_free; + } + + memcpy(buf, out_obj->buffer.pointer, len); + err = 0; + +out_free: + kfree(out_obj); + return err; +} + +static int galaxybook_enable_acpi_feature(struct samsung_galaxybook *galaxybook, const u16 sasb) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = sasb; + buf.gunm = GB_GUNM_FEATURE_ENABLE; + buf.guds[0] = GB_GUDS_FEATURE_ENABLE; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + if (buf.gunm != GB_GUNM_FEATURE_ENABLE_SUCCESS && + buf.guds[0] != GB_GUDS_FEATURE_ENABLE_SUCCESS) + return -ENODEV; + + return 0; +} + +/* + * Keyboard Backlight + */ + +static int kbd_backlight_acpi_get(struct samsung_galaxybook *galaxybook, + enum led_brightness *brightness) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_KBD_BACKLIGHT; + buf.gunm = GB_GUNM_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + *brightness = buf.gunm; + + return 0; +} + +static int kbd_backlight_acpi_set(struct samsung_galaxybook *galaxybook, + const enum led_brightness brightness) +{ + struct sawb buf = {}; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_KBD_BACKLIGHT; + buf.gunm = GB_GUNM_SET; + + buf.guds[0] = brightness; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); +} + +static enum led_brightness kbd_backlight_show(struct led_classdev *led) +{ + struct samsung_galaxybook *galaxybook = + container_of(led, struct samsung_galaxybook, kbd_backlight); + enum led_brightness brightness; + int err; + + err = kbd_backlight_acpi_get(galaxybook, &brightness); + if (err) + return err; + + return brightness; +} + +static int kbd_backlight_store(struct led_classdev *led, + const enum led_brightness brightness) +{ + struct samsung_galaxybook *galaxybook = + container_of_const(led, struct samsung_galaxybook, kbd_backlight); + + return kbd_backlight_acpi_set(galaxybook, brightness); +} + +static int galaxybook_kbd_backlight_init(struct samsung_galaxybook *galaxybook) +{ + struct led_init_data init_data = {}; + enum led_brightness brightness; + int err; + + err = devm_mutex_init(&galaxybook->platform->dev, &galaxybook->kbd_backlight_lock); + if (err) + return err; + + err = galaxybook_enable_acpi_feature(galaxybook, GB_SASB_KBD_BACKLIGHT); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to enable kbd_backlight feature, error %d\n", err); + return GB_NOT_SUPPORTED; + } + + err = kbd_backlight_acpi_get(galaxybook, &brightness); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to get initial kbd_backlight brightness, error %d\n", err); + return GB_NOT_SUPPORTED; + } + + init_data.devicename = DRIVER_NAME; + init_data.default_label = ":" LED_FUNCTION_KBD_BACKLIGHT; + init_data.devname_mandatory = true; + + galaxybook->kbd_backlight.brightness_get = kbd_backlight_show; + galaxybook->kbd_backlight.brightness_set_blocking = kbd_backlight_store; + galaxybook->kbd_backlight.flags = LED_BRIGHT_HW_CHANGED; + galaxybook->kbd_backlight.max_brightness = GB_KBD_BACKLIGHT_MAX_BRIGHTNESS; + + return devm_led_classdev_register_ext(&galaxybook->platform->dev, + &galaxybook->kbd_backlight, &init_data); +} + +/* + * Battery Extension (adds charge_control_end_threshold to the battery device) + */ + +static int charge_control_end_threshold_acpi_get(struct samsung_galaxybook *galaxybook, u8 *value) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_POWER_MANAGEMENT; + buf.gunm = GB_GUNM_POWER_MANAGEMENT; + buf.guds[0] = GB_GUDS_BATTERY_CHARGE_CONTROL; + buf.guds[1] = GB_GUDS_BATTERY_CHARGE_CONTROL_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + *value = buf.guds[1]; + + return 0; +} + +static int charge_control_end_threshold_acpi_set(struct samsung_galaxybook *galaxybook, u8 value) +{ + struct sawb buf = {}; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_POWER_MANAGEMENT; + buf.gunm = GB_GUNM_POWER_MANAGEMENT; + buf.guds[0] = GB_GUDS_BATTERY_CHARGE_CONTROL; + buf.guds[1] = GB_GUDS_BATTERY_CHARGE_CONTROL_SET; + buf.guds[2] = value; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); +} + +static int galaxybook_battery_ext_property_get(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct samsung_galaxybook *galaxybook = ext_data; + u8 value; + int err; + + if (psp != POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) + return -EINVAL; + + err = charge_control_end_threshold_acpi_get(galaxybook, &value); + if (err) + return err; + + /* + * device stores "no end threshold" as 0 instead of 100; + * if device has 0, report 100 + */ + if (value == 0) + value = 100; + + val->intval = value; + + return 0; +} + +static int galaxybook_battery_ext_property_set(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct samsung_galaxybook *galaxybook = ext_data; + u8 value; + + if (psp != POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) + return -EINVAL; + + value = val->intval; + + if (value < 1 || value > 100) + return -EINVAL; + + /* + * device stores "no end threshold" as 0 instead of 100; + * if setting to 100, send 0 + */ + if (value == 100) + value = 0; + + return charge_control_end_threshold_acpi_set(galaxybook, value); +} + +static int galaxybook_battery_ext_property_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp) +{ + if (psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) + return true; + + return false; +} + +static const enum power_supply_property galaxybook_battery_properties[] = { + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const struct power_supply_ext galaxybook_battery_ext = { + .name = DRIVER_NAME, + .properties = galaxybook_battery_properties, + .num_properties = ARRAY_SIZE(galaxybook_battery_properties), + .get_property = galaxybook_battery_ext_property_get, + .set_property = galaxybook_battery_ext_property_set, + .property_is_writeable = galaxybook_battery_ext_property_is_writeable, +}; + +static int galaxybook_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct samsung_galaxybook *galaxybook = + container_of(hook, struct samsung_galaxybook, battery_hook); + + return power_supply_register_extension(battery, &galaxybook_battery_ext, + &battery->dev, galaxybook); +} + +static int galaxybook_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &galaxybook_battery_ext); + return 0; +} + +static int galaxybook_battery_threshold_init(struct samsung_galaxybook *galaxybook) +{ + u8 value; + int err; + + err = charge_control_end_threshold_acpi_get(galaxybook, &value); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to get initial battery charge end threshold, error %d\n", err); + return 0; + } + + galaxybook->battery_hook.add_battery = galaxybook_battery_add; + galaxybook->battery_hook.remove_battery = galaxybook_battery_remove; + galaxybook->battery_hook.name = "Samsung Galaxy Book Battery Extension"; + + return devm_battery_hook_register(&galaxybook->platform->dev, &galaxybook->battery_hook); +} + +/* + * Platform Profile / Performance mode + */ + +static int performance_mode_acpi_get(struct samsung_galaxybook *galaxybook, u8 *performance_mode) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_PERFORMANCE_MODE; + export_guid(buf.caid, &GB_PERFORMANCE_MODE_GUID); + buf.fncn = GB_FNCN_PERFORMANCE_MODE; + buf.subn = GB_SUBN_PERFORMANCE_MODE_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_PERFORMANCE_MODE, + &buf, GB_SAWB_LEN_PERFORMANCE_MODE); + if (err) + return err; + + *performance_mode = buf.iob0; + + return 0; +} + +static int performance_mode_acpi_set(struct samsung_galaxybook *galaxybook, + const u8 performance_mode) +{ + struct sawb buf = {}; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_PERFORMANCE_MODE; + export_guid(buf.caid, &GB_PERFORMANCE_MODE_GUID); + buf.fncn = GB_FNCN_PERFORMANCE_MODE; + buf.subn = GB_SUBN_PERFORMANCE_MODE_SET; + buf.iob0 = performance_mode; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_PERFORMANCE_MODE, + &buf, GB_SAWB_LEN_PERFORMANCE_MODE); +} + +static int get_performance_mode_profile(struct samsung_galaxybook *galaxybook, + const u8 performance_mode, + enum platform_profile_option *profile) +{ + switch (performance_mode) { + case GB_PERFORMANCE_MODE_FANOFF: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case GB_PERFORMANCE_MODE_LOWNOISE: + *profile = PLATFORM_PROFILE_QUIET; + break; + case GB_PERFORMANCE_MODE_OPTIMIZED: + case GB_PERFORMANCE_MODE_OPTIMIZED_V2: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case GB_PERFORMANCE_MODE_PERFORMANCE: + case GB_PERFORMANCE_MODE_PERFORMANCE_V2: + case GB_PERFORMANCE_MODE_ULTRA: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case GB_PERFORMANCE_MODE_IGNORE1: + case GB_PERFORMANCE_MODE_IGNORE2: + return -EOPNOTSUPP; + default: + dev_warn(&galaxybook->platform->dev, + "unrecognized performance mode 0x%x\n", performance_mode); + return -EOPNOTSUPP; + } + + return 0; +} + +static int galaxybook_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct samsung_galaxybook *galaxybook = dev_get_drvdata(dev); + u8 performance_mode; + int err; + + err = performance_mode_acpi_get(galaxybook, &performance_mode); + if (err) + return err; + + return get_performance_mode_profile(galaxybook, performance_mode, profile); +} + +static int galaxybook_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct samsung_galaxybook *galaxybook = dev_get_drvdata(dev); + + return performance_mode_acpi_set(galaxybook, + galaxybook->profile_performance_modes[profile]); +} + +static int galaxybook_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + struct samsung_galaxybook *galaxybook = drvdata; + u8 *perfmodes = galaxybook->profile_performance_modes; + enum platform_profile_option profile; + struct sawb buf = {}; + unsigned int i; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_PERFORMANCE_MODE; + export_guid(buf.caid, &GB_PERFORMANCE_MODE_GUID); + buf.fncn = GB_FNCN_PERFORMANCE_MODE; + buf.subn = GB_SUBN_PERFORMANCE_MODE_LIST; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_PERFORMANCE_MODE, + &buf, GB_SAWB_LEN_PERFORMANCE_MODE); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to get supported performance modes, error %d\n", err); + return err; + } + + /* set initial default profile performance mode values */ + perfmodes[PLATFORM_PROFILE_LOW_POWER] = GB_PERFORMANCE_MODE_FANOFF; + perfmodes[PLATFORM_PROFILE_QUIET] = GB_PERFORMANCE_MODE_LOWNOISE; + perfmodes[PLATFORM_PROFILE_BALANCED] = GB_PERFORMANCE_MODE_OPTIMIZED; + perfmodes[PLATFORM_PROFILE_PERFORMANCE] = GB_PERFORMANCE_MODE_PERFORMANCE; + + /* + * Value returned in iob0 will have the number of supported performance + * modes per device. The performance mode values will then be given as a + * list after this (iob1-iobX). Loop through the supported values and + * enable their mapped platform_profile choice, overriding "legacy" + * values along the way if a non-legacy value exists. + */ + for (i = 1; i <= buf.iob0; i++) { + err = get_performance_mode_profile(galaxybook, buf.iobs[i], &profile); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "ignoring unmapped performance mode 0x%x\n", buf.iobs[i]); + continue; + } + switch (buf.iobs[i]) { + case GB_PERFORMANCE_MODE_OPTIMIZED_V2: + perfmodes[profile] = GB_PERFORMANCE_MODE_OPTIMIZED_V2; + break; + case GB_PERFORMANCE_MODE_PERFORMANCE_V2: + /* only update if not already overwritten by Ultra */ + if (perfmodes[profile] != GB_PERFORMANCE_MODE_ULTRA) + perfmodes[profile] = GB_PERFORMANCE_MODE_PERFORMANCE_V2; + break; + case GB_PERFORMANCE_MODE_ULTRA: + perfmodes[profile] = GB_PERFORMANCE_MODE_ULTRA; + break; + default: + break; + } + set_bit(profile, choices); + dev_dbg(&galaxybook->platform->dev, + "setting platform profile %d to use performance mode 0x%x\n", + profile, perfmodes[profile]); + } + + /* initialize performance_mode using balanced's mapped value */ + if (test_bit(PLATFORM_PROFILE_BALANCED, choices)) + return performance_mode_acpi_set(galaxybook, perfmodes[PLATFORM_PROFILE_BALANCED]); + + return 0; +} + +static const struct platform_profile_ops galaxybook_platform_profile_ops = { + .probe = galaxybook_platform_profile_probe, + .profile_get = galaxybook_platform_profile_get, + .profile_set = galaxybook_platform_profile_set, +}; + +static int galaxybook_platform_profile_init(struct samsung_galaxybook *galaxybook) +{ + struct device *platform_profile_dev; + u8 performance_mode; + int err; + + err = performance_mode_acpi_get(galaxybook, &performance_mode); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to get initial performance mode, error %d\n", err); + return GB_NOT_SUPPORTED; + } + + platform_profile_dev = devm_platform_profile_register(&galaxybook->platform->dev, + DRIVER_NAME, galaxybook, + &galaxybook_platform_profile_ops); + + return PTR_ERR_OR_ZERO(platform_profile_dev); +} + +/* + * Firmware Attributes + */ + +/* Power on lid open (device should power on when lid is opened) */ + +static int power_on_lid_open_acpi_get(struct samsung_galaxybook *galaxybook, bool *value) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_POWER_MANAGEMENT; + buf.gunm = GB_GUNM_POWER_MANAGEMENT; + buf.guds[0] = GB_GUDS_POWER_ON_LID_OPEN; + buf.guds[1] = GB_GUDS_POWER_ON_LID_OPEN_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + *value = buf.guds[1]; + + return 0; +} + +static int power_on_lid_open_acpi_set(struct samsung_galaxybook *galaxybook, const bool value) +{ + struct sawb buf = {}; + + lockdep_assert_held(&galaxybook->fw_attr_lock); + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_POWER_MANAGEMENT; + buf.gunm = GB_GUNM_POWER_MANAGEMENT; + buf.guds[0] = GB_GUDS_POWER_ON_LID_OPEN; + buf.guds[1] = GB_GUDS_POWER_ON_LID_OPEN_SET; + buf.guds[2] = value ? 1 : 0; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); +} + +/* USB Charging (USB ports can provide power when device is powered off) */ + +static int usb_charging_acpi_get(struct samsung_galaxybook *galaxybook, bool *value) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_USB_CHARGING_GET; + buf.gunm = GB_GUNM_USB_CHARGING_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + *value = buf.gunm == 1; + + return 0; +} + +static int usb_charging_acpi_set(struct samsung_galaxybook *galaxybook, const bool value) +{ + struct sawb buf = {}; + + lockdep_assert_held(&galaxybook->fw_attr_lock); + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_USB_CHARGING_SET; + buf.gunm = value ? GB_GUNM_USB_CHARGING_ON : GB_GUNM_USB_CHARGING_OFF; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); +} + +/* Block recording (blocks access to camera and microphone) */ + +static int block_recording_acpi_get(struct samsung_galaxybook *galaxybook, bool *value) +{ + struct sawb buf = {}; + int err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_BLOCK_RECORDING; + buf.gunm = GB_GUNM_GET; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + *value = buf.gunm == GB_BLOCK_RECORDING_ON; + + return 0; +} + +static int block_recording_acpi_set(struct samsung_galaxybook *galaxybook, const bool value) +{ + struct sawb buf = {}; + int err; + + lockdep_assert_held(&galaxybook->fw_attr_lock); + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_BLOCK_RECORDING; + buf.gunm = GB_GUNM_SET; + buf.guds[0] = value ? GB_BLOCK_RECORDING_ON : GB_BLOCK_RECORDING_OFF; + + err = galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); + if (err) + return err; + + input_report_switch(galaxybook->input, + SW_CAMERA_LENS_COVER, value ? 1 : 0); + input_sync(galaxybook->input); + + return 0; +} + +static int galaxybook_input_init(struct samsung_galaxybook *galaxybook) +{ + galaxybook->input = devm_input_allocate_device(&galaxybook->platform->dev); + if (!galaxybook->input) + return -ENOMEM; + + galaxybook->input->name = "Samsung Galaxy Book Camera Lens Cover"; + galaxybook->input->phys = DRIVER_NAME "/input0"; + galaxybook->input->id.bustype = BUS_HOST; + + input_set_capability(galaxybook->input, EV_KEY, KEY_MICMUTE); + input_set_capability(galaxybook->input, EV_SW, SW_CAMERA_LENS_COVER); + + return input_register_device(galaxybook->input); +} + +static int galaxybook_block_recording_init(struct samsung_galaxybook *galaxybook) +{ + bool value; + int err; + + err = galaxybook_enable_acpi_feature(galaxybook, GB_SASB_BLOCK_RECORDING); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to initialize block_recording, error %d\n", err); + return GB_NOT_SUPPORTED; + } + + guard(mutex)(&galaxybook->fw_attr_lock); + + err = block_recording_acpi_get(galaxybook, &value); + if (err) { + dev_dbg(&galaxybook->platform->dev, + "failed to get initial block_recording state, error %d\n", err); + return GB_NOT_SUPPORTED; + } + + input_report_switch(galaxybook->input, SW_CAMERA_LENS_COVER, value ? 1 : 0); + input_sync(galaxybook->input); + + return 0; +} + +/* Firmware Attributes setup */ + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "enumeration\n"); +} + +static struct kobj_attribute fw_attr_type = __ATTR_RO(type); + +static ssize_t default_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static struct kobj_attribute fw_attr_default_value = __ATTR_RO(default_value); + +static ssize_t possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0;1\n"); +} + +static struct kobj_attribute fw_attr_possible_values = __ATTR_RO(possible_values); + +static ssize_t display_name_language_code_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", GB_ATTR_LANGUAGE_CODE); +} + +static struct kobj_attribute fw_attr_display_name_language_code = + __ATTR_RO(display_name_language_code); + +static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct galaxybook_fw_attr *fw_attr = + container_of(attr, struct galaxybook_fw_attr, display_name); + + return sysfs_emit(buf, "%s\n", galaxybook_fw_attr_desc[fw_attr->fw_attr_id]); +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct galaxybook_fw_attr *fw_attr = + container_of(attr, struct galaxybook_fw_attr, current_value); + bool value; + int err; + + err = fw_attr->get_value(fw_attr->galaxybook, &value); + if (err) + return err; + + return sysfs_emit(buf, "%u\n", value); +} + +static ssize_t current_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct galaxybook_fw_attr *fw_attr = + container_of(attr, struct galaxybook_fw_attr, current_value); + struct samsung_galaxybook *galaxybook = fw_attr->galaxybook; + bool value; + int err; + + if (!count) + return -EINVAL; + + err = kstrtobool(buf, &value); + if (err) + return err; + + guard(mutex)(&galaxybook->fw_attr_lock); + + err = fw_attr->set_value(galaxybook, value); + if (err) + return err; + + return count; +} + +#define NUM_FW_ATTR_ENUM_ATTRS 6 + +static int galaxybook_fw_attr_init(struct samsung_galaxybook *galaxybook, + const enum galaxybook_fw_attr_id fw_attr_id, + int (*get_value)(struct samsung_galaxybook *galaxybook, + bool *value), + int (*set_value)(struct samsung_galaxybook *galaxybook, + const bool value)) +{ + struct galaxybook_fw_attr *fw_attr; + struct attribute **attrs; + + fw_attr = devm_kzalloc(&galaxybook->platform->dev, sizeof(*fw_attr), GFP_KERNEL); + if (!fw_attr) + return -ENOMEM; + + attrs = devm_kcalloc(&galaxybook->platform->dev, NUM_FW_ATTR_ENUM_ATTRS + 1, + sizeof(*attrs), GFP_KERNEL); + if (!attrs) + return -ENOMEM; + + attrs[0] = &fw_attr_type.attr; + attrs[1] = &fw_attr_default_value.attr; + attrs[2] = &fw_attr_possible_values.attr; + attrs[3] = &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[4] = &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[5] = &fw_attr->current_value.attr; + + attrs[6] = NULL; + + fw_attr->galaxybook = galaxybook; + fw_attr->fw_attr_id = fw_attr_id; + fw_attr->attr_group.name = galaxybook_fw_attr_name[fw_attr_id]; + fw_attr->attr_group.attrs = attrs; + fw_attr->get_value = get_value; + fw_attr->set_value = set_value; + + return sysfs_create_group(&galaxybook->fw_attrs_kset->kobj, &fw_attr->attr_group); +} + +static void galaxybook_kset_unregister(void *data) +{ + struct kset *kset = data; + + kset_unregister(kset); +} + +static void galaxybook_fw_attrs_dev_unregister(void *data) +{ + struct device *fw_attrs_dev = data; + + device_unregister(fw_attrs_dev); +} + +static int galaxybook_fw_attrs_init(struct samsung_galaxybook *galaxybook) +{ + bool value; + int err; + + err = devm_mutex_init(&galaxybook->platform->dev, &galaxybook->fw_attr_lock); + if (err) + return err; + + galaxybook->fw_attrs_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(galaxybook->fw_attrs_dev)) + return PTR_ERR(galaxybook->fw_attrs_dev); + + err = devm_add_action_or_reset(&galaxybook->platform->dev, + galaxybook_fw_attrs_dev_unregister, + galaxybook->fw_attrs_dev); + if (err) + return err; + + galaxybook->fw_attrs_kset = kset_create_and_add("attributes", NULL, + &galaxybook->fw_attrs_dev->kobj); + if (!galaxybook->fw_attrs_kset) + return -ENOMEM; + err = devm_add_action_or_reset(&galaxybook->platform->dev, + galaxybook_kset_unregister, galaxybook->fw_attrs_kset); + if (err) + return err; + + err = power_on_lid_open_acpi_get(galaxybook, &value); + if (!err) { + err = galaxybook_fw_attr_init(galaxybook, + GB_ATTR_POWER_ON_LID_OPEN, + &power_on_lid_open_acpi_get, + &power_on_lid_open_acpi_set); + if (err) + return err; + } + + err = usb_charging_acpi_get(galaxybook, &value); + if (!err) { + err = galaxybook_fw_attr_init(galaxybook, + GB_ATTR_USB_CHARGING, + &usb_charging_acpi_get, + &usb_charging_acpi_set); + if (err) + return err; + } + + err = galaxybook_block_recording_init(galaxybook); + if (err == GB_NOT_SUPPORTED) + return 0; + else if (err) + return err; + + galaxybook->has_block_recording = true; + + return galaxybook_fw_attr_init(galaxybook, + GB_ATTR_BLOCK_RECORDING, + &block_recording_acpi_get, + &block_recording_acpi_set); +} + +/* + * Hotkeys and notifications + */ + +static void galaxybook_kbd_backlight_hotkey_work(struct work_struct *work) +{ + struct samsung_galaxybook *galaxybook = + from_work(galaxybook, work, kbd_backlight_hotkey_work); + int brightness; + int err; + + guard(mutex)(&galaxybook->kbd_backlight_lock); + + brightness = galaxybook->kbd_backlight.brightness; + if (brightness < galaxybook->kbd_backlight.max_brightness) + brightness++; + else + brightness = 0; + + err = led_set_brightness_sync(&galaxybook->kbd_backlight, brightness); + if (err) { + dev_err(&galaxybook->platform->dev, + "failed to set kbd_backlight brightness, error %d\n", err); + return; + } + + led_classdev_notify_brightness_hw_changed(&galaxybook->kbd_backlight, brightness); +} + +static void galaxybook_block_recording_hotkey_work(struct work_struct *work) +{ + struct samsung_galaxybook *galaxybook = + from_work(galaxybook, work, block_recording_hotkey_work); + bool value; + int err; + + guard(mutex)(&galaxybook->fw_attr_lock); + + err = block_recording_acpi_get(galaxybook, &value); + if (err) { + dev_err(&galaxybook->platform->dev, + "failed to get block_recording, error %d\n", err); + return; + } + + err = block_recording_acpi_set(galaxybook, !value); + if (err) + dev_err(&galaxybook->platform->dev, + "failed to set block_recording, error %d\n", err); +} + +static bool galaxybook_i8042_filter(unsigned char data, unsigned char str, struct serio *port, + void *context) +{ + struct samsung_galaxybook *galaxybook = context; + static bool extended; + + if (str & I8042_STR_AUXDATA) + return false; + + if (data == 0xe0) { + extended = true; + return true; + } else if (extended) { + extended = false; + switch (data) { + case GB_KEY_KBD_BACKLIGHT_KEYDOWN: + return true; + case GB_KEY_KBD_BACKLIGHT_KEYUP: + if (galaxybook->has_kbd_backlight) + schedule_work(&galaxybook->kbd_backlight_hotkey_work); + return true; + + case GB_KEY_BLOCK_RECORDING_KEYDOWN: + return true; + case GB_KEY_BLOCK_RECORDING_KEYUP: + if (galaxybook->has_block_recording) + schedule_work(&galaxybook->block_recording_hotkey_work); + return true; + + /* battery notification already sent to battery + SCAI device */ + case GB_KEY_BATTERY_NOTIFY_KEYUP: + case GB_KEY_BATTERY_NOTIFY_KEYDOWN: + return true; + + default: + /* + * Report the previously filtered e0 before continuing + * with the next non-filtered byte. + */ + serio_interrupt(port, 0xe0, 0); + return false; + } + } + + return false; +} + +static void galaxybook_i8042_filter_remove(void *data) +{ + struct samsung_galaxybook *galaxybook = data; + + i8042_remove_filter(galaxybook_i8042_filter); + cancel_work_sync(&galaxybook->kbd_backlight_hotkey_work); + cancel_work_sync(&galaxybook->block_recording_hotkey_work); +} + +static int galaxybook_i8042_filter_install(struct samsung_galaxybook *galaxybook) +{ + int err; + + if (!galaxybook->has_kbd_backlight && !galaxybook->has_block_recording) + return 0; + + INIT_WORK(&galaxybook->kbd_backlight_hotkey_work, + galaxybook_kbd_backlight_hotkey_work); + INIT_WORK(&galaxybook->block_recording_hotkey_work, + galaxybook_block_recording_hotkey_work); + + err = i8042_install_filter(galaxybook_i8042_filter, galaxybook); + if (err) + return err; + + return devm_add_action_or_reset(&galaxybook->platform->dev, + galaxybook_i8042_filter_remove, galaxybook); +} + +/* + * ACPI device setup + */ + +static void galaxybook_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct samsung_galaxybook *galaxybook = data; + + switch (event) { + case GB_ACPI_NOTIFY_BATTERY_STATE_CHANGED: + case GB_ACPI_NOTIFY_DEVICE_ON_TABLE: + case GB_ACPI_NOTIFY_DEVICE_OFF_TABLE: + break; + case GB_ACPI_NOTIFY_HOTKEY_PERFORMANCE_MODE: + if (galaxybook->has_performance_mode) + platform_profile_cycle(); + break; + case GB_ACPI_NOTIFY_HOTKEY_KBD_BACKLIGHT: + if (galaxybook->has_kbd_backlight) + schedule_work(&galaxybook->kbd_backlight_hotkey_work); + break; + case GB_ACPI_NOTIFY_HOTKEY_MICMUTE: + input_report_key(galaxybook->input, KEY_MICMUTE, 1); + input_sync(galaxybook->input); + input_report_key(galaxybook->input, KEY_MICMUTE, 0); + input_sync(galaxybook->input); + break; + case GB_ACPI_NOTIFY_HOTKEY_CAMERA: + if (galaxybook->has_block_recording) { + schedule_work(&galaxybook->block_recording_hotkey_work); + } else { + input_report_switch(galaxybook->input, SW_CAMERA_LENS_COVER, + !test_bit(SW_CAMERA_LENS_COVER, galaxybook->input->sw)); + input_sync(galaxybook->input); + } + break; + default: + dev_warn(&galaxybook->platform->dev, + "unknown ACPI notification event: 0x%x\n", event); + } + + acpi_bus_generate_netlink_event(DRIVER_NAME, dev_name(&galaxybook->platform->dev), + event, 1); +} + +static int galaxybook_enable_acpi_notify(struct samsung_galaxybook *galaxybook) +{ + struct sawb buf = {}; + int err; + + err = galaxybook_enable_acpi_feature(galaxybook, GB_SASB_NOTIFICATIONS); + if (err) + return err; + + buf.safn = GB_SAFN; + buf.sasb = GB_SASB_NOTIFICATIONS; + buf.gunm = GB_GUNM_ACPI_NOTIFY_ENABLE; + buf.guds[0] = GB_GUDS_ACPI_NOTIFY_ENABLE; + + return galaxybook_acpi_method(galaxybook, GB_ACPI_METHOD_SETTINGS, + &buf, GB_SAWB_LEN_SETTINGS); +} + +static void galaxybook_acpi_remove_notify_handler(void *data) +{ + struct samsung_galaxybook *galaxybook = data; + + acpi_remove_notify_handler(galaxybook->acpi->handle, ACPI_ALL_NOTIFY, + galaxybook_acpi_notify); +} + +static void galaxybook_acpi_disable(void *data) +{ + struct samsung_galaxybook *galaxybook = data; + + acpi_execute_simple_method(galaxybook->acpi->handle, + GB_ACPI_METHOD_ENABLE, GB_ACPI_METHOD_ENABLE_OFF); +} + +static int galaxybook_acpi_init(struct samsung_galaxybook *galaxybook) +{ + acpi_status status; + int err; + + status = acpi_execute_simple_method(galaxybook->acpi->handle, GB_ACPI_METHOD_ENABLE, + GB_ACPI_METHOD_ENABLE_ON); + if (ACPI_FAILURE(status)) + return -EIO; + err = devm_add_action_or_reset(&galaxybook->platform->dev, + galaxybook_acpi_disable, galaxybook); + if (err) + return err; + + status = acpi_install_notify_handler(galaxybook->acpi->handle, ACPI_ALL_NOTIFY, + galaxybook_acpi_notify, galaxybook); + if (ACPI_FAILURE(status)) + return -EIO; + err = devm_add_action_or_reset(&galaxybook->platform->dev, + galaxybook_acpi_remove_notify_handler, galaxybook); + if (err) + return err; + + err = galaxybook_enable_acpi_notify(galaxybook); + if (err) + dev_dbg(&galaxybook->platform->dev, "failed to enable ACPI notifications; " + "some hotkeys will not be supported\n"); + + err = galaxybook_enable_acpi_feature(galaxybook, GB_SASB_POWER_MANAGEMENT); + if (err) + dev_dbg(&galaxybook->platform->dev, + "failed to initialize ACPI power management features; " + "many features of this driver will not be available\n"); + + return 0; +} + +/* + * Platform driver + */ + +static int galaxybook_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct samsung_galaxybook *galaxybook; + int err; + + if (!adev) + return -ENODEV; + + galaxybook = devm_kzalloc(&pdev->dev, sizeof(*galaxybook), GFP_KERNEL); + if (!galaxybook) + return -ENOMEM; + + galaxybook->platform = pdev; + galaxybook->acpi = adev; + + /* + * Features must be enabled and initialized in the following order to + * avoid failures seen on certain devices: + * - GB_SASB_POWER_MANAGEMENT (including performance mode) + * - GB_SASB_KBD_BACKLIGHT + * - GB_SASB_BLOCK_RECORDING (as part of fw_attrs init) + */ + + err = galaxybook_acpi_init(galaxybook); + if (err) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize ACPI device\n"); + + err = galaxybook_platform_profile_init(galaxybook); + if (!err) + galaxybook->has_performance_mode = true; + else if (err != GB_NOT_SUPPORTED) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize platform profile\n"); + + err = galaxybook_battery_threshold_init(galaxybook); + if (err) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize battery threshold\n"); + + err = galaxybook_kbd_backlight_init(galaxybook); + if (!err) + galaxybook->has_kbd_backlight = true; + else if (err != GB_NOT_SUPPORTED) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize kbd_backlight\n"); + + err = galaxybook_input_init(galaxybook); + if (err) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize input device\n"); + + err = galaxybook_fw_attrs_init(galaxybook); + if (err) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize firmware-attributes\n"); + + err = galaxybook_i8042_filter_install(galaxybook); + if (err) + return dev_err_probe(&galaxybook->platform->dev, err, + "failed to initialize i8042_filter\n"); + + return 0; +} + +static const struct acpi_device_id galaxybook_device_ids[] = { + { "SAM0426" }, + { "SAM0427" }, + { "SAM0428" }, + { "SAM0429" }, + { "SAM0430" }, + {} +}; +MODULE_DEVICE_TABLE(acpi, galaxybook_device_ids); + +static struct platform_driver galaxybook_platform_driver = { + .driver = { + .name = DRIVER_NAME, + .acpi_match_table = galaxybook_device_ids, + }, + .probe = galaxybook_probe, +}; +module_platform_driver(galaxybook_platform_driver); + +MODULE_AUTHOR("Joshua Grisham "); +MODULE_DESCRIPTION("Samsung Galaxy Book driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/samsung-laptop.c b/drivers/platform/x86/samsung-laptop.c new file mode 100644 index 000000000..710f3d5bf --- /dev/null +++ b/drivers/platform/x86/samsung-laptop.c @@ -0,0 +1,1767 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Samsung Laptop driver + * + * Copyright (C) 2009,2011 Greg Kroah-Hartman (gregkh@suse.de) + * Copyright (C) 2009,2011 Novell Inc. + */ +#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 + +/* + * This driver is needed because a number of Samsung laptops do not hook + * their control settings through ACPI. So we have to poke around in the + * BIOS to do things like brightness values, and "special" key controls. + */ + +/* + * We have 0 - 8 as valid brightness levels. The specs say that level 0 should + * be reserved by the BIOS (which really doesn't make much sense), we tell + * userspace that the value is 0 - 7 and then just tell the hardware 1 - 8 + */ +#define MAX_BRIGHT 0x07 + + +#define SABI_IFACE_MAIN 0x00 +#define SABI_IFACE_SUB 0x02 +#define SABI_IFACE_COMPLETE 0x04 +#define SABI_IFACE_DATA 0x05 + +#define WL_STATUS_WLAN 0x0 +#define WL_STATUS_BT 0x2 + +/* Structure get/set data using sabi */ +struct sabi_data { + union { + struct { + u32 d0; + u32 d1; + u16 d2; + u8 d3; + }; + u8 data[11]; + }; +}; + +struct sabi_header_offsets { + u8 port; + u8 re_mem; + u8 iface_func; + u8 en_mem; + u8 data_offset; + u8 data_segment; +}; + +struct sabi_commands { + /* + * Brightness is 0 - 8, as described above. + * Value 0 is for the BIOS to use + */ + u16 get_brightness; + u16 set_brightness; + + /* + * first byte: + * 0x00 - wireless is off + * 0x01 - wireless is on + * second byte: + * 0x02 - 3G is off + * 0x03 - 3G is on + * TODO, verify 3G is correct, that doesn't seem right... + */ + u16 get_wireless_button; + u16 set_wireless_button; + + /* 0 is off, 1 is on */ + u16 get_backlight; + u16 set_backlight; + + /* + * 0x80 or 0x00 - no action + * 0x81 - recovery key pressed + */ + u16 get_recovery_mode; + u16 set_recovery_mode; + + /* + * on seclinux: 0 is low, 1 is high, + * on swsmi: 0 is normal, 1 is silent, 2 is turbo + */ + u16 get_performance_level; + u16 set_performance_level; + + /* 0x80 is off, 0x81 is on */ + u16 get_battery_life_extender; + u16 set_battery_life_extender; + + /* 0x80 is off, 0x81 is on */ + u16 get_usb_charge; + u16 set_usb_charge; + + /* the first byte is for bluetooth and the third one is for wlan */ + u16 get_wireless_status; + u16 set_wireless_status; + + /* 0x80 is off, 0x81 is on */ + u16 get_lid_handling; + u16 set_lid_handling; + + /* 0x81 to read, (0x82 | level << 8) to set, 0xaabb to enable */ + u16 kbd_backlight; + + /* + * Tell the BIOS that Linux is running on this machine. + * 81 is on, 80 is off + */ + u16 set_linux; +}; + +struct sabi_performance_level { + const char *name; + u16 value; +}; + +struct sabi_config { + int sabi_version; + const char *test_string; + u16 main_function; + const struct sabi_header_offsets header_offsets; + const struct sabi_commands commands; + const struct sabi_performance_level performance_levels[4]; + u8 min_brightness; + u8 max_brightness; +}; + +static const struct sabi_config sabi_configs[] = { + { + /* I don't know if it is really 2, but it is + * less than 3 anyway */ + .sabi_version = 2, + + .test_string = "SECLINUX", + + .main_function = 0x4c49, + + .header_offsets = { + .port = 0x00, + .re_mem = 0x02, + .iface_func = 0x03, + .en_mem = 0x04, + .data_offset = 0x05, + .data_segment = 0x07, + }, + + .commands = { + .get_brightness = 0x00, + .set_brightness = 0x01, + + .get_wireless_button = 0x02, + .set_wireless_button = 0x03, + + .get_backlight = 0x04, + .set_backlight = 0x05, + + .get_recovery_mode = 0x06, + .set_recovery_mode = 0x07, + + .get_performance_level = 0x08, + .set_performance_level = 0x09, + + .get_battery_life_extender = 0xFFFF, + .set_battery_life_extender = 0xFFFF, + + .get_usb_charge = 0xFFFF, + .set_usb_charge = 0xFFFF, + + .get_wireless_status = 0xFFFF, + .set_wireless_status = 0xFFFF, + + .get_lid_handling = 0xFFFF, + .set_lid_handling = 0xFFFF, + + .kbd_backlight = 0xFFFF, + + .set_linux = 0x0a, + }, + + .performance_levels = { + { + .name = "silent", + .value = 0, + }, + { + .name = "normal", + .value = 1, + }, + { }, + }, + .min_brightness = 1, + .max_brightness = 8, + }, + { + .sabi_version = 3, + + .test_string = "SwSmi@", + + .main_function = 0x5843, + + .header_offsets = { + .port = 0x00, + .re_mem = 0x04, + .iface_func = 0x02, + .en_mem = 0x03, + .data_offset = 0x05, + .data_segment = 0x07, + }, + + .commands = { + .get_brightness = 0x10, + .set_brightness = 0x11, + + .get_wireless_button = 0x12, + .set_wireless_button = 0x13, + + .get_backlight = 0x2d, + .set_backlight = 0x2e, + + .get_recovery_mode = 0xff, + .set_recovery_mode = 0xff, + + .get_performance_level = 0x31, + .set_performance_level = 0x32, + + .get_battery_life_extender = 0x65, + .set_battery_life_extender = 0x66, + + .get_usb_charge = 0x67, + .set_usb_charge = 0x68, + + .get_wireless_status = 0x69, + .set_wireless_status = 0x6a, + + .get_lid_handling = 0x6d, + .set_lid_handling = 0x6e, + + .kbd_backlight = 0x78, + + .set_linux = 0xff, + }, + + .performance_levels = { + { + .name = "normal", + .value = 0, + }, + { + .name = "silent", + .value = 1, + }, + { + .name = "overclock", + .value = 2, + }, + { }, + }, + .min_brightness = 0, + .max_brightness = 8, + }, + { }, +}; + +/* + * samsung-laptop/ - debugfs root directory + * f0000_segment - dump f0000 segment + * command - current command + * data - current data + * d0, d1, d2, d3 - data fields + * call - call SABI using command and data + * + * This allow to call arbitrary sabi commands wihout + * modifying the driver at all. + * For example, setting the keyboard backlight brightness to 5 + * + * echo 0x78 > command + * echo 0x0582 > d0 + * echo 0 > d1 + * echo 0 > d2 + * echo 0 > d3 + * cat call + */ + +struct samsung_laptop_debug { + struct dentry *root; + struct sabi_data data; + u16 command; + + struct debugfs_blob_wrapper f0000_wrapper; + struct debugfs_blob_wrapper data_wrapper; + struct debugfs_blob_wrapper sdiag_wrapper; +}; + +struct samsung_laptop; + +struct samsung_rfkill { + struct samsung_laptop *samsung; + struct rfkill *rfkill; + enum rfkill_type type; +}; + +struct samsung_laptop { + const struct sabi_config *config; + + void __iomem *sabi; + void __iomem *sabi_iface; + void __iomem *f0000_segment; + + struct mutex sabi_mutex; + + struct platform_device *platform_device; + struct backlight_device *backlight_device; + + struct samsung_rfkill wlan; + struct samsung_rfkill bluetooth; + + struct led_classdev kbd_led; + int kbd_led_wk; + struct workqueue_struct *led_workqueue; + struct work_struct kbd_led_work; + + struct samsung_laptop_debug debug; + struct samsung_quirks *quirks; + + struct notifier_block pm_nb; + + struct acpi_battery_hook battery_hook; + + bool handle_backlight; + bool has_stepping_quirk; + + char sdiag[64]; +}; + +struct samsung_quirks { + bool four_kbd_backlight_levels; + bool enable_kbd_backlight; + bool lid_handling; +}; + +static struct samsung_quirks samsung_unknown = {}; + +static struct samsung_quirks samsung_np740u3e = { + .four_kbd_backlight_levels = true, + .enable_kbd_backlight = true, +}; + +static struct samsung_quirks samsung_lid_handling = { + .lid_handling = true, +}; + +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, + "Disable the DMI check and forces the driver to be loaded"); + +static bool debug; +module_param(debug, bool, 0644); +MODULE_PARM_DESC(debug, "Debug enabled or not"); + +static int sabi_command(struct samsung_laptop *samsung, u16 command, + struct sabi_data *in, + struct sabi_data *out) +{ + const struct sabi_config *config = samsung->config; + int ret = 0; + u16 port = readw(samsung->sabi + config->header_offsets.port); + u8 complete, iface_data; + + mutex_lock(&samsung->sabi_mutex); + + if (debug) { + if (in) + pr_info("SABI command:0x%04x " + "data:{0x%08x, 0x%08x, 0x%04x, 0x%02x}", + command, in->d0, in->d1, in->d2, in->d3); + else + pr_info("SABI command:0x%04x", command); + } + + /* enable memory to be able to write to it */ + outb(readb(samsung->sabi + config->header_offsets.en_mem), port); + + /* write out the command */ + writew(config->main_function, samsung->sabi_iface + SABI_IFACE_MAIN); + writew(command, samsung->sabi_iface + SABI_IFACE_SUB); + writeb(0, samsung->sabi_iface + SABI_IFACE_COMPLETE); + if (in) { + writel(in->d0, samsung->sabi_iface + SABI_IFACE_DATA); + writel(in->d1, samsung->sabi_iface + SABI_IFACE_DATA + 4); + writew(in->d2, samsung->sabi_iface + SABI_IFACE_DATA + 8); + writeb(in->d3, samsung->sabi_iface + SABI_IFACE_DATA + 10); + } + outb(readb(samsung->sabi + config->header_offsets.iface_func), port); + + /* write protect memory to make it safe */ + outb(readb(samsung->sabi + config->header_offsets.re_mem), port); + + /* see if the command actually succeeded */ + complete = readb(samsung->sabi_iface + SABI_IFACE_COMPLETE); + iface_data = readb(samsung->sabi_iface + SABI_IFACE_DATA); + + /* iface_data = 0xFF happens when a command is not known + * so we only add a warning in debug mode since we will + * probably issue some unknown command at startup to find + * out which features are supported */ + if (complete != 0xaa || (iface_data == 0xff && debug)) + pr_warn("SABI command 0x%04x failed with" + " completion flag 0x%02x and interface data 0x%02x", + command, complete, iface_data); + + if (complete != 0xaa || iface_data == 0xff) { + ret = -EINVAL; + goto exit; + } + + if (out) { + out->d0 = readl(samsung->sabi_iface + SABI_IFACE_DATA); + out->d1 = readl(samsung->sabi_iface + SABI_IFACE_DATA + 4); + out->d2 = readw(samsung->sabi_iface + SABI_IFACE_DATA + 2); + out->d3 = readb(samsung->sabi_iface + SABI_IFACE_DATA + 1); + } + + if (debug && out) { + pr_info("SABI return data:{0x%08x, 0x%08x, 0x%04x, 0x%02x}", + out->d0, out->d1, out->d2, out->d3); + } + +exit: + mutex_unlock(&samsung->sabi_mutex); + return ret; +} + +/* simple wrappers usable with most commands */ +static int sabi_set_commandb(struct samsung_laptop *samsung, + u16 command, u8 data) +{ + struct sabi_data in = { { { .d0 = 0, .d1 = 0, .d2 = 0, .d3 = 0 } } }; + + in.data[0] = data; + return sabi_command(samsung, command, &in, NULL); +} + +static int read_brightness(struct samsung_laptop *samsung) +{ + const struct sabi_config *config = samsung->config; + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data sretval; + int user_brightness = 0; + int retval; + + retval = sabi_command(samsung, commands->get_brightness, + NULL, &sretval); + if (retval) + return retval; + + user_brightness = sretval.data[0]; + if (user_brightness > config->min_brightness) + user_brightness -= config->min_brightness; + else + user_brightness = 0; + + return user_brightness; +} + +static void set_brightness(struct samsung_laptop *samsung, u8 user_brightness) +{ + const struct sabi_config *config = samsung->config; + const struct sabi_commands *commands = &samsung->config->commands; + u8 user_level = user_brightness + config->min_brightness; + + if (samsung->has_stepping_quirk && user_level != 0) { + /* + * short circuit if the specified level is what's already set + * to prevent the screen from flickering needlessly + */ + if (user_brightness == read_brightness(samsung)) + return; + + sabi_set_commandb(samsung, commands->set_brightness, 0); + } + + sabi_set_commandb(samsung, commands->set_brightness, user_level); +} + +static int get_brightness(struct backlight_device *bd) +{ + struct samsung_laptop *samsung = bl_get_data(bd); + + return read_brightness(samsung); +} + +static void check_for_stepping_quirk(struct samsung_laptop *samsung) +{ + int initial_level; + int check_level; + int orig_level = read_brightness(samsung); + + /* + * Some laptops exhibit the strange behaviour of stepping toward + * (rather than setting) the brightness except when changing to/from + * brightness level 0. This behaviour is checked for here and worked + * around in set_brightness. + */ + + if (orig_level == 0) + set_brightness(samsung, 1); + + initial_level = read_brightness(samsung); + + if (initial_level <= 2) + check_level = initial_level + 2; + else + check_level = initial_level - 2; + + samsung->has_stepping_quirk = false; + set_brightness(samsung, check_level); + + if (read_brightness(samsung) != check_level) { + samsung->has_stepping_quirk = true; + pr_info("enabled workaround for brightness stepping quirk\n"); + } + + set_brightness(samsung, orig_level); +} + +static int update_status(struct backlight_device *bd) +{ + struct samsung_laptop *samsung = bl_get_data(bd); + const struct sabi_commands *commands = &samsung->config->commands; + + set_brightness(samsung, bd->props.brightness); + + if (bd->props.power == BACKLIGHT_POWER_ON) + sabi_set_commandb(samsung, commands->set_backlight, 1); + else + sabi_set_commandb(samsung, commands->set_backlight, 0); + + return 0; +} + +static const struct backlight_ops backlight_ops = { + .get_brightness = get_brightness, + .update_status = update_status, +}; + +static int seclinux_rfkill_set(void *data, bool blocked) +{ + struct samsung_rfkill *srfkill = data; + struct samsung_laptop *samsung = srfkill->samsung; + const struct sabi_commands *commands = &samsung->config->commands; + + return sabi_set_commandb(samsung, commands->set_wireless_button, + !blocked); +} + +static const struct rfkill_ops seclinux_rfkill_ops = { + .set_block = seclinux_rfkill_set, +}; + +static int swsmi_wireless_status(struct samsung_laptop *samsung, + struct sabi_data *data) +{ + const struct sabi_commands *commands = &samsung->config->commands; + + return sabi_command(samsung, commands->get_wireless_status, + NULL, data); +} + +static int swsmi_rfkill_set(void *priv, bool blocked) +{ + struct samsung_rfkill *srfkill = priv; + struct samsung_laptop *samsung = srfkill->samsung; + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int ret, i; + + ret = swsmi_wireless_status(samsung, &data); + if (ret) + return ret; + + /* Don't set the state for non-present devices */ + for (i = 0; i < 4; i++) + if (data.data[i] == 0x02) + data.data[1] = 0; + + if (srfkill->type == RFKILL_TYPE_WLAN) + data.data[WL_STATUS_WLAN] = !blocked; + else if (srfkill->type == RFKILL_TYPE_BLUETOOTH) + data.data[WL_STATUS_BT] = !blocked; + + return sabi_command(samsung, commands->set_wireless_status, + &data, &data); +} + +static void swsmi_rfkill_query(struct rfkill *rfkill, void *priv) +{ + struct samsung_rfkill *srfkill = priv; + struct samsung_laptop *samsung = srfkill->samsung; + struct sabi_data data; + int ret; + + ret = swsmi_wireless_status(samsung, &data); + if (ret) + return ; + + if (srfkill->type == RFKILL_TYPE_WLAN) + ret = data.data[WL_STATUS_WLAN]; + else if (srfkill->type == RFKILL_TYPE_BLUETOOTH) + ret = data.data[WL_STATUS_BT]; + else + return ; + + rfkill_set_sw_state(rfkill, !ret); +} + +static const struct rfkill_ops swsmi_rfkill_ops = { + .set_block = swsmi_rfkill_set, + .query = swsmi_rfkill_query, +}; + +static ssize_t get_performance_level(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + const struct sabi_config *config = samsung->config; + const struct sabi_commands *commands = &config->commands; + struct sabi_data sretval; + int retval; + int i; + + /* Read the state */ + retval = sabi_command(samsung, commands->get_performance_level, + NULL, &sretval); + if (retval) + return retval; + + /* The logic is backwards, yeah, lots of fun... */ + for (i = 0; config->performance_levels[i].name; ++i) { + if (sretval.data[0] == config->performance_levels[i].value) + return sysfs_emit(buf, "%s\n", config->performance_levels[i].name); + } + return sysfs_emit(buf, "%s\n", "unknown"); +} + +static ssize_t set_performance_level(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + const struct sabi_config *config = samsung->config; + const struct sabi_commands *commands = &config->commands; + int i; + + if (count < 1) + return count; + + for (i = 0; config->performance_levels[i].name; ++i) { + const struct sabi_performance_level *level = + &config->performance_levels[i]; + if (!strncasecmp(level->name, buf, strlen(level->name))) { + sabi_set_commandb(samsung, + commands->set_performance_level, + level->value); + break; + } + } + + if (!config->performance_levels[i].name) + return -EINVAL; + + return count; +} + +static DEVICE_ATTR(performance_level, 0644, + get_performance_level, set_performance_level); + +static void show_battery_life_extender_deprecation_warning(struct device *dev) +{ + dev_warn_once(dev, "battery_life_extender attribute has been deprecated, see charge_types.\n"); +} + +static int read_battery_life_extender(struct samsung_laptop *samsung) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int retval; + + if (commands->get_battery_life_extender == 0xFFFF) + return -ENODEV; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x80; + retval = sabi_command(samsung, commands->get_battery_life_extender, + &data, &data); + + if (retval) + return retval; + + if (data.data[0] != 0 && data.data[0] != 1) + return -ENODEV; + + return data.data[0]; +} + +static int write_battery_life_extender(struct samsung_laptop *samsung, + int enabled) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x80 | enabled; + return sabi_command(samsung, commands->set_battery_life_extender, + &data, NULL); +} + +static ssize_t get_battery_life_extender(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret; + + show_battery_life_extender_deprecation_warning(dev); + + ret = read_battery_life_extender(samsung); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t set_battery_life_extender(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret, value; + + show_battery_life_extender_deprecation_warning(dev); + + if (!count || kstrtoint(buf, 0, &value) != 0) + return -EINVAL; + + ret = write_battery_life_extender(samsung, !!value); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(battery_life_extender, 0644, + get_battery_life_extender, set_battery_life_extender); + +static int samsung_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct samsung_laptop *samsung = ext_data; + + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + return write_battery_life_extender(samsung, 1); + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + return write_battery_life_extender(samsung, 0); + default: + return -EINVAL; + } +} + +static int samsung_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct samsung_laptop *samsung = ext_data; + int ret; + + ret = read_battery_life_extender(samsung); + if (ret < 0) + return ret; + + if (ret == 1) + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + + return 0; +} + +static int samsung_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 samsung_power_supply_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const struct power_supply_ext samsung_battery_ext = { + .name = "samsung_laptop", + .properties = samsung_power_supply_props, + .num_properties = ARRAY_SIZE(samsung_power_supply_props), + .charge_types = (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)), + .get_property = samsung_psy_ext_get_prop, + .set_property = samsung_psy_ext_set_prop, + .property_is_writeable = samsung_psy_prop_is_writeable, +}; + +static int samsung_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct samsung_laptop *samsung = container_of(hook, struct samsung_laptop, battery_hook); + + return power_supply_register_extension(battery, &samsung_battery_ext, + &samsung->platform_device->dev, samsung); +} + +static int samsung_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &samsung_battery_ext); + + return 0; +} + +static int read_usb_charge(struct samsung_laptop *samsung) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int retval; + + if (commands->get_usb_charge == 0xFFFF) + return -ENODEV; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x80; + retval = sabi_command(samsung, commands->get_usb_charge, + &data, &data); + + if (retval) + return retval; + + if (data.data[0] != 0 && data.data[0] != 1) + return -ENODEV; + + return data.data[0]; +} + +static int write_usb_charge(struct samsung_laptop *samsung, + int enabled) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x80 | enabled; + return sabi_command(samsung, commands->set_usb_charge, + &data, NULL); +} + +static ssize_t get_usb_charge(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret; + + ret = read_usb_charge(samsung); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t set_usb_charge(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret, value; + + if (!count || kstrtoint(buf, 0, &value) != 0) + return -EINVAL; + + ret = write_usb_charge(samsung, !!value); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(usb_charge, 0644, + get_usb_charge, set_usb_charge); + +static int read_lid_handling(struct samsung_laptop *samsung) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int retval; + + if (commands->get_lid_handling == 0xFFFF) + return -ENODEV; + + memset(&data, 0, sizeof(data)); + retval = sabi_command(samsung, commands->get_lid_handling, + &data, &data); + + if (retval) + return retval; + + return data.data[0] & 0x1; +} + +static int write_lid_handling(struct samsung_laptop *samsung, + int enabled) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x80 | enabled; + return sabi_command(samsung, commands->set_lid_handling, + &data, NULL); +} + +static ssize_t get_lid_handling(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret; + + ret = read_lid_handling(samsung); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t set_lid_handling(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct samsung_laptop *samsung = dev_get_drvdata(dev); + int ret, value; + + if (!count || kstrtoint(buf, 0, &value) != 0) + return -EINVAL; + + ret = write_lid_handling(samsung, !!value); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(lid_handling, 0644, + get_lid_handling, set_lid_handling); + +static struct attribute *platform_attributes[] = { + &dev_attr_performance_level.attr, + &dev_attr_battery_life_extender.attr, + &dev_attr_usb_charge.attr, + &dev_attr_lid_handling.attr, + NULL +}; + +static int find_signature(void __iomem *memcheck, const char *testStr) +{ + int i = 0; + int loca; + + for (loca = 0; loca < 0xffff; loca++) { + char temp = readb(memcheck + loca); + + if (temp == testStr[i]) { + if (i == strlen(testStr)-1) + break; + ++i; + } else { + i = 0; + } + } + return loca; +} + +static void samsung_rfkill_exit(struct samsung_laptop *samsung) +{ + if (samsung->wlan.rfkill) { + rfkill_unregister(samsung->wlan.rfkill); + rfkill_destroy(samsung->wlan.rfkill); + samsung->wlan.rfkill = NULL; + } + if (samsung->bluetooth.rfkill) { + rfkill_unregister(samsung->bluetooth.rfkill); + rfkill_destroy(samsung->bluetooth.rfkill); + samsung->bluetooth.rfkill = NULL; + } +} + +static int samsung_new_rfkill(struct samsung_laptop *samsung, + struct samsung_rfkill *arfkill, + const char *name, enum rfkill_type type, + const struct rfkill_ops *ops, + int blocked) +{ + struct rfkill **rfkill = &arfkill->rfkill; + int ret; + + arfkill->type = type; + arfkill->samsung = samsung; + + *rfkill = rfkill_alloc(name, &samsung->platform_device->dev, + type, ops, arfkill); + + if (!*rfkill) + return -EINVAL; + + if (blocked != -1) + rfkill_init_sw_state(*rfkill, blocked); + + ret = rfkill_register(*rfkill); + if (ret) { + rfkill_destroy(*rfkill); + *rfkill = NULL; + return ret; + } + return 0; +} + +static int __init samsung_rfkill_init_seclinux(struct samsung_laptop *samsung) +{ + return samsung_new_rfkill(samsung, &samsung->wlan, "samsung-wlan", + RFKILL_TYPE_WLAN, &seclinux_rfkill_ops, -1); +} + +static int __init samsung_rfkill_init_swsmi(struct samsung_laptop *samsung) +{ + struct sabi_data data; + int ret; + + ret = swsmi_wireless_status(samsung, &data); + if (ret) { + /* Some swsmi laptops use the old seclinux way to control + * wireless devices */ + if (ret == -EINVAL) + ret = samsung_rfkill_init_seclinux(samsung); + return ret; + } + + /* 0x02 seems to mean that the device is no present/available */ + + if (data.data[WL_STATUS_WLAN] != 0x02) + ret = samsung_new_rfkill(samsung, &samsung->wlan, + "samsung-wlan", + RFKILL_TYPE_WLAN, + &swsmi_rfkill_ops, + !data.data[WL_STATUS_WLAN]); + if (ret) + goto exit; + + if (data.data[WL_STATUS_BT] != 0x02) + ret = samsung_new_rfkill(samsung, &samsung->bluetooth, + "samsung-bluetooth", + RFKILL_TYPE_BLUETOOTH, + &swsmi_rfkill_ops, + !data.data[WL_STATUS_BT]); + if (ret) + goto exit; + +exit: + if (ret) + samsung_rfkill_exit(samsung); + + return ret; +} + +static int __init samsung_rfkill_init(struct samsung_laptop *samsung) +{ + if (samsung->config->sabi_version == 2) + return samsung_rfkill_init_seclinux(samsung); + if (samsung->config->sabi_version == 3) + return samsung_rfkill_init_swsmi(samsung); + return 0; +} + +static void samsung_lid_handling_exit(struct samsung_laptop *samsung) +{ + if (samsung->quirks->lid_handling) + write_lid_handling(samsung, 0); +} + +static int __init samsung_lid_handling_init(struct samsung_laptop *samsung) +{ + int retval = 0; + + if (samsung->quirks->lid_handling) + retval = write_lid_handling(samsung, 1); + + return retval; +} + +static int __init samsung_battery_hook_init(struct samsung_laptop *samsung) +{ + int retval = 0; + + if (samsung->config->commands.get_battery_life_extender != 0xFFFF) { + samsung->battery_hook.add_battery = samsung_battery_add; + samsung->battery_hook.remove_battery = samsung_battery_remove; + samsung->battery_hook.name = "Samsung Battery Extension"; + retval = devm_battery_hook_register(&samsung->platform_device->dev, + &samsung->battery_hook); + } + + return retval; +} + +static int kbd_backlight_enable(struct samsung_laptop *samsung) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int retval; + + if (commands->kbd_backlight == 0xFFFF) + return -ENODEV; + + memset(&data, 0, sizeof(data)); + data.d0 = 0xaabb; + retval = sabi_command(samsung, commands->kbd_backlight, + &data, &data); + + if (retval) + return retval; + + if (data.d0 != 0xccdd) + return -ENODEV; + return 0; +} + +static int kbd_backlight_read(struct samsung_laptop *samsung) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + int retval; + + memset(&data, 0, sizeof(data)); + data.data[0] = 0x81; + retval = sabi_command(samsung, commands->kbd_backlight, + &data, &data); + + if (retval) + return retval; + + return data.data[0]; +} + +static int kbd_backlight_write(struct samsung_laptop *samsung, int brightness) +{ + const struct sabi_commands *commands = &samsung->config->commands; + struct sabi_data data; + + memset(&data, 0, sizeof(data)); + data.d0 = 0x82 | ((brightness & 0xFF) << 8); + return sabi_command(samsung, commands->kbd_backlight, + &data, NULL); +} + +static void kbd_led_update(struct work_struct *work) +{ + struct samsung_laptop *samsung; + + samsung = container_of(work, struct samsung_laptop, kbd_led_work); + kbd_backlight_write(samsung, samsung->kbd_led_wk); +} + +static void kbd_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct samsung_laptop *samsung; + + samsung = container_of(led_cdev, struct samsung_laptop, kbd_led); + + if (value > samsung->kbd_led.max_brightness) + value = samsung->kbd_led.max_brightness; + + samsung->kbd_led_wk = value; + queue_work(samsung->led_workqueue, &samsung->kbd_led_work); +} + +static enum led_brightness kbd_led_get(struct led_classdev *led_cdev) +{ + struct samsung_laptop *samsung; + + samsung = container_of(led_cdev, struct samsung_laptop, kbd_led); + return kbd_backlight_read(samsung); +} + +static void samsung_leds_exit(struct samsung_laptop *samsung) +{ + led_classdev_unregister(&samsung->kbd_led); + if (samsung->led_workqueue) + destroy_workqueue(samsung->led_workqueue); +} + +static int __init samsung_leds_init(struct samsung_laptop *samsung) +{ + int ret = 0; + + samsung->led_workqueue = create_singlethread_workqueue("led_workqueue"); + if (!samsung->led_workqueue) + return -ENOMEM; + + if (kbd_backlight_enable(samsung) >= 0) { + INIT_WORK(&samsung->kbd_led_work, kbd_led_update); + + samsung->kbd_led.name = "samsung::kbd_backlight"; + samsung->kbd_led.brightness_set = kbd_led_set; + samsung->kbd_led.brightness_get = kbd_led_get; + samsung->kbd_led.max_brightness = 8; + if (samsung->quirks->four_kbd_backlight_levels) + samsung->kbd_led.max_brightness = 4; + + ret = led_classdev_register(&samsung->platform_device->dev, + &samsung->kbd_led); + } + + if (ret) + samsung_leds_exit(samsung); + + return ret; +} + +static void samsung_backlight_exit(struct samsung_laptop *samsung) +{ + if (samsung->backlight_device) { + backlight_device_unregister(samsung->backlight_device); + samsung->backlight_device = NULL; + } +} + +static int __init samsung_backlight_init(struct samsung_laptop *samsung) +{ + struct backlight_device *bd; + struct backlight_properties props; + + if (!samsung->handle_backlight) + return 0; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = samsung->config->max_brightness - + samsung->config->min_brightness; + + bd = backlight_device_register("samsung", + &samsung->platform_device->dev, + samsung, &backlight_ops, + &props); + if (IS_ERR(bd)) + return PTR_ERR(bd); + + samsung->backlight_device = bd; + samsung->backlight_device->props.brightness = read_brightness(samsung); + samsung->backlight_device->props.power = BACKLIGHT_POWER_ON; + backlight_update_status(samsung->backlight_device); + + return 0; +} + +static umode_t samsung_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct samsung_laptop *samsung = dev_get_drvdata(dev); + bool ok = true; + + if (attr == &dev_attr_performance_level.attr) + ok = !!samsung->config->performance_levels[0].name; + if (attr == &dev_attr_battery_life_extender.attr) + ok = !!(read_battery_life_extender(samsung) >= 0); + if (attr == &dev_attr_usb_charge.attr) + ok = !!(read_usb_charge(samsung) >= 0); + if (attr == &dev_attr_lid_handling.attr) + ok = !!(read_lid_handling(samsung) >= 0); + + return ok ? attr->mode : 0; +} + +static const struct attribute_group platform_attribute_group = { + .is_visible = samsung_sysfs_is_visible, + .attrs = platform_attributes +}; + +static void samsung_sysfs_exit(struct samsung_laptop *samsung) +{ + struct platform_device *device = samsung->platform_device; + + sysfs_remove_group(&device->dev.kobj, &platform_attribute_group); +} + +static int __init samsung_sysfs_init(struct samsung_laptop *samsung) +{ + struct platform_device *device = samsung->platform_device; + + return sysfs_create_group(&device->dev.kobj, &platform_attribute_group); + +} + +static int samsung_laptop_call_show(struct seq_file *m, void *data) +{ + struct samsung_laptop *samsung = m->private; + struct sabi_data *sdata = &samsung->debug.data; + int ret; + + seq_printf(m, "SABI 0x%04x {0x%08x, 0x%08x, 0x%04x, 0x%02x}\n", + samsung->debug.command, + sdata->d0, sdata->d1, sdata->d2, sdata->d3); + + ret = sabi_command(samsung, samsung->debug.command, sdata, sdata); + + if (ret) { + seq_printf(m, "SABI command 0x%04x failed\n", + samsung->debug.command); + return ret; + } + + seq_printf(m, "SABI {0x%08x, 0x%08x, 0x%04x, 0x%02x}\n", + sdata->d0, sdata->d1, sdata->d2, sdata->d3); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(samsung_laptop_call); + +static void samsung_debugfs_exit(struct samsung_laptop *samsung) +{ + debugfs_remove_recursive(samsung->debug.root); +} + +static void samsung_debugfs_init(struct samsung_laptop *samsung) +{ + struct dentry *root; + + root = debugfs_create_dir("samsung-laptop", NULL); + samsung->debug.root = root; + + samsung->debug.f0000_wrapper.data = samsung->f0000_segment; + samsung->debug.f0000_wrapper.size = 0xffff; + + samsung->debug.data_wrapper.data = &samsung->debug.data; + samsung->debug.data_wrapper.size = sizeof(samsung->debug.data); + + samsung->debug.sdiag_wrapper.data = samsung->sdiag; + samsung->debug.sdiag_wrapper.size = strlen(samsung->sdiag); + + debugfs_create_u16("command", 0644, root, &samsung->debug.command); + debugfs_create_u32("d0", 0644, root, &samsung->debug.data.d0); + debugfs_create_u32("d1", 0644, root, &samsung->debug.data.d1); + debugfs_create_u16("d2", 0644, root, &samsung->debug.data.d2); + debugfs_create_u8("d3", 0644, root, &samsung->debug.data.d3); + debugfs_create_blob("data", 0444, root, &samsung->debug.data_wrapper); + debugfs_create_blob("f0000_segment", 0400, root, + &samsung->debug.f0000_wrapper); + debugfs_create_file("call", 0444, root, samsung, + &samsung_laptop_call_fops); + debugfs_create_blob("sdiag", 0444, root, &samsung->debug.sdiag_wrapper); +} + +static void samsung_sabi_exit(struct samsung_laptop *samsung) +{ + const struct sabi_config *config = samsung->config; + + /* Turn off "Linux" mode in the BIOS */ + if (config && config->commands.set_linux != 0xff) + sabi_set_commandb(samsung, config->commands.set_linux, 0x80); + + if (samsung->sabi_iface) { + iounmap(samsung->sabi_iface); + samsung->sabi_iface = NULL; + } + if (samsung->f0000_segment) { + iounmap(samsung->f0000_segment); + samsung->f0000_segment = NULL; + } + + samsung->config = NULL; +} + +static __init void samsung_sabi_infos(struct samsung_laptop *samsung, int loca, + unsigned int ifaceP) +{ + const struct sabi_config *config = samsung->config; + + printk(KERN_DEBUG "This computer supports SABI==%x\n", + loca + 0xf0000 - 6); + + printk(KERN_DEBUG "SABI header:\n"); + printk(KERN_DEBUG " SMI Port Number = 0x%04x\n", + readw(samsung->sabi + config->header_offsets.port)); + printk(KERN_DEBUG " SMI Interface Function = 0x%02x\n", + readb(samsung->sabi + config->header_offsets.iface_func)); + printk(KERN_DEBUG " SMI enable memory buffer = 0x%02x\n", + readb(samsung->sabi + config->header_offsets.en_mem)); + printk(KERN_DEBUG " SMI restore memory buffer = 0x%02x\n", + readb(samsung->sabi + config->header_offsets.re_mem)); + printk(KERN_DEBUG " SABI data offset = 0x%04x\n", + readw(samsung->sabi + config->header_offsets.data_offset)); + printk(KERN_DEBUG " SABI data segment = 0x%04x\n", + readw(samsung->sabi + config->header_offsets.data_segment)); + + printk(KERN_DEBUG " SABI pointer = 0x%08x\n", ifaceP); +} + +static void __init samsung_sabi_diag(struct samsung_laptop *samsung) +{ + int loca = find_signature(samsung->f0000_segment, "SDiaG@"); + int i; + + if (loca == 0xffff) + return ; + + /* Example: + * Ident: @SDiaG@686XX-N90X3A/966-SEC-07HL-S90X3A + * + * Product name: 90X3A + * BIOS Version: 07HL + */ + loca += 1; + for (i = 0; loca < 0xffff && i < sizeof(samsung->sdiag) - 1; loca++) { + char temp = readb(samsung->f0000_segment + loca); + + if (isalnum(temp) || temp == '/' || temp == '-') + samsung->sdiag[i++] = temp; + else + break ; + } + + if (debug && samsung->sdiag[0]) + pr_info("sdiag: %s", samsung->sdiag); +} + +static int __init samsung_sabi_init(struct samsung_laptop *samsung) +{ + const struct sabi_config *config = NULL; + const struct sabi_commands *commands; + unsigned int ifaceP; + int loca = 0xffff; + int ret = 0; + int i; + + samsung->f0000_segment = ioremap(0xf0000, 0xffff); + if (!samsung->f0000_segment) { + if (debug || force) + pr_err("Can't map the segment at 0xf0000\n"); + ret = -EINVAL; + goto exit; + } + + samsung_sabi_diag(samsung); + + /* Try to find one of the signatures in memory to find the header */ + for (i = 0; sabi_configs[i].test_string != NULL; ++i) { + samsung->config = &sabi_configs[i]; + loca = find_signature(samsung->f0000_segment, + samsung->config->test_string); + if (loca != 0xffff) + break; + } + + if (loca == 0xffff) { + if (debug || force) + pr_err("This computer does not support SABI\n"); + ret = -ENODEV; + goto exit; + } + + config = samsung->config; + commands = &config->commands; + + /* point to the SMI port Number */ + loca += 1; + samsung->sabi = (samsung->f0000_segment + loca); + + /* Get a pointer to the SABI Interface */ + ifaceP = (readw(samsung->sabi + config->header_offsets.data_segment) & 0x0ffff) << 4; + ifaceP += readw(samsung->sabi + config->header_offsets.data_offset) & 0x0ffff; + + if (debug) + samsung_sabi_infos(samsung, loca, ifaceP); + + samsung->sabi_iface = ioremap(ifaceP, 16); + if (!samsung->sabi_iface) { + pr_err("Can't remap %x\n", ifaceP); + ret = -EINVAL; + goto exit; + } + + /* Turn on "Linux" mode in the BIOS */ + if (commands->set_linux != 0xff) { + int retval = sabi_set_commandb(samsung, + commands->set_linux, 0x81); + if (retval) { + pr_warn("Linux mode was not set!\n"); + ret = -ENODEV; + goto exit; + } + } + + /* Check for stepping quirk */ + if (samsung->handle_backlight) + check_for_stepping_quirk(samsung); + + pr_info("detected SABI interface: %s\n", + samsung->config->test_string); + +exit: + if (ret) + samsung_sabi_exit(samsung); + + return ret; +} + +static void samsung_platform_exit(struct samsung_laptop *samsung) +{ + if (samsung->platform_device) { + platform_device_unregister(samsung->platform_device); + samsung->platform_device = NULL; + } +} + +static int samsung_pm_notification(struct notifier_block *nb, + unsigned long val, void *ptr) +{ + struct samsung_laptop *samsung; + + samsung = container_of(nb, struct samsung_laptop, pm_nb); + if (val == PM_POST_HIBERNATION && + samsung->quirks->enable_kbd_backlight) + kbd_backlight_enable(samsung); + + if (val == PM_POST_HIBERNATION && samsung->quirks->lid_handling) + write_lid_handling(samsung, 1); + + return 0; +} + +static int __init samsung_platform_init(struct samsung_laptop *samsung) +{ + struct platform_device *pdev; + + pdev = platform_device_register_simple("samsung", PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(pdev)) + return PTR_ERR(pdev); + + samsung->platform_device = pdev; + platform_set_drvdata(samsung->platform_device, samsung); + return 0; +} + +static struct samsung_quirks *quirks; + +static int __init samsung_dmi_matched(const struct dmi_system_id *d) +{ + quirks = d->driver_data; + return 0; +} + +static const struct dmi_system_id samsung_dmi_table[] __initconst = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_CHASSIS_TYPE, "8"), /* Portable */ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_CHASSIS_TYPE, "9"), /* Laptop */ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_CHASSIS_TYPE, "10"), /* Notebook */ + }, + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_CHASSIS_TYPE, "14"), /* Sub-Notebook */ + }, + }, + /* DMI ids for laptops with bad Chassis Type */ + { + .ident = "R40/R41", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_PRODUCT_NAME, "R40/R41"), + DMI_MATCH(DMI_BOARD_NAME, "R40/R41"), + }, + }, + /* Specific DMI ids for laptop with quirks */ + { + .callback = samsung_dmi_matched, + .ident = "730U3E/740U3E", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_PRODUCT_NAME, "730U3E/740U3E"), + }, + .driver_data = &samsung_np740u3e, + }, + { + .callback = samsung_dmi_matched, + .ident = "300V3Z/300V4Z/300V5Z", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD."), + DMI_MATCH(DMI_PRODUCT_NAME, "300V3Z/300V4Z/300V5Z"), + }, + .driver_data = &samsung_lid_handling, + }, + { }, +}; +MODULE_DEVICE_TABLE(dmi, samsung_dmi_table); + +static struct platform_device *samsung_platform_device; + +static int __init samsung_init(void) +{ + struct samsung_laptop *samsung; + int ret; + + if (efi_enabled(EFI_BOOT)) + return -ENODEV; + + quirks = &samsung_unknown; + if (!force && !dmi_check_system(samsung_dmi_table)) + return -ENODEV; + + samsung = kzalloc_obj(*samsung); + if (!samsung) + return -ENOMEM; + + mutex_init(&samsung->sabi_mutex); + samsung->handle_backlight = true; + samsung->quirks = quirks; + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) + samsung->handle_backlight = false; + + ret = samsung_platform_init(samsung); + if (ret) + goto error_platform; + + ret = samsung_sabi_init(samsung); + if (ret) + goto error_sabi; + + ret = samsung_sysfs_init(samsung); + if (ret) + goto error_sysfs; + + ret = samsung_backlight_init(samsung); + if (ret) + goto error_backlight; + + ret = samsung_rfkill_init(samsung); + if (ret) + goto error_rfkill; + + ret = samsung_leds_init(samsung); + if (ret) + goto error_leds; + + ret = samsung_lid_handling_init(samsung); + if (ret) + goto error_lid_handling; + + ret = samsung_battery_hook_init(samsung); + if (ret) + goto error_lid_handling; + + samsung_debugfs_init(samsung); + + samsung->pm_nb.notifier_call = samsung_pm_notification; + register_pm_notifier(&samsung->pm_nb); + + samsung_platform_device = samsung->platform_device; + return ret; + +error_lid_handling: + samsung_leds_exit(samsung); +error_leds: + samsung_rfkill_exit(samsung); +error_rfkill: + samsung_backlight_exit(samsung); +error_backlight: + samsung_sysfs_exit(samsung); +error_sysfs: + samsung_sabi_exit(samsung); +error_sabi: + samsung_platform_exit(samsung); +error_platform: + kfree(samsung); + return ret; +} + +static void __exit samsung_exit(void) +{ + struct samsung_laptop *samsung; + + samsung = platform_get_drvdata(samsung_platform_device); + unregister_pm_notifier(&samsung->pm_nb); + + samsung_debugfs_exit(samsung); + samsung_lid_handling_exit(samsung); + samsung_leds_exit(samsung); + samsung_rfkill_exit(samsung); + samsung_backlight_exit(samsung); + samsung_sysfs_exit(samsung); + samsung_sabi_exit(samsung); + samsung_platform_exit(samsung); + + kfree(samsung); + samsung_platform_device = NULL; +} + +module_init(samsung_init); +module_exit(samsung_exit); + +MODULE_AUTHOR("Greg Kroah-Hartman "); +MODULE_DESCRIPTION("Samsung Laptop driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/samsung-q10.c b/drivers/platform/x86/samsung-q10.c new file mode 100644 index 000000000..8160d45f8 --- /dev/null +++ b/drivers/platform/x86/samsung-q10.c @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for Samsung Q10 and related laptops: controls the backlight + * + * Copyright (c) 2011 Frederick van der Wyck + */ + +#include +#include +#include +#include +#include +#include +#include + +#define SAMSUNGQ10_BL_MAX_INTENSITY 7 + +static acpi_handle ec_handle; + +static bool force; +module_param(force, bool, 0); +MODULE_PARM_DESC(force, + "Disable the DMI check and force the driver to be loaded"); + +static int samsungq10_bl_set_intensity(struct backlight_device *bd) +{ + + acpi_status status; + int i; + + for (i = 0; i < SAMSUNGQ10_BL_MAX_INTENSITY; i++) { + status = acpi_evaluate_object(ec_handle, "_Q63", NULL, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + } + for (i = 0; i < bd->props.brightness; i++) { + status = acpi_evaluate_object(ec_handle, "_Q64", NULL, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + } + + return 0; +} + +static const struct backlight_ops samsungq10_bl_ops = { + .update_status = samsungq10_bl_set_intensity, +}; + +static int samsungq10_probe(struct platform_device *pdev) +{ + + struct backlight_properties props; + struct backlight_device *bd; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = SAMSUNGQ10_BL_MAX_INTENSITY; + bd = backlight_device_register("samsung", &pdev->dev, NULL, + &samsungq10_bl_ops, &props); + if (IS_ERR(bd)) + return PTR_ERR(bd); + + platform_set_drvdata(pdev, bd); + + return 0; +} + +static void samsungq10_remove(struct platform_device *pdev) +{ + + struct backlight_device *bd = platform_get_drvdata(pdev); + + backlight_device_unregister(bd); +} + +static struct platform_driver samsungq10_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .probe = samsungq10_probe, + .remove = samsungq10_remove, +}; + +static struct platform_device *samsungq10_device; + +static int __init dmi_check_callback(const struct dmi_system_id *id) +{ + printk(KERN_INFO KBUILD_MODNAME ": found model '%s'\n", id->ident); + return 1; +} + +static const struct dmi_system_id samsungq10_dmi_table[] __initconst = { + { + .ident = "Samsung Q10", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Samsung"), + DMI_MATCH(DMI_PRODUCT_NAME, "SQ10"), + }, + .callback = dmi_check_callback, + }, + { + .ident = "Samsung Q20", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG Electronics"), + DMI_MATCH(DMI_PRODUCT_NAME, "SENS Q20"), + }, + .callback = dmi_check_callback, + }, + { + .ident = "Samsung Q25", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG Electronics"), + DMI_MATCH(DMI_PRODUCT_NAME, "NQ25"), + }, + .callback = dmi_check_callback, + }, + { + .ident = "Dell Latitude X200", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Computer Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "X200"), + }, + .callback = dmi_check_callback, + }, + { }, +}; +MODULE_DEVICE_TABLE(dmi, samsungq10_dmi_table); + +static int __init samsungq10_init(void) +{ + if (!force && !dmi_check_system(samsungq10_dmi_table)) + return -ENODEV; + + ec_handle = ec_get_handle(); + + if (!ec_handle) + return -ENODEV; + + samsungq10_device = platform_create_bundle(&samsungq10_driver, + samsungq10_probe, + NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(samsungq10_device); +} + +static void __exit samsungq10_exit(void) +{ + platform_device_unregister(samsungq10_device); + platform_driver_unregister(&samsungq10_driver); +} + +module_init(samsungq10_init); +module_exit(samsungq10_exit); + +MODULE_AUTHOR("Frederick van der Wyck "); +MODULE_DESCRIPTION("Samsung Q10 Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/sel3350-platform.c b/drivers/platform/x86/sel3350-platform.c new file mode 100644 index 000000000..f3a314235 --- /dev/null +++ b/drivers/platform/x86/sel3350-platform.c @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-only OR BSD-3-Clause +/* + * Copyright 2023 Schweitzer Engineering Laboratories, Inc. + * 2350 NE Hopkins Court, Pullman, WA 99163 USA + * + * Platform support for the b2093 mainboard used in SEL-3350 computers. + * Consumes GPIO from the SoC to provide standard LED and power supply + * devices. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Broxton communities */ +#define BXT_NW "INT3452:01" +#define BXT_W "INT3452:02" +#define BXT_SW "INT3452:03" + +#define B2093_GPIO_ACPI_ID "SEL0003" + +#define SEL_PS_A "sel_ps_a" +#define SEL_PS_A_DETECT "sel_ps_a_detect" +#define SEL_PS_A_GOOD "sel_ps_a_good" +#define SEL_PS_B "sel_ps_b" +#define SEL_PS_B_DETECT "sel_ps_b_detect" +#define SEL_PS_B_GOOD "sel_ps_b_good" + +#define AUX_LED_GRN1 "sel_aux_led_grn1" +#define AUX_LED_GRN2 "sel_aux_led_grn2" +#define AUX_LED_GRN3 "sel_aux_led_grn3" +#define AUX_LED_GRN4 "sel_aux_led_grn4" +#define ALARM_STATE_USER "sel_alarm_state_user" +#define ENABLE_STATE_USER "sel_enable_state_user" +#define AUX_LED_RED1 "sel_aux_led_red1" +#define AUX_LED_RED2 "sel_aux_led_red2" +#define AUX_LED_RED3 "sel_aux_led_red3" +#define AUX_LED_RED4 "sel_aux_led_red4" + +static const char *const sel3350_leds_gpio_names[] = { + AUX_LED_GRN1, + AUX_LED_GRN2, + AUX_LED_GRN3, + AUX_LED_GRN4, + ALARM_STATE_USER, + ENABLE_STATE_USER, + AUX_LED_RED1, + AUX_LED_RED2, + AUX_LED_RED3, + AUX_LED_RED4, +}; + +/* LEDs */ +static struct gpio_led sel3350_leds[] = { + { .name = "sel:green:aux1", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:green:aux2", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:green:aux3", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:green:aux4", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:red:alarm", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:green:enabled", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:red:aux1", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:red:aux2", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:red:aux3", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, + { .name = "sel:red:aux4", + .default_state = LEDS_GPIO_DEFSTATE_KEEP, + .retain_state_suspended = 1, + .retain_state_shutdown = 1, + }, +}; + +static const struct gpio_led_platform_data sel3350_leds_pdata = { + .num_leds = ARRAY_SIZE(sel3350_leds), + .leds = sel3350_leds, +}; + +static struct gpiod_lookup_table sel3350_gpios_table = { + .dev_id = B2093_GPIO_ACPI_ID ":00", + .table = { + GPIO_LOOKUP(BXT_NW, 44, SEL_PS_A_DETECT, GPIO_ACTIVE_LOW), + GPIO_LOOKUP(BXT_NW, 45, SEL_PS_A_GOOD, GPIO_ACTIVE_LOW), + GPIO_LOOKUP(BXT_NW, 46, SEL_PS_B_DETECT, GPIO_ACTIVE_LOW), + GPIO_LOOKUP(BXT_NW, 47, SEL_PS_B_GOOD, GPIO_ACTIVE_LOW), + GPIO_LOOKUP(BXT_NW, 49, AUX_LED_GRN1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_NW, 50, AUX_LED_GRN2, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_NW, 51, AUX_LED_GRN3, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_NW, 52, AUX_LED_GRN4, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_W, 20, ALARM_STATE_USER, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_W, 21, ENABLE_STATE_USER, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_SW, 37, AUX_LED_RED1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_SW, 38, AUX_LED_RED2, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_SW, 39, AUX_LED_RED3, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP(BXT_SW, 40, AUX_LED_RED4, GPIO_ACTIVE_HIGH), + {}, + } +}; + +/* Power Supplies */ + +struct sel3350_power_cfg_data { + struct gpio_desc *ps_detect; + struct gpio_desc *ps_good; +}; + +static int sel3350_power_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sel3350_power_cfg_data *data = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + if (gpiod_get_value(data->ps_detect)) { + if (gpiod_get_value(data->ps_good)) + val->intval = POWER_SUPPLY_HEALTH_GOOD; + else + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + } else { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + } + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = gpiod_get_value(data->ps_detect); + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = gpiod_get_value(data->ps_good); + break; + default: + return -EINVAL; + } + return 0; +} + +static const enum power_supply_property sel3350_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc sel3350_ps_a_desc = { + .name = SEL_PS_A, + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = sel3350_power_properties, + .num_properties = ARRAY_SIZE(sel3350_power_properties), + .get_property = sel3350_power_get_property, +}; + +static const struct power_supply_desc sel3350_ps_b_desc = { + .name = SEL_PS_B, + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = sel3350_power_properties, + .num_properties = ARRAY_SIZE(sel3350_power_properties), + .get_property = sel3350_power_get_property, +}; + +struct sel3350_data { + struct platform_device *leds_pdev; + struct power_supply *ps_a; + struct power_supply *ps_b; + struct sel3350_power_cfg_data ps_a_cfg_data; + struct sel3350_power_cfg_data ps_b_cfg_data; +}; + +static int sel3350_probe(struct platform_device *pdev) +{ + int rs; + int i; + struct sel3350_data *sel3350; + struct power_supply_config ps_cfg = {}; + + sel3350 = devm_kzalloc(&pdev->dev, sizeof(struct sel3350_data), GFP_KERNEL); + if (!sel3350) + return -ENOMEM; + + platform_set_drvdata(pdev, sel3350); + + gpiod_add_lookup_table(&sel3350_gpios_table); + + for (i = 0; i < ARRAY_SIZE(sel3350_leds); ++i) { + sel3350_leds[i].gpiod = devm_gpiod_get(&pdev->dev, + sel3350_leds_gpio_names[i], + GPIOD_ASIS); + if (IS_ERR_OR_NULL(sel3350_leds[i].gpiod)) { + rs = -EPROBE_DEFER; + goto err_gpio_loop; + } + gpiod_set_consumer_name(sel3350_leds[i].gpiod, sel3350_leds[i].name); + } + + sel3350->leds_pdev = platform_device_register_data( + NULL, + "leds-gpio", + PLATFORM_DEVID_NONE, + &sel3350_leds_pdata, + sizeof(sel3350_leds_pdata)); + if (IS_ERR(sel3350->leds_pdev)) { + rs = PTR_ERR(sel3350->leds_pdev); + dev_err(&pdev->dev, "Failed registering platform device: %d\n", rs); + goto err_platform; + } + + /* Power Supply A */ + sel3350->ps_a_cfg_data.ps_detect = devm_gpiod_get(&pdev->dev, + SEL_PS_A_DETECT, + GPIOD_IN); + sel3350->ps_a_cfg_data.ps_good = devm_gpiod_get(&pdev->dev, + SEL_PS_A_GOOD, + GPIOD_IN); + ps_cfg.drv_data = &sel3350->ps_a_cfg_data; + sel3350->ps_a = devm_power_supply_register(&pdev->dev, + &sel3350_ps_a_desc, + &ps_cfg); + if (IS_ERR(sel3350->ps_a)) { + rs = PTR_ERR(sel3350->ps_a); + dev_err(&pdev->dev, "Failed registering power supply A: %d\n", rs); + goto err_ps; + } + + /* Power Supply B */ + sel3350->ps_b_cfg_data.ps_detect = devm_gpiod_get(&pdev->dev, + SEL_PS_B_DETECT, + GPIOD_IN); + sel3350->ps_b_cfg_data.ps_good = devm_gpiod_get(&pdev->dev, + SEL_PS_B_GOOD, + GPIOD_IN); + ps_cfg.drv_data = &sel3350->ps_b_cfg_data; + sel3350->ps_b = devm_power_supply_register(&pdev->dev, + &sel3350_ps_b_desc, + &ps_cfg); + if (IS_ERR(sel3350->ps_b)) { + rs = PTR_ERR(sel3350->ps_b); + dev_err(&pdev->dev, "Failed registering power supply B: %d\n", rs); + goto err_ps; + } + + return 0; + +err_gpio_loop: + while (i--) + devm_gpiod_put(&pdev->dev, sel3350_leds[i].gpiod); + goto err_platform; + +err_ps: + platform_device_unregister(sel3350->leds_pdev); +err_platform: + gpiod_remove_lookup_table(&sel3350_gpios_table); + + return rs; +} + +static void sel3350_remove(struct platform_device *pdev) +{ + struct sel3350_data *sel3350 = platform_get_drvdata(pdev); + + platform_device_unregister(sel3350->leds_pdev); + gpiod_remove_lookup_table(&sel3350_gpios_table); +} + +static const struct acpi_device_id sel3350_device_ids[] = { + { B2093_GPIO_ACPI_ID, 0 }, + { "", 0 }, +}; +MODULE_DEVICE_TABLE(acpi, sel3350_device_ids); + +static struct platform_driver sel3350_platform_driver = { + .probe = sel3350_probe, + .remove = sel3350_remove, + .driver = { + .name = "sel3350-platform", + .acpi_match_table = sel3350_device_ids, + }, +}; +module_platform_driver(sel3350_platform_driver); + +MODULE_AUTHOR("Schweitzer Engineering Laboratories"); +MODULE_DESCRIPTION("SEL-3350 platform driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_SOFTDEP("pre: pinctrl_broxton leds-gpio"); diff --git a/drivers/platform/x86/serdev_helpers.h b/drivers/platform/x86/serdev_helpers.h new file mode 100644 index 000000000..57eac7580 --- /dev/null +++ b/drivers/platform/x86/serdev_helpers.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * In some cases UART attached devices which require an in kernel driver, + * e.g. UART attached Bluetooth HCIs are described in the ACPI tables + * by an ACPI device with a broken or missing UartSerialBusV2() resource. + * + * This causes the kernel to create a /dev/ttyS# char-device for the UART + * instead of creating an in kernel serdev-controller + serdev-device pair + * for the in kernel driver. + * + * The quirk handling in acpi_quirk_skip_serdev_enumeration() makes the kernel + * create a serdev-controller device for these UARTs instead of a /dev/ttyS#. + * + * Instantiating the actual serdev-device to bind to is up to pdx86 code, + * this header provides a helper for getting the serdev-controller device. + */ +#include +#include +#include +#include +#include +#include + +static inline struct device * +get_serdev_controller_from_parent(struct device *ctrl_dev, + int serial_ctrl_port, + const char *serdev_ctrl_name) +{ + struct device *child; + char name[32]; + int i; + + /* Walk host -> uart-ctrl -> port -> serdev-ctrl */ + for (i = 0; i < 3; i++) { + switch (i) { + case 0: + snprintf(name, sizeof(name), "%s:0", dev_name(ctrl_dev)); + break; + case 1: + snprintf(name, sizeof(name), "%s.%d", + dev_name(ctrl_dev), serial_ctrl_port); + break; + case 2: + strscpy(name, serdev_ctrl_name, sizeof(name)); + break; + } + + child = device_find_child_by_name(ctrl_dev, name); + put_device(ctrl_dev); + if (!child) { + pr_err("error could not find '%s' device\n", name); + return ERR_PTR(-ENODEV); + } + + ctrl_dev = child; + } + + return ctrl_dev; +} + +static inline struct device * +get_serdev_controller(const char *serial_ctrl_hid, + const char *serial_ctrl_uid, + int serial_ctrl_port, + const char *serdev_ctrl_name) +{ + struct acpi_device *adev; + struct device *parent; + + adev = acpi_dev_get_first_match_dev(serial_ctrl_hid, serial_ctrl_uid, -1); + if (!adev) { + pr_err("error could not get %s/%s serial-ctrl adev\n", + serial_ctrl_hid, serial_ctrl_uid ?: "*"); + return ERR_PTR(-ENODEV); + } + + /* get_first_physical_node() returns a weak ref */ + parent = get_device(acpi_get_first_physical_node(adev)); + acpi_dev_put(adev); + if (!parent) { + pr_err("error could not get %s/%s serial-ctrl physical node\n", + serial_ctrl_hid, serial_ctrl_uid ?: "*"); + return ERR_PTR(-ENODEV); + } + + /* This puts our reference on parent and returns a ref on the ctrl */ + return get_serdev_controller_from_parent(parent, serial_ctrl_port, serdev_ctrl_name); +} diff --git a/drivers/platform/x86/serial-multi-instantiate.c b/drivers/platform/x86/serial-multi-instantiate.c new file mode 100644 index 000000000..3e89fcdd4 --- /dev/null +++ b/drivers/platform/x86/serial-multi-instantiate.c @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Serial multi-instantiate driver, pseudo driver to instantiate multiple + * client devices from a single fwnode. + * + * Copyright 2018 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define IRQ_RESOURCE_TYPE GENMASK(1, 0) +#define IRQ_RESOURCE_NONE 0 +#define IRQ_RESOURCE_GPIO 1 +#define IRQ_RESOURCE_APIC 2 +#define IRQ_RESOURCE_AUTO 3 +#define IRQ_RESOURCE_OPT BIT(2) + +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[]; +}; + +struct smi { + int i2c_num; + int spi_num; + struct i2c_client **i2c_devs; + struct spi_device **spi_devs; +}; + +static int smi_get_irq(struct platform_device *pdev, struct acpi_device *adev, + const struct smi_instance *inst) +{ + int ret; + + switch (inst->flags & IRQ_RESOURCE_TYPE) { + case IRQ_RESOURCE_AUTO: + ret = acpi_dev_gpio_irq_get(adev, inst->irq_idx); + if (ret > 0) { + dev_dbg(&pdev->dev, "Using gpio irq\n"); + break; + } + ret = platform_get_irq(pdev, inst->irq_idx); + if (ret > 0) { + dev_dbg(&pdev->dev, "Using platform irq\n"); + break; + } + if (inst->flags & IRQ_RESOURCE_OPT) { + dev_dbg(&pdev->dev, "No irq\n"); + return 0; + } + break; + case IRQ_RESOURCE_GPIO: + ret = acpi_dev_gpio_irq_get(adev, inst->irq_idx); + break; + case IRQ_RESOURCE_APIC: + ret = platform_get_irq(pdev, inst->irq_idx); + break; + default: + return 0; + } + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, "Error requesting irq at index %d\n", + inst->irq_idx); + + return ret; +} + +static void smi_devs_unregister(struct smi *smi) +{ +#if IS_REACHABLE(CONFIG_I2C) + while (smi->i2c_num--) + i2c_unregister_device(smi->i2c_devs[smi->i2c_num]); +#endif + + if (IS_REACHABLE(CONFIG_SPI)) { + while (smi->spi_num--) + spi_unregister_device(smi->spi_devs[smi->spi_num]); + } +} + +/** + * smi_spi_probe - Instantiate multiple SPI devices from inst array + * @pdev: Platform device + * @smi: Internal struct for Serial multi instantiate driver + * @inst_array: Array of instances to probe + * + * Returns the number of SPI devices instantiate, Zero if none is found or a negative error code. + */ +static int smi_spi_probe(struct platform_device *pdev, struct smi *smi, + const struct smi_instance *inst_array) +{ + struct device *dev = &pdev->dev; + struct acpi_device *adev = ACPI_COMPANION(dev); + struct spi_controller *ctlr; + struct spi_device *spi_dev; + char name[50]; + int i, ret, count; + + ret = acpi_spi_count_resources(adev); + if (ret < 0) + return ret; + if (!ret) + return -ENOENT; + + count = ret; + + smi->spi_devs = devm_kcalloc(dev, count, sizeof(*smi->spi_devs), GFP_KERNEL); + if (!smi->spi_devs) + return -ENOMEM; + + for (i = 0; i < count && inst_array[i].type; i++) { + + spi_dev = acpi_spi_device_alloc(NULL, adev, i); + if (IS_ERR(spi_dev)) { + ret = dev_err_probe(dev, PTR_ERR(spi_dev), "failed to allocate SPI device %s from ACPI\n", + dev_name(&adev->dev)); + goto error; + } + + ctlr = spi_dev->controller; + + strscpy(spi_dev->modalias, inst_array[i].type); + + ret = smi_get_irq(pdev, adev, &inst_array[i]); + if (ret < 0) { + spi_dev_put(spi_dev); + goto error; + } + spi_dev->irq = ret; + + snprintf(name, sizeof(name), "%s-%s-%s.%d", dev_name(&ctlr->dev), dev_name(dev), + inst_array[i].type, i); + spi_dev->dev.init_name = name; + + ret = spi_add_device(spi_dev); + if (ret) { + dev_err_probe(&ctlr->dev, ret, "failed to add SPI device %s from ACPI\n", + dev_name(&adev->dev)); + spi_dev_put(spi_dev); + goto error; + } + + dev_dbg(dev, "SPI device %s using chip select %u", name, + spi_get_chipselect(spi_dev, 0)); + + smi->spi_devs[i] = spi_dev; + smi->spi_num++; + } + + if (smi->spi_num < count) { + dev_dbg(dev, "Error finding driver, idx %d\n", i); + ret = -ENODEV; + goto error; + } + + dev_info(dev, "Instantiated %d SPI devices.\n", smi->spi_num); + + return 0; +error: + smi_devs_unregister(smi); + + return ret; +} + +/** + * smi_i2c_probe - Instantiate multiple I2C devices from inst array + * @pdev: Platform device + * @smi: Internal struct for Serial multi instantiate driver + * @inst_array: Array of instances to probe + * + * Returns the number of I2C devices instantiate, Zero if none is found or a negative error code. + */ +static int smi_i2c_probe(struct platform_device *pdev, struct smi *smi, + const struct smi_instance *inst_array) +{ + struct i2c_board_info board_info = {}; + struct device *dev = &pdev->dev; + struct acpi_device *adev = ACPI_COMPANION(dev); + char name[32]; + int i, ret, count; + + ret = i2c_acpi_client_count(adev); + if (ret < 0) + return ret; + if (!ret) + return -ENOENT; + + count = ret; + + smi->i2c_devs = devm_kcalloc(dev, count, sizeof(*smi->i2c_devs), GFP_KERNEL); + if (!smi->i2c_devs) + return -ENOMEM; + + for (i = 0; i < count && inst_array[i].type; i++) { + memset(&board_info, 0, sizeof(board_info)); + strscpy(board_info.type, inst_array[i].type); + snprintf(name, sizeof(name), "%s-%s.%d", dev_name(dev), inst_array[i].type, i); + board_info.dev_name = name; + + ret = smi_get_irq(pdev, adev, &inst_array[i]); + if (ret < 0) + goto error; + board_info.irq = ret; + + smi->i2c_devs[i] = i2c_acpi_new_device(dev, i, &board_info); + if (IS_ERR(smi->i2c_devs[i])) { + ret = dev_err_probe(dev, PTR_ERR(smi->i2c_devs[i]), + "Error creating i2c-client, idx %d\n", i); + goto error; + } + smi->i2c_num++; + } + if (smi->i2c_num < count) { + dev_dbg(dev, "Error finding driver, idx %d\n", i); + ret = -ENODEV; + goto error; + } + + dev_info(dev, "Instantiated %d I2C devices.\n", smi->i2c_num); + + return 0; +error: + smi_devs_unregister(smi); + + return ret; +} + +static int smi_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + const struct smi_node *node; + struct smi *smi; + int ret; + + node = device_get_match_data(dev); + if (!node) { + dev_dbg(dev, "Error ACPI match data is missing\n"); + return -ENODEV; + } + + smi = devm_kzalloc(dev, sizeof(*smi), GFP_KERNEL); + if (!smi) + return -ENOMEM; + + platform_set_drvdata(pdev, smi); + + switch (node->bus_type) { + case SMI_I2C: + if (IS_REACHABLE(CONFIG_I2C)) + return smi_i2c_probe(pdev, smi, node->instances); + + return -ENODEV; + case SMI_SPI: + if (IS_REACHABLE(CONFIG_SPI)) + return smi_spi_probe(pdev, smi, node->instances); + + return -ENODEV; + case SMI_AUTO_DETECT: + /* + * For backwards-compatibility with the existing nodes I2C + * is checked first and if such entries are found ONLY I2C + * devices are created. Since some existing nodes that were + * already handled by this driver could also contain unrelated + * SpiSerialBus nodes that were previously ignored, and this + * preserves that behavior. + */ + if (IS_REACHABLE(CONFIG_I2C)) { + ret = smi_i2c_probe(pdev, smi, node->instances); + if (ret != -ENOENT) + return ret; + } + + if (IS_REACHABLE(CONFIG_SPI)) + return smi_spi_probe(pdev, smi, node->instances); + + return -ENODEV; + default: + return -EINVAL; + } +} + +static void smi_remove(struct platform_device *pdev) +{ + struct smi *smi = platform_get_drvdata(pdev); + + smi_devs_unregister(smi); +} + +static const struct smi_node aw88399_hda = { + .instances = { + { "aw88399-hda", IRQ_RESOURCE_AUTO, 0 }, + { "aw88399-hda", IRQ_RESOURCE_AUTO, 0 }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +static const struct smi_node bsg1160_data = { + .instances = { + { "bmc150_accel", IRQ_RESOURCE_GPIO, 0 }, + { "bmc150_magn" }, + { "bmg160" }, + {} + }, + .bus_type = SMI_I2C, +}; + +static const struct smi_node bsg2150_data = { + .instances = { + { "bmc150_accel", IRQ_RESOURCE_GPIO, 0 }, + { "bmc150_magn" }, + /* The resources describe a 3th client, but it is not really there. */ + { "bsg2150_dummy_dev" }, + {} + }, + .bus_type = SMI_I2C, +}; + +static const struct smi_node int3515_data = { + .instances = { + { "tps6598x", IRQ_RESOURCE_APIC, 0 }, + { "tps6598x", IRQ_RESOURCE_APIC, 1 }, + { "tps6598x", IRQ_RESOURCE_APIC, 2 }, + { "tps6598x", IRQ_RESOURCE_APIC, 3 }, + {} + }, + .bus_type = SMI_I2C, +}; + +static const struct smi_node cs35l41_hda = { + .instances = { + { "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +static const struct smi_node cs35l54_hda = { + .instances = { + { "cs35l54-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l54-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l54-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l54-hda", IRQ_RESOURCE_AUTO, 0 }, + /* a 5th entry is an alias address, not a real device */ + { "cs35l54-hda_dummy_dev" }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +static const struct smi_node cs35l56_hda = { + .instances = { + { "cs35l56-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l56-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l56-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l56-hda", IRQ_RESOURCE_AUTO, 0 }, + /* a 5th entry is an alias address, not a real device */ + { "cs35l56-hda_dummy_dev" }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +static const struct smi_node cs35l57_hda = { + .instances = { + { "cs35l57-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l57-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l57-hda", IRQ_RESOURCE_AUTO, 0 }, + { "cs35l57-hda", IRQ_RESOURCE_AUTO, 0 }, + /* a 5th entry is an alias address, not a real device */ + { "cs35l57-hda_dummy_dev" }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +static const struct smi_node tas2781_hda = { + .instances = { + { "tas2781-hda", IRQ_RESOURCE_AUTO | IRQ_RESOURCE_OPT, 0 }, + { "tas2781-hda", IRQ_RESOURCE_AUTO | IRQ_RESOURCE_OPT, 0 }, + { "tas2781-hda", IRQ_RESOURCE_AUTO | IRQ_RESOURCE_OPT, 0 }, + { "tas2781-hda", IRQ_RESOURCE_AUTO | IRQ_RESOURCE_OPT, 0 }, + {} + }, + .bus_type = SMI_AUTO_DETECT, +}; + +/* + * Note new device-ids must also be added to ignore_serial_bus_ids in + * drivers/acpi/scan.c: acpi_device_enumeration_by_parent(). + */ +static const struct acpi_device_id smi_acpi_ids[] = { + { "AWDZ8399", (unsigned long)&aw88399_hda }, + { "BSG1160", (unsigned long)&bsg1160_data }, + { "BSG2150", (unsigned long)&bsg2150_data }, + { "CSC3551", (unsigned long)&cs35l41_hda }, + { "CSC3554", (unsigned long)&cs35l54_hda }, + { "CSC3556", (unsigned long)&cs35l56_hda }, + { "CSC3557", (unsigned long)&cs35l57_hda }, + { "INT3515", (unsigned long)&int3515_data }, + { "TXNW2781", (unsigned long)&tas2781_hda }, + /* Non-conforming _HID for Cirrus Logic already released */ + { "CLSA0100", (unsigned long)&cs35l41_hda }, + { "CLSA0101", (unsigned long)&cs35l41_hda }, + { } +}; +MODULE_DEVICE_TABLE(acpi, smi_acpi_ids); + +static struct platform_driver smi_driver = { + .driver = { + .name = "Serial bus multi instantiate pseudo device driver", + .acpi_match_table = smi_acpi_ids, + }, + .probe = smi_probe, + .remove = smi_remove, +}; +module_platform_driver(smi_driver); + +MODULE_DESCRIPTION("Serial multi instantiate pseudo device driver"); +MODULE_AUTHOR("Hans de Goede "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/siemens/Kconfig b/drivers/platform/x86/siemens/Kconfig new file mode 100644 index 000000000..33d028c26 --- /dev/null +++ b/drivers/platform/x86/siemens/Kconfig @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Siemens X86 Platform Specific Drivers +# + +config SIEMENS_SIMATIC_IPC + tristate "Siemens Simatic IPC Class driver" + help + This Simatic IPC class driver is the central of several drivers. It + is mainly used for system identification, after which drivers in other + classes will take care of driving specifics of those machines. + i.e. LEDs and watchdog. + + To compile this driver as a module, choose M here: the module + will be called simatic-ipc. + +config SIEMENS_SIMATIC_IPC_BATT + tristate "CMOS battery driver for Siemens Simatic IPCs" + default SIEMENS_SIMATIC_IPC + depends on HWMON + depends on SIEMENS_SIMATIC_IPC + help + This option enables support for monitoring the voltage of the CMOS + batteries of several Industrial PCs from Siemens. + + To compile this driver as a module, choose M here: the module + will be called simatic-ipc-batt. + +config SIEMENS_SIMATIC_IPC_BATT_APOLLOLAKE + tristate "CMOS Battery monitoring for Simatic IPCs based on Apollo Lake GPIO" + default SIEMENS_SIMATIC_IPC_BATT + depends on PINCTRL_BROXTON + depends on SIEMENS_SIMATIC_IPC_BATT + help + This option enables CMOS battery monitoring for Simatic Industrial PCs + from Siemens based on Apollo Lake GPIO. + + To compile this driver as a module, choose M here: the module + will be called simatic-ipc-batt-apollolake. + +config SIEMENS_SIMATIC_IPC_BATT_ELKHARTLAKE + tristate "CMOS Battery monitoring for Simatic IPCs based on Elkhart Lake GPIO" + default SIEMENS_SIMATIC_IPC_BATT + depends on PINCTRL_ELKHARTLAKE + depends on SIEMENS_SIMATIC_IPC_BATT + help + This option enables CMOS battery monitoring for Simatic Industrial PCs + from Siemens based on Elkhart Lake GPIO. + + To compile this driver as a module, choose M here: the module + will be called simatic-ipc-batt-elkhartlake. + +config SIEMENS_SIMATIC_IPC_BATT_F7188X + tristate "CMOS Battery monitoring for Simatic IPCs based on Nuvoton GPIO" + default SIEMENS_SIMATIC_IPC_BATT + depends on GPIO_F7188X + depends on PINCTRL_ALDERLAKE + depends on SIEMENS_SIMATIC_IPC_BATT + help + This option enables CMOS battery monitoring for Simatic Industrial PCs + from Siemens based on Nuvoton GPIO. + + To compile this driver as a module, choose M here: the module + will be called simatic-ipc-batt-f7188x. diff --git a/drivers/platform/x86/siemens/Makefile b/drivers/platform/x86/siemens/Makefile new file mode 100644 index 000000000..2b384b4cb --- /dev/null +++ b/drivers/platform/x86/siemens/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for linux/drivers/platform/x86/siemens +# Siemens x86 Platform-Specific Drivers +# + +obj-$(CONFIG_SIEMENS_SIMATIC_IPC) += simatic-ipc.o +obj-$(CONFIG_SIEMENS_SIMATIC_IPC_BATT) += simatic-ipc-batt.o +obj-$(CONFIG_SIEMENS_SIMATIC_IPC_BATT_APOLLOLAKE) += simatic-ipc-batt-apollolake.o +obj-$(CONFIG_SIEMENS_SIMATIC_IPC_BATT_ELKHARTLAKE) += simatic-ipc-batt-elkhartlake.o +obj-$(CONFIG_SIEMENS_SIMATIC_IPC_BATT_F7188X) += simatic-ipc-batt-f7188x.o diff --git a/drivers/platform/x86/siemens/simatic-ipc-batt-apollolake.c b/drivers/platform/x86/siemens/simatic-ipc-batt-apollolake.c new file mode 100644 index 000000000..6ff6f3de4 --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc-batt-apollolake.c @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Siemens SIMATIC IPC driver for CMOS battery monitoring + * + * Copyright (c) Siemens AG, 2023 + * + * Authors: + * Henning Schild + */ + +#include +#include +#include +#include +#include + +#include "simatic-ipc-batt.h" + +static struct gpiod_lookup_table simatic_ipc_batt_gpio_table_127e = { + .table = { + GPIO_LOOKUP_IDX("apollolake-pinctrl.0", 55, NULL, 0, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("apollolake-pinctrl.0", 61, NULL, 1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("apollolake-pinctrl.1", 41, NULL, 2, GPIO_ACTIVE_HIGH), + {} /* Terminating entry */ + }, +}; + +static void simatic_ipc_batt_apollolake_remove(struct platform_device *pdev) +{ + simatic_ipc_batt_remove(pdev, &simatic_ipc_batt_gpio_table_127e); +} + +static int simatic_ipc_batt_apollolake_probe(struct platform_device *pdev) +{ + return simatic_ipc_batt_probe(pdev, &simatic_ipc_batt_gpio_table_127e); +} + +static struct platform_driver simatic_ipc_batt_driver = { + .probe = simatic_ipc_batt_apollolake_probe, + .remove = simatic_ipc_batt_apollolake_remove, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +module_platform_driver(simatic_ipc_batt_driver); + +MODULE_DESCRIPTION("CMOS Battery monitoring for Simatic IPCs based on Apollo Lake GPIO"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:" KBUILD_MODNAME); +MODULE_SOFTDEP("pre: simatic-ipc-batt platform:apollolake-pinctrl"); +MODULE_AUTHOR("Henning Schild "); diff --git a/drivers/platform/x86/siemens/simatic-ipc-batt-elkhartlake.c b/drivers/platform/x86/siemens/simatic-ipc-batt-elkhartlake.c new file mode 100644 index 000000000..83f532498 --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc-batt-elkhartlake.c @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Siemens SIMATIC IPC driver for CMOS battery monitoring + * + * Copyright (c) Siemens AG, 2023 + * + * Authors: + * Henning Schild + */ + +#include +#include +#include +#include +#include + +#include "simatic-ipc-batt.h" + +static struct gpiod_lookup_table simatic_ipc_batt_gpio_table_bx_21a = { + .table = { + GPIO_LOOKUP_IDX("INTC1020:04", 18, NULL, 0, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("INTC1020:04", 19, NULL, 1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("INTC1020:01", 66, NULL, 2, GPIO_ACTIVE_HIGH), + {} /* Terminating entry */ + }, +}; + +static void simatic_ipc_batt_elkhartlake_remove(struct platform_device *pdev) +{ + simatic_ipc_batt_remove(pdev, &simatic_ipc_batt_gpio_table_bx_21a); +} + +static int simatic_ipc_batt_elkhartlake_probe(struct platform_device *pdev) +{ + return simatic_ipc_batt_probe(pdev, &simatic_ipc_batt_gpio_table_bx_21a); +} + +static struct platform_driver simatic_ipc_batt_driver = { + .probe = simatic_ipc_batt_elkhartlake_probe, + .remove = simatic_ipc_batt_elkhartlake_remove, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +module_platform_driver(simatic_ipc_batt_driver); + +MODULE_DESCRIPTION("CMOS Battery monitoring for Simatic IPCs based on Elkhart Lake GPIO"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:" KBUILD_MODNAME); +MODULE_SOFTDEP("pre: simatic-ipc-batt platform:elkhartlake-pinctrl"); +MODULE_AUTHOR("Henning Schild "); diff --git a/drivers/platform/x86/siemens/simatic-ipc-batt-f7188x.c b/drivers/platform/x86/siemens/simatic-ipc-batt-f7188x.c new file mode 100644 index 000000000..c6a79338f --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc-batt-f7188x.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Siemens SIMATIC IPC driver for CMOS battery monitoring + * + * Copyright (c) Siemens AG, 2023 + * + * Authors: + * Henning Schild + */ + +#include +#include +#include +#include +#include +#include + +#include "simatic-ipc-batt.h" + +static struct gpiod_lookup_table *batt_lookup_table; + +static struct gpiod_lookup_table simatic_ipc_batt_gpio_table_227g = { + .table = { + GPIO_LOOKUP_IDX("gpio-f7188x-7", 6, NULL, 0, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("gpio-f7188x-7", 5, NULL, 1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("INTC1020:01", 66, NULL, 2, GPIO_ACTIVE_HIGH), + {} /* Terminating entry */ + }, +}; + +static struct gpiod_lookup_table simatic_ipc_batt_gpio_table_bx_39a = { + .table = { + GPIO_LOOKUP_IDX("gpio-f7188x-6", 4, NULL, 0, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("gpio-f7188x-6", 3, NULL, 1, GPIO_ACTIVE_HIGH), + {} /* Terminating entry */ + }, +}; + +static struct gpiod_lookup_table simatic_ipc_batt_gpio_table_bx_59a = { + .table = { + GPIO_LOOKUP_IDX("gpio-f7188x-7", 6, NULL, 0, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("gpio-f7188x-7", 5, NULL, 1, GPIO_ACTIVE_HIGH), + GPIO_LOOKUP_IDX("INTC1056:00", 438, NULL, 2, GPIO_ACTIVE_HIGH), + {} /* Terminating entry */ + } +}; + +static void simatic_ipc_batt_f7188x_remove(struct platform_device *pdev) +{ + simatic_ipc_batt_remove(pdev, batt_lookup_table); +} + +static int simatic_ipc_batt_f7188x_probe(struct platform_device *pdev) +{ + const struct simatic_ipc_platform *plat = pdev->dev.platform_data; + + switch (plat->devmode) { + case SIMATIC_IPC_DEVICE_227G: + batt_lookup_table = &simatic_ipc_batt_gpio_table_227g; + break; + case SIMATIC_IPC_DEVICE_BX_39A: + batt_lookup_table = &simatic_ipc_batt_gpio_table_bx_39a; + break; + case SIMATIC_IPC_DEVICE_BX_59A: + batt_lookup_table = &simatic_ipc_batt_gpio_table_bx_59a; + break; + default: + return -ENODEV; + } + + return simatic_ipc_batt_probe(pdev, batt_lookup_table); +} + +static struct platform_driver simatic_ipc_batt_driver = { + .probe = simatic_ipc_batt_f7188x_probe, + .remove = simatic_ipc_batt_f7188x_remove, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +module_platform_driver(simatic_ipc_batt_driver); + +MODULE_DESCRIPTION("CMOS Battery monitoring for Simatic IPCs based on Nuvoton GPIO"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:" KBUILD_MODNAME); +MODULE_SOFTDEP("pre: simatic-ipc-batt gpio_f7188x platform:elkhartlake-pinctrl platform:alderlake-pinctrl"); +MODULE_AUTHOR("Henning Schild "); diff --git a/drivers/platform/x86/siemens/simatic-ipc-batt.c b/drivers/platform/x86/siemens/simatic-ipc-batt.c new file mode 100644 index 000000000..7cfe991cb --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc-batt.c @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Siemens SIMATIC IPC driver for CMOS battery monitoring + * + * Copyright (c) Siemens AG, 2023 + * + * Authors: + * Gerd Haeussler + * Henning Schild + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "simatic-ipc-batt.h" + +#define BATT_DELAY_MS (1000 * 60 * 60 * 24) /* 24 h delay */ + +#define SIMATIC_IPC_BATT_LEVEL_FULL 3000 +#define SIMATIC_IPC_BATT_LEVEL_CRIT 2750 +#define SIMATIC_IPC_BATT_LEVEL_EMPTY 0 + +static struct simatic_ipc_batt { + u8 devmode; + long current_state; + struct gpio_desc *gpios[3]; + unsigned long last_updated_jiffies; +} priv; + +static long simatic_ipc_batt_read_gpio(void) +{ + long r = SIMATIC_IPC_BATT_LEVEL_FULL; + + if (priv.gpios[2]) { + gpiod_set_value(priv.gpios[2], 1); + msleep(150); + } + + if (gpiod_get_value_cansleep(priv.gpios[0])) + r = SIMATIC_IPC_BATT_LEVEL_EMPTY; + else if (gpiod_get_value_cansleep(priv.gpios[1])) + r = SIMATIC_IPC_BATT_LEVEL_CRIT; + + if (priv.gpios[2]) + gpiod_set_value(priv.gpios[2], 0); + + return r; +} + +#define SIMATIC_IPC_BATT_PORT_BASE 0x404D +static struct resource simatic_ipc_batt_io_res = + DEFINE_RES_IO_NAMED(SIMATIC_IPC_BATT_PORT_BASE, SZ_1, KBUILD_MODNAME); + +static long simatic_ipc_batt_read_io(struct device *dev) +{ + long r = SIMATIC_IPC_BATT_LEVEL_FULL; + struct resource *res = &simatic_ipc_batt_io_res; + u8 val; + + if (!request_muxed_region(res->start, resource_size(res), res->name)) { + dev_err(dev, "Unable to register IO resource at %pR\n", res); + return -EBUSY; + } + + val = inb(SIMATIC_IPC_BATT_PORT_BASE); + release_region(simatic_ipc_batt_io_res.start, resource_size(&simatic_ipc_batt_io_res)); + + if (val & (1 << 7)) + r = SIMATIC_IPC_BATT_LEVEL_EMPTY; + else if (val & (1 << 6)) + r = SIMATIC_IPC_BATT_LEVEL_CRIT; + + return r; +} + +static long simatic_ipc_batt_read_value(struct device *dev) +{ + unsigned long next_update; + + next_update = priv.last_updated_jiffies + msecs_to_jiffies(BATT_DELAY_MS); + if (time_after(jiffies, next_update) || !priv.last_updated_jiffies) { + if (priv.devmode == SIMATIC_IPC_DEVICE_227E) + priv.current_state = simatic_ipc_batt_read_io(dev); + else + priv.current_state = simatic_ipc_batt_read_gpio(); + + priv.last_updated_jiffies = jiffies; + if (priv.current_state < SIMATIC_IPC_BATT_LEVEL_FULL) + dev_warn(dev, "CMOS battery needs to be replaced.\n"); + } + + return priv.current_state; +} + +static int simatic_ipc_batt_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + switch (attr) { + case hwmon_in_input: + *val = simatic_ipc_batt_read_value(dev); + break; + case hwmon_in_lcrit: + *val = SIMATIC_IPC_BATT_LEVEL_CRIT; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static umode_t simatic_ipc_batt_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + if (attr == hwmon_in_input || attr == hwmon_in_lcrit) + return 0444; + + return 0; +} + +static const struct hwmon_ops simatic_ipc_batt_ops = { + .is_visible = simatic_ipc_batt_is_visible, + .read = simatic_ipc_batt_read, +}; + +static const struct hwmon_channel_info *simatic_ipc_batt_info[] = { + HWMON_CHANNEL_INFO(in, HWMON_I_INPUT | HWMON_I_LCRIT), + NULL +}; + +static const struct hwmon_chip_info simatic_ipc_batt_chip_info = { + .ops = &simatic_ipc_batt_ops, + .info = simatic_ipc_batt_info, +}; + +void simatic_ipc_batt_remove(struct platform_device *pdev, struct gpiod_lookup_table *table) +{ + gpiod_remove_lookup_table(table); +} +EXPORT_SYMBOL_GPL(simatic_ipc_batt_remove); + +int simatic_ipc_batt_probe(struct platform_device *pdev, struct gpiod_lookup_table *table) +{ + struct simatic_ipc_platform *plat; + struct device *dev = &pdev->dev; + struct device *hwmon_dev; + unsigned long flags; + int err; + + plat = pdev->dev.platform_data; + priv.devmode = plat->devmode; + + switch (priv.devmode) { + case SIMATIC_IPC_DEVICE_127E: + case SIMATIC_IPC_DEVICE_227G: + case SIMATIC_IPC_DEVICE_BX_39A: + case SIMATIC_IPC_DEVICE_BX_21A: + case SIMATIC_IPC_DEVICE_BX_59A: + table->dev_id = dev_name(dev); + gpiod_add_lookup_table(table); + break; + case SIMATIC_IPC_DEVICE_227E: + goto nogpio; + default: + return -ENODEV; + } + + priv.gpios[0] = devm_gpiod_get_index(dev, "CMOSBattery empty", 0, GPIOD_IN); + if (IS_ERR(priv.gpios[0])) { + err = PTR_ERR(priv.gpios[0]); + priv.gpios[0] = NULL; + goto out; + } + priv.gpios[1] = devm_gpiod_get_index(dev, "CMOSBattery low", 1, GPIOD_IN); + if (IS_ERR(priv.gpios[1])) { + err = PTR_ERR(priv.gpios[1]); + priv.gpios[1] = NULL; + goto out; + } + + if (table->table[2].key) { + flags = GPIOD_OUT_HIGH; + if (priv.devmode == SIMATIC_IPC_DEVICE_BX_21A || + priv.devmode == SIMATIC_IPC_DEVICE_BX_59A) + flags = GPIOD_OUT_LOW; + priv.gpios[2] = devm_gpiod_get_index(dev, "CMOSBattery meter", 2, flags); + if (IS_ERR(priv.gpios[2])) { + err = PTR_ERR(priv.gpios[2]); + priv.gpios[2] = NULL; + goto out; + } + } else { + priv.gpios[2] = NULL; + } + +nogpio: + hwmon_dev = devm_hwmon_device_register_with_info(dev, KBUILD_MODNAME, + &priv, + &simatic_ipc_batt_chip_info, + NULL); + if (IS_ERR(hwmon_dev)) { + err = PTR_ERR(hwmon_dev); + goto out; + } + + /* warn about aging battery even if userspace never reads hwmon */ + simatic_ipc_batt_read_value(dev); + + return 0; +out: + simatic_ipc_batt_remove(pdev, table); + + return err; +} +EXPORT_SYMBOL_GPL(simatic_ipc_batt_probe); + +static void simatic_ipc_batt_io_remove(struct platform_device *pdev) +{ + simatic_ipc_batt_remove(pdev, NULL); +} + +static int simatic_ipc_batt_io_probe(struct platform_device *pdev) +{ + return simatic_ipc_batt_probe(pdev, NULL); +} + +static struct platform_driver simatic_ipc_batt_driver = { + .probe = simatic_ipc_batt_io_probe, + .remove = simatic_ipc_batt_io_remove, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +module_platform_driver(simatic_ipc_batt_driver); + +MODULE_DESCRIPTION("CMOS core battery driver for Siemens Simatic IPCs"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:" KBUILD_MODNAME); +MODULE_AUTHOR("Henning Schild "); diff --git a/drivers/platform/x86/siemens/simatic-ipc-batt.h b/drivers/platform/x86/siemens/simatic-ipc-batt.h new file mode 100644 index 000000000..89891db26 --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc-batt.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Siemens SIMATIC IPC driver for CMOS battery monitoring + * + * Copyright (c) Siemens AG, 2023 + * + * Author: + * Henning Schild + */ + +#ifndef _SIMATIC_IPC_BATT_H +#define _SIMATIC_IPC_BATT_H + +int simatic_ipc_batt_probe(struct platform_device *pdev, + struct gpiod_lookup_table *table); + +void simatic_ipc_batt_remove(struct platform_device *pdev, + struct gpiod_lookup_table *table); + +#endif /* _SIMATIC_IPC_BATT_H */ diff --git a/drivers/platform/x86/siemens/simatic-ipc.c b/drivers/platform/x86/siemens/simatic-ipc.c new file mode 100644 index 000000000..7039874d8 --- /dev/null +++ b/drivers/platform/x86/siemens/simatic-ipc.c @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Siemens SIMATIC IPC platform driver + * + * Copyright (c) Siemens AG, 2018-2023 + * + * Authors: + * Henning Schild + * Jan Kiszka + * Gerd Haeussler + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include + +static struct platform_device *ipc_led_platform_device; +static struct platform_device *ipc_wdt_platform_device; +static struct platform_device *ipc_batt_platform_device; + +static const struct dmi_system_id simatic_ipc_whitelist[] = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SIEMENS AG"), + }, + }, + {} +}; + +static struct simatic_ipc_platform platform_data; + +#define SIMATIC_IPC_MAX_EXTRA_MODULES 2 + +static struct { + u32 station_id; + u8 led_mode; + u8 wdt_mode; + u8 batt_mode; + char *extra_modules[SIMATIC_IPC_MAX_EXTRA_MODULES]; +} device_modes[] = { + {SIMATIC_IPC_IPC127E, + SIMATIC_IPC_DEVICE_127E, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_127E, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC227D, + SIMATIC_IPC_DEVICE_227D, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_NONE, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC227E, + SIMATIC_IPC_DEVICE_427E, SIMATIC_IPC_DEVICE_227E, SIMATIC_IPC_DEVICE_227E, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC227G, + SIMATIC_IPC_DEVICE_227G, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_227G, + { "nct6775", "w83627hf_wdt" }}, + {SIMATIC_IPC_IPC277G, + SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_227G, + { "nct6775", "w83627hf_wdt" }}, + {SIMATIC_IPC_IPC277E, + SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_227E, SIMATIC_IPC_DEVICE_227E, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC427D, + SIMATIC_IPC_DEVICE_427E, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_NONE, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC427E, + SIMATIC_IPC_DEVICE_427E, SIMATIC_IPC_DEVICE_427E, SIMATIC_IPC_DEVICE_NONE, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPC477E, + SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_427E, SIMATIC_IPC_DEVICE_NONE, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPCBX_39A, + SIMATIC_IPC_DEVICE_227G, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_BX_39A, + { "nct6775", "w83627hf_wdt" }}, + {SIMATIC_IPC_IPCPX_39A, + SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_BX_39A, + { "nct6775", "w83627hf_wdt" }}, + {SIMATIC_IPC_IPCBX_21A, + SIMATIC_IPC_DEVICE_BX_21A, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_BX_21A, + { "emc1403", NULL }}, + {SIMATIC_IPC_IPCBX_56A, + SIMATIC_IPC_DEVICE_BX_59A, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_BX_59A, + { "emc1403", "w83627hf_wdt" }}, + {SIMATIC_IPC_IPCBX_59A, + SIMATIC_IPC_DEVICE_BX_59A, SIMATIC_IPC_DEVICE_NONE, SIMATIC_IPC_DEVICE_BX_59A, + { "emc1403", "w83627hf_wdt" }}, +}; + +static int register_platform_devices(u32 station_id) +{ + u8 ledmode = SIMATIC_IPC_DEVICE_NONE; + u8 wdtmode = SIMATIC_IPC_DEVICE_NONE; + u8 battmode = SIMATIC_IPC_DEVICE_NONE; + char *pdevname; + int i; + + for (i = 0; i < ARRAY_SIZE(device_modes); i++) { + if (device_modes[i].station_id == station_id) { + ledmode = device_modes[i].led_mode; + wdtmode = device_modes[i].wdt_mode; + battmode = device_modes[i].batt_mode; + break; + } + } + + if (battmode != SIMATIC_IPC_DEVICE_NONE) { + pdevname = KBUILD_MODNAME "_batt"; + if (battmode == SIMATIC_IPC_DEVICE_127E) + pdevname = KBUILD_MODNAME "_batt_apollolake"; + if (battmode == SIMATIC_IPC_DEVICE_BX_21A) + pdevname = KBUILD_MODNAME "_batt_elkhartlake"; + if (battmode == SIMATIC_IPC_DEVICE_227G || + battmode == SIMATIC_IPC_DEVICE_BX_39A || + battmode == SIMATIC_IPC_DEVICE_BX_59A) + pdevname = KBUILD_MODNAME "_batt_f7188x"; + platform_data.devmode = battmode; + ipc_batt_platform_device = + platform_device_register_data(NULL, pdevname, + PLATFORM_DEVID_NONE, &platform_data, + sizeof(struct simatic_ipc_platform)); + if (IS_ERR(ipc_batt_platform_device)) + return PTR_ERR(ipc_batt_platform_device); + + pr_debug("device=%s created\n", + ipc_batt_platform_device->name); + } + + if (ledmode != SIMATIC_IPC_DEVICE_NONE) { + pdevname = KBUILD_MODNAME "_leds"; + if (ledmode == SIMATIC_IPC_DEVICE_127E) + pdevname = KBUILD_MODNAME "_leds_gpio_apollolake"; + if (ledmode == SIMATIC_IPC_DEVICE_227G || ledmode == SIMATIC_IPC_DEVICE_BX_59A) + pdevname = KBUILD_MODNAME "_leds_gpio_f7188x"; + if (ledmode == SIMATIC_IPC_DEVICE_BX_21A) + pdevname = KBUILD_MODNAME "_leds_gpio_elkhartlake"; + platform_data.devmode = ledmode; + ipc_led_platform_device = + platform_device_register_data(NULL, + pdevname, PLATFORM_DEVID_NONE, + &platform_data, + sizeof(struct simatic_ipc_platform)); + if (IS_ERR(ipc_led_platform_device)) + return PTR_ERR(ipc_led_platform_device); + + pr_debug("device=%s created\n", + ipc_led_platform_device->name); + } + + if (wdtmode != SIMATIC_IPC_DEVICE_NONE) { + platform_data.devmode = wdtmode; + ipc_wdt_platform_device = + platform_device_register_data(NULL, + KBUILD_MODNAME "_wdt", PLATFORM_DEVID_NONE, + &platform_data, + sizeof(struct simatic_ipc_platform)); + if (IS_ERR(ipc_wdt_platform_device)) + return PTR_ERR(ipc_wdt_platform_device); + + pr_debug("device=%s created\n", + ipc_wdt_platform_device->name); + } + + if (ledmode == SIMATIC_IPC_DEVICE_NONE && + wdtmode == SIMATIC_IPC_DEVICE_NONE && + battmode == SIMATIC_IPC_DEVICE_NONE) { + pr_warn("unsupported IPC detected, station id=%08x\n", + station_id); + return -EINVAL; + } + + return 0; +} + +static void request_additional_modules(u32 station_id) +{ + char **extra_modules = NULL; + int i; + + for (i = 0; i < ARRAY_SIZE(device_modes); i++) { + if (device_modes[i].station_id == station_id) { + extra_modules = device_modes[i].extra_modules; + break; + } + } + + if (!extra_modules) + return; + + for (i = 0; i < SIMATIC_IPC_MAX_EXTRA_MODULES; i++) { + if (extra_modules[i]) + request_module(extra_modules[i]); + else + break; + } +} + +static int __init simatic_ipc_init_module(void) +{ + const struct dmi_system_id *match; + u32 station_id; + int err; + + match = dmi_first_match(simatic_ipc_whitelist); + if (!match) + return 0; + + err = dmi_walk(simatic_ipc_find_dmi_entry_helper, &station_id); + + if (err || station_id == SIMATIC_IPC_INVALID_STATION_ID) { + pr_warn("DMI entry %d not found\n", SIMATIC_IPC_DMI_ENTRY_OEM); + return 0; + } + + request_additional_modules(station_id); + + return register_platform_devices(station_id); +} + +static void __exit simatic_ipc_exit_module(void) +{ + platform_device_unregister(ipc_led_platform_device); + ipc_led_platform_device = NULL; + + platform_device_unregister(ipc_wdt_platform_device); + ipc_wdt_platform_device = NULL; + + platform_device_unregister(ipc_batt_platform_device); + ipc_batt_platform_device = NULL; +} + +module_init(simatic_ipc_init_module); +module_exit(simatic_ipc_exit_module); + +MODULE_DESCRIPTION("Siemens SIMATIC IPC platform driver"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Gerd Haeussler "); +MODULE_ALIAS("dmi:*:svnSIEMENSAG:*"); diff --git a/drivers/platform/x86/silicom-platform.c b/drivers/platform/x86/silicom-platform.c new file mode 100644 index 000000000..266f7bc5e --- /dev/null +++ b/drivers/platform/x86/silicom-platform.c @@ -0,0 +1,996 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// silicom-platform.c - Silicom MEC170x platform driver +// +// Copyright (C) 2023 Henry Shi +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define MEC_POWER_CYCLE_ADDR 0x24 +#define MEC_EFUSE_LSB_ADDR 0x28 +#define MEC_GPIO_IN_POS 0x08 +#define MEC_IO_BASE 0x0800 +#define MEC_IO_LEN 0x8 +#define IO_REG_BANK 0x0 +#define DEFAULT_CHAN_LO 0 +#define DEFAULT_CHAN_HI 0 +#define DEFAULT_CHAN_LO_T 0xc +#define MEC_ADDR (MEC_IO_BASE + 0x02) +#define EC_ADDR_LSB MEC_ADDR +#define SILICOM_MEC_MAGIC 0x5a + +#define MEC_PORT_CHANNEL_MASK GENMASK(2, 0) +#define MEC_PORT_DWORD_OFFSET GENMASK(31, 3) +#define MEC_DATA_OFFSET_MASK GENMASK(1, 0) +#define MEC_PORT_OFFSET_MASK GENMASK(7, 2) + +#define MEC_TEMP_LOC GENMASK(31, 16) +#define MEC_VERSION_LOC GENMASK(15, 8) +#define MEC_VERSION_MAJOR GENMASK(15, 14) +#define MEC_VERSION_MINOR GENMASK(13, 8) + +#define EC_ADDR_MSB (MEC_IO_BASE + 0x3) +#define MEC_DATA_OFFSET(offset) (MEC_IO_BASE + 0x04 + (offset)) + +#define OFFSET_BIT_TO_CHANNEL(off, bit) ((((off) + 0x014) << 3) | (bit)) +#define CHANNEL_TO_OFFSET(chan) (((chan) >> 3) - 0x14) + +static DEFINE_MUTEX(mec_io_mutex); +static unsigned int efuse_status; +static unsigned int mec_uc_version; +static unsigned int power_cycle; + +static const struct hwmon_channel_info *silicom_fan_control_info[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL), + NULL +}; + +struct silicom_platform_info { + int io_base; + int io_len; + struct led_classdev_mc *led_info; + struct gpio_chip *gpiochip; + u8 *gpio_channels; + u16 ngpio; +}; + +static const char * const plat_0222_gpio_names[] = { + "AUTOM0_SFP_TX_FAULT", + "SLOT2_LED_OUT", + "SIM_M2_SLOT2_B_DET", + "SIM_M2_SLOT2_A_DET", + "SLOT1_LED_OUT", + "SIM_M2_SLOT1_B_DET", + "SIM_M2_SLOT1_A_DET", + "SLOT0_LED_OUT", + "WAN_SFP0_RX_LOS", + "WAN_SFP0_PRSNT_N", + "WAN_SFP0_TX_FAULT", + "AUTOM1_SFP_RX_LOS", + "AUTOM1_SFP_PRSNT_N", + "AUTOM1_SFP_TX_FAULT", + "AUTOM0_SFP_RX_LOS", + "AUTOM0_SFP_PRSNT_N", + "WAN_SFP1_RX_LOS", + "WAN_SFP1_PRSNT_N", + "WAN_SFP1_TX_FAULT", + "SIM_M2_SLOT1_MUX_SEL", + "W_DISABLE_M2_SLOT1_N", + "W_DISABLE_MPCIE_SLOT0_N", + "W_DISABLE_M2_SLOT0_N", + "BT_COMMAND_MODE", + "WAN_SFP1_TX_DISABLE", + "WAN_SFP0_TX_DISABLE", + "AUTOM1_SFP_TX_DISABLE", + "AUTOM0_SFP_TX_DISABLE", + "SIM_M2_SLOT2_MUX_SEL", + "W_DISABLE_M2_SLOT2_N", + "RST_CTL_M2_SLOT_1_N", + "RST_CTL_M2_SLOT_2_N", + "PM_USB_PWR_EN_BOT", + "PM_USB_PWR_EN_TOP", +}; + +static u8 plat_0222_gpio_channels[] = { + OFFSET_BIT_TO_CHANNEL(0x00, 0), + OFFSET_BIT_TO_CHANNEL(0x00, 1), + OFFSET_BIT_TO_CHANNEL(0x00, 2), + OFFSET_BIT_TO_CHANNEL(0x00, 3), + OFFSET_BIT_TO_CHANNEL(0x00, 4), + OFFSET_BIT_TO_CHANNEL(0x00, 5), + OFFSET_BIT_TO_CHANNEL(0x00, 6), + OFFSET_BIT_TO_CHANNEL(0x00, 7), + OFFSET_BIT_TO_CHANNEL(0x01, 0), + OFFSET_BIT_TO_CHANNEL(0x01, 1), + OFFSET_BIT_TO_CHANNEL(0x01, 2), + OFFSET_BIT_TO_CHANNEL(0x01, 3), + OFFSET_BIT_TO_CHANNEL(0x01, 4), + OFFSET_BIT_TO_CHANNEL(0x01, 5), + OFFSET_BIT_TO_CHANNEL(0x01, 6), + OFFSET_BIT_TO_CHANNEL(0x01, 7), + OFFSET_BIT_TO_CHANNEL(0x02, 0), + OFFSET_BIT_TO_CHANNEL(0x02, 1), + OFFSET_BIT_TO_CHANNEL(0x02, 2), + OFFSET_BIT_TO_CHANNEL(0x09, 0), + OFFSET_BIT_TO_CHANNEL(0x09, 1), + OFFSET_BIT_TO_CHANNEL(0x09, 2), + OFFSET_BIT_TO_CHANNEL(0x09, 3), + OFFSET_BIT_TO_CHANNEL(0x0a, 0), + OFFSET_BIT_TO_CHANNEL(0x0a, 1), + OFFSET_BIT_TO_CHANNEL(0x0a, 2), + OFFSET_BIT_TO_CHANNEL(0x0a, 3), + OFFSET_BIT_TO_CHANNEL(0x0a, 4), + OFFSET_BIT_TO_CHANNEL(0x0a, 5), + OFFSET_BIT_TO_CHANNEL(0x0a, 6), + OFFSET_BIT_TO_CHANNEL(0x0b, 0), + OFFSET_BIT_TO_CHANNEL(0x0b, 1), + OFFSET_BIT_TO_CHANNEL(0x0b, 2), + OFFSET_BIT_TO_CHANNEL(0x0b, 3), +}; + +static struct platform_device *silicom_platform_dev; +static struct led_classdev_mc *silicom_led_info __initdata; +static struct gpio_chip *silicom_gpiochip __initdata; +static u8 *silicom_gpio_channels __initdata; + +static int silicom_mec_port_get(unsigned int offset) +{ + unsigned short mec_data_addr; + unsigned short mec_port_addr; + u8 reg; + + mec_data_addr = FIELD_GET(MEC_PORT_DWORD_OFFSET, offset) & MEC_DATA_OFFSET_MASK; + mec_port_addr = FIELD_GET(MEC_PORT_DWORD_OFFSET, offset) & MEC_PORT_OFFSET_MASK; + + mutex_lock(&mec_io_mutex); + outb(mec_port_addr, MEC_ADDR); + reg = inb(MEC_DATA_OFFSET(mec_data_addr)); + mutex_unlock(&mec_io_mutex); + + return (reg >> (offset & MEC_PORT_CHANNEL_MASK)) & 0x01; +} + +static enum led_brightness silicom_mec_led_get(int channel) +{ + /* Outputs are active low */ + return silicom_mec_port_get(channel) ? LED_OFF : LED_ON; +} + +static void silicom_mec_port_set(int channel, int on) +{ + + unsigned short mec_data_addr; + unsigned short mec_port_addr; + u8 reg; + + mec_data_addr = FIELD_GET(MEC_PORT_DWORD_OFFSET, channel) & MEC_DATA_OFFSET_MASK; + mec_port_addr = FIELD_GET(MEC_PORT_DWORD_OFFSET, channel) & MEC_PORT_OFFSET_MASK; + + mutex_lock(&mec_io_mutex); + outb(mec_port_addr, MEC_ADDR); + reg = inb(MEC_DATA_OFFSET(mec_data_addr)); + /* Outputs are active low, so clear the bit for on, or set it for off */ + if (on) + reg &= ~(1 << (channel & MEC_PORT_CHANNEL_MASK)); + else + reg |= 1 << (channel & MEC_PORT_CHANNEL_MASK); + outb(reg, MEC_DATA_OFFSET(mec_data_addr)); + mutex_unlock(&mec_io_mutex); +} + +static enum led_brightness silicom_mec_led_mc_brightness_get(struct led_classdev *led_cdev) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev); + enum led_brightness brightness = LED_OFF; + int i; + + for (i = 0; i < mc_cdev->num_colors; i++) { + mc_cdev->subled_info[i].brightness = + silicom_mec_led_get(mc_cdev->subled_info[i].channel); + /* Mark the overall brightness as LED_ON if any of the subleds are on */ + if (mc_cdev->subled_info[i].brightness != LED_OFF) + brightness = LED_ON; + } + + return brightness; +} + +static void silicom_mec_led_mc_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev); + int i; + + led_mc_calc_color_components(mc_cdev, brightness); + for (i = 0; i < mc_cdev->num_colors; i++) { + silicom_mec_port_set(mc_cdev->subled_info[i].channel, + mc_cdev->subled_info[i].brightness); + } +} + +static int silicom_gpio_get_direction(struct gpio_chip *gc, + unsigned int offset) +{ + u8 *channels = gpiochip_get_data(gc); + + /* Input registers have offsets between [0x00, 0x07] */ + if (CHANNEL_TO_OFFSET(channels[offset]) < MEC_GPIO_IN_POS) + return GPIO_LINE_DIRECTION_IN; + + return GPIO_LINE_DIRECTION_OUT; +} + +static int silicom_gpio_direction_input(struct gpio_chip *gc, + unsigned int offset) +{ + int direction = silicom_gpio_get_direction(gc, offset); + + return direction == GPIO_LINE_DIRECTION_IN ? 0 : -EINVAL; +} + +static int silicom_gpio_set(struct gpio_chip *gc, unsigned int offset, + int value) +{ + u8 *channels = gpiochip_get_data(gc); + int channel = channels[offset]; + + silicom_mec_port_set(channel, !value); + + return 0; +} + +static int silicom_gpio_direction_output(struct gpio_chip *gc, + unsigned int offset, + int value) +{ + int direction = silicom_gpio_get_direction(gc, offset); + + if (direction == GPIO_LINE_DIRECTION_IN) + return -EINVAL; + + silicom_gpio_set(gc, offset, value); + + return 0; +} + +static int silicom_gpio_get(struct gpio_chip *gc, unsigned int offset) +{ + u8 *channels = gpiochip_get_data(gc); + int channel = channels[offset]; + + return silicom_mec_port_get(channel); +} + +static struct mc_subled plat_0222_wan_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_WHITE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 7), + }, + { + .color_index = LED_COLOR_ID_YELLOW, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 6), + }, + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 5), + }, +}; + +static struct mc_subled plat_0222_sys_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_WHITE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 4), + }, + { + .color_index = LED_COLOR_ID_AMBER, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 3), + }, + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 2), + }, +}; + +static struct mc_subled plat_0222_stat1_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 1), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 0), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 7), + }, + { + .color_index = LED_COLOR_ID_YELLOW, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 6), + }, +}; + +static struct mc_subled plat_0222_stat2_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 5), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 4), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 3), + }, + { + .color_index = LED_COLOR_ID_YELLOW, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 2), + }, +}; + +static struct mc_subled plat_0222_stat3_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 1), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 0), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0e, 1), + }, + { + .color_index = LED_COLOR_ID_YELLOW, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x0e, 0), + }, +}; + +static struct led_classdev_mc plat_0222_mc_led_info[] __initdata = { + { + .led_cdev = { + .name = "platled::wan", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(plat_0222_wan_mc_subled_info), + .subled_info = plat_0222_wan_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::sys", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(plat_0222_sys_mc_subled_info), + .subled_info = plat_0222_sys_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::stat1", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(plat_0222_stat1_mc_subled_info), + .subled_info = plat_0222_stat1_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::stat2", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(plat_0222_stat2_mc_subled_info), + .subled_info = plat_0222_stat2_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::stat3", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(plat_0222_stat3_mc_subled_info), + .subled_info = plat_0222_stat3_mc_subled_info, + }, + { }, +}; + +static struct gpio_chip silicom_gpio_chip = { + .label = "silicom-gpio", + .get_direction = silicom_gpio_get_direction, + .direction_input = silicom_gpio_direction_input, + .direction_output = silicom_gpio_direction_output, + .get = silicom_gpio_get, + .set = silicom_gpio_set, + .base = -1, + .ngpio = ARRAY_SIZE(plat_0222_gpio_channels), + .names = plat_0222_gpio_names, + /* + * We're using a mutex to protect the indirect access, so we can sleep + * if the lock blocks + */ + .can_sleep = true, +}; + +static struct silicom_platform_info silicom_plat_0222_cordoba_info __initdata = { + .io_base = MEC_IO_BASE, + .io_len = MEC_IO_LEN, + .led_info = plat_0222_mc_led_info, + .gpiochip = &silicom_gpio_chip, + .gpio_channels = plat_0222_gpio_channels, + /* + * The original generic cordoba does not have the last 4 outputs of the + * plat_0222 variant, the rest are the same, so use the same longer list, + * but ignore the last entries here + */ + .ngpio = ARRAY_SIZE(plat_0222_gpio_channels), + +}; + +static struct mc_subled cordoba_fp_left_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 6), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 5), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x09, 7), + }, + { + .color_index = LED_COLOR_ID_AMBER, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x09, 4), + }, +}; + +static struct mc_subled cordoba_fp_center_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 7), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 4), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 3), + }, + { + .color_index = LED_COLOR_ID_AMBER, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x09, 6), + }, +}; + +static struct mc_subled cordoba_fp_right_mc_subled_info[] __initdata = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 2), + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 1), + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x08, 0), + }, + { + .color_index = LED_COLOR_ID_AMBER, + .brightness = 1, + .intensity = 0, + .channel = OFFSET_BIT_TO_CHANNEL(0x09, 5), + }, +}; + +static struct led_classdev_mc cordoba_mc_led_info[] __initdata = { + { + .led_cdev = { + .name = "platled::fp_left", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(cordoba_fp_left_mc_subled_info), + .subled_info = cordoba_fp_left_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::fp_center", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(cordoba_fp_center_mc_subled_info), + .subled_info = cordoba_fp_center_mc_subled_info, + }, + { + .led_cdev = { + .name = "platled::fp_right", + .brightness = 0, + .max_brightness = 1, + .brightness_set = silicom_mec_led_mc_brightness_set, + .brightness_get = silicom_mec_led_mc_brightness_get, + }, + .num_colors = ARRAY_SIZE(cordoba_fp_right_mc_subled_info), + .subled_info = cordoba_fp_right_mc_subled_info, + }, + { }, +}; + +static struct silicom_platform_info silicom_generic_cordoba_info __initdata = { + .io_base = MEC_IO_BASE, + .io_len = MEC_IO_LEN, + .led_info = cordoba_mc_led_info, + .gpiochip = &silicom_gpio_chip, + .gpio_channels = plat_0222_gpio_channels, + .ngpio = ARRAY_SIZE(plat_0222_gpio_channels), +}; + +/* + * sysfs interface + */ +static ssize_t efuse_status_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u32 reg; + + mutex_lock(&mec_io_mutex); + /* Select memory region */ + outb(IO_REG_BANK, EC_ADDR_MSB); + outb(MEC_EFUSE_LSB_ADDR, EC_ADDR_LSB); + + /* Get current data from the address */ + reg = inl(MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); + mutex_unlock(&mec_io_mutex); + + efuse_status = reg & 0x1; + + return sysfs_emit(buf, "%u\n", efuse_status); +} +static DEVICE_ATTR_RO(efuse_status); + +static ssize_t uc_version_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int uc_version; + u32 reg; + + mutex_lock(&mec_io_mutex); + outb(IO_REG_BANK, EC_ADDR_MSB); + outb(DEFAULT_CHAN_LO, EC_ADDR_LSB); + + reg = inl(MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); + mutex_unlock(&mec_io_mutex); + uc_version = FIELD_GET(MEC_VERSION_LOC, reg); + if (uc_version >= 192) + return -EINVAL; + + uc_version = FIELD_GET(MEC_VERSION_MAJOR, reg) * 100 + + FIELD_GET(MEC_VERSION_MINOR, reg); + + mec_uc_version = uc_version; + + return sysfs_emit(buf, "%u\n", mec_uc_version); +} +static DEVICE_ATTR_RO(uc_version); + +static ssize_t power_cycle_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u\n", power_cycle); +} + +static void powercycle_uc(void) +{ + /* Select memory region */ + outb(IO_REG_BANK, EC_ADDR_MSB); + outb(MEC_POWER_CYCLE_ADDR, EC_ADDR_LSB); + + /* Set to 1 for current data from the address */ + outb(1, MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); +} + +static ssize_t power_cycle_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int rc; + unsigned int power_cycle_cmd; + + rc = kstrtou32(buf, 0, &power_cycle_cmd); + if (rc) + return -EINVAL; + + if (power_cycle_cmd > 0) { + mutex_lock(&mec_io_mutex); + power_cycle = power_cycle_cmd; + powercycle_uc(); + mutex_unlock(&mec_io_mutex); + } + + return count; +} +static DEVICE_ATTR_RW(power_cycle); + +static struct attribute *silicom_attrs[] = { + &dev_attr_efuse_status.attr, + &dev_attr_uc_version.attr, + &dev_attr_power_cycle.attr, + NULL, +}; +ATTRIBUTE_GROUPS(silicom); + +static struct platform_driver silicom_platform_driver = { + .driver = { + .name = "silicom-platform", + .dev_groups = silicom_groups, + }, +}; + +static int __init silicom_mc_leds_register(struct device *dev, + const struct led_classdev_mc *mc_leds) +{ + int size = sizeof(struct mc_subled); + struct led_classdev_mc *led; + int i, err; + + for (i = 0; mc_leds[i].led_cdev.name; i++) { + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + memcpy(led, &mc_leds[i], sizeof(*led)); + + led->subled_info = devm_kzalloc(dev, led->num_colors * size, GFP_KERNEL); + if (!led->subled_info) + return -ENOMEM; + memcpy(led->subled_info, mc_leds[i].subled_info, led->num_colors * size); + + err = devm_led_classdev_multicolor_register(dev, led); + if (err) + return err; + } + + return 0; +} + +static u32 rpm_get(void) +{ + u32 reg; + + mutex_lock(&mec_io_mutex); + /* Select memory region */ + outb(IO_REG_BANK, EC_ADDR_MSB); + outb(DEFAULT_CHAN_LO_T, EC_ADDR_LSB); + reg = inw(MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); + mutex_unlock(&mec_io_mutex); + + return reg; +} + +static u32 temp_get(void) +{ + u32 reg; + + mutex_lock(&mec_io_mutex); + /* Select memory region */ + outb(IO_REG_BANK, EC_ADDR_MSB); + outb(DEFAULT_CHAN_LO_T, EC_ADDR_LSB); + reg = inl(MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); + mutex_unlock(&mec_io_mutex); + + return FIELD_GET(MEC_TEMP_LOC, reg) * 100; +} + +static umode_t silicom_fan_control_fan_is_visible(const u32 attr) +{ + switch (attr) { + case hwmon_fan_input: + case hwmon_fan_label: + return 0444; + default: + return 0; + } +} + +static umode_t silicom_fan_control_temp_is_visible(const u32 attr) +{ + switch (attr) { + case hwmon_temp_input: + case hwmon_temp_label: + return 0444; + default: + return 0; + } +} + +static int silicom_fan_control_read_fan(struct device *dev, u32 attr, long *val) +{ + switch (attr) { + case hwmon_fan_input: + *val = rpm_get(); + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int silicom_fan_control_read_temp(struct device *dev, u32 attr, long *val) +{ + switch (attr) { + case hwmon_temp_input: + *val = temp_get(); + return 0; + default: + return -EOPNOTSUPP; + } +} + +static umode_t silicom_fan_control_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + switch (type) { + case hwmon_fan: + return silicom_fan_control_fan_is_visible(attr); + case hwmon_temp: + return silicom_fan_control_temp_is_visible(attr); + default: + return 0; + } +} + +static int silicom_fan_control_read(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, int channel, + long *val) +{ + switch (type) { + case hwmon_fan: + return silicom_fan_control_read_fan(dev, attr, val); + case hwmon_temp: + return silicom_fan_control_read_temp(dev, attr, val); + default: + return -EOPNOTSUPP; + } +} + +static int silicom_fan_control_read_labels(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, int channel, + const char **str) +{ + switch (type) { + case hwmon_fan: + *str = "Silicom_platform: Fan Speed"; + return 0; + case hwmon_temp: + *str = "Silicom_platform: Thermostat Sensor"; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_ops silicom_fan_control_hwmon_ops = { + .is_visible = silicom_fan_control_is_visible, + .read = silicom_fan_control_read, + .read_string = silicom_fan_control_read_labels, +}; + +static const struct hwmon_chip_info silicom_chip_info = { + .ops = &silicom_fan_control_hwmon_ops, + .info = silicom_fan_control_info, +}; + +static int __init silicom_platform_probe(struct platform_device *device) +{ + struct device *hwmon_dev; + u8 magic, ver; + int err; + + if (!devm_request_region(&device->dev, MEC_IO_BASE, MEC_IO_LEN, "mec")) { + dev_err(&device->dev, "couldn't reserve MEC io ports\n"); + return -EBUSY; + } + + /* Sanity check magic number read for EC */ + outb(IO_REG_BANK, MEC_ADDR); + magic = inb(MEC_DATA_OFFSET(DEFAULT_CHAN_LO)); + ver = inb(MEC_DATA_OFFSET(DEFAULT_CHAN_HI)); + dev_dbg(&device->dev, "EC magic 0x%02x, version 0x%02x\n", magic, ver); + + if (magic != SILICOM_MEC_MAGIC) { + dev_err(&device->dev, "Bad EC magic 0x%02x!\n", magic); + return -ENODEV; + } + + err = silicom_mc_leds_register(&device->dev, silicom_led_info); + if (err) { + dev_err(&device->dev, "Failed to register LEDs\n"); + return err; + } + + err = devm_gpiochip_add_data(&device->dev, silicom_gpiochip, + silicom_gpio_channels); + if (err) { + dev_err(&device->dev, "Failed to register gpiochip: %d\n", err); + return err; + } + + hwmon_dev = devm_hwmon_device_register_with_info(&device->dev, "silicom_fan", NULL, + &silicom_chip_info, NULL); + err = PTR_ERR_OR_ZERO(hwmon_dev); + if (err) { + dev_err(&device->dev, "Failed to register hwmon_dev: %d\n", err); + return err; + } + + return err; +} + +static int __init silicom_platform_info_init(const struct dmi_system_id *id) +{ + struct silicom_platform_info *info = id->driver_data; + + silicom_led_info = info->led_info; + silicom_gpio_channels = info->gpio_channels; + silicom_gpiochip = info->gpiochip; + silicom_gpiochip->ngpio = info->ngpio; + + return 1; +} + +static const struct dmi_system_id silicom_dmi_ids[] __initconst = { + { + .callback = silicom_platform_info_init, + .ident = "Silicom Cordoba (Generic)", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Silicom"), + DMI_MATCH(DMI_BOARD_NAME, "80300-0214-G"), + }, + .driver_data = &silicom_generic_cordoba_info, + }, + { + .callback = silicom_platform_info_init, + .ident = "Silicom Cordoba (Generic)", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Silicom"), + DMI_MATCH(DMI_BOARD_NAME, "80500-0214-G"), + }, + .driver_data = &silicom_generic_cordoba_info, + }, + { + .callback = silicom_platform_info_init, + .ident = "Silicom Cordoba (plat_0222)", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Silicom"), + DMI_MATCH(DMI_BOARD_NAME, "80300-0222-G"), + }, + .driver_data = &silicom_plat_0222_cordoba_info, + }, + { }, +}; +MODULE_DEVICE_TABLE(dmi, silicom_dmi_ids); + +static int __init silicom_platform_init(void) +{ + if (!dmi_check_system(silicom_dmi_ids)) { + pr_err("No DMI match for this platform\n"); + return -ENODEV; + } + silicom_platform_dev = platform_create_bundle(&silicom_platform_driver, + silicom_platform_probe, + NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(silicom_platform_dev); +} + +static void __exit silicom_platform_exit(void) +{ + platform_device_unregister(silicom_platform_dev); + platform_driver_unregister(&silicom_platform_driver); +} + +module_init(silicom_platform_init); +module_exit(silicom_platform_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Henry Shi "); +MODULE_DESCRIPTION("Platform driver for Silicom network appliances"); diff --git a/drivers/platform/x86/sony-laptop.c b/drivers/platform/x86/sony-laptop.c new file mode 100644 index 000000000..67370967d --- /dev/null +++ b/drivers/platform/x86/sony-laptop.c @@ -0,0 +1,4750 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ACPI Sony Notebook Control Driver (SNC and SPIC) + * + * Copyright (C) 2004-2005 Stelian Pop + * Copyright (C) 2007-2009 Mattia Dongili + * + * Parts of this driver inspired from asus_acpi.c and ibm_acpi.c + * which are copyrighted by their respective authors. + * + * The SNY6001 driver part is based on the sonypi driver which includes + * material from: + * + * Copyright (C) 2001-2005 Stelian Pop + * + * Copyright (C) 2005 Narayanan R S + * + * Copyright (C) 2001-2002 Alcôve + * + * Copyright (C) 2001 Michael Ashley + * + * Copyright (C) 2001 Junichi Morita + * + * Copyright (C) 2000 Takaya Kinjo + * + * Copyright (C) 2000 Andrew Tridgell + * + * Earlier work by Werner Almesberger, Paul `Rusty' Russell and Paul Mackerras. + */ + +#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 +#ifdef CONFIG_SONYPI_COMPAT +#include +#include +#endif +#include +#include + +#define dprintk(fmt, ...) \ +do { \ + if (debug) \ + pr_warn(fmt, ##__VA_ARGS__); \ +} while (0) + +#define SONY_NC_CLASS "sony-nc" +#define SONY_NC_HID "SNY5001" +#define SONY_NC_DRIVER_NAME "Sony Notebook Control Driver" + +#define SONY_PIC_CLASS "sony-pic" +#define SONY_PIC_HID "SNY6001" +#define SONY_PIC_DRIVER_NAME "Sony Programmable IO Control Driver" + +MODULE_AUTHOR("Stelian Pop, Mattia Dongili"); +MODULE_DESCRIPTION("Sony laptop extras driver (SPIC and SNC ACPI device)"); +MODULE_LICENSE("GPL"); + +static int debug; +module_param(debug, int, 0); +MODULE_PARM_DESC(debug, "set this to 1 (and RTFM) if you want to help " + "the development of this driver"); + +static int no_spic; /* = 0 */ +module_param(no_spic, int, 0444); +MODULE_PARM_DESC(no_spic, + "set this if you don't want to enable the SPIC device"); + +static int compat; /* = 0 */ +module_param(compat, int, 0444); +MODULE_PARM_DESC(compat, + "set this if you want to enable backward compatibility mode"); + +static unsigned long mask = 0xffffffff; +module_param(mask, ulong, 0644); +MODULE_PARM_DESC(mask, + "set this to the mask of event you want to enable (see doc)"); + +static int camera; /* = 0 */ +module_param(camera, int, 0444); +MODULE_PARM_DESC(camera, + "set this to 1 to enable Motion Eye camera controls " + "(only use it if you have a C1VE or C1VN model)"); + +#ifdef CONFIG_SONYPI_COMPAT +static int minor = -1; +module_param(minor, int, 0); +MODULE_PARM_DESC(minor, + "minor number of the misc device for the SPIC compatibility code, " + "default is -1 (automatic)"); +#endif + +static int kbd_backlight = -1; +module_param(kbd_backlight, int, 0444); +MODULE_PARM_DESC(kbd_backlight, + "set this to 0 to disable keyboard backlight, " + "1 to enable it with automatic control and 2 to have it always " + "on (default: no change from current value)"); + +static int kbd_backlight_timeout = -1; +module_param(kbd_backlight_timeout, int, 0444); +MODULE_PARM_DESC(kbd_backlight_timeout, + "meaningful values vary from 0 to 3 and their meaning depends " + "on the model (default: no change from current value)"); + +#ifdef CONFIG_PM_SLEEP +static void sony_nc_thermal_resume(void); +#endif +static int sony_nc_kbd_backlight_setup(struct platform_device *pd, + unsigned int handle); +static void sony_nc_kbd_backlight_cleanup(struct platform_device *pd, + unsigned int handle); + +static int sony_nc_battery_care_setup(struct platform_device *pd, + unsigned int handle); +static void sony_nc_battery_care_cleanup(struct platform_device *pd); + +static int sony_nc_thermal_setup(struct platform_device *pd); +static void sony_nc_thermal_cleanup(struct platform_device *pd); + +static int sony_nc_lid_resume_setup(struct platform_device *pd, + unsigned int handle); +static void sony_nc_lid_resume_cleanup(struct platform_device *pd); + +static int sony_nc_gfx_switch_setup(struct platform_device *pd, + unsigned int handle); +static void sony_nc_gfx_switch_cleanup(struct platform_device *pd); +static int __sony_nc_gfx_switch_status_get(void); + +static int sony_nc_highspeed_charging_setup(struct platform_device *pd); +static void sony_nc_highspeed_charging_cleanup(struct platform_device *pd); + +static int sony_nc_lowbatt_setup(struct platform_device *pd); +static void sony_nc_lowbatt_cleanup(struct platform_device *pd); + +static int sony_nc_fanspeed_setup(struct platform_device *pd); +static void sony_nc_fanspeed_cleanup(struct platform_device *pd); + +static int sony_nc_usb_charge_setup(struct platform_device *pd); +static void sony_nc_usb_charge_cleanup(struct platform_device *pd); + +static int sony_nc_panelid_setup(struct platform_device *pd); +static void sony_nc_panelid_cleanup(struct platform_device *pd); + +static int sony_nc_smart_conn_setup(struct platform_device *pd); +static void sony_nc_smart_conn_cleanup(struct platform_device *pd); + +static int sony_nc_touchpad_setup(struct platform_device *pd, + unsigned int handle); +static void sony_nc_touchpad_cleanup(struct platform_device *pd); + +enum sony_nc_rfkill { + SONY_WIFI, + SONY_BLUETOOTH, + SONY_WWAN, + SONY_WIMAX, + N_SONY_RFKILL, +}; + +static int sony_rfkill_handle; +static struct rfkill *sony_rfkill_devices[N_SONY_RFKILL]; +static int sony_rfkill_address[N_SONY_RFKILL] = {0x300, 0x500, 0x700, 0x900}; +static int sony_nc_rfkill_setup(struct device *dev, unsigned int handle); +static void sony_nc_rfkill_cleanup(void); +static void sony_nc_rfkill_update(void); + +/*********** Input Devices ***********/ + +#define SONY_LAPTOP_BUF_SIZE 128 +struct sony_laptop_input_s { + atomic_t users; + struct input_dev *jog_dev; + struct input_dev *key_dev; + struct kfifo fifo; + spinlock_t fifo_lock; + struct timer_list release_key_timer; +}; + +static struct sony_laptop_input_s sony_laptop_input = { + .users = ATOMIC_INIT(0), +}; + +struct sony_laptop_keypress { + struct input_dev *dev; + int key; +}; + +/* Correspondance table between sonypi events + * and input layer indexes in the keymap + */ +static const int sony_laptop_input_index[] = { + -1, /* 0 no event */ + -1, /* 1 SONYPI_EVENT_JOGDIAL_DOWN */ + -1, /* 2 SONYPI_EVENT_JOGDIAL_UP */ + -1, /* 3 SONYPI_EVENT_JOGDIAL_DOWN_PRESSED */ + -1, /* 4 SONYPI_EVENT_JOGDIAL_UP_PRESSED */ + -1, /* 5 SONYPI_EVENT_JOGDIAL_PRESSED */ + -1, /* 6 SONYPI_EVENT_JOGDIAL_RELEASED */ + 0, /* 7 SONYPI_EVENT_CAPTURE_PRESSED */ + 1, /* 8 SONYPI_EVENT_CAPTURE_RELEASED */ + 2, /* 9 SONYPI_EVENT_CAPTURE_PARTIALPRESSED */ + 3, /* 10 SONYPI_EVENT_CAPTURE_PARTIALRELEASED */ + 4, /* 11 SONYPI_EVENT_FNKEY_ESC */ + 5, /* 12 SONYPI_EVENT_FNKEY_F1 */ + 6, /* 13 SONYPI_EVENT_FNKEY_F2 */ + 7, /* 14 SONYPI_EVENT_FNKEY_F3 */ + 8, /* 15 SONYPI_EVENT_FNKEY_F4 */ + 9, /* 16 SONYPI_EVENT_FNKEY_F5 */ + 10, /* 17 SONYPI_EVENT_FNKEY_F6 */ + 11, /* 18 SONYPI_EVENT_FNKEY_F7 */ + 12, /* 19 SONYPI_EVENT_FNKEY_F8 */ + 13, /* 20 SONYPI_EVENT_FNKEY_F9 */ + 14, /* 21 SONYPI_EVENT_FNKEY_F10 */ + 15, /* 22 SONYPI_EVENT_FNKEY_F11 */ + 16, /* 23 SONYPI_EVENT_FNKEY_F12 */ + 17, /* 24 SONYPI_EVENT_FNKEY_1 */ + 18, /* 25 SONYPI_EVENT_FNKEY_2 */ + 19, /* 26 SONYPI_EVENT_FNKEY_D */ + 20, /* 27 SONYPI_EVENT_FNKEY_E */ + 21, /* 28 SONYPI_EVENT_FNKEY_F */ + 22, /* 29 SONYPI_EVENT_FNKEY_S */ + 23, /* 30 SONYPI_EVENT_FNKEY_B */ + 24, /* 31 SONYPI_EVENT_BLUETOOTH_PRESSED */ + 25, /* 32 SONYPI_EVENT_PKEY_P1 */ + 26, /* 33 SONYPI_EVENT_PKEY_P2 */ + 27, /* 34 SONYPI_EVENT_PKEY_P3 */ + 28, /* 35 SONYPI_EVENT_BACK_PRESSED */ + -1, /* 36 SONYPI_EVENT_LID_CLOSED */ + -1, /* 37 SONYPI_EVENT_LID_OPENED */ + 29, /* 38 SONYPI_EVENT_BLUETOOTH_ON */ + 30, /* 39 SONYPI_EVENT_BLUETOOTH_OFF */ + 31, /* 40 SONYPI_EVENT_HELP_PRESSED */ + 32, /* 41 SONYPI_EVENT_FNKEY_ONLY */ + 33, /* 42 SONYPI_EVENT_JOGDIAL_FAST_DOWN */ + 34, /* 43 SONYPI_EVENT_JOGDIAL_FAST_UP */ + 35, /* 44 SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED */ + 36, /* 45 SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED */ + 37, /* 46 SONYPI_EVENT_JOGDIAL_VFAST_DOWN */ + 38, /* 47 SONYPI_EVENT_JOGDIAL_VFAST_UP */ + 39, /* 48 SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED */ + 40, /* 49 SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED */ + 41, /* 50 SONYPI_EVENT_ZOOM_PRESSED */ + 42, /* 51 SONYPI_EVENT_THUMBPHRASE_PRESSED */ + 43, /* 52 SONYPI_EVENT_MEYE_FACE */ + 44, /* 53 SONYPI_EVENT_MEYE_OPPOSITE */ + 45, /* 54 SONYPI_EVENT_MEMORYSTICK_INSERT */ + 46, /* 55 SONYPI_EVENT_MEMORYSTICK_EJECT */ + -1, /* 56 SONYPI_EVENT_ANYBUTTON_RELEASED */ + -1, /* 57 SONYPI_EVENT_BATTERY_INSERT */ + -1, /* 58 SONYPI_EVENT_BATTERY_REMOVE */ + -1, /* 59 SONYPI_EVENT_FNKEY_RELEASED */ + 47, /* 60 SONYPI_EVENT_WIRELESS_ON */ + 48, /* 61 SONYPI_EVENT_WIRELESS_OFF */ + 49, /* 62 SONYPI_EVENT_ZOOM_IN_PRESSED */ + 50, /* 63 SONYPI_EVENT_ZOOM_OUT_PRESSED */ + 51, /* 64 SONYPI_EVENT_CD_EJECT_PRESSED */ + 52, /* 65 SONYPI_EVENT_MODEKEY_PRESSED */ + 53, /* 66 SONYPI_EVENT_PKEY_P4 */ + 54, /* 67 SONYPI_EVENT_PKEY_P5 */ + 55, /* 68 SONYPI_EVENT_SETTINGKEY_PRESSED */ + 56, /* 69 SONYPI_EVENT_VOLUME_INC_PRESSED */ + 57, /* 70 SONYPI_EVENT_VOLUME_DEC_PRESSED */ + -1, /* 71 SONYPI_EVENT_BRIGHTNESS_PRESSED */ + 58, /* 72 SONYPI_EVENT_MEDIA_PRESSED */ + 59, /* 72 SONYPI_EVENT_VENDOR_PRESSED */ +}; + +static int sony_laptop_input_keycode_map[] = { + KEY_CAMERA, /* 0 SONYPI_EVENT_CAPTURE_PRESSED */ + KEY_RESERVED, /* 1 SONYPI_EVENT_CAPTURE_RELEASED */ + KEY_RESERVED, /* 2 SONYPI_EVENT_CAPTURE_PARTIALPRESSED */ + KEY_RESERVED, /* 3 SONYPI_EVENT_CAPTURE_PARTIALRELEASED */ + KEY_FN_ESC, /* 4 SONYPI_EVENT_FNKEY_ESC */ + KEY_FN_F1, /* 5 SONYPI_EVENT_FNKEY_F1 */ + KEY_FN_F2, /* 6 SONYPI_EVENT_FNKEY_F2 */ + KEY_FN_F3, /* 7 SONYPI_EVENT_FNKEY_F3 */ + KEY_FN_F4, /* 8 SONYPI_EVENT_FNKEY_F4 */ + KEY_FN_F5, /* 9 SONYPI_EVENT_FNKEY_F5 */ + KEY_FN_F6, /* 10 SONYPI_EVENT_FNKEY_F6 */ + KEY_FN_F7, /* 11 SONYPI_EVENT_FNKEY_F7 */ + KEY_FN_F8, /* 12 SONYPI_EVENT_FNKEY_F8 */ + KEY_FN_F9, /* 13 SONYPI_EVENT_FNKEY_F9 */ + KEY_FN_F10, /* 14 SONYPI_EVENT_FNKEY_F10 */ + KEY_FN_F11, /* 15 SONYPI_EVENT_FNKEY_F11 */ + KEY_FN_F12, /* 16 SONYPI_EVENT_FNKEY_F12 */ + KEY_FN_1, /* 17 SONYPI_EVENT_FNKEY_1 */ + KEY_FN_2, /* 18 SONYPI_EVENT_FNKEY_2 */ + KEY_FN_D, /* 19 SONYPI_EVENT_FNKEY_D */ + KEY_FN_E, /* 20 SONYPI_EVENT_FNKEY_E */ + KEY_FN_F, /* 21 SONYPI_EVENT_FNKEY_F */ + KEY_FN_S, /* 22 SONYPI_EVENT_FNKEY_S */ + KEY_FN_B, /* 23 SONYPI_EVENT_FNKEY_B */ + KEY_BLUETOOTH, /* 24 SONYPI_EVENT_BLUETOOTH_PRESSED */ + KEY_PROG1, /* 25 SONYPI_EVENT_PKEY_P1 */ + KEY_PROG2, /* 26 SONYPI_EVENT_PKEY_P2 */ + KEY_PROG3, /* 27 SONYPI_EVENT_PKEY_P3 */ + KEY_BACK, /* 28 SONYPI_EVENT_BACK_PRESSED */ + KEY_BLUETOOTH, /* 29 SONYPI_EVENT_BLUETOOTH_ON */ + KEY_BLUETOOTH, /* 30 SONYPI_EVENT_BLUETOOTH_OFF */ + KEY_HELP, /* 31 SONYPI_EVENT_HELP_PRESSED */ + KEY_FN, /* 32 SONYPI_EVENT_FNKEY_ONLY */ + KEY_RESERVED, /* 33 SONYPI_EVENT_JOGDIAL_FAST_DOWN */ + KEY_RESERVED, /* 34 SONYPI_EVENT_JOGDIAL_FAST_UP */ + KEY_RESERVED, /* 35 SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED */ + KEY_RESERVED, /* 36 SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED */ + KEY_RESERVED, /* 37 SONYPI_EVENT_JOGDIAL_VFAST_DOWN */ + KEY_RESERVED, /* 38 SONYPI_EVENT_JOGDIAL_VFAST_UP */ + KEY_RESERVED, /* 39 SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED */ + KEY_RESERVED, /* 40 SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED */ + KEY_ZOOM, /* 41 SONYPI_EVENT_ZOOM_PRESSED */ + BTN_THUMB, /* 42 SONYPI_EVENT_THUMBPHRASE_PRESSED */ + KEY_RESERVED, /* 43 SONYPI_EVENT_MEYE_FACE */ + KEY_RESERVED, /* 44 SONYPI_EVENT_MEYE_OPPOSITE */ + KEY_RESERVED, /* 45 SONYPI_EVENT_MEMORYSTICK_INSERT */ + KEY_RESERVED, /* 46 SONYPI_EVENT_MEMORYSTICK_EJECT */ + KEY_WLAN, /* 47 SONYPI_EVENT_WIRELESS_ON */ + KEY_WLAN, /* 48 SONYPI_EVENT_WIRELESS_OFF */ + KEY_ZOOMIN, /* 49 SONYPI_EVENT_ZOOM_IN_PRESSED */ + KEY_ZOOMOUT, /* 50 SONYPI_EVENT_ZOOM_OUT_PRESSED */ + KEY_EJECTCD, /* 51 SONYPI_EVENT_CD_EJECT_PRESSED */ + KEY_F13, /* 52 SONYPI_EVENT_MODEKEY_PRESSED */ + KEY_PROG4, /* 53 SONYPI_EVENT_PKEY_P4 */ + KEY_F14, /* 54 SONYPI_EVENT_PKEY_P5 */ + KEY_F15, /* 55 SONYPI_EVENT_SETTINGKEY_PRESSED */ + KEY_VOLUMEUP, /* 56 SONYPI_EVENT_VOLUME_INC_PRESSED */ + KEY_VOLUMEDOWN, /* 57 SONYPI_EVENT_VOLUME_DEC_PRESSED */ + KEY_MEDIA, /* 58 SONYPI_EVENT_MEDIA_PRESSED */ + KEY_VENDOR, /* 59 SONYPI_EVENT_VENDOR_PRESSED */ +}; + +/* release buttons after a short delay if pressed */ +static void do_sony_laptop_release_key(struct timer_list *unused) +{ + struct sony_laptop_keypress kp; + unsigned long flags; + + spin_lock_irqsave(&sony_laptop_input.fifo_lock, flags); + + if (kfifo_out(&sony_laptop_input.fifo, + (unsigned char *)&kp, sizeof(kp)) == sizeof(kp)) { + input_report_key(kp.dev, kp.key, 0); + input_sync(kp.dev); + } + + /* If there is something in the fifo schedule next release. */ + if (kfifo_len(&sony_laptop_input.fifo) != 0) + mod_timer(&sony_laptop_input.release_key_timer, + jiffies + msecs_to_jiffies(10)); + + spin_unlock_irqrestore(&sony_laptop_input.fifo_lock, flags); +} + +/* forward event to the input subsystem */ +static void sony_laptop_report_input_event(u8 event) +{ + struct input_dev *jog_dev = sony_laptop_input.jog_dev; + struct input_dev *key_dev = sony_laptop_input.key_dev; + struct sony_laptop_keypress kp = { NULL }; + int scancode = -1; + + if (event == SONYPI_EVENT_FNKEY_RELEASED || + event == SONYPI_EVENT_ANYBUTTON_RELEASED) { + /* Nothing, not all VAIOs generate this event */ + return; + } + + /* report events */ + switch (event) { + /* jog_dev events */ + case SONYPI_EVENT_JOGDIAL_UP: + case SONYPI_EVENT_JOGDIAL_UP_PRESSED: + input_report_rel(jog_dev, REL_WHEEL, 1); + input_sync(jog_dev); + return; + + case SONYPI_EVENT_JOGDIAL_DOWN: + case SONYPI_EVENT_JOGDIAL_DOWN_PRESSED: + input_report_rel(jog_dev, REL_WHEEL, -1); + input_sync(jog_dev); + return; + + /* key_dev events */ + case SONYPI_EVENT_JOGDIAL_PRESSED: + kp.key = BTN_MIDDLE; + kp.dev = jog_dev; + break; + + default: + if (event >= ARRAY_SIZE(sony_laptop_input_index)) { + dprintk("sony_laptop_report_input_event, event not known: %d\n", event); + break; + } + if ((scancode = sony_laptop_input_index[event]) != -1) { + kp.key = sony_laptop_input_keycode_map[scancode]; + if (kp.key != KEY_UNKNOWN) + kp.dev = key_dev; + } + break; + } + + if (kp.dev) { + /* if we have a scancode we emit it so we can always + remap the key */ + if (scancode != -1) + input_event(kp.dev, EV_MSC, MSC_SCAN, scancode); + input_report_key(kp.dev, kp.key, 1); + input_sync(kp.dev); + + /* schedule key release */ + kfifo_in_locked(&sony_laptop_input.fifo, + (unsigned char *)&kp, sizeof(kp), + &sony_laptop_input.fifo_lock); + mod_timer(&sony_laptop_input.release_key_timer, + jiffies + msecs_to_jiffies(10)); + } else + dprintk("unknown input event %.2x\n", event); +} + +static int sony_laptop_setup_input(struct device *parent) +{ + struct input_dev *jog_dev; + struct input_dev *key_dev; + int i; + int error; + + /* don't run again if already initialized */ + if (atomic_add_return(1, &sony_laptop_input.users) > 1) + return 0; + + /* kfifo */ + spin_lock_init(&sony_laptop_input.fifo_lock); + error = kfifo_alloc(&sony_laptop_input.fifo, + SONY_LAPTOP_BUF_SIZE, GFP_KERNEL); + if (error) { + pr_err("kfifo_alloc failed\n"); + goto err_dec_users; + } + + timer_setup(&sony_laptop_input.release_key_timer, + do_sony_laptop_release_key, 0); + + /* input keys */ + key_dev = input_allocate_device(); + if (!key_dev) { + error = -ENOMEM; + goto err_free_kfifo; + } + + key_dev->name = "Sony Vaio Keys"; + key_dev->id.bustype = BUS_ISA; + key_dev->id.vendor = PCI_VENDOR_ID_SONY; + key_dev->dev.parent = parent; + + /* Initialize the Input Drivers: special keys */ + input_set_capability(key_dev, EV_MSC, MSC_SCAN); + + __set_bit(EV_KEY, key_dev->evbit); + key_dev->keycodesize = sizeof(sony_laptop_input_keycode_map[0]); + key_dev->keycodemax = ARRAY_SIZE(sony_laptop_input_keycode_map); + key_dev->keycode = &sony_laptop_input_keycode_map; + for (i = 0; i < ARRAY_SIZE(sony_laptop_input_keycode_map); i++) + __set_bit(sony_laptop_input_keycode_map[i], key_dev->keybit); + __clear_bit(KEY_RESERVED, key_dev->keybit); + + error = input_register_device(key_dev); + if (error) + goto err_free_keydev; + + sony_laptop_input.key_dev = key_dev; + + /* jogdial */ + jog_dev = input_allocate_device(); + if (!jog_dev) { + error = -ENOMEM; + goto err_unregister_keydev; + } + + jog_dev->name = "Sony Vaio Jogdial"; + jog_dev->id.bustype = BUS_ISA; + jog_dev->id.vendor = PCI_VENDOR_ID_SONY; + jog_dev->dev.parent = parent; + + input_set_capability(jog_dev, EV_KEY, BTN_MIDDLE); + input_set_capability(jog_dev, EV_REL, REL_WHEEL); + + error = input_register_device(jog_dev); + if (error) + goto err_free_jogdev; + + sony_laptop_input.jog_dev = jog_dev; + + return 0; + +err_free_jogdev: + input_free_device(jog_dev); + +err_unregister_keydev: + input_unregister_device(key_dev); + /* to avoid kref underflow below at input_free_device */ + key_dev = NULL; + +err_free_keydev: + input_free_device(key_dev); + +err_free_kfifo: + kfifo_free(&sony_laptop_input.fifo); + +err_dec_users: + atomic_dec(&sony_laptop_input.users); + return error; +} + +static void sony_laptop_remove_input(void) +{ + struct sony_laptop_keypress kp = { NULL }; + + /* Cleanup only after the last user has gone */ + if (!atomic_dec_and_test(&sony_laptop_input.users)) + return; + + timer_delete_sync(&sony_laptop_input.release_key_timer); + + /* + * Generate key-up events for remaining keys. Note that we don't + * need locking since nobody is adding new events to the kfifo. + */ + while (kfifo_out(&sony_laptop_input.fifo, + (unsigned char *)&kp, sizeof(kp)) == sizeof(kp)) { + input_report_key(kp.dev, kp.key, 0); + input_sync(kp.dev); + } + + /* destroy input devs */ + input_unregister_device(sony_laptop_input.key_dev); + sony_laptop_input.key_dev = NULL; + + if (sony_laptop_input.jog_dev) { + input_unregister_device(sony_laptop_input.jog_dev); + sony_laptop_input.jog_dev = NULL; + } + + kfifo_free(&sony_laptop_input.fifo); +} + +/*********** Platform Device ***********/ + +static atomic_t sony_pf_users = ATOMIC_INIT(0); +static struct platform_driver sony_pf_driver = { + .driver = { + .name = "sony-laptop", + } +}; +static struct platform_device *sony_pf_device; + +static int sony_pf_add(void) +{ + int ret = 0; + + /* don't run again if already initialized */ + if (atomic_add_return(1, &sony_pf_users) > 1) + return 0; + + ret = platform_driver_register(&sony_pf_driver); + if (ret) + goto out; + + sony_pf_device = platform_device_alloc("sony-laptop", PLATFORM_DEVID_NONE); + if (!sony_pf_device) { + ret = -ENOMEM; + goto out_platform_registered; + } + + ret = platform_device_add(sony_pf_device); + if (ret) + goto out_platform_alloced; + + return 0; + + out_platform_alloced: + platform_device_put(sony_pf_device); + sony_pf_device = NULL; + out_platform_registered: + platform_driver_unregister(&sony_pf_driver); + out: + atomic_dec(&sony_pf_users); + return ret; +} + +static void sony_pf_remove(void) +{ + /* deregister only after the last user has gone */ + if (!atomic_dec_and_test(&sony_pf_users)) + return; + + platform_device_unregister(sony_pf_device); + platform_driver_unregister(&sony_pf_driver); +} + +/*********** SNC (SNY5001) Device ***********/ + +/* the device uses 1-based values, while the backlight subsystem uses + 0-based values */ +#define SONY_MAX_BRIGHTNESS 8 + +#define SNC_VALIDATE_IN 0 +#define SNC_VALIDATE_OUT 1 + +static ssize_t sony_nc_sysfs_show(struct device *, struct device_attribute *, + char *); +static ssize_t sony_nc_sysfs_store(struct device *, struct device_attribute *, + const char *, size_t); +static int boolean_validate(const int, const int); +static int brightness_default_validate(const int, const int); + +struct sony_nc_value { + char *name; /* name of the entry */ + char **acpiget; /* names of the ACPI get function */ + char **acpiset; /* names of the ACPI set function */ + int (*validate)(const int, const int); /* input/output validation */ + int value; /* current setting */ + int valid; /* Has ever been set */ + int debug; /* active only in debug mode ? */ + struct device_attribute devattr; /* sysfs attribute */ +}; + +#define SNC_HANDLE_NAMES(_name, _values...) \ + static char *snc_##_name[] = { _values, NULL } + +#define SNC_HANDLE(_name, _getters, _setters, _validate, _debug) \ + { \ + .name = __stringify(_name), \ + .acpiget = _getters, \ + .acpiset = _setters, \ + .validate = _validate, \ + .debug = _debug, \ + .devattr = __ATTR(_name, 0, sony_nc_sysfs_show, sony_nc_sysfs_store), \ + } + +#define SNC_HANDLE_NULL { .name = NULL } + +SNC_HANDLE_NAMES(fnkey_get, "GHKE"); + +SNC_HANDLE_NAMES(brightness_def_get, "GPBR"); +SNC_HANDLE_NAMES(brightness_def_set, "SPBR"); + +SNC_HANDLE_NAMES(cdpower_get, "GCDP"); +SNC_HANDLE_NAMES(cdpower_set, "SCDP", "CDPW"); + +SNC_HANDLE_NAMES(audiopower_get, "GAZP"); +SNC_HANDLE_NAMES(audiopower_set, "AZPW"); + +SNC_HANDLE_NAMES(lanpower_get, "GLNP"); +SNC_HANDLE_NAMES(lanpower_set, "LNPW"); + +SNC_HANDLE_NAMES(lidstate_get, "GLID"); + +SNC_HANDLE_NAMES(indicatorlamp_get, "GILS"); +SNC_HANDLE_NAMES(indicatorlamp_set, "SILS"); + +SNC_HANDLE_NAMES(gainbass_get, "GMGB"); +SNC_HANDLE_NAMES(gainbass_set, "CMGB"); + +SNC_HANDLE_NAMES(PID_get, "GPID"); + +SNC_HANDLE_NAMES(CTR_get, "GCTR"); +SNC_HANDLE_NAMES(CTR_set, "SCTR"); + +SNC_HANDLE_NAMES(PCR_get, "GPCR"); +SNC_HANDLE_NAMES(PCR_set, "SPCR"); + +SNC_HANDLE_NAMES(CMI_get, "GCMI"); +SNC_HANDLE_NAMES(CMI_set, "SCMI"); + +static struct sony_nc_value sony_nc_values[] = { + SNC_HANDLE(brightness_default, snc_brightness_def_get, + snc_brightness_def_set, brightness_default_validate, 0), + SNC_HANDLE(fnkey, snc_fnkey_get, NULL, NULL, 0), + SNC_HANDLE(cdpower, snc_cdpower_get, snc_cdpower_set, boolean_validate, 0), + SNC_HANDLE(audiopower, snc_audiopower_get, snc_audiopower_set, + boolean_validate, 0), + SNC_HANDLE(lanpower, snc_lanpower_get, snc_lanpower_set, + boolean_validate, 1), + SNC_HANDLE(lidstate, snc_lidstate_get, NULL, + boolean_validate, 0), + SNC_HANDLE(indicatorlamp, snc_indicatorlamp_get, snc_indicatorlamp_set, + boolean_validate, 0), + SNC_HANDLE(gainbass, snc_gainbass_get, snc_gainbass_set, + boolean_validate, 0), + /* unknown methods */ + SNC_HANDLE(PID, snc_PID_get, NULL, NULL, 1), + SNC_HANDLE(CTR, snc_CTR_get, snc_CTR_set, NULL, 1), + SNC_HANDLE(PCR, snc_PCR_get, snc_PCR_set, NULL, 1), + SNC_HANDLE(CMI, snc_CMI_get, snc_CMI_set, NULL, 1), + SNC_HANDLE_NULL +}; + +static acpi_handle sony_nc_acpi_handle; +static struct acpi_device *sony_nc_acpi_device = NULL; + +/* + * acpi_evaluate_object wrappers + * all useful calls into SNC methods take one or zero parameters and return + * integers or arrays. + */ +static union acpi_object *__call_snc_method(acpi_handle handle, char *method, + u64 *value) +{ + union acpi_object *result = NULL; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + if (value) { + struct acpi_object_list params; + union acpi_object in; + in.type = ACPI_TYPE_INTEGER; + in.integer.value = *value; + params.count = 1; + params.pointer = ∈ + status = acpi_evaluate_object(handle, method, ¶ms, &output); + dprintk("__call_snc_method: [%s:0x%.8x%.8x]\n", method, + (unsigned int)(*value >> 32), + (unsigned int)*value & 0xffffffff); + } else { + status = acpi_evaluate_object(handle, method, NULL, &output); + dprintk("__call_snc_method: [%s]\n", method); + } + + if (ACPI_FAILURE(status)) { + pr_err("Failed to evaluate [%s]\n", method); + return NULL; + } + + result = (union acpi_object *) output.pointer; + if (!result) + dprintk("No return object [%s]\n", method); + + return result; +} + +static int sony_nc_buffer_call(acpi_handle handle, char *name, u64 *value, + void *buffer, size_t buflen) +{ + int ret = 0; + size_t len; + union acpi_object *object = __call_snc_method(handle, name, value); + + if (!object) + return -EINVAL; + + if (!buffer) { + /* do nothing */ + } else if (object->type == ACPI_TYPE_BUFFER) { + len = MIN(buflen, object->buffer.length); + memset(buffer, 0, buflen); + memcpy(buffer, object->buffer.pointer, len); + + } else if (object->type == ACPI_TYPE_INTEGER) { + len = MIN(buflen, sizeof(object->integer.value)); + memset(buffer, 0, buflen); + memcpy(buffer, &object->integer.value, len); + + } else { + pr_warn("Unexpected acpi_object: 0x%x\n", object->type); + ret = -EINVAL; + } + + kfree(object); + return ret; +} + +static int sony_nc_int_call(acpi_handle handle, char *name, int *value, int + *result) +{ + int ret; + + if (value) { + u64 v = *value; + + ret = sony_nc_buffer_call(handle, name, &v, result, + sizeof(*result)); + } else { + ret = sony_nc_buffer_call(handle, name, NULL, result, + sizeof(*result)); + } + return ret; +} + +struct sony_nc_handles { + u16 cap[0x10]; + struct device_attribute devattr; +}; + +static struct sony_nc_handles *handles; + +static ssize_t sony_nc_handles_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + ssize_t len = 0; + int i; + + for (i = 0; i < ARRAY_SIZE(handles->cap); i++) { + len += sysfs_emit_at(buffer, len, "0x%.4x ", handles->cap[i]); + } + len += sysfs_emit_at(buffer, len, "\n"); + + return len; +} + +static int sony_nc_handles_setup(struct platform_device *pd) +{ + int i, r, result, arg; + + handles = kzalloc_obj(*handles); + if (!handles) + return -ENOMEM; + + for (i = 0; i < ARRAY_SIZE(handles->cap); i++) { + arg = i + 0x20; + r = sony_nc_int_call(sony_nc_acpi_handle, "SN00", &arg, + &result); + if (!r) { + dprintk("caching handle 0x%.4x (offset: 0x%.2x)\n", + result, i); + handles->cap[i] = result; + } + } + + if (debug) { + sysfs_attr_init(&handles->devattr.attr); + handles->devattr.attr.name = "handles"; + handles->devattr.attr.mode = S_IRUGO; + handles->devattr.show = sony_nc_handles_show; + + /* allow reading capabilities via sysfs */ + if (device_create_file(&pd->dev, &handles->devattr)) { + kfree(handles); + handles = NULL; + return -1; + } + } + + return 0; +} + +static int sony_nc_handles_cleanup(struct platform_device *pd) +{ + if (handles) { + if (debug) + device_remove_file(&pd->dev, &handles->devattr); + kfree(handles); + handles = NULL; + } + return 0; +} + +static int sony_find_snc_handle(int handle) +{ + int i; + + /* not initialized yet, return early */ + if (!handles || !handle) + return -EINVAL; + + for (i = 0; i < 0x10; i++) { + if (handles->cap[i] == handle) { + dprintk("found handle 0x%.4x (offset: 0x%.2x)\n", + handle, i); + return i; + } + } + dprintk("handle 0x%.4x not found\n", handle); + return -EINVAL; +} + +static int sony_call_snc_handle(int handle, int argument, int *result) +{ + int arg, ret = 0; + int offset = sony_find_snc_handle(handle); + + if (offset < 0) + return offset; + + arg = offset | argument; + ret = sony_nc_int_call(sony_nc_acpi_handle, "SN07", &arg, result); + dprintk("called SN07 with 0x%.4x (result: 0x%.4x)\n", arg, *result); + return ret; +} + +/* + * sony_nc_values input/output validate functions + */ + +/* brightness_default_validate: + * + * manipulate input output values to keep consistency with the + * backlight framework for which brightness values are 0-based. + */ +static int brightness_default_validate(const int direction, const int value) +{ + switch (direction) { + case SNC_VALIDATE_OUT: + return value - 1; + case SNC_VALIDATE_IN: + if (value >= 0 && value < SONY_MAX_BRIGHTNESS) + return value + 1; + } + return -EINVAL; +} + +/* boolean_validate: + * + * on input validate boolean values 0/1, on output just pass the + * received value. + */ +static int boolean_validate(const int direction, const int value) +{ + if (direction == SNC_VALIDATE_IN) { + if (value != 0 && value != 1) + return -EINVAL; + } + return value; +} + +/* + * Sysfs show/store common to all sony_nc_values + */ +static ssize_t sony_nc_sysfs_show(struct device *dev, struct device_attribute *attr, + char *buffer) +{ + int value, ret = 0; + struct sony_nc_value *item = + container_of(attr, struct sony_nc_value, devattr); + + if (!*item->acpiget) + return -EIO; + + ret = sony_nc_int_call(sony_nc_acpi_handle, *item->acpiget, NULL, + &value); + if (ret < 0) + return -EIO; + + if (item->validate) + value = item->validate(SNC_VALIDATE_OUT, value); + + return sysfs_emit(buffer, "%d\n", value); +} + +static ssize_t sony_nc_sysfs_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + int value; + int ret = 0; + struct sony_nc_value *item = + container_of(attr, struct sony_nc_value, devattr); + + if (!item->acpiset) + return -EIO; + + if (count > 31) + return -EINVAL; + + if (kstrtoint(buffer, 10, &value)) + return -EINVAL; + + if (item->validate) + value = item->validate(SNC_VALIDATE_IN, value); + + if (value < 0) + return value; + + ret = sony_nc_int_call(sony_nc_acpi_handle, *item->acpiset, + &value, NULL); + if (ret < 0) + return -EIO; + + item->value = value; + item->valid = 1; + return count; +} + + +/* + * Backlight device + */ +struct sony_backlight_props { + struct backlight_device *dev; + int handle; + int cmd_base; + u8 offset; + u8 maxlvl; +}; +static struct sony_backlight_props sony_bl_props; + +static int sony_backlight_update_status(struct backlight_device *bd) +{ + int arg = bd->props.brightness + 1; + return sony_nc_int_call(sony_nc_acpi_handle, "SBRT", &arg, NULL); +} + +static int sony_backlight_get_brightness(struct backlight_device *bd) +{ + int value; + + if (sony_nc_int_call(sony_nc_acpi_handle, "GBRT", NULL, &value)) + return 0; + /* brightness levels are 1-based, while backlight ones are 0-based */ + return value - 1; +} + +static int sony_nc_get_brightness_ng(struct backlight_device *bd) +{ + int result; + struct sony_backlight_props *sdev = + (struct sony_backlight_props *)bl_get_data(bd); + + sony_call_snc_handle(sdev->handle, sdev->cmd_base + 0x100, &result); + + return (result & 0xff) - sdev->offset; +} + +static int sony_nc_update_status_ng(struct backlight_device *bd) +{ + int value, result; + struct sony_backlight_props *sdev = + (struct sony_backlight_props *)bl_get_data(bd); + + value = bd->props.brightness + sdev->offset; + if (sony_call_snc_handle(sdev->handle, sdev->cmd_base | (value << 0x10), + &result)) + return -EIO; + + return value; +} + +static const struct backlight_ops sony_backlight_ops = { + .options = BL_CORE_SUSPENDRESUME, + .update_status = sony_backlight_update_status, + .get_brightness = sony_backlight_get_brightness, +}; +static const struct backlight_ops sony_backlight_ng_ops = { + .options = BL_CORE_SUSPENDRESUME, + .update_status = sony_nc_update_status_ng, + .get_brightness = sony_nc_get_brightness_ng, +}; + +/* + * New SNC-only Vaios event mapping to driver known keys + */ +struct sony_nc_event { + u8 data; + u8 event; +}; + +static struct sony_nc_event sony_100_events[] = { + { 0x90, SONYPI_EVENT_PKEY_P1 }, + { 0x10, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x91, SONYPI_EVENT_PKEY_P2 }, + { 0x11, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x81, SONYPI_EVENT_FNKEY_F1 }, + { 0x01, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x82, SONYPI_EVENT_FNKEY_F2 }, + { 0x02, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x83, SONYPI_EVENT_FNKEY_F3 }, + { 0x03, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x84, SONYPI_EVENT_FNKEY_F4 }, + { 0x04, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x85, SONYPI_EVENT_FNKEY_F5 }, + { 0x05, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x86, SONYPI_EVENT_FNKEY_F6 }, + { 0x06, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x87, SONYPI_EVENT_FNKEY_F7 }, + { 0x07, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x88, SONYPI_EVENT_FNKEY_F8 }, + { 0x08, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x89, SONYPI_EVENT_FNKEY_F9 }, + { 0x09, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x8A, SONYPI_EVENT_FNKEY_F10 }, + { 0x0A, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x8B, SONYPI_EVENT_FNKEY_F11 }, + { 0x0B, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x8C, SONYPI_EVENT_FNKEY_F12 }, + { 0x0C, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x9d, SONYPI_EVENT_ZOOM_PRESSED }, + { 0x1d, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x9f, SONYPI_EVENT_CD_EJECT_PRESSED }, + { 0x1f, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0xa1, SONYPI_EVENT_MEDIA_PRESSED }, + { 0x21, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0xa4, SONYPI_EVENT_CD_EJECT_PRESSED }, + { 0x24, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0xa5, SONYPI_EVENT_VENDOR_PRESSED }, + { 0x25, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0xa6, SONYPI_EVENT_HELP_PRESSED }, + { 0x26, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0xa8, SONYPI_EVENT_FNKEY_1 }, + { 0x28, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0, 0 }, +}; + +static struct sony_nc_event sony_127_events[] = { + { 0x81, SONYPI_EVENT_MODEKEY_PRESSED }, + { 0x01, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x82, SONYPI_EVENT_PKEY_P1 }, + { 0x02, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x83, SONYPI_EVENT_PKEY_P2 }, + { 0x03, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x84, SONYPI_EVENT_PKEY_P3 }, + { 0x04, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x85, SONYPI_EVENT_PKEY_P4 }, + { 0x05, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x86, SONYPI_EVENT_PKEY_P5 }, + { 0x06, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0x87, SONYPI_EVENT_SETTINGKEY_PRESSED }, + { 0x07, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0, 0 }, +}; + +static int sony_nc_hotkeys_decode(u32 event, unsigned int handle) +{ + int ret = -EINVAL; + unsigned int result = 0; + struct sony_nc_event *key_event; + + if (sony_call_snc_handle(handle, 0x200, &result)) { + dprintk("Unable to decode event 0x%.2x 0x%.2x\n", handle, + event); + return -EINVAL; + } + + result &= 0xFF; + + if (handle == 0x0100) + key_event = sony_100_events; + else + key_event = sony_127_events; + + for (; key_event->data; key_event++) { + if (key_event->data == result) { + ret = key_event->event; + break; + } + } + + if (!key_event->data) + pr_info("Unknown hotkey 0x%.2x/0x%.2x (handle 0x%.2x)\n", + event, result, handle); + + return ret; +} + +/* + * ACPI callbacks + */ +enum event_types { + HOTKEY = 1, + KILLSWITCH, + GFX_SWITCH +}; +static void sony_nc_notify(acpi_handle ah, u32 event, void *data) +{ + u32 real_ev = event; + u8 ev_type = 0; + int ret; + + dprintk("sony_nc_notify, event: 0x%.2x\n", event); + + if (event >= 0x90) { + unsigned int result = 0; + unsigned int arg = 0; + unsigned int handle = 0; + unsigned int offset = event - 0x90; + + if (offset >= ARRAY_SIZE(handles->cap)) { + pr_err("Event 0x%x outside of capabilities list\n", + event); + return; + } + handle = handles->cap[offset]; + + /* list of handles known for generating events */ + switch (handle) { + /* hotkey event */ + case 0x0100: + case 0x0127: + ev_type = HOTKEY; + ret = sony_nc_hotkeys_decode(event, handle); + + if (ret > 0) { + sony_laptop_report_input_event(ret); + real_ev = ret; + } + + break; + + /* wlan switch */ + case 0x0124: + case 0x0135: + /* events on this handle are reported when the + * switch changes position or for battery + * events. We'll notify both of them but only + * update the rfkill device status when the + * switch is moved. + */ + ev_type = KILLSWITCH; + sony_call_snc_handle(handle, 0x0100, &result); + real_ev = result & 0x03; + + /* hw switch event */ + if (real_ev == 1) + sony_nc_rfkill_update(); + + break; + + case 0x0128: + case 0x0146: + /* Hybrid GFX switching */ + sony_call_snc_handle(handle, 0x0000, &result); + dprintk("GFX switch event received (reason: %s)\n", + (result == 0x1) ? "switch change" : + (result == 0x2) ? "output switch" : + (result == 0x3) ? "output switch" : + ""); + + ev_type = GFX_SWITCH; + real_ev = __sony_nc_gfx_switch_status_get(); + break; + + case 0x015B: + /* Hybrid GFX switching SVS151290S */ + ev_type = GFX_SWITCH; + real_ev = __sony_nc_gfx_switch_status_get(); + break; + default: + dprintk("Unknown event 0x%x for handle 0x%x\n", + event, handle); + break; + } + + /* clear the event (and the event reason when present) */ + arg = 1 << offset; + sony_nc_int_call(sony_nc_acpi_handle, "SN05", &arg, &result); + + } else { + /* old style event */ + ev_type = HOTKEY; + sony_laptop_report_input_event(real_ev); + } + acpi_bus_generate_netlink_event(sony_nc_acpi_device->pnp.device_class, + dev_name(&sony_nc_acpi_device->dev), ev_type, real_ev); +} + +static acpi_status sony_walk_callback(acpi_handle handle, u32 level, + void *context, void **return_value) +{ + struct acpi_device_info *info; + + if (ACPI_SUCCESS(acpi_get_object_info(handle, &info))) { + pr_warn("method: name: %4.4s, args %X\n", + (char *)&info->name, info->param_count); + + kfree(info); + } + + return AE_OK; +} + +/* + * ACPI device + */ +static void sony_nc_function_setup(struct device *dev, + struct platform_device *pf_device) +{ + unsigned int i, result, bitmask, arg; + + if (!handles) + return; + + /* setup found handles here */ + for (i = 0; i < ARRAY_SIZE(handles->cap); i++) { + unsigned int handle = handles->cap[i]; + + if (!handle) + continue; + + dprintk("setting up handle 0x%.4x\n", handle); + + switch (handle) { + case 0x0100: + case 0x0101: + case 0x0127: + /* setup hotkeys */ + sony_call_snc_handle(handle, 0, &result); + break; + case 0x0102: + /* setup hotkeys */ + sony_call_snc_handle(handle, 0x100, &result); + break; + case 0x0105: + case 0x0148: + /* touchpad enable/disable */ + result = sony_nc_touchpad_setup(pf_device, handle); + if (result) + pr_err("couldn't set up touchpad control function (%d)\n", + result); + break; + case 0x0115: + case 0x0136: + case 0x013f: + result = sony_nc_battery_care_setup(pf_device, handle); + if (result) + pr_err("couldn't set up battery care function (%d)\n", + result); + break; + case 0x0119: + case 0x015D: + result = sony_nc_lid_resume_setup(pf_device, handle); + if (result) + pr_err("couldn't set up lid resume function (%d)\n", + result); + break; + case 0x0122: + result = sony_nc_thermal_setup(pf_device); + if (result) + pr_err("couldn't set up thermal profile function (%d)\n", + result); + break; + case 0x0128: + case 0x0146: + case 0x015B: + result = sony_nc_gfx_switch_setup(pf_device, handle); + if (result) + pr_err("couldn't set up GFX Switch status (%d)\n", + result); + break; + case 0x0131: + result = sony_nc_highspeed_charging_setup(pf_device); + if (result) + pr_err("couldn't set up high speed charging function (%d)\n", + result); + break; + case 0x0124: + case 0x0135: + result = sony_nc_rfkill_setup(dev, handle); + if (result) + pr_err("couldn't set up rfkill support (%d)\n", + result); + break; + case 0x0137: + case 0x0143: + case 0x014b: + case 0x014c: + case 0x0153: + case 0x0163: + result = sony_nc_kbd_backlight_setup(pf_device, handle); + if (result) + pr_err("couldn't set up keyboard backlight function (%d)\n", + result); + break; + case 0x0121: + result = sony_nc_lowbatt_setup(pf_device); + if (result) + pr_err("couldn't set up low battery function (%d)\n", + result); + break; + case 0x0149: + result = sony_nc_fanspeed_setup(pf_device); + if (result) + pr_err("couldn't set up fan speed function (%d)\n", + result); + break; + case 0x0155: + result = sony_nc_usb_charge_setup(pf_device); + if (result) + pr_err("couldn't set up USB charge support (%d)\n", + result); + break; + case 0x011D: + result = sony_nc_panelid_setup(pf_device); + if (result) + pr_err("couldn't set up panel ID function (%d)\n", + result); + break; + case 0x0168: + result = sony_nc_smart_conn_setup(pf_device); + if (result) + pr_err("couldn't set up smart connect support (%d)\n", + result); + break; + default: + continue; + } + } + + /* Enable all events */ + arg = 0x10; + if (!sony_nc_int_call(sony_nc_acpi_handle, "SN00", &arg, &bitmask)) + sony_nc_int_call(sony_nc_acpi_handle, "SN02", &bitmask, + &result); +} + +static void sony_nc_function_cleanup(struct platform_device *pd) +{ + unsigned int i, result, bitmask, handle; + + if (!handles) + return; + + /* get enabled events and disable them */ + sony_nc_int_call(sony_nc_acpi_handle, "SN01", NULL, &bitmask); + sony_nc_int_call(sony_nc_acpi_handle, "SN03", &bitmask, &result); + + /* cleanup handles here */ + for (i = 0; i < ARRAY_SIZE(handles->cap); i++) { + + handle = handles->cap[i]; + + if (!handle) + continue; + + switch (handle) { + case 0x0105: + case 0x0148: + sony_nc_touchpad_cleanup(pd); + break; + case 0x0115: + case 0x0136: + case 0x013f: + sony_nc_battery_care_cleanup(pd); + break; + case 0x0119: + case 0x015D: + sony_nc_lid_resume_cleanup(pd); + break; + case 0x0122: + sony_nc_thermal_cleanup(pd); + break; + case 0x0128: + case 0x0146: + case 0x015B: + sony_nc_gfx_switch_cleanup(pd); + break; + case 0x0131: + sony_nc_highspeed_charging_cleanup(pd); + break; + case 0x0124: + case 0x0135: + sony_nc_rfkill_cleanup(); + break; + case 0x0137: + case 0x0143: + case 0x014b: + case 0x014c: + case 0x0153: + case 0x0163: + sony_nc_kbd_backlight_cleanup(pd, handle); + break; + case 0x0121: + sony_nc_lowbatt_cleanup(pd); + break; + case 0x0149: + sony_nc_fanspeed_cleanup(pd); + break; + case 0x0155: + sony_nc_usb_charge_cleanup(pd); + break; + case 0x011D: + sony_nc_panelid_cleanup(pd); + break; + case 0x0168: + sony_nc_smart_conn_cleanup(pd); + break; + default: + continue; + } + } + + /* finally cleanup the handles list */ + sony_nc_handles_cleanup(pd); +} + +#ifdef CONFIG_PM_SLEEP +static void sony_nc_function_resume(void) +{ + unsigned int i, result, bitmask, arg; + + dprintk("Resuming SNC device\n"); + + for (i = 0; i < ARRAY_SIZE(handles->cap); i++) { + unsigned int handle = handles->cap[i]; + + if (!handle) + continue; + + switch (handle) { + case 0x0100: + case 0x0101: + case 0x0127: + /* re-enable hotkeys */ + sony_call_snc_handle(handle, 0, &result); + break; + case 0x0102: + /* re-enable hotkeys */ + sony_call_snc_handle(handle, 0x100, &result); + break; + case 0x0122: + sony_nc_thermal_resume(); + break; + case 0x0124: + case 0x0135: + sony_nc_rfkill_update(); + break; + default: + continue; + } + } + + /* Enable all events */ + arg = 0x10; + if (!sony_nc_int_call(sony_nc_acpi_handle, "SN00", &arg, &bitmask)) + sony_nc_int_call(sony_nc_acpi_handle, "SN02", &bitmask, + &result); +} + +static int sony_nc_resume(struct device *dev) +{ + struct sony_nc_value *item; + + for (item = sony_nc_values; item->name; item++) { + int ret; + + if (!item->valid) + continue; + ret = sony_nc_int_call(sony_nc_acpi_handle, *item->acpiset, + &item->value, NULL); + if (ret < 0) { + pr_err("%s: %d\n", __func__, ret); + break; + } + } + + if (acpi_has_method(sony_nc_acpi_handle, "ECON")) { + int arg = 1; + if (sony_nc_int_call(sony_nc_acpi_handle, "ECON", &arg, NULL)) + dprintk("ECON Method failed\n"); + } + + if (acpi_has_method(sony_nc_acpi_handle, "SN00")) + sony_nc_function_resume(); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(sony_nc_pm, NULL, sony_nc_resume); + +static void sony_nc_rfkill_cleanup(void) +{ + int i; + + for (i = 0; i < N_SONY_RFKILL; i++) { + if (sony_rfkill_devices[i]) { + rfkill_unregister(sony_rfkill_devices[i]); + rfkill_destroy(sony_rfkill_devices[i]); + } + } +} + +static int sony_nc_rfkill_set(void *data, bool blocked) +{ + int result; + int argument = sony_rfkill_address[(long) data] + 0x100; + + if (!blocked) + argument |= 0x070000; + + return sony_call_snc_handle(sony_rfkill_handle, argument, &result); +} + +static const struct rfkill_ops sony_rfkill_ops = { + .set_block = sony_nc_rfkill_set, +}; + +static int sony_nc_setup_rfkill(struct device *parent, enum sony_nc_rfkill nc_type) +{ + int err; + struct rfkill *rfk; + enum rfkill_type type; + const char *name; + int result; + bool hwblock, swblock; + + switch (nc_type) { + case SONY_WIFI: + type = RFKILL_TYPE_WLAN; + name = "sony-wifi"; + break; + case SONY_BLUETOOTH: + type = RFKILL_TYPE_BLUETOOTH; + name = "sony-bluetooth"; + break; + case SONY_WWAN: + type = RFKILL_TYPE_WWAN; + name = "sony-wwan"; + break; + case SONY_WIMAX: + type = RFKILL_TYPE_WIMAX; + name = "sony-wimax"; + break; + default: + return -EINVAL; + } + + rfk = rfkill_alloc(name, parent, type, &sony_rfkill_ops, (void *)nc_type); + if (!rfk) + return -ENOMEM; + + err = sony_call_snc_handle(sony_rfkill_handle, 0x200, &result); + if (err < 0) { + rfkill_destroy(rfk); + return err; + } + hwblock = !(result & 0x1); + + err = sony_call_snc_handle(sony_rfkill_handle, + sony_rfkill_address[nc_type], + &result); + if (err < 0) { + rfkill_destroy(rfk); + return err; + } + swblock = !(result & 0x2); + + rfkill_init_sw_state(rfk, swblock); + rfkill_set_hw_state(rfk, hwblock); + + err = rfkill_register(rfk); + if (err) { + rfkill_destroy(rfk); + return err; + } + sony_rfkill_devices[nc_type] = rfk; + return err; +} + +static void sony_nc_rfkill_update(void) +{ + enum sony_nc_rfkill i; + int result; + bool hwblock; + + sony_call_snc_handle(sony_rfkill_handle, 0x200, &result); + hwblock = !(result & 0x1); + + for (i = 0; i < N_SONY_RFKILL; i++) { + int argument = sony_rfkill_address[i]; + + if (!sony_rfkill_devices[i]) + continue; + + if (hwblock) { + if (rfkill_set_hw_state(sony_rfkill_devices[i], true)) { + /* we already know we're blocked */ + } + continue; + } + + sony_call_snc_handle(sony_rfkill_handle, argument, &result); + rfkill_set_states(sony_rfkill_devices[i], + !(result & 0x2), false); + } +} + +static int sony_nc_rfkill_setup(struct device *parent, unsigned int handle) +{ + u64 offset; + int i; + unsigned char buffer[32] = { 0 }; + + offset = sony_find_snc_handle(handle); + sony_rfkill_handle = handle; + + i = sony_nc_buffer_call(sony_nc_acpi_handle, "SN06", &offset, buffer, + 32); + if (i < 0) + return i; + + /* The buffer is filled with magic numbers describing the devices + * available, 0xff terminates the enumeration. + * Known codes: + * 0x00 WLAN + * 0x10 BLUETOOTH + * 0x20 WWAN GPRS-EDGE + * 0x21 WWAN HSDPA + * 0x22 WWAN EV-DO + * 0x23 WWAN GPS + * 0x25 Gobi WWAN no GPS + * 0x26 Gobi WWAN + GPS + * 0x28 Gobi WWAN no GPS + * 0x29 Gobi WWAN + GPS + * 0x30 WIMAX + * 0x50 Gobi WWAN no GPS + * 0x51 Gobi WWAN + GPS + * 0x70 no SIM card slot + * 0x71 SIM card slot + */ + for (i = 0; i < ARRAY_SIZE(buffer); i++) { + + if (buffer[i] == 0xff) + break; + + dprintk("Radio devices, found 0x%.2x\n", buffer[i]); + + if (buffer[i] == 0 && !sony_rfkill_devices[SONY_WIFI]) + sony_nc_setup_rfkill(parent, SONY_WIFI); + + if (buffer[i] == 0x10 && !sony_rfkill_devices[SONY_BLUETOOTH]) + sony_nc_setup_rfkill(parent, SONY_BLUETOOTH); + + if (((0xf0 & buffer[i]) == 0x20 || + (0xf0 & buffer[i]) == 0x50) && + !sony_rfkill_devices[SONY_WWAN]) + sony_nc_setup_rfkill(parent, SONY_WWAN); + + if (buffer[i] == 0x30 && !sony_rfkill_devices[SONY_WIMAX]) + sony_nc_setup_rfkill(parent, SONY_WIMAX); + } + return 0; +} + +/* Keyboard backlight feature */ +struct kbd_backlight { + unsigned int handle; + unsigned int base; + unsigned int mode; + unsigned int timeout; + unsigned int has_timeout; + struct device_attribute mode_attr; + struct device_attribute timeout_attr; +}; + +static struct kbd_backlight *kbdbl_ctl; + +static ssize_t __sony_nc_kbd_backlight_mode_set(u8 value) +{ + int result; + + if (value > 2) + return -EINVAL; + + if (sony_call_snc_handle(kbdbl_ctl->handle, + (value << 0x10) | (kbdbl_ctl->base), &result)) + return -EIO; + + /* Try to turn the light on/off immediately */ + if (value != 1) + sony_call_snc_handle(kbdbl_ctl->handle, + (value << 0x0f) | (kbdbl_ctl->base + 0x100), + &result); + + kbdbl_ctl->mode = value; + + return 0; +} + +static ssize_t sony_nc_kbd_backlight_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + int ret = 0; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + ret = __sony_nc_kbd_backlight_mode_set(value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t sony_nc_kbd_backlight_mode_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + return sysfs_emit(buffer, "%d\n", kbdbl_ctl->mode); +} + +static int __sony_nc_kbd_backlight_timeout_set(u8 value) +{ + int result; + + if (value > 3) + return -EINVAL; + + if (sony_call_snc_handle(kbdbl_ctl->handle, (value << 0x10) | + (kbdbl_ctl->base + 0x200), &result)) + return -EIO; + + kbdbl_ctl->timeout = value; + + return 0; +} + +static ssize_t sony_nc_kbd_backlight_timeout_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + int ret = 0; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + ret = __sony_nc_kbd_backlight_timeout_set(value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t sony_nc_kbd_backlight_timeout_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + return sysfs_emit(buffer, "%d\n", kbdbl_ctl->timeout); +} + +static int sony_nc_kbd_backlight_setup(struct platform_device *pd, + unsigned int handle) +{ + int result; + int probe_base = 0; + int ctl_base = 0; + int ret = 0; + + if (kbdbl_ctl) { + pr_warn("handle 0x%.4x: keyboard backlight setup already done for 0x%.4x\n", + handle, kbdbl_ctl->handle); + return -EBUSY; + } + + /* verify the kbd backlight presence, some of these handles are not used + * for keyboard backlight only + */ + switch (handle) { + case 0x0153: + probe_base = 0x0; + ctl_base = 0x0; + break; + case 0x0137: + probe_base = 0x0B00; + ctl_base = 0x0C00; + break; + default: + probe_base = 0x0100; + ctl_base = 0x4000; + break; + } + + /* + * Only probe if there is a separate probe_base, otherwise the probe call + * is equivalent to __sony_nc_kbd_backlight_mode_set(0), resulting in + * the keyboard backlight being turned off. + */ + if (probe_base) { + ret = sony_call_snc_handle(handle, probe_base, &result); + if (ret) + return ret; + + if ((handle == 0x0137 && !(result & 0x02)) || + !(result & 0x01)) { + dprintk("no backlight keyboard found\n"); + return 0; + } + } + + kbdbl_ctl = kzalloc_obj(*kbdbl_ctl); + if (!kbdbl_ctl) + return -ENOMEM; + + kbdbl_ctl->mode = kbd_backlight; + kbdbl_ctl->timeout = kbd_backlight_timeout; + kbdbl_ctl->handle = handle; + kbdbl_ctl->base = ctl_base; + /* Some models do not allow timeout control */ + kbdbl_ctl->has_timeout = handle != 0x0153; + + sysfs_attr_init(&kbdbl_ctl->mode_attr.attr); + kbdbl_ctl->mode_attr.attr.name = "kbd_backlight"; + kbdbl_ctl->mode_attr.attr.mode = S_IRUGO | S_IWUSR; + kbdbl_ctl->mode_attr.show = sony_nc_kbd_backlight_mode_show; + kbdbl_ctl->mode_attr.store = sony_nc_kbd_backlight_mode_store; + + ret = device_create_file(&pd->dev, &kbdbl_ctl->mode_attr); + if (ret) + goto outkzalloc; + + __sony_nc_kbd_backlight_mode_set(kbdbl_ctl->mode); + + if (kbdbl_ctl->has_timeout) { + sysfs_attr_init(&kbdbl_ctl->timeout_attr.attr); + kbdbl_ctl->timeout_attr.attr.name = "kbd_backlight_timeout"; + kbdbl_ctl->timeout_attr.attr.mode = S_IRUGO | S_IWUSR; + kbdbl_ctl->timeout_attr.show = + sony_nc_kbd_backlight_timeout_show; + kbdbl_ctl->timeout_attr.store = + sony_nc_kbd_backlight_timeout_store; + + ret = device_create_file(&pd->dev, &kbdbl_ctl->timeout_attr); + if (ret) + goto outmode; + + __sony_nc_kbd_backlight_timeout_set(kbdbl_ctl->timeout); + } + + + return 0; + +outmode: + device_remove_file(&pd->dev, &kbdbl_ctl->mode_attr); +outkzalloc: + kfree(kbdbl_ctl); + kbdbl_ctl = NULL; + return ret; +} + +static void sony_nc_kbd_backlight_cleanup(struct platform_device *pd, + unsigned int handle) +{ + if (kbdbl_ctl && handle == kbdbl_ctl->handle) { + device_remove_file(&pd->dev, &kbdbl_ctl->mode_attr); + if (kbdbl_ctl->has_timeout) + device_remove_file(&pd->dev, &kbdbl_ctl->timeout_attr); + kfree(kbdbl_ctl); + kbdbl_ctl = NULL; + } +} + +struct battery_care_control { + struct device_attribute attrs[2]; + unsigned int handle; +}; +static struct battery_care_control *bcare_ctl; + +static ssize_t sony_nc_battery_care_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result, cmd; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + /* limit values (2 bits): + * 00 - none + * 01 - 80% + * 10 - 50% + * 11 - 100% + * + * bit 0: 0 disable BCL, 1 enable BCL + * bit 1: 1 tell to store the battery limit (see bits 6,7) too + * bits 2,3: reserved + * bits 4,5: store the limit into the EC + * bits 6,7: store the limit into the battery + */ + cmd = 0; + + if (value > 0) { + if (value <= 50) + cmd = 0x20; + + else if (value <= 80) + cmd = 0x10; + + else if (value <= 100) + cmd = 0x30; + + else + return -EINVAL; + + /* + * handle 0x0115 should allow storing on battery too; + * handle 0x0136 same as 0x0115 + health status; + * handle 0x013f, same as 0x0136 but no storing on the battery + */ + if (bcare_ctl->handle != 0x013f) + cmd = cmd | (cmd << 2); + + cmd = (cmd | 0x1) << 0x10; + } + + if (sony_call_snc_handle(bcare_ctl->handle, cmd | 0x0100, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_battery_care_limit_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result, status; + + if (sony_call_snc_handle(bcare_ctl->handle, 0x0000, &result)) + return -EIO; + + status = (result & 0x01) ? ((result & 0x30) >> 0x04) : 0; + switch (status) { + case 1: + status = 80; + break; + case 2: + status = 50; + break; + case 3: + status = 100; + break; + default: + status = 0; + break; + } + + return sysfs_emit(buffer, "%d\n", status); +} + +static ssize_t sony_nc_battery_care_health_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int health; + + if (sony_call_snc_handle(bcare_ctl->handle, 0x0200, &health)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", health & 0xff); +} + +static int sony_nc_battery_care_setup(struct platform_device *pd, + unsigned int handle) +{ + int ret = 0; + + bcare_ctl = kzalloc_obj(struct battery_care_control); + if (!bcare_ctl) + return -ENOMEM; + + bcare_ctl->handle = handle; + + sysfs_attr_init(&bcare_ctl->attrs[0].attr); + bcare_ctl->attrs[0].attr.name = "battery_care_limiter"; + bcare_ctl->attrs[0].attr.mode = S_IRUGO | S_IWUSR; + bcare_ctl->attrs[0].show = sony_nc_battery_care_limit_show; + bcare_ctl->attrs[0].store = sony_nc_battery_care_limit_store; + + ret = device_create_file(&pd->dev, &bcare_ctl->attrs[0]); + if (ret) + goto outkzalloc; + + /* 0x0115 is for models with no health reporting capability */ + if (handle == 0x0115) + return 0; + + sysfs_attr_init(&bcare_ctl->attrs[1].attr); + bcare_ctl->attrs[1].attr.name = "battery_care_health"; + bcare_ctl->attrs[1].attr.mode = S_IRUGO; + bcare_ctl->attrs[1].show = sony_nc_battery_care_health_show; + + ret = device_create_file(&pd->dev, &bcare_ctl->attrs[1]); + if (ret) + goto outlimiter; + + return 0; + +outlimiter: + device_remove_file(&pd->dev, &bcare_ctl->attrs[0]); + +outkzalloc: + kfree(bcare_ctl); + bcare_ctl = NULL; + + return ret; +} + +static void sony_nc_battery_care_cleanup(struct platform_device *pd) +{ + if (bcare_ctl) { + device_remove_file(&pd->dev, &bcare_ctl->attrs[0]); + if (bcare_ctl->handle != 0x0115) + device_remove_file(&pd->dev, &bcare_ctl->attrs[1]); + + kfree(bcare_ctl); + bcare_ctl = NULL; + } +} + +struct snc_thermal_ctrl { + unsigned int mode; + unsigned int profiles; + struct device_attribute mode_attr; + struct device_attribute profiles_attr; +}; +static struct snc_thermal_ctrl *th_handle; + +#define THM_PROFILE_MAX 3 +static const char * const snc_thermal_profiles[] = { + "balanced", + "silent", + "performance" +}; + +static int sony_nc_thermal_mode_set(unsigned short mode) +{ + unsigned int result; + + /* the thermal profile seems to be a two bit bitmask: + * lsb -> silent + * msb -> performance + * no bit set is the normal operation and is always valid + * Some vaio models only have "balanced" and "performance" + */ + if ((mode && !(th_handle->profiles & mode)) || mode >= THM_PROFILE_MAX) + return -EINVAL; + + if (sony_call_snc_handle(0x0122, mode << 0x10 | 0x0200, &result)) + return -EIO; + + th_handle->mode = mode; + + return 0; +} + +static int sony_nc_thermal_mode_get(void) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0122, 0x0100, &result)) + return -EIO; + + return result & 0xff; +} + +static ssize_t sony_nc_thermal_profiles_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + short cnt; + size_t idx = 0; + + for (cnt = 0; cnt < THM_PROFILE_MAX; cnt++) { + if (!cnt || (th_handle->profiles & cnt)) + idx += sysfs_emit_at(buffer, idx, "%s ", snc_thermal_profiles[cnt]); + } + idx += sysfs_emit_at(buffer, idx, "\n"); + + return idx; +} + +static ssize_t sony_nc_thermal_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned short cmd; + size_t len = count; + + if (count == 0) + return -EINVAL; + + /* skip the newline if present */ + if (buffer[len - 1] == '\n') + len--; + + for (cmd = 0; cmd < THM_PROFILE_MAX; cmd++) + if (strncmp(buffer, snc_thermal_profiles[cmd], len) == 0) + break; + + if (sony_nc_thermal_mode_set(cmd)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_thermal_mode_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + int mode = sony_nc_thermal_mode_get(); + + if (mode < 0) + return mode; + + return sysfs_emit(buffer, "%s\n", snc_thermal_profiles[mode]); +} + +static int sony_nc_thermal_setup(struct platform_device *pd) +{ + int ret = 0; + th_handle = kzalloc_obj(struct snc_thermal_ctrl); + if (!th_handle) + return -ENOMEM; + + ret = sony_call_snc_handle(0x0122, 0x0000, &th_handle->profiles); + if (ret) { + pr_warn("couldn't to read the thermal profiles\n"); + goto outkzalloc; + } + + ret = sony_nc_thermal_mode_get(); + if (ret < 0) { + pr_warn("couldn't to read the current thermal profile"); + goto outkzalloc; + } + th_handle->mode = ret; + + sysfs_attr_init(&th_handle->profiles_attr.attr); + th_handle->profiles_attr.attr.name = "thermal_profiles"; + th_handle->profiles_attr.attr.mode = S_IRUGO; + th_handle->profiles_attr.show = sony_nc_thermal_profiles_show; + + sysfs_attr_init(&th_handle->mode_attr.attr); + th_handle->mode_attr.attr.name = "thermal_control"; + th_handle->mode_attr.attr.mode = S_IRUGO | S_IWUSR; + th_handle->mode_attr.show = sony_nc_thermal_mode_show; + th_handle->mode_attr.store = sony_nc_thermal_mode_store; + + ret = device_create_file(&pd->dev, &th_handle->profiles_attr); + if (ret) + goto outkzalloc; + + ret = device_create_file(&pd->dev, &th_handle->mode_attr); + if (ret) + goto outprofiles; + + return 0; + +outprofiles: + device_remove_file(&pd->dev, &th_handle->profiles_attr); +outkzalloc: + kfree(th_handle); + th_handle = NULL; + return ret; +} + +static void sony_nc_thermal_cleanup(struct platform_device *pd) +{ + if (th_handle) { + device_remove_file(&pd->dev, &th_handle->profiles_attr); + device_remove_file(&pd->dev, &th_handle->mode_attr); + kfree(th_handle); + th_handle = NULL; + } +} + +#ifdef CONFIG_PM_SLEEP +static void sony_nc_thermal_resume(void) +{ + int status; + + if (!th_handle) + return; + + status = sony_nc_thermal_mode_get(); + + if (status != th_handle->mode) + sony_nc_thermal_mode_set(th_handle->mode); +} +#endif + +/* resume on LID open */ +#define LID_RESUME_S5 0 +#define LID_RESUME_S4 1 +#define LID_RESUME_S3 2 +#define LID_RESUME_MAX 3 +struct snc_lid_resume_control { + struct device_attribute attrs[LID_RESUME_MAX]; + unsigned int status; + int handle; +}; +static struct snc_lid_resume_control *lid_ctl; + +static ssize_t sony_nc_lid_resume_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + unsigned int pos = LID_RESUME_S5; + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + /* the value we have to write to SNC is a bitmask: + * +--------------+ + * | S3 | S4 | S5 | + * +--------------+ + * 2 1 0 + */ + while (pos < LID_RESUME_MAX) { + if (&lid_ctl->attrs[pos].attr == &attr->attr) + break; + pos++; + } + if (pos == LID_RESUME_MAX) + return -EINVAL; + + if (value) + value = lid_ctl->status | (1 << pos); + else + value = lid_ctl->status & ~(1 << pos); + + if (sony_call_snc_handle(lid_ctl->handle, value << 0x10 | 0x0100, + &result)) + return -EIO; + + lid_ctl->status = value; + + return count; +} + +static ssize_t sony_nc_lid_resume_show(struct device *dev, + struct device_attribute *attr, + char *buffer) +{ + unsigned int pos = LID_RESUME_S5; + + while (pos < LID_RESUME_MAX) { + if (&lid_ctl->attrs[pos].attr == &attr->attr) + return sysfs_emit(buffer, "%d\n", + (lid_ctl->status >> pos) & 0x01); + pos++; + } + return -EINVAL; +} + +static int sony_nc_lid_resume_setup(struct platform_device *pd, + unsigned int handle) +{ + unsigned int result; + int i; + + if (sony_call_snc_handle(handle, 0x0000, &result)) + return -EIO; + + lid_ctl = kzalloc_obj(struct snc_lid_resume_control); + if (!lid_ctl) + return -ENOMEM; + + lid_ctl->status = result & 0x7; + lid_ctl->handle = handle; + + sysfs_attr_init(&lid_ctl->attrs[0].attr); + lid_ctl->attrs[LID_RESUME_S5].attr.name = "lid_resume_S5"; + lid_ctl->attrs[LID_RESUME_S5].attr.mode = S_IRUGO | S_IWUSR; + lid_ctl->attrs[LID_RESUME_S5].show = sony_nc_lid_resume_show; + lid_ctl->attrs[LID_RESUME_S5].store = sony_nc_lid_resume_store; + + if (handle == 0x0119) { + sysfs_attr_init(&lid_ctl->attrs[1].attr); + lid_ctl->attrs[LID_RESUME_S4].attr.name = "lid_resume_S4"; + lid_ctl->attrs[LID_RESUME_S4].attr.mode = S_IRUGO | S_IWUSR; + lid_ctl->attrs[LID_RESUME_S4].show = sony_nc_lid_resume_show; + lid_ctl->attrs[LID_RESUME_S4].store = sony_nc_lid_resume_store; + + sysfs_attr_init(&lid_ctl->attrs[2].attr); + lid_ctl->attrs[LID_RESUME_S3].attr.name = "lid_resume_S3"; + lid_ctl->attrs[LID_RESUME_S3].attr.mode = S_IRUGO | S_IWUSR; + lid_ctl->attrs[LID_RESUME_S3].show = sony_nc_lid_resume_show; + lid_ctl->attrs[LID_RESUME_S3].store = sony_nc_lid_resume_store; + } + for (i = 0; i < LID_RESUME_MAX && + lid_ctl->attrs[i].attr.name; i++) { + result = device_create_file(&pd->dev, &lid_ctl->attrs[i]); + if (result) + goto liderror; + } + + return 0; + +liderror: + for (i--; i >= 0; i--) + device_remove_file(&pd->dev, &lid_ctl->attrs[i]); + + kfree(lid_ctl); + lid_ctl = NULL; + + return result; +} + +static void sony_nc_lid_resume_cleanup(struct platform_device *pd) +{ + int i; + + if (lid_ctl) { + for (i = 0; i < LID_RESUME_MAX; i++) { + if (!lid_ctl->attrs[i].attr.name) + break; + + device_remove_file(&pd->dev, &lid_ctl->attrs[i]); + } + + kfree(lid_ctl); + lid_ctl = NULL; + } +} + +/* GFX Switch position */ +enum gfx_switch { + SPEED, + STAMINA, + AUTO +}; +struct snc_gfx_switch_control { + struct device_attribute attr; + unsigned int handle; +}; +static struct snc_gfx_switch_control *gfxs_ctl; + +/* returns 0 for speed, 1 for stamina */ +static int __sony_nc_gfx_switch_status_get(void) +{ + unsigned int result; + + if (sony_call_snc_handle(gfxs_ctl->handle, + gfxs_ctl->handle == 0x015B ? 0x0000 : 0x0100, + &result)) + return -EIO; + + switch (gfxs_ctl->handle) { + case 0x0146: + /* 1: discrete GFX (speed) + * 0: integrated GFX (stamina) + */ + return result & 0x1 ? SPEED : STAMINA; + case 0x015B: + /* 0: discrete GFX (speed) + * 1: integrated GFX (stamina) + */ + return result & 0x1 ? STAMINA : SPEED; + case 0x0128: + /* it's a more elaborated bitmask, for now: + * 2: integrated GFX (stamina) + * 0: discrete GFX (speed) + */ + dprintk("GFX Status: 0x%x\n", result); + return result & 0x80 ? AUTO : + result & 0x02 ? STAMINA : SPEED; + } + return -EINVAL; +} + +static ssize_t sony_nc_gfx_switch_status_show(struct device *dev, + struct device_attribute *attr, + char *buffer) +{ + int pos = __sony_nc_gfx_switch_status_get(); + + if (pos < 0) + return pos; + + return sysfs_emit(buffer, "%s\n", + pos == SPEED ? "speed" : + pos == STAMINA ? "stamina" : + pos == AUTO ? "auto" : "unknown"); +} + +static int sony_nc_gfx_switch_setup(struct platform_device *pd, + unsigned int handle) +{ + unsigned int result; + + gfxs_ctl = kzalloc_obj(struct snc_gfx_switch_control); + if (!gfxs_ctl) + return -ENOMEM; + + gfxs_ctl->handle = handle; + + sysfs_attr_init(&gfxs_ctl->attr.attr); + gfxs_ctl->attr.attr.name = "gfx_switch_status"; + gfxs_ctl->attr.attr.mode = S_IRUGO; + gfxs_ctl->attr.show = sony_nc_gfx_switch_status_show; + + result = device_create_file(&pd->dev, &gfxs_ctl->attr); + if (result) + goto gfxerror; + + return 0; + +gfxerror: + kfree(gfxs_ctl); + gfxs_ctl = NULL; + + return result; +} + +static void sony_nc_gfx_switch_cleanup(struct platform_device *pd) +{ + if (gfxs_ctl) { + device_remove_file(&pd->dev, &gfxs_ctl->attr); + + kfree(gfxs_ctl); + gfxs_ctl = NULL; + } +} + +/* High speed charging function */ +static struct device_attribute *hsc_handle; + +static ssize_t sony_nc_highspeed_charging_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + if (sony_call_snc_handle(0x0131, value << 0x10 | 0x0200, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_highspeed_charging_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0131, 0x0100, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result & 0x01); +} + +static int sony_nc_highspeed_charging_setup(struct platform_device *pd) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0131, 0x0000, &result) || !(result & 0x01)) { + /* some models advertise the handle but have no implementation + * for it + */ + pr_info("No High Speed Charging capability found\n"); + return 0; + } + + hsc_handle = kzalloc_obj(struct device_attribute); + if (!hsc_handle) + return -ENOMEM; + + sysfs_attr_init(&hsc_handle->attr); + hsc_handle->attr.name = "battery_highspeed_charging"; + hsc_handle->attr.mode = S_IRUGO | S_IWUSR; + hsc_handle->show = sony_nc_highspeed_charging_show; + hsc_handle->store = sony_nc_highspeed_charging_store; + + result = device_create_file(&pd->dev, hsc_handle); + if (result) { + kfree(hsc_handle); + hsc_handle = NULL; + return result; + } + + return 0; +} + +static void sony_nc_highspeed_charging_cleanup(struct platform_device *pd) +{ + if (hsc_handle) { + device_remove_file(&pd->dev, hsc_handle); + kfree(hsc_handle); + hsc_handle = NULL; + } +} + +/* low battery function */ +static struct device_attribute *lowbatt_handle; + +static ssize_t sony_nc_lowbatt_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + if (sony_call_snc_handle(0x0121, value << 8, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_lowbatt_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0121, 0x0200, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result & 1); +} + +static int sony_nc_lowbatt_setup(struct platform_device *pd) +{ + unsigned int result; + + lowbatt_handle = kzalloc_obj(struct device_attribute); + if (!lowbatt_handle) + return -ENOMEM; + + sysfs_attr_init(&lowbatt_handle->attr); + lowbatt_handle->attr.name = "lowbatt_hibernate"; + lowbatt_handle->attr.mode = S_IRUGO | S_IWUSR; + lowbatt_handle->show = sony_nc_lowbatt_show; + lowbatt_handle->store = sony_nc_lowbatt_store; + + result = device_create_file(&pd->dev, lowbatt_handle); + if (result) { + kfree(lowbatt_handle); + lowbatt_handle = NULL; + return result; + } + + return 0; +} + +static void sony_nc_lowbatt_cleanup(struct platform_device *pd) +{ + if (lowbatt_handle) { + device_remove_file(&pd->dev, lowbatt_handle); + kfree(lowbatt_handle); + lowbatt_handle = NULL; + } +} + +/* fan speed function */ +static struct device_attribute *fan_handle, *hsf_handle; + +static ssize_t sony_nc_hsfan_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + if (sony_call_snc_handle(0x0149, value << 0x10 | 0x0200, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_hsfan_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0149, 0x0100, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result & 0x01); +} + +static ssize_t sony_nc_fanspeed_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0149, 0x0300, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result & 0xff); +} + +static int sony_nc_fanspeed_setup(struct platform_device *pd) +{ + unsigned int result; + + fan_handle = kzalloc_obj(struct device_attribute); + if (!fan_handle) + return -ENOMEM; + + hsf_handle = kzalloc_obj(struct device_attribute); + if (!hsf_handle) { + result = -ENOMEM; + goto out_hsf_handle_alloc; + } + + sysfs_attr_init(&fan_handle->attr); + fan_handle->attr.name = "fanspeed"; + fan_handle->attr.mode = S_IRUGO; + fan_handle->show = sony_nc_fanspeed_show; + fan_handle->store = NULL; + + sysfs_attr_init(&hsf_handle->attr); + hsf_handle->attr.name = "fan_forced"; + hsf_handle->attr.mode = S_IRUGO | S_IWUSR; + hsf_handle->show = sony_nc_hsfan_show; + hsf_handle->store = sony_nc_hsfan_store; + + result = device_create_file(&pd->dev, fan_handle); + if (result) + goto out_fan_handle; + + result = device_create_file(&pd->dev, hsf_handle); + if (result) + goto out_hsf_handle; + + return 0; + +out_hsf_handle: + device_remove_file(&pd->dev, fan_handle); + +out_fan_handle: + kfree(hsf_handle); + hsf_handle = NULL; + +out_hsf_handle_alloc: + kfree(fan_handle); + fan_handle = NULL; + return result; +} + +static void sony_nc_fanspeed_cleanup(struct platform_device *pd) +{ + if (fan_handle) { + device_remove_file(&pd->dev, fan_handle); + kfree(fan_handle); + fan_handle = NULL; + } + if (hsf_handle) { + device_remove_file(&pd->dev, hsf_handle); + kfree(hsf_handle); + hsf_handle = NULL; + } +} + +/* USB charge function */ +static struct device_attribute *uc_handle; + +static ssize_t sony_nc_usb_charge_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + if (sony_call_snc_handle(0x0155, value << 0x10 | 0x0100, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_usb_charge_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0155, 0x0000, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result & 0x01); +} + +static int sony_nc_usb_charge_setup(struct platform_device *pd) +{ + unsigned int result; + + if (sony_call_snc_handle(0x0155, 0x0000, &result) || !(result & 0x01)) { + /* some models advertise the handle but have no implementation + * for it + */ + pr_info("No USB Charge capability found\n"); + return 0; + } + + uc_handle = kzalloc_obj(struct device_attribute); + if (!uc_handle) + return -ENOMEM; + + sysfs_attr_init(&uc_handle->attr); + uc_handle->attr.name = "usb_charge"; + uc_handle->attr.mode = S_IRUGO | S_IWUSR; + uc_handle->show = sony_nc_usb_charge_show; + uc_handle->store = sony_nc_usb_charge_store; + + result = device_create_file(&pd->dev, uc_handle); + if (result) { + kfree(uc_handle); + uc_handle = NULL; + return result; + } + + return 0; +} + +static void sony_nc_usb_charge_cleanup(struct platform_device *pd) +{ + if (uc_handle) { + device_remove_file(&pd->dev, uc_handle); + kfree(uc_handle); + uc_handle = NULL; + } +} + +/* Panel ID function */ +static struct device_attribute *panel_handle; + +static ssize_t sony_nc_panelid_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(0x011D, 0x0000, &result)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", result); +} + +static int sony_nc_panelid_setup(struct platform_device *pd) +{ + unsigned int result; + + panel_handle = kzalloc_obj(struct device_attribute); + if (!panel_handle) + return -ENOMEM; + + sysfs_attr_init(&panel_handle->attr); + panel_handle->attr.name = "panel_id"; + panel_handle->attr.mode = S_IRUGO; + panel_handle->show = sony_nc_panelid_show; + panel_handle->store = NULL; + + result = device_create_file(&pd->dev, panel_handle); + if (result) { + kfree(panel_handle); + panel_handle = NULL; + return result; + } + + return 0; +} + +static void sony_nc_panelid_cleanup(struct platform_device *pd) +{ + if (panel_handle) { + device_remove_file(&pd->dev, panel_handle); + kfree(panel_handle); + panel_handle = NULL; + } +} + +/* smart connect function */ +static struct device_attribute *sc_handle; + +static ssize_t sony_nc_smart_conn_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + if (sony_call_snc_handle(0x0168, value << 0x10, &result)) + return -EIO; + + return count; +} + +static int sony_nc_smart_conn_setup(struct platform_device *pd) +{ + unsigned int result; + + sc_handle = kzalloc_obj(struct device_attribute); + if (!sc_handle) + return -ENOMEM; + + sysfs_attr_init(&sc_handle->attr); + sc_handle->attr.name = "smart_connect"; + sc_handle->attr.mode = S_IWUSR; + sc_handle->show = NULL; + sc_handle->store = sony_nc_smart_conn_store; + + result = device_create_file(&pd->dev, sc_handle); + if (result) { + kfree(sc_handle); + sc_handle = NULL; + return result; + } + + return 0; +} + +static void sony_nc_smart_conn_cleanup(struct platform_device *pd) +{ + if (sc_handle) { + device_remove_file(&pd->dev, sc_handle); + kfree(sc_handle); + sc_handle = NULL; + } +} + +/* Touchpad enable/disable */ +struct touchpad_control { + struct device_attribute attr; + int handle; +}; +static struct touchpad_control *tp_ctl; + +static ssize_t sony_nc_touchpad_store(struct device *dev, + struct device_attribute *attr, const char *buffer, size_t count) +{ + unsigned int result; + unsigned long value; + + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value) || value > 1) + return -EINVAL; + + /* sysfs: 0 disabled, 1 enabled + * EC: 0 enabled, 1 disabled + */ + if (sony_call_snc_handle(tp_ctl->handle, + (!value << 0x10) | 0x100, &result)) + return -EIO; + + return count; +} + +static ssize_t sony_nc_touchpad_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + unsigned int result; + + if (sony_call_snc_handle(tp_ctl->handle, 0x000, &result)) + return -EINVAL; + + return sysfs_emit(buffer, "%d\n", !(result & 0x01)); +} + +static int sony_nc_touchpad_setup(struct platform_device *pd, + unsigned int handle) +{ + int ret = 0; + + tp_ctl = kzalloc_obj(struct touchpad_control); + if (!tp_ctl) + return -ENOMEM; + + tp_ctl->handle = handle; + + sysfs_attr_init(&tp_ctl->attr.attr); + tp_ctl->attr.attr.name = "touchpad"; + tp_ctl->attr.attr.mode = S_IRUGO | S_IWUSR; + tp_ctl->attr.show = sony_nc_touchpad_show; + tp_ctl->attr.store = sony_nc_touchpad_store; + + ret = device_create_file(&pd->dev, &tp_ctl->attr); + if (ret) { + kfree(tp_ctl); + tp_ctl = NULL; + } + + return ret; +} + +static void sony_nc_touchpad_cleanup(struct platform_device *pd) +{ + if (tp_ctl) { + device_remove_file(&pd->dev, &tp_ctl->attr); + kfree(tp_ctl); + tp_ctl = NULL; + } +} + +static void sony_nc_backlight_ng_read_limits(int handle, + struct sony_backlight_props *props) +{ + u64 offset; + int i; + int lvl_table_len = 0; + u8 min = 0xff, max = 0x00; + unsigned char buffer[32] = { 0 }; + + props->handle = handle; + props->offset = 0; + props->maxlvl = 0xff; + + offset = sony_find_snc_handle(handle); + + /* try to read the boundaries from ACPI tables, if we fail the above + * defaults should be reasonable + */ + i = sony_nc_buffer_call(sony_nc_acpi_handle, "SN06", &offset, buffer, + 32); + if (i < 0) + return; + + switch (handle) { + case 0x012f: + case 0x0137: + lvl_table_len = 9; + break; + case 0x143: + case 0x14b: + case 0x14c: + lvl_table_len = 16; + break; + } + + /* the buffer lists brightness levels available, brightness levels are + * from position 0 to 8 in the array, other values are used by ALS + * control. + */ + for (i = 0; i < lvl_table_len && i < ARRAY_SIZE(buffer); i++) { + + dprintk("Brightness level: %d\n", buffer[i]); + + if (!buffer[i]) + break; + + if (buffer[i] > max) + max = buffer[i]; + if (buffer[i] < min) + min = buffer[i]; + } + props->offset = min; + props->maxlvl = max; + dprintk("Brightness levels: min=%d max=%d\n", props->offset, + props->maxlvl); +} + +static void sony_nc_backlight_setup(void) +{ + int max_brightness = 0; + const struct backlight_ops *ops = NULL; + struct backlight_properties props; + + if (sony_find_snc_handle(0x12f) >= 0) { + ops = &sony_backlight_ng_ops; + sony_bl_props.cmd_base = 0x0100; + sony_nc_backlight_ng_read_limits(0x12f, &sony_bl_props); + max_brightness = sony_bl_props.maxlvl - sony_bl_props.offset; + + } else if (sony_find_snc_handle(0x137) >= 0) { + ops = &sony_backlight_ng_ops; + sony_bl_props.cmd_base = 0x0100; + sony_nc_backlight_ng_read_limits(0x137, &sony_bl_props); + max_brightness = sony_bl_props.maxlvl - sony_bl_props.offset; + + } else if (sony_find_snc_handle(0x143) >= 0) { + ops = &sony_backlight_ng_ops; + sony_bl_props.cmd_base = 0x3000; + sony_nc_backlight_ng_read_limits(0x143, &sony_bl_props); + max_brightness = sony_bl_props.maxlvl - sony_bl_props.offset; + + } else if (sony_find_snc_handle(0x14b) >= 0) { + ops = &sony_backlight_ng_ops; + sony_bl_props.cmd_base = 0x3000; + sony_nc_backlight_ng_read_limits(0x14b, &sony_bl_props); + max_brightness = sony_bl_props.maxlvl - sony_bl_props.offset; + + } else if (sony_find_snc_handle(0x14c) >= 0) { + ops = &sony_backlight_ng_ops; + sony_bl_props.cmd_base = 0x3000; + sony_nc_backlight_ng_read_limits(0x14c, &sony_bl_props); + max_brightness = sony_bl_props.maxlvl - sony_bl_props.offset; + + } else if (acpi_has_method(sony_nc_acpi_handle, "GBRT")) { + ops = &sony_backlight_ops; + max_brightness = SONY_MAX_BRIGHTNESS - 1; + + } else + return; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = max_brightness; + sony_bl_props.dev = backlight_device_register("sony", NULL, + &sony_bl_props, + ops, &props); + + if (IS_ERR(sony_bl_props.dev)) { + pr_warn("unable to register backlight device\n"); + sony_bl_props.dev = NULL; + } else + sony_bl_props.dev->props.brightness = + ops->get_brightness(sony_bl_props.dev); +} + +static void sony_nc_backlight_cleanup(void) +{ + backlight_device_unregister(sony_bl_props.dev); +} + +static int sony_nc_probe(struct platform_device *pdev) +{ + struct acpi_device *device; + acpi_status status; + int result = 0; + struct sony_nc_value *item; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + sony_nc_acpi_device = device; + strscpy(acpi_device_class(device), "sony/hotkey"); + + sony_nc_acpi_handle = device->handle; + + /* read device status */ + result = acpi_bus_get_status(device); + /* bail IFF the above call was successful and the device is not present */ + if (!result && !device->status.present) { + dprintk("Device not present\n"); + result = -ENODEV; + goto outwalk; + } + + result = sony_pf_add(); + if (result) + goto outpresent; + + if (debug) { + status = acpi_walk_namespace(ACPI_TYPE_METHOD, + sony_nc_acpi_handle, 1, sony_walk_callback, + NULL, NULL, NULL); + if (ACPI_FAILURE(status)) { + pr_warn("unable to walk acpi resources\n"); + result = -ENODEV; + goto outpresent; + } + } + + result = sony_laptop_setup_input(&pdev->dev); + if (result) { + pr_err("Unable to create input devices\n"); + goto outplatform; + } + + if (acpi_has_method(sony_nc_acpi_handle, "ECON")) { + int arg = 1; + if (sony_nc_int_call(sony_nc_acpi_handle, "ECON", &arg, NULL)) + dprintk("ECON Method failed\n"); + } + + if (acpi_has_method(sony_nc_acpi_handle, "SN00")) { + dprintk("Doing SNC setup\n"); + /* retrieve the available handles */ + result = sony_nc_handles_setup(sony_pf_device); + if (!result) + sony_nc_function_setup(&pdev->dev, sony_pf_device); + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) + sony_nc_backlight_setup(); + + /* create sony_pf sysfs attributes related to the SNC device */ + for (item = sony_nc_values; item->name; ++item) { + + if (!debug && item->debug) + continue; + + /* find the available acpiget as described in the DSDT */ + for (; item->acpiget && *item->acpiget; ++item->acpiget) { + if (acpi_has_method(sony_nc_acpi_handle, + *item->acpiget)) { + dprintk("Found %s getter: %s\n", + item->name, *item->acpiget); + item->devattr.attr.mode |= S_IRUGO; + break; + } + } + + /* find the available acpiset as described in the DSDT */ + for (; item->acpiset && *item->acpiset; ++item->acpiset) { + if (acpi_has_method(sony_nc_acpi_handle, + *item->acpiset)) { + dprintk("Found %s setter: %s\n", + item->name, *item->acpiset); + item->devattr.attr.mode |= S_IWUSR; + break; + } + } + + if (item->devattr.attr.mode != 0) { + result = + device_create_file(&sony_pf_device->dev, + &item->devattr); + if (result) + goto out_sysfs; + } + } + + result = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + sony_nc_notify, NULL); + if (result) + goto out_sysfs; + + pr_info("SNC setup done.\n"); + return 0; + +out_sysfs: + for (item = sony_nc_values; item->name; ++item) { + device_remove_file(&sony_pf_device->dev, &item->devattr); + } + sony_nc_backlight_cleanup(); + sony_nc_function_cleanup(sony_pf_device); + sony_nc_handles_cleanup(sony_pf_device); + +outplatform: + sony_laptop_remove_input(); + +outpresent: + sony_pf_remove(); + +outwalk: + sony_nc_rfkill_cleanup(); + return result; +} + +static void sony_nc_remove(struct platform_device *pdev) +{ + struct sony_nc_value *item; + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, sony_nc_notify); + + sony_nc_backlight_cleanup(); + + sony_nc_acpi_device = NULL; + + for (item = sony_nc_values; item->name; ++item) { + device_remove_file(&sony_pf_device->dev, &item->devattr); + } + + sony_nc_function_cleanup(sony_pf_device); + sony_nc_handles_cleanup(sony_pf_device); + sony_pf_remove(); + sony_laptop_remove_input(); + dprintk(SONY_NC_DRIVER_NAME " removed.\n"); +} + +static const struct acpi_device_id sony_device_ids[] __maybe_unused = { + {SONY_NC_HID, 0}, + {SONY_PIC_HID, 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, sony_device_ids); + +static const struct acpi_device_id sony_nc_device_ids[] = { + {SONY_NC_HID, 0}, + {"", 0}, +}; + +static struct platform_driver sony_nc_driver = { + .probe = sony_nc_probe, + .remove = sony_nc_remove, + .driver = { + .name = SONY_NC_DRIVER_NAME, + .acpi_match_table = sony_nc_device_ids, + .pm = &sony_nc_pm, + }, +}; + +/*********** SPIC (SNY6001) Device ***********/ + +#define SONYPI_DEVICE_TYPE1 0x00000001 +#define SONYPI_DEVICE_TYPE2 0x00000002 +#define SONYPI_DEVICE_TYPE3 0x00000004 + +#define SONYPI_TYPE1_OFFSET 0x04 +#define SONYPI_TYPE2_OFFSET 0x12 +#define SONYPI_TYPE3_OFFSET 0x12 + +struct sony_pic_ioport { + struct acpi_resource_io io1; + struct acpi_resource_io io2; + struct list_head list; +}; + +struct sony_pic_irq { + struct list_head list; + + /* Must be last --ends in a flexible-array member. */ + struct acpi_resource_irq irq; +}; + +struct sonypi_eventtypes { + u8 data; + unsigned long mask; + struct sonypi_event *events; +}; + +struct sony_pic_dev { + struct acpi_device *acpi_dev; + struct sony_pic_irq *cur_irq; + struct sony_pic_ioport *cur_ioport; + struct list_head interrupts; + struct list_head ioports; + struct mutex lock; + struct sonypi_eventtypes *event_types; + int (*handle_irq)(const u8, const u8); + int model; + u16 evport_offset; + u8 camera_power; + u8 bluetooth_power; + u8 wwan_power; +}; + +static struct sony_pic_dev spic_dev = { + .interrupts = LIST_HEAD_INIT(spic_dev.interrupts), + .ioports = LIST_HEAD_INIT(spic_dev.ioports), +}; + +static int spic_drv_registered; + +/* Event masks */ +#define SONYPI_JOGGER_MASK 0x00000001 +#define SONYPI_CAPTURE_MASK 0x00000002 +#define SONYPI_FNKEY_MASK 0x00000004 +#define SONYPI_BLUETOOTH_MASK 0x00000008 +#define SONYPI_PKEY_MASK 0x00000010 +#define SONYPI_BACK_MASK 0x00000020 +#define SONYPI_HELP_MASK 0x00000040 +#define SONYPI_LID_MASK 0x00000080 +#define SONYPI_ZOOM_MASK 0x00000100 +#define SONYPI_THUMBPHRASE_MASK 0x00000200 +#define SONYPI_MEYE_MASK 0x00000400 +#define SONYPI_MEMORYSTICK_MASK 0x00000800 +#define SONYPI_BATTERY_MASK 0x00001000 +#define SONYPI_WIRELESS_MASK 0x00002000 + +struct sonypi_event { + u8 data; + u8 event; +}; + +/* The set of possible button release events */ +static struct sonypi_event sonypi_releaseev[] = { + { 0x00, SONYPI_EVENT_ANYBUTTON_RELEASED }, + { 0, 0 } +}; + +/* The set of possible jogger events */ +static struct sonypi_event sonypi_joggerev[] = { + { 0x1f, SONYPI_EVENT_JOGDIAL_UP }, + { 0x01, SONYPI_EVENT_JOGDIAL_DOWN }, + { 0x5f, SONYPI_EVENT_JOGDIAL_UP_PRESSED }, + { 0x41, SONYPI_EVENT_JOGDIAL_DOWN_PRESSED }, + { 0x1e, SONYPI_EVENT_JOGDIAL_FAST_UP }, + { 0x02, SONYPI_EVENT_JOGDIAL_FAST_DOWN }, + { 0x5e, SONYPI_EVENT_JOGDIAL_FAST_UP_PRESSED }, + { 0x42, SONYPI_EVENT_JOGDIAL_FAST_DOWN_PRESSED }, + { 0x1d, SONYPI_EVENT_JOGDIAL_VFAST_UP }, + { 0x03, SONYPI_EVENT_JOGDIAL_VFAST_DOWN }, + { 0x5d, SONYPI_EVENT_JOGDIAL_VFAST_UP_PRESSED }, + { 0x43, SONYPI_EVENT_JOGDIAL_VFAST_DOWN_PRESSED }, + { 0x40, SONYPI_EVENT_JOGDIAL_PRESSED }, + { 0, 0 } +}; + +/* The set of possible capture button events */ +static struct sonypi_event sonypi_captureev[] = { + { 0x05, SONYPI_EVENT_CAPTURE_PARTIALPRESSED }, + { 0x07, SONYPI_EVENT_CAPTURE_PRESSED }, + { 0x40, SONYPI_EVENT_CAPTURE_PRESSED }, + { 0x01, SONYPI_EVENT_CAPTURE_PARTIALRELEASED }, + { 0, 0 } +}; + +/* The set of possible fnkeys events */ +static struct sonypi_event sonypi_fnkeyev[] = { + { 0x10, SONYPI_EVENT_FNKEY_ESC }, + { 0x11, SONYPI_EVENT_FNKEY_F1 }, + { 0x12, SONYPI_EVENT_FNKEY_F2 }, + { 0x13, SONYPI_EVENT_FNKEY_F3 }, + { 0x14, SONYPI_EVENT_FNKEY_F4 }, + { 0x15, SONYPI_EVENT_FNKEY_F5 }, + { 0x16, SONYPI_EVENT_FNKEY_F6 }, + { 0x17, SONYPI_EVENT_FNKEY_F7 }, + { 0x18, SONYPI_EVENT_FNKEY_F8 }, + { 0x19, SONYPI_EVENT_FNKEY_F9 }, + { 0x1a, SONYPI_EVENT_FNKEY_F10 }, + { 0x1b, SONYPI_EVENT_FNKEY_F11 }, + { 0x1c, SONYPI_EVENT_FNKEY_F12 }, + { 0x1f, SONYPI_EVENT_FNKEY_RELEASED }, + { 0x21, SONYPI_EVENT_FNKEY_1 }, + { 0x22, SONYPI_EVENT_FNKEY_2 }, + { 0x31, SONYPI_EVENT_FNKEY_D }, + { 0x32, SONYPI_EVENT_FNKEY_E }, + { 0x33, SONYPI_EVENT_FNKEY_F }, + { 0x34, SONYPI_EVENT_FNKEY_S }, + { 0x35, SONYPI_EVENT_FNKEY_B }, + { 0x36, SONYPI_EVENT_FNKEY_ONLY }, + { 0, 0 } +}; + +/* The set of possible program key events */ +static struct sonypi_event sonypi_pkeyev[] = { + { 0x01, SONYPI_EVENT_PKEY_P1 }, + { 0x02, SONYPI_EVENT_PKEY_P2 }, + { 0x04, SONYPI_EVENT_PKEY_P3 }, + { 0x20, SONYPI_EVENT_PKEY_P1 }, + { 0, 0 } +}; + +/* The set of possible bluetooth events */ +static struct sonypi_event sonypi_blueev[] = { + { 0x55, SONYPI_EVENT_BLUETOOTH_PRESSED }, + { 0x59, SONYPI_EVENT_BLUETOOTH_ON }, + { 0x5a, SONYPI_EVENT_BLUETOOTH_OFF }, + { 0, 0 } +}; + +/* The set of possible wireless events */ +static struct sonypi_event sonypi_wlessev[] = { + { 0x59, SONYPI_EVENT_IGNORE }, + { 0x5a, SONYPI_EVENT_IGNORE }, + { 0, 0 } +}; + +/* The set of possible back button events */ +static struct sonypi_event sonypi_backev[] = { + { 0x20, SONYPI_EVENT_BACK_PRESSED }, + { 0, 0 } +}; + +/* The set of possible help button events */ +static struct sonypi_event sonypi_helpev[] = { + { 0x3b, SONYPI_EVENT_HELP_PRESSED }, + { 0, 0 } +}; + + +/* The set of possible lid events */ +static struct sonypi_event sonypi_lidev[] = { + { 0x51, SONYPI_EVENT_LID_CLOSED }, + { 0x50, SONYPI_EVENT_LID_OPENED }, + { 0, 0 } +}; + +/* The set of possible zoom events */ +static struct sonypi_event sonypi_zoomev[] = { + { 0x39, SONYPI_EVENT_ZOOM_PRESSED }, + { 0x10, SONYPI_EVENT_ZOOM_IN_PRESSED }, + { 0x20, SONYPI_EVENT_ZOOM_OUT_PRESSED }, + { 0x04, SONYPI_EVENT_ZOOM_PRESSED }, + { 0, 0 } +}; + +/* The set of possible thumbphrase events */ +static struct sonypi_event sonypi_thumbphraseev[] = { + { 0x3a, SONYPI_EVENT_THUMBPHRASE_PRESSED }, + { 0, 0 } +}; + +/* The set of possible motioneye camera events */ +static struct sonypi_event sonypi_meyeev[] = { + { 0x00, SONYPI_EVENT_MEYE_FACE }, + { 0x01, SONYPI_EVENT_MEYE_OPPOSITE }, + { 0, 0 } +}; + +/* The set of possible memorystick events */ +static struct sonypi_event sonypi_memorystickev[] = { + { 0x53, SONYPI_EVENT_MEMORYSTICK_INSERT }, + { 0x54, SONYPI_EVENT_MEMORYSTICK_EJECT }, + { 0, 0 } +}; + +/* The set of possible battery events */ +static struct sonypi_event sonypi_batteryev[] = { + { 0x20, SONYPI_EVENT_BATTERY_INSERT }, + { 0x30, SONYPI_EVENT_BATTERY_REMOVE }, + { 0, 0 } +}; + +/* The set of possible volume events */ +static struct sonypi_event sonypi_volumeev[] = { + { 0x01, SONYPI_EVENT_VOLUME_INC_PRESSED }, + { 0x02, SONYPI_EVENT_VOLUME_DEC_PRESSED }, + { 0, 0 } +}; + +/* The set of possible brightness events */ +static struct sonypi_event sonypi_brightnessev[] = { + { 0x80, SONYPI_EVENT_BRIGHTNESS_PRESSED }, + { 0, 0 } +}; + +static struct sonypi_eventtypes type1_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x70, SONYPI_MEYE_MASK, sonypi_meyeev }, + { 0x30, SONYPI_LID_MASK, sonypi_lidev }, + { 0x60, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0x10, SONYPI_JOGGER_MASK, sonypi_joggerev }, + { 0x20, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x30, SONYPI_BLUETOOTH_MASK, sonypi_blueev }, + { 0x40, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x30, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x40, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0 }, +}; +static struct sonypi_eventtypes type2_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x38, SONYPI_LID_MASK, sonypi_lidev }, + { 0x11, SONYPI_JOGGER_MASK, sonypi_joggerev }, + { 0x61, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0x21, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x31, SONYPI_BLUETOOTH_MASK, sonypi_blueev }, + { 0x08, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x11, SONYPI_BACK_MASK, sonypi_backev }, + { 0x21, SONYPI_HELP_MASK, sonypi_helpev }, + { 0x21, SONYPI_ZOOM_MASK, sonypi_zoomev }, + { 0x20, SONYPI_THUMBPHRASE_MASK, sonypi_thumbphraseev }, + { 0x31, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x41, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0x31, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0 }, +}; +static struct sonypi_eventtypes type3_events[] = { + { 0, 0xffffffff, sonypi_releaseev }, + { 0x21, SONYPI_FNKEY_MASK, sonypi_fnkeyev }, + { 0x31, SONYPI_WIRELESS_MASK, sonypi_wlessev }, + { 0x31, SONYPI_MEMORYSTICK_MASK, sonypi_memorystickev }, + { 0x41, SONYPI_BATTERY_MASK, sonypi_batteryev }, + { 0x31, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x05, SONYPI_PKEY_MASK, sonypi_pkeyev }, + { 0x05, SONYPI_ZOOM_MASK, sonypi_zoomev }, + { 0x05, SONYPI_CAPTURE_MASK, sonypi_captureev }, + { 0x05, SONYPI_PKEY_MASK, sonypi_volumeev }, + { 0x05, SONYPI_PKEY_MASK, sonypi_brightnessev }, + { 0 }, +}; + +/* low level spic calls */ +#define ITERATIONS_LONG 10000 +#define ITERATIONS_SHORT 10 +#define wait_on_command(command, iterations) { \ + unsigned int n = iterations; \ + while (--n && (command)) \ + udelay(1); \ + if (!n) \ + dprintk("command failed at %s : %s (line %d)\n", \ + __FILE__, __func__, __LINE__); \ +} + +static u8 sony_pic_call1(u8 dev) +{ + u8 v1, v2; + + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(dev, spic_dev.cur_ioport->io1.minimum + 4); + v1 = inb_p(spic_dev.cur_ioport->io1.minimum + 4); + v2 = inb_p(spic_dev.cur_ioport->io1.minimum); + dprintk("sony_pic_call1(0x%.2x): 0x%.4x\n", dev, (v2 << 8) | v1); + return v2; +} + +static u8 sony_pic_call2(u8 dev, u8 fn) +{ + u8 v1; + + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(dev, spic_dev.cur_ioport->io1.minimum + 4); + wait_on_command(inb_p(spic_dev.cur_ioport->io1.minimum + 4) & 2, + ITERATIONS_LONG); + outb(fn, spic_dev.cur_ioport->io1.minimum); + v1 = inb_p(spic_dev.cur_ioport->io1.minimum); + dprintk("sony_pic_call2(0x%.2x - 0x%.2x): 0x%.4x\n", dev, fn, v1); + return v1; +} + +/* + * minidrivers for SPIC models + */ +static int type3_handle_irq(const u8 data_mask, const u8 ev) +{ + /* + * 0x31 could mean we have to take some extra action and wait for + * the next irq for some Type3 models, it will generate a new + * irq and we can read new data from the device: + * - 0x5c and 0x5f requires 0xA0 + * - 0x61 requires 0xB3 + */ + if (data_mask == 0x31) { + if (ev == 0x5c || ev == 0x5f) + sony_pic_call1(0xA0); + else if (ev == 0x61) + sony_pic_call1(0xB3); + return 0; + } + return 1; +} + +static void sony_pic_detect_device_type(struct sony_pic_dev *dev) +{ + struct pci_dev *pcidev; + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_82371AB_3, NULL); + if (pcidev) { + dev->model = SONYPI_DEVICE_TYPE1; + dev->evport_offset = SONYPI_TYPE1_OFFSET; + dev->event_types = type1_events; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH6_1, NULL); + if (pcidev) { + dev->model = SONYPI_DEVICE_TYPE2; + dev->evport_offset = SONYPI_TYPE2_OFFSET; + dev->event_types = type2_events; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH7_1, NULL); + if (pcidev) { + dev->model = SONYPI_DEVICE_TYPE3; + dev->handle_irq = type3_handle_irq; + dev->evport_offset = SONYPI_TYPE3_OFFSET; + dev->event_types = type3_events; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH8_4, NULL); + if (pcidev) { + dev->model = SONYPI_DEVICE_TYPE3; + dev->handle_irq = type3_handle_irq; + dev->evport_offset = SONYPI_TYPE3_OFFSET; + dev->event_types = type3_events; + goto out; + } + + pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, + PCI_DEVICE_ID_INTEL_ICH9_1, NULL); + if (pcidev) { + dev->model = SONYPI_DEVICE_TYPE3; + dev->handle_irq = type3_handle_irq; + dev->evport_offset = SONYPI_TYPE3_OFFSET; + dev->event_types = type3_events; + goto out; + } + + /* default */ + dev->model = SONYPI_DEVICE_TYPE2; + dev->evport_offset = SONYPI_TYPE2_OFFSET; + dev->event_types = type2_events; + +out: + pci_dev_put(pcidev); + + pr_info("detected Type%d model\n", + dev->model == SONYPI_DEVICE_TYPE1 ? 1 : + dev->model == SONYPI_DEVICE_TYPE2 ? 2 : 3); +} + +/* gprs/edge modem (SZ460N and SZ210P), thanks to Joshua Wise */ +static void __sony_pic_set_wwanpower(u8 state) +{ + state = !!state; + if (spic_dev.wwan_power == state) + return; + sony_pic_call2(0xB0, state); + sony_pic_call1(0x82); + spic_dev.wwan_power = state; +} + +static ssize_t sony_pic_wwanpower_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + mutex_lock(&spic_dev.lock); + __sony_pic_set_wwanpower(value); + mutex_unlock(&spic_dev.lock); + + return count; +} + +static ssize_t sony_pic_wwanpower_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + ssize_t count; + mutex_lock(&spic_dev.lock); + count = sysfs_emit(buffer, "%d\n", spic_dev.wwan_power); + mutex_unlock(&spic_dev.lock); + return count; +} + +/* bluetooth subsystem power state */ +static void __sony_pic_set_bluetoothpower(u8 state) +{ + state = !!state; + if (spic_dev.bluetooth_power == state) + return; + sony_pic_call2(0x96, state); + sony_pic_call1(0x82); + spic_dev.bluetooth_power = state; +} + +static ssize_t sony_pic_bluetoothpower_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + mutex_lock(&spic_dev.lock); + __sony_pic_set_bluetoothpower(value); + mutex_unlock(&spic_dev.lock); + + return count; +} + +static ssize_t sony_pic_bluetoothpower_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + ssize_t count = 0; + mutex_lock(&spic_dev.lock); + count = sysfs_emit(buffer, "%d\n", spic_dev.bluetooth_power); + mutex_unlock(&spic_dev.lock); + return count; +} + +/* fan speed */ +/* FAN0 information (reverse engineered from ACPI tables) */ +#define SONY_PIC_FAN0_STATUS 0x93 +static int sony_pic_set_fanspeed(unsigned long value) +{ + return ec_write(SONY_PIC_FAN0_STATUS, value); +} + +static int sony_pic_get_fanspeed(u8 *value) +{ + return ec_read(SONY_PIC_FAN0_STATUS, value); +} + +static ssize_t sony_pic_fanspeed_store(struct device *dev, + struct device_attribute *attr, + const char *buffer, size_t count) +{ + unsigned long value; + if (count > 31) + return -EINVAL; + + if (kstrtoul(buffer, 10, &value)) + return -EINVAL; + + if (sony_pic_set_fanspeed(value)) + return -EIO; + + return count; +} + +static ssize_t sony_pic_fanspeed_show(struct device *dev, + struct device_attribute *attr, char *buffer) +{ + u8 value = 0; + if (sony_pic_get_fanspeed(&value)) + return -EIO; + + return sysfs_emit(buffer, "%d\n", value); +} + +#define SPIC_ATTR(_name, _mode) \ +struct device_attribute spic_attr_##_name = __ATTR(_name, \ + _mode, sony_pic_## _name ##_show, \ + sony_pic_## _name ##_store) + +static SPIC_ATTR(bluetoothpower, 0644); +static SPIC_ATTR(wwanpower, 0644); +static SPIC_ATTR(fanspeed, 0644); + +static struct attribute *spic_attributes[] = { + &spic_attr_bluetoothpower.attr, + &spic_attr_wwanpower.attr, + &spic_attr_fanspeed.attr, + NULL +}; + +static const struct attribute_group spic_attribute_group = { + .attrs = spic_attributes +}; + +/******** SONYPI compatibility **********/ +#ifdef CONFIG_SONYPI_COMPAT + +/* battery / brightness / temperature addresses */ +#define SONYPI_BAT_FLAGS 0x81 +#define SONYPI_LCD_LIGHT 0x96 +#define SONYPI_BAT1_PCTRM 0xa0 +#define SONYPI_BAT1_LEFT 0xa2 +#define SONYPI_BAT1_MAXRT 0xa4 +#define SONYPI_BAT2_PCTRM 0xa8 +#define SONYPI_BAT2_LEFT 0xaa +#define SONYPI_BAT2_MAXRT 0xac +#define SONYPI_BAT1_MAXTK 0xb0 +#define SONYPI_BAT1_FULL 0xb2 +#define SONYPI_BAT2_MAXTK 0xb8 +#define SONYPI_BAT2_FULL 0xba +#define SONYPI_TEMP_STATUS 0xC1 + +struct sonypi_compat_s { + struct fasync_struct *fifo_async; + struct kfifo fifo; + spinlock_t fifo_lock; + wait_queue_head_t fifo_proc_list; + atomic_t open_count; +}; +static struct sonypi_compat_s sonypi_compat = { + .open_count = ATOMIC_INIT(0), +}; + +static int sonypi_misc_fasync(int fd, struct file *filp, int on) +{ + return fasync_helper(fd, filp, on, &sonypi_compat.fifo_async); +} + +static int sonypi_misc_release(struct inode *inode, struct file *file) +{ + atomic_dec(&sonypi_compat.open_count); + return 0; +} + +static int sonypi_misc_open(struct inode *inode, struct file *file) +{ + /* Flush input queue on first open */ + unsigned long flags; + + spin_lock_irqsave(&sonypi_compat.fifo_lock, flags); + + if (atomic_inc_return(&sonypi_compat.open_count) == 1) + kfifo_reset(&sonypi_compat.fifo); + + spin_unlock_irqrestore(&sonypi_compat.fifo_lock, flags); + + return 0; +} + +static ssize_t sonypi_misc_read(struct file *file, char __user *buf, + size_t count, loff_t *pos) +{ + ssize_t ret; + unsigned char c; + + if ((kfifo_len(&sonypi_compat.fifo) == 0) && + (file->f_flags & O_NONBLOCK)) + return -EAGAIN; + + ret = wait_event_interruptible(sonypi_compat.fifo_proc_list, + kfifo_len(&sonypi_compat.fifo) != 0); + if (ret) + return ret; + + while (ret < count && + (kfifo_out_locked(&sonypi_compat.fifo, &c, sizeof(c), + &sonypi_compat.fifo_lock) == sizeof(c))) { + if (put_user(c, buf++)) + return -EFAULT; + ret++; + } + + if (ret > 0) { + struct inode *inode = file_inode(file); + inode_set_atime_to_ts(inode, current_time(inode)); + } + + return ret; +} + +static __poll_t sonypi_misc_poll(struct file *file, poll_table *wait) +{ + poll_wait(file, &sonypi_compat.fifo_proc_list, wait); + if (kfifo_len(&sonypi_compat.fifo)) + return EPOLLIN | EPOLLRDNORM; + return 0; +} + +static int ec_read16(u8 addr, u16 *value) +{ + u8 val_lb, val_hb; + if (ec_read(addr, &val_lb)) + return -1; + if (ec_read(addr + 1, &val_hb)) + return -1; + *value = val_lb | (val_hb << 8); + return 0; +} + +static long sonypi_misc_ioctl(struct file *fp, unsigned int cmd, + unsigned long arg) +{ + int ret = 0; + void __user *argp = (void __user *)arg; + u8 val8; + u16 val16; + int value; + + mutex_lock(&spic_dev.lock); + switch (cmd) { + case SONYPI_IOCGBRT: + if (sony_bl_props.dev == NULL) { + ret = -EIO; + break; + } + if (sony_nc_int_call(sony_nc_acpi_handle, "GBRT", NULL, + &value)) { + ret = -EIO; + break; + } + val8 = ((value & 0xff) - 1) << 5; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSBRT: + if (sony_bl_props.dev == NULL) { + ret = -EIO; + break; + } + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + value = (val8 >> 5) + 1; + if (sony_nc_int_call(sony_nc_acpi_handle, "SBRT", &value, + NULL)) { + ret = -EIO; + break; + } + /* sync the backlight device status */ + sony_bl_props.dev->props.brightness = + sony_backlight_get_brightness(sony_bl_props.dev); + break; + case SONYPI_IOCGBAT1CAP: + if (ec_read16(SONYPI_BAT1_FULL, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT1REM: + if (ec_read16(SONYPI_BAT1_LEFT, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT2CAP: + if (ec_read16(SONYPI_BAT2_FULL, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBAT2REM: + if (ec_read16(SONYPI_BAT2_LEFT, &val16)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val16, sizeof(val16))) + ret = -EFAULT; + break; + case SONYPI_IOCGBATFLAGS: + if (ec_read(SONYPI_BAT_FLAGS, &val8)) { + ret = -EIO; + break; + } + val8 &= 0x07; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCGBLUE: + val8 = spic_dev.bluetooth_power; + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSBLUE: + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + __sony_pic_set_bluetoothpower(val8); + break; + /* FAN Controls */ + case SONYPI_IOCGFAN: + if (sony_pic_get_fanspeed(&val8)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + case SONYPI_IOCSFAN: + if (copy_from_user(&val8, argp, sizeof(val8))) { + ret = -EFAULT; + break; + } + if (sony_pic_set_fanspeed(val8)) + ret = -EIO; + break; + /* GET Temperature (useful under APM) */ + case SONYPI_IOCGTEMP: + if (ec_read(SONYPI_TEMP_STATUS, &val8)) { + ret = -EIO; + break; + } + if (copy_to_user(argp, &val8, sizeof(val8))) + ret = -EFAULT; + break; + default: + ret = -EINVAL; + } + mutex_unlock(&spic_dev.lock); + return ret; +} + +static const struct file_operations sonypi_misc_fops = { + .owner = THIS_MODULE, + .read = sonypi_misc_read, + .poll = sonypi_misc_poll, + .open = sonypi_misc_open, + .release = sonypi_misc_release, + .fasync = sonypi_misc_fasync, + .unlocked_ioctl = sonypi_misc_ioctl, + .llseek = noop_llseek, +}; + +static struct miscdevice sonypi_misc_device = { + .minor = MISC_DYNAMIC_MINOR, + .name = "sonypi", + .fops = &sonypi_misc_fops, +}; + +static void sonypi_compat_report_event(u8 event) +{ + kfifo_in_locked(&sonypi_compat.fifo, (unsigned char *)&event, + sizeof(event), &sonypi_compat.fifo_lock); + kill_fasync(&sonypi_compat.fifo_async, SIGIO, POLL_IN); + wake_up_interruptible(&sonypi_compat.fifo_proc_list); +} + +static int sonypi_compat_init(void) +{ + int error; + + spin_lock_init(&sonypi_compat.fifo_lock); + error = + kfifo_alloc(&sonypi_compat.fifo, SONY_LAPTOP_BUF_SIZE, GFP_KERNEL); + if (error) { + pr_err("kfifo_alloc failed\n"); + return error; + } + + init_waitqueue_head(&sonypi_compat.fifo_proc_list); + + if (minor != -1) + sonypi_misc_device.minor = minor; + error = misc_register(&sonypi_misc_device); + if (error) { + pr_err("misc_register failed\n"); + goto err_free_kfifo; + } + if (minor == -1) + pr_info("device allocated minor is %d\n", + sonypi_misc_device.minor); + + return 0; + +err_free_kfifo: + kfifo_free(&sonypi_compat.fifo); + return error; +} + +static void sonypi_compat_exit(void) +{ + misc_deregister(&sonypi_misc_device); + kfifo_free(&sonypi_compat.fifo); +} +#else +static int sonypi_compat_init(void) { return 0; } +static void sonypi_compat_exit(void) { } +static void sonypi_compat_report_event(u8 event) { } +#endif /* CONFIG_SONYPI_COMPAT */ + +/* + * ACPI callbacks + */ +static acpi_status +sony_pic_read_possible_resource(struct acpi_resource *resource, void *context) +{ + u32 i; + struct sony_pic_dev *dev = (struct sony_pic_dev *)context; + + switch (resource->type) { + case ACPI_RESOURCE_TYPE_START_DEPENDENT: + { + /* start IO enumeration */ + struct sony_pic_ioport *ioport = kzalloc_obj(*ioport); + if (!ioport) + return AE_ERROR; + + list_add(&ioport->list, &dev->ioports); + return AE_OK; + } + + case ACPI_RESOURCE_TYPE_END_DEPENDENT: + /* end IO enumeration */ + return AE_OK; + + case ACPI_RESOURCE_TYPE_IRQ: + { + struct acpi_resource_irq *p = &resource->data.irq; + struct sony_pic_irq *interrupt = NULL; + if (!p->interrupt_count) { + /* + * IRQ descriptors may have no IRQ# bits set, + * particularly those those w/ _STA disabled + */ + dprintk("Blank IRQ resource\n"); + return AE_OK; + } + for (i = 0; i < p->interrupt_count; i++) { + if (!p->interrupts[i]) { + pr_warn("Invalid IRQ %d\n", + p->interrupts[i]); + continue; + } + interrupt = kzalloc_obj(*interrupt); + if (!interrupt) + return AE_ERROR; + + list_add(&interrupt->list, &dev->interrupts); + interrupt->irq.triggering = p->triggering; + interrupt->irq.polarity = p->polarity; + interrupt->irq.shareable = p->shareable; + interrupt->irq.interrupt_count = 1; + interrupt->irq.interrupts[0] = p->interrupts[i]; + } + return AE_OK; + } + case ACPI_RESOURCE_TYPE_IO: + { + struct acpi_resource_io *io = &resource->data.io; + struct sony_pic_ioport *ioport = + list_first_entry(&dev->ioports, struct sony_pic_ioport, list); + if (!ioport->io1.minimum) { + memcpy(&ioport->io1, io, sizeof(*io)); + dprintk("IO1 at 0x%.4x (0x%.2x)\n", ioport->io1.minimum, + ioport->io1.address_length); + } + else if (!ioport->io2.minimum) { + memcpy(&ioport->io2, io, sizeof(*io)); + dprintk("IO2 at 0x%.4x (0x%.2x)\n", ioport->io2.minimum, + ioport->io2.address_length); + } + else { + pr_err("Unknown SPIC Type, more than 2 IO Ports\n"); + return AE_ERROR; + } + return AE_OK; + } + + case ACPI_RESOURCE_TYPE_END_TAG: + return AE_OK; + + default: + dprintk("Resource %d isn't an IRQ nor an IO port\n", + resource->type); + return AE_CTRL_TERMINATE; + + } +} + +static int sony_pic_possible_resources(struct acpi_device *device) +{ + int result = 0; + acpi_status status = AE_OK; + + if (!device) + return -EINVAL; + + /* get device status */ + /* see acpi_pci_link_get_current acpi_pci_link_get_possible */ + dprintk("Evaluating _STA\n"); + result = acpi_bus_get_status(device); + if (result) { + pr_warn("Unable to read status\n"); + goto end; + } + + if (!device->status.enabled) + dprintk("Device disabled\n"); + else + dprintk("Device enabled\n"); + + /* + * Query and parse 'method' + */ + dprintk("Evaluating %s\n", METHOD_NAME__PRS); + status = acpi_walk_resources(device->handle, METHOD_NAME__PRS, + sony_pic_read_possible_resource, &spic_dev); + if (ACPI_FAILURE(status)) { + pr_warn("Failure evaluating %s\n", METHOD_NAME__PRS); + result = -ENODEV; + } +end: + return result; +} + +/* + * Disable the spic device by calling its _DIS method + */ +static int sony_pic_disable(struct device *dev) +{ + acpi_status ret = acpi_evaluate_object(ACPI_HANDLE(dev), "_DIS", NULL, + NULL); + + if (ACPI_FAILURE(ret) && ret != AE_NOT_FOUND) + return -ENXIO; + + dprintk("Device disabled\n"); + return 0; +} + + +/* + * Based on drivers/acpi/pci_link.c:acpi_pci_link_set + * + * Call _SRS to set current resources + */ +static int sony_pic_enable(struct device *dev, + struct sony_pic_ioport *ioport, struct sony_pic_irq *irq) +{ + acpi_status status; + int result = 0; + /* Type 1 resource layout is: + * IO + * IO + * IRQNoFlags + * End + * + * Type 2 and 3 resource layout is: + * IO + * IRQNoFlags + * End + */ + struct { + struct acpi_resource res1; + struct acpi_resource res2; + struct acpi_resource res3; + struct acpi_resource res4; + } *resource; + struct acpi_buffer buffer = { 0, NULL }; + + if (!ioport || !irq) + return -EINVAL; + + /* init acpi_buffer */ + resource = kzalloc(sizeof(*resource) + 1, GFP_KERNEL); + if (!resource) + return -ENOMEM; + + buffer.length = sizeof(*resource) + 1; + buffer.pointer = resource; + + /* setup Type 1 resources */ + if (spic_dev.model == SONYPI_DEVICE_TYPE1) { + + /* setup io resources */ + resource->res1.type = ACPI_RESOURCE_TYPE_IO; + resource->res1.length = sizeof(struct acpi_resource); + memcpy(&resource->res1.data.io, &ioport->io1, + sizeof(struct acpi_resource_io)); + + resource->res2.type = ACPI_RESOURCE_TYPE_IO; + resource->res2.length = sizeof(struct acpi_resource); + memcpy(&resource->res2.data.io, &ioport->io2, + sizeof(struct acpi_resource_io)); + + /* setup irq resource */ + resource->res3.type = ACPI_RESOURCE_TYPE_IRQ; + resource->res3.length = sizeof(struct acpi_resource); + memcpy(&resource->res3.data.irq, &irq->irq, + sizeof(struct acpi_resource_irq)); + /* we requested a shared irq */ + resource->res3.data.irq.shareable = ACPI_SHARED; + + resource->res4.type = ACPI_RESOURCE_TYPE_END_TAG; + resource->res4.length = sizeof(struct acpi_resource); + } + /* setup Type 2/3 resources */ + else { + /* setup io resource */ + resource->res1.type = ACPI_RESOURCE_TYPE_IO; + resource->res1.length = sizeof(struct acpi_resource); + memcpy(&resource->res1.data.io, &ioport->io1, + sizeof(struct acpi_resource_io)); + + /* setup irq resource */ + resource->res2.type = ACPI_RESOURCE_TYPE_IRQ; + resource->res2.length = sizeof(struct acpi_resource); + memcpy(&resource->res2.data.irq, &irq->irq, + sizeof(struct acpi_resource_irq)); + /* we requested a shared irq */ + resource->res2.data.irq.shareable = ACPI_SHARED; + + resource->res3.type = ACPI_RESOURCE_TYPE_END_TAG; + resource->res3.length = sizeof(struct acpi_resource); + } + + /* Attempt to set the resource */ + dprintk("Evaluating _SRS\n"); + status = acpi_set_current_resources(ACPI_HANDLE(dev), &buffer); + + /* check for total failure */ + if (ACPI_FAILURE(status)) { + pr_err("Error evaluating _SRS\n"); + result = -ENODEV; + goto end; + } + + /* Necessary device initializations calls (from sonypi) */ + sony_pic_call1(0x82); + sony_pic_call2(0x81, 0xff); + sony_pic_call1(compat ? 0x92 : 0x82); + +end: + kfree(resource); + return result; +} + +/***************** + * + * ISR: some event is available + * + *****************/ +static irqreturn_t sony_pic_irq(int irq, void *dev_id) +{ + int i, j; + u8 ev = 0; + u8 data_mask = 0; + u8 device_event = 0; + + struct sony_pic_dev *dev = (struct sony_pic_dev *) dev_id; + + ev = inb_p(dev->cur_ioport->io1.minimum); + if (dev->cur_ioport->io2.minimum) + data_mask = inb_p(dev->cur_ioport->io2.minimum); + else + data_mask = inb_p(dev->cur_ioport->io1.minimum + + dev->evport_offset); + + dprintk("event ([%.2x] [%.2x]) at port 0x%.4x(+0x%.2x)\n", + ev, data_mask, dev->cur_ioport->io1.minimum, + dev->evport_offset); + + if (ev == 0x00 || ev == 0xff) + return IRQ_HANDLED; + + for (i = 0; dev->event_types[i].mask; i++) { + + if ((data_mask & dev->event_types[i].data) != + dev->event_types[i].data) + continue; + + if (!(mask & dev->event_types[i].mask)) + continue; + + for (j = 0; dev->event_types[i].events[j].event; j++) { + if (ev == dev->event_types[i].events[j].data) { + device_event = + dev->event_types[i].events[j].event; + /* some events may require ignoring */ + if (!device_event) + return IRQ_HANDLED; + goto found; + } + } + } + /* Still not able to decode the event try to pass + * it over to the minidriver + */ + if (dev->handle_irq && dev->handle_irq(data_mask, ev) == 0) + return IRQ_HANDLED; + + dprintk("unknown event ([%.2x] [%.2x]) at port 0x%.4x(+0x%.2x)\n", + ev, data_mask, dev->cur_ioport->io1.minimum, + dev->evport_offset); + return IRQ_HANDLED; + +found: + sony_laptop_report_input_event(device_event); + sonypi_compat_report_event(device_event); + return IRQ_HANDLED; +} + +/***************** + * + * ACPI driver + * + *****************/ +static void sony_pic_remove(struct platform_device *pdev) +{ + struct sony_pic_ioport *io, *tmp_io; + struct sony_pic_irq *irq, *tmp_irq; + + if (sony_pic_disable(&pdev->dev)) { + pr_err("Couldn't disable device\n"); + return; + } + + free_irq(spic_dev.cur_irq->irq.interrupts[0], &spic_dev); + release_region(spic_dev.cur_ioport->io1.minimum, + spic_dev.cur_ioport->io1.address_length); + if (spic_dev.cur_ioport->io2.minimum) + release_region(spic_dev.cur_ioport->io2.minimum, + spic_dev.cur_ioport->io2.address_length); + + sonypi_compat_exit(); + + sony_laptop_remove_input(); + + /* pf attrs */ + sysfs_remove_group(&sony_pf_device->dev.kobj, &spic_attribute_group); + sony_pf_remove(); + + list_for_each_entry_safe(io, tmp_io, &spic_dev.ioports, list) { + list_del(&io->list); + kfree(io); + } + list_for_each_entry_safe(irq, tmp_irq, &spic_dev.interrupts, list) { + list_del(&irq->list); + kfree(irq); + } + spic_dev.cur_ioport = NULL; + spic_dev.cur_irq = NULL; + + dprintk(SONY_PIC_DRIVER_NAME " removed.\n"); +} + +static int sony_pic_probe(struct platform_device *pdev) +{ + struct sony_pic_ioport *io, *tmp_io; + struct sony_pic_irq *irq, *tmp_irq; + struct acpi_device *device; + int result; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + spic_dev.acpi_dev = device; + strscpy(acpi_device_class(device), "sony/hotkey"); + sony_pic_detect_device_type(&spic_dev); + mutex_init(&spic_dev.lock); + + /* read _PRS resources */ + result = sony_pic_possible_resources(device); + if (result) { + pr_err("Unable to read possible resources\n"); + goto err_free_resources; + } + + /* setup input devices and helper fifo */ + result = sony_laptop_setup_input(&pdev->dev); + if (result) { + pr_err("Unable to create input devices\n"); + goto err_free_resources; + } + + result = sonypi_compat_init(); + if (result) + goto err_remove_input; + + /* request io port */ + list_for_each_entry_reverse(io, &spic_dev.ioports, list) { + if (request_region(io->io1.minimum, io->io1.address_length, + "Sony Programmable I/O Device")) { + dprintk("I/O port1: 0x%.4x (0x%.4x) + 0x%.2x\n", + io->io1.minimum, io->io1.maximum, + io->io1.address_length); + /* Type 1 have 2 ioports */ + if (io->io2.minimum) { + if (request_region(io->io2.minimum, + io->io2.address_length, + "Sony Programmable I/O Device")) { + dprintk("I/O port2: 0x%.4x (0x%.4x) + 0x%.2x\n", + io->io2.minimum, io->io2.maximum, + io->io2.address_length); + spic_dev.cur_ioport = io; + break; + } + else { + dprintk("Unable to get I/O port2: " + "0x%.4x (0x%.4x) + 0x%.2x\n", + io->io2.minimum, io->io2.maximum, + io->io2.address_length); + release_region(io->io1.minimum, + io->io1.address_length); + } + } + else { + spic_dev.cur_ioport = io; + break; + } + } + } + if (!spic_dev.cur_ioport) { + pr_err("Failed to request_region\n"); + result = -ENODEV; + goto err_remove_compat; + } + + /* request IRQ */ + list_for_each_entry_reverse(irq, &spic_dev.interrupts, list) { + if (!request_irq(irq->irq.interrupts[0], sony_pic_irq, + 0, "sony-laptop", &spic_dev)) { + dprintk("IRQ: %d - triggering: %d - " + "polarity: %d - shr: %d\n", + irq->irq.interrupts[0], + irq->irq.triggering, + irq->irq.polarity, + irq->irq.shareable); + spic_dev.cur_irq = irq; + break; + } + } + if (!spic_dev.cur_irq) { + pr_err("Failed to request_irq\n"); + result = -ENODEV; + goto err_release_region; + } + + /* set resource status _SRS */ + result = sony_pic_enable(&pdev->dev, spic_dev.cur_ioport, spic_dev.cur_irq); + if (result) { + pr_err("Couldn't enable device\n"); + goto err_free_irq; + } + + spic_dev.bluetooth_power = -1; + /* create device attributes */ + result = sony_pf_add(); + if (result) + goto err_disable_device; + + result = sysfs_create_group(&sony_pf_device->dev.kobj, &spic_attribute_group); + if (result) + goto err_remove_pf; + + pr_info("SPIC setup done.\n"); + return 0; + +err_remove_pf: + sony_pf_remove(); + +err_disable_device: + sony_pic_disable(&pdev->dev); + +err_free_irq: + free_irq(spic_dev.cur_irq->irq.interrupts[0], &spic_dev); + +err_release_region: + release_region(spic_dev.cur_ioport->io1.minimum, + spic_dev.cur_ioport->io1.address_length); + if (spic_dev.cur_ioport->io2.minimum) + release_region(spic_dev.cur_ioport->io2.minimum, + spic_dev.cur_ioport->io2.address_length); + +err_remove_compat: + sonypi_compat_exit(); + +err_remove_input: + sony_laptop_remove_input(); + +err_free_resources: + list_for_each_entry_safe(io, tmp_io, &spic_dev.ioports, list) { + list_del(&io->list); + kfree(io); + } + list_for_each_entry_safe(irq, tmp_irq, &spic_dev.interrupts, list) { + list_del(&irq->list); + kfree(irq); + } + spic_dev.cur_ioport = NULL; + spic_dev.cur_irq = NULL; + + return result; +} + +#ifdef CONFIG_PM_SLEEP +static int sony_pic_suspend(struct device *dev) +{ + if (sony_pic_disable(dev)) + return -ENXIO; + return 0; +} + +static int sony_pic_resume(struct device *dev) +{ + sony_pic_enable(dev, spic_dev.cur_ioport, spic_dev.cur_irq); + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(sony_pic_pm, sony_pic_suspend, sony_pic_resume); + +static const struct acpi_device_id sony_pic_device_ids[] = { + {SONY_PIC_HID, 0}, + {"", 0}, +}; + +static struct platform_driver sony_pic_driver = { + .probe = sony_pic_probe, + .remove = sony_pic_remove, + .driver = { + .name = SONY_PIC_DRIVER_NAME, + .acpi_match_table = sony_pic_device_ids, + .pm = &sony_pic_pm, + }, +}; + +static const struct dmi_system_id sonypi_dmi_table[] __initconst = { + { + .ident = "Sony Vaio", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "PCG-"), + }, + }, + { + .ident = "Sony Vaio", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Sony Corporation"), + DMI_MATCH(DMI_PRODUCT_NAME, "VGN-"), + }, + }, + { } +}; + +static int __init sony_laptop_init(void) +{ + int result; + + if (!no_spic && dmi_check_system(sonypi_dmi_table)) { + result = platform_driver_register(&sony_pic_driver); + if (result) { + pr_err("Unable to register SPIC driver\n"); + goto out; + } + spic_drv_registered = 1; + } + + result = platform_driver_register(&sony_nc_driver); + if (result) { + pr_err("Unable to register SNC driver\n"); + goto out_unregister_pic; + } + + return 0; + +out_unregister_pic: + if (spic_drv_registered) + platform_driver_unregister(&sony_pic_driver); +out: + return result; +} + +static void __exit sony_laptop_exit(void) +{ + platform_driver_unregister(&sony_nc_driver); + if (spic_drv_registered) + platform_driver_unregister(&sony_pic_driver); +} + +module_init(sony_laptop_init); +module_exit(sony_laptop_exit); diff --git a/drivers/platform/x86/system76_acpi.c b/drivers/platform/x86/system76_acpi.c new file mode 100644 index 000000000..dd7b1b07c --- /dev/null +++ b/drivers/platform/x86/system76_acpi.c @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * System76 ACPI Driver + * + * Copyright (C) 2023 System76 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +enum kbled_type { + KBLED_NONE, + KBLED_WHITE, + KBLED_RGB, +}; + +struct system76_data { + struct acpi_device *acpi_dev; + struct led_classdev ap_led; + struct led_classdev kb_led; + enum led_brightness kb_brightness; + enum led_brightness kb_toggle_brightness; + int kb_color; + struct device *therm; + union acpi_object *nfan; + union acpi_object *ntmp; + struct input_dev *input; + bool has_open_ec; + enum kbled_type kbled_type; +}; + +static const struct acpi_device_id device_ids[] = { + {"17761776", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, device_ids); + +// Array of keyboard LED brightness levels +static const enum led_brightness kb_levels[] = { + 48, + 72, + 96, + 144, + 192, + 255 +}; + +// Array of keyboard LED colors in 24-bit RGB format +static const int kb_colors[] = { + 0xFFFFFF, + 0x0000FF, + 0xFF0000, + 0xFF00FF, + 0x00FF00, + 0x00FFFF, + 0xFFFF00 +}; + +// Get a System76 ACPI device value by name +static int system76_get(struct system76_data *data, char *method) +{ + acpi_handle handle; + acpi_status status; + unsigned long long ret = 0; + + handle = acpi_device_handle(data->acpi_dev); + status = acpi_evaluate_integer(handle, method, NULL, &ret); + if (ACPI_SUCCESS(status)) + return ret; + return -ENODEV; +} + +// Get a System76 ACPI device value by name with index +static int system76_get_index(struct system76_data *data, char *method, int index) +{ + union acpi_object obj; + struct acpi_object_list obj_list; + acpi_handle handle; + acpi_status status; + unsigned long long ret = 0; + + obj.type = ACPI_TYPE_INTEGER; + obj.integer.value = index; + obj_list.count = 1; + obj_list.pointer = &obj; + + handle = acpi_device_handle(data->acpi_dev); + status = acpi_evaluate_integer(handle, method, &obj_list, &ret); + if (ACPI_SUCCESS(status)) + return ret; + return -ENODEV; +} + +// Get a System76 ACPI device object by name +static int system76_get_object(struct system76_data *data, char *method, union acpi_object **obj) +{ + acpi_handle handle; + acpi_status status; + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + + handle = acpi_device_handle(data->acpi_dev); + status = acpi_evaluate_object(handle, method, NULL, &buf); + if (ACPI_SUCCESS(status)) { + *obj = buf.pointer; + return 0; + } + + return -ENODEV; +} + +// Get a name from a System76 ACPI device object +static char *system76_name(union acpi_object *obj, int index) +{ + if (obj && obj->type == ACPI_TYPE_PACKAGE && index <= obj->package.count) { + if (obj->package.elements[index].type == ACPI_TYPE_STRING) + return obj->package.elements[index].string.pointer; + } + + return NULL; +} + +// Set a System76 ACPI device value by name +static int system76_set(struct system76_data *data, char *method, int value) +{ + union acpi_object obj; + struct acpi_object_list obj_list; + acpi_handle handle; + acpi_status status; + + obj.type = ACPI_TYPE_INTEGER; + obj.integer.value = value; + obj_list.count = 1; + obj_list.pointer = &obj; + handle = acpi_device_handle(data->acpi_dev); + status = acpi_evaluate_object(handle, method, &obj_list, NULL); + if (ACPI_SUCCESS(status)) + return 0; + else + return -1; +} + +#define BATTERY_THRESHOLD_INVALID 0xFF + +enum { + THRESHOLD_START, + THRESHOLD_END, +}; + +static ssize_t battery_get_threshold(int which, char *buf) +{ + struct acpi_object_list input; + union acpi_object param; + acpi_handle handle; + acpi_status status; + unsigned long long ret = BATTERY_THRESHOLD_INVALID; + + handle = ec_get_handle(); + if (!handle) + return -ENODEV; + + input.count = 1; + input.pointer = ¶m; + // Start/stop selection + param.type = ACPI_TYPE_INTEGER; + param.integer.value = which; + + status = acpi_evaluate_integer(handle, "GBCT", &input, &ret); + if (ACPI_FAILURE(status)) + return -EIO; + if (ret == BATTERY_THRESHOLD_INVALID) + return -EINVAL; + + return sysfs_emit(buf, "%d\n", (int)ret); +} + +static ssize_t battery_set_threshold(int which, const char *buf, size_t count) +{ + struct acpi_object_list input; + union acpi_object params[2]; + acpi_handle handle; + acpi_status status; + unsigned int value; + int ret; + + handle = ec_get_handle(); + if (!handle) + return -ENODEV; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value > 100) + return -EINVAL; + + input.count = 2; + input.pointer = params; + // Start/stop selection + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = which; + // Threshold value + params[1].type = ACPI_TYPE_INTEGER; + params[1].integer.value = value; + + status = acpi_evaluate_object(handle, "SBCT", &input, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return count; +} + +static ssize_t charge_control_start_threshold_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return battery_get_threshold(THRESHOLD_START, buf); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return battery_set_threshold(THRESHOLD_START, buf, count); +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); + +static ssize_t charge_control_end_threshold_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return battery_get_threshold(THRESHOLD_END, buf); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return battery_set_threshold(THRESHOLD_END, buf, count); +} + +static DEVICE_ATTR_RW(charge_control_end_threshold); + +static struct attribute *system76_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(system76_battery); + +static int system76_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + // System76 EC only supports 1 battery + if (strcmp(battery->desc->name, "BAT0") != 0) + return -ENODEV; + + if (device_add_groups(&battery->dev, system76_battery_groups)) + return -ENODEV; + + return 0; +} + +static int system76_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_groups(&battery->dev, system76_battery_groups); + return 0; +} + +static struct acpi_battery_hook system76_battery_hook = { + .add_battery = system76_battery_add, + .remove_battery = system76_battery_remove, + .name = "System76 Battery Extension", +}; + +static void system76_battery_init(void) +{ + battery_hook_register(&system76_battery_hook); +} + +static void system76_battery_exit(void) +{ + battery_hook_unregister(&system76_battery_hook); +} + +// Get the airplane mode LED brightness +static enum led_brightness ap_led_get(struct led_classdev *led) +{ + struct system76_data *data; + int value; + + data = container_of(led, struct system76_data, ap_led); + value = system76_get(data, "GAPL"); + if (value > 0) + return (enum led_brightness)value; + else + return LED_OFF; +} + +// Set the airplane mode LED brightness +static int ap_led_set(struct led_classdev *led, enum led_brightness value) +{ + struct system76_data *data; + + data = container_of(led, struct system76_data, ap_led); + return system76_set(data, "SAPL", value == LED_OFF ? 0 : 1); +} + +// Get the last set keyboard LED brightness +static enum led_brightness kb_led_get(struct led_classdev *led) +{ + struct system76_data *data; + + data = container_of(led, struct system76_data, kb_led); + return data->kb_brightness; +} + +// Set the keyboard LED brightness +static int kb_led_set(struct led_classdev *led, enum led_brightness value) +{ + struct system76_data *data; + + data = container_of(led, struct system76_data, kb_led); + data->kb_brightness = value; + if (acpi_has_method(acpi_device_handle(data->acpi_dev), "GKBK")) { + return system76_set(data, "SKBB", (int)data->kb_brightness); + } else { + return system76_set(data, "SKBL", (int)data->kb_brightness); + } +} + +// Get the last set keyboard LED color +static ssize_t kb_led_color_show( + struct device *dev, + struct device_attribute *dev_attr, + char *buf) +{ + struct led_classdev *led; + struct system76_data *data; + + led = dev_get_drvdata(dev); + data = container_of(led, struct system76_data, kb_led); + return sysfs_emit(buf, "%06X\n", data->kb_color); +} + +// Set the keyboard LED color +static ssize_t kb_led_color_store( + struct device *dev, + struct device_attribute *dev_attr, + const char *buf, + size_t size) +{ + struct led_classdev *led; + struct system76_data *data; + unsigned int val; + int ret; + + led = dev_get_drvdata(dev); + data = container_of(led, struct system76_data, kb_led); + ret = kstrtouint(buf, 16, &val); + if (ret) + return ret; + if (val > 0xFFFFFF) + return -EINVAL; + data->kb_color = (int)val; + system76_set(data, "SKBC", data->kb_color); + + return size; +} + +static struct device_attribute dev_attr_kb_led_color = { + .attr = { + .name = "color", + .mode = 0644, + }, + .show = kb_led_color_show, + .store = kb_led_color_store, +}; + +static struct attribute *system76_kb_led_color_attrs[] = { + &dev_attr_kb_led_color.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(system76_kb_led_color); + +// Notify that the keyboard LED was changed by hardware +static void kb_led_notify(struct system76_data *data) +{ + led_classdev_notify_brightness_hw_changed( + &data->kb_led, + data->kb_brightness + ); +} + +// Read keyboard LED brightness as set by hardware +static void kb_led_hotkey_hardware(struct system76_data *data) +{ + int value; + + if (acpi_has_method(acpi_device_handle(data->acpi_dev), "GKBK")) { + value = system76_get(data, "GKBB"); + } else { + value = system76_get(data, "GKBL"); + } + + if (value < 0) + return; + data->kb_brightness = value; + kb_led_notify(data); +} + +// Toggle the keyboard LED +static void kb_led_hotkey_toggle(struct system76_data *data) +{ + if (data->kb_brightness > 0) { + data->kb_toggle_brightness = data->kb_brightness; + kb_led_set(&data->kb_led, 0); + } else { + kb_led_set(&data->kb_led, data->kb_toggle_brightness); + } + kb_led_notify(data); +} + +// Decrease the keyboard LED brightness +static void kb_led_hotkey_down(struct system76_data *data) +{ + int i; + + if (data->kb_brightness > 0) { + for (i = ARRAY_SIZE(kb_levels); i > 0; i--) { + if (kb_levels[i - 1] < data->kb_brightness) { + kb_led_set(&data->kb_led, kb_levels[i - 1]); + break; + } + } + } else { + kb_led_set(&data->kb_led, data->kb_toggle_brightness); + } + kb_led_notify(data); +} + +// Increase the keyboard LED brightness +static void kb_led_hotkey_up(struct system76_data *data) +{ + int i; + + if (data->kb_brightness > 0) { + for (i = 0; i < ARRAY_SIZE(kb_levels); i++) { + if (kb_levels[i] > data->kb_brightness) { + kb_led_set(&data->kb_led, kb_levels[i]); + break; + } + } + } else { + kb_led_set(&data->kb_led, data->kb_toggle_brightness); + } + kb_led_notify(data); +} + +// Cycle the keyboard LED color +static void kb_led_hotkey_color(struct system76_data *data) +{ + int i; + + if (data->kbled_type != KBLED_RGB) + return; + + if (data->kb_brightness > 0) { + for (i = 0; i < ARRAY_SIZE(kb_colors); i++) { + if (kb_colors[i] == data->kb_color) + break; + } + i += 1; + if (i >= ARRAY_SIZE(kb_colors)) + i = 0; + data->kb_color = kb_colors[i]; + system76_set(data, "SKBC", data->kb_color); + } else { + kb_led_set(&data->kb_led, data->kb_toggle_brightness); + } + kb_led_notify(data); +} + +static umode_t thermal_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct system76_data *data = drvdata; + + switch (type) { + case hwmon_fan: + case hwmon_pwm: + if (system76_name(data->nfan, channel)) + return 0444; + break; + + case hwmon_temp: + if (system76_name(data->ntmp, channel)) + return 0444; + break; + + default: + return 0; + } + + return 0; +} + +static int thermal_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long *val) +{ + struct system76_data *data = dev_get_drvdata(dev); + int raw; + + switch (type) { + case hwmon_fan: + if (attr == hwmon_fan_input) { + raw = system76_get_index(data, "GFAN", channel); + if (raw < 0) + return raw; + *val = (raw >> 8) & 0xFFFF; + return 0; + } + break; + + case hwmon_pwm: + if (attr == hwmon_pwm_input) { + raw = system76_get_index(data, "GFAN", channel); + if (raw < 0) + return raw; + *val = raw & 0xFF; + return 0; + } + break; + + case hwmon_temp: + if (attr == hwmon_temp_input) { + raw = system76_get_index(data, "GTMP", channel); + if (raw < 0) + return raw; + *val = raw * 1000; + return 0; + } + break; + + default: + return -EOPNOTSUPP; + } + + return -EOPNOTSUPP; +} + +static int thermal_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + struct system76_data *data = dev_get_drvdata(dev); + + switch (type) { + case hwmon_fan: + if (attr == hwmon_fan_label) { + *str = system76_name(data->nfan, channel); + if (*str) + return 0; + } + break; + + case hwmon_temp: + if (attr == hwmon_temp_label) { + *str = system76_name(data->ntmp, channel); + if (*str) + return 0; + } + break; + + default: + return -EOPNOTSUPP; + } + + return -EOPNOTSUPP; +} + +static const struct hwmon_ops thermal_ops = { + .is_visible = thermal_is_visible, + .read = thermal_read, + .read_string = thermal_read_string, +}; + +// Allocate up to 8 fans and temperatures +static const struct hwmon_channel_info * const thermal_channel_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + NULL +}; + +static const struct hwmon_chip_info thermal_chip_info = { + .ops = &thermal_ops, + .info = thermal_channel_info, +}; + +static void input_key(struct system76_data *data, unsigned int code) +{ + input_report_key(data->input, code, 1); + input_sync(data->input); + + input_report_key(data->input, code, 0); + input_sync(data->input); +} + +// Handle ACPI notification +static void system76_notify(acpi_handle handle, u32 event, void *context) +{ + struct system76_data *data = context; + + switch (event) { + case 0x80: + kb_led_hotkey_hardware(data); + break; + case 0x81: + kb_led_hotkey_toggle(data); + break; + case 0x82: + kb_led_hotkey_down(data); + break; + case 0x83: + kb_led_hotkey_up(data); + break; + case 0x84: + kb_led_hotkey_color(data); + break; + case 0x85: + input_key(data, KEY_SCREENLOCK); + break; + } +} + +// Probe a System76 platform device +static int system76_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi_dev; + struct system76_data *data; + int err; + + acpi_dev = ACPI_COMPANION(&pdev->dev); + if (!acpi_dev) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + + data->acpi_dev = acpi_dev; + + // Some models do not run open EC firmware. Check for an ACPI method + // that only exists on open EC to guard functionality specific to it. + data->has_open_ec = acpi_has_method(acpi_device_handle(data->acpi_dev), "NFAN"); + + err = system76_get(data, "INIT"); + if (err) + return err; + data->ap_led.name = "system76_acpi::airplane"; + data->ap_led.flags = LED_CORE_SUSPENDRESUME; + data->ap_led.brightness_get = ap_led_get; + data->ap_led.brightness_set_blocking = ap_led_set; + data->ap_led.max_brightness = 1; + data->ap_led.default_trigger = "rfkill-none"; + err = devm_led_classdev_register(&pdev->dev, &data->ap_led); + if (err) + return err; + + data->kb_led.name = "system76_acpi::kbd_backlight"; + data->kb_led.flags = LED_BRIGHT_HW_CHANGED | LED_CORE_SUSPENDRESUME; + data->kb_led.brightness_get = kb_led_get; + data->kb_led.brightness_set_blocking = kb_led_set; + if (acpi_has_method(acpi_device_handle(data->acpi_dev), "GKBK")) { + // Use the new ACPI methods + data->kbled_type = system76_get(data, "GKBK"); + + switch (data->kbled_type) { + case KBLED_NONE: + // Nothing to do: Device will not be registered. + break; + case KBLED_WHITE: + data->kb_led.max_brightness = 255; + data->kb_toggle_brightness = 72; + break; + case KBLED_RGB: + data->kb_led.max_brightness = 255; + data->kb_led.groups = system76_kb_led_color_groups; + data->kb_toggle_brightness = 72; + data->kb_color = 0xffffff; + system76_set(data, "SKBC", data->kb_color); + break; + } + } else { + // Use the old ACPI methods + if (acpi_has_method(acpi_device_handle(data->acpi_dev), "SKBC")) { + data->kbled_type = KBLED_RGB; + data->kb_led.max_brightness = 255; + data->kb_led.groups = system76_kb_led_color_groups; + data->kb_toggle_brightness = 72; + data->kb_color = 0xffffff; + system76_set(data, "SKBC", data->kb_color); + } else { + data->kbled_type = KBLED_WHITE; + data->kb_led.max_brightness = 5; + } + } + + if (data->kbled_type != KBLED_NONE) { + err = devm_led_classdev_register(&pdev->dev, &data->kb_led); + if (err) + return err; + } + + data->input = devm_input_allocate_device(&pdev->dev); + if (!data->input) + return -ENOMEM; + + data->input->name = "System76 ACPI Hotkeys"; + data->input->phys = "system76_acpi/input0"; + data->input->id.bustype = BUS_HOST; + data->input->dev.parent = &pdev->dev; + input_set_capability(data->input, EV_KEY, KEY_SCREENLOCK); + + err = input_register_device(data->input); + if (err) + return err; + + err = acpi_dev_install_notify_handler(acpi_dev, ACPI_DEVICE_NOTIFY, + system76_notify, data); + if (err) + return err; + + if (data->has_open_ec) { + err = system76_get_object(data, "NFAN", &data->nfan); + if (err) + goto error; + + err = system76_get_object(data, "NTMP", &data->ntmp); + if (err) + goto error; + + data->therm = devm_hwmon_device_register_with_info(&pdev->dev, + "system76_acpi", data, &thermal_chip_info, NULL); + err = PTR_ERR_OR_ZERO(data->therm); + if (err) + goto error; + + system76_battery_init(); + } + + return 0; + +error: + if (data->has_open_ec) { + kfree(data->ntmp); + kfree(data->nfan); + } + acpi_dev_remove_notify_handler(acpi_dev, ACPI_DEVICE_NOTIFY, system76_notify); + return err; +} + +// Remove a System76 platform device +static void system76_remove(struct platform_device *pdev) +{ + struct system76_data *data = platform_get_drvdata(pdev); + + if (data->has_open_ec) { + system76_battery_exit(); + kfree(data->nfan); + kfree(data->ntmp); + } + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, system76_notify); + + system76_get(data, "FINI"); +} + +static struct platform_driver system76_driver = { + .probe = system76_probe, + .remove = system76_remove, + .driver = { + .name = "System76 ACPI Driver", + .acpi_match_table = device_ids, + }, +}; +module_platform_driver(system76_driver); + +MODULE_DESCRIPTION("System76 ACPI Driver"); +MODULE_AUTHOR("Jeremy Soller "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/topstar-laptop.c b/drivers/platform/x86/topstar-laptop.c new file mode 100644 index 000000000..e09d7f8ce --- /dev/null +++ b/drivers/platform/x86/topstar-laptop.c @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Topstar Laptop ACPI Extras driver + * + * Copyright (c) 2009 Herton Ronaldo Krzesinski + * Copyright (c) 2018 Guillaume Douézan-Grard + * + * Implementation inspired by existing x86 platform drivers, in special + * asus/eepc/fujitsu-laptop, thanks to their authors. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TOPSTAR_LAPTOP_CLASS "topstar" + +struct topstar_laptop { + struct acpi_device *device; + struct platform_device *platform; + struct input_dev *input; + struct led_classdev led; +}; + +/* + * LED + */ + +static enum led_brightness topstar_led_get(struct led_classdev *led) +{ + return led->brightness; +} + +static int topstar_led_set(struct led_classdev *led, + enum led_brightness state) +{ + struct topstar_laptop *topstar = container_of(led, + struct topstar_laptop, led); + + struct acpi_object_list params; + union acpi_object in_obj; + unsigned long long int ret; + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = 0x83; + + /* + * Topstar ACPI returns 0x30001 when the LED is ON and 0x30000 when it + * is OFF. + */ + status = acpi_evaluate_integer(topstar->device->handle, + "GETX", ¶ms, &ret); + if (ACPI_FAILURE(status)) + return -1; + + /* + * FNCX(0x83) toggles the LED (more precisely, it is supposed to + * act as an hardware switch and disconnect the WLAN adapter but + * it seems to be faulty on some models like the Topstar U931 + * Notebook). + */ + if ((ret == 0x30001 && state == LED_OFF) + || (ret == 0x30000 && state != LED_OFF)) { + status = acpi_execute_simple_method(topstar->device->handle, + "FNCX", 0x83); + if (ACPI_FAILURE(status)) + return -1; + } + + return 0; +} + +static int topstar_led_init(struct topstar_laptop *topstar) +{ + topstar->led = (struct led_classdev) { + .default_trigger = "rfkill0", + .brightness_get = topstar_led_get, + .brightness_set_blocking = topstar_led_set, + .name = TOPSTAR_LAPTOP_CLASS "::wlan", + }; + + return led_classdev_register(&topstar->platform->dev, &topstar->led); +} + +static void topstar_led_exit(struct topstar_laptop *topstar) +{ + led_classdev_unregister(&topstar->led); +} + +/* + * Input + */ + +static const struct key_entry topstar_keymap[] = { + { KE_KEY, 0x80, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x81, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x83, { KEY_VOLUMEUP } }, + { KE_KEY, 0x84, { KEY_VOLUMEDOWN } }, + { KE_KEY, 0x85, { KEY_MUTE } }, + { KE_KEY, 0x86, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x87, { KEY_F13 } }, /* touchpad enable/disable key */ + { KE_KEY, 0x88, { KEY_WLAN } }, + { KE_KEY, 0x8a, { KEY_WWW } }, + { KE_KEY, 0x8b, { KEY_MAIL } }, + { KE_KEY, 0x8c, { KEY_MEDIA } }, + + /* Known non hotkey events don't handled or that we don't care yet */ + { KE_IGNORE, 0x82, }, /* backlight event */ + { KE_IGNORE, 0x8e, }, + { KE_IGNORE, 0x8f, }, + { KE_IGNORE, 0x90, }, + + /* + * 'G key' generate two event codes, convert to only + * one event/key code for now, consider replacing by + * a switch (3G switch - SW_3G?) + */ + { KE_KEY, 0x96, { KEY_F14 } }, + { KE_KEY, 0x97, { KEY_F14 } }, + + { KE_END, 0 } +}; + +static void topstar_input_notify(struct topstar_laptop *topstar, int event) +{ + if (!sparse_keymap_report_event(topstar->input, event, 1, true)) + pr_info("unknown event = 0x%02x\n", event); +} + +static int topstar_input_init(struct topstar_laptop *topstar) +{ + struct input_dev *input; + int err; + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input->name = "Topstar Laptop extra buttons"; + input->phys = TOPSTAR_LAPTOP_CLASS "/input0"; + input->id.bustype = BUS_HOST; + input->dev.parent = &topstar->platform->dev; + + err = sparse_keymap_setup(input, topstar_keymap, NULL); + if (err) { + pr_err("Unable to setup input device keymap\n"); + goto err_free_dev; + } + + err = input_register_device(input); + if (err) { + pr_err("Unable to register input device\n"); + goto err_free_dev; + } + + topstar->input = input; + return 0; + +err_free_dev: + input_free_device(input); + return err; +} + +static void topstar_input_exit(struct topstar_laptop *topstar) +{ + input_unregister_device(topstar->input); +} + +/* + * Platform + */ + +static struct platform_driver topstar_platform_driver = { + .driver = { + .name = TOPSTAR_LAPTOP_CLASS, + }, +}; + +static int topstar_platform_init(struct topstar_laptop *topstar) +{ + int err; + + topstar->platform = platform_device_alloc(TOPSTAR_LAPTOP_CLASS, PLATFORM_DEVID_NONE); + if (!topstar->platform) + return -ENOMEM; + + platform_set_drvdata(topstar->platform, topstar); + + err = platform_device_add(topstar->platform); + if (err) + goto err_device_put; + + return 0; + +err_device_put: + platform_device_put(topstar->platform); + return err; +} + +static void topstar_platform_exit(struct topstar_laptop *topstar) +{ + platform_device_unregister(topstar->platform); +} + +/* + * ACPI + */ + +static int topstar_acpi_fncx_switch(struct acpi_device *device, bool state) +{ + acpi_status status; + u64 arg = state ? 0x86 : 0x87; + + status = acpi_execute_simple_method(device->handle, "FNCX", arg); + if (ACPI_FAILURE(status)) { + pr_err("Unable to switch FNCX notifications\n"); + return -ENODEV; + } + + return 0; +} + +static void topstar_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct topstar_laptop *topstar = data; + static bool dup_evnt[2]; + bool *dup; + + /* 0x83 and 0x84 key events comes duplicated... */ + if (event == 0x83 || event == 0x84) { + dup = &dup_evnt[event - 0x83]; + if (*dup) { + *dup = false; + return; + } + *dup = true; + } + + topstar_input_notify(topstar, event); +} + +static int topstar_acpi_init(struct topstar_laptop *topstar) +{ + return topstar_acpi_fncx_switch(topstar->device, true); +} + +static void topstar_acpi_exit(struct topstar_laptop *topstar) +{ + topstar_acpi_fncx_switch(topstar->device, false); +} + +/* + * Enable software-based WLAN LED control on systems with defective + * hardware switch. + */ +static bool led_workaround; + +static int dmi_led_workaround(const struct dmi_system_id *id) +{ + led_workaround = true; + return 0; +} + +static const struct dmi_system_id topstar_dmi_ids[] = { + { + .callback = dmi_led_workaround, + .ident = "Topstar U931/RVP7", + .matches = { + DMI_MATCH(DMI_BOARD_NAME, "U931"), + DMI_MATCH(DMI_BOARD_VERSION, "RVP7"), + }, + }, + {} +}; + +static int topstar_acpi_probe(struct platform_device *pdev) +{ + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + struct topstar_laptop *topstar; + int err; + + dmi_check_system(topstar_dmi_ids); + + topstar = kzalloc_obj(struct topstar_laptop); + if (!topstar) + return -ENOMEM; + + platform_set_drvdata(pdev, topstar); + + strscpy(acpi_device_name(device), "Topstar TPSACPI"); + strscpy(acpi_device_class(device), TOPSTAR_LAPTOP_CLASS); + topstar->device = device; + + err = topstar_acpi_init(topstar); + if (err) + goto err_free; + + err = topstar_platform_init(topstar); + if (err) + goto err_acpi_exit; + + err = topstar_input_init(topstar); + if (err) + goto err_platform_exit; + + err = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + topstar_acpi_notify, topstar); + if (err) + goto err_input_exit; + + if (led_workaround) { + err = topstar_led_init(topstar); + if (err) + goto err_notify_handler_exit; + } + + return 0; + +err_notify_handler_exit: + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, topstar_acpi_notify); +err_input_exit: + topstar_input_exit(topstar); +err_platform_exit: + topstar_platform_exit(topstar); +err_acpi_exit: + topstar_acpi_exit(topstar); +err_free: + kfree(topstar); + return err; +} + +static void topstar_acpi_remove(struct platform_device *pdev) +{ + struct topstar_laptop *topstar = platform_get_drvdata(pdev); + + if (led_workaround) + topstar_led_exit(topstar); + + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, topstar_acpi_notify); + topstar_input_exit(topstar); + topstar_platform_exit(topstar); + topstar_acpi_exit(topstar); + + kfree(topstar); +} + +static const struct acpi_device_id topstar_device_ids[] = { + { "TPS0001", 0 }, + { "TPSACPI01", 0 }, + { "", 0 }, +}; +MODULE_DEVICE_TABLE(acpi, topstar_device_ids); + +static struct platform_driver topstar_acpi_driver = { + .probe = topstar_acpi_probe, + .remove = topstar_acpi_remove, + .driver = { + .name = "Topstar laptop ACPI driver", + .acpi_match_table = topstar_device_ids, + }, +}; + +static int __init topstar_laptop_init(void) +{ + int ret; + + ret = platform_driver_register(&topstar_platform_driver); + if (ret < 0) + return ret; + + ret = platform_driver_register(&topstar_acpi_driver); + if (ret < 0) + goto err_driver_unreg; + + pr_info("ACPI extras driver loaded\n"); + return 0; + +err_driver_unreg: + platform_driver_unregister(&topstar_platform_driver); + return ret; +} + +static void __exit topstar_laptop_exit(void) +{ + platform_driver_unregister(&topstar_acpi_driver); + platform_driver_unregister(&topstar_platform_driver); +} + +module_init(topstar_laptop_init); +module_exit(topstar_laptop_exit); + +MODULE_AUTHOR("Herton Ronaldo Krzesinski"); +MODULE_AUTHOR("Guillaume Douézan-Grard"); +MODULE_DESCRIPTION("Topstar Laptop ACPI Extras driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/toshiba-wmi.c b/drivers/platform/x86/toshiba-wmi.c new file mode 100644 index 000000000..12c46455e --- /dev/null +++ b/drivers/platform/x86/toshiba-wmi.c @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * toshiba_wmi.c - Toshiba WMI Hotkey Driver + * + * Copyright (C) 2015 Azael Avalos + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Azael Avalos"); +MODULE_DESCRIPTION("Toshiba WMI Hotkey Driver"); +MODULE_LICENSE("GPL"); + +#define WMI_EVENT_GUID "59142400-C6A3-40FA-BADB-8A2652834100" + +MODULE_ALIAS("wmi:"WMI_EVENT_GUID); + +static struct input_dev *toshiba_wmi_input_dev; + +static const struct key_entry toshiba_wmi_keymap[] __initconst = { + /* TODO: Add keymap values once found... */ + /*{ KE_KEY, 0x00, { KEY_ } },*/ + { KE_END, 0 } +}; + +static void toshiba_wmi_notify(union acpi_object *obj, void *context) +{ + if (!obj) + return; + + /* TODO: Add proper checks once we have data */ + pr_debug("Unknown event received, obj type %x\n", obj->type); +} + +static const struct dmi_system_id toshiba_wmi_dmi_table[] __initconst = { + { + .ident = "Toshiba laptop", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TOSHIBA"), + }, + }, + {} +}; + +static int __init toshiba_wmi_input_setup(void) +{ + acpi_status status; + int err; + + toshiba_wmi_input_dev = input_allocate_device(); + if (!toshiba_wmi_input_dev) + return -ENOMEM; + + toshiba_wmi_input_dev->name = "Toshiba WMI hotkeys"; + toshiba_wmi_input_dev->phys = "wmi/input0"; + toshiba_wmi_input_dev->id.bustype = BUS_HOST; + + err = sparse_keymap_setup(toshiba_wmi_input_dev, + toshiba_wmi_keymap, NULL); + if (err) + goto err_free_dev; + + status = wmi_install_notify_handler(WMI_EVENT_GUID, + toshiba_wmi_notify, NULL); + if (ACPI_FAILURE(status)) { + err = -EIO; + goto err_free_dev; + } + + err = input_register_device(toshiba_wmi_input_dev); + if (err) + goto err_remove_notifier; + + return 0; + + err_remove_notifier: + wmi_remove_notify_handler(WMI_EVENT_GUID); + err_free_dev: + input_free_device(toshiba_wmi_input_dev); + return err; +} + +static void toshiba_wmi_input_destroy(void) +{ + wmi_remove_notify_handler(WMI_EVENT_GUID); + input_unregister_device(toshiba_wmi_input_dev); +} + +static int __init toshiba_wmi_init(void) +{ + int ret; + + if (!wmi_has_guid(WMI_EVENT_GUID) || + !dmi_check_system(toshiba_wmi_dmi_table)) + return -ENODEV; + + ret = toshiba_wmi_input_setup(); + if (ret) { + pr_err("Failed to setup input device\n"); + return ret; + } + + pr_info("Toshiba WMI Hotkey Driver\n"); + + return 0; +} + +static void __exit toshiba_wmi_exit(void) +{ + if (wmi_has_guid(WMI_EVENT_GUID)) + toshiba_wmi_input_destroy(); +} + +module_init(toshiba_wmi_init); +module_exit(toshiba_wmi_exit); diff --git a/drivers/platform/x86/toshiba_acpi.c b/drivers/platform/x86/toshiba_acpi.c new file mode 100644 index 000000000..7cecb3a70 --- /dev/null +++ b/drivers/platform/x86/toshiba_acpi.c @@ -0,0 +1,3674 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * toshiba_acpi.c - Toshiba Laptop ACPI Extras + * + * Copyright (C) 2002-2004 John Belmonte + * Copyright (C) 2008 Philip Langdale + * Copyright (C) 2010 Pierre Ducroquet + * Copyright (C) 2014-2016 Azael Avalos + * + * The devolpment page for this driver is located at + * http://memebeam.org/toys/ToshibaAcpiDriver. + * + * Credits: + * Jonathan A. Buzzard - Toshiba HCI info, and critical tips on reverse + * engineering the Windows drivers + * Yasushi Nagato - changes for linux kernel 2.4 -> 2.5 + * Rob Miller - TV out and hotkeys help + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#define TOSHIBA_ACPI_VERSION "0.24" +#define PROC_INTERFACE_VERSION 1 + +#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 + +MODULE_AUTHOR("John Belmonte"); +MODULE_DESCRIPTION("Toshiba Laptop ACPI Extras Driver"); +MODULE_LICENSE("GPL"); + +static int turn_on_panel_on_resume = -1; +module_param(turn_on_panel_on_resume, int, 0644); +MODULE_PARM_DESC(turn_on_panel_on_resume, + "Call HCI_PANEL_POWER_ON on resume (-1 = auto, 0 = no, 1 = yes"); + +static int hci_hotkey_quickstart = -1; +module_param(hci_hotkey_quickstart, int, 0644); +MODULE_PARM_DESC(hci_hotkey_quickstart, + "Call HCI_HOTKEY_EVENT with value 0x5 for quickstart button support (-1 = auto, 0 = no, 1 = yes"); + +#define TOSHIBA_WMI_EVENT_GUID "59142400-C6A3-40FA-BADB-8A2652834100" + +/* Scan code for Fn key on TOS1900 models */ +#define TOS1900_FN_SCAN 0x6e + +/* Toshiba ACPI method paths */ +#define METHOD_VIDEO_OUT "\\_SB_.VALX.DSSX" + +/* + * The Toshiba configuration interface is composed of the HCI and the SCI, + * which are defined as follows: + * + * HCI is Toshiba's "Hardware Control Interface" which is supposed to + * be uniform across all their models. Ideally we would just call + * dedicated ACPI methods instead of using this primitive interface. + * However the ACPI methods seem to be incomplete in some areas (for + * example they allow setting, but not reading, the LCD brightness value), + * so this is still useful. + * + * SCI stands for "System Configuration Interface" which aim is to + * conceal differences in hardware between different models. + */ + +#define TCI_WORDS 6 + +/* Operations */ +#define HCI_SET 0xff00 +#define HCI_GET 0xfe00 +#define SCI_OPEN 0xf100 +#define SCI_CLOSE 0xf200 +#define SCI_GET 0xf300 +#define SCI_SET 0xf400 + +/* Return codes */ +#define TOS_SUCCESS 0x0000 +#define TOS_SUCCESS2 0x0001 +#define TOS_OPEN_CLOSE_OK 0x0044 +#define TOS_FAILURE 0x1000 +#define TOS_NOT_SUPPORTED 0x8000 +#define TOS_ALREADY_OPEN 0x8100 +#define TOS_NOT_OPENED 0x8200 +#define TOS_INPUT_DATA_ERROR 0x8300 +#define TOS_WRITE_PROTECTED 0x8400 +#define TOS_NOT_PRESENT 0x8600 +#define TOS_FIFO_EMPTY 0x8c00 +#define TOS_DATA_NOT_AVAILABLE 0x8d20 +#define TOS_NOT_INITIALIZED 0x8d50 +#define TOS_NOT_INSTALLED 0x8e00 + +/* Registers */ +#define HCI_PANEL_POWER_ON 0x0002 +#define HCI_FAN 0x0004 +#define HCI_TR_BACKLIGHT 0x0005 +#define HCI_SYSTEM_EVENT 0x0016 +#define HCI_VIDEO_OUT 0x001c +#define HCI_HOTKEY_EVENT 0x001e +#define HCI_LCD_BRIGHTNESS 0x002a +#define HCI_FAN_RPM 0x0045 +#define HCI_WIRELESS 0x0056 +#define HCI_ACCELEROMETER 0x006d +#define HCI_COOLING_METHOD 0x007f +#define HCI_KBD_ILLUMINATION 0x0095 +#define HCI_ECO_MODE 0x0097 +#define HCI_ACCELEROMETER2 0x00a6 +#define HCI_BATTERY_CHARGE_MODE 0x00ba +#define HCI_SYSTEM_INFO 0xc000 +#define SCI_PANEL_POWER_ON 0x010d +#define SCI_ILLUMINATION 0x014e +#define SCI_USB_SLEEP_CHARGE 0x0150 +#define SCI_KBD_ILLUM_STATUS 0x015c +#define SCI_USB_SLEEP_MUSIC 0x015e +#define SCI_USB_THREE 0x0169 +#define SCI_TOUCHPAD 0x050e +#define SCI_KBD_FUNCTION_KEYS 0x0522 + +/* Field definitions */ +#define HCI_ACCEL_MASK 0x7fff +#define HCI_ACCEL_DIRECTION_MASK 0x8000 +#define HCI_HOTKEY_DISABLE 0x0b +#define HCI_HOTKEY_ENABLE_QUICKSTART 0x05 +#define HCI_HOTKEY_ENABLE 0x09 +#define HCI_HOTKEY_SPECIAL_FUNCTIONS 0x10 +#define HCI_LCD_BRIGHTNESS_BITS 3 +#define HCI_LCD_BRIGHTNESS_SHIFT (16-HCI_LCD_BRIGHTNESS_BITS) +#define HCI_LCD_BRIGHTNESS_LEVELS (1 << HCI_LCD_BRIGHTNESS_BITS) +#define HCI_MISC_SHIFT 0x10 +#define HCI_SYSTEM_TYPE1 0x10 +#define HCI_SYSTEM_TYPE2 0x11 +#define HCI_VIDEO_OUT_LCD 0x1 +#define HCI_VIDEO_OUT_CRT 0x2 +#define HCI_VIDEO_OUT_TV 0x4 +#define SCI_KBD_MODE_MASK 0x1f +#define SCI_KBD_MODE_FNZ 0x1 +#define SCI_KBD_MODE_AUTO 0x2 +#define SCI_KBD_MODE_ON 0x8 +#define SCI_KBD_MODE_OFF 0x10 +#define SCI_KBD_TIME_MAX 0x3c001a +#define HCI_WIRELESS_STATUS 0x1 +#define HCI_WIRELESS_WWAN 0x3 +#define HCI_WIRELESS_WWAN_STATUS 0x2000 +#define HCI_WIRELESS_WWAN_POWER 0x4000 +#define SCI_USB_CHARGE_MODE_MASK 0xff +#define SCI_USB_CHARGE_DISABLED 0x00 +#define SCI_USB_CHARGE_ALTERNATE 0x09 +#define SCI_USB_CHARGE_TYPICAL 0x11 +#define SCI_USB_CHARGE_AUTO 0x21 +#define SCI_USB_CHARGE_BAT_MASK 0x7 +#define SCI_USB_CHARGE_BAT_LVL_OFF 0x1 +#define SCI_USB_CHARGE_BAT_LVL_ON 0x4 +#define SCI_USB_CHARGE_BAT_LVL 0x0200 +#define SCI_USB_CHARGE_RAPID_DSP 0x0300 + +struct toshiba_acpi_dev { + struct acpi_device *acpi_dev; + const char *method_hci; + struct input_dev *hotkey_dev; + struct work_struct hotkey_work; + struct backlight_device *backlight_dev; + struct led_classdev led_dev; + struct led_classdev kbd_led; + struct led_classdev eco_led; + struct miscdevice miscdev; + struct rfkill *wwan_rfk; + struct iio_dev *indio_dev; +#if IS_ENABLED(CONFIG_HWMON) + struct device *hwmon_device; +#endif + + int force_fan; + int last_key_event; + int key_event_valid; + int kbd_type; + int kbd_mode; + int kbd_time; + int usbsc_bat_level; + int usbsc_mode_base; + int hotkey_event_type; + int max_cooling_method; + + unsigned int illumination_supported:1; + unsigned int video_supported:1; + unsigned int fan_supported:1; + unsigned int fan_rpm_supported:1; + unsigned int system_event_supported:1; + unsigned int ntfy_supported:1; + unsigned int info_supported:1; + unsigned int tr_backlight_supported:1; + unsigned int kbd_illum_supported:1; + unsigned int touchpad_supported:1; + unsigned int eco_supported:1; + unsigned int accelerometer_supported:1; + unsigned int usb_sleep_charge_supported:1; + unsigned int usb_rapid_charge_supported:1; + unsigned int usb_sleep_music_supported:1; + unsigned int kbd_function_keys_supported:1; + unsigned int panel_power_on_supported:1; + unsigned int usb_three_supported:1; + unsigned int wwan_supported:1; + unsigned int cooling_method_supported:1; + unsigned int battery_charge_mode_supported:1; + unsigned int sysfs_created:1; + unsigned int notify_handler_installed:1; + unsigned int special_functions; + + bool kbd_event_generated; + bool killswitch; +}; + +static struct toshiba_acpi_dev *toshiba_acpi; + +static bool disable_hotkeys; +module_param(disable_hotkeys, bool, 0444); +MODULE_PARM_DESC(disable_hotkeys, "Disables the hotkeys activation"); + +static const struct acpi_device_id toshiba_device_ids[] = { + {"TOS6200", 0}, + {"TOS6207", 0}, + {"TOS6208", 0}, + {"TOS1900", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, toshiba_device_ids); + +static const struct key_entry toshiba_acpi_keymap[] = { + { KE_KEY, 0x9e, { KEY_RFKILL } }, + { KE_KEY, 0x101, { KEY_MUTE } }, + { KE_KEY, 0x102, { KEY_ZOOMOUT } }, + { KE_KEY, 0x103, { KEY_ZOOMIN } }, + { KE_KEY, 0x10f, { KEY_TAB } }, + { KE_KEY, 0x12c, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, 0x139, { KEY_ZOOMRESET } }, + { KE_KEY, 0x13b, { KEY_COFFEE } }, + { KE_KEY, 0x13c, { KEY_BATTERY } }, + { KE_KEY, 0x13d, { KEY_SLEEP } }, + { KE_KEY, 0x13e, { KEY_SUSPEND } }, + { KE_KEY, 0x13f, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x140, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x141, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x142, { KEY_WLAN } }, + { KE_KEY, 0x143, { KEY_TOUCHPAD_TOGGLE } }, + { KE_KEY, 0x17f, { KEY_FN } }, + { KE_KEY, 0xb05, { KEY_PROG2 } }, + { KE_KEY, 0xb06, { KEY_WWW } }, + { KE_KEY, 0xb07, { KEY_MAIL } }, + { KE_KEY, 0xb30, { KEY_STOP } }, + { KE_KEY, 0xb31, { KEY_PREVIOUSSONG } }, + { KE_KEY, 0xb32, { KEY_NEXTSONG } }, + { KE_KEY, 0xb33, { KEY_PLAYPAUSE } }, + { KE_KEY, 0xb5a, { KEY_MEDIA } }, + { KE_IGNORE, 0x0e00, { KEY_RESERVED } }, /* Wake from sleep */ + { KE_IGNORE, 0x1430, { KEY_RESERVED } }, /* Wake from sleep */ + { KE_IGNORE, 0x1501, { KEY_RESERVED } }, /* Output changed */ + { KE_IGNORE, 0x1502, { KEY_RESERVED } }, /* HDMI plugged/unplugged */ + { KE_IGNORE, 0x1ABE, { KEY_RESERVED } }, /* Protection level set */ + { KE_IGNORE, 0x1ABF, { KEY_RESERVED } }, /* Protection level off */ + { KE_END, 0 }, +}; + +static const struct key_entry toshiba_acpi_alt_keymap[] = { + { KE_KEY, 0x102, { KEY_ZOOMOUT } }, + { KE_KEY, 0x103, { KEY_ZOOMIN } }, + { KE_KEY, 0x12c, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, 0x139, { KEY_ZOOMRESET } }, + { KE_KEY, 0x13c, { KEY_BRIGHTNESSDOWN } }, + { KE_KEY, 0x13d, { KEY_BRIGHTNESSUP } }, + { KE_KEY, 0x13e, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 0x13f, { KEY_TOUCHPAD_TOGGLE } }, + { KE_KEY, 0x157, { KEY_MUTE } }, + { KE_KEY, 0x158, { KEY_WLAN } }, + { KE_END, 0 }, +}; + +/* + * Utility + */ + +static inline void _set_bit(u32 *word, u32 mask, int value) +{ + *word = (*word & ~mask) | (mask * value); +} + +/* + * ACPI interface wrappers + */ + +static int write_acpi_int(const char *methodName, int val) +{ + acpi_status status; + + status = acpi_execute_simple_method(NULL, (char *)methodName, val); + return (status == AE_OK) ? 0 : -EIO; +} + +/* + * Perform a raw configuration call. Here we don't care about input or output + * buffer format. + */ +static acpi_status tci_raw(struct toshiba_acpi_dev *dev, + const u32 in[TCI_WORDS], u32 out[TCI_WORDS]) +{ + union acpi_object in_objs[TCI_WORDS], out_objs[TCI_WORDS + 1]; + struct acpi_object_list params; + struct acpi_buffer results; + acpi_status status; + int i; + + params.count = TCI_WORDS; + params.pointer = in_objs; + for (i = 0; i < TCI_WORDS; ++i) { + in_objs[i].type = ACPI_TYPE_INTEGER; + in_objs[i].integer.value = in[i]; + } + + results.length = sizeof(out_objs); + results.pointer = out_objs; + + status = acpi_evaluate_object(dev->acpi_dev->handle, + (char *)dev->method_hci, ¶ms, + &results); + if ((status == AE_OK) && (out_objs->package.count <= TCI_WORDS)) { + for (i = 0; i < out_objs->package.count; ++i) + out[i] = out_objs->package.elements[i].integer.value; + } + + return status; +} + +/* + * Common hci tasks + * + * In addition to the ACPI status, the HCI system returns a result which + * may be useful (such as "not supported"). + */ + +static u32 hci_write(struct toshiba_acpi_dev *dev, u32 reg, u32 in1) +{ + u32 in[TCI_WORDS] = { HCI_SET, reg, in1, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + return ACPI_SUCCESS(status) ? out[0] : TOS_FAILURE; +} + +static u32 hci_read(struct toshiba_acpi_dev *dev, u32 reg, u32 *out1) +{ + u32 in[TCI_WORDS] = { HCI_GET, reg, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) + return TOS_FAILURE; + + *out1 = out[2]; + + return out[0]; +} + +/* + * Common sci tasks + */ + +static int sci_open(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { SCI_OPEN, 0, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to open SCI failed\n"); + return 0; + } + + if (out[0] == TOS_OPEN_CLOSE_OK) { + return 1; + } else if (out[0] == TOS_ALREADY_OPEN) { + pr_info("Toshiba SCI already opened\n"); + return 1; + } else if (out[0] == TOS_NOT_SUPPORTED) { + /* + * Some BIOSes do not have the SCI open/close functions + * implemented and return 0x8000 (Not Supported), failing to + * register some supported features. + * + * Simply return 1 if we hit those affected laptops to make the + * supported features work. + * + * In the case that some laptops really do not support the SCI, + * all the SCI dependent functions check for TOS_NOT_SUPPORTED, + * and thus, not registering support for the queried feature. + */ + return 1; + } else if (out[0] == TOS_NOT_PRESENT) { + pr_info("Toshiba SCI is not present\n"); + } + + return 0; +} + +static void sci_close(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { SCI_CLOSE, 0, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to close SCI failed\n"); + return; + } + + if (out[0] == TOS_OPEN_CLOSE_OK) + return; + else if (out[0] == TOS_NOT_OPENED) + pr_info("Toshiba SCI not opened\n"); + else if (out[0] == TOS_NOT_PRESENT) + pr_info("Toshiba SCI is not present\n"); +} + +static u32 sci_read(struct toshiba_acpi_dev *dev, u32 reg, u32 *out1) +{ + u32 in[TCI_WORDS] = { SCI_GET, reg, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) + return TOS_FAILURE; + + *out1 = out[2]; + + return out[0]; +} + +static u32 sci_write(struct toshiba_acpi_dev *dev, u32 reg, u32 in1) +{ + u32 in[TCI_WORDS] = { SCI_SET, reg, in1, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + return ACPI_SUCCESS(status) ? out[0] : TOS_FAILURE; +} + +/* Illumination support */ +static void toshiba_illumination_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { SCI_GET, SCI_ILLUMINATION, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->illumination_supported = 0; + + if (!sci_open(dev)) + return; + + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query Illumination support failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + dev->illumination_supported = 1; +} + +static void toshiba_illumination_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, led_dev); + u32 result; + u32 state; + + /* First request : initialize communication. */ + if (!sci_open(dev)) + return; + + /* Switch the illumination on/off */ + state = brightness ? 1 : 0; + result = sci_write(dev, SCI_ILLUMINATION, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call for illumination failed\n"); +} + +static enum led_brightness toshiba_illumination_get(struct led_classdev *cdev) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, led_dev); + u32 result; + u32 state; + + /* First request : initialize communication. */ + if (!sci_open(dev)) + return LED_OFF; + + /* Check the illumination */ + result = sci_read(dev, SCI_ILLUMINATION, &state); + sci_close(dev); + if (result == TOS_FAILURE) { + pr_err("ACPI call for illumination failed\n"); + return LED_OFF; + } else if (result != TOS_SUCCESS) { + return LED_OFF; + } + + return state ? LED_FULL : LED_OFF; +} + +/* KBD Illumination */ +static void toshiba_kbd_illum_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { SCI_GET, SCI_KBD_ILLUM_STATUS, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->kbd_illum_supported = 0; + dev->kbd_event_generated = false; + + if (!sci_open(dev)) + return; + + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query kbd illumination support failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + /* + * Check for keyboard backlight timeout max value, + * previous kbd backlight implementation set this to + * 0x3c0003, and now the new implementation set this + * to 0x3c001a, use this to distinguish between them. + */ + if (out[3] == SCI_KBD_TIME_MAX) + dev->kbd_type = 2; + else + dev->kbd_type = 1; + /* Get the current keyboard backlight mode */ + dev->kbd_mode = out[2] & SCI_KBD_MODE_MASK; + /* Get the current time (1-60 seconds) */ + dev->kbd_time = out[2] >> HCI_MISC_SHIFT; + /* Flag as supported */ + dev->kbd_illum_supported = 1; +} + +static int toshiba_kbd_illum_status_set(struct toshiba_acpi_dev *dev, u32 time) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_KBD_ILLUM_STATUS, time); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set KBD backlight status failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_kbd_illum_status_get(struct toshiba_acpi_dev *dev, u32 *time) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_KBD_ILLUM_STATUS, time); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to get KBD backlight status failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static enum led_brightness toshiba_kbd_backlight_get(struct led_classdev *cdev) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, kbd_led); + u32 result; + u32 state; + + /* Check the keyboard backlight state */ + result = hci_read(dev, HCI_KBD_ILLUMINATION, &state); + if (result == TOS_FAILURE) { + pr_err("ACPI call to get the keyboard backlight failed\n"); + return LED_OFF; + } else if (result != TOS_SUCCESS) { + return LED_OFF; + } + + return state ? LED_FULL : LED_OFF; +} + +static void toshiba_kbd_backlight_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, kbd_led); + u32 result; + u32 state; + + /* Set the keyboard backlight state */ + state = brightness ? 1 : 0; + result = hci_write(dev, HCI_KBD_ILLUMINATION, state); + if (result == TOS_FAILURE) + pr_err("ACPI call to set KBD Illumination mode failed\n"); +} + +/* TouchPad support */ +static int toshiba_touchpad_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_TOUCHPAD, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set the touchpad failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_touchpad_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_TOUCHPAD, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to query the touchpad failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +/* Eco Mode support */ +static void toshiba_eco_mode_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_ECO_MODE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->eco_supported = 0; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get ECO led failed\n"); + return; + } + + if (out[0] == TOS_INPUT_DATA_ERROR || out[0] == TOS_NOT_SUPPORTED) { + /* + * If we receive 0x8300 (Input Data Error), it means that the + * LED device is present, but that we just screwed the input + * parameters. + * + * On some laptops 0x8000 (Not supported) is also returned in + * this case, so we need to allow for that as well. + * + * Let's query the status of the LED to see if we really have a + * success response, indicating the actual presense of the LED, + * bail out otherwise. + */ + in[3] = 1; + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get ECO led failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + dev->eco_supported = 1; + } +} + +static enum led_brightness +toshiba_eco_mode_get_status(struct led_classdev *cdev) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, eco_led); + u32 in[TCI_WORDS] = { HCI_GET, HCI_ECO_MODE, 0, 1, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get ECO led failed\n"); + return LED_OFF; + } + + if (out[0] != TOS_SUCCESS) + return LED_OFF; + + return out[2] ? LED_FULL : LED_OFF; +} + +static void toshiba_eco_mode_set_status(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct toshiba_acpi_dev *dev = container_of(cdev, + struct toshiba_acpi_dev, eco_led); + u32 in[TCI_WORDS] = { HCI_SET, HCI_ECO_MODE, 0, 1, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + /* Switch the Eco Mode led on/off */ + in[2] = (brightness) ? 1 : 0; + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) + pr_err("ACPI call to set ECO led failed\n"); +} + +/* Accelerometer support */ +static void toshiba_accelerometer_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_ACCELEROMETER2, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->accelerometer_supported = 0; + + /* + * Check if the accelerometer call exists, + * this call also serves as initialization + */ + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query the accelerometer failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + dev->accelerometer_supported = 1; +} + +static int toshiba_accelerometer_get(struct toshiba_acpi_dev *dev, + u32 *xy, u32 *z) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_ACCELEROMETER, 0, 1, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + /* Check the Accelerometer status */ + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query the accelerometer failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS) + return -EIO; + + *xy = out[2]; + *z = out[4]; + + return 0; +} + +/* Sleep (Charge and Music) utilities support */ +static void toshiba_usb_sleep_charge_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { SCI_GET, SCI_USB_SLEEP_CHARGE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->usb_sleep_charge_supported = 0; + + if (!sci_open(dev)) + return; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get USB Sleep and Charge mode failed\n"); + sci_close(dev); + return; + } + + if (out[0] != TOS_SUCCESS) { + sci_close(dev); + return; + } + + dev->usbsc_mode_base = out[4]; + + in[5] = SCI_USB_CHARGE_BAT_LVL; + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get USB Sleep and Charge mode failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + dev->usbsc_bat_level = out[2]; + /* Flag as supported */ + dev->usb_sleep_charge_supported = 1; +} + +static int toshiba_usb_sleep_charge_get(struct toshiba_acpi_dev *dev, + u32 *mode) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_USB_SLEEP_CHARGE, mode); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set USB S&C mode failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_usb_sleep_charge_set(struct toshiba_acpi_dev *dev, + u32 mode) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_USB_SLEEP_CHARGE, mode); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set USB S&C mode failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_sleep_functions_status_get(struct toshiba_acpi_dev *dev, + u32 *mode) +{ + u32 in[TCI_WORDS] = { SCI_GET, SCI_USB_SLEEP_CHARGE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + if (!sci_open(dev)) + return -EIO; + + in[5] = SCI_USB_CHARGE_BAT_LVL; + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get USB S&C battery level failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS) + return -EIO; + + *mode = out[2]; + + return 0; + +} + +static int toshiba_sleep_functions_status_set(struct toshiba_acpi_dev *dev, + u32 mode) +{ + u32 in[TCI_WORDS] = { SCI_SET, SCI_USB_SLEEP_CHARGE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + if (!sci_open(dev)) + return -EIO; + + in[2] = mode; + in[5] = SCI_USB_CHARGE_BAT_LVL; + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to set USB S&C battery level failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + return out[0] == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_usb_rapid_charge_get(struct toshiba_acpi_dev *dev, + u32 *state) +{ + u32 in[TCI_WORDS] = { SCI_GET, SCI_USB_SLEEP_CHARGE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + if (!sci_open(dev)) + return -EIO; + + in[5] = SCI_USB_CHARGE_RAPID_DSP; + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get USB Rapid Charge failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS && out[0] != TOS_SUCCESS2) + return -EIO; + + *state = out[2]; + + return 0; +} + +static int toshiba_usb_rapid_charge_set(struct toshiba_acpi_dev *dev, + u32 state) +{ + u32 in[TCI_WORDS] = { SCI_SET, SCI_USB_SLEEP_CHARGE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + if (!sci_open(dev)) + return -EIO; + + in[2] = state; + in[5] = SCI_USB_CHARGE_RAPID_DSP; + status = tci_raw(dev, in, out); + sci_close(dev); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to set USB Rapid Charge failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (out[0] == TOS_SUCCESS || out[0] == TOS_SUCCESS2) ? 0 : -EIO; +} + +static int toshiba_usb_sleep_music_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_USB_SLEEP_MUSIC, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to get Sleep and Music failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_usb_sleep_music_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_USB_SLEEP_MUSIC, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set Sleep and Music failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +/* Keyboard function keys */ +static int toshiba_function_keys_get(struct toshiba_acpi_dev *dev, u32 *mode) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_KBD_FUNCTION_KEYS, mode); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to get KBD function keys failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +static int toshiba_function_keys_set(struct toshiba_acpi_dev *dev, u32 mode) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_KBD_FUNCTION_KEYS, mode); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set KBD function keys failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +/* Panel Power ON */ +static int toshiba_panel_power_on_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_PANEL_POWER_ON, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to get Panel Power ON failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int toshiba_panel_power_on_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_PANEL_POWER_ON, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set Panel Power ON failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +/* USB Three */ +static int toshiba_usb_three_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_read(dev, SCI_USB_THREE, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to get USB 3 failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +static int toshiba_usb_three_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result; + + if (!sci_open(dev)) + return -EIO; + + result = sci_write(dev, SCI_USB_THREE, state); + sci_close(dev); + if (result == TOS_FAILURE) + pr_err("ACPI call to set USB 3 failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +/* Hotkey Event type */ +static int toshiba_hotkey_event_type_get(struct toshiba_acpi_dev *dev, + u32 *type) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_SYSTEM_INFO, 0x03, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get System type failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS) + return -EIO; + + *type = out[3]; + + return 0; +} + +/* Wireless status (RFKill, WLAN, BT, WWAN) */ +static int toshiba_wireless_status(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_WIRELESS, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + in[3] = HCI_WIRELESS_STATUS; + status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get Wireless status failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS) + return -EIO; + + dev->killswitch = !!(out[2] & HCI_WIRELESS_STATUS); + + return 0; +} + +/* WWAN */ +static void toshiba_wwan_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_WIRELESS, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->wwan_supported = 0; + + /* + * WWAN support can be queried by setting the in[3] value to + * HCI_WIRELESS_WWAN (0x03). + * + * If supported, out[0] contains TOS_SUCCESS and out[2] contains + * HCI_WIRELESS_WWAN_STATUS (0x2000). + * + * If not supported, out[0] contains TOS_INPUT_DATA_ERROR (0x8300) + * or TOS_NOT_SUPPORTED (0x8000). + */ + in[3] = HCI_WIRELESS_WWAN; + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get WWAN status failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS) + return; + + dev->wwan_supported = (out[2] == HCI_WIRELESS_WWAN_STATUS); +} + +static int toshiba_wwan_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 in[TCI_WORDS] = { HCI_SET, HCI_WIRELESS, state, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + in[3] = HCI_WIRELESS_WWAN_STATUS; + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to set WWAN status failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] != TOS_SUCCESS) + return -EIO; + + /* + * Some devices only need to call HCI_WIRELESS_WWAN_STATUS to + * (de)activate the device, but some others need the + * HCI_WIRELESS_WWAN_POWER call as well. + */ + in[3] = HCI_WIRELESS_WWAN_POWER; + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to set WWAN power failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + return out[0] == TOS_SUCCESS ? 0 : -EIO; +} + +/* Cooling Method */ +static void toshiba_cooling_method_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_COOLING_METHOD, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->cooling_method_supported = 0; + dev->max_cooling_method = 0; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get Cooling Method failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS && out[0] != TOS_SUCCESS2) + return; + + dev->cooling_method_supported = 1; + dev->max_cooling_method = out[3]; +} + +static int toshiba_cooling_method_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 result = hci_read(dev, HCI_COOLING_METHOD, state); + + if (result == TOS_FAILURE) + pr_err("ACPI call to get Cooling Method failed\n"); + + if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +static int toshiba_cooling_method_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result = hci_write(dev, HCI_COOLING_METHOD, state); + + if (result == TOS_FAILURE) + pr_err("ACPI call to set Cooling Method failed\n"); + + if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +/* Battery charge control */ +static void toshiba_battery_charge_mode_available(struct toshiba_acpi_dev *dev) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_BATTERY_CHARGE_MODE, 0, 0, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status; + + dev->battery_charge_mode_supported = 0; + + status = tci_raw(dev, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get Battery Charge Mode failed\n"); + return; + } + + if (out[0] != TOS_SUCCESS && out[0] != TOS_SUCCESS2) + return; + + dev->battery_charge_mode_supported = 1; +} + +static int toshiba_battery_charge_mode_get(struct toshiba_acpi_dev *dev, u32 *state) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_BATTERY_CHARGE_MODE, 0, 0, 0, 0x1 }; + u32 out[TCI_WORDS]; + int retries = 3; + + do { + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) + pr_err("ACPI call to get Battery Charge Mode failed\n"); + switch (out[0]) { + case TOS_SUCCESS: + case TOS_SUCCESS2: + *state = out[2]; + return 0; + case TOS_NOT_SUPPORTED: + return -ENODEV; + case TOS_DATA_NOT_AVAILABLE: + retries--; + break; + default: + return -EIO; + } + } while (retries); + + return -EIO; +} + +static int toshiba_battery_charge_mode_set(struct toshiba_acpi_dev *dev, u32 state) +{ + u32 result = hci_write(dev, HCI_BATTERY_CHARGE_MODE, state); + + if (result == TOS_FAILURE) + pr_err("ACPI call to set Battery Charge Mode failed\n"); + + if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return (result == TOS_SUCCESS || result == TOS_SUCCESS2) ? 0 : -EIO; +} + +/* Transflective Backlight */ +static int get_tr_backlight_status(struct toshiba_acpi_dev *dev, u32 *status) +{ + u32 result = hci_read(dev, HCI_TR_BACKLIGHT, status); + + if (result == TOS_FAILURE) + pr_err("ACPI call to get Transflective Backlight failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int set_tr_backlight_status(struct toshiba_acpi_dev *dev, u32 status) +{ + u32 result = hci_write(dev, HCI_TR_BACKLIGHT, !status); + + if (result == TOS_FAILURE) + pr_err("ACPI call to set Transflective Backlight failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static struct proc_dir_entry *toshiba_proc_dir; + +/* LCD Brightness */ +static int __get_lcd_brightness(struct toshiba_acpi_dev *dev) +{ + int brightness = 0; + u32 result; + u32 value; + + if (dev->tr_backlight_supported) { + int ret = get_tr_backlight_status(dev, &value); + + if (ret) + return ret; + if (value) + return 0; + brightness++; + } + + result = hci_read(dev, HCI_LCD_BRIGHTNESS, &value); + if (result == TOS_FAILURE) + pr_err("ACPI call to get LCD Brightness failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? + brightness + (value >> HCI_LCD_BRIGHTNESS_SHIFT) : + -EIO; +} + +static int get_lcd_brightness(struct backlight_device *bd) +{ + struct toshiba_acpi_dev *dev = bl_get_data(bd); + + return __get_lcd_brightness(dev); +} + +static int lcd_proc_show(struct seq_file *m, void *v) +{ + struct toshiba_acpi_dev *dev = m->private; + int levels; + int value; + + if (!dev->backlight_dev) + return -ENODEV; + + levels = dev->backlight_dev->props.max_brightness + 1; + value = get_lcd_brightness(dev->backlight_dev); + if (value < 0) { + pr_err("Error reading LCD brightness\n"); + return value; + } + + seq_printf(m, "brightness: %d\n", value); + seq_printf(m, "brightness_levels: %d\n", levels); + + return 0; +} + +static int lcd_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, lcd_proc_show, pde_data(inode)); +} + +static int set_lcd_brightness(struct toshiba_acpi_dev *dev, int value) +{ + u32 result; + + if (dev->tr_backlight_supported) { + int ret = set_tr_backlight_status(dev, !value); + + if (ret) + return ret; + if (value) + value--; + } + + value = value << HCI_LCD_BRIGHTNESS_SHIFT; + result = hci_write(dev, HCI_LCD_BRIGHTNESS, value); + if (result == TOS_FAILURE) + pr_err("ACPI call to set LCD Brightness failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int set_lcd_status(struct backlight_device *bd) +{ + struct toshiba_acpi_dev *dev = bl_get_data(bd); + + return set_lcd_brightness(dev, bd->props.brightness); +} + +static ssize_t lcd_proc_write(struct file *file, const char __user *buf, + size_t count, loff_t *pos) +{ + struct toshiba_acpi_dev *dev = pde_data(file_inode(file)); + char cmd[42]; + size_t len; + int levels; + int value; + + len = min(count, sizeof(cmd) - 1); + if (copy_from_user(cmd, buf, len)) + return -EFAULT; + cmd[len] = '\0'; + + levels = dev->backlight_dev->props.max_brightness + 1; + if (sscanf(cmd, " brightness : %i", &value) != 1 && + value < 0 && value > levels) + return -EINVAL; + + if (set_lcd_brightness(dev, value)) + return -EIO; + + return count; +} + +static const struct proc_ops lcd_proc_ops = { + .proc_open = lcd_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = lcd_proc_write, +}; + +/* Video-Out */ +static int get_video_status(struct toshiba_acpi_dev *dev, u32 *status) +{ + u32 result = hci_read(dev, HCI_VIDEO_OUT, status); + + if (result == TOS_FAILURE) + pr_err("ACPI call to get Video-Out failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int video_proc_show(struct seq_file *m, void *v) +{ + struct toshiba_acpi_dev *dev = m->private; + int is_lcd, is_crt, is_tv; + u32 value; + + if (get_video_status(dev, &value)) + return -EIO; + + is_lcd = (value & HCI_VIDEO_OUT_LCD) ? 1 : 0; + is_crt = (value & HCI_VIDEO_OUT_CRT) ? 1 : 0; + is_tv = (value & HCI_VIDEO_OUT_TV) ? 1 : 0; + + seq_printf(m, "lcd_out: %d\n", is_lcd); + seq_printf(m, "crt_out: %d\n", is_crt); + seq_printf(m, "tv_out: %d\n", is_tv); + + return 0; +} + +static int video_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, video_proc_show, pde_data(inode)); +} + +static ssize_t video_proc_write(struct file *file, const char __user *buf, + size_t count, loff_t *pos) +{ + struct toshiba_acpi_dev *dev = pde_data(file_inode(file)); + char *buffer; + char *cmd; + int lcd_out = -1, crt_out = -1, tv_out = -1; + int remain = count; + int value; + int ret; + u32 video_out; + + cmd = memdup_user_nul(buf, count); + if (IS_ERR(cmd)) + return PTR_ERR(cmd); + + buffer = cmd; + + /* + * Scan expression. Multiple expressions may be delimited with ; + * NOTE: To keep scanning simple, invalid fields are ignored. + */ + while (remain) { + if (sscanf(buffer, " lcd_out : %i", &value) == 1) + lcd_out = value & 1; + else if (sscanf(buffer, " crt_out : %i", &value) == 1) + crt_out = value & 1; + else if (sscanf(buffer, " tv_out : %i", &value) == 1) + tv_out = value & 1; + /* Advance to one character past the next ; */ + do { + ++buffer; + --remain; + } while (remain && *(buffer - 1) != ';'); + } + + kfree(cmd); + + ret = get_video_status(dev, &video_out); + if (!ret) { + unsigned int new_video_out = video_out; + + if (lcd_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_LCD, lcd_out); + if (crt_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_CRT, crt_out); + if (tv_out != -1) + _set_bit(&new_video_out, HCI_VIDEO_OUT_TV, tv_out); + /* + * To avoid unnecessary video disruption, only write the new + * video setting if something changed. + */ + if (new_video_out != video_out) + ret = write_acpi_int(METHOD_VIDEO_OUT, new_video_out); + } + + return ret ? -EIO : count; +} + +static const struct proc_ops video_proc_ops = { + .proc_open = video_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = video_proc_write, +}; + +/* Fan status */ +static int get_fan_status(struct toshiba_acpi_dev *dev, u32 *status) +{ + u32 result = hci_read(dev, HCI_FAN, status); + + if (result == TOS_FAILURE) + pr_err("ACPI call to get Fan status failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int set_fan_status(struct toshiba_acpi_dev *dev, u32 status) +{ + u32 result = hci_write(dev, HCI_FAN, status); + + if (result == TOS_FAILURE) + pr_err("ACPI call to set Fan status failed\n"); + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return result == TOS_SUCCESS ? 0 : -EIO; +} + +static int fan_proc_show(struct seq_file *m, void *v) +{ + struct toshiba_acpi_dev *dev = m->private; + u32 value; + + if (get_fan_status(dev, &value)) + return -EIO; + + seq_printf(m, "running: %d\n", (value > 0)); + seq_printf(m, "force_on: %d\n", dev->force_fan); + + return 0; +} + +static int fan_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, fan_proc_show, pde_data(inode)); +} + +static ssize_t fan_proc_write(struct file *file, const char __user *buf, + size_t count, loff_t *pos) +{ + struct toshiba_acpi_dev *dev = pde_data(file_inode(file)); + char cmd[42]; + size_t len; + int value; + + len = min(count, sizeof(cmd) - 1); + if (copy_from_user(cmd, buf, len)) + return -EFAULT; + cmd[len] = '\0'; + + if (sscanf(cmd, " force_on : %i", &value) != 1 && + value != 0 && value != 1) + return -EINVAL; + + if (set_fan_status(dev, value)) + return -EIO; + + dev->force_fan = value; + + return count; +} + +static const struct proc_ops fan_proc_ops = { + .proc_open = fan_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = fan_proc_write, +}; + +/* Fan RPM */ +static int get_fan_rpm(struct toshiba_acpi_dev *dev, u32 *rpm) +{ + u32 in[TCI_WORDS] = { HCI_GET, HCI_FAN_RPM, 0, 1, 0, 0 }; + u32 out[TCI_WORDS]; + acpi_status status = tci_raw(dev, in, out); + + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to get Fan speed failed\n"); + return -EIO; + } + + if (out[0] == TOS_NOT_SUPPORTED) + return -ENODEV; + + if (out[0] == TOS_SUCCESS) { + *rpm = out[2]; + return 0; + } + + return -EIO; +} + +static int keys_proc_show(struct seq_file *m, void *v) +{ + struct toshiba_acpi_dev *dev = m->private; + + seq_printf(m, "hotkey_ready: %d\n", dev->key_event_valid); + seq_printf(m, "hotkey: 0x%04x\n", dev->last_key_event); + + return 0; +} + +static int keys_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, keys_proc_show, pde_data(inode)); +} + +static ssize_t keys_proc_write(struct file *file, const char __user *buf, + size_t count, loff_t *pos) +{ + struct toshiba_acpi_dev *dev = pde_data(file_inode(file)); + char cmd[42]; + size_t len; + int value; + + len = min(count, sizeof(cmd) - 1); + if (copy_from_user(cmd, buf, len)) + return -EFAULT; + cmd[len] = '\0'; + + if (sscanf(cmd, " hotkey_ready : %i", &value) == 1 && value == 0) + dev->key_event_valid = 0; + else + return -EINVAL; + + return count; +} + +static const struct proc_ops keys_proc_ops = { + .proc_open = keys_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = keys_proc_write, +}; + +static int __maybe_unused version_proc_show(struct seq_file *m, void *v) +{ + seq_printf(m, "driver: %s\n", TOSHIBA_ACPI_VERSION); + seq_printf(m, "proc_interface: %d\n", PROC_INTERFACE_VERSION); + return 0; +} + +/* + * Proc and module init + */ + +#define PROC_TOSHIBA "toshiba" + +static void create_toshiba_proc_entries(struct toshiba_acpi_dev *dev) +{ + if (dev->backlight_dev) + proc_create_data("lcd", S_IRUGO | S_IWUSR, toshiba_proc_dir, + &lcd_proc_ops, dev); + if (dev->video_supported) + proc_create_data("video", S_IRUGO | S_IWUSR, toshiba_proc_dir, + &video_proc_ops, dev); + if (dev->fan_supported) + proc_create_data("fan", S_IRUGO | S_IWUSR, toshiba_proc_dir, + &fan_proc_ops, dev); + if (dev->hotkey_dev) + proc_create_data("keys", S_IRUGO | S_IWUSR, toshiba_proc_dir, + &keys_proc_ops, dev); + proc_create_single_data("version", S_IRUGO, toshiba_proc_dir, + version_proc_show, dev); +} + +static void remove_toshiba_proc_entries(struct toshiba_acpi_dev *dev) +{ + if (dev->backlight_dev) + remove_proc_entry("lcd", toshiba_proc_dir); + if (dev->video_supported) + remove_proc_entry("video", toshiba_proc_dir); + if (dev->fan_supported) + remove_proc_entry("fan", toshiba_proc_dir); + if (dev->hotkey_dev) + remove_proc_entry("keys", toshiba_proc_dir); + remove_proc_entry("version", toshiba_proc_dir); +} + +static const struct backlight_ops toshiba_backlight_data = { + .options = BL_CORE_SUSPENDRESUME, + .get_brightness = get_lcd_brightness, + .update_status = set_lcd_status, +}; + +/* Keyboard backlight work */ +static void toshiba_acpi_kbd_bl_work(struct work_struct *work); + +static DECLARE_WORK(kbd_bl_work, toshiba_acpi_kbd_bl_work); + +/* + * Sysfs files + */ +static DEVICE_STRING_ATTR_RO(version, 0444, TOSHIBA_ACPI_VERSION); + +static ssize_t fan_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + + if (state != 0 && state != 1) + return -EINVAL; + + ret = set_fan_status(toshiba, state); + if (ret) + return ret; + + return count; +} + +static ssize_t fan_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 value; + int ret; + + ret = get_fan_status(toshiba, &value); + if (ret) + return ret; + + return sprintf(buf, "%d\n", value); +} +static DEVICE_ATTR_RW(fan); + +static ssize_t kbd_backlight_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int mode; + int ret; + + + ret = kstrtoint(buf, 0, &mode); + if (ret) + return ret; + + /* Check for supported modes depending on keyboard backlight type */ + if (toshiba->kbd_type == 1) { + /* Type 1 supports SCI_KBD_MODE_FNZ and SCI_KBD_MODE_AUTO */ + if (mode != SCI_KBD_MODE_FNZ && mode != SCI_KBD_MODE_AUTO) + return -EINVAL; + } else if (toshiba->kbd_type == 2) { + /* Type 2 doesn't support SCI_KBD_MODE_FNZ */ + if (mode != SCI_KBD_MODE_AUTO && mode != SCI_KBD_MODE_ON && + mode != SCI_KBD_MODE_OFF) + return -EINVAL; + } + + /* + * Set the Keyboard Backlight Mode where: + * Auto - KBD backlight turns off automatically in given time + * FN-Z - KBD backlight "toggles" when hotkey pressed + * ON - KBD backlight is always on + * OFF - KBD backlight is always off + */ + + /* Only make a change if the actual mode has changed */ + if (toshiba->kbd_mode != mode) { + /* Shift the time to "base time" (0x3c0000 == 60 seconds) */ + int time = toshiba->kbd_time << HCI_MISC_SHIFT; + + /* OR the "base time" to the actual method format */ + if (toshiba->kbd_type == 1) { + /* Type 1 requires the current mode */ + time |= toshiba->kbd_mode; + } else if (toshiba->kbd_type == 2) { + /* Type 2 requires the desired mode */ + time |= mode; + } + + ret = toshiba_kbd_illum_status_set(toshiba, time); + if (ret) + return ret; + + toshiba->kbd_mode = mode; + toshiba_acpi->kbd_mode = mode; + + /* + * Some laptop models with the second generation backlit + * keyboard (type 2) do not generate the keyboard backlight + * changed event (0x92), and thus, the driver will never update + * the sysfs entries. + * + * The event is generated right when changing the keyboard + * backlight mode and the *notify function will set the + * kbd_event_generated to true. + * + * In case the event is not generated, schedule the keyboard + * backlight work to update the sysfs entries and emulate the + * event via genetlink. + */ + if (toshiba->kbd_type == 2 && + !toshiba->kbd_event_generated) + schedule_work(&kbd_bl_work); + } + + return count; +} + +static ssize_t kbd_backlight_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 time; + + if (toshiba_kbd_illum_status_get(toshiba, &time) < 0) + return -EIO; + + return sprintf(buf, "%i\n", time & SCI_KBD_MODE_MASK); +} +static DEVICE_ATTR_RW(kbd_backlight_mode); + +static ssize_t kbd_type_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + + return sprintf(buf, "%d\n", toshiba->kbd_type); +} +static DEVICE_ATTR_RO(kbd_type); + +static ssize_t available_kbd_modes_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + + if (toshiba->kbd_type == 1) + return sprintf(buf, "0x%x 0x%x\n", + SCI_KBD_MODE_FNZ, SCI_KBD_MODE_AUTO); + + return sprintf(buf, "0x%x 0x%x 0x%x\n", + SCI_KBD_MODE_AUTO, SCI_KBD_MODE_ON, SCI_KBD_MODE_OFF); +} +static DEVICE_ATTR_RO(available_kbd_modes); + +static ssize_t kbd_backlight_timeout_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int time; + int ret; + + ret = kstrtoint(buf, 0, &time); + if (ret) + return ret; + + /* Check for supported values depending on kbd_type */ + if (toshiba->kbd_type == 1) { + if (time < 0 || time > 60) + return -EINVAL; + } else if (toshiba->kbd_type == 2) { + if (time < 1 || time > 60) + return -EINVAL; + } + + /* Set the Keyboard Backlight Timeout */ + + /* Only make a change if the actual timeout has changed */ + if (toshiba->kbd_time != time) { + /* Shift the time to "base time" (0x3c0000 == 60 seconds) */ + time = time << HCI_MISC_SHIFT; + /* OR the "base time" to the actual method format */ + if (toshiba->kbd_type == 1) + time |= SCI_KBD_MODE_FNZ; + else if (toshiba->kbd_type == 2) + time |= SCI_KBD_MODE_AUTO; + + ret = toshiba_kbd_illum_status_set(toshiba, time); + if (ret) + return ret; + + toshiba->kbd_time = time >> HCI_MISC_SHIFT; + } + + return count; +} + +static ssize_t kbd_backlight_timeout_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 time; + + if (toshiba_kbd_illum_status_get(toshiba, &time) < 0) + return -EIO; + + return sprintf(buf, "%i\n", time >> HCI_MISC_SHIFT); +} +static DEVICE_ATTR_RW(kbd_backlight_timeout); + +static ssize_t touchpad_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + /* Set the TouchPad on/off, 0 - Disable | 1 - Enable */ + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + if (state != 0 && state != 1) + return -EINVAL; + + ret = toshiba_touchpad_set(toshiba, state); + if (ret) + return ret; + + return count; +} + +static ssize_t touchpad_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 state; + int ret; + + ret = toshiba_touchpad_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%i\n", state); +} +static DEVICE_ATTR_RW(touchpad); + +static ssize_t usb_sleep_charge_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 mode; + int ret; + + ret = toshiba_usb_sleep_charge_get(toshiba, &mode); + if (ret < 0) + return ret; + + return sprintf(buf, "%x\n", mode & SCI_USB_CHARGE_MODE_MASK); +} + +static ssize_t usb_sleep_charge_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + u32 mode; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + /* + * Check for supported values, where: + * 0 - Disabled + * 1 - Alternate (Non USB conformant devices that require more power) + * 2 - Auto (USB conformant devices) + * 3 - Typical + */ + if (state != 0 && state != 1 && state != 2 && state != 3) + return -EINVAL; + + /* Set the USB charging mode to internal value */ + mode = toshiba->usbsc_mode_base; + if (state == 0) + mode |= SCI_USB_CHARGE_DISABLED; + else if (state == 1) + mode |= SCI_USB_CHARGE_ALTERNATE; + else if (state == 2) + mode |= SCI_USB_CHARGE_AUTO; + else if (state == 3) + mode |= SCI_USB_CHARGE_TYPICAL; + + ret = toshiba_usb_sleep_charge_set(toshiba, mode); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW(usb_sleep_charge); + +static ssize_t sleep_functions_on_battery_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int bat_lvl, status; + u32 state; + int ret; + int tmp; + + ret = toshiba_sleep_functions_status_get(toshiba, &state); + if (ret < 0) + return ret; + + /* Determine the status: 0x4 - Enabled | 0x1 - Disabled */ + tmp = state & SCI_USB_CHARGE_BAT_MASK; + status = (tmp == 0x4) ? 1 : 0; + /* Determine the battery level set */ + bat_lvl = state >> HCI_MISC_SHIFT; + + return sprintf(buf, "%d %d\n", status, bat_lvl); +} + +static ssize_t sleep_functions_on_battery_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 status; + int value; + int ret; + int tmp; + + ret = kstrtoint(buf, 0, &value); + if (ret) + return ret; + + /* + * Set the status of the function: + * 0 - Disabled + * 1-100 - Enabled + */ + if (value < 0 || value > 100) + return -EINVAL; + + if (value == 0) { + tmp = toshiba->usbsc_bat_level << HCI_MISC_SHIFT; + status = tmp | SCI_USB_CHARGE_BAT_LVL_OFF; + } else { + tmp = value << HCI_MISC_SHIFT; + status = tmp | SCI_USB_CHARGE_BAT_LVL_ON; + } + ret = toshiba_sleep_functions_status_set(toshiba, status); + if (ret < 0) + return ret; + + toshiba->usbsc_bat_level = status >> HCI_MISC_SHIFT; + + return count; +} +static DEVICE_ATTR_RW(sleep_functions_on_battery); + +static ssize_t usb_rapid_charge_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 state; + int ret; + + ret = toshiba_usb_rapid_charge_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", state); +} + +static ssize_t usb_rapid_charge_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + if (state != 0 && state != 1) + return -EINVAL; + + ret = toshiba_usb_rapid_charge_set(toshiba, state); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW(usb_rapid_charge); + +static ssize_t usb_sleep_music_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 state; + int ret; + + ret = toshiba_usb_sleep_music_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", state); +} + +static ssize_t usb_sleep_music_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + if (state != 0 && state != 1) + return -EINVAL; + + ret = toshiba_usb_sleep_music_set(toshiba, state); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW(usb_sleep_music); + +static ssize_t kbd_function_keys_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int mode; + int ret; + + ret = toshiba_function_keys_get(toshiba, &mode); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", mode); +} + +static ssize_t kbd_function_keys_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int mode; + int ret; + + ret = kstrtoint(buf, 0, &mode); + if (ret) + return ret; + /* + * Check for the function keys mode where: + * 0 - Normal operation (F{1-12} as usual and hotkeys via FN-F{1-12}) + * 1 - Special functions (Opposite of the above setting) + */ + if (mode != 0 && mode != 1) + return -EINVAL; + + ret = toshiba_function_keys_set(toshiba, mode); + if (ret) + return ret; + + pr_info("Reboot for changes to KBD Function Keys to take effect"); + + return count; +} +static DEVICE_ATTR_RW(kbd_function_keys); + +static ssize_t panel_power_on_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 state; + int ret; + + ret = toshiba_panel_power_on_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", state); +} + +static ssize_t panel_power_on_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + if (state != 0 && state != 1) + return -EINVAL; + + ret = toshiba_panel_power_on_set(toshiba, state); + if (ret) + return ret; + + pr_info("Reboot for changes to Panel Power ON to take effect"); + + return count; +} +static DEVICE_ATTR_RW(panel_power_on); + +static ssize_t usb_three_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + u32 state; + int ret; + + ret = toshiba_usb_three_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", state); +} + +static ssize_t usb_three_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + /* + * Check for USB 3 mode where: + * 0 - Disabled (Acts like a USB 2 port, saving power) + * 1 - Enabled + */ + if (state != 0 && state != 1) + return -EINVAL; + + ret = toshiba_usb_three_set(toshiba, state); + if (ret) + return ret; + + pr_info("Reboot for changes to USB 3 to take effect"); + + return count; +} +static DEVICE_ATTR_RW(usb_three); + +static ssize_t cooling_method_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = toshiba_cooling_method_get(toshiba, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%d %d\n", state, toshiba->max_cooling_method); +} + +static ssize_t cooling_method_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_acpi_dev *toshiba = dev_get_drvdata(dev); + int state; + int ret; + + ret = kstrtoint(buf, 0, &state); + if (ret) + return ret; + + /* + * Check for supported values + * Depending on the laptop model, some only support these two: + * 0 - Maximum Performance + * 1 - Battery Optimized + * + * While some others support all three methods: + * 0 - Maximum Performance + * 1 - Performance + * 2 - Battery Optimized + */ + if (state < 0 || state > toshiba->max_cooling_method) + return -EINVAL; + + ret = toshiba_cooling_method_set(toshiba, state); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW(cooling_method); + +static struct attribute *toshiba_attributes[] = { + &dev_attr_version.attr.attr, + &dev_attr_fan.attr, + &dev_attr_kbd_backlight_mode.attr, + &dev_attr_kbd_type.attr, + &dev_attr_available_kbd_modes.attr, + &dev_attr_kbd_backlight_timeout.attr, + &dev_attr_touchpad.attr, + &dev_attr_usb_sleep_charge.attr, + &dev_attr_sleep_functions_on_battery.attr, + &dev_attr_usb_rapid_charge.attr, + &dev_attr_usb_sleep_music.attr, + &dev_attr_kbd_function_keys.attr, + &dev_attr_panel_power_on.attr, + &dev_attr_usb_three.attr, + &dev_attr_cooling_method.attr, + NULL, +}; + +static umode_t toshiba_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct toshiba_acpi_dev *drv = dev_get_drvdata(dev); + bool exists = true; + + if (attr == &dev_attr_fan.attr) + exists = (drv->fan_supported) ? true : false; + else if (attr == &dev_attr_kbd_backlight_mode.attr) + exists = (drv->kbd_illum_supported) ? true : false; + else if (attr == &dev_attr_kbd_backlight_timeout.attr) + exists = (drv->kbd_mode == SCI_KBD_MODE_AUTO) ? true : false; + else if (attr == &dev_attr_touchpad.attr) + exists = (drv->touchpad_supported) ? true : false; + else if (attr == &dev_attr_usb_sleep_charge.attr) + exists = (drv->usb_sleep_charge_supported) ? true : false; + else if (attr == &dev_attr_sleep_functions_on_battery.attr) + exists = (drv->usb_sleep_charge_supported) ? true : false; + else if (attr == &dev_attr_usb_rapid_charge.attr) + exists = (drv->usb_rapid_charge_supported) ? true : false; + else if (attr == &dev_attr_usb_sleep_music.attr) + exists = (drv->usb_sleep_music_supported) ? true : false; + else if (attr == &dev_attr_kbd_function_keys.attr) + exists = (drv->kbd_function_keys_supported) ? true : false; + else if (attr == &dev_attr_panel_power_on.attr) + exists = (drv->panel_power_on_supported) ? true : false; + else if (attr == &dev_attr_usb_three.attr) + exists = (drv->usb_three_supported) ? true : false; + else if (attr == &dev_attr_cooling_method.attr) + exists = (drv->cooling_method_supported) ? true : false; + + return exists ? attr->mode : 0; +} + +static const struct attribute_group toshiba_attr_group = { + .is_visible = toshiba_sysfs_is_visible, + .attrs = toshiba_attributes, +}; + +static void toshiba_acpi_kbd_bl_work(struct work_struct *work) +{ + /* Update the sysfs entries */ + if (sysfs_update_group(&toshiba_acpi->acpi_dev->dev.kobj, + &toshiba_attr_group)) + pr_err("Unable to update sysfs entries\n"); + + /* Notify LED subsystem about keyboard backlight change */ + if (toshiba_acpi->kbd_type == 2 && + toshiba_acpi->kbd_mode != SCI_KBD_MODE_AUTO) + led_classdev_notify_brightness_hw_changed(&toshiba_acpi->kbd_led, + (toshiba_acpi->kbd_mode == SCI_KBD_MODE_ON) ? + LED_FULL : LED_OFF); + + /* Emulate the keyboard backlight event */ + acpi_bus_generate_netlink_event(toshiba_acpi->acpi_dev->pnp.device_class, + dev_name(&toshiba_acpi->acpi_dev->dev), + 0x92, 0); +} + +/* + * IIO device + */ + +enum toshiba_iio_accel_chan { + AXIS_X, + AXIS_Y, + AXIS_Z +}; + +static int toshiba_iio_accel_get_axis(enum toshiba_iio_accel_chan chan) +{ + u32 xyval, zval; + int ret; + + ret = toshiba_accelerometer_get(toshiba_acpi, &xyval, &zval); + if (ret < 0) + return ret; + + switch (chan) { + case AXIS_X: + return xyval & HCI_ACCEL_DIRECTION_MASK ? + -(xyval & HCI_ACCEL_MASK) : xyval & HCI_ACCEL_MASK; + case AXIS_Y: + return (xyval >> HCI_MISC_SHIFT) & HCI_ACCEL_DIRECTION_MASK ? + -((xyval >> HCI_MISC_SHIFT) & HCI_ACCEL_MASK) : + (xyval >> HCI_MISC_SHIFT) & HCI_ACCEL_MASK; + case AXIS_Z: + return zval & HCI_ACCEL_DIRECTION_MASK ? + -(zval & HCI_ACCEL_MASK) : zval & HCI_ACCEL_MASK; + } + + return ret; +} + +static int toshiba_iio_accel_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int *val, int *val2, long mask) +{ + int ret; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + ret = toshiba_iio_accel_get_axis(chan->channel); + if (ret == -EIO || ret == -ENODEV) + return ret; + + *val = ret; + + return IIO_VAL_INT; + } + + return -EINVAL; +} + +#define TOSHIBA_IIO_ACCEL_CHANNEL(axis, chan) { \ + .type = IIO_ACCEL, \ + .modified = 1, \ + .channel = chan, \ + .channel2 = IIO_MOD_##axis, \ + .output = 1, \ + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ +} + +static const struct iio_chan_spec toshiba_iio_accel_channels[] = { + TOSHIBA_IIO_ACCEL_CHANNEL(X, AXIS_X), + TOSHIBA_IIO_ACCEL_CHANNEL(Y, AXIS_Y), + TOSHIBA_IIO_ACCEL_CHANNEL(Z, AXIS_Z), +}; + +static const struct iio_info toshiba_iio_accel_info = { + .read_raw = &toshiba_iio_accel_read_raw, +}; + +/* + * Misc device + */ +static int toshiba_acpi_smm_bridge(SMMRegisters *regs) +{ + u32 in[TCI_WORDS] = { regs->eax, regs->ebx, regs->ecx, + regs->edx, regs->esi, regs->edi }; + u32 out[TCI_WORDS]; + acpi_status status; + + status = tci_raw(toshiba_acpi, in, out); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query SMM registers failed\n"); + return -EIO; + } + + /* Fillout the SMM struct with the TCI call results */ + regs->eax = out[0]; + regs->ebx = out[1]; + regs->ecx = out[2]; + regs->edx = out[3]; + regs->esi = out[4]; + regs->edi = out[5]; + + return 0; +} + +static long toshiba_acpi_ioctl(struct file *fp, unsigned int cmd, + unsigned long arg) +{ + SMMRegisters __user *argp = (SMMRegisters __user *)arg; + SMMRegisters regs; + int ret; + + if (!argp) + return -EINVAL; + + switch (cmd) { + case TOSH_SMM: + if (copy_from_user(®s, argp, sizeof(SMMRegisters))) + return -EFAULT; + ret = toshiba_acpi_smm_bridge(®s); + if (ret) + return ret; + if (copy_to_user(argp, ®s, sizeof(SMMRegisters))) + return -EFAULT; + break; + case TOSHIBA_ACPI_SCI: + if (copy_from_user(®s, argp, sizeof(SMMRegisters))) + return -EFAULT; + /* Ensure we are being called with a SCI_{GET, SET} register */ + if (regs.eax != SCI_GET && regs.eax != SCI_SET) + return -EINVAL; + if (!sci_open(toshiba_acpi)) + return -EIO; + ret = toshiba_acpi_smm_bridge(®s); + sci_close(toshiba_acpi); + if (ret) + return ret; + if (copy_to_user(argp, ®s, sizeof(SMMRegisters))) + return -EFAULT; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct file_operations toshiba_acpi_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = toshiba_acpi_ioctl, + .llseek = noop_llseek, +}; + +/* + * WWAN RFKill handlers + */ +static int toshiba_acpi_wwan_set_block(void *data, bool blocked) +{ + struct toshiba_acpi_dev *dev = data; + int ret; + + ret = toshiba_wireless_status(dev); + if (ret) + return ret; + + if (!dev->killswitch) + return 0; + + return toshiba_wwan_set(dev, !blocked); +} + +static void toshiba_acpi_wwan_poll(struct rfkill *rfkill, void *data) +{ + struct toshiba_acpi_dev *dev = data; + + if (toshiba_wireless_status(dev)) + return; + + rfkill_set_hw_state(dev->wwan_rfk, !dev->killswitch); +} + +static const struct rfkill_ops wwan_rfk_ops = { + .set_block = toshiba_acpi_wwan_set_block, + .poll = toshiba_acpi_wwan_poll, +}; + +static int toshiba_acpi_setup_wwan_rfkill(struct toshiba_acpi_dev *dev) +{ + int ret = toshiba_wireless_status(dev); + + if (ret) + return ret; + + dev->wwan_rfk = rfkill_alloc("Toshiba WWAN", + &dev->acpi_dev->dev, + RFKILL_TYPE_WWAN, + &wwan_rfk_ops, + dev); + if (!dev->wwan_rfk) { + pr_err("Unable to allocate WWAN rfkill device\n"); + return -ENOMEM; + } + + rfkill_set_hw_state(dev->wwan_rfk, !dev->killswitch); + + ret = rfkill_register(dev->wwan_rfk); + if (ret) { + pr_err("Unable to register WWAN rfkill device\n"); + rfkill_destroy(dev->wwan_rfk); + } + + return ret; +} + +/* + * Hotkeys + */ +static int toshiba_acpi_enable_hotkeys(struct toshiba_acpi_dev *dev) +{ + acpi_status status; + u32 result; + + status = acpi_evaluate_object(dev->acpi_dev->handle, + "ENAB", NULL, NULL); + if (ACPI_FAILURE(status)) + return -ENODEV; + + /* + * Enable quickstart buttons if supported. + * + * Enable the "Special Functions" mode only if they are + * supported and if they are activated. + */ + if (hci_hotkey_quickstart) + result = hci_write(dev, HCI_HOTKEY_EVENT, + HCI_HOTKEY_ENABLE_QUICKSTART); + else if (dev->kbd_function_keys_supported && dev->special_functions) + result = hci_write(dev, HCI_HOTKEY_EVENT, + HCI_HOTKEY_SPECIAL_FUNCTIONS); + else + result = hci_write(dev, HCI_HOTKEY_EVENT, HCI_HOTKEY_ENABLE); + + if (result == TOS_FAILURE) + return -EIO; + else if (result == TOS_NOT_SUPPORTED) + return -ENODEV; + + return 0; +} + +static bool toshiba_acpi_i8042_filter(unsigned char data, unsigned char str, + struct serio *port, void *context) +{ + if (str & I8042_STR_AUXDATA) + return false; + + if (unlikely(data == 0xe0)) + return false; + + if ((data & 0x7f) == TOS1900_FN_SCAN) { + schedule_work(&toshiba_acpi->hotkey_work); + return true; + } + + return false; +} + +static void toshiba_acpi_hotkey_work(struct work_struct *work) +{ + acpi_handle ec_handle = ec_get_handle(); + acpi_status status; + + if (!ec_handle) + return; + + status = acpi_evaluate_object(ec_handle, "NTFY", NULL, NULL); + if (ACPI_FAILURE(status)) + pr_err("ACPI NTFY method execution failed\n"); +} + +/* + * Returns hotkey scancode, or < 0 on failure. + */ +static int toshiba_acpi_query_hotkey(struct toshiba_acpi_dev *dev) +{ + unsigned long long value; + acpi_status status; + + status = acpi_evaluate_integer(dev->acpi_dev->handle, "INFO", + NULL, &value); + if (ACPI_FAILURE(status)) { + pr_err("ACPI INFO method execution failed\n"); + return -EIO; + } + + return value; +} + +static void toshiba_acpi_report_hotkey(struct toshiba_acpi_dev *dev, + int scancode) +{ + if (scancode == 0x100) + return; + + /* Act on key press; ignore key release */ + if (scancode & 0x80) + return; + + if (!sparse_keymap_report_event(dev->hotkey_dev, scancode, 1, true)) + pr_info("Unknown key %x\n", scancode); +} + +static void toshiba_acpi_process_hotkeys(struct toshiba_acpi_dev *dev) +{ + if (dev->info_supported) { + int scancode = toshiba_acpi_query_hotkey(dev); + + if (scancode < 0) { + pr_err("Failed to query hotkey event\n"); + } else if (scancode != 0) { + toshiba_acpi_report_hotkey(dev, scancode); + dev->key_event_valid = 1; + dev->last_key_event = scancode; + } + } else if (dev->system_event_supported) { + u32 result; + u32 value; + int retries = 3; + + do { + result = hci_read(dev, HCI_SYSTEM_EVENT, &value); + switch (result) { + case TOS_SUCCESS: + toshiba_acpi_report_hotkey(dev, (int)value); + dev->key_event_valid = 1; + dev->last_key_event = value; + break; + case TOS_NOT_SUPPORTED: + /* + * This is a workaround for an unresolved + * issue on some machines where system events + * sporadically become disabled. + */ + result = hci_write(dev, HCI_SYSTEM_EVENT, 1); + if (result == TOS_SUCCESS) + pr_notice("Re-enabled hotkeys\n"); + fallthrough; + default: + retries--; + break; + } + } while (retries && result != TOS_FIFO_EMPTY); + } +} + +static int toshiba_acpi_setup_keyboard(struct toshiba_acpi_dev *dev) +{ + const struct key_entry *keymap = toshiba_acpi_keymap; + acpi_handle ec_handle; + int error; + + if (disable_hotkeys) { + pr_info("Hotkeys disabled by module parameter\n"); + return 0; + } + + if (wmi_has_guid(TOSHIBA_WMI_EVENT_GUID)) { + pr_info("WMI event detected, hotkeys will not be monitored\n"); + return 0; + } + + error = toshiba_acpi_enable_hotkeys(dev); + if (error) + return error; + + if (toshiba_hotkey_event_type_get(dev, &dev->hotkey_event_type)) + pr_notice("Unable to query Hotkey Event Type\n"); + + dev->hotkey_dev = input_allocate_device(); + if (!dev->hotkey_dev) + return -ENOMEM; + + dev->hotkey_dev->name = "Toshiba input device"; + dev->hotkey_dev->phys = "toshiba_acpi/input0"; + dev->hotkey_dev->id.bustype = BUS_HOST; + dev->hotkey_dev->dev.parent = &dev->acpi_dev->dev; + + if (dev->hotkey_event_type == HCI_SYSTEM_TYPE1 || + !dev->kbd_function_keys_supported) + keymap = toshiba_acpi_keymap; + else if (dev->hotkey_event_type == HCI_SYSTEM_TYPE2 || + dev->kbd_function_keys_supported) + keymap = toshiba_acpi_alt_keymap; + else + pr_info("Unknown event type received %x\n", + dev->hotkey_event_type); + error = sparse_keymap_setup(dev->hotkey_dev, keymap, NULL); + if (error) + goto err_free_dev; + + /* + * For some machines the SCI responsible for providing hotkey + * notification doesn't fire. We can trigger the notification + * whenever the Fn key is pressed using the NTFY method, if + * supported, so if it's present set up an i8042 key filter + * for this purpose. + */ + ec_handle = ec_get_handle(); + if (ec_handle && acpi_has_method(ec_handle, "NTFY")) { + INIT_WORK(&dev->hotkey_work, toshiba_acpi_hotkey_work); + + error = i8042_install_filter(toshiba_acpi_i8042_filter, NULL); + if (error) { + pr_err("Error installing key filter\n"); + goto err_free_dev; + } + + dev->ntfy_supported = 1; + } + + /* + * Determine hotkey query interface. Prefer using the INFO + * method when it is available. + */ + if (acpi_has_method(dev->acpi_dev->handle, "INFO")) + dev->info_supported = 1; + else if (hci_write(dev, HCI_SYSTEM_EVENT, 1) == TOS_SUCCESS) + dev->system_event_supported = 1; + + if (!dev->info_supported && !dev->system_event_supported) { + pr_warn("No hotkey query interface found\n"); + error = -EINVAL; + goto err_remove_filter; + } + + error = input_register_device(dev->hotkey_dev); + if (error) { + pr_info("Unable to register input device\n"); + goto err_remove_filter; + } + + return 0; + + err_remove_filter: + if (dev->ntfy_supported) + i8042_remove_filter(toshiba_acpi_i8042_filter); + err_free_dev: + input_free_device(dev->hotkey_dev); + dev->hotkey_dev = NULL; + return error; +} + +static int toshiba_acpi_setup_backlight(struct toshiba_acpi_dev *dev) +{ + struct backlight_properties props; + int brightness; + int ret; + + /* + * Some machines don't support the backlight methods at all, and + * others support it read-only. Either of these is pretty useless, + * so only register the backlight device if the backlight method + * supports both reads and writes. + */ + brightness = __get_lcd_brightness(dev); + if (brightness < 0) + return 0; + /* + * If transflective backlight is supported and the brightness is zero + * (lowest brightness level), the set_lcd_brightness function will + * activate the transflective backlight, making the LCD appear to be + * turned off, simply increment the brightness level to avoid that. + */ + if (dev->tr_backlight_supported && brightness == 0) + brightness++; + ret = set_lcd_brightness(dev, brightness); + if (ret) { + pr_debug("Backlight method is read-only, disabling backlight support\n"); + return 0; + } + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) + return 0; + + memset(&props, 0, sizeof(props)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = HCI_LCD_BRIGHTNESS_LEVELS - 1; + + /* Adding an extra level and having 0 change to transflective mode */ + if (dev->tr_backlight_supported) + props.max_brightness++; + + dev->backlight_dev = backlight_device_register("toshiba", + &dev->acpi_dev->dev, + dev, + &toshiba_backlight_data, + &props); + if (IS_ERR(dev->backlight_dev)) { + ret = PTR_ERR(dev->backlight_dev); + pr_err("Could not register toshiba backlight device\n"); + dev->backlight_dev = NULL; + return ret; + } + + dev->backlight_dev->props.brightness = brightness; + return 0; +} + +/* HWMON support for fan */ +#if IS_ENABLED(CONFIG_HWMON) +static umode_t toshiba_acpi_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return 0444; +} + +static int toshiba_acpi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + /* + * There is only a single channel and single attribute (for the + * fan) at this point. + * This can be replaced with more advanced logic in the future, + * should the need arise. + */ + if (type == hwmon_fan && channel == 0 && attr == hwmon_fan_input) { + u32 value; + int ret; + + ret = get_fan_rpm(toshiba_acpi, &value); + if (ret) + return ret; + + *val = value; + return 0; + } + return -EOPNOTSUPP; +} + +static const struct hwmon_channel_info * const toshiba_acpi_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + NULL +}; + +static const struct hwmon_ops toshiba_acpi_hwmon_ops = { + .is_visible = toshiba_acpi_hwmon_is_visible, + .read = toshiba_acpi_hwmon_read, +}; + +static const struct hwmon_chip_info toshiba_acpi_hwmon_chip_info = { + .ops = &toshiba_acpi_hwmon_ops, + .info = toshiba_acpi_hwmon_info, +}; +#endif + +/* ACPI battery hooking */ +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + u32 state; + int status; + + if (toshiba_acpi == NULL) { + pr_err("Toshiba ACPI object invalid\n"); + return -ENODEV; + } + + status = toshiba_battery_charge_mode_get(toshiba_acpi, &state); + + if (status != 0) + return status; + + if (state == 1) + return sprintf(buf, "80\n"); + else + return sprintf(buf, "100\n"); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + u32 value; + int rval; + + if (toshiba_acpi == NULL) { + pr_err("Toshiba ACPI object invalid\n"); + return -ENODEV; + } + + rval = kstrtou32(buf, 10, &value); + if (rval) + return rval; + + if (value < 1 || value > 100) + return -EINVAL; + rval = toshiba_battery_charge_mode_set(toshiba_acpi, + (value < 90) ? 1 : 0); + if (rval < 0) + return rval; + else + return count; +} + +static DEVICE_ATTR_RW(charge_control_end_threshold); + +static struct attribute *toshiba_acpi_battery_attrs[] = { + &dev_attr_charge_control_end_threshold.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(toshiba_acpi_battery); + +static int toshiba_acpi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + if (toshiba_acpi == NULL) { + pr_err("Init order issue\n"); + return -ENODEV; + } + if (!toshiba_acpi->battery_charge_mode_supported) + return -ENODEV; + if (device_add_groups(&battery->dev, toshiba_acpi_battery_groups)) + return -ENODEV; + return 0; +} + +static int toshiba_acpi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_groups(&battery->dev, toshiba_acpi_battery_groups); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = toshiba_acpi_battery_add, + .remove_battery = toshiba_acpi_battery_remove, + .name = "Toshiba Battery Extension", +}; + +static void print_supported_features(struct toshiba_acpi_dev *dev) +{ + pr_info("Supported laptop features:"); + + if (dev->hotkey_dev) + pr_cont(" hotkeys"); + if (dev->backlight_dev) + pr_cont(" backlight"); + if (dev->video_supported) + pr_cont(" video-out"); + if (dev->fan_supported) + pr_cont(" fan"); + if (dev->fan_rpm_supported) + pr_cont(" fan-rpm"); + if (dev->tr_backlight_supported) + pr_cont(" transflective-backlight"); + if (dev->illumination_supported) + pr_cont(" illumination"); + if (dev->kbd_illum_supported) + pr_cont(" keyboard-backlight"); + if (dev->touchpad_supported) + pr_cont(" touchpad"); + if (dev->eco_supported) + pr_cont(" eco-led"); + if (dev->accelerometer_supported) + pr_cont(" accelerometer-axes"); + if (dev->usb_sleep_charge_supported) + pr_cont(" usb-sleep-charge"); + if (dev->usb_rapid_charge_supported) + pr_cont(" usb-rapid-charge"); + if (dev->usb_sleep_music_supported) + pr_cont(" usb-sleep-music"); + if (dev->kbd_function_keys_supported) + pr_cont(" special-function-keys"); + if (dev->panel_power_on_supported) + pr_cont(" panel-power-on"); + if (dev->usb_three_supported) + pr_cont(" usb3"); + if (dev->wwan_supported) + pr_cont(" wwan"); + if (dev->cooling_method_supported) + pr_cont(" cooling-method"); + if (dev->battery_charge_mode_supported) + pr_cont(" battery-charge-mode"); + + pr_cont("\n"); +} + +static void toshiba_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct toshiba_acpi_dev *dev = data; + struct acpi_device *acpi_dev = dev->acpi_dev; + + switch (event) { + case 0x80: /* Hotkeys and some system events */ + /* + * Machines with this WMI GUID aren't supported due to bugs in + * their AML. + * + * Return silently to avoid triggering a netlink event. + */ + if (wmi_has_guid(TOSHIBA_WMI_EVENT_GUID)) + return; + toshiba_acpi_process_hotkeys(dev); + break; + case 0x81: /* Dock events */ + case 0x82: + case 0x83: + pr_info("Dock event received %x\n", event); + break; + case 0x88: /* Thermal events */ + pr_info("Thermal event received\n"); + break; + case 0x8f: /* LID closed */ + case 0x90: /* LID is closed and Dock has been ejected */ + break; + case 0x8c: /* SATA power events */ + case 0x8b: + pr_info("SATA power event received %x\n", event); + break; + case 0x92: /* Keyboard backlight mode changed */ + dev->kbd_event_generated = true; + /* Update sysfs entries */ + if (sysfs_update_group(&acpi_dev->dev.kobj, + &toshiba_attr_group)) + pr_err("Unable to update sysfs entries\n"); + /* Notify LED subsystem about keyboard backlight change */ + if (dev->kbd_type == 2 && dev->kbd_mode != SCI_KBD_MODE_AUTO) + led_classdev_notify_brightness_hw_changed(&dev->kbd_led, + (dev->kbd_mode == SCI_KBD_MODE_ON) ? + LED_FULL : LED_OFF); + break; + case 0x8e: /* Power button pressed */ + break; + case 0x85: /* Unknown */ + case 0x8d: /* Unknown */ + case 0x94: /* Unknown */ + case 0x95: /* Unknown */ + default: + pr_info("Unknown event received %x\n", event); + break; + } + + acpi_bus_generate_netlink_event(acpi_dev->pnp.device_class, + dev_name(&acpi_dev->dev), + event, (event == 0x80) ? + dev->last_key_event : 0); +} + +static void toshiba_acpi_remove(struct platform_device *pdev) +{ + struct toshiba_acpi_dev *dev = platform_get_drvdata(pdev); + + misc_deregister(&dev->miscdev); + + remove_toshiba_proc_entries(dev); + + if (dev->notify_handler_installed) + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, + toshiba_acpi_notify); + +#if IS_ENABLED(CONFIG_HWMON) + if (dev->hwmon_device) + hwmon_device_unregister(dev->hwmon_device); +#endif + + if (dev->accelerometer_supported && dev->indio_dev) { + iio_device_unregister(dev->indio_dev); + iio_device_free(dev->indio_dev); + } + + if (dev->sysfs_created) + sysfs_remove_group(&dev->acpi_dev->dev.kobj, + &toshiba_attr_group); + + if (dev->ntfy_supported) { + i8042_remove_filter(toshiba_acpi_i8042_filter); + cancel_work_sync(&dev->hotkey_work); + } + + if (dev->hotkey_dev) + input_unregister_device(dev->hotkey_dev); + + backlight_device_unregister(dev->backlight_dev); + + led_classdev_unregister(&dev->led_dev); + led_classdev_unregister(&dev->kbd_led); + led_classdev_unregister(&dev->eco_led); + + if (dev->wwan_rfk) { + rfkill_unregister(dev->wwan_rfk); + rfkill_destroy(dev->wwan_rfk); + } + + if (dev->battery_charge_mode_supported) + battery_hook_unregister(&battery_hook); + + if (toshiba_acpi) + toshiba_acpi = NULL; + + dev_set_drvdata(&dev->acpi_dev->dev, NULL); + + kfree(dev); +} + +static const char *find_hci_method(acpi_handle handle) +{ + if (acpi_has_method(handle, "GHCI")) + return "GHCI"; + + if (acpi_has_method(handle, "SPFC")) + return "SPFC"; + + return NULL; +} + +/* + * Some Toshibas have a broken acpi-video interface for brightness control, + * these are quirked in drivers/acpi/video_detect.c to use the GPU native + * (/sys/class/backlight/intel_backlight) instead. + * But these need a HCI_SET call to actually turn the panel back on at resume, + * without this call the screen stays black at resume. + * Either HCI_LCD_BRIGHTNESS (used by acpi_video's _BCM) or HCI_PANEL_POWER_ON + * works. toshiba_acpi_resume() uses HCI_PANEL_POWER_ON to avoid changing + * the configured brightness level. + */ +#define QUIRK_TURN_ON_PANEL_ON_RESUME BIT(0) +/* + * Some Toshibas use "quickstart" keys. On these, HCI_HOTKEY_EVENT must use + * the value HCI_HOTKEY_ENABLE_QUICKSTART. + */ +#define QUIRK_HCI_HOTKEY_QUICKSTART BIT(1) + +static const struct dmi_system_id toshiba_dmi_quirks[] __initconst = { + { + /* Toshiba Portégé R700 */ + /* https://bugzilla.kernel.org/show_bug.cgi?id=21012 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TOSHIBA"), + DMI_MATCH(DMI_PRODUCT_NAME, "PORTEGE R700"), + }, + .driver_data = (void *)QUIRK_TURN_ON_PANEL_ON_RESUME, + }, + { + /* Toshiba Satellite/Portégé R830 */ + /* Portégé: https://bugs.freedesktop.org/show_bug.cgi?id=82634 */ + /* Satellite: https://bugzilla.kernel.org/show_bug.cgi?id=21012 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TOSHIBA"), + DMI_MATCH(DMI_PRODUCT_NAME, "R830"), + }, + .driver_data = (void *)QUIRK_TURN_ON_PANEL_ON_RESUME, + }, + { + /* Toshiba Satellite/Portégé Z830 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TOSHIBA"), + DMI_MATCH(DMI_PRODUCT_NAME, "Z830"), + }, + .driver_data = (void *)(QUIRK_TURN_ON_PANEL_ON_RESUME | QUIRK_HCI_HOTKEY_QUICKSTART), + }, + { } +}; + +static int toshiba_acpi_probe(struct platform_device *pdev) +{ + struct acpi_device *acpi_dev; + struct toshiba_acpi_dev *dev; + const char *hci_method; + u32 dummy; + int ret = 0; + + if (toshiba_acpi) + return -EBUSY; + + acpi_dev = ACPI_COMPANION(&pdev->dev); + if (!acpi_dev) + return -ENODEV; + + pr_info("Toshiba Laptop ACPI Extras version %s\n", + TOSHIBA_ACPI_VERSION); + + hci_method = find_hci_method(acpi_dev->handle); + if (!hci_method) { + pr_err("HCI interface not found\n"); + return -ENODEV; + } + + dev = kzalloc_obj(*dev); + if (!dev) + return -ENOMEM; + dev->acpi_dev = acpi_dev; + dev->method_hci = hci_method; + dev->miscdev.minor = MISC_DYNAMIC_MINOR; + dev->miscdev.name = "toshiba_acpi"; + dev->miscdev.fops = &toshiba_acpi_fops; + + ret = misc_register(&dev->miscdev); + if (ret) { + pr_err("Failed to register miscdevice\n"); + kfree(dev); + return ret; + } + + platform_set_drvdata(pdev, dev); + dev_set_drvdata(&acpi_dev->dev, dev); + + /* Query the BIOS for supported features */ + + /* + * The "Special Functions" are always supported by the laptops + * with the new keyboard layout, query for its presence to help + * determine the keymap layout to use. + */ + ret = toshiba_function_keys_get(dev, &dev->special_functions); + dev->kbd_function_keys_supported = !ret; + + dev->hotkey_event_type = 0; + if (toshiba_acpi_setup_keyboard(dev)) + pr_info("Unable to activate hotkeys\n"); + + /* Determine whether or not BIOS supports transflective backlight */ + ret = get_tr_backlight_status(dev, &dummy); + dev->tr_backlight_supported = !ret; + + ret = toshiba_acpi_setup_backlight(dev); + if (ret) + goto error; + + toshiba_illumination_available(dev); + if (dev->illumination_supported) { + dev->led_dev.name = "toshiba::illumination"; + dev->led_dev.max_brightness = 1; + dev->led_dev.brightness_set = toshiba_illumination_set; + dev->led_dev.brightness_get = toshiba_illumination_get; + led_classdev_register(&pdev->dev, &dev->led_dev); + } + + toshiba_eco_mode_available(dev); + if (dev->eco_supported) { + dev->eco_led.name = "toshiba::eco_mode"; + dev->eco_led.max_brightness = 1; + dev->eco_led.brightness_set = toshiba_eco_mode_set_status; + dev->eco_led.brightness_get = toshiba_eco_mode_get_status; + led_classdev_register(&pdev->dev, &dev->eco_led); + } + + toshiba_kbd_illum_available(dev); + /* + * Only register the LED if KBD illumination is supported + * and the keyboard backlight operation mode is set to FN-Z + * or we detect a second gen keyboard backlight + */ + if (dev->kbd_illum_supported && + (dev->kbd_mode == SCI_KBD_MODE_FNZ || dev->kbd_type == 2)) { + dev->kbd_led.name = "toshiba::kbd_backlight"; + dev->kbd_led.flags = LED_BRIGHT_HW_CHANGED; + dev->kbd_led.max_brightness = 1; + dev->kbd_led.brightness_set = toshiba_kbd_backlight_set; + dev->kbd_led.brightness_get = toshiba_kbd_backlight_get; + led_classdev_register(&pdev->dev, &dev->kbd_led); + } + + ret = toshiba_touchpad_get(dev, &dummy); + dev->touchpad_supported = !ret; + + toshiba_accelerometer_available(dev); + if (dev->accelerometer_supported) { + dev->indio_dev = iio_device_alloc(&pdev->dev, sizeof(*dev)); + if (!dev->indio_dev) { + pr_err("Unable to allocate iio device\n"); + goto iio_error; + } + + pr_info("Registering Toshiba accelerometer iio device\n"); + + dev->indio_dev->info = &toshiba_iio_accel_info; + dev->indio_dev->name = "Toshiba accelerometer"; + dev->indio_dev->modes = INDIO_DIRECT_MODE; + dev->indio_dev->channels = toshiba_iio_accel_channels; + dev->indio_dev->num_channels = + ARRAY_SIZE(toshiba_iio_accel_channels); + + ret = iio_device_register(dev->indio_dev); + if (ret < 0) { + pr_err("Unable to register iio device\n"); + iio_device_free(dev->indio_dev); + } + } +iio_error: + + toshiba_usb_sleep_charge_available(dev); + + ret = toshiba_usb_rapid_charge_get(dev, &dummy); + dev->usb_rapid_charge_supported = !ret; + + ret = toshiba_usb_sleep_music_get(dev, &dummy); + dev->usb_sleep_music_supported = !ret; + + ret = toshiba_panel_power_on_get(dev, &dummy); + dev->panel_power_on_supported = !ret; + + ret = toshiba_usb_three_get(dev, &dummy); + dev->usb_three_supported = !ret; + + ret = get_video_status(dev, &dummy); + dev->video_supported = !ret; + + ret = get_fan_status(dev, &dummy); + dev->fan_supported = !ret; + + ret = get_fan_rpm(dev, &dummy); + dev->fan_rpm_supported = !ret; + +#if IS_ENABLED(CONFIG_HWMON) + if (dev->fan_rpm_supported) { + dev->hwmon_device = hwmon_device_register_with_info( + &pdev->dev, "toshiba_acpi_sensors", NULL, + &toshiba_acpi_hwmon_chip_info, NULL); + if (IS_ERR(dev->hwmon_device)) { + dev->hwmon_device = NULL; + pr_warn("unable to register hwmon device, skipping\n"); + } + } +#endif + + toshiba_wwan_available(dev); + if (dev->wwan_supported) + toshiba_acpi_setup_wwan_rfkill(dev); + + toshiba_cooling_method_available(dev); + + toshiba_battery_charge_mode_available(dev); + + print_supported_features(dev); + + ret = sysfs_create_group(&dev->acpi_dev->dev.kobj, + &toshiba_attr_group); + if (ret) { + dev->sysfs_created = 0; + goto error; + } + dev->sysfs_created = !ret; + + ret = acpi_dev_install_notify_handler(acpi_dev, ACPI_DEVICE_NOTIFY, + toshiba_acpi_notify, dev); + if (ret) + goto error; + + dev->notify_handler_installed = 1; + + create_toshiba_proc_entries(dev); + + toshiba_acpi = dev; + + /* + * As the battery hook relies on the static variable toshiba_acpi being + * set, this must be done after toshiba_acpi is assigned. + */ + if (dev->battery_charge_mode_supported) + battery_hook_register(&battery_hook); + + return 0; + +error: + toshiba_acpi_remove(pdev); + return ret; +} + +#ifdef CONFIG_PM_SLEEP +static int toshiba_acpi_suspend(struct device *device) +{ + struct toshiba_acpi_dev *dev = dev_get_drvdata(device); + + if (dev->hotkey_dev) { + u32 result; + + result = hci_write(dev, HCI_HOTKEY_EVENT, HCI_HOTKEY_DISABLE); + if (result != TOS_SUCCESS) + pr_info("Unable to disable hotkeys\n"); + } + + return 0; +} + +static int toshiba_acpi_resume(struct device *device) +{ + struct toshiba_acpi_dev *dev = dev_get_drvdata(device); + + if (dev->hotkey_dev) { + if (toshiba_acpi_enable_hotkeys(dev)) + pr_info("Unable to re-enable hotkeys\n"); + } + + if (dev->wwan_rfk) { + if (!toshiba_wireless_status(dev)) + rfkill_set_hw_state(dev->wwan_rfk, !dev->killswitch); + } + + if (turn_on_panel_on_resume) + hci_write(dev, HCI_PANEL_POWER_ON, 1); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(toshiba_acpi_pm, + toshiba_acpi_suspend, toshiba_acpi_resume); + +static struct platform_driver toshiba_acpi_driver = { + .probe = toshiba_acpi_probe, + .remove = toshiba_acpi_remove, + .driver = { + .name = "Toshiba ACPI driver", + .acpi_match_table = toshiba_device_ids, + .pm = &toshiba_acpi_pm, + }, +}; + +static void __init toshiba_dmi_init(void) +{ + const struct dmi_system_id *dmi_id; + long quirks = 0; + + dmi_id = dmi_first_match(toshiba_dmi_quirks); + if (dmi_id) + quirks = (long)dmi_id->driver_data; + + if (turn_on_panel_on_resume == -1) + turn_on_panel_on_resume = !!(quirks & QUIRK_TURN_ON_PANEL_ON_RESUME); + + if (hci_hotkey_quickstart == -1) + hci_hotkey_quickstart = !!(quirks & QUIRK_HCI_HOTKEY_QUICKSTART); +} + +static int __init toshiba_acpi_init(void) +{ + int ret; + + toshiba_dmi_init(); + toshiba_proc_dir = proc_mkdir(PROC_TOSHIBA, acpi_root_dir); + if (!toshiba_proc_dir) { + pr_err("Unable to create proc dir " PROC_TOSHIBA "\n"); + return -ENODEV; + } + + ret = platform_driver_register(&toshiba_acpi_driver); + if (ret) { + pr_err("Failed to register ACPI driver: %d\n", ret); + remove_proc_entry(PROC_TOSHIBA, acpi_root_dir); + } + + return ret; +} + +static void __exit toshiba_acpi_exit(void) +{ + platform_driver_unregister(&toshiba_acpi_driver); + if (toshiba_proc_dir) + remove_proc_entry(PROC_TOSHIBA, acpi_root_dir); +} + +module_init(toshiba_acpi_init); +module_exit(toshiba_acpi_exit); diff --git a/drivers/platform/x86/toshiba_bluetooth.c b/drivers/platform/x86/toshiba_bluetooth.c new file mode 100644 index 000000000..e00abba58 --- /dev/null +++ b/drivers/platform/x86/toshiba_bluetooth.c @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Toshiba Bluetooth Enable Driver + * + * Copyright (C) 2009 Jes Sorensen + * Copyright (C) 2015 Azael Avalos + * + * Thanks to Matthew Garrett for background info on ACPI innards which + * normal people aren't meant to understand :-) + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include + +#define BT_KILLSWITCH_MASK 0x01 +#define BT_PLUGGED_MASK 0x40 +#define BT_POWER_MASK 0x80 + +MODULE_AUTHOR("Jes Sorensen "); +MODULE_DESCRIPTION("Toshiba Laptop ACPI Bluetooth Enable Driver"); +MODULE_LICENSE("GPL"); + +struct toshiba_bluetooth_dev { + struct acpi_device *acpi_dev; + struct rfkill *rfk; + + bool killswitch; + bool plugged; + bool powered; +}; + +static int toshiba_bt_rfkill_probe(struct platform_device *pdev); +static void toshiba_bt_rfkill_remove(struct platform_device *pdev); +static void toshiba_bt_rfkill_notify(acpi_handle handle, u32 event, void *data); + +static const struct acpi_device_id bt_device_ids[] = { + { "TOS6205", 0}, + { "", 0}, +}; +MODULE_DEVICE_TABLE(acpi, bt_device_ids); + +#ifdef CONFIG_PM_SLEEP +static int toshiba_bt_resume(struct device *dev); +#endif +static SIMPLE_DEV_PM_OPS(toshiba_bt_pm, NULL, toshiba_bt_resume); + +static struct platform_driver toshiba_bt_rfkill_driver = { + .probe = toshiba_bt_rfkill_probe, + .remove = toshiba_bt_rfkill_remove, + .driver = { + .name = "Toshiba BT", + .acpi_match_table = bt_device_ids, + .pm = &toshiba_bt_pm, + }, +}; + +static int toshiba_bluetooth_present(acpi_handle handle) +{ + acpi_status result; + u64 bt_present; + + /* + * Some Toshiba laptops may have a fake TOS6205 device in + * their ACPI BIOS, so query the _STA method to see if there + * is really anything there. + */ + result = acpi_evaluate_integer(handle, "_STA", NULL, &bt_present); + if (ACPI_FAILURE(result)) { + pr_err("ACPI call to query Bluetooth presence failed\n"); + return -ENXIO; + } + + if (!bt_present) { + pr_info("Bluetooth device not present\n"); + return -ENODEV; + } + + return 0; +} + +static int toshiba_bluetooth_status(acpi_handle handle) +{ + acpi_status result; + u64 status; + + result = acpi_evaluate_integer(handle, "BTST", NULL, &status); + if (ACPI_FAILURE(result)) { + pr_err("Could not get Bluetooth device status\n"); + return -ENXIO; + } + + return status; +} + +static int toshiba_bluetooth_enable(acpi_handle handle) +{ + acpi_status result; + + result = acpi_evaluate_object(handle, "AUSB", NULL, NULL); + if (ACPI_FAILURE(result)) { + pr_err("Could not attach USB Bluetooth device\n"); + return -ENXIO; + } + + result = acpi_evaluate_object(handle, "BTPO", NULL, NULL); + if (ACPI_FAILURE(result)) { + pr_err("Could not power ON Bluetooth device\n"); + return -ENXIO; + } + + return 0; +} + +static int toshiba_bluetooth_disable(acpi_handle handle) +{ + acpi_status result; + + result = acpi_evaluate_object(handle, "BTPF", NULL, NULL); + if (ACPI_FAILURE(result)) { + pr_err("Could not power OFF Bluetooth device\n"); + return -ENXIO; + } + + result = acpi_evaluate_object(handle, "DUSB", NULL, NULL); + if (ACPI_FAILURE(result)) { + pr_err("Could not detach USB Bluetooth device\n"); + return -ENXIO; + } + + return 0; +} + +/* Helper function */ +static int toshiba_bluetooth_sync_status(struct toshiba_bluetooth_dev *bt_dev) +{ + int status; + + status = toshiba_bluetooth_status(bt_dev->acpi_dev->handle); + if (status < 0) { + pr_err("Could not sync bluetooth device status\n"); + return status; + } + + bt_dev->killswitch = (status & BT_KILLSWITCH_MASK) ? true : false; + bt_dev->plugged = (status & BT_PLUGGED_MASK) ? true : false; + bt_dev->powered = (status & BT_POWER_MASK) ? true : false; + + pr_debug("Bluetooth status %d killswitch %d plugged %d powered %d\n", + status, bt_dev->killswitch, bt_dev->plugged, bt_dev->powered); + + return 0; +} + +/* RFKill handlers */ +static int bt_rfkill_set_block(void *data, bool blocked) +{ + struct toshiba_bluetooth_dev *bt_dev = data; + int ret; + + ret = toshiba_bluetooth_sync_status(bt_dev); + if (ret) + return ret; + + if (!bt_dev->killswitch) + return 0; + + if (blocked) + ret = toshiba_bluetooth_disable(bt_dev->acpi_dev->handle); + else + ret = toshiba_bluetooth_enable(bt_dev->acpi_dev->handle); + + return ret; +} + +static void bt_rfkill_poll(struct rfkill *rfkill, void *data) +{ + struct toshiba_bluetooth_dev *bt_dev = data; + + if (toshiba_bluetooth_sync_status(bt_dev)) + return; + + /* + * Note the Toshiba Bluetooth RFKill switch seems to be a strange + * fish. It only provides a BT event when the switch is flipped to + * the 'on' position. When flipping it to 'off', the USB device is + * simply pulled away underneath us, without any BT event being + * delivered. + */ + rfkill_set_hw_state(bt_dev->rfk, !bt_dev->killswitch); +} + +static const struct rfkill_ops rfk_ops = { + .set_block = bt_rfkill_set_block, + .poll = bt_rfkill_poll, +}; + +/* ACPI driver functions */ +static void toshiba_bt_rfkill_notify(acpi_handle handle, u32 event, void *data) +{ + struct toshiba_bluetooth_dev *bt_dev = data; + + if (toshiba_bluetooth_sync_status(bt_dev)) + return; + + rfkill_set_hw_state(bt_dev->rfk, !bt_dev->killswitch); +} + +#ifdef CONFIG_PM_SLEEP +static int toshiba_bt_resume(struct device *dev) +{ + struct toshiba_bluetooth_dev *bt_dev = dev_get_drvdata(dev); + int ret; + + ret = toshiba_bluetooth_sync_status(bt_dev); + if (ret) + return ret; + + rfkill_set_hw_state(bt_dev->rfk, !bt_dev->killswitch); + + return 0; +} +#endif + +static int toshiba_bt_rfkill_probe(struct platform_device *pdev) +{ + struct toshiba_bluetooth_dev *bt_dev; + struct acpi_device *device; + int result; + + device = ACPI_COMPANION(&pdev->dev); + if (!device) + return -ENODEV; + + result = toshiba_bluetooth_present(device->handle); + if (result) + return result; + + pr_info("Toshiba ACPI Bluetooth device driver\n"); + + bt_dev = kzalloc_obj(*bt_dev); + if (!bt_dev) + return -ENOMEM; + bt_dev->acpi_dev = device; + + platform_set_drvdata(pdev, bt_dev); + + result = toshiba_bluetooth_sync_status(bt_dev); + if (result) { + kfree(bt_dev); + return result; + } + + bt_dev->rfk = rfkill_alloc("Toshiba Bluetooth", + &pdev->dev, + RFKILL_TYPE_BLUETOOTH, + &rfk_ops, + bt_dev); + if (!bt_dev->rfk) { + pr_err("Unable to allocate rfkill device\n"); + result = -ENOMEM; + goto err_free_bt_dev; + } + + rfkill_set_hw_state(bt_dev->rfk, !bt_dev->killswitch); + + result = rfkill_register(bt_dev->rfk); + if (result) { + pr_err("Unable to register rfkill device\n"); + goto err_rfkill_destroy; + } + + result = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + toshiba_bt_rfkill_notify, bt_dev); + if (result) { + pr_err("Unable to register ACPI notify handler\n"); + goto err_rfkill_unregister; + } + + return 0; + +err_rfkill_unregister: + rfkill_unregister(bt_dev->rfk); +err_rfkill_destroy: + rfkill_destroy(bt_dev->rfk); +err_free_bt_dev: + kfree(bt_dev); + return result; +} + +static void toshiba_bt_rfkill_remove(struct platform_device *pdev) +{ + struct toshiba_bluetooth_dev *bt_dev = platform_get_drvdata(pdev); + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + + /* clean up */ + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, + toshiba_bt_rfkill_notify); + + if (bt_dev->rfk) { + rfkill_unregister(bt_dev->rfk); + rfkill_destroy(bt_dev->rfk); + } + + kfree(bt_dev); + + toshiba_bluetooth_disable(device->handle); +} + +module_platform_driver(toshiba_bt_rfkill_driver); diff --git a/drivers/platform/x86/toshiba_haps.c b/drivers/platform/x86/toshiba_haps.c new file mode 100644 index 000000000..8d1224192 --- /dev/null +++ b/drivers/platform/x86/toshiba_haps.c @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Toshiba HDD Active Protection Sensor (HAPS) driver + * + * Copyright (C) 2014 Azael Avalos + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Azael Avalos "); +MODULE_DESCRIPTION("Toshiba HDD Active Protection Sensor"); +MODULE_LICENSE("GPL"); + +struct toshiba_haps_dev { + struct acpi_device *acpi_dev; + + int protection_level; +}; + +static struct toshiba_haps_dev *toshiba_haps; + +/* HAPS functions */ +static int toshiba_haps_reset_protection(acpi_handle handle) +{ + acpi_status status; + + status = acpi_evaluate_object(handle, "RSSS", NULL, NULL); + if (ACPI_FAILURE(status)) { + pr_err("Unable to reset the HDD protection\n"); + return -EIO; + } + + return 0; +} + +static int toshiba_haps_protection_level(acpi_handle handle, int level) +{ + acpi_status status; + + status = acpi_execute_simple_method(handle, "PTLV", level); + if (ACPI_FAILURE(status)) { + pr_err("Error while setting the protection level\n"); + return -EIO; + } + + pr_debug("HDD protection level set to: %d\n", level); + + return 0; +} + +/* sysfs files */ +static ssize_t protection_level_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct toshiba_haps_dev *haps = dev_get_drvdata(dev); + + return sprintf(buf, "%i\n", haps->protection_level); +} + +static ssize_t protection_level_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_haps_dev *haps = dev_get_drvdata(dev); + int level; + int ret; + + ret = kstrtoint(buf, 0, &level); + if (ret) + return ret; + /* + * Check for supported levels, which can be: + * 0 - Disabled | 1 - Low | 2 - Medium | 3 - High + */ + if (level < 0 || level > 3) + return -EINVAL; + + /* Set the sensor level */ + ret = toshiba_haps_protection_level(haps->acpi_dev->handle, level); + if (ret != 0) + return ret; + + haps->protection_level = level; + + return count; +} +static DEVICE_ATTR_RW(protection_level); + +static ssize_t reset_protection_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct toshiba_haps_dev *haps = dev_get_drvdata(dev); + int reset; + int ret; + + ret = kstrtoint(buf, 0, &reset); + if (ret) + return ret; + /* The only accepted value is 1 */ + if (reset != 1) + return -EINVAL; + + /* Reset the protection interface */ + ret = toshiba_haps_reset_protection(haps->acpi_dev->handle); + if (ret != 0) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset_protection); + +static struct attribute *haps_attributes[] = { + &dev_attr_protection_level.attr, + &dev_attr_reset_protection.attr, + NULL, +}; + +static const struct attribute_group haps_attr_group = { + .attrs = haps_attributes, +}; + +/* + * ACPI stuff + */ +static void toshiba_haps_notify(acpi_handle handle, u32 event, void *data) +{ + struct acpi_device *device = data; + + pr_debug("Received event: 0x%x\n", event); + + acpi_bus_generate_netlink_event(device->pnp.device_class, + dev_name(&device->dev), + event, 0); +} + +static void toshiba_haps_remove(struct platform_device *pdev) +{ + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, + toshiba_haps_notify); + + sysfs_remove_group(&device->dev.kobj, &haps_attr_group); + + if (toshiba_haps) + toshiba_haps = NULL; + + dev_set_drvdata(&device->dev, NULL); +} + +/* Helper function */ +static int toshiba_haps_available(acpi_handle handle) +{ + acpi_status status; + u64 hdd_present; + + /* + * A non existent device as well as having (only) + * Solid State Drives can cause the call to fail. + */ + status = acpi_evaluate_integer(handle, "_STA", NULL, &hdd_present); + if (ACPI_FAILURE(status)) { + pr_err("ACPI call to query HDD protection failed\n"); + return 0; + } + + if (!hdd_present) { + pr_info("HDD protection not available or using SSD\n"); + return 0; + } + + return 1; +} + +static int toshiba_haps_probe(struct platform_device *pdev) +{ + struct toshiba_haps_dev *haps; + struct acpi_device *acpi_dev; + int ret; + + if (toshiba_haps) + return -EBUSY; + + acpi_dev = ACPI_COMPANION(&pdev->dev); + if (!acpi_dev) + return -ENODEV; + + if (!toshiba_haps_available(acpi_dev->handle)) + return -ENODEV; + + pr_info("Toshiba HDD Active Protection Sensor device\n"); + + haps = devm_kzalloc(&pdev->dev, sizeof(*haps), GFP_KERNEL); + if (!haps) + return -ENOMEM; + + haps->acpi_dev = acpi_dev; + haps->protection_level = 2; + + dev_set_drvdata(&acpi_dev->dev, haps); + platform_set_drvdata(pdev, haps); + + /* Set the protection level, currently at level 2 (Medium) */ + ret = toshiba_haps_protection_level(acpi_dev->handle, 2); + if (ret != 0) + return ret; + + ret = sysfs_create_group(&acpi_dev->dev.kobj, &haps_attr_group); + if (ret) + return ret; + + ret = acpi_dev_install_notify_handler(acpi_dev, ACPI_DEVICE_NOTIFY, + toshiba_haps_notify, acpi_dev); + if (ret) + goto err; + + toshiba_haps = haps; + + return 0; + +err: + sysfs_remove_group(&acpi_dev->dev.kobj, &haps_attr_group); + return ret; +} + +#ifdef CONFIG_PM_SLEEP +static int toshiba_haps_suspend(struct device *device) +{ + struct toshiba_haps_dev *haps = dev_get_drvdata(device); + int ret; + + /* Deactivate the protection on suspend */ + ret = toshiba_haps_protection_level(haps->acpi_dev->handle, 0); + + return ret; +} + +static int toshiba_haps_resume(struct device *device) +{ + struct toshiba_haps_dev *haps = dev_get_drvdata(device); + int ret; + + /* Set the stored protection level */ + ret = toshiba_haps_protection_level(haps->acpi_dev->handle, + haps->protection_level); + + /* Reset the protection on resume */ + ret = toshiba_haps_reset_protection(haps->acpi_dev->handle); + if (ret != 0) + return ret; + + return ret; +} +#endif + +static SIMPLE_DEV_PM_OPS(toshiba_haps_pm, + toshiba_haps_suspend, toshiba_haps_resume); + +static const struct acpi_device_id haps_device_ids[] = { + {"TOS620A", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, haps_device_ids); + +static struct platform_driver toshiba_haps_driver = { + .probe = toshiba_haps_probe, + .remove = toshiba_haps_remove, + .driver = { + .name = "Toshiba HAPS", + .acpi_match_table = haps_device_ids, + .pm = &toshiba_haps_pm, + }, +}; + +module_platform_driver(toshiba_haps_driver); diff --git a/drivers/platform/x86/touchscreen_dmi.c b/drivers/platform/x86/touchscreen_dmi.c new file mode 100644 index 000000000..d83c38782 --- /dev/null +++ b/drivers/platform/x86/touchscreen_dmi.c @@ -0,0 +1,2008 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Touchscreen driver DMI based configuration code + * + * Copyright (c) 2017 Red Hat Inc. + * + * Red Hat authors: + * Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct ts_dmi_data { + /* The EFI embedded-fw code expects this to be the first member! */ + struct efi_embedded_fw_desc embedded_fw; + const char *acpi_name; + const struct property_entry *properties; +}; + +/* NOTE: Please keep all entries sorted alphabetically */ + +static const struct property_entry archos_101_cesium_educ_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1850), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-archos-101-cesium-educ.fw"), + { } +}; + +static const struct ts_dmi_data archos_101_cesium_educ_data = { + .acpi_name = "MSSL1680:00", + .properties = archos_101_cesium_educ_props, +}; + +static const struct property_entry bush_bush_windows_tablet_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1850), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-bush-bush-windows-tablet.fw"), + { } +}; + +static const struct ts_dmi_data bush_bush_windows_tablet_data = { + .acpi_name = "MSSL1680:00", + .properties = bush_bush_windows_tablet_props, +}; + +static const struct property_entry chuwi_hi8_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1665), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi8.fw"), + { } +}; + +static const struct ts_dmi_data chuwi_hi8_data = { + .acpi_name = "MSSL0001:00", + .properties = chuwi_hi8_props, +}; + +static const struct property_entry chuwi_hi8_air_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1148), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-chuwi-hi8-air.fw"), + { } +}; + +static const struct ts_dmi_data chuwi_hi8_air_data = { + .acpi_name = "MSSL1680", + .properties = chuwi_hi8_air_props, +}; + +static const struct property_entry chuwi_hi8_pro_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 6), + PROPERTY_ENTRY_U32("touchscreen-min-y", 3), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1148), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-chuwi-hi8-pro.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data chuwi_hi8_pro_data = { + .embedded_fw = { + .name = "silead/gsl3680-chuwi-hi8-pro.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 39864, + .sha256 = { 0xc0, 0x88, 0xc5, 0xef, 0xd1, 0x70, 0x77, 0x59, + 0x4e, 0xe9, 0xc4, 0xd8, 0x2e, 0xcd, 0xbf, 0x95, + 0x32, 0xd9, 0x03, 0x28, 0x0d, 0x48, 0x9f, 0x92, + 0x35, 0x37, 0xf6, 0x8b, 0x2a, 0xe4, 0x73, 0xff }, + }, + .acpi_name = "MSSL1680:00", + .properties = chuwi_hi8_pro_props, +}; + +static const struct property_entry chuwi_hi10_air_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1981), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1271), + PROPERTY_ENTRY_U32("touchscreen-min-x", 99), + PROPERTY_ENTRY_U32("touchscreen-min-y", 9), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_U32("touchscreen-fuzz-x", 5), + PROPERTY_ENTRY_U32("touchscreen-fuzz-y", 4), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi10-air.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data chuwi_hi10_air_data = { + .acpi_name = "MSSL1680:00", + .properties = chuwi_hi10_air_props, +}; + +static const struct property_entry chuwi_hi10_plus_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 12), + PROPERTY_ENTRY_U32("touchscreen-min-y", 10), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1908), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1270), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi10plus.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_BOOL("silead,pen-supported"), + PROPERTY_ENTRY_U32("silead,pen-resolution-x", 8), + PROPERTY_ENTRY_U32("silead,pen-resolution-y", 8), + { } +}; + +static const struct ts_dmi_data chuwi_hi10_plus_data = { + .embedded_fw = { + .name = "silead/gsl1680-chuwi-hi10plus.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 34056, + .sha256 = { 0xfd, 0x0a, 0x08, 0x08, 0x3c, 0xa6, 0x34, 0x4e, + 0x2c, 0x49, 0x9c, 0xcd, 0x7d, 0x44, 0x9d, 0x38, + 0x10, 0x68, 0xb5, 0xbd, 0xb7, 0x2a, 0x63, 0xb5, + 0x67, 0x0b, 0x96, 0xbd, 0x89, 0x67, 0x85, 0x09 }, + }, + .acpi_name = "MSSL0017:00", + .properties = chuwi_hi10_plus_props, +}; + +static const u32 chuwi_hi10_pro_efi_min_max[] = { 8, 1911, 8, 1271 }; + +static const struct property_entry chuwi_hi10_pro_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 80), + PROPERTY_ENTRY_U32("touchscreen-min-y", 26), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1962), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1254), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi10-pro.fw"), + PROPERTY_ENTRY_U32_ARRAY("silead,efi-fw-min-max", chuwi_hi10_pro_efi_min_max), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_BOOL("silead,pen-supported"), + PROPERTY_ENTRY_U32("silead,pen-resolution-x", 8), + PROPERTY_ENTRY_U32("silead,pen-resolution-y", 8), + { } +}; + +static const struct ts_dmi_data chuwi_hi10_pro_data = { + .embedded_fw = { + .name = "silead/gsl1680-chuwi-hi10-pro.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 42504, + .sha256 = { 0xdb, 0x92, 0x68, 0xa8, 0xdb, 0x81, 0x31, 0x00, + 0x1f, 0x58, 0x89, 0xdb, 0x19, 0x1b, 0x15, 0x8c, + 0x05, 0x14, 0xf4, 0x95, 0xba, 0x15, 0x45, 0x98, + 0x42, 0xa3, 0xbb, 0x65, 0xe3, 0x30, 0xa5, 0x93 }, + }, + .acpi_name = "MSSL1680:00", + .properties = chuwi_hi10_pro_props, +}; + +static const struct property_entry chuwi_hibook_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 30), + PROPERTY_ENTRY_U32("touchscreen-min-y", 4), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1892), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1276), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hibook.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data chuwi_hibook_data = { + .embedded_fw = { + .name = "silead/gsl1680-chuwi-hibook.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 40392, + .sha256 = { 0xf7, 0xc0, 0xe8, 0x5a, 0x6c, 0xf2, 0xeb, 0x8d, + 0x12, 0xc4, 0x45, 0xbf, 0x55, 0x13, 0x4c, 0x1a, + 0x13, 0x04, 0x31, 0x08, 0x65, 0x73, 0xf7, 0xa8, + 0x1b, 0x7d, 0x59, 0xc9, 0xe6, 0x97, 0xf7, 0x38 }, + }, + .acpi_name = "MSSL0017:00", + .properties = chuwi_hibook_props, +}; + +static const struct property_entry chuwi_vi8_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 4), + PROPERTY_ENTRY_U32("touchscreen-min-y", 6), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1724), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-chuwi-vi8.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data chuwi_vi8_data = { + .acpi_name = "MSSL1680:00", + .properties = chuwi_vi8_props, +}; + +static const struct ts_dmi_data chuwi_vi8_plus_data = { + .embedded_fw = { + .name = "chipone/icn8505-HAMP0002.fw", + .prefix = { 0xb0, 0x07, 0x00, 0x00, 0xe4, 0x07, 0x00, 0x00 }, + .length = 35012, + .sha256 = { 0x93, 0xe5, 0x49, 0xe0, 0xb6, 0xa2, 0xb4, 0xb3, + 0x88, 0x96, 0x34, 0x97, 0x5e, 0xa8, 0x13, 0x78, + 0x72, 0x98, 0xb8, 0x29, 0xeb, 0x5c, 0xa7, 0xf1, + 0x25, 0x13, 0x43, 0xf4, 0x30, 0x7c, 0xfc, 0x7c }, + }, +}; + +static const struct property_entry chuwi_vi10_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 0), + PROPERTY_ENTRY_U32("touchscreen-min-y", 4), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1858), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-chuwi-vi10.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data chuwi_vi10_data = { + .acpi_name = "MSSL0002:00", + .properties = chuwi_vi10_props, +}; + +static const struct property_entry chuwi_surbook_mini_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 88), + PROPERTY_ENTRY_U32("touchscreen-min-y", 13), + PROPERTY_ENTRY_U32("touchscreen-size-x", 2040), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1524), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-surbook-mini.fw"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + { } +}; + +static const struct ts_dmi_data chuwi_surbook_mini_data = { + .acpi_name = "MSSL1680:00", + .properties = chuwi_surbook_mini_props, +}; + +static const struct property_entry connect_tablet9_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 9), + PROPERTY_ENTRY_U32("touchscreen-min-y", 10), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1664), + PROPERTY_ENTRY_U32("touchscreen-size-y", 880), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-connect-tablet9.fw"), + { } +}; + +static const struct ts_dmi_data connect_tablet9_data = { + .acpi_name = "MSSL1680:00", + .properties = connect_tablet9_props, +}; + +static const struct property_entry csl_panther_tab_hd_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 20), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1980), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1526), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-csl-panther-tab-hd.fw"), + { } +}; + +static const struct ts_dmi_data csl_panther_tab_hd_data = { + .acpi_name = "MSSL1680:00", + .properties = csl_panther_tab_hd_props, +}; + +static const struct property_entry cube_iwork8_air_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 3), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1664), + PROPERTY_ENTRY_U32("touchscreen-size-y", 896), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3670-cube-iwork8-air.fw"), + { } +}; + +static const struct ts_dmi_data cube_iwork8_air_data = { + .embedded_fw = { + .name = "silead/gsl3670-cube-iwork8-air.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 38808, + .sha256 = { 0xff, 0x62, 0x2d, 0xd1, 0x8a, 0x78, 0x04, 0x7b, + 0x33, 0x06, 0xb0, 0x4f, 0x7f, 0x02, 0x08, 0x9c, + 0x96, 0xd4, 0x9f, 0x04, 0xe1, 0x47, 0x25, 0x25, + 0x60, 0x77, 0x41, 0x33, 0xeb, 0x12, 0x82, 0xfc }, + }, + .acpi_name = "MSSL1680:00", + .properties = cube_iwork8_air_props, +}; + +static const struct property_entry cube_knote_i1101_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 20), + PROPERTY_ENTRY_U32("touchscreen-min-y", 22), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1961), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1513), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-cube-knote-i1101.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data cube_knote_i1101_data = { + .acpi_name = "MSSL1680:00", + .properties = cube_knote_i1101_props, +}; + +static const struct property_entry dexp_ursus_7w_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 890), + PROPERTY_ENTRY_U32("touchscreen-size-y", 630), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1686-dexp-ursus-7w.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data dexp_ursus_7w_data = { + .acpi_name = "MSSL1680:00", + .properties = dexp_ursus_7w_props, +}; + +static const struct property_entry dexp_ursus_kx210i_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 5), + PROPERTY_ENTRY_U32("touchscreen-min-y", 2), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1720), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1137), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-dexp-ursus-kx210i.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data dexp_ursus_kx210i_data = { + .acpi_name = "MSSL1680:00", + .properties = dexp_ursus_kx210i_props, +}; + +static const struct property_entry digma_citi_e200_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1980), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1500), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1686-digma_citi_e200.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data digma_citi_e200_data = { + .acpi_name = "MSSL1680:00", + .properties = digma_citi_e200_props, +}; + +static const struct property_entry estar_beauty_hd_props[] = { + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + { } +}; + +static const struct ts_dmi_data estar_beauty_hd_data = { + .acpi_name = "GDIX1001:00", + .properties = estar_beauty_hd_props, +}; + +/* Generic props + data for upside-down mounted GDIX1001 touchscreens */ +static const struct property_entry gdix1001_upside_down_props[] = { + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + { } +}; + +static const struct ts_dmi_data gdix1001_upside_down_data = { + .acpi_name = "GDIX1001", + .properties = gdix1001_upside_down_props, +}; + +static const struct ts_dmi_data gdix1002_upside_down_data = { + .acpi_name = "GDIX1002", + .properties = gdix1001_upside_down_props, +}; + +static const struct property_entry gdix1001_y_inverted_props[] = { + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + { } +}; + +static const struct ts_dmi_data gdix1001_y_inverted_data = { + .acpi_name = "GDIX1001", + .properties = gdix1001_y_inverted_props, +}; + +static const struct property_entry gp_electronic_t701_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 960), + PROPERTY_ENTRY_U32("touchscreen-size-y", 640), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-gp-electronic-t701.fw"), + { } +}; + +static const struct ts_dmi_data gp_electronic_t701_data = { + .acpi_name = "MSSL1680:00", + .properties = gp_electronic_t701_props, +}; + +static const struct property_entry irbis_tw90_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1720), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1138), + PROPERTY_ENTRY_U32("touchscreen-min-x", 8), + PROPERTY_ENTRY_U32("touchscreen-min-y", 14), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-irbis_tw90.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data irbis_tw90_data = { + .acpi_name = "MSSL1680:00", + .properties = irbis_tw90_props, +}; + +static const struct property_entry irbis_tw118_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 20), + PROPERTY_ENTRY_U32("touchscreen-min-y", 30), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1960), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1510), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-irbis-tw118.fw"), + { } +}; + +static const struct ts_dmi_data irbis_tw118_data = { + .acpi_name = "MSSL1680:00", + .properties = irbis_tw118_props, +}; + +static const struct property_entry itworks_tw891_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 5), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1600), + PROPERTY_ENTRY_U32("touchscreen-size-y", 896), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3670-itworks-tw891.fw"), + { } +}; + +static const struct ts_dmi_data itworks_tw891_data = { + .acpi_name = "MSSL1680:00", + .properties = itworks_tw891_props, +}; + +static const struct property_entry jumper_ezpad_6_pro_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1980), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1500), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-jumper-ezpad-6-pro.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data jumper_ezpad_6_pro_data = { + .acpi_name = "MSSL1680:00", + .properties = jumper_ezpad_6_pro_props, +}; + +static const struct property_entry jumper_ezpad_6_pro_b_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1980), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1500), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-jumper-ezpad-6-pro-b.fw"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data jumper_ezpad_6_pro_b_data = { + .acpi_name = "MSSL1680:00", + .properties = jumper_ezpad_6_pro_b_props, +}; + +static const struct property_entry jumper_ezpad_6_m4_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 35), + PROPERTY_ENTRY_U32("touchscreen-min-y", 15), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1950), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1525), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-jumper-ezpad-6-m4.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data jumper_ezpad_6_m4_data = { + .acpi_name = "MSSL1680:00", + .properties = jumper_ezpad_6_m4_props, +}; + +static const struct property_entry jumper_ezpad_7_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 4), + PROPERTY_ENTRY_U32("touchscreen-min-y", 10), + PROPERTY_ENTRY_U32("touchscreen-size-x", 2044), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1526), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-jumper-ezpad-7.fw"), + PROPERTY_ENTRY_BOOL("silead,stuck-controller-bug"), + { } +}; + +static const struct ts_dmi_data jumper_ezpad_7_data = { + .acpi_name = "MSSL1680:00", + .properties = jumper_ezpad_7_props, +}; + +static const struct property_entry jumper_ezpad_mini3_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 23), + PROPERTY_ENTRY_U32("touchscreen-min-y", 16), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1700), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1138), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-jumper-ezpad-mini3.fw"), + { } +}; + +static const struct ts_dmi_data jumper_ezpad_mini3_data = { + .acpi_name = "MSSL1680:00", + .properties = jumper_ezpad_mini3_props, +}; + +static const struct property_entry mpman_converter9_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 8), + PROPERTY_ENTRY_U32("touchscreen-min-y", 8), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1664), + PROPERTY_ENTRY_U32("touchscreen-size-y", 880), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-mpman-converter9.fw"), + { } +}; + +static const struct ts_dmi_data mpman_converter9_data = { + .acpi_name = "MSSL1680:00", + .properties = mpman_converter9_props, +}; + +static const struct property_entry mpman_mpwin895cl_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 3), + PROPERTY_ENTRY_U32("touchscreen-min-y", 9), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1150), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-mpman-mpwin895cl.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data mpman_mpwin895cl_data = { + .acpi_name = "MSSL1680:00", + .properties = mpman_mpwin895cl_props, +}; + +static const struct property_entry myria_my8307_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1720), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-myria-my8307.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data myria_my8307_data = { + .acpi_name = "MSSL1680:00", + .properties = myria_my8307_props, +}; + +static const struct property_entry onda_obook_20_plus_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1148), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-onda-obook-20-plus.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_obook_20_plus_data = { + .acpi_name = "MSSL1680:00", + .properties = onda_obook_20_plus_props, +}; + +static const struct property_entry onda_v80_plus_v3_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 22), + PROPERTY_ENTRY_U32("touchscreen-min-y", 15), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1698), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-onda-v80-plus-v3.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_v80_plus_v3_data = { + .embedded_fw = { + .name = "silead/gsl3676-onda-v80-plus-v3.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 37224, + .sha256 = { 0x8f, 0xbd, 0x8f, 0x0c, 0x6b, 0xba, 0x5b, 0xf5, + 0xa3, 0xc7, 0xa3, 0xc0, 0x4f, 0xcd, 0xdf, 0x32, + 0xcc, 0xe4, 0x70, 0xd6, 0x46, 0x9c, 0xd7, 0xa7, + 0x4b, 0x82, 0x3f, 0xab, 0xc7, 0x90, 0xea, 0x23 }, + }, + .acpi_name = "MSSL1680:00", + .properties = onda_v80_plus_v3_props, +}; + +static const struct property_entry onda_v820w_32g_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1665), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-onda-v820w-32g.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_v820w_32g_data = { + .acpi_name = "MSSL1680:00", + .properties = onda_v820w_32g_props, +}; + +static const struct property_entry onda_v891_v5_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1715), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", + "gsl3676-onda-v891-v5.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_v891_v5_data = { + .acpi_name = "MSSL1680:00", + .properties = onda_v891_v5_props, +}; + +static const struct property_entry onda_v891w_v1_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 46), + PROPERTY_ENTRY_U32("touchscreen-min-y", 8), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1676), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1130), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-onda-v891w-v1.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_v891w_v1_data = { + .acpi_name = "MSSL1680:00", + .properties = onda_v891w_v1_props, +}; + +static const struct property_entry onda_v891w_v3_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 35), + PROPERTY_ENTRY_U32("touchscreen-min-y", 15), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1625), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1135), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3676-onda-v891w-v3.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data onda_v891w_v3_data = { + .acpi_name = "MSSL1680:00", + .properties = onda_v891w_v3_props, +}; + +static const struct property_entry pipo_w2s_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1660), + PROPERTY_ENTRY_U32("touchscreen-size-y", 880), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-pipo-w2s.fw"), + { } +}; + +static const struct ts_dmi_data pipo_w2s_data = { + .embedded_fw = { + .name = "silead/gsl1680-pipo-w2s.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 39072, + .sha256 = { 0xd0, 0x58, 0xc4, 0x7d, 0x55, 0x2d, 0x62, 0x18, + 0xd1, 0x6a, 0x71, 0x73, 0x0b, 0x3f, 0xbe, 0x60, + 0xbb, 0x45, 0x8c, 0x52, 0x27, 0xb7, 0x18, 0xf4, + 0x31, 0x00, 0x6a, 0x49, 0x76, 0xd8, 0x7c, 0xd3 }, + }, + .acpi_name = "MSSL1680:00", + .properties = pipo_w2s_props, +}; + +static const struct property_entry pipo_w11_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 15), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1984), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1532), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-pipo-w11.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data pipo_w11_data = { + .acpi_name = "MSSL1680:00", + .properties = pipo_w11_props, +}; + +static const struct property_entry positivo_c4128b_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 4), + PROPERTY_ENTRY_U32("touchscreen-min-y", 13), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1915), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1269), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-positivo-c4128b.fw"), + { } +}; + +static const struct ts_dmi_data positivo_c4128b_data = { + .acpi_name = "MSSL1680:00", + .properties = positivo_c4128b_props, +}; + +static const struct property_entry pov_mobii_wintab_p800w_v20_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 32), + PROPERTY_ENTRY_U32("touchscreen-min-y", 16), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1692), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1146), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-pov-mobii-wintab-p800w-v20.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data pov_mobii_wintab_p800w_v20_data = { + .acpi_name = "MSSL1680:00", + .properties = pov_mobii_wintab_p800w_v20_props, +}; + +static const struct property_entry pov_mobii_wintab_p800w_v21_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 8), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1794), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1148), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-pov-mobii-wintab-p800w.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data pov_mobii_wintab_p800w_v21_data = { + .acpi_name = "MSSL1680:00", + .properties = pov_mobii_wintab_p800w_v21_props, +}; + +static const struct property_entry pov_mobii_wintab_p1006w_v10_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 1), + PROPERTY_ENTRY_U32("touchscreen-min-y", 3), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1984), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1520), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-pov-mobii-wintab-p1006w-v10.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data pov_mobii_wintab_p1006w_v10_data = { + .acpi_name = "MSSL1680:00", + .properties = pov_mobii_wintab_p1006w_v10_props, +}; + +static const struct property_entry predia_basic_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 3), + PROPERTY_ENTRY_U32("touchscreen-min-y", 10), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1144), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-predia-basic.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data predia_basic_data = { + .acpi_name = "MSSL1680:00", + .properties = predia_basic_props, +}; + +static const struct property_entry rca_cambio_w101_v2_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 4), + PROPERTY_ENTRY_U32("touchscreen-min-y", 20), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1644), + PROPERTY_ENTRY_U32("touchscreen-size-y", 874), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-rca-cambio-w101-v2.fw"), + { } +}; + +static const struct ts_dmi_data rca_cambio_w101_v2_data = { + .acpi_name = "MSSL1680:00", + .properties = rca_cambio_w101_v2_props, +}; + +static const struct property_entry rwc_nanote_p8_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-y", 46), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1728), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-rwc-nanote-p8.fw"), + { } +}; + +static const struct ts_dmi_data rwc_nanote_p8_data = { + .acpi_name = "MSSL1680:00", + .properties = rwc_nanote_p8_props, +}; + +static const struct property_entry rwc_nanote_next_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 5), + PROPERTY_ENTRY_U32("touchscreen-min-y", 5), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1785), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1145), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-rwc-nanote-next.fw"), + { } +}; + +static const struct ts_dmi_data rwc_nanote_next_data = { + .acpi_name = "MSSL1680:00", + .properties = rwc_nanote_next_props, +}; + +static const struct property_entry sary_tab_3_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1730), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1151), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-sary-tab-3.fw"), + PROPERTY_ENTRY_U32("silead,max-fingers", 10), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data sary_tab_3_data = { + .acpi_name = "MSSL1680:00", + .properties = sary_tab_3_props, +}; + +static const struct property_entry schneider_sct101ctm_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1715), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-schneider-sct101ctm.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data schneider_sct101ctm_data = { + .acpi_name = "MSSL1680:00", + .properties = schneider_sct101ctm_props, +}; + +static const struct property_entry globalspace_solt_ivw116_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 7), + PROPERTY_ENTRY_U32("touchscreen-min-y", 22), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1723), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1077), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-globalspace-solt-ivw116.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data globalspace_solt_ivw116_data = { + .acpi_name = "MSSL1680:00", + .properties = globalspace_solt_ivw116_props, +}; + +static const struct property_entry techbite_arc_11_6_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 5), + PROPERTY_ENTRY_U32("touchscreen-min-y", 7), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1981), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1270), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-techbite-arc-11-6.fw"), + { } +}; + +static const struct ts_dmi_data techbite_arc_11_6_data = { + .acpi_name = "MSSL1680:00", + .properties = techbite_arc_11_6_props, +}; + +static const struct property_entry teclast_tbook11_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 8), + PROPERTY_ENTRY_U32("touchscreen-min-y", 14), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1916), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1264), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-teclast-tbook11.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data teclast_tbook11_data = { + .embedded_fw = { + .name = "silead/gsl3692-teclast-tbook11.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 43560, + .sha256 = { 0x9d, 0xb0, 0x3d, 0xf1, 0x00, 0x3c, 0xb5, 0x25, + 0x62, 0x8a, 0xa0, 0x93, 0x4b, 0xe0, 0x4e, 0x75, + 0xd1, 0x27, 0xb1, 0x65, 0x3c, 0xba, 0xa5, 0x0f, + 0xcd, 0xb4, 0xbe, 0x00, 0xbb, 0xf6, 0x43, 0x29 }, + }, + .acpi_name = "MSSL1680:00", + .properties = teclast_tbook11_props, +}; + +static const struct property_entry teclast_x16_plus_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 8), + PROPERTY_ENTRY_U32("touchscreen-min-y", 14), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1916), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1264), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3692-teclast-x16-plus.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data teclast_x16_plus_data = { + .embedded_fw = { + .name = "silead/gsl3692-teclast-x16-plus.fw", + .prefix = { 0xf0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }, + .length = 43560, + .sha256 = { 0x9d, 0xb0, 0x3d, 0xf1, 0x00, 0x3c, 0xb5, 0x25, + 0x62, 0x8a, 0xa0, 0x93, 0x4b, 0xe0, 0x4e, 0x75, + 0xd1, 0x27, 0xb1, 0x65, 0x3c, 0xba, 0xa5, 0x0f, + 0xcd, 0xb4, 0xbe, 0x00, 0xbb, 0xf6, 0x43, 0x29 }, + }, + .acpi_name = "MSSL1680:00", + .properties = teclast_x16_plus_props, +}; + +static const struct property_entry teclast_x3_plus_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1980), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1500), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-teclast-x3-plus.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data teclast_x3_plus_data = { + .acpi_name = "MSSL1680:00", + .properties = teclast_x3_plus_props, +}; + +static const struct property_entry teclast_x98plus2_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 2048), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1686-teclast_x98plus2.fw"), + { } +}; + +static const struct ts_dmi_data teclast_x98plus2_data = { + .acpi_name = "MSSL1680:00", + .properties = teclast_x98plus2_props, +}; + +static const struct property_entry trekstor_primebook_c11_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1970), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1530), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-trekstor-primebook-c11.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data trekstor_primebook_c11_data = { + .acpi_name = "MSSL1680:00", + .properties = trekstor_primebook_c11_props, +}; + +static const struct property_entry trekstor_primebook_c13_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 2624), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1920), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-trekstor-primebook-c13.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data trekstor_primebook_c13_data = { + .acpi_name = "MSSL1680:00", + .properties = trekstor_primebook_c13_props, +}; + +static const struct property_entry trekstor_primetab_t13b_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 2500), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1900), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-trekstor-primetab-t13b.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"), + { } +}; + +static const struct ts_dmi_data trekstor_primetab_t13b_data = { + .acpi_name = "MSSL1680:00", + .properties = trekstor_primetab_t13b_props, +}; + +static const struct property_entry trekstor_surftab_duo_w1_props[] = { + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + { } +}; + +static const struct ts_dmi_data trekstor_surftab_duo_w1_data = { + .acpi_name = "GDIX1001:00", + .properties = trekstor_surftab_duo_w1_props, +}; + +static const struct property_entry trekstor_surftab_twin_10_1_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 20), + PROPERTY_ENTRY_U32("touchscreen-min-y", 0), + PROPERTY_ENTRY_U32("touchscreen-size-x", 1890), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_U32("touchscreen-inverted-y", 1), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3670-surftab-twin-10-1-st10432-8.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data trekstor_surftab_twin_10_1_data = { + .acpi_name = "MSSL1680:00", + .properties = trekstor_surftab_twin_10_1_props, +}; + +static const struct property_entry trekstor_surftab_wintron70_props[] = { + PROPERTY_ENTRY_U32("touchscreen-min-x", 12), + PROPERTY_ENTRY_U32("touchscreen-min-y", 8), + PROPERTY_ENTRY_U32("touchscreen-size-x", 884), + PROPERTY_ENTRY_U32("touchscreen-size-y", 632), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1686-surftab-wintron70-st70416-6.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data trekstor_surftab_wintron70_data = { + .acpi_name = "MSSL1680:00", + .properties = trekstor_surftab_wintron70_props, +}; + +static const struct property_entry viglen_connect_10_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1890), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_U32("touchscreen-fuzz-x", 6), + PROPERTY_ENTRY_U32("touchscreen-fuzz-y", 6), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl3680-viglen-connect-10.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data viglen_connect_10_data = { + .acpi_name = "MSSL1680:00", + .properties = viglen_connect_10_props, +}; + +static const struct property_entry vinga_twizzle_j116_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1920), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-vinga-twizzle_j116.fw"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + { } +}; + +static const struct ts_dmi_data vinga_twizzle_j116_data = { + .acpi_name = "MSSL1680:00", + .properties = vinga_twizzle_j116_props, +}; + +/* NOTE: Please keep this table sorted alphabetically */ +const struct dmi_system_id touchscreen_dmi_table[] = { + { + /* Archos 101 Cesium Educ */ + .driver_data = (void *)&archos_101_cesium_educ_data, + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "ARCHOS 101 Cesium Educ"), + }, + }, + { + /* Bush Windows tablet */ + .driver_data = (void *)&bush_bush_windows_tablet_data, + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Bush Windows tablet"), + }, + }, + { + /* Chuwi Hi8 */ + .driver_data = (void *)&chuwi_hi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ilife"), + DMI_MATCH(DMI_PRODUCT_NAME, "S806"), + }, + }, + { + /* Chuwi Hi8 (H1D_S806_206) */ + .driver_data = (void *)&chuwi_hi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "BayTrail"), + DMI_MATCH(DMI_BIOS_VERSION, "H1D_S806_206"), + }, + }, + { + /* Chuwi Hi8 Air (CWI543) */ + .driver_data = (void *)&chuwi_hi8_air_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Default string"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + DMI_MATCH(DMI_PRODUCT_NAME, "Hi8 Air"), + }, + }, + { + /* Chuwi Hi8 Pro (CWI513) */ + .driver_data = (void *)&chuwi_hi8_pro_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Hampoo"), + DMI_MATCH(DMI_PRODUCT_NAME, "X1D3_C806N"), + }, + }, + { + /* Chuwi Hi10 Air */ + .driver_data = (void *)&chuwi_hi10_air_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "CHUWI INNOVATION AND TECHNOLOGY(SHENZHEN)CO.LTD"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + DMI_MATCH(DMI_PRODUCT_SKU, "P1W6_C109D_B"), + }, + }, + { + /* Chuwi Hi10 Plus (CWI527) */ + .driver_data = (void *)&chuwi_hi10_plus_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_PRODUCT_NAME, "Hi10 plus tablet"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + }, + }, + { + /* Chuwi Hi10 Pro (CWI529) */ + .driver_data = (void *)&chuwi_hi10_pro_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_PRODUCT_NAME, "Hi10 pro tablet"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + }, + }, + { + /* Chuwi HiBook (CWI514) */ + .driver_data = (void *)&chuwi_hibook_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + /* Above matches are too generic, add bios-date match */ + DMI_MATCH(DMI_BIOS_DATE, "05/07/2016"), + }, + }, + { + /* Chuwi Vi8 (CWI501) */ + .driver_data = (void *)&chuwi_vi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "i86"), + DMI_MATCH(DMI_BIOS_VERSION, "CHUWI.W86JLBNR01"), + }, + }, + { + /* Chuwi Vi8 (CWI506) */ + .driver_data = (void *)&chuwi_vi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "i86"), + DMI_MATCH(DMI_BIOS_VERSION, "CHUWI.D86JLBNR"), + }, + }, + { + /* Chuwi Vi8 dual-boot (CWI506) */ + .driver_data = (void *)&chuwi_vi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "i86"), + DMI_MATCH(DMI_BIOS_VERSION, "CHUWI2.D86JHBNR02"), + }, + }, + { + /* Chuwi Vi8 Plus (CWI519) */ + .driver_data = (void *)&chuwi_vi8_plus_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Hampoo"), + DMI_MATCH(DMI_PRODUCT_NAME, "D2D3_Vi8A1"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + }, + }, + { + /* Chuwi Vi10 (CWI505) */ + .driver_data = (void *)&chuwi_vi10_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_BOARD_NAME, "BYT-PF02"), + DMI_MATCH(DMI_SYS_VENDOR, "ilife"), + DMI_MATCH(DMI_PRODUCT_NAME, "S165"), + }, + }, + { + /* Chuwi Surbook Mini (CWI540) */ + .driver_data = (void *)&chuwi_surbook_mini_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_PRODUCT_NAME, "C3W6_AP108_4G"), + }, + }, + { + /* Connect Tablet 9 */ + .driver_data = (void *)&connect_tablet9_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Connect"), + DMI_MATCH(DMI_PRODUCT_NAME, "Tablet 9"), + }, + }, + { + /* CSL Panther Tab HD */ + .driver_data = (void *)&csl_panther_tab_hd_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "CSL Computer GmbH & Co. KG"), + DMI_MATCH(DMI_PRODUCT_NAME, "CSL Panther Tab HD"), + }, + }, + { + /* CUBE iwork8 Air */ + .driver_data = (void *)&cube_iwork8_air_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "cube"), + DMI_MATCH(DMI_PRODUCT_NAME, "i1-TF"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + }, + }, + { + /* Cube KNote i1101 */ + .driver_data = (void *)&cube_knote_i1101_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_BOARD_NAME, "L1W6_I1101"), + DMI_MATCH(DMI_SYS_VENDOR, "ALLDOCUBE"), + DMI_MATCH(DMI_PRODUCT_NAME, "i1101"), + }, + }, + { + /* DEXP Ursus 7W */ + .driver_data = (void *)&dexp_ursus_7w_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "7W"), + }, + }, + { + /* DEXP Ursus KX210i */ + .driver_data = (void *)&dexp_ursus_kx210i_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "INSYDE Corp."), + DMI_MATCH(DMI_PRODUCT_NAME, "S107I"), + }, + }, + { + /* Digma Citi E200 */ + .driver_data = (void *)&digma_citi_e200_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Digma"), + DMI_MATCH(DMI_PRODUCT_NAME, "CITI E200"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + }, + }, + { + /* Estar Beauty HD (MID 7316R) */ + .driver_data = (void *)&estar_beauty_hd_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Estar"), + DMI_MATCH(DMI_PRODUCT_NAME, "eSTAR BEAUTY HD Intel Quad core"), + }, + }, + { + /* GP-electronic T701 */ + .driver_data = (void *)&gp_electronic_t701_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "T701"), + DMI_MATCH(DMI_BIOS_VERSION, "BYT70A.YNCHENG.WIN.007"), + }, + }, + { + /* I.T.Works TW701 (same hardware as the Trekstor ST70416-6) */ + .driver_data = (void *)&trekstor_surftab_wintron70_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "i71c"), + DMI_MATCH(DMI_BIOS_VERSION, "itWORKS.G.WI71C.JGBMRB"), + }, + }, + { + /* Irbis TW90 */ + .driver_data = (void *)&irbis_tw90_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "IRBIS"), + DMI_MATCH(DMI_PRODUCT_NAME, "TW90"), + }, + }, + { + /* Irbis TW118 */ + .driver_data = (void *)&irbis_tw118_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "IRBIS"), + DMI_MATCH(DMI_PRODUCT_NAME, "TW118"), + }, + }, + { + /* I.T.Works TW891 */ + .driver_data = (void *)&itworks_tw891_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "To be filled by O.E.M."), + DMI_MATCH(DMI_PRODUCT_NAME, "TW891"), + }, + }, + { + /* Jumper EZpad 6 Pro */ + .driver_data = (void *)&jumper_ezpad_6_pro_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Jumper"), + DMI_MATCH(DMI_PRODUCT_NAME, "EZpad"), + DMI_MATCH(DMI_BIOS_VERSION, "5.12"), + /* Above matches are too generic, add bios-date match */ + DMI_MATCH(DMI_BIOS_DATE, "08/18/2017"), + }, + }, + { + /* Jumper EZpad 6 Pro B */ + .driver_data = (void *)&jumper_ezpad_6_pro_b_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Jumper"), + DMI_MATCH(DMI_PRODUCT_NAME, "EZpad"), + DMI_MATCH(DMI_BIOS_VERSION, "5.12"), + /* Above matches are too generic, add bios-date match */ + DMI_MATCH(DMI_BIOS_DATE, "04/24/2018"), + }, + }, + { + /* Jumper EZpad 6s Pro */ + .driver_data = (void *)&jumper_ezpad_6_pro_b_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Jumper"), + DMI_MATCH(DMI_PRODUCT_NAME, "Ezpad"), + /* Above matches are too generic, add bios match */ + DMI_MATCH(DMI_BIOS_VERSION, "E.WSA116_8.E1.042.bin"), + DMI_MATCH(DMI_BIOS_DATE, "01/08/2020"), + }, + }, + { + /* Jumper EZpad 6 m4 */ + .driver_data = (void *)&jumper_ezpad_6_m4_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "jumper"), + DMI_MATCH(DMI_PRODUCT_NAME, "EZpad"), + /* Jumper8.S106x.A00C.1066 with the version dropped */ + DMI_MATCH(DMI_BIOS_VERSION, "Jumper8.S106x"), + }, + }, + { + /* Jumper EZpad 7 */ + .driver_data = (void *)&jumper_ezpad_7_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Jumper"), + DMI_MATCH(DMI_PRODUCT_NAME, "EZpad"), + /* Jumper12x.WJ2012.bsBKRCP05 with the version dropped */ + DMI_MATCH(DMI_BIOS_VERSION, "Jumper12x.WJ2012.bsBKRCP"), + }, + }, + { + /* Jumper EZpad mini3 */ + .driver_data = (void *)&jumper_ezpad_mini3_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + /* jumperx.T87.KFBNEEA02 with the version-nr dropped */ + DMI_MATCH(DMI_BIOS_VERSION, "jumperx.T87.KFBNEEA"), + }, + }, + { + /* Juno Tablet */ + .driver_data = (void *)&gdix1002_upside_down_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Default string"), + /* Both product- and board-name being "Default string" is somewhat rare */ + DMI_MATCH(DMI_PRODUCT_NAME, "Default string"), + DMI_MATCH(DMI_BOARD_NAME, "Default string"), + /* Above matches are too generic, add partial bios-version match */ + DMI_MATCH(DMI_BIOS_VERSION, "JP2V1."), + }, + }, + { + /* Mediacom WinPad 7.0 W700 (same hw as Wintron surftab 7") */ + .driver_data = (void *)&trekstor_surftab_wintron70_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MEDIACOM"), + DMI_MATCH(DMI_PRODUCT_NAME, "WinPad 7 W10 - WPW700"), + }, + }, + { + /* Mediacom Flexbook Edge 11 (same hw as TS Primebook C11) */ + .driver_data = (void *)&trekstor_primebook_c11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MEDIACOM"), + DMI_MATCH(DMI_PRODUCT_NAME, "FlexBook edge11 - M-FBE11"), + }, + }, + { + /* MP Man Converter 9 */ + .driver_data = (void *)&mpman_converter9_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MPMAN"), + DMI_MATCH(DMI_PRODUCT_NAME, "Converter9"), + }, + }, + { + /* MP Man MPWIN895CL */ + .driver_data = (void *)&mpman_mpwin895cl_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MPMAN"), + DMI_MATCH(DMI_PRODUCT_NAME, "MPWIN8900CL"), + }, + }, + { + /* Myria MY8307 */ + .driver_data = (void *)&myria_my8307_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Complet Electro Serv"), + DMI_MATCH(DMI_PRODUCT_NAME, "MY8307"), + }, + }, + { + /* Onda oBook 20 Plus */ + .driver_data = (void *)&onda_obook_20_plus_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONDA"), + DMI_MATCH(DMI_PRODUCT_NAME, "OBOOK 20 PLUS"), + }, + }, + { + /* ONDA V80 plus v3 (P80PSBG9V3A01501) */ + .driver_data = (void *)&onda_v80_plus_v3_data, + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONDA"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "V80 PLUS") + }, + }, + { + /* ONDA V820w DualOS */ + .driver_data = (void *)&onda_v820w_32g_data, + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ONDA"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "V820w DualOS") + }, + }, + { + /* ONDA V891 v5 */ + .driver_data = (void *)&onda_v891_v5_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONDA"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONDA Tablet"), + DMI_MATCH(DMI_BIOS_VERSION, "ONDA.D869CJABNRBA06"), + }, + }, + { + /* ONDA V891w revision P891WBEBV1B00 aka v1 */ + .driver_data = (void *)&onda_v891w_v1_data, + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ONDA"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONDA Tablet"), + DMI_EXACT_MATCH(DMI_BOARD_VERSION, "V001"), + /* Exact match, different versions need different fw */ + DMI_EXACT_MATCH(DMI_BIOS_VERSION, "ONDA.W89EBBN08"), + }, + }, + { + /* ONDA V891w Dual OS P891DCF2V1A01274 64GB */ + .driver_data = (void *)&onda_v891w_v3_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONDA Tablet"), + DMI_MATCH(DMI_BIOS_VERSION, "ONDA.D890HBBNR0A"), + }, + }, + { + /* Pipo W2S */ + .driver_data = (void *)&pipo_w2s_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PIPO"), + DMI_MATCH(DMI_PRODUCT_NAME, "W2S"), + }, + }, + { + /* Pipo W11 */ + .driver_data = (void *)&pipo_w11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PIPO"), + DMI_MATCH(DMI_PRODUCT_NAME, "To be filled by O.E.M."), + /* Above matches are too generic, add bios-ver match */ + DMI_MATCH(DMI_BIOS_VERSION, "JS-BI-10.6-SF133GR300-GA55B-024-F"), + }, + }, + { + /* Ployer Momo7w (same hardware as the Trekstor ST70416-6) */ + .driver_data = (void *)&trekstor_surftab_wintron70_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Shenzhen PLOYER"), + DMI_MATCH(DMI_PRODUCT_NAME, "MOMO7W"), + /* Exact match, different versions need different fw */ + DMI_MATCH(DMI_BIOS_VERSION, "MOMO.G.WI71C.MABMRBA02"), + }, + }, + { + /* Positivo C4128B */ + .driver_data = (void *)&positivo_c4128b_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Positivo Tecnologia SA"), + DMI_MATCH(DMI_PRODUCT_NAME, "C4128B-1"), + }, + }, + { + /* Point of View mobii wintab p800w (v2.0) */ + .driver_data = (void *)&pov_mobii_wintab_p800w_v20_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_MATCH(DMI_BOARD_NAME, "Aptio CRB"), + DMI_MATCH(DMI_BIOS_VERSION, "3BAIR1014"), + /* Above matches are too generic, add bios-date match */ + DMI_MATCH(DMI_BIOS_DATE, "10/24/2014"), + }, + }, + { + /* Predia Basic tablet) */ + .driver_data = (void *)&predia_basic_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "CherryTrail"), + /* Above matches are too generic, add bios-version match */ + DMI_MATCH(DMI_BIOS_VERSION, "Mx.WT107.KUBNGEA"), + }, + }, + { + /* Point of View mobii wintab p800w (v2.1) */ + .driver_data = (void *)&pov_mobii_wintab_p800w_v21_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_MATCH(DMI_BOARD_NAME, "Aptio CRB"), + DMI_MATCH(DMI_BIOS_VERSION, "3BAIR1013"), + /* Above matches are too generic, add bios-date match */ + DMI_MATCH(DMI_BIOS_DATE, "08/22/2014"), + }, + }, + { + /* Point of View mobii wintab p1006w (v1.0) */ + .driver_data = (void *)&pov_mobii_wintab_p1006w_v10_data, + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "BayTrail"), + /* Note 105b is Foxcon's USB/PCI vendor id */ + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "105B"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "0E57"), + }, + }, + { + /* RCA Cambio W101 v2 */ + /* https://github.com/onitake/gsl-firmware/discussions/193 */ + .driver_data = (void *)&rca_cambio_w101_v2_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "RCA"), + DMI_MATCH(DMI_PRODUCT_NAME, "W101SA23T1"), + }, + }, + { + /* RWC NANOTE P8 */ + .driver_data = (void *)&rwc_nanote_p8_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Default string"), + DMI_MATCH(DMI_PRODUCT_NAME, "AY07J"), + DMI_MATCH(DMI_PRODUCT_SKU, "0001") + }, + }, + { + /* RWC NANOTE NEXT */ + .driver_data = (void *)&rwc_nanote_next_data, + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "To be filled by O.E.M."), + DMI_MATCH(DMI_BOARD_NAME, "To be filled by O.E.M."), + DMI_MATCH(DMI_BOARD_VENDOR, "To be filled by O.E.M."), + /* Above matches are too generic, add bios-version match */ + DMI_MATCH(DMI_BIOS_VERSION, "S8A70R100-V005"), + }, + }, + { + /* SARY Tab 3 */ + .driver_data = (void *)&sary_tab_3_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SARY"), + DMI_MATCH(DMI_PRODUCT_NAME, "C210C"), + DMI_MATCH(DMI_PRODUCT_SKU, "TAB3"), + }, + }, + { + /* Schneider SCT101CTM */ + .driver_data = (void *)&schneider_sct101ctm_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Default string"), + DMI_MATCH(DMI_PRODUCT_NAME, "SCT101CTM"), + }, + }, + { + /* GlobalSpace SoLT IVW 11.6" */ + .driver_data = (void *)&globalspace_solt_ivw116_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Globalspace Tech Pvt Ltd"), + DMI_MATCH(DMI_PRODUCT_NAME, "SolTIVW"), + DMI_MATCH(DMI_PRODUCT_SKU, "PN20170413488"), + }, + }, + { + /* SUPI S10 */ + .driver_data = (void *)&gdix1001_y_inverted_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SUPI"), + DMI_MATCH(DMI_PRODUCT_NAME, "S10"), + }, + }, + { + /* Techbite Arc 11.6 */ + .driver_data = (void *)&techbite_arc_11_6_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "mPTech"), + DMI_MATCH(DMI_PRODUCT_NAME, "techBite Arc 11.6"), + DMI_MATCH(DMI_BOARD_NAME, "G8316_272B"), + }, + }, + { + /* Teclast Tbook 11 */ + .driver_data = (void *)&teclast_tbook11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TECLAST"), + DMI_MATCH(DMI_PRODUCT_NAME, "TbooK 11"), + DMI_MATCH(DMI_PRODUCT_SKU, "E5A6_A1"), + }, + }, + { + /* Teclast X16 Plus */ + .driver_data = (void *)&teclast_x16_plus_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TECLAST"), + DMI_MATCH(DMI_PRODUCT_NAME, "Default string"), + DMI_MATCH(DMI_PRODUCT_SKU, "D3A5_A1"), + }, + }, + { + /* Teclast X3 Plus */ + .driver_data = (void *)&teclast_x3_plus_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TECLAST"), + DMI_MATCH(DMI_PRODUCT_NAME, "X3 Plus"), + DMI_MATCH(DMI_BOARD_NAME, "X3 Plus"), + }, + }, + { + /* Teclast X89 (Android version / BIOS) */ + .driver_data = (void *)&gdix1001_upside_down_data, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "WISKY"), + DMI_MATCH(DMI_BOARD_NAME, "3G062i"), + }, + }, + { + /* Teclast X89 (Windows version / BIOS) */ + .driver_data = (void *)&gdix1001_upside_down_data, + .matches = { + /* tPAD is too generic, also match on bios date */ + DMI_MATCH(DMI_BOARD_VENDOR, "TECLAST"), + DMI_MATCH(DMI_BOARD_NAME, "tPAD"), + DMI_MATCH(DMI_BIOS_DATE, "12/19/2014"), + }, + }, + { + /* Teclast X98 Plus II */ + .driver_data = (void *)&teclast_x98plus2_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TECLAST"), + DMI_MATCH(DMI_PRODUCT_NAME, "X98 Plus II"), + }, + }, + { + /* Teclast X98 Pro */ + .driver_data = (void *)&gdix1001_upside_down_data, + .matches = { + /* + * Only match BIOS date, because the manufacturers + * BIOS does not report the board name at all + * (sometimes)... + */ + DMI_MATCH(DMI_BOARD_VENDOR, "TECLAST"), + DMI_MATCH(DMI_BIOS_DATE, "10/28/2015"), + }, + }, + { + /* Trekstor Primebook C11 */ + .driver_data = (void *)&trekstor_primebook_c11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TREKSTOR"), + DMI_MATCH(DMI_PRODUCT_NAME, "Primebook C11"), + }, + }, + { + /* Trekstor Primebook C11B (same touchscreen as the C11) */ + .driver_data = (void *)&trekstor_primebook_c11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TREKSTOR"), + DMI_MATCH(DMI_PRODUCT_NAME, "PRIMEBOOK C11B"), + }, + }, + { + /* Trekstor Primebook C13 */ + .driver_data = (void *)&trekstor_primebook_c13_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TREKSTOR"), + DMI_MATCH(DMI_PRODUCT_NAME, "Primebook C13"), + }, + }, + { + /* Trekstor Primetab T13B */ + .driver_data = (void *)&trekstor_primetab_t13b_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TREKSTOR"), + DMI_MATCH(DMI_PRODUCT_NAME, "Primetab T13B"), + }, + }, + { + /* TrekStor SurfTab duo W1 10.1 ST10432-10b */ + .driver_data = (void *)&trekstor_surftab_duo_w1_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TrekStor"), + DMI_MATCH(DMI_PRODUCT_NAME, "SurfTab duo W1 10.1 (VT4)"), + }, + }, + { + /* TrekStor SurfTab twin 10.1 ST10432-8 */ + .driver_data = (void *)&trekstor_surftab_twin_10_1_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TrekStor"), + DMI_MATCH(DMI_PRODUCT_NAME, "SurfTab twin 10.1"), + }, + }, + { + /* Trekstor Surftab Wintron 7.0 ST70416-6 */ + .driver_data = (void *)&trekstor_surftab_wintron70_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "ST70416-6"), + /* Exact match, different versions need different fw */ + DMI_MATCH(DMI_BIOS_VERSION, "TREK.G.WI71C.JGBMRBA04"), + }, + }, + { + /* Trekstor Surftab Wintron 7.0 ST70416-6, newer BIOS */ + .driver_data = (void *)&trekstor_surftab_wintron70_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TrekStor"), + DMI_MATCH(DMI_PRODUCT_NAME, "SurfTab wintron 7.0 ST70416-6"), + /* Exact match, different versions need different fw */ + DMI_MATCH(DMI_BIOS_VERSION, "TREK.G.WI71C.JGBMRBA05"), + }, + }, + { + /* Trekstor Yourbook C11B (same touchscreen as the Primebook C11) */ + .driver_data = (void *)&trekstor_primebook_c11_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TREKSTOR"), + DMI_MATCH(DMI_PRODUCT_NAME, "YOURBOOK C11B"), + }, + }, + { + /* Viglen Connect 10 */ + .driver_data = (void *)&viglen_connect_10_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Viglen Ltd."), + DMI_MATCH(DMI_PRODUCT_NAME, "Connect 10'' Tablet PC"), + }, + }, + { + /* Vinga Twizzle J116 */ + .driver_data = (void *)&vinga_twizzle_j116_data, + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "VINGA Twizzle J116"), + }, + }, + { + /* "WinBook TW100" */ + .driver_data = (void *)&gdix1001_upside_down_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "WinBook"), + DMI_MATCH(DMI_PRODUCT_NAME, "TW100") + } + }, + { + /* WinBook TW700 */ + .driver_data = (void *)&gdix1001_upside_down_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "WinBook"), + DMI_MATCH(DMI_PRODUCT_NAME, "TW700") + }, + }, + { + /* Yours Y8W81, same case and touchscreen as Chuwi Vi8 */ + .driver_data = (void *)&chuwi_vi8_data, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "YOURS"), + DMI_MATCH(DMI_PRODUCT_NAME, "Y8W81"), + }, + }, + { } +}; + +static struct ts_dmi_data *ts_data; + +static void ts_dmi_add_props(struct i2c_client *client) +{ + struct device *dev = &client->dev; + int error; + + if (has_acpi_companion(dev) && + strstarts(client->name, ts_data->acpi_name)) { + error = device_create_managed_software_node(dev, ts_data->properties, NULL); + if (error) + dev_err(dev, "failed to add properties: %d\n", error); + } +} + +static int ts_dmi_notifier_call(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct device *dev = data; + struct i2c_client *client; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + client = i2c_verify_client(dev); + if (client) + ts_dmi_add_props(client); + break; + + default: + break; + } + + return 0; +} + +#define MAX_CMDLINE_PROPS 16 + +static struct property_entry ts_cmdline_props[MAX_CMDLINE_PROPS + 1]; + +static struct ts_dmi_data ts_cmdline_data = { + .properties = ts_cmdline_props, +}; + +static int __init ts_parse_props(char *str) +{ + /* Save the original str to show it on syntax errors */ + char orig_str[256]; + char *name, *value; + u32 u32val; + int i, ret; + + strscpy(orig_str, str); + + /* + * str is part of the static_command_line from init/main.c and poking + * holes in that by writing 0 to it is allowed, as is taking long + * lasting references to it. + */ + ts_cmdline_data.acpi_name = strsep(&str, ":"); + + for (i = 0; i < MAX_CMDLINE_PROPS; i++) { + name = strsep(&str, ":"); + if (!name || !name[0]) + break; + + /* Replace '=' with 0 and make value point past '=' or NULL */ + value = name; + strsep(&value, "="); + if (!value) { + ts_cmdline_props[i] = PROPERTY_ENTRY_BOOL(name); + } else if (isdigit(value[0])) { + ret = kstrtou32(value, 0, &u32val); + if (ret) + goto syntax_error; + + ts_cmdline_props[i] = PROPERTY_ENTRY_U32(name, u32val); + } else { + ts_cmdline_props[i] = PROPERTY_ENTRY_STRING(name, value); + } + } + + if (!i || str) + goto syntax_error; + + ts_data = &ts_cmdline_data; + return 1; + +syntax_error: + pr_err("Invalid '%s' value for 'i2c_touchscreen_props='\n", orig_str); + return 1; /* "i2c_touchscreen_props=" is still a known parameter */ +} +__setup("i2c_touchscreen_props=", ts_parse_props); + +static struct notifier_block ts_dmi_notifier = { + .notifier_call = ts_dmi_notifier_call, +}; + +static int __init ts_dmi_init(void) +{ + const struct dmi_system_id *dmi_id; + struct ts_dmi_data *ts_data_dmi; + int error; + + dmi_id = dmi_first_match(touchscreen_dmi_table); + ts_data_dmi = dmi_id ? dmi_id->driver_data : NULL; + + if (ts_data) { + /* + * Kernel cmdline provided data takes precedence, copy over + * DMI efi_embedded_fw info if available. + */ + if (ts_data_dmi) + ts_data->embedded_fw = ts_data_dmi->embedded_fw; + } else if (ts_data_dmi) { + ts_data = ts_data_dmi; + } else { + return 0; /* Not an error */ + } + + /* Some dmi table entries only provide an efi_embedded_fw_desc */ + if (!ts_data->properties) + return 0; + + error = bus_register_notifier(&i2c_bus_type, &ts_dmi_notifier); + if (error) + pr_err("%s: failed to register i2c bus notifier: %d\n", + __func__, error); + + return error; +} + +/* + * We are registering out notifier after i2c core is initialized and i2c bus + * itself is ready (which happens at postcore initcall level), but before + * ACPI starts enumerating devices (at subsys initcall level). + */ +arch_initcall(ts_dmi_init); diff --git a/drivers/platform/x86/tuxedo/Kconfig b/drivers/platform/x86/tuxedo/Kconfig new file mode 100644 index 000000000..80be0947d --- /dev/null +++ b/drivers/platform/x86/tuxedo/Kconfig @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2024-2025 Werner Sembach wse@tuxedocomputers.com +# +# TUXEDO X86 Platform Specific Drivers +# + +source "drivers/platform/x86/tuxedo/nb04/Kconfig" diff --git a/drivers/platform/x86/tuxedo/Makefile b/drivers/platform/x86/tuxedo/Makefile new file mode 100644 index 000000000..0afe0d0f4 --- /dev/null +++ b/drivers/platform/x86/tuxedo/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2024-2025 Werner Sembach wse@tuxedocomputers.com +# +# TUXEDO X86 Platform Specific Drivers +# + +obj-y += nb04/ diff --git a/drivers/platform/x86/tuxedo/nb04/Kconfig b/drivers/platform/x86/tuxedo/nb04/Kconfig new file mode 100644 index 000000000..9e7a9f923 --- /dev/null +++ b/drivers/platform/x86/tuxedo/nb04/Kconfig @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2024-2025 Werner Sembach wse@tuxedocomputers.com +# +# TUXEDO X86 Platform Specific Drivers +# + +config TUXEDO_NB04_WMI_AB + tristate "TUXEDO NB04 WMI AB Platform Driver" + depends on ACPI_WMI + depends on HID + help + This driver implements the WMI AB device found on TUXEDO notebooks + with board vendor NB04. This enables keyboard backlight control via a + virtual HID LampArray device. + + When compiled as a module it will be called tuxedo_nb04_wmi_ab. diff --git a/drivers/platform/x86/tuxedo/nb04/Makefile b/drivers/platform/x86/tuxedo/nb04/Makefile new file mode 100644 index 000000000..c963e0d60 --- /dev/null +++ b/drivers/platform/x86/tuxedo/nb04/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2024-2025 Werner Sembach wse@tuxedocomputers.com +# +# TUXEDO X86 Platform Specific Drivers +# + +tuxedo_nb04_wmi_ab-y := wmi_ab.o +tuxedo_nb04_wmi_ab-y += wmi_util.o +obj-$(CONFIG_TUXEDO_NB04_WMI_AB) += tuxedo_nb04_wmi_ab.o diff --git a/drivers/platform/x86/tuxedo/nb04/wmi_ab.c b/drivers/platform/x86/tuxedo/nb04/wmi_ab.c new file mode 100644 index 000000000..32d775602 --- /dev/null +++ b/drivers/platform/x86/tuxedo/nb04/wmi_ab.c @@ -0,0 +1,923 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This driver implements the WMI AB device found on TUXEDO notebooks with board + * vendor NB04. + * + * Copyright (C) 2024-2025 Werner Sembach + */ + +#include +#include +#include +#include +#include + +#include "wmi_util.h" + +static const struct wmi_device_id tuxedo_nb04_wmi_ab_device_ids[] = { + { .guid_string = "80C9BAA6-AC48-4538-9234-9F81A55E7C85" }, + { } +}; +MODULE_DEVICE_TABLE(wmi, tuxedo_nb04_wmi_ab_device_ids); + +enum { + LAMP_ARRAY_ATTRIBUTES_REPORT_ID = 0x01, + LAMP_ATTRIBUTES_REQUEST_REPORT_ID = 0x02, + LAMP_ATTRIBUTES_RESPONSE_REPORT_ID = 0x03, + LAMP_MULTI_UPDATE_REPORT_ID = 0x04, + LAMP_RANGE_UPDATE_REPORT_ID = 0x05, + LAMP_ARRAY_CONTROL_REPORT_ID = 0x06, +}; + +static u8 tux_report_descriptor[327] = { + 0x05, 0x59, // Usage Page (Lighting and Illumination) + 0x09, 0x01, // Usage (Lamp Array) + 0xa1, 0x01, // Collection (Application) + 0x85, LAMP_ARRAY_ATTRIBUTES_REPORT_ID, // Report ID (1) + 0x09, 0x02, // Usage (Lamp Array Attributes Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x03, // Usage (Lamp Count) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x75, 0x10, // Report Size (16) + 0x95, 0x01, // Report Count (1) + 0xb1, 0x03, // Feature (Cnst,Var,Abs) + 0x09, 0x04, // Usage (Bounding Box Width In Micrometers) + 0x09, 0x05, // Usage (Bounding Box Height In Micrometers) + 0x09, 0x06, // Usage (Bounding Box Depth In Micrometers) + 0x09, 0x07, // Usage (Lamp Array Kind) + 0x09, 0x08, // Usage (Min Update Interval In Microseconds) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0xff, 0x7f, // Logical Maximum (2147483647) + 0x75, 0x20, // Report Size (32) + 0x95, 0x05, // Report Count (5) + 0xb1, 0x03, // Feature (Cnst,Var,Abs) + 0xc0, // End Collection + 0x85, LAMP_ATTRIBUTES_REQUEST_REPORT_ID, // Report ID (2) + 0x09, 0x20, // Usage (Lamp Attributes Request Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x21, // Usage (Lamp Id) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x75, 0x10, // Report Size (16) + 0x95, 0x01, // Report Count (1) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0xc0, // End Collection + 0x85, LAMP_ATTRIBUTES_RESPONSE_REPORT_ID, // Report ID (3) + 0x09, 0x22, // Usage (Lamp Attributes Response Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x21, // Usage (Lamp Id) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x75, 0x10, // Report Size (16) + 0x95, 0x01, // Report Count (1) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x23, // Usage (Position X In Micrometers) + 0x09, 0x24, // Usage (Position Y In Micrometers) + 0x09, 0x25, // Usage (Position Z In Micrometers) + 0x09, 0x27, // Usage (Update Latency In Microseconds) + 0x09, 0x26, // Usage (Lamp Purposes) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0xff, 0x7f, // Logical Maximum (2147483647) + 0x75, 0x20, // Report Size (32) + 0x95, 0x05, // Report Count (5) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x28, // Usage (Red Level Count) + 0x09, 0x29, // Usage (Green Level Count) + 0x09, 0x2a, // Usage (Blue Level Count) + 0x09, 0x2b, // Usage (Intensity Level Count) + 0x09, 0x2c, // Usage (Is Programmable) + 0x09, 0x2d, // Usage (Input Binding) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x06, // Report Count (6) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0xc0, // End Collection + 0x85, LAMP_MULTI_UPDATE_REPORT_ID, // Report ID (4) + 0x09, 0x50, // Usage (Lamp Multi Update Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x03, // Usage (Lamp Count) + 0x09, 0x55, // Usage (Lamp Update Flags) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x08, // Logical Maximum (8) + 0x75, 0x08, // Report Size (8) + 0x95, 0x02, // Report Count (2) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x21, // Usage (Lamp Id) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x75, 0x10, // Report Size (16) + 0x95, 0x08, // Report Count (8) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x20, // Report Count (32) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0xc0, // End Collection + 0x85, LAMP_RANGE_UPDATE_REPORT_ID, // Report ID (5) + 0x09, 0x60, // Usage (Lamp Range Update Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x55, // Usage (Lamp Update Flags) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x08, // Logical Maximum (8) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x61, // Usage (Lamp Id Start) + 0x09, 0x62, // Usage (Lamp Id End) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x75, 0x10, // Report Size (16) + 0x95, 0x02, // Report Count (2) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0x09, 0x51, // Usage (Red Update Channel) + 0x09, 0x52, // Usage (Green Update Channel) + 0x09, 0x53, // Usage (Blue Update Channel) + 0x09, 0x54, // Usage (Intensity Update Channel) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x04, // Report Count (4) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0xc0, // End Collection + 0x85, LAMP_ARRAY_CONTROL_REPORT_ID, // Report ID (6) + 0x09, 0x70, // Usage (Lamp Array Control Report) + 0xa1, 0x02, // Collection (Logical) + 0x09, 0x71, // Usage (Autonomous Mode) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0xb1, 0x02, // Feature (Data,Var,Abs) + 0xc0, // End Collection + 0xc0 // End Collection +}; + +struct tux_kbl_map_entry_t { + u8 code; + struct { + u32 x; + u32 y; + u32 z; + } pos; +}; + +static const struct tux_kbl_map_entry_t sirius_16_ansii_kbl_map[] = { + { 0x29, { 25000, 53000, 5000 } }, + { 0x3a, { 41700, 53000, 5000 } }, + { 0x3b, { 58400, 53000, 5000 } }, + { 0x3c, { 75100, 53000, 5000 } }, + { 0x3d, { 91800, 53000, 5000 } }, + { 0x3e, { 108500, 53000, 5000 } }, + { 0x3f, { 125200, 53000, 5000 } }, + { 0x40, { 141900, 53000, 5000 } }, + { 0x41, { 158600, 53000, 5000 } }, + { 0x42, { 175300, 53000, 5000 } }, + { 0x43, { 192000, 53000, 5000 } }, + { 0x44, { 208700, 53000, 5000 } }, + { 0x45, { 225400, 53000, 5000 } }, + { 0xf1, { 242100, 53000, 5000 } }, + { 0x46, { 258800, 53000, 5000 } }, + { 0x4c, { 275500, 53000, 5000 } }, + { 0x4a, { 294500, 53000, 5000 } }, + { 0x4d, { 311200, 53000, 5000 } }, + { 0x4b, { 327900, 53000, 5000 } }, + { 0x4e, { 344600, 53000, 5000 } }, + { 0x35, { 24500, 67500, 5250 } }, + { 0x1e, { 42500, 67500, 5250 } }, + { 0x1f, { 61000, 67500, 5250 } }, + { 0x20, { 79500, 67500, 5250 } }, + { 0x21, { 98000, 67500, 5250 } }, + { 0x22, { 116500, 67500, 5250 } }, + { 0x23, { 135000, 67500, 5250 } }, + { 0x24, { 153500, 67500, 5250 } }, + { 0x25, { 172000, 67500, 5250 } }, + { 0x26, { 190500, 67500, 5250 } }, + { 0x27, { 209000, 67500, 5250 } }, + { 0x2d, { 227500, 67500, 5250 } }, + { 0x2e, { 246000, 67500, 5250 } }, + { 0x2a, { 269500, 67500, 5250 } }, + { 0x53, { 294500, 67500, 5250 } }, + { 0x55, { 311200, 67500, 5250 } }, + { 0x54, { 327900, 67500, 5250 } }, + { 0x56, { 344600, 67500, 5250 } }, + { 0x2b, { 31000, 85500, 5500 } }, + { 0x14, { 51500, 85500, 5500 } }, + { 0x1a, { 70000, 85500, 5500 } }, + { 0x08, { 88500, 85500, 5500 } }, + { 0x15, { 107000, 85500, 5500 } }, + { 0x17, { 125500, 85500, 5500 } }, + { 0x1c, { 144000, 85500, 5500 } }, + { 0x18, { 162500, 85500, 5500 } }, + { 0x0c, { 181000, 85500, 5500 } }, + { 0x12, { 199500, 85500, 5500 } }, + { 0x13, { 218000, 85500, 5500 } }, + { 0x2f, { 236500, 85500, 5500 } }, + { 0x30, { 255000, 85500, 5500 } }, + { 0x31, { 273500, 85500, 5500 } }, + { 0x5f, { 294500, 85500, 5500 } }, + { 0x60, { 311200, 85500, 5500 } }, + { 0x61, { 327900, 85500, 5500 } }, + { 0x39, { 33000, 103500, 5750 } }, + { 0x04, { 57000, 103500, 5750 } }, + { 0x16, { 75500, 103500, 5750 } }, + { 0x07, { 94000, 103500, 5750 } }, + { 0x09, { 112500, 103500, 5750 } }, + { 0x0a, { 131000, 103500, 5750 } }, + { 0x0b, { 149500, 103500, 5750 } }, + { 0x0d, { 168000, 103500, 5750 } }, + { 0x0e, { 186500, 103500, 5750 } }, + { 0x0f, { 205000, 103500, 5750 } }, + { 0x33, { 223500, 103500, 5750 } }, + { 0x34, { 242000, 103500, 5750 } }, + { 0x28, { 267500, 103500, 5750 } }, + { 0x5c, { 294500, 103500, 5750 } }, + { 0x5d, { 311200, 103500, 5750 } }, + { 0x5e, { 327900, 103500, 5750 } }, + { 0x57, { 344600, 94500, 5625 } }, + { 0xe1, { 37000, 121500, 6000 } }, + { 0x1d, { 66000, 121500, 6000 } }, + { 0x1b, { 84500, 121500, 6000 } }, + { 0x06, { 103000, 121500, 6000 } }, + { 0x19, { 121500, 121500, 6000 } }, + { 0x05, { 140000, 121500, 6000 } }, + { 0x11, { 158500, 121500, 6000 } }, + { 0x10, { 177000, 121500, 6000 } }, + { 0x36, { 195500, 121500, 6000 } }, + { 0x37, { 214000, 121500, 6000 } }, + { 0x38, { 232500, 121500, 6000 } }, + { 0xe5, { 251500, 121500, 6000 } }, + { 0x52, { 273500, 129000, 6125 } }, + { 0x59, { 294500, 121500, 6000 } }, + { 0x5a, { 311200, 121500, 6000 } }, + { 0x5b, { 327900, 121500, 6000 } }, + { 0xe0, { 28000, 139500, 6250 } }, + { 0xfe, { 47500, 139500, 6250 } }, + { 0xe3, { 66000, 139500, 6250 } }, + { 0xe2, { 84500, 139500, 6250 } }, + { 0x2c, { 140000, 139500, 6250 } }, + { 0xe6, { 195500, 139500, 6250 } }, + { 0x65, { 214000, 139500, 6250 } }, + { 0xe4, { 234000, 139500, 6250 } }, + { 0x50, { 255000, 147000, 6375 } }, + { 0x51, { 273500, 147000, 6375 } }, + { 0x4f, { 292000, 147000, 6375 } }, + { 0x62, { 311200, 139500, 6250 } }, + { 0x63, { 327900, 139500, 6250 } }, + { 0x58, { 344600, 130500, 6125 } }, +}; + +static const struct tux_kbl_map_entry_t sirius_16_iso_kbl_map[] = { + { 0x29, { 25000, 53000, 5000 } }, + { 0x3a, { 41700, 53000, 5000 } }, + { 0x3b, { 58400, 53000, 5000 } }, + { 0x3c, { 75100, 53000, 5000 } }, + { 0x3d, { 91800, 53000, 5000 } }, + { 0x3e, { 108500, 53000, 5000 } }, + { 0x3f, { 125200, 53000, 5000 } }, + { 0x40, { 141900, 53000, 5000 } }, + { 0x41, { 158600, 53000, 5000 } }, + { 0x42, { 175300, 53000, 5000 } }, + { 0x43, { 192000, 53000, 5000 } }, + { 0x44, { 208700, 53000, 5000 } }, + { 0x45, { 225400, 53000, 5000 } }, + { 0xf1, { 242100, 53000, 5000 } }, + { 0x46, { 258800, 53000, 5000 } }, + { 0x4c, { 275500, 53000, 5000 } }, + { 0x4a, { 294500, 53000, 5000 } }, + { 0x4d, { 311200, 53000, 5000 } }, + { 0x4b, { 327900, 53000, 5000 } }, + { 0x4e, { 344600, 53000, 5000 } }, + { 0x35, { 24500, 67500, 5250 } }, + { 0x1e, { 42500, 67500, 5250 } }, + { 0x1f, { 61000, 67500, 5250 } }, + { 0x20, { 79500, 67500, 5250 } }, + { 0x21, { 98000, 67500, 5250 } }, + { 0x22, { 116500, 67500, 5250 } }, + { 0x23, { 135000, 67500, 5250 } }, + { 0x24, { 153500, 67500, 5250 } }, + { 0x25, { 172000, 67500, 5250 } }, + { 0x26, { 190500, 67500, 5250 } }, + { 0x27, { 209000, 67500, 5250 } }, + { 0x2d, { 227500, 67500, 5250 } }, + { 0x2e, { 246000, 67500, 5250 } }, + { 0x2a, { 269500, 67500, 5250 } }, + { 0x53, { 294500, 67500, 5250 } }, + { 0x55, { 311200, 67500, 5250 } }, + { 0x54, { 327900, 67500, 5250 } }, + { 0x56, { 344600, 67500, 5250 } }, + { 0x2b, { 31000, 85500, 5500 } }, + { 0x14, { 51500, 85500, 5500 } }, + { 0x1a, { 70000, 85500, 5500 } }, + { 0x08, { 88500, 85500, 5500 } }, + { 0x15, { 107000, 85500, 5500 } }, + { 0x17, { 125500, 85500, 5500 } }, + { 0x1c, { 144000, 85500, 5500 } }, + { 0x18, { 162500, 85500, 5500 } }, + { 0x0c, { 181000, 85500, 5500 } }, + { 0x12, { 199500, 85500, 5500 } }, + { 0x13, { 218000, 85500, 5500 } }, + { 0x2f, { 234500, 85500, 5500 } }, + { 0x30, { 251000, 85500, 5500 } }, + { 0x5f, { 294500, 85500, 5500 } }, + { 0x60, { 311200, 85500, 5500 } }, + { 0x61, { 327900, 85500, 5500 } }, + { 0x39, { 33000, 103500, 5750 } }, + { 0x04, { 57000, 103500, 5750 } }, + { 0x16, { 75500, 103500, 5750 } }, + { 0x07, { 94000, 103500, 5750 } }, + { 0x09, { 112500, 103500, 5750 } }, + { 0x0a, { 131000, 103500, 5750 } }, + { 0x0b, { 149500, 103500, 5750 } }, + { 0x0d, { 168000, 103500, 5750 } }, + { 0x0e, { 186500, 103500, 5750 } }, + { 0x0f, { 205000, 103500, 5750 } }, + { 0x33, { 223500, 103500, 5750 } }, + { 0x34, { 240000, 103500, 5750 } }, + { 0x32, { 256500, 103500, 5750 } }, + { 0x28, { 271500, 94500, 5750 } }, + { 0x5c, { 294500, 103500, 5750 } }, + { 0x5d, { 311200, 103500, 5750 } }, + { 0x5e, { 327900, 103500, 5750 } }, + { 0x57, { 344600, 94500, 5625 } }, + { 0xe1, { 28000, 121500, 6000 } }, + { 0x64, { 47500, 121500, 6000 } }, + { 0x1d, { 66000, 121500, 6000 } }, + { 0x1b, { 84500, 121500, 6000 } }, + { 0x06, { 103000, 121500, 6000 } }, + { 0x19, { 121500, 121500, 6000 } }, + { 0x05, { 140000, 121500, 6000 } }, + { 0x11, { 158500, 121500, 6000 } }, + { 0x10, { 177000, 121500, 6000 } }, + { 0x36, { 195500, 121500, 6000 } }, + { 0x37, { 214000, 121500, 6000 } }, + { 0x38, { 232500, 121500, 6000 } }, + { 0xe5, { 251500, 121500, 6000 } }, + { 0x52, { 273500, 129000, 6125 } }, + { 0x59, { 294500, 121500, 6000 } }, + { 0x5a, { 311200, 121500, 6000 } }, + { 0x5b, { 327900, 121500, 6000 } }, + { 0xe0, { 28000, 139500, 6250 } }, + { 0xfe, { 47500, 139500, 6250 } }, + { 0xe3, { 66000, 139500, 6250 } }, + { 0xe2, { 84500, 139500, 6250 } }, + { 0x2c, { 140000, 139500, 6250 } }, + { 0xe6, { 195500, 139500, 6250 } }, + { 0x65, { 214000, 139500, 6250 } }, + { 0xe4, { 234000, 139500, 6250 } }, + { 0x50, { 255000, 147000, 6375 } }, + { 0x51, { 273500, 147000, 6375 } }, + { 0x4f, { 292000, 147000, 6375 } }, + { 0x62, { 311200, 139500, 6250 } }, + { 0x63, { 327900, 139500, 6250 } }, + { 0x58, { 344600, 130500, 6125 } }, +}; + +struct tux_driver_data_t { + struct hid_device *hdev; +}; + +struct tux_hdev_driver_data_t { + u8 lamp_count; + const struct tux_kbl_map_entry_t *kbl_map; + u8 next_lamp_id; + union tux_wmi_xx_496in_80out_in_t next_kbl_set_multiple_keys_in; +}; + +static int tux_ll_start(struct hid_device *hdev) +{ + struct wmi_device *wdev = to_wmi_device(hdev->dev.parent); + struct tux_hdev_driver_data_t *driver_data; + union tux_wmi_xx_8in_80out_out_t out; + union tux_wmi_xx_8in_80out_in_t in; + u8 keyboard_type; + int ret; + + driver_data = devm_kzalloc(&hdev->dev, sizeof(*driver_data), GFP_KERNEL); + if (!driver_data) + return -ENOMEM; + + in.get_device_status_in.device_type = TUX_GET_DEVICE_STATUS_DEVICE_ID_KEYBOARD; + ret = tux_wmi_xx_8in_80out(wdev, TUX_GET_DEVICE_STATUS, &in, &out); + if (ret) + return ret; + + keyboard_type = out.get_device_status_out.keyboard_physical_layout; + if (keyboard_type == TUX_GET_DEVICE_STATUS_KEYBOARD_LAYOUT_ANSII) { + driver_data->lamp_count = ARRAY_SIZE(sirius_16_ansii_kbl_map); + driver_data->kbl_map = sirius_16_ansii_kbl_map; + } else if (keyboard_type == TUX_GET_DEVICE_STATUS_KEYBOARD_LAYOUT_ISO) { + driver_data->lamp_count = ARRAY_SIZE(sirius_16_iso_kbl_map); + driver_data->kbl_map = sirius_16_iso_kbl_map; + } else { + return -EINVAL; + } + driver_data->next_lamp_id = 0; + + dev_set_drvdata(&hdev->dev, driver_data); + + return ret; +} + +static void tux_ll_stop(struct hid_device *hdev __always_unused) +{ +} + +static int tux_ll_open(struct hid_device *hdev __always_unused) +{ + return 0; +} + +static void tux_ll_close(struct hid_device *hdev __always_unused) +{ +} + +static int tux_ll_parse(struct hid_device *hdev) +{ + return hid_parse_report(hdev, tux_report_descriptor, + sizeof(tux_report_descriptor)); +} + +struct __packed lamp_array_attributes_report_t { + const u8 report_id; + u16 lamp_count; + u32 bounding_box_width_in_micrometers; + u32 bounding_box_height_in_micrometers; + u32 bounding_box_depth_in_micrometers; + u32 lamp_array_kind; + u32 min_update_interval_in_microseconds; +}; + +static int handle_lamp_array_attributes_report(struct hid_device *hdev, + struct lamp_array_attributes_report_t *rep) +{ + struct tux_hdev_driver_data_t *driver_data = dev_get_drvdata(&hdev->dev); + + rep->lamp_count = driver_data->lamp_count; + rep->bounding_box_width_in_micrometers = 368000; + rep->bounding_box_height_in_micrometers = 266000; + rep->bounding_box_depth_in_micrometers = 30000; + /* + * LampArrayKindKeyboard, see "26.2.1 LampArrayKind Values" of + * "HID Usage Tables v1.5" + */ + rep->lamp_array_kind = 1; + // Some guessed value for interval microseconds + rep->min_update_interval_in_microseconds = 500; + + return sizeof(*rep); +} + +struct __packed lamp_attributes_request_report_t { + const u8 report_id; + u16 lamp_id; +}; + +static int handle_lamp_attributes_request_report(struct hid_device *hdev, + struct lamp_attributes_request_report_t *rep) +{ + struct tux_hdev_driver_data_t *driver_data = dev_get_drvdata(&hdev->dev); + + if (rep->lamp_id < driver_data->lamp_count) + driver_data->next_lamp_id = rep->lamp_id; + else + driver_data->next_lamp_id = 0; + + return sizeof(*rep); +} + +struct __packed lamp_attributes_response_report_t { + const u8 report_id; + u16 lamp_id; + u32 position_x_in_micrometers; + u32 position_y_in_micrometers; + u32 position_z_in_micrometers; + u32 update_latency_in_microseconds; + u32 lamp_purpose; + u8 red_level_count; + u8 green_level_count; + u8 blue_level_count; + u8 intensity_level_count; + u8 is_programmable; + u8 input_binding; +}; + +static int handle_lamp_attributes_response_report(struct hid_device *hdev, + struct lamp_attributes_response_report_t *rep) +{ + struct tux_hdev_driver_data_t *driver_data = dev_get_drvdata(&hdev->dev); + u16 lamp_id = driver_data->next_lamp_id; + + rep->lamp_id = lamp_id; + // Some guessed value for latency microseconds + rep->update_latency_in_microseconds = 100; + /* + * LampPurposeControl, see "26.3.1 LampPurposes Flags" of + * "HID Usage Tables v1.5" + */ + rep->lamp_purpose = 1; + rep->red_level_count = 0xff; + rep->green_level_count = 0xff; + rep->blue_level_count = 0xff; + rep->intensity_level_count = 0xff; + rep->is_programmable = 1; + + if (driver_data->kbl_map[lamp_id].code <= 0xe8) { + rep->input_binding = driver_data->kbl_map[lamp_id].code; + } else { + /* + * Everything bigger is reserved/undefined, see + * "10 Keyboard/Keypad Page (0x07)" of "HID Usage Tables v1.5" + * and should return 0, see "26.8.3 Lamp Attributes" of the same + * document. + */ + rep->input_binding = 0; + } + rep->position_x_in_micrometers = driver_data->kbl_map[lamp_id].pos.x; + rep->position_y_in_micrometers = driver_data->kbl_map[lamp_id].pos.y; + rep->position_z_in_micrometers = driver_data->kbl_map[lamp_id].pos.z; + + driver_data->next_lamp_id = (driver_data->next_lamp_id + 1) % driver_data->lamp_count; + + return sizeof(*rep); +} + +#define LAMP_UPDATE_FLAGS_LAMP_UPDATE_COMPLETE BIT(0) + +struct __packed lamp_rgbi_tuple_t { + u8 red; + u8 green; + u8 blue; + u8 intensity; +}; + +#define LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX 8 + +struct __packed lamp_multi_update_report_t { + const u8 report_id; + u8 lamp_count; + u8 lamp_update_flags; + u16 lamp_id[LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX]; + struct lamp_rgbi_tuple_t update_channels[LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX]; +}; + +static int handle_lamp_multi_update_report(struct hid_device *hdev, + struct lamp_multi_update_report_t *rep) +{ + struct tux_hdev_driver_data_t *driver_data = dev_get_drvdata(&hdev->dev); + union tux_wmi_xx_496in_80out_in_t *next = &driver_data->next_kbl_set_multiple_keys_in; + struct tux_kbl_set_multiple_keys_in_rgb_config_t *rgb_configs_j; + struct wmi_device *wdev = to_wmi_device(hdev->dev.parent); + union tux_wmi_xx_496in_80out_out_t out; + u8 key_id, key_id_j, intensity_i, red_i, green_i, blue_i; + int ret; + + /* + * Catching misformatted lamp_multi_update_report and fail silently + * according to "HID Usage Tables v1.5" + */ + for (unsigned int i = 0; i < rep->lamp_count; ++i) { + if (rep->lamp_id[i] > driver_data->lamp_count) { + hid_dbg(hdev, "Out of bounds lamp_id in lamp_multi_update_report. Skipping whole report!\n"); + return sizeof(*rep); + } + + for (unsigned int j = i + 1; j < rep->lamp_count; ++j) { + if (rep->lamp_id[i] == rep->lamp_id[j]) { + hid_dbg(hdev, "Duplicate lamp_id in lamp_multi_update_report. Skipping whole report!\n"); + return sizeof(*rep); + } + } + } + + for (unsigned int i = 0; i < rep->lamp_count; ++i) { + key_id = driver_data->kbl_map[rep->lamp_id[i]].code; + + for (unsigned int j = 0; + j < TUX_KBL_SET_MULTIPLE_KEYS_LIGHTING_SETTINGS_COUNT_MAX; + ++j) { + rgb_configs_j = &next->kbl_set_multiple_keys_in.rgb_configs[j]; + key_id_j = rgb_configs_j->key_id; + if (key_id_j != 0x00 && key_id_j != key_id) + continue; + + if (key_id_j == 0x00) + next->kbl_set_multiple_keys_in.rgb_configs_cnt = + j + 1; + rgb_configs_j->key_id = key_id; + /* + * While this driver respects update_channel.intensity + * according to "HID Usage Tables v1.5" also on RGB + * leds, the Microsoft MacroPad reference implementation + * (https://github.com/microsoft/RP2040MacropadHidSample + * 1d6c3ad) does not and ignores it. If it turns out + * that Windows writes intensity = 0 for RGB leds + * instead of intensity = 255, this driver should also + * ignore the update_channel.intensity. + */ + intensity_i = rep->update_channels[i].intensity; + red_i = rep->update_channels[i].red; + green_i = rep->update_channels[i].green; + blue_i = rep->update_channels[i].blue; + rgb_configs_j->red = red_i * intensity_i / 0xff; + rgb_configs_j->green = green_i * intensity_i / 0xff; + rgb_configs_j->blue = blue_i * intensity_i / 0xff; + + break; + } + } + + if (rep->lamp_update_flags & LAMP_UPDATE_FLAGS_LAMP_UPDATE_COMPLETE) { + ret = tux_wmi_xx_496in_80out(wdev, TUX_KBL_SET_MULTIPLE_KEYS, + next, &out); + memset(next, 0, sizeof(*next)); + if (ret) + return ret; + } + + return sizeof(*rep); +} + +struct __packed lamp_range_update_report_t { + const u8 report_id; + u8 lamp_update_flags; + u16 lamp_id_start; + u16 lamp_id_end; + struct lamp_rgbi_tuple_t update_channel; +}; + +static int handle_lamp_range_update_report(struct hid_device *hdev, + struct lamp_range_update_report_t *rep) +{ + struct tux_hdev_driver_data_t *driver_data = dev_get_drvdata(&hdev->dev); + struct lamp_multi_update_report_t lamp_multi_update_report = { + .report_id = LAMP_MULTI_UPDATE_REPORT_ID, + }; + struct lamp_rgbi_tuple_t *update_channels_j; + int ret; + + /* + * Catching misformatted lamp_range_update_report and fail silently + * according to "HID Usage Tables v1.5" + */ + if (rep->lamp_id_start > rep->lamp_id_end) { + hid_dbg(hdev, "lamp_id_start > lamp_id_end in lamp_range_update_report. Skipping whole report!\n"); + return sizeof(*rep); + } + + if (rep->lamp_id_end > driver_data->lamp_count - 1) { + hid_dbg(hdev, "Out of bounds lamp_id_end in lamp_range_update_report. Skipping whole report!\n"); + return sizeof(*rep); + } + + /* + * Break handle_lamp_range_update_report call down to multiple + * handle_lamp_multi_update_report calls to easily ensure that mixing + * handle_lamp_range_update_report and handle_lamp_multi_update_report + * does not break things. + */ + for (unsigned int i = rep->lamp_id_start; i < rep->lamp_id_end + 1; + i = i + LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX) { + lamp_multi_update_report.lamp_count = + min(rep->lamp_id_end + 1 - i, + LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX); + lamp_multi_update_report.lamp_update_flags = + i + LAMP_MULTI_UPDATE_REPORT_LAMP_COUNT_MAX >= + rep->lamp_id_end + 1 ? + LAMP_UPDATE_FLAGS_LAMP_UPDATE_COMPLETE : 0; + + for (unsigned int j = 0; j < lamp_multi_update_report.lamp_count; ++j) { + lamp_multi_update_report.lamp_id[j] = i + j; + update_channels_j = + &lamp_multi_update_report.update_channels[j]; + update_channels_j->red = rep->update_channel.red; + update_channels_j->green = rep->update_channel.green; + update_channels_j->blue = rep->update_channel.blue; + update_channels_j->intensity = rep->update_channel.intensity; + } + + ret = handle_lamp_multi_update_report(hdev, &lamp_multi_update_report); + if (ret < 0) + return ret; + if (ret != sizeof(lamp_multi_update_report)) + return -EIO; + } + + return sizeof(*rep); +} + +struct __packed lamp_array_control_report_t { + const u8 report_id; + u8 autonomous_mode; +}; + +static int handle_lamp_array_control_report(struct hid_device *hdev __always_unused, + struct lamp_array_control_report_t *rep) +{ + /* + * The keyboards firmware doesn't have any built in controls and the + * built in effects are not implemented so this is a NOOP. + * According to the HID Documentation (HID Usage Tables v1.5) this + * function is optional and can be removed from the HID Report + * Descriptor, but it should first be confirmed that userspace respects + * this possibility too. The Microsoft MacroPad reference implementation + * (https://github.com/microsoft/RP2040MacropadHidSample 1d6c3ad) + * already deviates from the spec at another point, see + * handle_lamp_*_update_report. + */ + + return sizeof(*rep); +} + +static int tux_ll_raw_request(struct hid_device *hdev, u8 reportnum, u8 *buf, + size_t len, unsigned char rtype, int reqtype) +{ + if (rtype != HID_FEATURE_REPORT) + return -EINVAL; + + switch (reqtype) { + case HID_REQ_GET_REPORT: + switch (reportnum) { + case LAMP_ARRAY_ATTRIBUTES_REPORT_ID: + if (len != sizeof(struct lamp_array_attributes_report_t)) + return -EINVAL; + return handle_lamp_array_attributes_report(hdev, + (struct lamp_array_attributes_report_t *)buf); + case LAMP_ATTRIBUTES_RESPONSE_REPORT_ID: + if (len != sizeof(struct lamp_attributes_response_report_t)) + return -EINVAL; + return handle_lamp_attributes_response_report(hdev, + (struct lamp_attributes_response_report_t *)buf); + } + break; + case HID_REQ_SET_REPORT: + switch (reportnum) { + case LAMP_ATTRIBUTES_REQUEST_REPORT_ID: + if (len != sizeof(struct lamp_attributes_request_report_t)) + return -EINVAL; + return handle_lamp_attributes_request_report(hdev, + (struct lamp_attributes_request_report_t *)buf); + case LAMP_MULTI_UPDATE_REPORT_ID: + if (len != sizeof(struct lamp_multi_update_report_t)) + return -EINVAL; + return handle_lamp_multi_update_report(hdev, + (struct lamp_multi_update_report_t *)buf); + case LAMP_RANGE_UPDATE_REPORT_ID: + if (len != sizeof(struct lamp_range_update_report_t)) + return -EINVAL; + return handle_lamp_range_update_report(hdev, + (struct lamp_range_update_report_t *)buf); + case LAMP_ARRAY_CONTROL_REPORT_ID: + if (len != sizeof(struct lamp_array_control_report_t)) + return -EINVAL; + return handle_lamp_array_control_report(hdev, + (struct lamp_array_control_report_t *)buf); + } + break; + } + + return -EINVAL; +} + +static const struct hid_ll_driver tux_ll_driver = { + .start = &tux_ll_start, + .stop = &tux_ll_stop, + .open = &tux_ll_open, + .close = &tux_ll_close, + .parse = &tux_ll_parse, + .raw_request = &tux_ll_raw_request, +}; + +static int tux_virt_lamparray_add_device(struct wmi_device *wdev, + struct hid_device **hdev_out) +{ + struct hid_device *hdev; + int ret; + + dev_dbg(&wdev->dev, "Adding TUXEDO NB04 Virtual LampArray device.\n"); + + hdev = hid_allocate_device(); + if (IS_ERR(hdev)) + return PTR_ERR(hdev); + *hdev_out = hdev; + + strscpy(hdev->name, "TUXEDO NB04 RGB Lighting", sizeof(hdev->name)); + + hdev->ll_driver = &tux_ll_driver; + hdev->bus = BUS_VIRTUAL; + hdev->vendor = 0x21ba; + hdev->product = 0x0400; + hdev->dev.parent = &wdev->dev; + + ret = hid_add_device(hdev); + if (ret) + hid_destroy_device(hdev); + return ret; +} + +static int tux_probe(struct wmi_device *wdev, const void *context __always_unused) +{ + struct tux_driver_data_t *driver_data; + + driver_data = devm_kzalloc(&wdev->dev, sizeof(*driver_data), GFP_KERNEL); + if (!driver_data) + return -ENOMEM; + + dev_set_drvdata(&wdev->dev, driver_data); + + return tux_virt_lamparray_add_device(wdev, &driver_data->hdev); +} + +static void tux_remove(struct wmi_device *wdev) +{ + struct tux_driver_data_t *driver_data = dev_get_drvdata(&wdev->dev); + + hid_destroy_device(driver_data->hdev); +} + +static struct wmi_driver tuxedo_nb04_wmi_tux_driver = { + .driver = { + .name = "tuxedo_nb04_wmi_ab", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = tuxedo_nb04_wmi_ab_device_ids, + .probe = tux_probe, + .remove = tux_remove, + .no_singleton = true, +}; + +/* + * We don't know if the WMI API is stable and how unique the GUID is for this + * ODM. To be on the safe side we therefore only run this driver on tested + * devices defined by this list. + */ +static const struct dmi_system_id tested_devices_dmi_table[] __initconst = { + { + // TUXEDO Sirius 16 Gen1 + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "APX958"), + }, + }, + { + // TUXEDO Sirius 16 Gen2 + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AHP958"), + }, + }, + { } +}; + +static int __init tuxedo_nb04_wmi_tux_init(void) +{ + if (!dmi_check_system(tested_devices_dmi_table)) + return -ENODEV; + + return wmi_driver_register(&tuxedo_nb04_wmi_tux_driver); +} +module_init(tuxedo_nb04_wmi_tux_init); + +static void __exit tuxedo_nb04_wmi_tux_exit(void) +{ + return wmi_driver_unregister(&tuxedo_nb04_wmi_tux_driver); +} +module_exit(tuxedo_nb04_wmi_tux_exit); + +MODULE_DESCRIPTION("Virtual HID LampArray interface for TUXEDO NB04 devices"); +MODULE_AUTHOR("Werner Sembach "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/tuxedo/nb04/wmi_util.c b/drivers/platform/x86/tuxedo/nb04/wmi_util.c new file mode 100644 index 000000000..e894690da --- /dev/null +++ b/drivers/platform/x86/tuxedo/nb04/wmi_util.c @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code gives functions to avoid code duplication while interacting with + * the TUXEDO NB04 wmi interfaces. + * + * Copyright (C) 2024-2025 Werner Sembach + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include + +#include "wmi_util.h" + +static int __wmi_method_acpi_object_out(struct wmi_device *wdev, + u32 wmi_method_id, + u8 *in, + acpi_size in_len, + union acpi_object **out) +{ + struct acpi_buffer acpi_buffer_in = { in_len, in }; + struct acpi_buffer acpi_buffer_out = { ACPI_ALLOCATE_BUFFER, NULL }; + + dev_dbg(&wdev->dev, "Evaluate WMI method: %u in:\n", wmi_method_id); + print_hex_dump_bytes("", DUMP_PREFIX_OFFSET, in, in_len); + + acpi_status status = wmidev_evaluate_method(wdev, 0, wmi_method_id, + &acpi_buffer_in, + &acpi_buffer_out); + if (ACPI_FAILURE(status)) { + dev_err(&wdev->dev, "Failed to evaluate WMI method.\n"); + return -EIO; + } + if (!acpi_buffer_out.pointer) { + dev_err(&wdev->dev, "Unexpected empty out buffer.\n"); + return -ENODATA; + } + + *out = acpi_buffer_out.pointer; + + return 0; +} + +static int __wmi_method_buffer_out(struct wmi_device *wdev, + u32 wmi_method_id, + u8 *in, + acpi_size in_len, + u8 *out, + acpi_size out_len) +{ + int ret; + + union acpi_object *acpi_object_out __free(kfree) = NULL; + + ret = __wmi_method_acpi_object_out(wdev, wmi_method_id, + in, in_len, + &acpi_object_out); + if (ret) + return ret; + + if (acpi_object_out->type != ACPI_TYPE_BUFFER) { + dev_err(&wdev->dev, "Unexpected out buffer type. Expected: %u Got: %u\n", + ACPI_TYPE_BUFFER, acpi_object_out->type); + return -EIO; + } + if (acpi_object_out->buffer.length < out_len) { + dev_err(&wdev->dev, "Unexpected out buffer length.\n"); + return -EIO; + } + + memcpy(out, acpi_object_out->buffer.pointer, out_len); + + return 0; +} + +int tux_wmi_xx_8in_80out(struct wmi_device *wdev, + enum tux_wmi_xx_8in_80out_methods method, + union tux_wmi_xx_8in_80out_in_t *in, + union tux_wmi_xx_8in_80out_out_t *out) +{ + return __wmi_method_buffer_out(wdev, method, in->raw, 8, out->raw, 80); +} + +int tux_wmi_xx_496in_80out(struct wmi_device *wdev, + enum tux_wmi_xx_496in_80out_methods method, + union tux_wmi_xx_496in_80out_in_t *in, + union tux_wmi_xx_496in_80out_out_t *out) +{ + return __wmi_method_buffer_out(wdev, method, in->raw, 496, out->raw, 80); +} diff --git a/drivers/platform/x86/tuxedo/nb04/wmi_util.h b/drivers/platform/x86/tuxedo/nb04/wmi_util.h new file mode 100644 index 000000000..c44093fd5 --- /dev/null +++ b/drivers/platform/x86/tuxedo/nb04/wmi_util.h @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * This code gives functions to avoid code duplication while interacting with + * the TUXEDO NB04 wmi interfaces. + * + * Copyright (C) 2024-2025 Werner Sembach + */ + +#ifndef TUXEDO_NB04_WMI_UTIL_H +#define TUXEDO_NB04_WMI_UTIL_H + +#include + +#define TUX_GET_DEVICE_STATUS_DEVICE_ID_TOUCHPAD 1 +#define TUX_GET_DEVICE_STATUS_DEVICE_ID_KEYBOARD 2 +#define TUX_GET_DEVICE_STATUS_DEVICE_ID_APP_PAGES 3 + +#define TUX_GET_DEVICE_STATUS_KBL_TYPE_NONE 0 +#define TUX_GET_DEVICE_STATUS_KBL_TYPE_PER_KEY 1 +#define TUX_GET_DEVICE_STATUS_KBL_TYPE_FOUR_ZONE 2 +#define TUX_GET_DEVICE_STATUS_KBL_TYPE_WHITE_ONLY 3 + +#define TUX_GET_DEVICE_STATUS_KEYBOARD_LAYOUT_ANSII 0 +#define TUX_GET_DEVICE_STATUS_KEYBOARD_LAYOUT_ISO 1 + +#define TUX_GET_DEVICE_STATUS_COLOR_ID_RED 1 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_GREEN 2 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_YELLOW 3 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_BLUE 4 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_PURPLE 5 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_INDIGO 6 +#define TUX_GET_DEVICE_STATUS_COLOR_ID_WHITE 7 + +#define TUX_GET_DEVICE_STATUS_APP_PAGES_DASHBOARD BIT(0) +#define TUX_GET_DEVICE_STATUS_APP_PAGES_SYSTEMINFOS BIT(1) +#define TUX_GET_DEVICE_STATUS_APP_PAGES_KBL BIT(2) +#define TUX_GET_DEVICE_STATUS_APP_PAGES_HOTKEYS BIT(3) + +union tux_wmi_xx_8in_80out_in_t { + u8 raw[8]; + struct __packed { + u8 device_type; + u8 reserved[7]; + } get_device_status_in; +}; + +union tux_wmi_xx_8in_80out_out_t { + u8 raw[80]; + struct __packed { + u16 return_status; + u8 device_enabled; + u8 kbl_type; + u8 kbl_side_bar_supported; + u8 keyboard_physical_layout; + u8 app_pages; + u8 per_key_kbl_default_color; + u8 four_zone_kbl_default_color_1; + u8 four_zone_kbl_default_color_2; + u8 four_zone_kbl_default_color_3; + u8 four_zone_kbl_default_color_4; + u8 light_bar_kbl_default_color; + u8 reserved_0[1]; + u16 dedicated_gpu_id; + u8 reserved_1[64]; + } get_device_status_out; +}; + +enum tux_wmi_xx_8in_80out_methods { + TUX_GET_DEVICE_STATUS = 2, +}; + +#define TUX_KBL_SET_MULTIPLE_KEYS_LIGHTING_SETTINGS_COUNT_MAX 120 + +union tux_wmi_xx_496in_80out_in_t { + u8 raw[496]; + struct __packed { + u8 reserved[15]; + u8 rgb_configs_cnt; + struct tux_kbl_set_multiple_keys_in_rgb_config_t { + u8 key_id; + u8 red; + u8 green; + u8 blue; + } rgb_configs[TUX_KBL_SET_MULTIPLE_KEYS_LIGHTING_SETTINGS_COUNT_MAX]; + } kbl_set_multiple_keys_in; +}; + +union tux_wmi_xx_496in_80out_out_t { + u8 raw[80]; + struct __packed { + u8 return_value; + u8 reserved[79]; + } kbl_set_multiple_keys_out; +}; + +enum tux_wmi_xx_496in_80out_methods { + TUX_KBL_SET_MULTIPLE_KEYS = 6, +}; + +int tux_wmi_xx_8in_80out(struct wmi_device *wdev, + enum tux_wmi_xx_8in_80out_methods method, + union tux_wmi_xx_8in_80out_in_t *in, + union tux_wmi_xx_8in_80out_out_t *out); +int tux_wmi_xx_496in_80out(struct wmi_device *wdev, + enum tux_wmi_xx_496in_80out_methods method, + union tux_wmi_xx_496in_80out_in_t *in, + union tux_wmi_xx_496in_80out_out_t *out); + +#endif diff --git a/drivers/platform/x86/uniwill/Kconfig b/drivers/platform/x86/uniwill/Kconfig new file mode 100644 index 000000000..d07cc8440 --- /dev/null +++ b/drivers/platform/x86/uniwill/Kconfig @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Uniwill X86 Platform Specific Drivers +# + +menuconfig X86_PLATFORM_DRIVERS_UNIWILL + bool "Uniwill X86 Platform Specific Device Drivers" + depends on X86_PLATFORM_DEVICES + help + Say Y here to see options for device drivers for various + Uniwill x86 platforms, including many OEM laptops originally + manufactured by Uniwill. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if X86_PLATFORM_DRIVERS_UNIWILL + +config UNIWILL_LAPTOP + tristate "Uniwill Laptop Extras" + default m + depends on ACPI + depends on ACPI_WMI + depends on ACPI_BATTERY + depends on HWMON + depends on INPUT + depends on LEDS_CLASS_MULTICOLOR + depends on DMI + select REGMAP + select INPUT_SPARSEKMAP + help + This driver adds support for various extra features found on Uniwill laptops, + like the lightbar, hwmon sensors and hotkeys. It also supports many OEM laptops + originally manufactured by Uniwill. + + If you have such a laptop, say Y or M here. + +endif diff --git a/drivers/platform/x86/uniwill/Makefile b/drivers/platform/x86/uniwill/Makefile new file mode 100644 index 000000000..05cd1747a --- /dev/null +++ b/drivers/platform/x86/uniwill/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Makefile for linux/drivers/platform/x86/uniwill +# Uniwill X86 Platform Specific Drivers +# + +obj-$(CONFIG_UNIWILL_LAPTOP) += uniwill-laptop.o +uniwill-laptop-y := uniwill-acpi.o uniwill-wmi.o diff --git a/drivers/platform/x86/uniwill/uniwill-acpi.c b/drivers/platform/x86/uniwill/uniwill-acpi.c new file mode 100644 index 000000000..ab063ead4 --- /dev/null +++ b/drivers/platform/x86/uniwill/uniwill-acpi.c @@ -0,0 +1,2774 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Linux driver for Uniwill notebooks. + * + * Special thanks go to Pőcze Barnabás, Christoffer Sandberg and Werner Sembach + * for supporting the development of this driver either through prior work or + * by answering questions regarding the underlying ACPI and WMI interfaces. + * + * Copyright (C) 2025 Armin Wolf + */ + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "uniwill-wmi.h" + +#define EC_ADDR_BAT_POWER_UNIT_1 0x0400 + +#define EC_ADDR_BAT_POWER_UNIT_2 0x0401 + +#define EC_ADDR_BAT_DESIGN_CAPACITY_1 0x0402 + +#define EC_ADDR_BAT_DESIGN_CAPACITY_2 0x0403 + +#define EC_ADDR_BAT_FULL_CAPACITY_1 0x0404 + +#define EC_ADDR_BAT_FULL_CAPACITY_2 0x0405 + +#define EC_ADDR_BAT_DESIGN_VOLTAGE_1 0x0408 + +#define EC_ADDR_BAT_DESIGN_VOLTAGE_2 0x0409 + +#define EC_ADDR_BAT_STATUS_1 0x0432 +#define BAT_DISCHARGING BIT(0) + +#define EC_ADDR_BAT_STATUS_2 0x0433 + +#define EC_ADDR_BAT_CURRENT_1 0x0434 + +#define EC_ADDR_BAT_CURRENT_2 0x0435 + +#define EC_ADDR_BAT_REMAIN_CAPACITY_1 0x0436 + +#define EC_ADDR_BAT_REMAIN_CAPACITY_2 0x0437 + +#define EC_ADDR_BAT_VOLTAGE_1 0x0438 + +#define EC_ADDR_BAT_VOLTAGE_2 0x0439 + +#define EC_ADDR_CPU_TEMP 0x043E + +#define EC_ADDR_GPU_TEMP 0x044F + +#define EC_ADDR_SYSTEM_ID 0x0456 +#define HAS_GPU BIT(7) + +#define EC_ADDR_MAIN_FAN_RPM_1 0x0464 + +#define EC_ADDR_MAIN_FAN_RPM_2 0x0465 + +#define EC_ADDR_SECOND_FAN_RPM_1 0x046C + +#define EC_ADDR_SECOND_FAN_RPM_2 0x046D + +#define EC_ADDR_DEVICE_STATUS 0x047B +#define WIFI_STATUS_ON BIT(7) +/* BIT(5) is also unset depending on the rfkill state (bluetooth?) */ + +#define EC_ADDR_BAT_ALERT 0x0494 + +#define EC_ADDR_BAT_CYCLE_COUNT_1 0x04A6 + +#define EC_ADDR_BAT_CYCLE_COUNT_2 0x04A7 + +#define EC_ADDR_PROJECT_ID 0x0740 +#define PROJECT_ID_PH4TRX1 0x12 +#define PROJECT_ID_PH6TRX1 0x15 + +#define EC_ADDR_AP_OEM 0x0741 +#define ENABLE_MANUAL_CTRL BIT(0) +#define ITE_KBD_EFFECT_REACTIVE BIT(3) +#define FAN_ABNORMAL BIT(5) + +#define EC_ADDR_SUPPORT_5 0x0742 +#define FAN_TURBO_SUPPORTED BIT(4) +#define FAN_SUPPORT BIT(5) + +#define EC_ADDR_CTGP_DB_CTRL 0x0743 +#define CTGP_DB_GENERAL_ENABLE BIT(0) +#define CTGP_DB_DB_ENABLE BIT(1) +#define CTGP_DB_CTGP_ENABLE BIT(2) + +#define EC_ADDR_CTGP_DB_CTGP_OFFSET 0x0744 + +#define EC_ADDR_CTGP_DB_TPP_OFFSET 0x0745 + +#define EC_ADDR_CTGP_DB_DB_OFFSET 0x0746 + +#define EC_ADDR_LIGHTBAR_AC_CTRL 0x0748 +#define LIGHTBAR_APP_EXISTS BIT(0) +#define LIGHTBAR_POWER_SAVE BIT(1) +#define LIGHTBAR_S0_OFF BIT(2) +#define LIGHTBAR_S3_OFF BIT(3) // Breathing animation when suspended +#define LIGHTBAR_WELCOME BIT(7) // Rainbow animation + +#define EC_ADDR_LIGHTBAR_AC_RED 0x0749 + +#define EC_ADDR_LIGHTBAR_AC_GREEN 0x074A + +#define EC_ADDR_LIGHTBAR_AC_BLUE 0x074B + +#define EC_ADDR_BIOS_OEM 0x074E +#define FN_LOCK_STATUS BIT(4) + +#define EC_ADDR_MANUAL_FAN_CTRL 0x0751 +#define FAN_LEVEL_MASK GENMASK(2, 0) +#define FAN_MODE_TURBO BIT(4) +#define FAN_MODE_HIGH BIT(5) +#define FAN_MODE_BOOST BIT(6) +#define FAN_MODE_USER BIT(7) + +#define EC_ADDR_PWM_1 0x075B + +#define EC_ADDR_PWM_2 0x075C + +/* Unreliable */ +#define EC_ADDR_SUPPORT_1 0x0765 +#define AIRPLANE_MODE BIT(0) +#define GPS_SWITCH BIT(1) +#define OVERCLOCK BIT(2) +#define MACRO_KEY BIT(3) +#define SHORTCUT_KEY BIT(4) +#define SUPER_KEY_LOCK BIT(5) +#define LIGHTBAR BIT(6) +#define FAN_BOOST BIT(7) + +#define EC_ADDR_SUPPORT_2 0x0766 +#define SILENT_MODE BIT(0) +#define USB_CHARGING BIT(1) +#define RGB_KEYBOARD BIT(2) +#define CHINA_MODE BIT(5) +#define MY_BATTERY BIT(6) + +#define EC_ADDR_TRIGGER 0x0767 +#define TRIGGER_SUPER_KEY_LOCK BIT(0) +#define TRIGGER_LIGHTBAR BIT(1) +#define TRIGGER_FAN_BOOST BIT(2) +#define TRIGGER_SILENT_MODE BIT(3) +#define TRIGGER_USB_CHARGING BIT(4) +#define RGB_APPLY_COLOR BIT(5) +#define RGB_LOGO_EFFECT BIT(6) +#define RGB_RAINBOW_EFFECT BIT(7) + +#define EC_ADDR_SWITCH_STATUS 0x0768 +#define SUPER_KEY_LOCK_STATUS BIT(0) +#define LIGHTBAR_STATUS BIT(1) +#define FAN_BOOST_STATUS BIT(2) +#define MACRO_KEY_STATUS BIT(3) +#define MY_BAT_POWER_BAT_STATUS BIT(4) + +#define EC_ADDR_RGB_RED 0x0769 + +#define EC_ADDR_RGB_GREEN 0x076A + +#define EC_ADDR_RGB_BLUE 0x076B + +#define EC_ADDR_ROMID_START 0x0770 +#define ROMID_LENGTH 14 + +#define EC_ADDR_ROMID_EXTRA_1 0x077E + +#define EC_ADDR_ROMID_EXTRA_2 0x077F + +#define EC_ADDR_BIOS_OEM_2 0x0782 +#define FAN_V2_NEW BIT(0) +#define FAN_QKEY BIT(1) +#define FAN_TABLE_OFFICE_MODE BIT(2) +#define FAN_V3 BIT(3) +#define DEFAULT_MODE BIT(4) + +#define EC_ADDR_PL1_SETTING 0x0783 + +#define EC_ADDR_PL2_SETTING 0x0784 + +#define EC_ADDR_PL4_SETTING 0x0785 + +#define EC_ADDR_FAN_DEFAULT 0x0786 +#define FAN_CURVE_LENGTH 5 + +#define EC_ADDR_KBD_STATUS 0x078C +#define KBD_WHITE_ONLY BIT(0) // ~single color +#define KBD_SINGLE_COLOR_OFF BIT(1) +#define KBD_TURBO_LEVEL_MASK GENMASK(3, 2) +#define KBD_APPLY BIT(4) +#define KBD_BRIGHTNESS GENMASK(7, 5) + +#define EC_ADDR_FAN_CTRL 0x078E +#define FAN3P5 BIT(1) +#define CHARGING_PROFILE BIT(3) +#define UNIVERSAL_FAN_CTRL BIT(6) + +#define EC_ADDR_BIOS_OEM_3 0x07A3 +#define FAN_REDUCED_DURY_CYCLE BIT(5) +#define FAN_ALWAYS_ON BIT(6) + +#define EC_ADDR_BIOS_BYTE 0x07A4 +#define FN_LOCK_SWITCH BIT(3) + +#define EC_ADDR_OEM_3 0x07A5 +#define POWER_LED_MASK GENMASK(1, 0) +#define POWER_LED_LEFT 0x00 +#define POWER_LED_BOTH 0x01 +#define POWER_LED_NONE 0x02 +#define FAN_QUIET BIT(2) +#define OVERBOOST BIT(4) +#define HIGH_POWER BIT(7) + +#define EC_ADDR_OEM_4 0x07A6 +#define OVERBOOST_DYN_TEMP_OFF BIT(1) +#define CHARGING_PROFILE_MASK GENMASK(5, 4) +#define CHARGING_PROFILE_HIGH_CAPACITY 0x00 +#define CHARGING_PROFILE_BALANCED 0x01 +#define CHARGING_PROFILE_STATIONARY 0x02 +#define TOUCHPAD_TOGGLE_OFF BIT(6) + +#define EC_ADDR_CHARGE_CTRL 0x07B9 +#define CHARGE_CTRL_MASK GENMASK(6, 0) +#define CHARGE_CTRL_REACHED BIT(7) + +#define EC_ADDR_UNIVERSAL_FAN_CTRL 0x07C5 +#define SPLIT_TABLES BIT(7) + +#define EC_ADDR_AP_OEM_6 0x07C6 +#define ENABLE_UNIVERSAL_FAN_CTRL BIT(2) +#define BATTERY_CHARGE_FULL_OVER_24H BIT(3) +#define BATTERY_ERM_STATUS_REACHED BIT(4) + +#define EC_ADDR_USB_C_POWER_PRIORITY 0x07CC +#define USB_C_POWER_PRIORITY BIT(7) + +/* Same bits as EC_ADDR_LIGHTBAR_AC_CTRL except LIGHTBAR_S3_OFF */ +#define EC_ADDR_LIGHTBAR_BAT_CTRL 0x07E2 + +#define EC_ADDR_LIGHTBAR_BAT_RED 0x07E3 + +#define EC_ADDR_LIGHTBAR_BAT_GREEN 0x07E4 + +#define EC_ADDR_LIGHTBAR_BAT_BLUE 0x07E5 + +#define EC_ADDR_CPU_TEMP_END_TABLE 0x0F00 + +#define EC_ADDR_CPU_TEMP_START_TABLE 0x0F10 + +#define EC_ADDR_CPU_FAN_SPEED_TABLE 0x0F20 + +#define EC_ADDR_GPU_TEMP_END_TABLE 0x0F30 + +#define EC_ADDR_GPU_TEMP_START_TABLE 0x0F40 + +#define EC_ADDR_GPU_FAN_SPEED_TABLE 0x0F50 + +/* + * Those two registers technically allow for manual fan control, + * but are unstable on some models and are likely not meant to + * be used by applications as they are only accessible when using + * the WMI interface. + */ +#define EC_ADDR_PWM_1_WRITEABLE 0x1804 + +#define EC_ADDR_PWM_2_WRITEABLE 0x1809 + +#define DRIVER_NAME "uniwill" + +/* + * The OEM software always sleeps up to 6 ms after reading/writing EC + * registers, so we emulate this behaviour for maximum compatibility. + */ +#define UNIWILL_EC_DELAY_US 6000 + +#define PWM_MAX 200 +#define FAN_TABLE_LENGTH 16 + +#define LED_CHANNELS 3 +#define LED_MAX_BRIGHTNESS 200 + +#define UNIWILL_FEATURE_FN_LOCK BIT(0) +#define UNIWILL_FEATURE_SUPER_KEY BIT(1) +#define UNIWILL_FEATURE_TOUCHPAD_TOGGLE BIT(2) +#define UNIWILL_FEATURE_LIGHTBAR BIT(3) +#define UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT BIT(4) +/* Mutually exclusive with the charge limit feature */ +#define UNIWILL_FEATURE_BATTERY_CHARGE_MODES BIT(5) +#define UNIWILL_FEATURE_CPU_TEMP BIT(6) +#define UNIWILL_FEATURE_GPU_TEMP BIT(7) +#define UNIWILL_FEATURE_PRIMARY_FAN BIT(8) +#define UNIWILL_FEATURE_SECONDARY_FAN BIT(9) +#define UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL BIT(10) +#define UNIWILL_FEATURE_USB_C_POWER_PRIORITY BIT(11) + +enum usb_c_power_priority_options { + USB_C_POWER_PRIORITY_CHARGING = 0, + USB_C_POWER_PRIORITY_PERFORMANCE, +}; + +struct uniwill_data { + struct device *dev; + acpi_handle handle; + struct regmap *regmap; + unsigned int features; + struct acpi_battery_hook hook; + struct mutex battery_lock; /* Protects the list of currently registered batteries */ + union { + struct { + /* Protects writes to last_charge_type */ + struct mutex charge_type_lock; + enum power_supply_charge_type last_charge_type; + }; + unsigned int last_charge_ctrl; + }; + bool last_fn_lock_state; + bool last_super_key_enable_state; + bool last_touchpad_toggle_enable_state; + struct mutex super_key_lock; /* Protects the toggling of the super key lock state */ + struct list_head batteries; + struct mutex led_lock; /* Protects writes to the lightbar registers */ + struct led_classdev_mc led_mc_cdev; + struct mc_subled led_mc_subled_info[LED_CHANNELS]; + struct mutex input_lock; /* Protects input sequence during notify */ + struct input_dev *input_device; + struct notifier_block nb; + struct mutex usb_c_power_priority_lock; /* Protects dependent bit write and state safe */ + enum usb_c_power_priority_options last_usb_c_power_priority_option; +}; + +struct uniwill_battery_entry { + struct list_head head; + struct power_supply *battery; +}; + +struct uniwill_device_descriptor { + unsigned int features; + /* Executed during driver probing */ + int (*probe)(struct uniwill_data *data); +}; + +static bool force; +module_param_unsafe(force, bool, 0); +MODULE_PARM_DESC(force, "Force loading without checking for supported devices\n"); + +/* + * Contains device specific data like the feature bitmap since + * the associated registers are not always reliable. + */ +static struct uniwill_device_descriptor device_descriptor __ro_after_init; + +static const char * const uniwill_temp_labels[] = { + "CPU", + "GPU", +}; + +static const char * const uniwill_fan_labels[] = { + "Main", + "Secondary", +}; + +static const struct key_entry uniwill_keymap[] = { + /* Reported via keyboard controller */ + { KE_IGNORE, UNIWILL_OSD_CAPSLOCK, { KEY_CAPSLOCK }}, + { KE_IGNORE, UNIWILL_OSD_NUMLOCK, { KEY_NUMLOCK }}, + + /* + * Reported when the user enables/disables the super key. + * Those events might even be reported when the change was done + * using the sysfs attribute! + */ + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_DISABLE, { KEY_UNKNOWN }}, + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_ENABLE, { KEY_UNKNOWN }}, + /* Optional, might not be reported by all devices */ + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_STATE_CHANGED, { KEY_UNKNOWN }}, + + /* Reported in manual mode when toggling the airplane mode status */ + { KE_KEY, UNIWILL_OSD_RFKILL, { KEY_RFKILL }}, + { KE_IGNORE, UNIWILL_OSD_RADIOON, { KEY_UNKNOWN }}, + { KE_IGNORE, UNIWILL_OSD_RADIOOFF, { KEY_UNKNOWN }}, + + /* Reported when user wants to cycle the platform profile */ + { KE_KEY, UNIWILL_OSD_PERFORMANCE_MODE_TOGGLE, { KEY_F14 }}, + + /* Reported when the user wants to adjust the brightness of the keyboard */ + { KE_KEY, UNIWILL_OSD_KBDILLUMDOWN, { KEY_KBDILLUMDOWN }}, + { KE_KEY, UNIWILL_OSD_KBDILLUMUP, { KEY_KBDILLUMUP }}, + + /* Reported when the user wants to toggle the microphone mute status */ + { KE_KEY, UNIWILL_OSD_MIC_MUTE, { KEY_MICMUTE }}, + + /* Reported when the user wants to toggle the mute status */ + { KE_IGNORE, UNIWILL_OSD_MUTE, { KEY_MUTE }}, + + /* Reported when the user wants to toggle the brightness of the keyboard */ + { KE_KEY, UNIWILL_OSD_KBDILLUMTOGGLE, { KEY_KBDILLUMTOGGLE }}, + { KE_KEY, UNIWILL_OSD_KB_LED_LEVEL0, { KEY_KBDILLUMTOGGLE }}, + { KE_KEY, UNIWILL_OSD_KB_LED_LEVEL1, { KEY_KBDILLUMTOGGLE }}, + { KE_KEY, UNIWILL_OSD_KB_LED_LEVEL2, { KEY_KBDILLUMTOGGLE }}, + { KE_KEY, UNIWILL_OSD_KB_LED_LEVEL3, { KEY_KBDILLUMTOGGLE }}, + { KE_KEY, UNIWILL_OSD_KB_LED_LEVEL4, { KEY_KBDILLUMTOGGLE }}, + + /* FIXME: find out the exact meaning of those events */ + { KE_IGNORE, UNIWILL_OSD_BAT_CHARGE_FULL_24_H, { KEY_UNKNOWN }}, + { KE_IGNORE, UNIWILL_OSD_BAT_ERM_UPDATE, { KEY_UNKNOWN }}, + + /* Reported when the user wants to toggle the benchmark mode status */ + { KE_IGNORE, UNIWILL_OSD_BENCHMARK_MODE_TOGGLE, { KEY_UNKNOWN }}, + + /* Reported when the user wants to toggle the webcam */ + { KE_IGNORE, UNIWILL_OSD_WEBCAM_TOGGLE, { KEY_UNKNOWN }}, + + { KE_END } +}; + +static inline bool uniwill_device_supports(const struct uniwill_data *data, + unsigned int features) +{ + return (data->features & features) == features; +} + +static inline bool uniwill_device_supports_any(const struct uniwill_data *data, + unsigned int features) +{ + return data->features & features; +} + +static int uniwill_ec_reg_write(void *context, unsigned int reg, unsigned int val) +{ + union acpi_object params[2] = { + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = reg, + }, + }, + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = val, + }, + }, + }; + struct uniwill_data *data = context; + struct acpi_object_list input = { + .count = ARRAY_SIZE(params), + .pointer = params, + }; + acpi_status status; + + status = acpi_evaluate_object(data->handle, "ECRW", &input, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2); + + return 0; +} + +static int uniwill_ec_reg_read(void *context, unsigned int reg, unsigned int *val) +{ + union acpi_object params[1] = { + { + .integer = { + .type = ACPI_TYPE_INTEGER, + .value = reg, + }, + }, + }; + struct uniwill_data *data = context; + struct acpi_object_list input = { + .count = ARRAY_SIZE(params), + .pointer = params, + }; + unsigned long long output; + acpi_status status; + + status = acpi_evaluate_integer(data->handle, "ECRR", &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + if (output > U8_MAX) + return -ENXIO; + + usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2); + + *val = output; + + return 0; +} + +static const struct regmap_bus uniwill_ec_bus = { + .reg_write = uniwill_ec_reg_write, + .reg_read = uniwill_ec_reg_read, + .reg_format_endian_default = REGMAP_ENDIAN_LITTLE, + .val_format_endian_default = REGMAP_ENDIAN_LITTLE, +}; + +static bool uniwill_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case EC_ADDR_AP_OEM: + case EC_ADDR_LIGHTBAR_AC_CTRL: + case EC_ADDR_LIGHTBAR_AC_RED: + case EC_ADDR_LIGHTBAR_AC_GREEN: + case EC_ADDR_LIGHTBAR_AC_BLUE: + case EC_ADDR_BIOS_OEM: + case EC_ADDR_TRIGGER: + case EC_ADDR_OEM_4: + case EC_ADDR_CHARGE_CTRL: + case EC_ADDR_LIGHTBAR_BAT_CTRL: + case EC_ADDR_LIGHTBAR_BAT_RED: + case EC_ADDR_LIGHTBAR_BAT_GREEN: + case EC_ADDR_LIGHTBAR_BAT_BLUE: + case EC_ADDR_CTGP_DB_CTRL: + case EC_ADDR_CTGP_DB_CTGP_OFFSET: + case EC_ADDR_CTGP_DB_TPP_OFFSET: + case EC_ADDR_CTGP_DB_DB_OFFSET: + case EC_ADDR_USB_C_POWER_PRIORITY: + return true; + default: + return false; + } +} + +static bool uniwill_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case EC_ADDR_CPU_TEMP: + case EC_ADDR_GPU_TEMP: + case EC_ADDR_MAIN_FAN_RPM_1: + case EC_ADDR_MAIN_FAN_RPM_2: + case EC_ADDR_SECOND_FAN_RPM_1: + case EC_ADDR_SECOND_FAN_RPM_2: + case EC_ADDR_BAT_ALERT: + case EC_ADDR_PROJECT_ID: + case EC_ADDR_AP_OEM: + case EC_ADDR_LIGHTBAR_AC_CTRL: + case EC_ADDR_LIGHTBAR_AC_RED: + case EC_ADDR_LIGHTBAR_AC_GREEN: + case EC_ADDR_LIGHTBAR_AC_BLUE: + case EC_ADDR_BIOS_OEM: + case EC_ADDR_PWM_1: + case EC_ADDR_PWM_2: + case EC_ADDR_TRIGGER: + case EC_ADDR_SWITCH_STATUS: + case EC_ADDR_OEM_4: + case EC_ADDR_CHARGE_CTRL: + case EC_ADDR_LIGHTBAR_BAT_CTRL: + case EC_ADDR_LIGHTBAR_BAT_RED: + case EC_ADDR_LIGHTBAR_BAT_GREEN: + case EC_ADDR_LIGHTBAR_BAT_BLUE: + case EC_ADDR_SYSTEM_ID: + case EC_ADDR_CTGP_DB_CTRL: + case EC_ADDR_CTGP_DB_CTGP_OFFSET: + case EC_ADDR_CTGP_DB_TPP_OFFSET: + case EC_ADDR_CTGP_DB_DB_OFFSET: + case EC_ADDR_USB_C_POWER_PRIORITY: + return true; + default: + return false; + } +} + +static bool uniwill_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case EC_ADDR_CPU_TEMP: + case EC_ADDR_GPU_TEMP: + case EC_ADDR_MAIN_FAN_RPM_1: + case EC_ADDR_MAIN_FAN_RPM_2: + case EC_ADDR_SECOND_FAN_RPM_1: + case EC_ADDR_SECOND_FAN_RPM_2: + case EC_ADDR_BAT_ALERT: + case EC_ADDR_BIOS_OEM: + case EC_ADDR_PWM_1: + case EC_ADDR_PWM_2: + case EC_ADDR_TRIGGER: + case EC_ADDR_SWITCH_STATUS: + case EC_ADDR_OEM_4: + case EC_ADDR_CHARGE_CTRL: + case EC_ADDR_USB_C_POWER_PRIORITY: + return true; + default: + return false; + } +} + +static const struct regmap_config uniwill_ec_config = { + .reg_bits = 16, + .val_bits = 8, + .writeable_reg = uniwill_writeable_reg, + .readable_reg = uniwill_readable_reg, + .volatile_reg = uniwill_volatile_reg, + .can_sleep = true, + .max_register = 0xFFF, + .cache_type = REGCACHE_MAPLE, + .use_single_read = true, + .use_single_write = true, +}; + +static int uniwill_write_fn_lock(struct uniwill_data *data, bool status) +{ + unsigned int value; + + if (status) + value = FN_LOCK_STATUS; + else + value = 0; + + return regmap_update_bits(data->regmap, EC_ADDR_BIOS_OEM, FN_LOCK_STATUS, value); +} + +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + ret = uniwill_write_fn_lock(data, enable); + if (ret < 0) + return ret; + + return count; +} + +static int uniwill_read_fn_lock(struct uniwill_data *data, bool *status) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_BIOS_OEM, &value); + if (ret < 0) + return ret; + + *status = !!(value & FN_LOCK_STATUS); + + return 0; +} + +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool status; + int ret; + + ret = uniwill_read_fn_lock(data, &status); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", status); +} + +static DEVICE_ATTR_RW(fn_lock); + +static int uniwill_write_super_key_enable(struct uniwill_data *data, bool status) +{ + unsigned int value; + int ret; + + guard(mutex)(&data->super_key_lock); + + ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value); + if (ret < 0) + return ret; + + /* + * We can only toggle the super key lock, so we return early if the setting + * is already in the correct state. + */ + if (status == !(value & SUPER_KEY_LOCK_STATUS)) + return 0; + + return regmap_write_bits(data->regmap, EC_ADDR_TRIGGER, TRIGGER_SUPER_KEY_LOCK, + TRIGGER_SUPER_KEY_LOCK); +} + +static ssize_t super_key_enable_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + ret = uniwill_write_super_key_enable(data, enable); + if (ret < 0) + return ret; + + return count; +} + +static int uniwill_read_super_key_enable(struct uniwill_data *data, bool *status) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value); + if (ret < 0) + return ret; + + *status = !(value & SUPER_KEY_LOCK_STATUS); + + return 0; +} + +static ssize_t super_key_enable_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool status; + int ret; + + ret = uniwill_read_super_key_enable(data, &status); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", status); +} + +static DEVICE_ATTR_RW(super_key_enable); + +static int uniwill_write_touchpad_toggle_enable(struct uniwill_data *data, bool status) +{ + unsigned int value; + + if (status) + value = 0; + else + value = TOUCHPAD_TOGGLE_OFF; + + return regmap_update_bits(data->regmap, EC_ADDR_OEM_4, TOUCHPAD_TOGGLE_OFF, value); +} + +static ssize_t touchpad_toggle_enable_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + ret = uniwill_write_touchpad_toggle_enable(data, enable); + if (ret < 0) + return ret; + + return count; +} + +static int uniwill_read_touchpad_toggle_enable(struct uniwill_data *data, bool *status) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_OEM_4, &value); + if (ret < 0) + return ret; + + *status = !(value & TOUCHPAD_TOGGLE_OFF); + + return 0; +} + +static ssize_t touchpad_toggle_enable_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + bool status; + int ret; + + ret = uniwill_read_touchpad_toggle_enable(data, &status); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", status); +} + +static DEVICE_ATTR_RW(touchpad_toggle_enable); + +static ssize_t rainbow_animation_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + if (enable) + value = LIGHTBAR_WELCOME; + else + value = 0; + + guard(mutex)(&data->led_lock); + + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_WELCOME, value); + if (ret < 0) + return ret; + + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_WELCOME, value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t rainbow_animation_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", !!(value & LIGHTBAR_WELCOME)); +} + +static DEVICE_ATTR_RW(rainbow_animation); + +static ssize_t breathing_in_suspend_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + if (enable) + value = 0; + else + value = LIGHTBAR_S3_OFF; + + /* We only access a single register here, so we do not need to use data->led_lock */ + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S3_OFF, value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t breathing_in_suspend_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%d\n", !(value & LIGHTBAR_S3_OFF)); +} + +static DEVICE_ATTR_RW(breathing_in_suspend); + +static ssize_t ctgp_offset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + int ret; + + ret = kstrtouint(buf, 0, &value); + if (ret < 0) + return ret; + + if (value > U8_MAX) + return -EINVAL; + + ret = regmap_write(data->regmap, EC_ADDR_CTGP_DB_CTGP_OFFSET, value); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ctgp_offset_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_CTGP_DB_CTGP_OFFSET, &value); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%u\n", value); +} + +static DEVICE_ATTR_RW(ctgp_offset); + +static int uniwill_nvidia_ctgp_init(struct uniwill_data *data) +{ + int ret; + + if (!uniwill_device_supports(data, UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL)) + return 0; + + ret = regmap_write(data->regmap, EC_ADDR_CTGP_DB_CTGP_OFFSET, 0); + if (ret < 0) + return ret; + + ret = regmap_write(data->regmap, EC_ADDR_CTGP_DB_TPP_OFFSET, 255); + if (ret < 0) + return ret; + + ret = regmap_write(data->regmap, EC_ADDR_CTGP_DB_DB_OFFSET, 25); + if (ret < 0) + return ret; + + ret = regmap_set_bits(data->regmap, EC_ADDR_CTGP_DB_CTRL, + CTGP_DB_GENERAL_ENABLE | CTGP_DB_DB_ENABLE | CTGP_DB_CTGP_ENABLE); + if (ret < 0) + return ret; + + return 0; +} + +static const char * const usb_c_power_priority_text[] = { + [USB_C_POWER_PRIORITY_CHARGING] = "charging", + [USB_C_POWER_PRIORITY_PERFORMANCE] = "performance", +}; + +static const u8 usb_c_power_priority_value[] = { + [USB_C_POWER_PRIORITY_CHARGING] = 0, + [USB_C_POWER_PRIORITY_PERFORMANCE] = USB_C_POWER_PRIORITY, +}; + +static ssize_t usb_c_power_priority_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + enum usb_c_power_priority_options option; + unsigned int value; + int ret; + + ret = sysfs_match_string(usb_c_power_priority_text, buf); + if (ret < 0) + return ret; + + option = ret; + value = usb_c_power_priority_value[option]; + + guard(mutex)(&data->usb_c_power_priority_lock); + + ret = regmap_update_bits(data->regmap, EC_ADDR_USB_C_POWER_PRIORITY, + USB_C_POWER_PRIORITY, value); + if (ret < 0) + return ret; + + data->last_usb_c_power_priority_option = option; + + return count; +} + +static ssize_t usb_c_power_priority_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_USB_C_POWER_PRIORITY, &value); + if (ret < 0) + return ret; + + value &= USB_C_POWER_PRIORITY; + + if (usb_c_power_priority_value[USB_C_POWER_PRIORITY_PERFORMANCE] == value) + return sysfs_emit(buf, "%s\n", + usb_c_power_priority_text[USB_C_POWER_PRIORITY_PERFORMANCE]); + + return sysfs_emit(buf, "%s\n", usb_c_power_priority_text[USB_C_POWER_PRIORITY_CHARGING]); +} + +static DEVICE_ATTR_RW(usb_c_power_priority); + +static int usb_c_power_priority_restore(struct uniwill_data *data) +{ + unsigned int value; + + value = usb_c_power_priority_value[data->last_usb_c_power_priority_option]; + + guard(mutex)(&data->usb_c_power_priority_lock); + + return regmap_update_bits(data->regmap, EC_ADDR_USB_C_POWER_PRIORITY, + USB_C_POWER_PRIORITY, value); +} + +static int usb_c_power_priority_init(struct uniwill_data *data) +{ + unsigned int value; + int ret; + + if (!uniwill_device_supports(data, UNIWILL_FEATURE_USB_C_POWER_PRIORITY)) + return 0; + + ret = devm_mutex_init(data->dev, &data->usb_c_power_priority_lock); + if (ret < 0) + return ret; + + ret = regmap_read(data->regmap, EC_ADDR_USB_C_POWER_PRIORITY, &value); + if (ret < 0) + return ret; + + value &= USB_C_POWER_PRIORITY; + + data->last_usb_c_power_priority_option = + usb_c_power_priority_value[USB_C_POWER_PRIORITY_PERFORMANCE] == value ? + USB_C_POWER_PRIORITY_PERFORMANCE : + USB_C_POWER_PRIORITY_CHARGING; + + return 0; +} + +static struct attribute *uniwill_attrs[] = { + /* Keyboard-related */ + &dev_attr_fn_lock.attr, + &dev_attr_super_key_enable.attr, + &dev_attr_touchpad_toggle_enable.attr, + /* Lightbar-related */ + &dev_attr_rainbow_animation.attr, + &dev_attr_breathing_in_suspend.attr, + /* Power-management-related */ + &dev_attr_ctgp_offset.attr, + &dev_attr_usb_c_power_priority.attr, + NULL +}; + +static umode_t uniwill_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct uniwill_data *data = dev_get_drvdata(dev); + + if (attr == &dev_attr_fn_lock.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_FN_LOCK)) + return attr->mode; + } + + if (attr == &dev_attr_super_key_enable.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_SUPER_KEY)) + return attr->mode; + } + + if (attr == &dev_attr_touchpad_toggle_enable.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_TOUCHPAD_TOGGLE)) + return attr->mode; + } + + if (attr == &dev_attr_rainbow_animation.attr || + attr == &dev_attr_breathing_in_suspend.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_LIGHTBAR)) + return attr->mode; + } + + if (attr == &dev_attr_ctgp_offset.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL)) + return attr->mode; + } + + if (attr == &dev_attr_usb_c_power_priority.attr) { + if (uniwill_device_supports(data, UNIWILL_FEATURE_USB_C_POWER_PRIORITY)) + return attr->mode; + } + + return 0; +} + +static const struct attribute_group uniwill_group = { + .is_visible = uniwill_attr_is_visible, + .attrs = uniwill_attrs, +}; + +static const struct attribute_group *uniwill_groups[] = { + &uniwill_group, + NULL +}; + +static umode_t uniwill_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, + int channel) +{ + const struct uniwill_data *data = drvdata; + unsigned int feature; + + switch (type) { + case hwmon_temp: + switch (channel) { + case 0: + feature = UNIWILL_FEATURE_CPU_TEMP; + break; + case 1: + feature = UNIWILL_FEATURE_GPU_TEMP; + break; + default: + return 0; + } + break; + case hwmon_fan: + case hwmon_pwm: + switch (channel) { + case 0: + feature = UNIWILL_FEATURE_PRIMARY_FAN; + break; + case 1: + feature = UNIWILL_FEATURE_SECONDARY_FAN; + break; + default: + return 0; + } + break; + default: + return 0; + } + + if (uniwill_device_supports(data, feature)) + return 0444; + + return 0; +} + +static int uniwill_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, + long *val) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + unsigned int value; + __be16 rpm; + int ret; + + switch (type) { + case hwmon_temp: + switch (channel) { + case 0: + ret = regmap_read(data->regmap, EC_ADDR_CPU_TEMP, &value); + break; + case 1: + ret = regmap_read(data->regmap, EC_ADDR_GPU_TEMP, &value); + break; + default: + return -EOPNOTSUPP; + } + + if (ret < 0) + return ret; + + *val = value * MILLIDEGREE_PER_DEGREE; + return 0; + case hwmon_fan: + switch (channel) { + case 0: + ret = regmap_bulk_read(data->regmap, EC_ADDR_MAIN_FAN_RPM_1, &rpm, + sizeof(rpm)); + break; + case 1: + ret = regmap_bulk_read(data->regmap, EC_ADDR_SECOND_FAN_RPM_1, &rpm, + sizeof(rpm)); + break; + default: + return -EOPNOTSUPP; + } + + if (ret < 0) + return ret; + + *val = be16_to_cpu(rpm); + return 0; + case hwmon_pwm: + switch (channel) { + case 0: + ret = regmap_read(data->regmap, EC_ADDR_PWM_1, &value); + break; + case 1: + ret = regmap_read(data->regmap, EC_ADDR_PWM_2, &value); + break; + default: + return -EOPNOTSUPP; + } + + if (ret < 0) + return ret; + + *val = fixp_linear_interpolate(0, 0, PWM_MAX, U8_MAX, value); + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int uniwill_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + switch (type) { + case hwmon_temp: + *str = uniwill_temp_labels[channel]; + return 0; + case hwmon_fan: + *str = uniwill_fan_labels[channel]; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_ops uniwill_ops = { + .is_visible = uniwill_is_visible, + .read = uniwill_read, + .read_string = uniwill_read_string, +}; + +static const struct hwmon_channel_info * const uniwill_info[] = { + HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT, + HWMON_PWM_INPUT), + NULL +}; + +static const struct hwmon_chip_info uniwill_chip_info = { + .ops = &uniwill_ops, + .info = uniwill_info, +}; + +static int uniwill_hwmon_init(struct uniwill_data *data) +{ + struct device *hdev; + + if (!uniwill_device_supports(data, UNIWILL_FEATURE_CPU_TEMP) && + !uniwill_device_supports(data, UNIWILL_FEATURE_GPU_TEMP) && + !uniwill_device_supports(data, UNIWILL_FEATURE_PRIMARY_FAN) && + !uniwill_device_supports(data, UNIWILL_FEATURE_SECONDARY_FAN)) + return 0; + + hdev = devm_hwmon_device_register_with_info(data->dev, "uniwill", data, + &uniwill_chip_info, NULL); + + return PTR_ERR_OR_ZERO(hdev); +} + +static const unsigned int uniwill_led_channel_to_bat_reg[LED_CHANNELS] = { + EC_ADDR_LIGHTBAR_BAT_RED, + EC_ADDR_LIGHTBAR_BAT_GREEN, + EC_ADDR_LIGHTBAR_BAT_BLUE, +}; + +static const unsigned int uniwill_led_channel_to_ac_reg[LED_CHANNELS] = { + EC_ADDR_LIGHTBAR_AC_RED, + EC_ADDR_LIGHTBAR_AC_GREEN, + EC_ADDR_LIGHTBAR_AC_BLUE, +}; + +static int uniwill_led_brightness_set(struct led_classdev *led_cdev, enum led_brightness brightness) +{ + struct led_classdev_mc *led_mc_cdev = lcdev_to_mccdev(led_cdev); + struct uniwill_data *data = container_of(led_mc_cdev, struct uniwill_data, led_mc_cdev); + unsigned int value; + int ret; + + ret = led_mc_calc_color_components(led_mc_cdev, brightness); + if (ret < 0) + return ret; + + guard(mutex)(&data->led_lock); + + for (int i = 0; i < LED_CHANNELS; i++) { + /* Prevent the brightness values from overflowing */ + value = min(LED_MAX_BRIGHTNESS, data->led_mc_subled_info[i].brightness); + ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value); + if (ret < 0) + return ret; + + ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value); + if (ret < 0) + return ret; + } + + if (brightness) + value = 0; + else + value = LIGHTBAR_S0_OFF; + + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S0_OFF, value); + if (ret < 0) + return ret; + + return regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_S0_OFF, value); +} + +#define LIGHTBAR_MASK (LIGHTBAR_APP_EXISTS | LIGHTBAR_S0_OFF | LIGHTBAR_S3_OFF | LIGHTBAR_WELCOME) + +static int uniwill_led_init(struct uniwill_data *data) +{ + struct led_init_data init_data = { + .devicename = DRIVER_NAME, + .default_label = "multicolor:" LED_FUNCTION_STATUS, + .devname_mandatory = true, + }; + unsigned int color_indices[3] = { + LED_COLOR_ID_RED, + LED_COLOR_ID_GREEN, + LED_COLOR_ID_BLUE, + }; + unsigned int value; + int ret; + + if (!uniwill_device_supports(data, UNIWILL_FEATURE_LIGHTBAR)) + return 0; + + ret = devm_mutex_init(data->dev, &data->led_lock); + if (ret < 0) + return ret; + + /* + * The EC has separate lightbar settings for AC and battery mode, + * so we have to ensure that both settings are the same. + */ + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); + if (ret < 0) + return ret; + + value |= LIGHTBAR_APP_EXISTS; + ret = regmap_write(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, value); + if (ret < 0) + return ret; + + /* + * The breathing animation during suspend is not supported when + * running on battery power. + */ + value |= LIGHTBAR_S3_OFF; + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_MASK, value); + if (ret < 0) + return ret; + + data->led_mc_cdev.led_cdev.color = LED_COLOR_ID_MULTI; + data->led_mc_cdev.led_cdev.max_brightness = LED_MAX_BRIGHTNESS; + data->led_mc_cdev.led_cdev.flags = LED_REJECT_NAME_CONFLICT; + data->led_mc_cdev.led_cdev.brightness_set_blocking = uniwill_led_brightness_set; + + if (value & LIGHTBAR_S0_OFF) + data->led_mc_cdev.led_cdev.brightness = 0; + else + data->led_mc_cdev.led_cdev.brightness = LED_MAX_BRIGHTNESS; + + for (int i = 0; i < LED_CHANNELS; i++) { + data->led_mc_subled_info[i].color_index = color_indices[i]; + + ret = regmap_read(data->regmap, uniwill_led_channel_to_ac_reg[i], &value); + if (ret < 0) + return ret; + + /* + * Make sure that the initial intensity value is not greater than + * the maximum brightness. + */ + value = min(LED_MAX_BRIGHTNESS, value); + ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value); + if (ret < 0) + return ret; + + ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value); + if (ret < 0) + return ret; + + data->led_mc_subled_info[i].intensity = value; + data->led_mc_subled_info[i].channel = i; + } + + data->led_mc_cdev.subled_info = data->led_mc_subled_info; + data->led_mc_cdev.num_colors = LED_CHANNELS; + + return devm_led_classdev_multicolor_register_ext(data->dev, &data->led_mc_cdev, + &init_data); +} + +static unsigned int uniwill_sanitize_battery_threshold(unsigned int value) +{ + /* 0 means "charging threshold not active" */ + if (!value) + return 100; + + /* Guard against invalid values */ + return min(value, 100); +} + +static int uniwill_read_charge_type(struct uniwill_data *data, enum power_supply_charge_type *type) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_OEM_4, &value); + if (ret < 0) + return ret; + + switch (FIELD_GET(CHARGING_PROFILE_MASK, value)) { + case CHARGING_PROFILE_HIGH_CAPACITY: + *type = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + return 0; + case CHARGING_PROFILE_BALANCED: + *type = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + return 0; + case CHARGING_PROFILE_STATIONARY: + *type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + return 0; + default: + return -EPROTO; + } +} + +static int uniwill_get_property(struct power_supply *psy, const struct power_supply_ext *ext, + void *drvdata, enum power_supply_property psp, + union power_supply_propval *val) +{ + struct uniwill_data *data = drvdata; + union power_supply_propval prop; + unsigned int regval; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPES: + /* + * We need to use the cached value here because the charging mode + * reported by the EC might temporarily change when a external power + * source has been connected. + */ + mutex_lock(&data->charge_type_lock); + val->intval = data->last_charge_type; + mutex_unlock(&data->charge_type_lock); + return 0; + case POWER_SUPPLY_PROP_HEALTH: + ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_PRESENT, &prop); + if (ret < 0) + return ret; + + if (!prop.intval) { + val->intval = POWER_SUPPLY_HEALTH_NO_BATTERY; + return 0; + } + + ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_STATUS, &prop); + if (ret < 0) + return ret; + + if (prop.intval == POWER_SUPPLY_STATUS_UNKNOWN) { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + return 0; + } + + ret = regmap_read(data->regmap, EC_ADDR_BAT_ALERT, ®val); + if (ret < 0) + return ret; + + if (regval) { + /* Charging issue */ + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return 0; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + ret = regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, ®val); + if (ret < 0) + return ret; + + regval = FIELD_GET(CHARGE_CTRL_MASK, regval); + val->intval = uniwill_sanitize_battery_threshold(regval); + return 0; + default: + return -EINVAL; + } +} + +static int uniwill_write_charge_type(struct uniwill_data *data, enum power_supply_charge_type type) +{ + unsigned int value; + + switch (type) { + case POWER_SUPPLY_CHARGE_TYPE_TRICKLE: + value = FIELD_PREP(CHARGING_PROFILE_MASK, CHARGING_PROFILE_STATIONARY); + break; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + value = FIELD_PREP(CHARGING_PROFILE_MASK, CHARGING_PROFILE_HIGH_CAPACITY); + break; + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + value = FIELD_PREP(CHARGING_PROFILE_MASK, CHARGING_PROFILE_BALANCED); + break; + default: + return -EINVAL; + } + + return regmap_update_bits(data->regmap, EC_ADDR_OEM_4, CHARGING_PROFILE_MASK, value); +} + +static int uniwill_restore_charge_type(struct uniwill_data *data) +{ + guard(mutex)(&data->charge_type_lock); + + return uniwill_write_charge_type(data, data->last_charge_type); +} + +static int uniwill_set_property(struct power_supply *psy, const struct power_supply_ext *ext, + void *drvdata, enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct uniwill_data *data = drvdata; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPES: + mutex_lock(&data->charge_type_lock); + + ret = uniwill_write_charge_type(data, val->intval); + if (ret >= 0) + data->last_charge_type = val->intval; + + mutex_unlock(&data->charge_type_lock); + + return ret; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval < 0 || val->intval > 100) + return -EINVAL; + + return regmap_update_bits(data->regmap, EC_ADDR_CHARGE_CTRL, CHARGE_CTRL_MASK, + max(val->intval, 1)); + default: + return -EINVAL; + } +} + +static int uniwill_property_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, void *drvdata, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPES: + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + return true; + default: + return false; + } +} + +static const enum power_supply_property uniwill_charge_limit_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const struct power_supply_ext uniwill_charge_limit_extension = { + .name = DRIVER_NAME, + .properties = uniwill_charge_limit_properties, + .num_properties = ARRAY_SIZE(uniwill_charge_limit_properties), + .get_property = uniwill_get_property, + .set_property = uniwill_set_property, + .property_is_writeable = uniwill_property_is_writeable, +}; + +static const enum power_supply_property uniwill_charge_modes_properties[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, + POWER_SUPPLY_PROP_HEALTH, +}; + +static const struct power_supply_ext uniwill_charge_modes_extension = { + .name = DRIVER_NAME, + .charge_types = BIT(POWER_SUPPLY_CHARGE_TYPE_TRICKLE) | + BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE), + .properties = uniwill_charge_modes_properties, + .num_properties = ARRAY_SIZE(uniwill_charge_modes_properties), + .get_property = uniwill_get_property, + .set_property = uniwill_set_property, + .property_is_writeable = uniwill_property_is_writeable, +}; + +static int uniwill_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct uniwill_data *data = container_of(hook, struct uniwill_data, hook); + struct uniwill_battery_entry *entry; + int ret; + + entry = kzalloc_obj(*entry); + if (!entry) + return -ENOMEM; + + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT)) + ret = power_supply_register_extension(battery, &uniwill_charge_limit_extension, + data->dev, data); + else + ret = power_supply_register_extension(battery, &uniwill_charge_modes_extension, + data->dev, data); + + if (ret < 0) { + kfree(entry); + return ret; + } + + guard(mutex)(&data->battery_lock); + + entry->battery = battery; + list_add(&entry->head, &data->batteries); + + return 0; +} + +static int uniwill_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct uniwill_data *data = container_of(hook, struct uniwill_data, hook); + struct uniwill_battery_entry *entry, *tmp; + + scoped_guard(mutex, &data->battery_lock) { + list_for_each_entry_safe(entry, tmp, &data->batteries, head) { + if (entry->battery == battery) { + list_del(&entry->head); + kfree(entry); + break; + } + } + } + + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT)) + power_supply_unregister_extension(battery, &uniwill_charge_limit_extension); + else + power_supply_unregister_extension(battery, &uniwill_charge_modes_extension); + + return 0; +} + +static int uniwill_battery_init(struct uniwill_data *data) +{ + unsigned int value, threshold, sanitized; + int ret; + + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT)) { + ret = regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, &value); + if (ret < 0) + return ret; + + /* + * The charge control threshold might be initialized with 0 by + * the EC to signal that said threshold is uninitialized. We thus + * need to replace this placeholder value with a valid one (100) + * to signal that we want to take control of battery charging. + * For the sake of completeness we also apply this to other + * invalid threshold values. + */ + threshold = FIELD_GET(CHARGE_CTRL_MASK, value); + sanitized = uniwill_sanitize_battery_threshold(threshold); + if (threshold != sanitized) { + FIELD_MODIFY(CHARGE_CTRL_MASK, &value, sanitized); + ret = regmap_write(data->regmap, EC_ADDR_CHARGE_CTRL, value); + if (ret < 0) + return ret; + } + } else if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_MODES)) { + ret = devm_mutex_init(data->dev, &data->charge_type_lock); + if (ret < 0) + return ret; + + ret = uniwill_read_charge_type(data, &data->last_charge_type); + if (ret < 0) + return ret; + } else { + return 0; + } + + ret = devm_mutex_init(data->dev, &data->battery_lock); + if (ret < 0) + return ret; + + INIT_LIST_HEAD(&data->batteries); + data->hook.name = "Uniwill Battery Extension"; + data->hook.add_battery = uniwill_add_battery; + data->hook.remove_battery = uniwill_remove_battery; + + return devm_battery_hook_register(data->dev, &data->hook); +} + +static int uniwill_notifier_call(struct notifier_block *nb, unsigned long action, void *dummy) +{ + struct uniwill_data *data = container_of(nb, struct uniwill_data, nb); + struct uniwill_battery_entry *entry; + int ret; + + switch (action) { + case UNIWILL_OSD_BATTERY_ALERT: + if (!uniwill_device_supports_any(data, + UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES)) + return NOTIFY_DONE; + + mutex_lock(&data->battery_lock); + list_for_each_entry(entry, &data->batteries, head) { + power_supply_changed(entry->battery); + } + mutex_unlock(&data->battery_lock); + + return NOTIFY_OK; + case UNIWILL_OSD_DC_ADAPTER_CHANGED: + if (!uniwill_device_supports_any(data, + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY)) + return NOTIFY_DONE; + + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_MODES)) { + ret = uniwill_restore_charge_type(data); + if (ret < 0) + return notifier_from_errno(ret); + } + + if (uniwill_device_supports(data, UNIWILL_FEATURE_USB_C_POWER_PRIORITY)) { + ret = usb_c_power_priority_restore(data); + if (ret < 0) + return notifier_from_errno(ret); + } + + return NOTIFY_OK; + case UNIWILL_OSD_FN_LOCK: + if (!uniwill_device_supports(data, UNIWILL_FEATURE_FN_LOCK)) + return NOTIFY_DONE; + + sysfs_notify(&data->dev->kobj, NULL, "fn_lock"); + + return NOTIFY_OK; + default: + mutex_lock(&data->input_lock); + sparse_keymap_report_event(data->input_device, action, 1, true); + mutex_unlock(&data->input_lock); + + return NOTIFY_OK; + } +} + +static int uniwill_input_init(struct uniwill_data *data) +{ + int ret; + + ret = devm_mutex_init(data->dev, &data->input_lock); + if (ret < 0) + return ret; + + data->input_device = devm_input_allocate_device(data->dev); + if (!data->input_device) + return -ENOMEM; + + ret = sparse_keymap_setup(data->input_device, uniwill_keymap, NULL); + if (ret < 0) + return ret; + + data->input_device->name = "Uniwill WMI hotkeys"; + data->input_device->phys = "wmi/input0"; + data->input_device->id.bustype = BUS_HOST; + ret = input_register_device(data->input_device); + if (ret < 0) + return ret; + + data->nb.notifier_call = uniwill_notifier_call; + + return devm_uniwill_wmi_register_notifier(data->dev, &data->nb); +} + +static void uniwill_disable_manual_control(void *context) +{ + struct uniwill_data *data = context; + + regmap_clear_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL); +} + +static int uniwill_ec_init(struct uniwill_data *data) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_PROJECT_ID, &value); + if (ret < 0) + return ret; + + dev_dbg(data->dev, "Project ID: %u\n", value); + + ret = regmap_set_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(data->dev, uniwill_disable_manual_control, data); +} + +static int uniwill_probe(struct platform_device *pdev) +{ + struct uniwill_data *data; + struct regmap *regmap; + acpi_handle handle; + int ret; + + handle = ACPI_HANDLE(&pdev->dev); + if (!handle) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = &pdev->dev; + data->handle = handle; + platform_set_drvdata(pdev, data); + + regmap = devm_regmap_init(&pdev->dev, &uniwill_ec_bus, data, &uniwill_ec_config); + if (IS_ERR(regmap)) + return PTR_ERR(regmap); + + data->regmap = regmap; + + ret = devm_mutex_init(&pdev->dev, &data->super_key_lock); + if (ret < 0) + return ret; + + ret = uniwill_ec_init(data); + if (ret < 0) + return ret; + + data->features = device_descriptor.features; + + /* + * Some devices might need to perform some device-specific initialization steps + * before the supported features are initialized. Because of this we have to call + * this callback just after the EC itself was initialized. + */ + if (device_descriptor.probe) { + ret = device_descriptor.probe(data); + if (ret < 0) + return ret; + } + + ret = uniwill_battery_init(data); + if (ret < 0) + return ret; + + ret = uniwill_led_init(data); + if (ret < 0) + return ret; + + ret = uniwill_hwmon_init(data); + if (ret < 0) + return ret; + + ret = uniwill_nvidia_ctgp_init(data); + if (ret < 0) + return ret; + + ret = usb_c_power_priority_init(data); + if (ret < 0) + return ret; + + return uniwill_input_init(data); +} + +static void uniwill_shutdown(struct platform_device *pdev) +{ + struct uniwill_data *data = platform_get_drvdata(pdev); + + regmap_clear_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL); +} + +static int uniwill_suspend_fn_lock(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_FN_LOCK)) + return 0; + + /* + * EC_ADDR_BIOS_OEM is marked as volatile, so we have to restore it + * ourselves. + */ + return uniwill_read_fn_lock(data, &data->last_fn_lock_state); +} + +static int uniwill_suspend_super_key(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_SUPER_KEY)) + return 0; + + /* + * EC_ADDR_SWITCH_STATUS is marked as volatile, so we have to restore it + * ourselves. + */ + return uniwill_read_super_key_enable(data, &data->last_super_key_enable_state); +} + +static int uniwill_suspend_touchpad_toggle(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_TOUCHPAD_TOGGLE)) + return 0; + + /* + * EC_ADDR_OEM_4 is marked as volatile, so we have to restore it + * ourselves. + */ + return uniwill_read_touchpad_toggle_enable(data, &data->last_touchpad_toggle_enable_state); +} + +static int uniwill_suspend_battery(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT)) + return 0; + + /* + * Save the current charge limit in order to restore it during resume. + * We cannot use the regmap code for that since this register needs to + * be declared as volatile due to CHARGE_CTRL_REACHED. + */ + return regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, &data->last_charge_ctrl); +} + +static int uniwill_suspend_nvidia_ctgp(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL)) + return 0; + + return regmap_clear_bits(data->regmap, EC_ADDR_CTGP_DB_CTRL, + CTGP_DB_DB_ENABLE | CTGP_DB_CTGP_ENABLE); +} + +static int uniwill_suspend(struct device *dev) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + int ret; + + ret = uniwill_suspend_fn_lock(data); + if (ret < 0) + return ret; + + ret = uniwill_suspend_super_key(data); + if (ret < 0) + return ret; + + ret = uniwill_suspend_touchpad_toggle(data); + if (ret < 0) + return ret; + + ret = uniwill_suspend_battery(data); + if (ret < 0) + return ret; + + ret = uniwill_suspend_nvidia_ctgp(data); + if (ret < 0) + return ret; + + regcache_cache_only(data->regmap, true); + regcache_mark_dirty(data->regmap); + + return 0; +} + +static int uniwill_resume_fn_lock(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_FN_LOCK)) + return 0; + + return uniwill_write_fn_lock(data, data->last_fn_lock_state); +} + +static int uniwill_resume_super_key(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_SUPER_KEY)) + return 0; + + return uniwill_write_super_key_enable(data, data->last_super_key_enable_state); +} + +static int uniwill_resume_touchpad_toggle(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_TOUCHPAD_TOGGLE)) + return 0; + + return uniwill_write_touchpad_toggle_enable(data, data->last_touchpad_toggle_enable_state); +} + +static int uniwill_resume_battery(struct uniwill_data *data) +{ + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_MODES)) + return uniwill_restore_charge_type(data); + + if (uniwill_device_supports(data, UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT)) + return regmap_update_bits(data->regmap, EC_ADDR_CHARGE_CTRL, CHARGE_CTRL_MASK, + data->last_charge_ctrl); + + return 0; +} + +static int uniwill_resume_nvidia_ctgp(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL)) + return 0; + + return regmap_set_bits(data->regmap, EC_ADDR_CTGP_DB_CTRL, + CTGP_DB_DB_ENABLE | CTGP_DB_CTGP_ENABLE); +} + +static int uniwill_resume_usb_c_power_priority(struct uniwill_data *data) +{ + if (!uniwill_device_supports(data, UNIWILL_FEATURE_USB_C_POWER_PRIORITY)) + return 0; + + return usb_c_power_priority_restore(data); +} + +static int uniwill_resume(struct device *dev) +{ + struct uniwill_data *data = dev_get_drvdata(dev); + int ret; + + regcache_cache_only(data->regmap, false); + + ret = regcache_sync(data->regmap); + if (ret < 0) + return ret; + + ret = uniwill_resume_fn_lock(data); + if (ret < 0) + return ret; + + ret = uniwill_resume_super_key(data); + if (ret < 0) + return ret; + + ret = uniwill_resume_touchpad_toggle(data); + if (ret < 0) + return ret; + + ret = uniwill_resume_battery(data); + if (ret < 0) + return ret; + + ret = uniwill_resume_nvidia_ctgp(data); + if (ret < 0) + return ret; + + return uniwill_resume_usb_c_power_priority(data); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(uniwill_pm_ops, uniwill_suspend, uniwill_resume); + +/* + * We only use the DMI table for auoloading because the ACPI device itself + * does not guarantee that the underlying EC implementation is supported. + */ +static const struct acpi_device_id uniwill_id_table[] = { + { "INOU0000" }, + { }, +}; + +static struct platform_driver uniwill_driver = { + .driver = { + .name = DRIVER_NAME, + .dev_groups = uniwill_groups, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .acpi_match_table = uniwill_id_table, + .pm = pm_sleep_ptr(&uniwill_pm_ops), + }, + .probe = uniwill_probe, + .shutdown = uniwill_shutdown, +}; + +static struct uniwill_device_descriptor lapqc71a_lapqc71b_descriptor __initdata = { + .features = UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN, +}; + +static struct uniwill_device_descriptor lapac71h_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_TOUCHPAD_TOGGLE | + UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN, +}; + +static struct uniwill_device_descriptor lapkc71f_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_TOUCHPAD_TOGGLE | + UNIWILL_FEATURE_LIGHTBAR | + UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN, +}; + +/* + * The featuresets below reflect somewhat chronological changes: + * 1 -> 2: UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL is added to the EC firmware. + * 2 -> 3: UNIWILL_FEATURE_USB_C_POWER_PRIORITY is removed from the EC firmware. + * Some devices might divert from this timeline. + */ + +static struct uniwill_device_descriptor tux_featureset_1_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY, +}; + +static struct uniwill_device_descriptor tux_featureset_1_nvidia_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY, +}; + +static struct uniwill_device_descriptor tux_featureset_2_nvidia_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN | + UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY, +}; + +static struct uniwill_device_descriptor tux_featureset_3_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN, +}; + +static struct uniwill_device_descriptor tux_featureset_3_nvidia_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN | + UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL, +}; + +static int phxtxx1_probe(struct uniwill_data *data) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_PROJECT_ID, &value); + if (ret < 0) + return ret; + + if (value == PROJECT_ID_PH4TRX1 || value == PROJECT_ID_PH6TRX1) + data->features |= UNIWILL_FEATURE_SECONDARY_FAN; + + return 0; +}; + +static struct uniwill_device_descriptor phxtxx1_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY, + .probe = phxtxx1_probe, +}; + +static int phxarx1_phxaqf1_probe(struct uniwill_data *data) +{ + unsigned int value; + int ret; + + ret = regmap_read(data->regmap, EC_ADDR_SYSTEM_ID, &value); + if (ret < 0) + return ret; + + if (value & HAS_GPU) + data->features |= UNIWILL_FEATURE_GPU_TEMP | + UNIWILL_FEATURE_NVIDIA_CTGP_CONTROL; + + return 0; +}; + +static struct uniwill_device_descriptor phxarx1_phxaqf1_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_BATTERY_CHARGE_MODES | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN | + UNIWILL_FEATURE_SECONDARY_FAN | + UNIWILL_FEATURE_USB_C_POWER_PRIORITY, + .probe = phxarx1_phxaqf1_probe, +}; + +static struct uniwill_device_descriptor pf5pu1g_descriptor __initdata = { + .features = UNIWILL_FEATURE_FN_LOCK | + UNIWILL_FEATURE_SUPER_KEY | + UNIWILL_FEATURE_CPU_TEMP | + UNIWILL_FEATURE_PRIMARY_FAN, +}; + +static const struct dmi_system_id uniwill_dmi_table[] __initconst = { + { + .ident = "XMG FUSION 15 (L19)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SchenkerTechnologiesGmbH"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "LAPQC71A"), + }, + .driver_data = &lapqc71a_lapqc71b_descriptor, + }, + { + .ident = "XMG FUSION 15 (L19)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "SchenkerTechnologiesGmbH"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "LAPQC71B"), + }, + .driver_data = &lapqc71a_lapqc71b_descriptor, + }, + { + .ident = "XMG FUSION 15 (L19)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "LAPQC71A"), + }, + .driver_data = &lapqc71a_lapqc71b_descriptor, + }, + { + .ident = "XMG FUSION 15 (L19)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "LAPQC71B"), + }, + .driver_data = &lapqc71a_lapqc71b_descriptor, + }, + { + .ident = "Intel NUC x15", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel(R) Client Systems"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "LAPAC71H"), + }, + .driver_data = &lapac71h_descriptor, + }, + { + .ident = "Intel NUC x15", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel(R) Client Systems"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "LAPKC71F"), + }, + .driver_data = &lapkc71f_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14 Gen6 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PHxTxX1"), + }, + .driver_data = &phxtxx1_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14 Gen6 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PHxTQx1"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/16 Gen7 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PHxARX1_PHxAQF1"), + }, + .driver_data = &phxarx1_phxaqf1_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 16 Gen7 Intel/Commodore Omnia-Book Pro Gen 7", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PH6AG01_PH6AQ71_PH6AQI1"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/16 Gen8 Intel/Commodore Omnia-Book Pro Gen 8", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PH4PRX1_PH6PRX1"), + }, + .driver_data = &tux_featureset_1_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14 Gen8 Intel/Commodore Omnia-Book Pro Gen 8", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PH4PG31"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 16 Gen8 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PH6PG01_PH6PG71"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 Gen9 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GXxHRXx"), + }, + .driver_data = &tux_featureset_3_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 Gen9 Intel/Commodore Omnia-Book 15 Gen9", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GXxMRXx"), + }, + .driver_data = &tux_featureset_3_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "XxHP4NAx"), + }, + .driver_data = &tux_featureset_3_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 14/15 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "XxKK4NAx_XxSP4NAx"), + }, + .driver_data = &tux_featureset_3_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Pro 15 Gen10 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "XxAR4NAx"), + }, + .driver_data = &tux_featureset_3_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Max 15 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X5KK45xS_X5SP45xS"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Max 16 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6HP45xU"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Max 16 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6KK45xU_X6SP45xU"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Max 15 Gen10 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X5AR45xS"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO InfinityBook Max 16 Gen10 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6AR55xU"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1501A1650TI"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1501A2060"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 17 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1701A1650TI"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 17 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1701A2060"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15 Gen1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1501I1650TI"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15 Gen1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1501I2060"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 17 Gen1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1701I1650TI"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 17 Gen1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "POLARIS1701I2060"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Trinity 15 Intel Gen1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "TRINITY1501I"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Trinity 17 Intel Gen1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "TRINITY1701I"), + }, + .driver_data = &tux_featureset_1_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15/17 Gen2 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxMGxx"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15/17 Gen2 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxNGxx"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris/Polaris 15/17 Gen3 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxZGxx"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris/Polaris 15/17 Gen3 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxTGxx"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris/Polaris 15/17 Gen4 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxRGxx"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 15 Gen4 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxAGxx"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Polaris 15/17 Gen5 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxXGxx"), + }, + .driver_data = &tux_featureset_2_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen5 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GM6XGxX"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16/17 Gen5 Intel/Commodore ORION Gen 5", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxPXxx"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris Slim 15 Gen6 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GMxHGxx"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris Slim 15 Gen6 Intel/Commodore ORION Slim 15 Gen6", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GM5IXxA"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen6 Intel/Commodore ORION 16 Gen6", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GM6IXxB_MB1"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen6 Intel/Commodore ORION 16 Gen6", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GM6IXxB_MB2"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 17 Gen6 Intel/Commodore ORION 17 Gen6", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "GM7IXxN"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen7 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6FR5xxY"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen7 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6AR5xxY"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Stellaris 16 Gen7 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "X6AR5xxY_mLED"), + }, + .driver_data = &tux_featureset_3_nvidia_descriptor, + }, + { + .ident = "TUXEDO Book BA15 Gen10 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PF5PU1G"), + }, + .driver_data = &pf5pu1g_descriptor, + }, + { + .ident = "TUXEDO Pulse 14 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PULSE1401"), + }, + .driver_data = &tux_featureset_1_descriptor, + }, + { + .ident = "TUXEDO Pulse 15 Gen1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PULSE1501"), + }, + .driver_data = &tux_featureset_1_descriptor, + }, + { + .ident = "TUXEDO Pulse 15 Gen2 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "TUXEDO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "PF5LUXG"), + }, + .driver_data = &tux_featureset_1_descriptor, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, uniwill_dmi_table); + +static int __init uniwill_init(void) +{ + const struct uniwill_device_descriptor *descriptor; + const struct dmi_system_id *id; + int ret; + + id = dmi_first_match(uniwill_dmi_table); + if (!id) { + if (!force) + return -ENODEV; + + pr_warn("Loading on a potentially unsupported device\n"); + } else { + /* + * Some devices might support additional features depending on + * the BIOS version/date, so we call this callback to let them + * modify their device descriptor accordingly. + */ + if (id->callback) { + ret = id->callback(id); + if (ret < 0) + return ret; + } + + descriptor = id->driver_data; + device_descriptor = *descriptor; + } + + if (force) { + /* Assume that the device supports all features except the charge limit */ + device_descriptor.features = UINT_MAX & ~UNIWILL_FEATURE_BATTERY_CHARGE_LIMIT; + pr_warn("Enabling potentially unsupported features\n"); + } + + ret = platform_driver_register(&uniwill_driver); + if (ret < 0) + return ret; + + ret = uniwill_wmi_register_driver(); + if (ret < 0) { + platform_driver_unregister(&uniwill_driver); + return ret; + } + + return 0; +} +module_init(uniwill_init); + +static void __exit uniwill_exit(void) +{ + uniwill_wmi_unregister_driver(); + platform_driver_unregister(&uniwill_driver); +} +module_exit(uniwill_exit); + +MODULE_AUTHOR("Armin Wolf "); +MODULE_DESCRIPTION("Uniwill notebook driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/uniwill/uniwill-wmi.c b/drivers/platform/x86/uniwill/uniwill-wmi.c new file mode 100644 index 000000000..afcbfa4f7 --- /dev/null +++ b/drivers/platform/x86/uniwill/uniwill-wmi.c @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Linux hotkey driver for Uniwill notebooks. + * + * Special thanks go to Pőcze Barnabás, Christoffer Sandberg and Werner Sembach + * for supporting the development of this driver either through prior work or + * by answering questions regarding the underlying WMI interface. + * + * Copyright (C) 2025 Armin Wolf + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include + +#include "uniwill-wmi.h" + +#define DRIVER_NAME "uniwill-wmi" +#define UNIWILL_EVENT_GUID "ABBC0F72-8EA1-11D1-00A0-C90629100000" + +static BLOCKING_NOTIFIER_HEAD(uniwill_wmi_chain_head); + +static void devm_uniwill_wmi_unregister_notifier(void *data) +{ + struct notifier_block *nb = data; + + blocking_notifier_chain_unregister(&uniwill_wmi_chain_head, nb); +} + +int devm_uniwill_wmi_register_notifier(struct device *dev, struct notifier_block *nb) +{ + int ret; + + ret = blocking_notifier_chain_register(&uniwill_wmi_chain_head, nb); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(dev, devm_uniwill_wmi_unregister_notifier, nb); +} + +static void uniwill_wmi_notify(struct wmi_device *wdev, union acpi_object *obj) +{ + u32 value; + int ret; + + if (obj->type != ACPI_TYPE_INTEGER) + return; + + value = obj->integer.value; + + dev_dbg(&wdev->dev, "Received WMI event %u\n", value); + + ret = blocking_notifier_call_chain(&uniwill_wmi_chain_head, value, NULL); + if (notifier_to_errno(ret) < 0) + dev_err(&wdev->dev, "Failed to handle event %u\n", value); +} + +/* + * We cannot fully trust this GUID since Uniwill just copied the WMI GUID + * from the Windows driver example, and others probably did the same. + * + * Because of this we cannot use this WMI GUID for autoloading. Instead the + * associated driver will be registered manually after matching a DMI table. + */ +static const struct wmi_device_id uniwill_wmi_id_table[] = { + { UNIWILL_EVENT_GUID, NULL }, + { } +}; + +static struct wmi_driver uniwill_wmi_driver = { + .driver = { + .name = DRIVER_NAME, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = uniwill_wmi_id_table, + .min_event_size = sizeof(u32), + .notify = uniwill_wmi_notify, + .no_singleton = true, +}; + +int __init uniwill_wmi_register_driver(void) +{ + return wmi_driver_register(&uniwill_wmi_driver); +} + +void __exit uniwill_wmi_unregister_driver(void) +{ + wmi_driver_unregister(&uniwill_wmi_driver); +} diff --git a/drivers/platform/x86/uniwill/uniwill-wmi.h b/drivers/platform/x86/uniwill/uniwill-wmi.h new file mode 100644 index 000000000..fb1910c0f --- /dev/null +++ b/drivers/platform/x86/uniwill/uniwill-wmi.h @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Linux hotkey driver for Uniwill notebooks. + * + * Copyright (C) 2025 Armin Wolf + */ + +#ifndef UNIWILL_WMI_H +#define UNIWILL_WMI_H + +#include + +#define UNIWILL_OSD_CAPSLOCK 0x01 +#define UNIWILL_OSD_NUMLOCK 0x02 +#define UNIWILL_OSD_SCROLLLOCK 0x03 + +#define UNIWILL_OSD_TOUCHPAD_ON 0x04 +#define UNIWILL_OSD_TOUCHPAD_OFF 0x05 + +#define UNIWILL_OSD_SILENT_MODE_ON 0x06 +#define UNIWILL_OSD_SILENT_MODE_OFF 0x07 + +#define UNIWILL_OSD_WLAN_ON 0x08 +#define UNIWILL_OSD_WLAN_OFF 0x09 + +#define UNIWILL_OSD_WIMAX_ON 0x0A +#define UNIWILL_OSD_WIMAX_OFF 0x0B + +#define UNIWILL_OSD_BLUETOOTH_ON 0x0C +#define UNIWILL_OSD_BLUETOOTH_OFF 0x0D + +#define UNIWILL_OSD_RF_ON 0x0E +#define UNIWILL_OSD_RF_OFF 0x0F + +#define UNIWILL_OSD_3G_ON 0x10 +#define UNIWILL_OSD_3G_OFF 0x11 + +#define UNIWILL_OSD_WEBCAM_ON 0x12 +#define UNIWILL_OSD_WEBCAM_OFF 0x13 + +#define UNIWILL_OSD_BRIGHTNESSUP 0x14 +#define UNIWILL_OSD_BRIGHTNESSDOWN 0x15 + +#define UNIWILL_OSD_RADIOON 0x1A +#define UNIWILL_OSD_RADIOOFF 0x1B + +#define UNIWILL_OSD_POWERSAVE_ON 0x31 +#define UNIWILL_OSD_POWERSAVE_OFF 0x32 + +#define UNIWILL_OSD_MENU 0x34 + +#define UNIWILL_OSD_MUTE 0x35 +#define UNIWILL_OSD_VOLUMEDOWN 0x36 +#define UNIWILL_OSD_VOLUMEUP 0x37 + +#define UNIWILL_OSD_MENU_2 0x38 + +#define UNIWILL_OSD_LIGHTBAR_ON 0x39 +#define UNIWILL_OSD_LIGHTBAR_OFF 0x3A + +#define UNIWILL_OSD_KB_LED_LEVEL0 0x3B +#define UNIWILL_OSD_KB_LED_LEVEL1 0x3C +#define UNIWILL_OSD_KB_LED_LEVEL2 0x3D +#define UNIWILL_OSD_KB_LED_LEVEL3 0x3E +#define UNIWILL_OSD_KB_LED_LEVEL4 0x3F + +#define UNIWILL_OSD_SUPER_KEY_DISABLE 0x40 +#define UNIWILL_OSD_SUPER_KEY_ENABLE 0x41 + +#define UNIWILL_OSD_MENU_JP 0x42 + +#define UNIWILL_OSD_CAMERA_ON 0x90 +#define UNIWILL_OSD_CAMERA_OFF 0x91 + +#define UNIWILL_OSD_RFKILL 0xA4 + +#define UNIWILL_OSD_SUPER_KEY_STATE_CHANGED 0xA5 + +#define UNIWILL_OSD_LIGHTBAR_STATE_CHANGED 0xA6 + +#define UNIWILL_OSD_FAN_BOOST_STATE_CHANGED 0xA7 + +#define UNIWILL_OSD_LCD_SW 0xA9 + +#define UNIWILL_OSD_FAN_OVERTEMP 0xAA + +#define UNIWILL_OSD_DC_ADAPTER_CHANGED 0xAB + +#define UNIWILL_OSD_BAT_HP_OFF 0xAC + +#define UNIWILL_OSD_FAN_DOWN_TEMP 0xAD + +#define UNIWILL_OSD_BATTERY_ALERT 0xAE + +#define UNIWILL_OSD_TIMAP_HAIERLB_SW 0xAF + +#define UNIWILL_OSD_PERFORMANCE_MODE_TOGGLE 0xB0 + +#define UNIWILL_OSD_KBDILLUMDOWN 0xB1 +#define UNIWILL_OSD_KBDILLUMUP 0xB2 + +#define UNIWILL_OSD_BACKLIGHT_LEVEL_CHANGE 0xB3 +#define UNIWILL_OSD_BACKLIGHT_POWER_CHANGE 0xB4 + +#define UNIWILL_OSD_MIC_MUTE 0xB7 + +#define UNIWILL_OSD_FN_LOCK 0xB8 +#define UNIWILL_OSD_KBDILLUMTOGGLE 0xB9 + +#define UNIWILL_OSD_BAT_CHARGE_FULL_24_H 0xBE + +#define UNIWILL_OSD_BAT_ERM_UPDATE 0xBF + +#define UNIWILL_OSD_BENCHMARK_MODE_TOGGLE 0xC0 + +#define UNIWILL_OSD_WEBCAM_TOGGLE 0xCF + +#define UNIWILL_OSD_KBD_BACKLIGHT_CHANGED 0xF0 + +struct device; +struct notifier_block; + +int devm_uniwill_wmi_register_notifier(struct device *dev, struct notifier_block *nb); + +int __init uniwill_wmi_register_driver(void); + +void __exit uniwill_wmi_unregister_driver(void); + +#endif /* UNIWILL_WMI_H */ diff --git a/drivers/platform/x86/uv_sysfs.c b/drivers/platform/x86/uv_sysfs.c new file mode 100644 index 000000000..a8c50b323 --- /dev/null +++ b/drivers/platform/x86/uv_sysfs.c @@ -0,0 +1,931 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file supports the /sys/firmware/sgi_uv topology tree on HPE UV. + * + * Copyright (c) 2020 Hewlett Packard Enterprise. All Rights Reserved. + * Copyright (c) Justin Ernst + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define INVALID_CNODE -1 + +struct kobject *sgi_uv_kobj; +static struct kset *uv_pcibus_kset; +static struct kset *uv_hubs_kset; +static struct uv_bios_hub_info *hub_buf; +static struct uv_bios_port_info **port_buf; +static struct uv_hub **uv_hubs; +static struct uv_pci_top_obj **uv_pci_objs; +static int num_pci_lines; +static int num_cnodes; +static int *prev_obj_to_cnode; +static int uv_bios_obj_cnt; +static signed short uv_master_nasid = -1; +static void *uv_biosheap; + +static const char *uv_type_string(void) +{ + if (is_uv5_hub()) + return "9.0"; + else if (is_uv4a_hub()) + return "7.1"; + else if (is_uv4_hub()) + return "7.0"; + else if (is_uv3_hub()) + return "5.0"; + else if (is_uv2_hub()) + return "3.0"; + else if (uv_get_hubless_system()) + return "0.1"; + else + return "unknown"; +} + +static int ordinal_to_nasid(int ordinal) +{ + if (ordinal < num_cnodes && ordinal >= 0) + return UV_PNODE_TO_NASID(uv_blade_to_pnode(ordinal)); + else + return -1; +} + +static union geoid_u cnode_to_geoid(int cnode) +{ + union geoid_u geoid; + + uv_bios_get_geoinfo(ordinal_to_nasid(cnode), (u64)sizeof(union geoid_u), (u64 *)&geoid); + return geoid; +} + +static int location_to_bpos(char *location, int *rack, int *slot, int *blade) +{ + char type, r, b, h; + int idb, idh; + + if (sscanf(location, "%c%03d%c%02d%c%2d%c%d", + &r, rack, &type, slot, &b, &idb, &h, &idh) != 8) + return -1; + *blade = idb * 2 + idh; + + return 0; +} + +static int cache_obj_to_cnode(struct uv_bios_hub_info *obj) +{ + int cnode; + union geoid_u geoid; + int obj_rack, obj_slot, obj_blade; + int rack, slot, blade; + + if (!obj->f.fields.this_part && !obj->f.fields.is_shared) + return 0; + + if (location_to_bpos(obj->location, &obj_rack, &obj_slot, &obj_blade)) + return -1; + + for (cnode = 0; cnode < num_cnodes; cnode++) { + geoid = cnode_to_geoid(cnode); + rack = geo_rack(geoid); + slot = geo_slot(geoid); + blade = geo_blade(geoid); + if (obj_rack == rack && obj_slot == slot && obj_blade == blade) + prev_obj_to_cnode[obj->id] = cnode; + } + + return 0; +} + +static int get_obj_to_cnode(int obj_id) +{ + return prev_obj_to_cnode[obj_id]; +} + +struct uv_hub { + struct kobject kobj; + struct uv_bios_hub_info *hub_info; + struct uv_port **ports; +}; + +#define to_uv_hub(kobj_ptr) container_of(kobj_ptr, struct uv_hub, kobj) + +static ssize_t hub_name_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + return sysfs_emit(buf, "%s\n", hub_info->name); +} + +static ssize_t hub_location_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + return sysfs_emit(buf, "%s\n", hub_info->location); +} + +static ssize_t hub_partition_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + return sysfs_emit(buf, "%d\n", hub_info->f.fields.this_part); +} + +static ssize_t hub_shared_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + return sysfs_emit(buf, "%d\n", hub_info->f.fields.is_shared); +} +static ssize_t hub_nasid_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + int cnode = get_obj_to_cnode(hub_info->id); + + return sysfs_emit(buf, "%d\n", ordinal_to_nasid(cnode)); +} +static ssize_t hub_cnode_show(struct uv_bios_hub_info *hub_info, char *buf) +{ + return sysfs_emit(buf, "%d\n", get_obj_to_cnode(hub_info->id)); +} + +struct hub_sysfs_entry { + struct attribute attr; + ssize_t (*show)(struct uv_bios_hub_info *hub_info, char *buf); + ssize_t (*store)(struct uv_bios_hub_info *hub_info, const char *buf, size_t sz); +}; + +static struct hub_sysfs_entry name_attribute = + __ATTR(name, 0444, hub_name_show, NULL); +static struct hub_sysfs_entry location_attribute = + __ATTR(location, 0444, hub_location_show, NULL); +static struct hub_sysfs_entry partition_attribute = + __ATTR(this_partition, 0444, hub_partition_show, NULL); +static struct hub_sysfs_entry shared_attribute = + __ATTR(shared, 0444, hub_shared_show, NULL); +static struct hub_sysfs_entry nasid_attribute = + __ATTR(nasid, 0444, hub_nasid_show, NULL); +static struct hub_sysfs_entry cnode_attribute = + __ATTR(cnode, 0444, hub_cnode_show, NULL); + +static struct attribute *uv_hub_attrs[] = { + &name_attribute.attr, + &location_attribute.attr, + &partition_attribute.attr, + &shared_attribute.attr, + &nasid_attribute.attr, + &cnode_attribute.attr, + NULL, +}; +ATTRIBUTE_GROUPS(uv_hub); + +static void hub_release(struct kobject *kobj) +{ + struct uv_hub *hub = to_uv_hub(kobj); + + kfree(hub); +} + +static ssize_t hub_type_show(struct kobject *kobj, struct attribute *attr, + char *buf) +{ + struct uv_hub *hub = to_uv_hub(kobj); + struct uv_bios_hub_info *bios_hub_info = hub->hub_info; + struct hub_sysfs_entry *entry; + + entry = container_of(attr, struct hub_sysfs_entry, attr); + + if (!entry->show) + return -EIO; + + return entry->show(bios_hub_info, buf); +} + +static const struct sysfs_ops hub_sysfs_ops = { + .show = hub_type_show, +}; + +static const struct kobj_type hub_attr_type = { + .release = hub_release, + .sysfs_ops = &hub_sysfs_ops, + .default_groups = uv_hub_groups, +}; + +static int uv_hubs_init(void) +{ + s64 biosr; + u64 sz; + int i, ret; + + prev_obj_to_cnode = kmalloc_objs(*prev_obj_to_cnode, uv_bios_obj_cnt); + if (!prev_obj_to_cnode) + return -ENOMEM; + + for (i = 0; i < uv_bios_obj_cnt; i++) + prev_obj_to_cnode[i] = INVALID_CNODE; + + uv_hubs_kset = kset_create_and_add("hubs", NULL, sgi_uv_kobj); + if (!uv_hubs_kset) { + ret = -ENOMEM; + goto err_hubs_kset; + } + sz = uv_bios_obj_cnt * sizeof(*hub_buf); + hub_buf = kzalloc(sz, GFP_KERNEL); + if (!hub_buf) { + ret = -ENOMEM; + goto err_hub_buf; + } + + biosr = uv_bios_enum_objs((u64)uv_master_nasid, sz, (u64 *)hub_buf); + if (biosr) { + ret = -EINVAL; + goto err_enum_objs; + } + + uv_hubs = kzalloc_objs(*uv_hubs, uv_bios_obj_cnt); + if (!uv_hubs) { + ret = -ENOMEM; + goto err_enum_objs; + } + + for (i = 0; i < uv_bios_obj_cnt; i++) { + uv_hubs[i] = kzalloc_obj(*uv_hubs[i]); + if (!uv_hubs[i]) { + i--; + ret = -ENOMEM; + goto err_hubs; + } + + uv_hubs[i]->hub_info = &hub_buf[i]; + cache_obj_to_cnode(uv_hubs[i]->hub_info); + + uv_hubs[i]->kobj.kset = uv_hubs_kset; + + ret = kobject_init_and_add(&uv_hubs[i]->kobj, &hub_attr_type, + NULL, "hub_%u", hub_buf[i].id); + if (ret) + goto err_hubs; + kobject_uevent(&uv_hubs[i]->kobj, KOBJ_ADD); + } + return 0; + +err_hubs: + for (; i >= 0; i--) + kobject_put(&uv_hubs[i]->kobj); + kfree(uv_hubs); +err_enum_objs: + kfree(hub_buf); +err_hub_buf: + kset_unregister(uv_hubs_kset); +err_hubs_kset: + kfree(prev_obj_to_cnode); + return ret; + +} + +static void uv_hubs_exit(void) +{ + int i; + + for (i = 0; i < uv_bios_obj_cnt; i++) + kobject_put(&uv_hubs[i]->kobj); + + kfree(uv_hubs); + kfree(hub_buf); + kset_unregister(uv_hubs_kset); + kfree(prev_obj_to_cnode); +} + +struct uv_port { + struct kobject kobj; + struct uv_bios_port_info *port_info; +}; + +#define to_uv_port(kobj_ptr) container_of(kobj_ptr, struct uv_port, kobj) + +static ssize_t uv_port_conn_hub_show(struct uv_bios_port_info *port, char *buf) +{ + return sysfs_emit(buf, "%d\n", port->conn_id); +} + +static ssize_t uv_port_conn_port_show(struct uv_bios_port_info *port, char *buf) +{ + return sysfs_emit(buf, "%d\n", port->conn_port); +} + +struct uv_port_sysfs_entry { + struct attribute attr; + ssize_t (*show)(struct uv_bios_port_info *port_info, char *buf); + ssize_t (*store)(struct uv_bios_port_info *port_info, const char *buf, size_t size); +}; + +static struct uv_port_sysfs_entry uv_port_conn_hub_attribute = + __ATTR(conn_hub, 0444, uv_port_conn_hub_show, NULL); +static struct uv_port_sysfs_entry uv_port_conn_port_attribute = + __ATTR(conn_port, 0444, uv_port_conn_port_show, NULL); + +static struct attribute *uv_port_attrs[] = { + &uv_port_conn_hub_attribute.attr, + &uv_port_conn_port_attribute.attr, + NULL, +}; +ATTRIBUTE_GROUPS(uv_port); + +static void uv_port_release(struct kobject *kobj) +{ + struct uv_port *port = to_uv_port(kobj); + + kfree(port); +} + +static ssize_t uv_port_type_show(struct kobject *kobj, struct attribute *attr, + char *buf) +{ + struct uv_port *port = to_uv_port(kobj); + struct uv_bios_port_info *port_info = port->port_info; + struct uv_port_sysfs_entry *entry; + + entry = container_of(attr, struct uv_port_sysfs_entry, attr); + + if (!entry->show) + return -EIO; + + return entry->show(port_info, buf); +} + +static const struct sysfs_ops uv_port_sysfs_ops = { + .show = uv_port_type_show, +}; + +static const struct kobj_type uv_port_attr_type = { + .release = uv_port_release, + .sysfs_ops = &uv_port_sysfs_ops, + .default_groups = uv_port_groups, +}; + +static int uv_ports_init(void) +{ + s64 biosr; + int j = 0, k = 0, ret, sz; + + port_buf = kzalloc_objs(*port_buf, uv_bios_obj_cnt); + if (!port_buf) + return -ENOMEM; + + for (j = 0; j < uv_bios_obj_cnt; j++) { + sz = hub_buf[j].ports * sizeof(*port_buf[j]); + port_buf[j] = kzalloc(sz, GFP_KERNEL); + if (!port_buf[j]) { + ret = -ENOMEM; + j--; + goto err_port_info; + } + biosr = uv_bios_enum_ports((u64)uv_master_nasid, (u64)hub_buf[j].id, sz, + (u64 *)port_buf[j]); + if (biosr) { + ret = -EINVAL; + goto err_port_info; + } + } + for (j = 0; j < uv_bios_obj_cnt; j++) { + uv_hubs[j]->ports = kzalloc_objs(*uv_hubs[j]->ports, + hub_buf[j].ports); + if (!uv_hubs[j]->ports) { + ret = -ENOMEM; + j--; + goto err_ports; + } + } + for (j = 0; j < uv_bios_obj_cnt; j++) { + for (k = 0; k < hub_buf[j].ports; k++) { + uv_hubs[j]->ports[k] = kzalloc_obj(*uv_hubs[j]->ports[k]); + if (!uv_hubs[j]->ports[k]) { + ret = -ENOMEM; + k--; + goto err_kobj_ports; + } + uv_hubs[j]->ports[k]->port_info = &port_buf[j][k]; + ret = kobject_init_and_add(&uv_hubs[j]->ports[k]->kobj, &uv_port_attr_type, + &uv_hubs[j]->kobj, "port_%d", port_buf[j][k].port); + if (ret) + goto err_kobj_ports; + kobject_uevent(&uv_hubs[j]->ports[k]->kobj, KOBJ_ADD); + } + } + return 0; + +err_kobj_ports: + for (; j >= 0; j--) { + for (; k >= 0; k--) + kobject_put(&uv_hubs[j]->ports[k]->kobj); + if (j > 0) + k = hub_buf[j-1].ports - 1; + } + j = uv_bios_obj_cnt - 1; +err_ports: + for (; j >= 0; j--) + kfree(uv_hubs[j]->ports); + j = uv_bios_obj_cnt - 1; +err_port_info: + for (; j >= 0; j--) + kfree(port_buf[j]); + kfree(port_buf); + return ret; +} + +static void uv_ports_exit(void) +{ + int j, k; + + for (j = 0; j < uv_bios_obj_cnt; j++) { + for (k = hub_buf[j].ports - 1; k >= 0; k--) + kobject_put(&uv_hubs[j]->ports[k]->kobj); + } + for (j = 0; j < uv_bios_obj_cnt; j++) { + kfree(uv_hubs[j]->ports); + kfree(port_buf[j]); + } + kfree(port_buf); +} + +struct uv_pci_top_obj { + struct kobject kobj; + char *type; + char *location; + int iio_stack; + char *ppb_addr; + int slot; +}; + +#define to_uv_pci_top_obj(kobj_ptr) container_of(kobj_ptr, struct uv_pci_top_obj, kobj) + +static ssize_t uv_pci_type_show(struct uv_pci_top_obj *top_obj, char *buf) +{ + return sysfs_emit(buf, "%s\n", top_obj->type); +} + +static ssize_t uv_pci_location_show(struct uv_pci_top_obj *top_obj, char *buf) +{ + return sysfs_emit(buf, "%s\n", top_obj->location); +} + +static ssize_t uv_pci_iio_stack_show(struct uv_pci_top_obj *top_obj, char *buf) +{ + return sysfs_emit(buf, "%d\n", top_obj->iio_stack); +} + +static ssize_t uv_pci_ppb_addr_show(struct uv_pci_top_obj *top_obj, char *buf) +{ + return sysfs_emit(buf, "%s\n", top_obj->ppb_addr); +} + +static ssize_t uv_pci_slot_show(struct uv_pci_top_obj *top_obj, char *buf) +{ + return sysfs_emit(buf, "%d\n", top_obj->slot); +} + +struct uv_pci_top_sysfs_entry { + struct attribute attr; + ssize_t (*show)(struct uv_pci_top_obj *top_obj, char *buf); + ssize_t (*store)(struct uv_pci_top_obj *top_obj, const char *buf, size_t size); +}; + +static struct uv_pci_top_sysfs_entry uv_pci_type_attribute = + __ATTR(type, 0444, uv_pci_type_show, NULL); +static struct uv_pci_top_sysfs_entry uv_pci_location_attribute = + __ATTR(location, 0444, uv_pci_location_show, NULL); +static struct uv_pci_top_sysfs_entry uv_pci_iio_stack_attribute = + __ATTR(iio_stack, 0444, uv_pci_iio_stack_show, NULL); +static struct uv_pci_top_sysfs_entry uv_pci_ppb_addr_attribute = + __ATTR(ppb_addr, 0444, uv_pci_ppb_addr_show, NULL); +static struct uv_pci_top_sysfs_entry uv_pci_slot_attribute = + __ATTR(slot, 0444, uv_pci_slot_show, NULL); + +static void uv_pci_top_release(struct kobject *kobj) +{ + struct uv_pci_top_obj *top_obj = to_uv_pci_top_obj(kobj); + + kfree(top_obj->type); + kfree(top_obj->location); + kfree(top_obj->ppb_addr); + kfree(top_obj); +} + +static ssize_t pci_top_type_show(struct kobject *kobj, + struct attribute *attr, char *buf) +{ + struct uv_pci_top_obj *top_obj = to_uv_pci_top_obj(kobj); + struct uv_pci_top_sysfs_entry *entry; + + entry = container_of(attr, struct uv_pci_top_sysfs_entry, attr); + + if (!entry->show) + return -EIO; + + return entry->show(top_obj, buf); +} + +static const struct sysfs_ops uv_pci_top_sysfs_ops = { + .show = pci_top_type_show, +}; + +static const struct kobj_type uv_pci_top_attr_type = { + .release = uv_pci_top_release, + .sysfs_ops = &uv_pci_top_sysfs_ops, +}; + +static int init_pci_top_obj(struct uv_pci_top_obj *top_obj, char *line) +{ + char *start; + char type[11], location[14], ppb_addr[15]; + int str_cnt, ret; + unsigned int tmp_match[2]; + + // Minimum line length + if (strlen(line) < 36) + return -EINVAL; + + //Line must match format "pcibus %4x:%2x" to be valid + str_cnt = sscanf(line, "pcibus %4x:%2x", &tmp_match[0], &tmp_match[1]); + if (str_cnt < 2) + return -EINVAL; + + /* Connect pcibus to segment:bus number with '_' + * to concatenate name tokens. + * pcibus 0000:00 ... -> pcibus_0000:00 ... + */ + line[6] = '_'; + + /* Null terminate after the concatencated name tokens + * to produce kobj name string. + */ + line[14] = '\0'; + + // Use start to index after name tokens string for remainder of line info. + start = &line[15]; + + top_obj->iio_stack = -1; + top_obj->slot = -1; + + /* r001i01b00h0 BASE IO (IIO Stack 0) + * r001i01b00h1 PCIe IO (IIO Stack 1) + * r001i01b03h1 PCIe SLOT + * r001i01b00h0 NODE IO + * r001i01b00h0 Riser + * (IIO Stack #) may not be present. + */ + if (start[0] == 'r') { + str_cnt = sscanf(start, "%13s %10[^(] %*s %*s %d)", + location, type, &top_obj->iio_stack); + if (str_cnt < 2) + return -EINVAL; + top_obj->type = kstrdup(type, GFP_KERNEL); + if (!top_obj->type) + return -ENOMEM; + top_obj->location = kstrdup(location, GFP_KERNEL); + if (!top_obj->location) { + kfree(top_obj->type); + return -ENOMEM; + } + } + /* PPB at 0000:80:00.00 (slot 3) + * (slot #) may not be present. + */ + else if (start[0] == 'P') { + str_cnt = sscanf(start, "%10s %*s %14s %*s %d)", + type, ppb_addr, &top_obj->slot); + if (str_cnt < 2) + return -EINVAL; + top_obj->type = kstrdup(type, GFP_KERNEL); + if (!top_obj->type) + return -ENOMEM; + top_obj->ppb_addr = kstrdup(ppb_addr, GFP_KERNEL); + if (!top_obj->ppb_addr) { + kfree(top_obj->type); + return -ENOMEM; + } + } else + return -EINVAL; + + top_obj->kobj.kset = uv_pcibus_kset; + + ret = kobject_init_and_add(&top_obj->kobj, &uv_pci_top_attr_type, NULL, "%s", line); + if (ret) + goto err_add_sysfs; + + if (top_obj->type) { + ret = sysfs_create_file(&top_obj->kobj, &uv_pci_type_attribute.attr); + if (ret) + goto err_add_sysfs; + } + if (top_obj->location) { + ret = sysfs_create_file(&top_obj->kobj, &uv_pci_location_attribute.attr); + if (ret) + goto err_add_sysfs; + } + if (top_obj->iio_stack >= 0) { + ret = sysfs_create_file(&top_obj->kobj, &uv_pci_iio_stack_attribute.attr); + if (ret) + goto err_add_sysfs; + } + if (top_obj->ppb_addr) { + ret = sysfs_create_file(&top_obj->kobj, &uv_pci_ppb_addr_attribute.attr); + if (ret) + goto err_add_sysfs; + } + if (top_obj->slot >= 0) { + ret = sysfs_create_file(&top_obj->kobj, &uv_pci_slot_attribute.attr); + if (ret) + goto err_add_sysfs; + } + + kobject_uevent(&top_obj->kobj, KOBJ_ADD); + return 0; + +err_add_sysfs: + kobject_put(&top_obj->kobj); + return ret; +} + +static int pci_topology_init(void) +{ + char *pci_top_str, *start, *found, *count; + size_t sz; + s64 biosr; + int l = 0, k = 0; + int len, ret; + + uv_pcibus_kset = kset_create_and_add("pcibuses", NULL, sgi_uv_kobj); + if (!uv_pcibus_kset) + return -ENOMEM; + + for (sz = PAGE_SIZE; sz < 16 * PAGE_SIZE; sz += PAGE_SIZE) { + pci_top_str = kmalloc(sz, GFP_KERNEL); + if (!pci_top_str) { + ret = -ENOMEM; + goto err_pci_top_str; + } + biosr = uv_bios_get_pci_topology((u64)sz, (u64 *)pci_top_str); + if (biosr == BIOS_STATUS_SUCCESS) { + len = strnlen(pci_top_str, sz); + for (count = pci_top_str; count < pci_top_str + len; count++) { + if (*count == '\n') + l++; + } + num_pci_lines = l; + + uv_pci_objs = kzalloc_objs(*uv_pci_objs, num_pci_lines); + if (!uv_pci_objs) { + kfree(pci_top_str); + ret = -ENOMEM; + goto err_pci_top_str; + } + start = pci_top_str; + while ((found = strsep(&start, "\n")) != NULL) { + uv_pci_objs[k] = kzalloc_obj(*uv_pci_objs[k]); + if (!uv_pci_objs[k]) { + ret = -ENOMEM; + goto err_pci_obj; + } + ret = init_pci_top_obj(uv_pci_objs[k], found); + if (ret) + goto err_pci_obj; + k++; + if (k == num_pci_lines) + break; + } + } + kfree(pci_top_str); + if (biosr == BIOS_STATUS_SUCCESS || biosr == BIOS_STATUS_UNIMPLEMENTED) + break; + } + + return 0; +err_pci_obj: + k--; + for (; k >= 0; k--) + kobject_put(&uv_pci_objs[k]->kobj); + kfree(uv_pci_objs); + kfree(pci_top_str); +err_pci_top_str: + kset_unregister(uv_pcibus_kset); + return ret; +} + +static void pci_topology_exit(void) +{ + int k; + + for (k = 0; k < num_pci_lines; k++) + kobject_put(&uv_pci_objs[k]->kobj); + kset_unregister(uv_pcibus_kset); + kfree(uv_pci_objs); +} + +static ssize_t partition_id_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%ld\n", sn_partition_id); +} + +static ssize_t coherence_id_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%ld\n", sn_coherency_id); +} + +static ssize_t uv_type_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%s\n", uv_type_string()); +} + +static ssize_t uv_archtype_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return uv_get_archtype(buf, PAGE_SIZE); +} + +static ssize_t uv_hub_type_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0x%x\n", uv_hub_type()); +} + +static ssize_t uv_hubless_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0x%x\n", uv_get_hubless_system()); +} + +static struct kobj_attribute partition_id_attr = + __ATTR(partition_id, 0444, partition_id_show, NULL); +static struct kobj_attribute coherence_id_attr = + __ATTR(coherence_id, 0444, coherence_id_show, NULL); +static struct kobj_attribute uv_type_attr = + __ATTR(uv_type, 0444, uv_type_show, NULL); +static struct kobj_attribute uv_archtype_attr = + __ATTR(archtype, 0444, uv_archtype_show, NULL); +static struct kobj_attribute uv_hub_type_attr = + __ATTR(hub_type, 0444, uv_hub_type_show, NULL); +static struct kobj_attribute uv_hubless_attr = + __ATTR(hubless, 0444, uv_hubless_show, NULL); + +static struct attribute *base_attrs[] = { + &partition_id_attr.attr, + &coherence_id_attr.attr, + &uv_type_attr.attr, + &uv_archtype_attr.attr, + &uv_hub_type_attr.attr, + NULL, +}; + +static const struct attribute_group base_attr_group = { + .attrs = base_attrs +}; + +static int initial_bios_setup(void) +{ + u64 v; + s64 biosr; + + biosr = uv_bios_get_master_nasid((u64)sizeof(uv_master_nasid), (u64 *)&uv_master_nasid); + if (biosr) + return -EINVAL; + + biosr = uv_bios_get_heapsize((u64)uv_master_nasid, (u64)sizeof(u64), &v); + if (biosr) + return -EINVAL; + + uv_biosheap = vmalloc(v); + if (!uv_biosheap) + return -ENOMEM; + + biosr = uv_bios_install_heap((u64)uv_master_nasid, v, (u64 *)uv_biosheap); + if (biosr) { + vfree(uv_biosheap); + return -EINVAL; + } + + biosr = uv_bios_obj_count((u64)uv_master_nasid, sizeof(u64), &v); + if (biosr) { + vfree(uv_biosheap); + return -EINVAL; + } + uv_bios_obj_cnt = (int)v; + + return 0; +} + +static struct attribute *hubless_base_attrs[] = { + &partition_id_attr.attr, + &uv_type_attr.attr, + &uv_archtype_attr.attr, + &uv_hubless_attr.attr, + NULL, +}; + +static const struct attribute_group hubless_base_attr_group = { + .attrs = hubless_base_attrs +}; + + +static int __init uv_sysfs_hubless_init(void) +{ + int ret; + + ret = sysfs_create_group(sgi_uv_kobj, &hubless_base_attr_group); + if (ret) { + pr_warn("sysfs_create_group hubless_base_attr_group failed\n"); + kobject_put(sgi_uv_kobj); + } + return ret; +} + +static int __init uv_sysfs_init(void) +{ + int ret = 0; + + if (!is_uv_system() && !uv_get_hubless_system()) + return -ENODEV; + + num_cnodes = uv_num_possible_blades(); + + if (!sgi_uv_kobj) + sgi_uv_kobj = kobject_create_and_add("sgi_uv", firmware_kobj); + if (!sgi_uv_kobj) { + pr_warn("kobject_create_and_add sgi_uv failed\n"); + return -EINVAL; + } + + if (uv_get_hubless_system()) + return uv_sysfs_hubless_init(); + + ret = sysfs_create_group(sgi_uv_kobj, &base_attr_group); + if (ret) { + pr_warn("sysfs_create_group base_attr_group failed\n"); + goto err_create_group; + } + + ret = initial_bios_setup(); + if (ret) + goto err_bios_setup; + + ret = uv_hubs_init(); + if (ret) + goto err_hubs_init; + + ret = uv_ports_init(); + if (ret) + goto err_ports_init; + + ret = pci_topology_init(); + if (ret) + goto err_pci_init; + + return 0; + +err_pci_init: + uv_ports_exit(); +err_ports_init: + uv_hubs_exit(); +err_hubs_init: + vfree(uv_biosheap); +err_bios_setup: + sysfs_remove_group(sgi_uv_kobj, &base_attr_group); +err_create_group: + kobject_put(sgi_uv_kobj); + return ret; +} + +static void __exit uv_sysfs_hubless_exit(void) +{ + sysfs_remove_group(sgi_uv_kobj, &hubless_base_attr_group); + kobject_put(sgi_uv_kobj); +} + +static void __exit uv_sysfs_exit(void) +{ + if (!is_uv_system()) { + if (uv_get_hubless_system()) + uv_sysfs_hubless_exit(); + return; + } + + pci_topology_exit(); + uv_ports_exit(); + uv_hubs_exit(); + vfree(uv_biosheap); + sysfs_remove_group(sgi_uv_kobj, &base_attr_group); + kobject_put(sgi_uv_kobj); +} + +#ifndef MODULE +device_initcall(uv_sysfs_init); +#else +module_init(uv_sysfs_init); +#endif +module_exit(uv_sysfs_exit); + +MODULE_AUTHOR("Hewlett Packard Enterprise"); +MODULE_DESCRIPTION("Sysfs structure for HPE UV systems"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/winmate-fm07-keys.c b/drivers/platform/x86/winmate-fm07-keys.c new file mode 100644 index 000000000..465ffad81 --- /dev/null +++ b/drivers/platform/x86/winmate-fm07-keys.c @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Driver for the Winmate FM07 front-panel keys +// +// Author: Daniel Beer + +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "winmate-fm07keys" + +#define PORT_CMD 0x6c +#define PORT_DATA 0x68 + +#define EC_ADDR_KEYS 0x3b +#define EC_CMD_READ 0x80 + +#define BASE_KEY KEY_F13 +#define NUM_KEYS 5 + +/* Typically we're done in fewer than 10 iterations */ +#define LOOP_TIMEOUT 1000 + +static void fm07keys_poll(struct input_dev *input) +{ + uint8_t k; + int i; + + /* Flush output buffer */ + i = 0; + while (inb(PORT_CMD) & 0x01) { + if (++i >= LOOP_TIMEOUT) + goto timeout; + inb(PORT_DATA); + } + + /* Send request and wait for write completion */ + outb(EC_CMD_READ, PORT_CMD); + i = 0; + while (inb(PORT_CMD) & 0x02) + if (++i >= LOOP_TIMEOUT) + goto timeout; + + outb(EC_ADDR_KEYS, PORT_DATA); + i = 0; + while (inb(PORT_CMD) & 0x02) + if (++i >= LOOP_TIMEOUT) + goto timeout; + + /* Wait for data ready */ + i = 0; + while (!(inb(PORT_CMD) & 0x01)) + if (++i >= LOOP_TIMEOUT) + goto timeout; + k = inb(PORT_DATA); + + /* Notify of new key states */ + for (i = 0; i < NUM_KEYS; i++) { + input_report_key(input, BASE_KEY + i, (~k) & 1); + k >>= 1; + } + + input_sync(input); + return; + +timeout: + dev_warn_ratelimited(&input->dev, "timeout polling IO memory\n"); +} + +static int fm07keys_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct input_dev *input; + int ret; + int i; + + input = devm_input_allocate_device(dev); + if (!input) { + dev_err(dev, "no memory for input device\n"); + return -ENOMEM; + } + + if (!devm_request_region(dev, PORT_CMD, 1, "Winmate FM07 EC")) + return -EBUSY; + if (!devm_request_region(dev, PORT_DATA, 1, "Winmate FM07 EC")) + return -EBUSY; + + input->name = "Winmate FM07 front-panel keys"; + input->phys = DRV_NAME "/input0"; + + input->id.bustype = BUS_HOST; + input->id.vendor = 0x0001; + input->id.product = 0x0001; + input->id.version = 0x0100; + + __set_bit(EV_KEY, input->evbit); + + for (i = 0; i < NUM_KEYS; i++) + __set_bit(BASE_KEY + i, input->keybit); + + ret = input_setup_polling(input, fm07keys_poll); + if (ret) { + dev_err(dev, "unable to set up polling, err=%d\n", ret); + return ret; + } + + /* These are silicone buttons. They can't be pressed in rapid + * succession too quickly, and 50 Hz seems to be an adequate + * sampling rate without missing any events when tested. + */ + input_set_poll_interval(input, 20); + + ret = input_register_device(input); + if (ret) { + dev_err(dev, "unable to register polled device, err=%d\n", + ret); + return ret; + } + + input_sync(input); + return 0; +} + +static struct platform_driver fm07keys_driver = { + .probe = fm07keys_probe, + .driver = { + .name = DRV_NAME + }, +}; + +static struct platform_device *dev; + +static const struct dmi_system_id fm07keys_dmi_table[] __initconst = { + { + /* FM07 and FM07P */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Winmate Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "IP30"), + }, + }, + { } +}; + +MODULE_DEVICE_TABLE(dmi, fm07keys_dmi_table); + +static int __init fm07keys_init(void) +{ + int ret; + + if (!dmi_check_system(fm07keys_dmi_table)) + return -ENODEV; + + ret = platform_driver_register(&fm07keys_driver); + if (ret) { + pr_err("fm07keys: failed to register driver, err=%d\n", ret); + return ret; + } + + dev = platform_device_register_simple(DRV_NAME, PLATFORM_DEVID_NONE, NULL, 0); + if (IS_ERR(dev)) { + ret = PTR_ERR(dev); + pr_err("fm07keys: failed to allocate device, err = %d\n", ret); + goto fail_register; + } + + return 0; + +fail_register: + platform_driver_unregister(&fm07keys_driver); + return ret; +} + +static void __exit fm07keys_exit(void) +{ + platform_driver_unregister(&fm07keys_driver); + platform_device_unregister(dev); +} + +module_init(fm07keys_init); +module_exit(fm07keys_exit); + +MODULE_AUTHOR("Daniel Beer "); +MODULE_DESCRIPTION("Winmate FM07 front-panel keys driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/wireless-hotkey.c b/drivers/platform/x86/wireless-hotkey.c new file mode 100644 index 000000000..3151844d1 --- /dev/null +++ b/drivers/platform/x86/wireless-hotkey.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Airplane mode button for AMD, HP & Xiaomi laptops + * + * Copyright (C) 2014-2017 Alex Hung + * Copyright (C) 2021 Advanced Micro Devices + */ + +#include +#include +#include +#include +#include +#include +#include + +MODULE_DESCRIPTION("Airplane mode button for AMD, HP & Xiaomi laptops"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Alex Hung"); +MODULE_ALIAS("acpi*:HPQ6001:*"); +MODULE_ALIAS("acpi*:WSTADEF:*"); +MODULE_ALIAS("acpi*:AMDI0051:*"); +MODULE_ALIAS("acpi*:LGEX0815:*"); + +struct wl_button { + struct input_dev *input_dev; + char phys[32]; +}; + +static const struct acpi_device_id wl_ids[] = { + {"HPQ6001", 0}, + {"WSTADEF", 0}, + {"AMDI0051", 0}, + {"LGEX0815", 0}, + {"", 0}, +}; + +static int wireless_input_setup(struct device *dev) +{ + struct wl_button *button = dev_get_drvdata(dev); + int err; + + button->input_dev = input_allocate_device(); + if (!button->input_dev) + return -ENOMEM; + + snprintf(button->phys, sizeof(button->phys), "%s/input0", + acpi_device_hid(ACPI_COMPANION(dev))); + + button->input_dev->name = "Wireless hotkeys"; + button->input_dev->phys = button->phys; + button->input_dev->id.bustype = BUS_HOST; + button->input_dev->evbit[0] = BIT(EV_KEY); + set_bit(KEY_RFKILL, button->input_dev->keybit); + + err = input_register_device(button->input_dev); + if (err) + goto err_free_dev; + + return 0; + +err_free_dev: + input_free_device(button->input_dev); + return err; +} + +static void wireless_input_destroy(struct device *dev) +{ + struct wl_button *button = dev_get_drvdata(dev); + + input_unregister_device(button->input_dev); + kfree(button); +} + +static void wl_notify(acpi_handle handle, u32 event, void *data) +{ + struct wl_button *button = data; + + if (event != 0x80) { + pr_info("Received unknown event (0x%x)\n", event); + return; + } + + input_report_key(button->input_dev, KEY_RFKILL, 1); + input_sync(button->input_dev); + input_report_key(button->input_dev, KEY_RFKILL, 0); + input_sync(button->input_dev); +} + +static int wl_probe(struct platform_device *pdev) +{ + struct acpi_device *adev; + struct wl_button *button; + int err; + + adev = ACPI_COMPANION(&pdev->dev); + if (!adev) + return -ENODEV; + + button = kzalloc_obj(struct wl_button); + if (!button) + return -ENOMEM; + + platform_set_drvdata(pdev, button); + + err = wireless_input_setup(&pdev->dev); + if (err) { + pr_err("Failed to setup wireless hotkeys\n"); + kfree(button); + return err; + } + err = acpi_dev_install_notify_handler(adev, ACPI_DEVICE_NOTIFY, + wl_notify, button); + if (err) { + pr_err("Failed to install ACPI notify handler\n"); + wireless_input_destroy(&pdev->dev); + } + + return err; +} + +static void wl_remove(struct platform_device *pdev) +{ + acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev), + ACPI_DEVICE_NOTIFY, wl_notify); + wireless_input_destroy(&pdev->dev); +} + +static struct platform_driver wl_driver = { + .probe = wl_probe, + .remove = wl_remove, + .driver = { + .name = "wireless-hotkey", + .acpi_match_table = wl_ids, + }, +}; + +module_platform_driver(wl_driver); diff --git a/drivers/platform/x86/wmi-bmof.c b/drivers/platform/x86/wmi-bmof.c new file mode 100644 index 000000000..6623cf60e --- /dev/null +++ b/drivers/platform/x86/wmi-bmof.c @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * WMI embedded Binary MOF driver + * + * Copyright (c) 2015 Andrew Lutomirski + * Copyright (C) 2017 VMware, Inc. All Rights Reserved. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include + +#define WMI_BMOF_GUID "05901221-D566-11D1-B2F0-00A0C9062910" + +static ssize_t bmof_read(struct file *filp, struct kobject *kobj, const struct bin_attribute *attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct wmi_buffer *buffer = dev_get_drvdata(dev); + + return memory_read_from_buffer(buf, count, &off, buffer->data, buffer->length); +} + +static const BIN_ATTR_ADMIN_RO(bmof, 0); + +static const struct bin_attribute * const bmof_attrs[] = { + &bin_attr_bmof, + NULL +}; + +static size_t bmof_bin_size(struct kobject *kobj, const struct bin_attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct wmi_buffer *buffer = dev_get_drvdata(dev); + + return buffer->length; +} + +static const struct attribute_group bmof_group = { + .bin_size = bmof_bin_size, + .bin_attrs = bmof_attrs, +}; + +static const struct attribute_group *bmof_groups[] = { + &bmof_group, + NULL +}; + +static int wmi_bmof_probe(struct wmi_device *wdev, const void *context) +{ + struct wmi_buffer *buffer; + int ret; + + buffer = devm_kzalloc(&wdev->dev, sizeof(*buffer), GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + ret = wmidev_query_block(wdev, 0, buffer, 0); + if (ret < 0) + return ret; + + dev_set_drvdata(&wdev->dev, buffer); + + return 0; +} + +static void wmi_bmof_remove(struct wmi_device *wdev) +{ + struct wmi_buffer *buffer = dev_get_drvdata(&wdev->dev); + + kfree(buffer->data); +} + +static const struct wmi_device_id wmi_bmof_id_table[] = { + { .guid_string = WMI_BMOF_GUID }, + { }, +}; + +static struct wmi_driver wmi_bmof_driver = { + .driver = { + .name = "wmi-bmof", + .dev_groups = bmof_groups, + }, + .probe = wmi_bmof_probe, + .remove = wmi_bmof_remove, + .id_table = wmi_bmof_id_table, + .no_singleton = true, +}; + +module_wmi_driver(wmi_bmof_driver); + +MODULE_DEVICE_TABLE(wmi, wmi_bmof_id_table); +MODULE_AUTHOR("Andrew Lutomirski "); +MODULE_DESCRIPTION("WMI embedded Binary MOF driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/x86-android-tablets/Kconfig b/drivers/platform/x86/x86-android-tablets/Kconfig new file mode 100644 index 000000000..193da15ee --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/Kconfig @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# X86 Android tablet support Kconfig +# + +config X86_ANDROID_TABLETS + tristate "X86 Android tablet support" + depends on I2C && SPI && SERIAL_DEV_BUS + depends on GPIOLIB && PMIC_OPREGION + depends on ACPI && EFI && PCI + select NEW_LEDS + select LEDS_CLASS + select POWER_SUPPLY + help + X86 tablets which ship with Android as (part of) the factory image + typically have various problems with their DSDTs. The factory kernels + shipped on these devices typically have device addresses and GPIOs + hardcoded in the kernel, rather than specified in their DSDT. + + With the DSDT containing a random collection of devices which may or + may not actually be present. This driver contains various fixes for + such tablets, including instantiating kernel devices for devices which + are missing from the DSDT. + + If you have a x86 Android tablet say Y or M here, for a generic x86 + distro configuration say M here. diff --git a/drivers/platform/x86/x86-android-tablets/Makefile b/drivers/platform/x86/x86-android-tablets/Makefile new file mode 100644 index 000000000..a2cf8cbdb --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# X86 Android tablet support Makefile +# + +obj-$(CONFIG_X86_ANDROID_TABLETS) += vexia_atla10_ec.o +obj-$(CONFIG_X86_ANDROID_TABLETS) += x86-android-tablets.o +x86-android-tablets-y := core.o dmi.o shared-psy-info.o \ + acer.o asus.o lenovo.o other.o diff --git a/drivers/platform/x86/x86-android-tablets/acer.c b/drivers/platform/x86/x86-android-tablets/acer.c new file mode 100644 index 000000000..d48c70ffd --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/acer.c @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Board info for Acer X86 tablets which ship with Android as the factory image + * and which have broken DSDT tables. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2025 Hans de Goede + */ + +#include +#include +#include +#include + +#include "shared-psy-info.h" +#include "x86-android-tablets.h" + +/* Acer Iconia One 8 A1-840 (non FHD version) */ +static const struct property_entry acer_a1_840_bq24190_props[] = { + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), + PROPERTY_ENTRY_BOOL("omit-battery-class"), + PROPERTY_ENTRY_BOOL("disable-reset"), + { } +}; + +static const struct software_node acer_a1_840_bq24190_node = { + .properties = acer_a1_840_bq24190_props, +}; + +static const struct property_entry acer_a1_840_touchscreen_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 800), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[1], 26, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node acer_a1_840_touchscreen_node = { + .properties = acer_a1_840_touchscreen_props, +}; + +static const struct x86_i2c_client_info acer_a1_840_i2c_clients[] __initconst = { + { + /* BQ24297 charger IC */ + .board_info = { + .type = "bq24297", + .addr = 0x6b, + .dev_name = "bq24297", + .swnode = &acer_a1_840_bq24190_node, + .platform_data = &bq24190_pdata, + }, + .adapter_path = "\\_SB_.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 2, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "bq24297_irq", + }, + }, { + /* MPU6515 sensors */ + .board_info = { + .type = "mpu6515", + .addr = 0x69, + .dev_name = "mpu6515", + }, + .adapter_path = "\\_SB_.I2C3", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x47, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + }, { + /* FT5416 touchscreen controller */ + .board_info = { + .type = "edt-ft5x06", + .addr = 0x38, + .dev_name = "ft5416", + .swnode = &acer_a1_840_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x45, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + } +}; + +static const struct property_entry acer_a1_840_int3496_props[] __initconst = { + PROPERTY_ENTRY_GPIO("mux-gpios", &baytrail_gpiochip_nodes[2], 1, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("id-gpios", &baytrail_gpiochip_nodes[2], 18, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct platform_device_info acer_a1_840_pdevs[] __initconst = { + { + /* For micro USB ID pin handling */ + .name = "intel-int3496", + .id = PLATFORM_DEVID_NONE, + .properties = acer_a1_840_int3496_props, + }, +}; + +/* Properties for the Dollar Cove TI PMIC battery MFD child used as fuel-gauge */ +static const struct property_entry acer_a1_840_fg_props[] = { + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", bq24190_psy, 1), + PROPERTY_ENTRY_GPIO("charged-gpios", &baytrail_gpiochip_nodes[2], 10, GPIO_ACTIVE_HIGH), + { } +}; + +static struct device *acer_a1_840_fg_dev; +static struct fwnode_handle *acer_a1_840_fg_node; + +static int __init acer_a1_840_init(struct device *dev) +{ + int ret; + + acer_a1_840_fg_dev = bus_find_device_by_name(&platform_bus_type, NULL, "chtdc_ti_battery"); + if (!acer_a1_840_fg_dev) + return dev_err_probe(dev, -EPROBE_DEFER, "getting chtdc_ti_battery dev\n"); + + acer_a1_840_fg_node = fwnode_create_software_node(acer_a1_840_fg_props, NULL); + if (IS_ERR(acer_a1_840_fg_node)) { + ret = PTR_ERR(acer_a1_840_fg_node); + goto err_put; + } + + ret = device_add_software_node(acer_a1_840_fg_dev, + to_software_node(acer_a1_840_fg_node)); + if (ret) + goto err_put; + + return 0; + +err_put: + fwnode_handle_put(acer_a1_840_fg_node); + acer_a1_840_fg_node = NULL; + put_device(acer_a1_840_fg_dev); + acer_a1_840_fg_dev = NULL; + return ret; +} + +static void acer_a1_840_exit(void) +{ + device_remove_software_node(acer_a1_840_fg_dev); + /* + * Skip fwnode_handle_put(acer_a1_840_fg_node), instead leak the node. + * The intel_dc_ti_battery driver may still reference the strdup-ed + * "supplied-from" string. This string will be free-ed if the node + * is released. + */ + acer_a1_840_fg_node = NULL; + put_device(acer_a1_840_fg_dev); + acer_a1_840_fg_dev = NULL; +} + +static const char * const acer_a1_840_modules[] __initconst = { + "bq24190_charger", /* For the Vbus regulator for intel-int3496 */ + NULL +}; + +const struct x86_dev_info acer_a1_840_info __initconst = { + .i2c_client_info = acer_a1_840_i2c_clients, + .i2c_client_count = ARRAY_SIZE(acer_a1_840_i2c_clients), + .pdev_info = acer_a1_840_pdevs, + .pdev_count = ARRAY_SIZE(acer_a1_840_pdevs), + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, + .swnode_group = generic_lipo_4v2_battery_swnodes, + .modules = acer_a1_840_modules, + .init = acer_a1_840_init, + .exit = acer_a1_840_exit, +}; + +/* Acer Iconia One 7 B1-750 has an Android factory image with everything hardcoded */ +static const char * const acer_b1_750_mount_matrix[] = { + "-1", "0", "0", + "0", "1", "0", + "0", "0", "1" +}; + +static const struct property_entry acer_b1_750_bma250e_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", acer_b1_750_mount_matrix), + { } +}; + +static const struct software_node acer_b1_750_bma250e_node = { + .properties = acer_b1_750_bma250e_props, +}; + +static const struct property_entry acer_b1_750_novatek_props[] = { + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[1], 26, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node acer_b1_750_novatek_node = { + .properties = acer_b1_750_novatek_props, +}; + +static const struct x86_i2c_client_info acer_b1_750_i2c_clients[] __initconst = { + { + /* Novatek NVT-ts touchscreen */ + .board_info = { + .type = "nt11205-ts", + .addr = 0x34, + .dev_name = "NVT-ts", + .swnode = &acer_b1_750_novatek_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 3, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "NVT-ts_irq", + }, + }, { + /* BMA250E accelerometer */ + .board_info = { + .type = "bma250e", + .addr = 0x18, + .swnode = &acer_b1_750_bma250e_node, + }, + .adapter_path = "\\_SB_.I2C3", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 25, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "bma250e_irq", + }, + }, +}; + +const struct x86_dev_info acer_b1_750_info __initconst = { + .i2c_client_info = acer_b1_750_i2c_clients, + .i2c_client_count = ARRAY_SIZE(acer_b1_750_i2c_clients), + .pdev_info = int3496_pdevs, + .pdev_count = 1, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; diff --git a/drivers/platform/x86/x86-android-tablets/asus.c b/drivers/platform/x86/x86-android-tablets/asus.c new file mode 100644 index 000000000..7d29c7654 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/asus.c @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Board info for Asus X86 tablets which ship with Android as the factory image + * and which have broken DSDT tables. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#include +#include +#include +#include + +#include "shared-psy-info.h" +#include "x86-android-tablets.h" + +/* Asus ME176C and TF103C tablets shared data */ +static const struct property_entry asus_me176c_tf103c_int3496_props[] __initconst = { + PROPERTY_ENTRY_GPIO("id-gpios", &baytrail_gpiochip_nodes[2], 22, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct platform_device_info asus_me176c_tf103c_pdevs[] __initconst = { + { + /* For micro USB ID pin handling */ + .name = "intel-int3496", + .id = PLATFORM_DEVID_NONE, + .properties = asus_me176c_tf103c_int3496_props, + }, +}; + +static const struct software_node asus_me176c_tf103c_gpio_keys_node = { + .name = "lid_sw", +}; + +static const struct property_entry asus_me176c_tf103c_lid_props[] = { + PROPERTY_ENTRY_U32("linux,input-type", EV_SW), + PROPERTY_ENTRY_U32("linux,code", SW_LID), + PROPERTY_ENTRY_STRING("label", "lid_sw"), + PROPERTY_ENTRY_GPIO("gpios", &baytrail_gpiochip_nodes[2], 12, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + PROPERTY_ENTRY_BOOL("wakeup-source"), + { } +}; + +static const struct software_node asus_me176c_tf103c_lid_node = { + .parent = &asus_me176c_tf103c_gpio_keys_node, + .properties = asus_me176c_tf103c_lid_props, +}; + +static const struct software_node *asus_me176c_tf103c_lid_swnodes[] = { + &asus_me176c_tf103c_gpio_keys_node, + &asus_me176c_tf103c_lid_node, + NULL +}; + +/* Asus ME176C tablets have an Android factory image with everything hardcoded */ +static const char * const asus_me176c_accel_mount_matrix[] = { + "-1", "0", "0", + "0", "1", "0", + "0", "0", "1" +}; + +static const struct property_entry asus_me176c_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", asus_me176c_accel_mount_matrix), + { } +}; + +static const struct software_node asus_me176c_accel_node = { + .properties = asus_me176c_accel_props, +}; + +static const struct property_entry asus_me176c_bq24190_props[] = { + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", tusb1211_chg_det_psy, 1), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_hv_4v35_battery_node), + PROPERTY_ENTRY_U32("ti,system-minimum-microvolt", 3600000), + PROPERTY_ENTRY_BOOL("omit-battery-class"), + PROPERTY_ENTRY_BOOL("disable-reset"), + { } +}; + +static const struct software_node asus_me176c_bq24190_node = { + .properties = asus_me176c_bq24190_props, +}; + +static const struct property_entry asus_me176c_ug3105_props[] = { + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", bq24190_psy, 1), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_hv_4v35_battery_node), + PROPERTY_ENTRY_U32("upisemi,rsns-microohm", 10000), + { } +}; + +static const struct software_node asus_me176c_ug3105_node = { + .properties = asus_me176c_ug3105_props, +}; + +static const struct property_entry asus_me176c_touchscreen_props[] = { + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[0], 60, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("irq-gpios", &baytrail_gpiochip_nodes[2], 28, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node asus_me176c_touchscreen_node = { + .properties = asus_me176c_touchscreen_props, +}; + +static const struct x86_i2c_client_info asus_me176c_i2c_clients[] __initconst = { + { + /* bq24297 battery charger */ + .board_info = { + .type = "bq24190", + .addr = 0x6b, + .dev_name = "bq24297", + .swnode = &asus_me176c_bq24190_node, + .platform_data = &bq24190_pdata, + }, + .adapter_path = "\\_SB_.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_PMIC, + .chip = "\\_SB_.I2C7.PMIC", + .domain = DOMAIN_BUS_WAKEUP, + .index = 0, + }, + }, { + /* ug3105 battery monitor */ + .board_info = { + .type = "ug3105", + .addr = 0x70, + .dev_name = "ug3105", + .swnode = &asus_me176c_ug3105_node, + }, + .adapter_path = "\\_SB_.I2C1", + }, { + /* ak09911 compass */ + .board_info = { + .type = "ak09911", + .addr = 0x0c, + .dev_name = "ak09911", + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* kxtj21009 accelerometer */ + .board_info = { + .type = "kxtj21009", + .addr = 0x0f, + .dev_name = "kxtj21009", + .swnode = &asus_me176c_accel_node, + }, + .adapter_path = "\\_SB_.I2C5", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x44, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + }, + }, { + /* goodix touchscreen */ + .board_info = { + .type = "GDIX1001:00", + .addr = 0x14, + .dev_name = "goodix_ts", + .swnode = &asus_me176c_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x45, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + }, + }, +}; + +static const struct x86_serdev_info asus_me176c_serdevs[] __initconst = { + { + .ctrl.acpi.hid = "80860F0A", + .ctrl.acpi.uid = "2", + .ctrl_devname = "serial0", + .serdev_hid = "BCM2E3A", + }, +}; + +const struct x86_dev_info asus_me176c_info __initconst = { + .i2c_client_info = asus_me176c_i2c_clients, + .i2c_client_count = ARRAY_SIZE(asus_me176c_i2c_clients), + .pdev_info = asus_me176c_tf103c_pdevs, + .pdev_count = ARRAY_SIZE(asus_me176c_tf103c_pdevs), + .serdev_info = asus_me176c_serdevs, + .serdev_count = ARRAY_SIZE(asus_me176c_serdevs), + .gpio_button_swnodes = asus_me176c_tf103c_lid_swnodes, + .swnode_group = generic_lipo_hv_4v35_battery_swnodes, + .modules = bq24190_modules, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* Asus TF103C tablets have an Android factory image with everything hardcoded */ +static const char * const asus_tf103c_accel_mount_matrix[] = { + "0", "-1", "0", + "-1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry asus_tf103c_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", asus_tf103c_accel_mount_matrix), + { } +}; + +static const struct software_node asus_tf103c_accel_node = { + .properties = asus_tf103c_accel_props, +}; + +static const struct property_entry asus_tf103c_touchscreen_props[] = { + PROPERTY_ENTRY_STRING("compatible", "atmel,atmel_mxt_ts"), + { } +}; + +static const struct software_node asus_tf103c_touchscreen_node = { + .properties = asus_tf103c_touchscreen_props, +}; + +static const struct property_entry asus_tf103c_bq24190_props[] = { + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", tusb1211_chg_det_psy, 1), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), + PROPERTY_ENTRY_U32("ti,system-minimum-microvolt", 3600000), + PROPERTY_ENTRY_BOOL("omit-battery-class"), + PROPERTY_ENTRY_BOOL("disable-reset"), + { } +}; + +static const struct software_node asus_tf103c_bq24190_node = { + .properties = asus_tf103c_bq24190_props, +}; + +static const struct property_entry asus_tf103c_ug3105_props[] = { + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", bq24190_psy, 1), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), + PROPERTY_ENTRY_U32("upisemi,rsns-microohm", 5000), + { } +}; + +static const struct software_node asus_tf103c_ug3105_node = { + .properties = asus_tf103c_ug3105_props, +}; + +static const struct x86_i2c_client_info asus_tf103c_i2c_clients[] __initconst = { + { + /* bq24297 battery charger */ + .board_info = { + .type = "bq24190", + .addr = 0x6b, + .dev_name = "bq24297", + .swnode = &asus_tf103c_bq24190_node, + .platform_data = &bq24190_pdata, + }, + .adapter_path = "\\_SB_.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_PMIC, + .chip = "\\_SB_.I2C7.PMIC", + .domain = DOMAIN_BUS_WAKEUP, + .index = 0, + }, + }, { + /* ug3105 battery monitor */ + .board_info = { + .type = "ug3105", + .addr = 0x70, + .dev_name = "ug3105", + .swnode = &asus_tf103c_ug3105_node, + }, + .adapter_path = "\\_SB_.I2C1", + }, { + /* ak09911 compass */ + .board_info = { + .type = "ak09911", + .addr = 0x0c, + .dev_name = "ak09911", + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* kxtj21009 accelerometer */ + .board_info = { + .type = "kxtj21009", + .addr = 0x0f, + .dev_name = "kxtj21009", + .swnode = &asus_tf103c_accel_node, + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* atmel touchscreen */ + .board_info = { + .type = "atmel_mxt_ts", + .addr = 0x4a, + .dev_name = "atmel_mxt_ts", + .swnode = &asus_tf103c_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 28, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "atmel_mxt_ts_irq", + }, + }, +}; + +const struct x86_dev_info asus_tf103c_info __initconst = { + .i2c_client_info = asus_tf103c_i2c_clients, + .i2c_client_count = ARRAY_SIZE(asus_tf103c_i2c_clients), + .pdev_info = asus_me176c_tf103c_pdevs, + .pdev_count = ARRAY_SIZE(asus_me176c_tf103c_pdevs), + .gpio_button_swnodes = asus_me176c_tf103c_lid_swnodes, + .swnode_group = generic_lipo_4v2_battery_swnodes, + .modules = bq24190_modules, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; diff --git a/drivers/platform/x86/x86-android-tablets/core.c b/drivers/platform/x86/x86-android-tablets/core.c new file mode 100644 index 000000000..5db794d65 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/core.c @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DMI based code to deal with broken DSDTs on X86 tablets which ship with + * Android as (part of) the factory image. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "x86-android-tablets.h" +#include "../serdev_helpers.h" + +static struct platform_device *x86_android_tablet_device; + +/* + * This helper allows getting a GPIO descriptor *before* the actual device + * consuming it has been instantiated. This function MUST only be used to + * handle this special case such as, e.g.: + * + * 1. Getting an IRQ from a GPIO for i2c_board_info.irq which is passed to + * i2c_client_new() to instantiate i2c_client-s; or + * 2. Calling desc_to_gpio() to get an old style GPIO number for gpio-keys + * platform_data which still uses old style GPIO numbers. + * + * Since the consuming device has not been instantiated yet a dynamic lookup + * is generated using the special x86_android_tablet device for dev_id. + * + * For normal GPIO lookups a standard static struct gpiod_lookup_table MUST be used. + */ +int x86_android_tablet_get_gpiod(const char *chip, int pin, const char *con_id, + bool active_low, enum gpiod_flags dflags, + struct gpio_desc **desc) +{ + struct gpiod_lookup_table *lookup; + struct gpio_desc *gpiod; + + lookup = kzalloc_flex(*lookup, table, 2); + if (!lookup) + return -ENOMEM; + + lookup->dev_id = KBUILD_MODNAME; + lookup->table[0] = + GPIO_LOOKUP(chip, pin, con_id, active_low ? GPIO_ACTIVE_LOW : GPIO_ACTIVE_HIGH); + + gpiod_add_lookup_table(lookup); + gpiod = devm_gpiod_get(&x86_android_tablet_device->dev, con_id, dflags); + gpiod_remove_lookup_table(lookup); + kfree(lookup); + + if (IS_ERR(gpiod)) { + pr_err("error %ld getting GPIO %s %d\n", PTR_ERR(gpiod), chip, pin); + return PTR_ERR(gpiod); + } + + if (desc) + *desc = gpiod; + + return 0; +} + +int x86_acpi_irq_helper_get(const struct x86_acpi_irq_data *data) +{ + struct irq_fwspec fwspec = { }; + struct irq_domain *domain; + struct acpi_device *adev; + struct gpio_desc *gpiod; + unsigned int irq_type; + acpi_handle handle; + acpi_status status; + int irq, ret; + + switch (data->type) { + case X86_ACPI_IRQ_TYPE_APIC: + /* + * The DSDT may already reference the GSI in a device skipped by + * acpi_quirk_skip_i2c_client_enumeration(). Unregister the GSI + * to avoid -EBUSY errors in this case. + */ + acpi_unregister_gsi(data->index); + irq = acpi_register_gsi(NULL, data->index, data->trigger, data->polarity); + if (irq < 0) + pr_err("error %d getting APIC IRQ %d\n", irq, data->index); + + return irq; + case X86_ACPI_IRQ_TYPE_GPIOINT: + /* Like acpi_dev_gpio_irq_get(), but without parsing ACPI resources */ + ret = x86_android_tablet_get_gpiod(data->chip, data->index, data->con_id, + false, GPIOD_ASIS, &gpiod); + if (ret) + return ret; + + irq = gpiod_to_irq(gpiod); + if (irq < 0) { + pr_err("error %d getting IRQ %s %d\n", irq, data->chip, data->index); + return irq; + } + + irq_type = acpi_dev_get_irq_type(data->trigger, data->polarity); + if (irq_type != IRQ_TYPE_NONE && irq_type != irq_get_trigger_type(irq)) + irq_set_irq_type(irq, irq_type); + + if (data->free_gpio) + devm_gpiod_put(&x86_android_tablet_device->dev, gpiod); + + return irq; + case X86_ACPI_IRQ_TYPE_PMIC: + status = acpi_get_handle(NULL, data->chip, &handle); + if (ACPI_FAILURE(status)) { + pr_err("error could not get %s handle\n", data->chip); + return -ENODEV; + } + + adev = acpi_fetch_acpi_dev(handle); + if (!adev) { + pr_err("error could not get %s adev\n", data->chip); + return -ENODEV; + } + + fwspec.fwnode = acpi_fwnode_handle(adev); + domain = irq_find_matching_fwspec(&fwspec, data->domain); + if (!domain) { + pr_err("error could not find IRQ domain for %s\n", data->chip); + return -ENODEV; + } + + return irq_create_mapping(domain, data->index); + default: + return 0; + } +} + +static int i2c_client_count; +static int spi_dev_count; +static int pdev_count; +static int serdev_count; +static struct i2c_client **i2c_clients; +static struct spi_device **spi_devs; +static struct platform_device **pdevs; +static struct serdev_device **serdevs; +static const struct software_node **gpio_button_swnodes; +static const struct software_node **swnode_group; +static const struct software_node **gpiochip_node_group; +static void (*exit_handler)(void); + +static __init struct i2c_adapter * +get_i2c_adap_by_handle(const struct x86_i2c_client_info *client_info) +{ + acpi_handle handle; + acpi_status status; + + status = acpi_get_handle(NULL, client_info->adapter_path, &handle); + if (ACPI_FAILURE(status)) { + pr_err("Error could not get %s handle\n", client_info->adapter_path); + return NULL; + } + + return i2c_acpi_find_adapter_by_handle(handle); +} + +static __init int match_parent(struct device *dev, const void *data) +{ + return dev->parent == data; +} + +static __init struct i2c_adapter * +get_i2c_adap_by_pci_parent(const struct x86_i2c_client_info *client_info) +{ + struct i2c_adapter *adap = NULL; + struct device *pdev, *adap_dev; + + pdev = bus_find_device_by_name(&pci_bus_type, NULL, client_info->adapter_path); + if (!pdev) { + pr_err("Error could not find %s PCI device\n", client_info->adapter_path); + return NULL; + } + + adap_dev = bus_find_device(&i2c_bus_type, NULL, pdev, match_parent); + if (adap_dev) { + adap = i2c_verify_adapter(adap_dev); + if (!adap) + put_device(adap_dev); + } + + put_device(pdev); + + return adap; +} + +static __init int x86_instantiate_i2c_client(const struct x86_dev_info *dev_info, + int idx) +{ + const struct x86_i2c_client_info *client_info = &dev_info->i2c_client_info[idx]; + struct i2c_board_info board_info = client_info->board_info; + struct i2c_adapter *adap; + + board_info.irq = x86_acpi_irq_helper_get(&client_info->irq_data); + if (board_info.irq < 0) + return board_info.irq; + + if (dev_info->use_pci) + adap = get_i2c_adap_by_pci_parent(client_info); + else + adap = get_i2c_adap_by_handle(client_info); + + if (!adap) { + pr_err("error could not get %s adapter\n", client_info->adapter_path); + return -ENODEV; + } + + i2c_clients[idx] = i2c_new_client_device(adap, &board_info); + put_device(&adap->dev); + if (IS_ERR(i2c_clients[idx])) + return dev_err_probe(&adap->dev, PTR_ERR(i2c_clients[idx]), + "creating I2C-client %d\n", idx); + + return 0; +} + +static __init int x86_instantiate_spi_dev(const struct x86_dev_info *dev_info, int idx) +{ + const struct x86_spi_dev_info *spi_dev_info = &dev_info->spi_dev_info[idx]; + struct spi_board_info board_info = spi_dev_info->board_info; + struct spi_controller *controller; + struct acpi_device *adev; + acpi_handle handle; + acpi_status status; + + board_info.irq = x86_acpi_irq_helper_get(&spi_dev_info->irq_data); + if (board_info.irq < 0) + return board_info.irq; + + status = acpi_get_handle(NULL, spi_dev_info->ctrl_path, &handle); + if (ACPI_FAILURE(status)) { + pr_err("Error could not get %s handle\n", spi_dev_info->ctrl_path); + return -ENODEV; + } + + adev = acpi_fetch_acpi_dev(handle); + if (!adev) { + pr_err("Error could not get adev for %s\n", spi_dev_info->ctrl_path); + return -ENODEV; + } + + controller = acpi_spi_find_controller_by_adev(adev); + if (!controller) { + pr_err("Error could not get SPI controller for %s\n", spi_dev_info->ctrl_path); + return -ENODEV; + } + + spi_devs[idx] = spi_new_device(controller, &board_info); + put_device(&controller->dev); + if (!spi_devs[idx]) + return -ENOMEM; + + return 0; +} + +static __init struct device * +get_serdev_controller_by_pci_parent(const struct x86_serdev_info *info) +{ + struct pci_dev *pdev; + + pdev = pci_get_domain_bus_and_slot(0, 0, info->ctrl.pci.devfn); + if (!pdev) { + pr_err("error could not get PCI serdev at devfn 0x%02x\n", info->ctrl.pci.devfn); + return ERR_PTR(-ENODEV); + } + + /* This puts our reference on pdev and returns a ref on the ctrl */ + return get_serdev_controller_from_parent(&pdev->dev, 0, info->ctrl_devname); +} + +static __init int x86_instantiate_serdev(const struct x86_dev_info *dev_info, int idx) +{ + const struct x86_serdev_info *info = &dev_info->serdev_info[idx]; + struct acpi_device *serdev_adev; + struct serdev_device *serdev; + struct device *ctrl_dev; + int ret = -ENODEV; + + if (dev_info->use_pci) + ctrl_dev = get_serdev_controller_by_pci_parent(info); + else + ctrl_dev = get_serdev_controller(info->ctrl.acpi.hid, info->ctrl.acpi.uid, + 0, info->ctrl_devname); + if (IS_ERR(ctrl_dev)) + return PTR_ERR(ctrl_dev); + + serdev_adev = acpi_dev_get_first_match_dev(info->serdev_hid, NULL, -1); + if (!serdev_adev) { + pr_err("error could not get %s serdev adev\n", info->serdev_hid); + goto put_ctrl_dev; + } + + serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); + if (!serdev) { + ret = -ENOMEM; + goto put_serdev_adev; + } + + ACPI_COMPANION_SET(&serdev->dev, serdev_adev); + acpi_device_set_enumerated(serdev_adev); + + ret = serdev_device_add(serdev); + if (ret) { + dev_err(&serdev->dev, "error %d adding serdev\n", ret); + serdev_device_put(serdev); + goto put_serdev_adev; + } + + serdevs[idx] = serdev; + +put_serdev_adev: + acpi_dev_put(serdev_adev); +put_ctrl_dev: + put_device(ctrl_dev); + return ret; +} + +const struct software_node baytrail_gpiochip_nodes[] = { + { .name = "INT33FC:00" }, + { .name = "INT33FC:01" }, + { .name = "INT33FC:02" }, +}; + +static const struct software_node *baytrail_gpiochip_node_group[] = { + &baytrail_gpiochip_nodes[0], + &baytrail_gpiochip_nodes[1], + &baytrail_gpiochip_nodes[2], + NULL +}; + +const struct software_node cherryview_gpiochip_nodes[] = { + { .name = "INT33FF:00" }, + { .name = "INT33FF:01" }, + { .name = "INT33FF:02" }, + { .name = "INT33FF:03" }, +}; + +static const struct software_node *cherryview_gpiochip_node_group[] = { + &cherryview_gpiochip_nodes[0], + &cherryview_gpiochip_nodes[1], + &cherryview_gpiochip_nodes[2], + &cherryview_gpiochip_nodes[3], + NULL +}; + +static void gpio_secondary_unset(void *data) +{ + struct device *dev = data; + + set_secondary_fwnode(dev, NULL); +} + +static void gpio_secondary_unregister_node_group(void *data) +{ + const struct software_node **nodes = data; + + software_node_unregister_node_group(nodes); +} + +static int gpio_secondary_fwnode_init(struct device *parent) +{ + const struct software_node *const *swnode; + struct fwnode_handle *fwnode; + int ret; + + if (!gpiochip_node_group) + return 0; + + ret = software_node_register_node_group(gpiochip_node_group); + if (ret) + return ret; + + ret = devm_add_action_or_reset(parent, + gpio_secondary_unregister_node_group, + gpiochip_node_group); + if (ret) + return ret; + + for (swnode = gpiochip_node_group; *swnode; swnode++) { + struct device *dev __free(put_device) = + acpi_bus_find_device_by_name((*swnode)->name); + if (!dev) + return dev_err_probe(parent, + -ENODEV, "Failed to find the required GPIO controller: %s\n", + (*swnode)->name); + + fwnode = software_node_fwnode(*swnode); + if (WARN_ON(!fwnode)) + return -ENOENT; + + set_secondary_fwnode(dev, fwnode); + + ret = devm_add_action_or_reset(parent, gpio_secondary_unset, dev); + if (ret) + return ret; + } + + return 0; +} + +static void x86_android_tablet_remove(struct platform_device *pdev) +{ + int i; + + for (i = serdev_count - 1; i >= 0; i--) { + if (serdevs[i]) + serdev_device_remove(serdevs[i]); + } + + kfree(serdevs); + + for (i = pdev_count - 1; i >= 0; i--) + platform_device_unregister(pdevs[i]); + + kfree(pdevs); + + for (i = spi_dev_count - 1; i >= 0; i--) + spi_unregister_device(spi_devs[i]); + + kfree(spi_devs); + + for (i = i2c_client_count - 1; i >= 0; i--) + i2c_unregister_device(i2c_clients[i]); + + kfree(i2c_clients); + + if (exit_handler) + exit_handler(); + + software_node_unregister_node_group(gpio_button_swnodes); + software_node_unregister_node_group(swnode_group); +} + +static __init int x86_android_tablet_probe(struct platform_device *pdev) +{ + const struct x86_dev_info *dev_info; + const struct dmi_system_id *id; + int i, ret = 0; + + id = dmi_first_match(x86_android_tablet_ids); + if (!id) + return -ENODEV; + + dev_info = id->driver_data; + /* Allow x86_android_tablet_device use before probe() exits */ + x86_android_tablet_device = pdev; + + /* + * Since this runs from module_init() it cannot use -EPROBE_DEFER, + * instead pre-load any modules which are listed as requirements. + */ + for (i = 0; dev_info->modules && dev_info->modules[i]; i++) + request_module(dev_info->modules[i]); + + switch (dev_info->gpiochip_type) { + case X86_GPIOCHIP_BAYTRAIL: + gpiochip_node_group = baytrail_gpiochip_node_group; + break; + case X86_GPIOCHIP_CHERRYVIEW: + gpiochip_node_group = cherryview_gpiochip_node_group; + break; + case X86_GPIOCHIP_UNSPECIFIED: + gpiochip_node_group = NULL; + break; + } + + ret = gpio_secondary_fwnode_init(&pdev->dev); + if (ret) { + x86_android_tablet_remove(pdev); + return ret; + } + + ret = software_node_register_node_group(dev_info->swnode_group); + if (ret) { + x86_android_tablet_remove(pdev); + return ret; + } + swnode_group = dev_info->swnode_group; + + if (dev_info->init) { + ret = dev_info->init(&pdev->dev); + if (ret < 0) { + x86_android_tablet_remove(pdev); + return ret; + } + exit_handler = dev_info->exit; + } + + i2c_clients = kzalloc_objs(*i2c_clients, dev_info->i2c_client_count); + if (!i2c_clients) { + x86_android_tablet_remove(pdev); + return -ENOMEM; + } + + i2c_client_count = dev_info->i2c_client_count; + for (i = 0; i < i2c_client_count; i++) { + ret = x86_instantiate_i2c_client(dev_info, i); + if (ret < 0) { + x86_android_tablet_remove(pdev); + return ret; + } + } + + spi_devs = kzalloc_objs(*spi_devs, dev_info->spi_dev_count); + if (!spi_devs) { + x86_android_tablet_remove(pdev); + return -ENOMEM; + } + + spi_dev_count = dev_info->spi_dev_count; + for (i = 0; i < spi_dev_count; i++) { + ret = x86_instantiate_spi_dev(dev_info, i); + if (ret < 0) { + x86_android_tablet_remove(pdev); + return ret; + } + } + + /* + 1 to make space for the (optional) gpio_keys_button platform device */ + pdevs = kzalloc_objs(*pdevs, dev_info->pdev_count + 1); + if (!pdevs) { + x86_android_tablet_remove(pdev); + return -ENOMEM; + } + + pdev_count = dev_info->pdev_count; + for (i = 0; i < pdev_count; i++) { + pdevs[i] = platform_device_register_full(&dev_info->pdev_info[i]); + if (IS_ERR(pdevs[i])) { + ret = PTR_ERR(pdevs[i]); + x86_android_tablet_remove(pdev); + return ret; + } + } + + serdevs = kzalloc_objs(*serdevs, dev_info->serdev_count); + if (!serdevs) { + x86_android_tablet_remove(pdev); + return -ENOMEM; + } + + serdev_count = dev_info->serdev_count; + for (i = 0; i < serdev_count; i++) { + ret = x86_instantiate_serdev(dev_info, i); + if (ret < 0) { + x86_android_tablet_remove(pdev); + return ret; + } + } + + if (dev_info->gpio_button_swnodes) { + struct platform_device_info button_info = { + .name = "gpio-keys", + .id = PLATFORM_DEVID_AUTO, + }; + + ret = software_node_register_node_group(dev_info->gpio_button_swnodes); + if (ret < 0) { + x86_android_tablet_remove(pdev); + return ret; + } + + gpio_button_swnodes = dev_info->gpio_button_swnodes; + + button_info.fwnode = software_node_fwnode(dev_info->gpio_button_swnodes[0]); + pdevs[pdev_count] = platform_device_register_full(&button_info); + if (IS_ERR(pdevs[pdev_count])) { + ret = PTR_ERR(pdevs[pdev_count]); + x86_android_tablet_remove(pdev); + return ret; + } + pdev_count++; + } + + return 0; +} + +static struct platform_driver x86_android_tablet_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .remove = x86_android_tablet_remove, +}; + +static int __init x86_android_tablet_init(void) +{ + x86_android_tablet_device = platform_create_bundle(&x86_android_tablet_driver, + x86_android_tablet_probe, + NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(x86_android_tablet_device); +} +module_init(x86_android_tablet_init); + +static void __exit x86_android_tablet_exit(void) +{ + platform_device_unregister(x86_android_tablet_device); + platform_driver_unregister(&x86_android_tablet_driver); +} +module_exit(x86_android_tablet_exit); + +MODULE_AUTHOR("Hans de Goede "); +MODULE_DESCRIPTION("X86 Android tablets DSDT fixups driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/x86-android-tablets/dmi.c b/drivers/platform/x86/x86-android-tablets/dmi.c new file mode 100644 index 000000000..2ab53030e --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/dmi.c @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DMI based code to deal with broken DSDTs on X86 tablets which ship with + * Android as (part of) the factory image. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#include +#include +#include + +#include "x86-android-tablets.h" + +const struct dmi_system_id x86_android_tablet_ids[] __initconst = { + { + /* Acer Iconia One 8 A1-840 (non FHD version) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "BayTrail"), + /* Above strings are too generic also match BIOS date */ + DMI_MATCH(DMI_BIOS_DATE, "04/01/2014"), + }, + .driver_data = (void *)&acer_a1_840_info, + }, + { + /* Acer Iconia One 7 B1-750 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "VESPA2"), + }, + .driver_data = (void *)&acer_b1_750_info, + }, + { + /* Advantech MICA-071 */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Advantech"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "MICA-071"), + }, + .driver_data = (void *)&advantech_mica_071_info, + }, + { + /* Asus MeMO Pad 7 ME176C */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ME176C"), + }, + .driver_data = (void *)&asus_me176c_info, + }, + { + /* Asus TF103C */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."), + DMI_MATCH(DMI_PRODUCT_NAME, "TF103C"), + }, + .driver_data = (void *)&asus_tf103c_info, + }, + { + /* Chuwi Hi8 (CWI509) */ + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), + DMI_MATCH(DMI_BOARD_NAME, "BYT-PA03C"), + DMI_MATCH(DMI_SYS_VENDOR, "ilife"), + DMI_MATCH(DMI_PRODUCT_NAME, "S806"), + }, + .driver_data = (void *)&chuwi_hi8_info, + }, + { + /* Cyberbook T116 Android version */ + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Default string"), + DMI_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + /* Above strings are much too generic, also match on SKU + BIOS date */ + DMI_MATCH(DMI_PRODUCT_SKU, "20170531"), + DMI_MATCH(DMI_BIOS_DATE, "07/12/2017"), + }, + .driver_data = (void *)&cyberbook_t116_info, + }, + { + /* CZC P10T */ + .ident = "CZC ODEON TPC-10 (\"P10T\")", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "CZC"), + DMI_MATCH(DMI_PRODUCT_NAME, "ODEON*TPC-10"), + }, + .driver_data = (void *)&czc_p10t, + }, + { + /* CZC P10T variant */ + .ident = "ViewSonic ViewPad 10", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ViewSonic"), + DMI_MATCH(DMI_PRODUCT_NAME, "VPAD10"), + }, + .driver_data = (void *)&czc_p10t, + }, + { + /* Lenovo Yoga Book X90F / X90L */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel Corporation"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "CHERRYVIEW D1 PLATFORM"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "YETI-11"), + }, + .driver_data = (void *)&lenovo_yogabook_x90_info, + }, + { + /* Lenovo Yoga Book X91F / X91L */ + .matches = { + /* Inexact match to match F + L versions */ + DMI_MATCH(DMI_PRODUCT_NAME, "Lenovo YB1-X91"), + }, + .driver_data = (void *)&lenovo_yogabook_x91_info, + }, + { + /* + * Lenovo Yoga Tablet 2 Pro 1380F/L (13") + * This has more or less the same BIOS as the 830F/L or 1050F/L + * (8" and 10") below, but unlike the 8"/10" models which share + * the same mainboard this model has a different mainboard. + * This match for the 13" model MUST come before the 8" + 10" + * match since that one will also match the 13" model! + */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel Corp."), + DMI_MATCH(DMI_PRODUCT_NAME, "VALLEYVIEW C0 PLATFORM"), + DMI_MATCH(DMI_BOARD_NAME, "BYT-T FFD8"), + /* Full match so as to NOT match the 830/1050 BIOS */ + DMI_MATCH(DMI_BIOS_VERSION, "BLADE_21.X64.0005.R00.1504101516"), + }, + .driver_data = (void *)&lenovo_yoga_tab2_1380_info, + }, + { + /* + * Lenovo Yoga Tablet 2 830F/L or 1050F/L + * The 8" and 10" Lenovo Yoga Tablet 2 use the same mainboard. + */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel Corp."), + DMI_MATCH(DMI_PRODUCT_NAME, "VALLEYVIEW C0 PLATFORM"), + DMI_MATCH(DMI_BOARD_NAME, "BYT-T FFD8"), + /* Partial match on beginning of BIOS version */ + DMI_MATCH(DMI_BIOS_VERSION, "BLADE_21"), + }, + .driver_data = (void *)&lenovo_yoga_tab2_830_1050_info, + }, + { + /* Lenovo Yoga Tab 3 Pro YT3-X90F */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel Corporation"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Blade3-10A-001"), + }, + .driver_data = (void *)&lenovo_yt3_info, + }, + { + /* Medion Lifetab S10346 */ + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_MATCH(DMI_BOARD_NAME, "Aptio CRB"), + /* Above strings are much too generic, also match on BIOS date */ + DMI_MATCH(DMI_BIOS_DATE, "10/22/2015"), + }, + .driver_data = (void *)&medion_lifetab_s10346_info, + }, + { + /* Nextbook Ares 8 (BYT version) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "M890BAP"), + }, + .driver_data = (void *)&nextbook_ares8_info, + }, + { + /* Nextbook Ares 8A (CHT version) */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Insyde"), + DMI_MATCH(DMI_PRODUCT_NAME, "CherryTrail"), + DMI_MATCH(DMI_BIOS_VERSION, "M882"), + }, + .driver_data = (void *)&nextbook_ares8a_info, + }, + { + /* Peaq C1010 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PEAQ"), + DMI_MATCH(DMI_PRODUCT_NAME, "PEAQ PMM C1010 MD99187"), + }, + .driver_data = (void *)&peaq_c1010_info, + }, + { + /* Vexia Edu Atla 10 tablet 5V version */ + .matches = { + /* Having all 3 of these not set is somewhat unique */ + DMI_MATCH(DMI_SYS_VENDOR, "To be filled by O.E.M."), + DMI_MATCH(DMI_PRODUCT_NAME, "To be filled by O.E.M."), + DMI_MATCH(DMI_BOARD_NAME, "To be filled by O.E.M."), + /* Above strings are too generic, also match on BIOS date */ + DMI_MATCH(DMI_BIOS_DATE, "05/14/2015"), + }, + .driver_data = (void *)&vexia_edu_atla10_5v_info, + }, + { + /* Vexia Edu Atla 10 tablet 9V version */ + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_MATCH(DMI_BOARD_NAME, "Aptio CRB"), + /* Above strings are too generic, also match on BIOS date */ + DMI_MATCH(DMI_BIOS_DATE, "08/25/2014"), + }, + .driver_data = (void *)&vexia_edu_atla10_9v_info, + }, + { + /* Whitelabel (sold as various brands) TM800A550L */ + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AMI Corporation"), + DMI_MATCH(DMI_BOARD_NAME, "Aptio CRB"), + /* Above strings are too generic, also match on BIOS version */ + DMI_MATCH(DMI_BIOS_VERSION, "ZY-8-BI-PX4S70VTR400-X423B-005-D"), + }, + .driver_data = (void *)&whitelabel_tm800a550l_info, + }, + { + /* Xiaomi Mi Pad 2 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Xiaomi Inc"), + DMI_MATCH(DMI_PRODUCT_NAME, "Mipad2"), + }, + .driver_data = (void *)&xiaomi_mipad2_info, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, x86_android_tablet_ids); diff --git a/drivers/platform/x86/x86-android-tablets/lenovo.c b/drivers/platform/x86/x86-android-tablets/lenovo.c new file mode 100644 index 000000000..8d825e0b4 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/lenovo.c @@ -0,0 +1,1079 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Board info for Lenovo X86 tablets which ship with Android as the factory image + * and which have broken DSDT tables. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shared-psy-info.h" +#include "x86-android-tablets.h" + +/* + * Various Lenovo models use a TI LP8557 LED backlight controller with its PWM + * input connected to a PWM output coming from the LCD panel's controller. + * The Android kernels have a hack in the i915 driver to write a non-standard + * panel specific DSI register to set the duty-cycle of the LCD's PWM output. + * + * To avoid having to have a similar hack in the mainline kernel program the + * LP8557 to directly set the level and use the lp855x_bl driver for control. + * + * The LP8557 can either be configured to multiply its PWM input and + * the I2C register set level (requiring both to be at 100% for 100% output); + * or to only take the I2C register set level into account. + * + * Multiplying the 2 levels is useful because this will turn off the backlight + * when the panel goes off and turns off its PWM output. + * + * But on some models the panel's PWM output defaults to a duty-cycle of + * much less then 100%, severely limiting max brightness. In this case + * the LP8557 should be configured to only take the I2C register into + * account and the i915 driver must turn off the panel and the backlight + * separately using e.g. VBT MIPI sequences to turn off the backlight. + */ +static struct lp855x_platform_data lenovo_lp8557_pwm_and_reg_pdata = { + .device_control = 0x86, + .initial_brightness = 128, +}; + +static struct lp855x_platform_data lenovo_lp8557_reg_only_pdata = { + .device_control = 0x85, + .initial_brightness = 128, +}; + +static const struct software_node arizona_gpiochip_node = { + .name = "arizona", +}; + +static const struct software_node crystalcove_gpiochip_node = { + .name = "gpio_crystalcove", +}; + +/* Lenovo Yoga Book X90F / X90L's Android factory image has everything hardcoded */ + +static const struct property_entry lenovo_yb1_x90_goodix_props[] = { + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[1], 53, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("irq-gpios", &cherryview_gpiochip_nodes[1], 56, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node lenovo_yb1_x90_goodix_node = { + .properties = lenovo_yb1_x90_goodix_props, +}; + +static const struct property_entry lenovo_yb1_x90_wacom_props[] = { + PROPERTY_ENTRY_U32("hid-descr-addr", 0x0001), + PROPERTY_ENTRY_U32("post-reset-deassert-delay-ms", 150), + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[0], 82, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node lenovo_yb1_x90_wacom_node = { + .properties = lenovo_yb1_x90_wacom_props, +}; + +/* + * The HiDeep IST940E touchscreen comes up in I2C-HID mode. The native protocol + * reports ABS_MT_PRESSURE and ABS_MT_TOUCH_MAJOR which are not reported in HID + * mode, so using native mode is preferred. + * It could alternatively be used in HID mode by changing the properties to: + * PROPERTY_ENTRY_U32("hid-descr-addr", 0x0020), + * PROPERTY_ENTRY_U32("post-reset-deassert-delay-ms", 120), + * and changing board_info.type to "hid-over-i2c". + */ +static const struct property_entry lenovo_yb1_x90_hideep_ts_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1200), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1920), + PROPERTY_ENTRY_U32("touchscreen-max-pressure", 16384), + PROPERTY_ENTRY_BOOL("hideep,force-native-protocol"), + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[0], 7, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node lenovo_yb1_x90_hideep_ts_node = { + .properties = lenovo_yb1_x90_hideep_ts_props, +}; + +static const struct x86_i2c_client_info lenovo_yb1_x90_i2c_clients[] __initconst = { + { + /* BQ27542 fuel-gauge */ + .board_info = { + .type = "bq27542", + .addr = 0x55, + .dev_name = "bq27542", + .swnode = &fg_bq25890_supply_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + }, { + /* Goodix Touchscreen in keyboard half */ + .board_info = { + .type = "GDIX1001:00", + .addr = 0x14, + .dev_name = "goodix_ts", + .swnode = &lenovo_yb1_x90_goodix_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C2", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:01", + .index = 56, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "goodix_ts_irq", + .free_gpio = true, + }, + }, { + /* Wacom Digitizer in keyboard half */ + .board_info = { + .type = "hid-over-i2c", + .addr = 0x09, + .dev_name = "wacom", + .swnode = &lenovo_yb1_x90_wacom_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:01", + .index = 49, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "wacom_irq", + }, + }, { + /* LP8557 Backlight controller */ + .board_info = { + .type = "lp8557", + .addr = 0x2c, + .dev_name = "lp8557", + .platform_data = &lenovo_lp8557_pwm_and_reg_pdata, + }, + .adapter_path = "\\_SB_.PCI0.I2C4", + }, { + /* HiDeep IST940E Touchscreen in display half */ + .board_info = { + .type = "hideep_ts", + .addr = 0x6c, + .dev_name = "hideep_ts", + .swnode = &lenovo_yb1_x90_hideep_ts_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:03", + .index = 77, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "hideep_ts_irq", + }, + }, +}; + +static const struct platform_device_info lenovo_yb1_x90_pdevs[] __initconst = { + { + .name = "yogabook-touch-kbd-digitizer-switch", + .id = PLATFORM_DEVID_NONE, + }, +}; + +/* + * DSDT says UART path is "\\_SB.PCIO.URT1" with a letter 'O' instead of + * the number '0' add the link manually. + */ +static const struct x86_serdev_info lenovo_yb1_x90_serdevs[] __initconst = { + { + .ctrl.acpi.hid = "8086228A", + .ctrl.acpi.uid = "1", + .ctrl_devname = "serial0", + .serdev_hid = "BCM2E1A", + }, +}; + +/* + * Software node attached to gpio-keys device representing the LID and + * serving as a parent to software nodes representing individual keys/buttons + * as required by the device tree binding. + */ +static const struct software_node lenovo_lid_gpio_keys_node = { + .name = "lid_sw", +}; + +static const struct property_entry lenovo_yb1_x90_lid_props[] = { + PROPERTY_ENTRY_U32("linux,input-type", EV_SW), + PROPERTY_ENTRY_U32("linux,code", SW_LID), + PROPERTY_ENTRY_STRING("label", "lid_sw"), + PROPERTY_ENTRY_GPIO("gpios", &cherryview_gpiochip_nodes[2], 19, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + PROPERTY_ENTRY_BOOL("wakeup-source"), + { } +}; + +static const struct software_node lenovo_yb1_x90_lid_node = { + .parent = &lenovo_lid_gpio_keys_node, + .properties = lenovo_yb1_x90_lid_props, +}; + +static const struct software_node *lenovo_yb1_x90_lid_swnodes[] = { + &lenovo_lid_gpio_keys_node, + &lenovo_yb1_x90_lid_node, + NULL +}; + +static int __init lenovo_yb1_x90_init(struct device *dev) +{ + /* Enable the regulators used by the touchscreens */ + + /* Vprog3B 3.0V used by the goodix touchscreen in the keyboard half */ + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0x9b, 0x02, 0xff); + + /* Vprog4D 3.0V used by the HiDeep touchscreen in the display half */ + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0x9f, 0x02, 0xff); + + /* Vprog5A 1.8V used by the HiDeep touchscreen in the display half */ + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0xa0, 0x02, 0xff); + + /* Vprog5B 1.8V used by the goodix touchscreen in the keyboard half */ + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0xa1, 0x02, 0xff); + + return 0; +} + +const struct x86_dev_info lenovo_yogabook_x90_info __initconst = { + .i2c_client_info = lenovo_yb1_x90_i2c_clients, + .i2c_client_count = ARRAY_SIZE(lenovo_yb1_x90_i2c_clients), + .pdev_info = lenovo_yb1_x90_pdevs, + .pdev_count = ARRAY_SIZE(lenovo_yb1_x90_pdevs), + .serdev_info = lenovo_yb1_x90_serdevs, + .serdev_count = ARRAY_SIZE(lenovo_yb1_x90_serdevs), + .gpio_button_swnodes = lenovo_yb1_x90_lid_swnodes, + .gpiochip_type = X86_GPIOCHIP_CHERRYVIEW, + .init = lenovo_yb1_x90_init, +}; + +/* Lenovo Yoga Book X91F/L Windows tablet needs manual instantiation of the fuel-gauge client */ +static const struct x86_i2c_client_info lenovo_yogabook_x91_i2c_clients[] __initconst = { + { + /* BQ27542 fuel-gauge */ + .board_info = { + .type = "bq27542", + .addr = 0x55, + .dev_name = "bq27542", + .swnode = &fg_bq25890_supply_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + }, +}; + +const struct x86_dev_info lenovo_yogabook_x91_info __initconst = { + .i2c_client_info = lenovo_yogabook_x91_i2c_clients, + .i2c_client_count = ARRAY_SIZE(lenovo_yogabook_x91_i2c_clients), +}; + +/* Lenovo Yoga Tablet 2 1050F/L's Android factory image has everything hardcoded */ +static const struct property_entry lenovo_yoga_tab2_830_1050_bq24190_props[] = { + PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", tusb1211_chg_det_psy, 1), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_hv_4v35_battery_node), + PROPERTY_ENTRY_BOOL("omit-battery-class"), + PROPERTY_ENTRY_BOOL("disable-reset"), + { } +}; + +static const struct software_node lenovo_yoga_tab2_830_1050_bq24190_node = { + .properties = lenovo_yoga_tab2_830_1050_bq24190_props, +}; + +static const struct property_entry lenovo_yoga_tab2_830_1050_lid_props[] = { + PROPERTY_ENTRY_U32("linux,input-type", EV_SW), + PROPERTY_ENTRY_U32("linux,code", SW_LID), + PROPERTY_ENTRY_STRING("label", "lid_sw"), + PROPERTY_ENTRY_GPIO("gpios", &baytrail_gpiochip_nodes[2], 26, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + PROPERTY_ENTRY_BOOL("wakeup-source"), + { } +}; + +static const struct software_node lenovo_yoga_tab2_830_1050_lid_node = { + .parent = &lenovo_lid_gpio_keys_node, + .properties = lenovo_yoga_tab2_830_1050_lid_props, +}; + +static const struct software_node *lenovo_yoga_tab2_830_1050_lid_swnodes[] = { + &lenovo_lid_gpio_keys_node, + &lenovo_yoga_tab2_830_1050_lid_node, + NULL +}; + +/* This gets filled by lenovo_yoga_tab2_830_1050_init() */ +static struct rmi_device_platform_data lenovo_yoga_tab2_830_1050_rmi_pdata = { }; + +static struct x86_i2c_client_info lenovo_yoga_tab2_830_1050_i2c_clients[] __initdata = { + { + /* + * This must be the first entry because lenovo_yoga_tab2_830_1050_init() + * may update its swnode. LSM303DA accelerometer + magnetometer. + */ + .board_info = { + .type = "lsm303d", + .addr = 0x1d, + .dev_name = "lsm303d", + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* AL3320A ambient light sensor */ + .board_info = { + .type = "al3320a", + .addr = 0x1c, + .dev_name = "al3320a", + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* bq24292i battery charger */ + .board_info = { + .type = "bq24190", + .addr = 0x6b, + .dev_name = "bq24292i", + .swnode = &lenovo_yoga_tab2_830_1050_bq24190_node, + .platform_data = &bq24190_pdata, + }, + .adapter_path = "\\_SB_.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 2, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "bq24292i_irq", + }, + }, { + /* BQ27541 fuel-gauge */ + .board_info = { + .type = "bq27541", + .addr = 0x55, + .dev_name = "bq27541", + .swnode = &fg_bq24190_supply_node, + }, + .adapter_path = "\\_SB_.I2C1", + }, { + /* Synaptics RMI touchscreen */ + .board_info = { + .type = "rmi4_i2c", + .addr = 0x38, + .dev_name = "rmi4_i2c", + .platform_data = &lenovo_yoga_tab2_830_1050_rmi_pdata, + }, + .adapter_path = "\\_SB_.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x45, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + }, { + /* LP8557 Backlight controller */ + .board_info = { + .type = "lp8557", + .addr = 0x2c, + .dev_name = "lp8557", + .platform_data = &lenovo_lp8557_pwm_and_reg_pdata, + }, + .adapter_path = "\\_SB_.I2C3", + }, +}; + +static const struct property_entry lenovo_yoga_tab2_830_1050_int3496_props[] __initconst = { + PROPERTY_ENTRY_GPIO("mux-gpios", &baytrail_gpiochip_nodes[2], 1, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_GPIO("id-gpios", &baytrail_gpiochip_nodes[2], 24, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct platform_device_info lenovo_yoga_tab2_830_1050_pdevs[] __initconst = { + { + /* For micro USB ID pin handling */ + .name = "intel-int3496", + .id = PLATFORM_DEVID_NONE, + .properties = lenovo_yoga_tab2_830_1050_int3496_props, + }, +}; + +#define LENOVO_YOGA_TAB2_830_1050_CODEC_NAME "spi-10WM5102:00" + +static const struct property_entry lenovo_yoga_tab2_830_1050_wm1502_props[] = { + PROPERTY_ENTRY_GPIO("reset-gpios", + &crystalcove_gpiochip_node, 3, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("wlf,ldoena-gpios", + &baytrail_gpiochip_nodes[1], 23, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("wlf,spkvdd-ena-gpios", + &arizona_gpiochip_node, 2, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("wlf,micd-pol-gpios", + &arizona_gpiochip_node, 4, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node lenovo_yoga_tab2_830_1050_wm5102 = { + .properties = lenovo_yoga_tab2_830_1050_wm1502_props, +}; + +static const struct software_node *lenovo_yoga_tab2_830_1050_swnodes[] = { + &crystalcove_gpiochip_node, + &arizona_gpiochip_node, + &lenovo_yoga_tab2_830_1050_wm5102, + &generic_lipo_hv_4v35_battery_node, + NULL +}; + +static int __init lenovo_yoga_tab2_830_1050_init(struct device *dev); +static void lenovo_yoga_tab2_830_1050_exit(void); + +static const char * const lenovo_yoga_tab2_modules[] __initconst = { + "spi_pxa2xx_platform", /* For the SPI codec device */ + "bq24190_charger", /* For the Vbus regulator for int3496/lc824206xa */ + NULL +}; + +const struct x86_dev_info lenovo_yoga_tab2_830_1050_info __initconst = { + .i2c_client_info = lenovo_yoga_tab2_830_1050_i2c_clients, + .i2c_client_count = ARRAY_SIZE(lenovo_yoga_tab2_830_1050_i2c_clients), + .pdev_info = lenovo_yoga_tab2_830_1050_pdevs, + .pdev_count = ARRAY_SIZE(lenovo_yoga_tab2_830_1050_pdevs), + .gpio_button_swnodes = lenovo_yoga_tab2_830_1050_lid_swnodes, + .swnode_group = lenovo_yoga_tab2_830_1050_swnodes, + .modules = lenovo_yoga_tab2_modules, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, + .init = lenovo_yoga_tab2_830_1050_init, + .exit = lenovo_yoga_tab2_830_1050_exit, +}; + +/* + * The Lenovo Yoga Tablet 2 830 and 1050 (8" vs 10") versions use the same + * mainboard, but the 830 uses a portrait LCD panel with a landscape touchscreen, + * requiring the touchscreen driver to adjust the touch-coords to match the LCD. + * And requiring the accelerometer to have a mount-matrix set to correct for + * the 90° rotation of the LCD vs the frame. + */ +static const char * const lenovo_yoga_tab2_830_lms303d_mount_matrix[] = { + "0", "1", "0", + "-1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry lenovo_yoga_tab2_830_lms303d_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", lenovo_yoga_tab2_830_lms303d_mount_matrix), + { } +}; + +static const struct software_node lenovo_yoga_tab2_830_lms303d_node = { + .properties = lenovo_yoga_tab2_830_lms303d_props, +}; + +static int __init lenovo_yoga_tab2_830_1050_init_touchscreen(void) +{ + struct gpio_desc *gpiod; + int ret; + + /* Use PMIC GPIO 10 bootstrap pin to differentiate 830 vs 1050 */ + ret = x86_android_tablet_get_gpiod("gpio_crystalcove", 10, "yoga_bootstrap", + false, GPIOD_ASIS, &gpiod); + if (ret) + return ret; + + ret = gpiod_get_value_cansleep(gpiod); + if (ret) { + pr_info("detected Lenovo Yoga Tablet 2 1050F/L\n"); + } else { + pr_info("detected Lenovo Yoga Tablet 2 830F/L\n"); + lenovo_yoga_tab2_830_1050_rmi_pdata.sensor_pdata.axis_align.swap_axes = true; + lenovo_yoga_tab2_830_1050_rmi_pdata.sensor_pdata.axis_align.flip_y = true; + lenovo_yoga_tab2_830_1050_i2c_clients[0].board_info.swnode = + &lenovo_yoga_tab2_830_lms303d_node; + } + + return 0; +} + +/* SUS (INT33FC:02) pin 6 needs to be configured as pmu_clk for the audio codec */ +static const struct pinctrl_map lenovo_yoga_tab2_830_1050_codec_pinctrl_map = + PIN_MAP_MUX_GROUP(LENOVO_YOGA_TAB2_830_1050_CODEC_NAME, "codec_32khz_clk", + "INT33FC:02", "pmu_clk2_grp", "pmu_clk"); + +static struct device *lenovo_yoga_tab2_830_1050_codec_dev; +static struct pinctrl *lenovo_yoga_tab2_830_1050_codec_pinctrl; +static struct sys_off_handler *lenovo_yoga_tab2_830_1050_sys_off_handler; + +static int __init lenovo_yoga_tab2_830_1050_init_codec(void) +{ + struct device *codec_dev; + struct pinctrl *pinctrl; + int ret; + + codec_dev = bus_find_device_by_name(&spi_bus_type, NULL, + LENOVO_YOGA_TAB2_830_1050_CODEC_NAME); + if (!codec_dev) { + pr_err("error cannot find %s device\n", LENOVO_YOGA_TAB2_830_1050_CODEC_NAME); + return -ENODEV; + } + + ret = pinctrl_register_mappings(&lenovo_yoga_tab2_830_1050_codec_pinctrl_map, 1); + if (ret) + goto err_put_device; + + pinctrl = pinctrl_get_select(codec_dev, "codec_32khz_clk"); + if (IS_ERR(pinctrl)) { + ret = dev_err_probe(codec_dev, PTR_ERR(pinctrl), "selecting codec_32khz_clk\n"); + goto err_unregister_mappings; + } + + ret = device_add_software_node(codec_dev, &lenovo_yoga_tab2_830_1050_wm5102); + if (ret) { + dev_err_probe(codec_dev, ret, "adding software node\n"); + goto err_put_pinctrl; + } + + lenovo_yoga_tab2_830_1050_codec_dev = codec_dev; + lenovo_yoga_tab2_830_1050_codec_pinctrl = pinctrl; + return 0; + +err_put_pinctrl: + pinctrl_put(lenovo_yoga_tab2_830_1050_codec_pinctrl); +err_unregister_mappings: + pinctrl_unregister_mappings(&lenovo_yoga_tab2_830_1050_codec_pinctrl_map); +err_put_device: + put_device(codec_dev); + return ret; +} + +/* + * These tablet's DSDT does not set acpi_gbl_reduced_hardware, so acpi_power_off() + * gets used as pm_power_off handler. This causes "poweroff" on these tablets + * to hang hard. Requiring pressing the power button for 30 seconds *twice* + * followed by a normal 3 second press to recover. Avoid this by doing an EFI + * poweroff instead. + */ +static int lenovo_yoga_tab2_830_1050_power_off(struct sys_off_data *data) +{ + efi.reset_system(EFI_RESET_SHUTDOWN, EFI_SUCCESS, 0, NULL); + + return NOTIFY_DONE; +} + +static int __init lenovo_yoga_tab2_830_1050_init(struct device *dev) +{ + int ret; + + ret = lenovo_yoga_tab2_830_1050_init_touchscreen(); + if (ret) + return ret; + + ret = lenovo_yoga_tab2_830_1050_init_codec(); + if (ret) + return ret; + + /* SYS_OFF_PRIO_FIRMWARE + 1 so that it runs before acpi_power_off() */ + lenovo_yoga_tab2_830_1050_sys_off_handler = + register_sys_off_handler(SYS_OFF_MODE_POWER_OFF, SYS_OFF_PRIO_FIRMWARE + 1, + lenovo_yoga_tab2_830_1050_power_off, NULL); + if (IS_ERR(lenovo_yoga_tab2_830_1050_sys_off_handler)) + return PTR_ERR(lenovo_yoga_tab2_830_1050_sys_off_handler); + + return 0; +} + +static void lenovo_yoga_tab2_830_1050_exit(void) +{ + unregister_sys_off_handler(lenovo_yoga_tab2_830_1050_sys_off_handler); + + device_remove_software_node(lenovo_yoga_tab2_830_1050_codec_dev); + pinctrl_put(lenovo_yoga_tab2_830_1050_codec_pinctrl); + pinctrl_unregister_mappings(&lenovo_yoga_tab2_830_1050_codec_pinctrl_map); + put_device(lenovo_yoga_tab2_830_1050_codec_dev); +} + +/* + * Lenovo Yoga Tablet 2 Pro 1380F/L + * + * The Lenovo Yoga Tablet 2 Pro 1380F/L mostly has the same design as the 830F/L + * and the 1050F/L so this re-uses some of the handling for that from above. + */ +static const char * const lc824206xa_chg_det_psy[] = { "lc824206xa-charger-detect" }; + +static const struct property_entry lenovo_yoga_tab2_1380_bq24190_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", lc824206xa_chg_det_psy), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_hv_4v35_battery_node), + PROPERTY_ENTRY_BOOL("omit-battery-class"), + PROPERTY_ENTRY_BOOL("disable-reset"), + { } +}; + +static const struct software_node lenovo_yoga_tab2_1380_bq24190_node = { + .properties = lenovo_yoga_tab2_1380_bq24190_props, +}; + +/* For enabling the bq24190 5V boost based on id-pin */ +static struct regulator_consumer_supply lc824206xa_consumer = { + .supply = "vbus", + .dev_name = "i2c-lc824206xa", +}; + +static const struct regulator_init_data lenovo_yoga_tab2_1380_bq24190_vbus_init_data = { + .constraints = { + .name = "bq24190_vbus", + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .consumer_supplies = &lc824206xa_consumer, + .num_consumer_supplies = 1, +}; + +static struct bq24190_platform_data lenovo_yoga_tab2_1380_bq24190_pdata = { + .regulator_init_data = &lenovo_yoga_tab2_1380_bq24190_vbus_init_data, +}; + +static const struct property_entry lenovo_yoga_tab2_1380_lc824206xa_props[] = { + PROPERTY_ENTRY_BOOL("onnn,enable-miclr-for-dcp"), + { } +}; + +static const struct software_node lenovo_yoga_tab2_1380_lc824206xa_node = { + .properties = lenovo_yoga_tab2_1380_lc824206xa_props, +}; + +static const char * const lenovo_yoga_tab2_1380_lms303d_mount_matrix[] = { + "0", "-1", "0", + "-1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry lenovo_yoga_tab2_1380_lms303d_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", lenovo_yoga_tab2_1380_lms303d_mount_matrix), + { } +}; + +static const struct software_node lenovo_yoga_tab2_1380_lms303d_node = { + .properties = lenovo_yoga_tab2_1380_lms303d_props, +}; + +static const struct x86_i2c_client_info lenovo_yoga_tab2_1380_i2c_clients[] __initconst = { + { + /* BQ27541 fuel-gauge */ + .board_info = { + .type = "bq27541", + .addr = 0x55, + .dev_name = "bq27541", + .swnode = &fg_bq24190_supply_node, + }, + .adapter_path = "\\_SB_.I2C1", + }, { + /* bq24292i battery charger */ + .board_info = { + .type = "bq24190", + .addr = 0x6b, + .dev_name = "bq24292i", + .swnode = &lenovo_yoga_tab2_1380_bq24190_node, + .platform_data = &lenovo_yoga_tab2_1380_bq24190_pdata, + }, + .adapter_path = "\\_SB_.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 2, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "bq24292i_irq", + }, + }, { + /* LP8557 Backlight controller */ + .board_info = { + .type = "lp8557", + .addr = 0x2c, + .dev_name = "lp8557", + .platform_data = &lenovo_lp8557_pwm_and_reg_pdata, + }, + .adapter_path = "\\_SB_.I2C3", + }, { + /* LC824206XA Micro USB Switch */ + .board_info = { + .type = "lc824206xa", + .addr = 0x48, + .dev_name = "lc824206xa", + .swnode = &lenovo_yoga_tab2_1380_lc824206xa_node, + }, + .adapter_path = "\\_SB_.I2C3", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 1, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "lc824206xa_irq", + }, + }, { + /* AL3320A ambient light sensor */ + .board_info = { + .type = "al3320a", + .addr = 0x1c, + .dev_name = "al3320a", + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* LSM303DA accelerometer + magnetometer */ + .board_info = { + .type = "lsm303d", + .addr = 0x1d, + .dev_name = "lsm303d", + .swnode = &lenovo_yoga_tab2_1380_lms303d_node, + }, + .adapter_path = "\\_SB_.I2C5", + }, { + /* Synaptics RMI touchscreen */ + .board_info = { + .type = "rmi4_i2c", + .addr = 0x38, + .dev_name = "rmi4_i2c", + .platform_data = &lenovo_yoga_tab2_830_1050_rmi_pdata, + }, + .adapter_path = "\\_SB_.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x45, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + } +}; + +static const struct property_entry lenovo_yoga_tab2_1380_fc_props[] __initconst = { + PROPERTY_ENTRY_GPIO("uart3_txd-gpios", &baytrail_gpiochip_nodes[0], 57, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("uart3_rxd-gpios", &baytrail_gpiochip_nodes[0], 61, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct platform_device_info lenovo_yoga_tab2_1380_pdevs[] __initconst = { + { + /* For the Tablet 2 Pro 1380's custom fast charging driver */ + .name = "lenovo-yoga-tab2-pro-1380-fastcharger", + .id = PLATFORM_DEVID_NONE, + .properties = lenovo_yoga_tab2_1380_fc_props, + }, +}; + +static int __init lenovo_yoga_tab2_1380_init(struct device *dev) +{ + int ret; + + /* To verify that the DMI matching works vs the 830 / 1050 models */ + pr_info("detected Lenovo Yoga Tablet 2 Pro 1380F/L\n"); + + ret = lenovo_yoga_tab2_830_1050_init_codec(); + if (ret) + return ret; + + /* SYS_OFF_PRIO_FIRMWARE + 1 so that it runs before acpi_power_off() */ + lenovo_yoga_tab2_830_1050_sys_off_handler = + register_sys_off_handler(SYS_OFF_MODE_POWER_OFF, SYS_OFF_PRIO_FIRMWARE + 1, + lenovo_yoga_tab2_830_1050_power_off, NULL); + if (IS_ERR(lenovo_yoga_tab2_830_1050_sys_off_handler)) + return PTR_ERR(lenovo_yoga_tab2_830_1050_sys_off_handler); + + return 0; +} + +const struct x86_dev_info lenovo_yoga_tab2_1380_info __initconst = { + .i2c_client_info = lenovo_yoga_tab2_1380_i2c_clients, + .i2c_client_count = ARRAY_SIZE(lenovo_yoga_tab2_1380_i2c_clients), + .pdev_info = lenovo_yoga_tab2_1380_pdevs, + .pdev_count = ARRAY_SIZE(lenovo_yoga_tab2_1380_pdevs), + .gpio_button_swnodes = lenovo_yoga_tab2_830_1050_lid_swnodes, + .swnode_group = lenovo_yoga_tab2_830_1050_swnodes, + .modules = lenovo_yoga_tab2_modules, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, + .init = lenovo_yoga_tab2_1380_init, + .exit = lenovo_yoga_tab2_830_1050_exit, +}; + +/* Lenovo Yoga Tab 3 Pro YT3-X90F */ + +/* + * There are 2 batteries, with 2 bq27500 fuel-gauges and 2 bq25892 chargers, + * "bq25890-charger-1" is instantiated from: drivers/i2c/busses/i2c-cht-wc.c. + */ +static const char * const lenovo_yt3_bq25892_0_suppliers[] = { "cht_wcove_pwrsrc" }; +static const char * const bq25890_1_psy[] = { "bq25890-charger-1" }; + +static const struct property_entry fg_bq25890_1_supply_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", bq25890_1_psy), + { } +}; + +static const struct software_node fg_bq25890_1_supply_node = { + .properties = fg_bq25890_1_supply_props, +}; + +/* bq25892 charger settings for the flat LiPo battery behind the screen */ +static const struct property_entry lenovo_yt3_bq25892_0_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", lenovo_yt3_bq25892_0_suppliers), + PROPERTY_ENTRY_U32("linux,iinlim-percentage", 40), + PROPERTY_ENTRY_BOOL("linux,skip-reset"), + /* Values taken from Android Factory Image */ + PROPERTY_ENTRY_U32("ti,charge-current", 2048000), + PROPERTY_ENTRY_U32("ti,battery-regulation-voltage", 4352000), + PROPERTY_ENTRY_U32("ti,termination-current", 128000), + PROPERTY_ENTRY_U32("ti,precharge-current", 128000), + PROPERTY_ENTRY_U32("ti,minimum-sys-voltage", 3700000), + PROPERTY_ENTRY_U32("ti,boost-voltage", 4998000), + PROPERTY_ENTRY_U32("ti,boost-max-current", 500000), + PROPERTY_ENTRY_BOOL("ti,use-ilim-pin"), + { } +}; + +static const struct software_node lenovo_yt3_bq25892_0_node = { + .properties = lenovo_yt3_bq25892_0_props, +}; + +static const struct property_entry lenovo_yt3_hideep_ts_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1600), + PROPERTY_ENTRY_U32("touchscreen-size-y", 2560), + PROPERTY_ENTRY_U32("touchscreen-max-pressure", 255), + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[0], 7, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node lenovo_yt3_hideep_ts_node = { + .properties = lenovo_yt3_hideep_ts_props, +}; + +static const struct x86_i2c_client_info lenovo_yt3_i2c_clients[] __initconst = { + { + /* bq27500 fuel-gauge for the flat LiPo battery behind the screen */ + .board_info = { + .type = "bq27500", + .addr = 0x55, + .dev_name = "bq27500_0", + .swnode = &fg_bq25890_supply_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + }, { + /* bq25892 charger for the flat LiPo battery behind the screen */ + .board_info = { + .type = "bq25892", + .addr = 0x6b, + .dev_name = "bq25892_0", + .swnode = &lenovo_yt3_bq25892_0_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:01", + .index = 5, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "bq25892_0_irq", + }, + }, { + /* bq27500 fuel-gauge for the round Li-ion cells in the hinge */ + .board_info = { + .type = "bq27500", + .addr = 0x55, + .dev_name = "bq27500_1", + .swnode = &fg_bq25890_1_supply_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C2", + }, { + /* HiDeep IST520E Touchscreen */ + .board_info = { + .type = "hideep_ts", + .addr = 0x6c, + .dev_name = "hideep_ts", + .swnode = &lenovo_yt3_hideep_ts_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:03", + .index = 77, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "hideep_ts_irq", + }, + }, { + /* LP8557 Backlight controller */ + .board_info = { + .type = "lp8557", + .addr = 0x2c, + .dev_name = "lp8557", + .platform_data = &lenovo_lp8557_reg_only_pdata, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + } +}; + +/* + * The AOSP 3.5 mm Headset: Accessory Specification gives the following values: + * Function A Play/Pause: 0 ohm + * Function D Voice assistant: 135 ohm + * Function B Volume Up 240 ohm + * Function C Volume Down 470 ohm + * Minimum Mic DC resistance 1000 ohm + * Minimum Ear speaker impedance 16 ohm + * Note the first max value below must be less then the min. speaker impedance, + * to allow CTIA/OMTP detection to work. The other max values are the closest + * value from extcon-arizona.c:arizona_micd_levels halfway 2 button resistances. + */ +static const struct arizona_micd_range arizona_micd_aosp_ranges[] = { + { .max = 11, .key = KEY_PLAYPAUSE }, + { .max = 186, .key = KEY_VOICECOMMAND }, + { .max = 348, .key = KEY_VOLUMEUP }, + { .max = 752, .key = KEY_VOLUMEDOWN }, +}; + +/* YT3 WM5102 arizona_micd_config comes from Android kernel sources */ +static struct arizona_micd_config lenovo_yt3_wm5102_micd_config[] = { + { 0, 1, 0 }, + { ARIZONA_ACCDET_SRC, 2, 1 }, +}; + +static struct arizona_pdata lenovo_yt3_wm5102_pdata = { + .irq_flags = IRQF_TRIGGER_LOW, + .micd_detect_debounce = 200, + .micd_ranges = arizona_micd_aosp_ranges, + .num_micd_ranges = ARRAY_SIZE(arizona_micd_aosp_ranges), + .hpdet_channel = ARIZONA_ACCDET_MODE_HPL, + + /* Below settings come from Android kernel sources */ + .micd_bias_start_time = 1, + .micd_rate = 6, + .micd_configs = lenovo_yt3_wm5102_micd_config, + .num_micd_configs = ARRAY_SIZE(lenovo_yt3_wm5102_micd_config), + .micbias = { + [0] = { /* MICBIAS1 */ + .mV = 2800, + .ext_cap = 1, + .discharge = 1, + .soft_start = 0, + .bypass = 0, + }, + [1] = { /* MICBIAS2 */ + .mV = 2800, + .ext_cap = 1, + .discharge = 1, + .soft_start = 0, + .bypass = 0, + }, + [2] = { /* MICBIAS2 */ + .mV = 2800, + .ext_cap = 1, + .discharge = 1, + .soft_start = 0, + .bypass = 0, + }, + }, +}; + +static const struct property_entry lenovo_yt3_wm1502_props[] = { + PROPERTY_ENTRY_GPIO("wlf,spkvdd-ena-gpios", + &cherryview_gpiochip_nodes[0], 75, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("wlf,ldoena-gpios", + &cherryview_gpiochip_nodes[0], 81, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[0], 82, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("wlf,micd-pol-gpios", &arizona_gpiochip_node, 2, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node lenovo_yt3_wm5102 = { + .properties = lenovo_yt3_wm1502_props, + .name = "wm5102", +}; + +static const struct software_node *lenovo_yt3_swnodes[] = { + &arizona_gpiochip_node, + &lenovo_yt3_wm5102, + NULL +}; + +static const struct x86_spi_dev_info lenovo_yt3_spi_devs[] __initconst = { + { + /* WM5102 codec */ + .board_info = { + .modalias = "wm5102", + .platform_data = &lenovo_yt3_wm5102_pdata, + .swnode = &lenovo_yt3_wm5102, + .max_speed_hz = 5000000, + }, + .ctrl_path = "\\_SB_.PCI0.SPI1", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:00", + .index = 91, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "wm5102_irq", + }, + } +}; + +static int __init lenovo_yt3_init(struct device *dev) +{ + int ret; + + /* + * The "bq25892_0" charger IC has its /CE (Charge-Enable) and OTG pins + * connected to GPIOs, rather then having them hardwired to the correct + * values as is normally done. + * + * The bq25890_charger driver controls these through I2C, but this only + * works if not overridden by the pins. Set these pins here: + * 1. Set /CE to 1 to allow charging. + * 2. Set OTG to 0 disable V5 boost output since the 5V boost output of + * the main "bq25892_1" charger is used when necessary. + */ + + /* /CE pin */ + ret = x86_android_tablet_get_gpiod("INT33FF:02", 22, "bq25892_0_ce", + true, GPIOD_OUT_HIGH, NULL); + if (ret < 0) + return ret; + + /* OTG pin */ + ret = x86_android_tablet_get_gpiod("INT33FF:03", 19, "bq25892_0_otg", + false, GPIOD_OUT_LOW, NULL); + if (ret < 0) + return ret; + + /* Enable the regulators used by the touchscreen */ + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0x9b, 0x02, 0xff); + intel_soc_pmic_exec_mipi_pmic_seq_element(0x6e, 0xa0, 0x02, 0xff); + + return 0; +} + +static const char * const lenovo_yt3_modules[] __initconst = { + "spi_pxa2xx_platform", /* For the SPI codec device */ + NULL +}; + +const struct x86_dev_info lenovo_yt3_info __initconst = { + .i2c_client_info = lenovo_yt3_i2c_clients, + .i2c_client_count = ARRAY_SIZE(lenovo_yt3_i2c_clients), + .spi_dev_info = lenovo_yt3_spi_devs, + .spi_dev_count = ARRAY_SIZE(lenovo_yt3_spi_devs), + .swnode_group = lenovo_yt3_swnodes, + .modules = lenovo_yt3_modules, + .gpiochip_type = X86_GPIOCHIP_CHERRYVIEW, + .init = lenovo_yt3_init, +}; diff --git a/drivers/platform/x86/x86-android-tablets/other.c b/drivers/platform/x86/x86-android-tablets/other.c new file mode 100644 index 000000000..7532af2d7 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/other.c @@ -0,0 +1,883 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DMI based code to deal with broken DSDTs on X86 tablets which ship with + * Android as (part of) the factory image. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "shared-psy-info.h" +#include "x86-android-tablets.h" + +/* + * Advantech MICA-071 + * This is a standard Windows tablet, but it has an extra "quick launch" button + * which is not described in the ACPI tables in anyway. + * Use the x86-android-tablets infra to create a gpio-keys device for this. + */ +static const struct software_node advantech_mica_071_gpio_keys_node = { + .name = "prog1_key", +}; + +static const struct property_entry advantech_mica_071_prog1_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_PROG1), + PROPERTY_ENTRY_STRING("label", "prog1_key"), + PROPERTY_ENTRY_GPIO("gpios", &baytrail_gpiochip_nodes[0], 2, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + { } +}; + +static const struct software_node advantech_mica_071_prog1_key_node = { + .parent = &advantech_mica_071_gpio_keys_node, + .properties = advantech_mica_071_prog1_key_props, +}; + +static const struct software_node *advantech_mica_071_button_swnodes[] = { + &advantech_mica_071_gpio_keys_node, + &advantech_mica_071_prog1_key_node, + NULL +}; + +const struct x86_dev_info advantech_mica_071_info __initconst = { + .gpio_button_swnodes = advantech_mica_071_button_swnodes, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* + * When booted with the BIOS set to Android mode the Chuwi Hi8 (CWI509) DSDT + * contains a whole bunch of bogus ACPI I2C devices and is missing entries + * for the touchscreen and the accelerometer. + */ +static const struct property_entry chuwi_hi8_gsl1680_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 1665), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_BOOL("silead,home-button"), + PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi8.fw"), + { } +}; + +static const struct software_node chuwi_hi8_gsl1680_node = { + .properties = chuwi_hi8_gsl1680_props, +}; + +static const char * const chuwi_hi8_mount_matrix[] = { + "1", "0", "0", + "0", "-1", "0", + "0", "0", "1" +}; + +static const struct property_entry chuwi_hi8_bma250e_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", chuwi_hi8_mount_matrix), + { } +}; + +static const struct software_node chuwi_hi8_bma250e_node = { + .properties = chuwi_hi8_bma250e_props, +}; + +static const struct x86_i2c_client_info chuwi_hi8_i2c_clients[] __initconst = { + { + /* Silead touchscreen */ + .board_info = { + .type = "gsl1680", + .addr = 0x40, + .swnode = &chuwi_hi8_gsl1680_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x44, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + }, { + /* BMA250E accelerometer */ + .board_info = { + .type = "bma250e", + .addr = 0x18, + .swnode = &chuwi_hi8_bma250e_node, + }, + .adapter_path = "\\_SB_.I2C3", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 23, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "bma250e_irq", + }, + }, +}; + +static int __init chuwi_hi8_init(struct device *dev) +{ + /* + * Avoid the acpi_unregister_gsi() call in x86_acpi_irq_helper_get() + * breaking the touchscreen + logging various errors when the Windows + * BIOS is used. + */ + if (acpi_dev_present("MSSL0001", NULL, 1)) + return -ENODEV; + + return 0; +} + +const struct x86_dev_info chuwi_hi8_info __initconst = { + .i2c_client_info = chuwi_hi8_i2c_clients, + .i2c_client_count = ARRAY_SIZE(chuwi_hi8_i2c_clients), + .init = chuwi_hi8_init, +}; + +/* + * Cyberbook T116 Android version + * This comes in both Windows and Android versions and even on Android + * the DSDT is mostly sane. This tablet has 2 extra general purpose buttons + * in the button row with the power + volume-buttons labeled P and F. + * Use the x86-android-tablets infra to create a gpio-keys device for these. + */ +static const struct software_node cyberbook_t116_gpio_keys_node = { + .name = "prog_keys", +}; + +static const struct property_entry cyberbook_t116_prog1_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_PROG1), + PROPERTY_ENTRY_STRING("label", "prog1_key"), + PROPERTY_ENTRY_GPIO("gpios", &cherryview_gpiochip_nodes[0], 30, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + { } +}; + +static const struct software_node cyberbook_t116_prog1_key_node = { + .parent = &cyberbook_t116_gpio_keys_node, + .properties = cyberbook_t116_prog1_key_props, +}; + +static const struct property_entry cyberbook_t116_prog2_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_PROG2), + PROPERTY_ENTRY_STRING("label", "prog2_key"), + PROPERTY_ENTRY_GPIO("gpios", &cherryview_gpiochip_nodes[3], 48, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + { } +}; + +static const struct software_node cyberbook_t116_prog2_key_node = { + .parent = &cyberbook_t116_gpio_keys_node, + .properties = cyberbook_t116_prog2_key_props, +}; + +static const struct software_node *cyberbook_t116_buttons_swnodes[] = { + &cyberbook_t116_gpio_keys_node, + &cyberbook_t116_prog1_key_node, + &cyberbook_t116_prog2_key_node, + NULL +}; + +const struct x86_dev_info cyberbook_t116_info __initconst = { + .gpio_button_swnodes = cyberbook_t116_buttons_swnodes, + .gpiochip_type = X86_GPIOCHIP_CHERRYVIEW, +}; + +#define CZC_EC_EXTRA_PORT 0x68 +#define CZC_EC_ANDROID_KEYS 0x63 + +static int __init czc_p10t_init(struct device *dev) +{ + /* + * The device boots up in "Windows 7" mode, when the home button sends a + * Windows specific key sequence (Left Meta + D) and the second button + * sends an unknown one while also toggling the Radio Kill Switch. + * This is a surprising behavior when the second button is labeled "Back". + * + * The vendor-supplied Android-x86 build switches the device to a "Android" + * mode by writing value 0x63 to the I/O port 0x68. This just seems to just + * set bit 6 on address 0x96 in the EC region; switching the bit directly + * seems to achieve the same result. It uses a "p10t_switcher" to do the + * job. It doesn't seem to be able to do anything else, and no other use + * of the port 0x68 is known. + * + * In the Android mode, the home button sends just a single scancode, + * which can be handled in Linux userspace more reasonably and the back + * button only sends a scancode without toggling the kill switch. + * The scancode can then be mapped either to Back or RF Kill functionality + * in userspace, depending on how the button is labeled on that particular + * model. + */ + outb(CZC_EC_ANDROID_KEYS, CZC_EC_EXTRA_PORT); + return 0; +} + +const struct x86_dev_info czc_p10t __initconst = { + .init = czc_p10t_init, +}; + +/* Medion Lifetab S10346 tablets have an Android factory image with everything hardcoded */ +static const char * const medion_lifetab_s10346_accel_mount_matrix[] = { + "0", "1", "0", + "1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry medion_lifetab_s10346_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", medion_lifetab_s10346_accel_mount_matrix), + { } +}; + +static const struct software_node medion_lifetab_s10346_accel_node = { + .properties = medion_lifetab_s10346_accel_props, +}; + +/* Note the LCD panel is mounted upside down, this is correctly indicated in the VBT */ +static const struct property_entry medion_lifetab_s10346_touchscreen_props[] = { + PROPERTY_ENTRY_BOOL("touchscreen-inverted-x"), + PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[1], 26, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("irq-gpios", &baytrail_gpiochip_nodes[2], 3, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node medion_lifetab_s10346_touchscreen_node = { + .properties = medion_lifetab_s10346_touchscreen_props, +}; + +static const struct x86_i2c_client_info medion_lifetab_s10346_i2c_clients[] __initconst = { + { + /* kxtj21009 accelerometer */ + .board_info = { + .type = "kxtj21009", + .addr = 0x0f, + .dev_name = "kxtj21009", + .swnode = &medion_lifetab_s10346_accel_node, + }, + .adapter_path = "\\_SB_.I2C3", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 23, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "kxtj21009_irq", + }, + }, { + /* goodix touchscreen */ + .board_info = { + .type = "GDIX1001:00", + .addr = 0x14, + .dev_name = "goodix_ts", + .swnode = &medion_lifetab_s10346_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x44, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + }, + }, +}; + +const struct x86_dev_info medion_lifetab_s10346_info __initconst = { + .i2c_client_info = medion_lifetab_s10346_i2c_clients, + .i2c_client_count = ARRAY_SIZE(medion_lifetab_s10346_i2c_clients), + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* Nextbook Ares 8 (BYT) tablets have an Android factory image with everything hardcoded */ +static const char * const nextbook_ares8_accel_mount_matrix[] = { + "0", "-1", "0", + "-1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry nextbook_ares8_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", nextbook_ares8_accel_mount_matrix), + { } +}; + +static const struct software_node nextbook_ares8_accel_node = { + .properties = nextbook_ares8_accel_props, +}; + +static const struct property_entry nextbook_ares8_touchscreen_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 800), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + { } +}; + +static const struct software_node nextbook_ares8_touchscreen_node = { + .properties = nextbook_ares8_touchscreen_props, +}; + +static const struct x86_i2c_client_info nextbook_ares8_i2c_clients[] __initconst = { + { + /* Freescale MMA8653FC accelerometer */ + .board_info = { + .type = "mma8653", + .addr = 0x1d, + .dev_name = "mma8653", + .swnode = &nextbook_ares8_accel_node, + }, + .adapter_path = "\\_SB_.I2C3", + }, { + /* FT5416DQ9 touchscreen controller */ + .board_info = { + .type = "edt-ft5x06", + .addr = 0x38, + .dev_name = "ft5416", + .swnode = &nextbook_ares8_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 3, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "ft5416_irq", + }, + }, +}; + +const struct x86_dev_info nextbook_ares8_info __initconst = { + .i2c_client_info = nextbook_ares8_i2c_clients, + .i2c_client_count = ARRAY_SIZE(nextbook_ares8_i2c_clients), + .pdev_info = int3496_pdevs, + .pdev_count = 1, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* Nextbook Ares 8A (CHT) tablets have an Android factory image with everything hardcoded */ +static const char * const nextbook_ares8a_accel_mount_matrix[] = { + "1", "0", "0", + "0", "-1", "0", + "0", "0", "1" +}; + +static const struct property_entry nextbook_ares8a_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", nextbook_ares8a_accel_mount_matrix), + { } +}; + +static const struct software_node nextbook_ares8a_accel_node = { + .properties = nextbook_ares8a_accel_props, +}; + +static const struct property_entry nextbook_ares8a_ft5416_props[] = { + PROPERTY_ENTRY_U32("touchscreen-size-x", 800), + PROPERTY_ENTRY_U32("touchscreen-size-y", 1280), + PROPERTY_ENTRY_GPIO("reset-gpios", &cherryview_gpiochip_nodes[1], 25, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node nextbook_ares8a_ft5416_node = { + .properties = nextbook_ares8a_ft5416_props, +}; + +static const struct x86_i2c_client_info nextbook_ares8a_i2c_clients[] __initconst = { + { + /* Freescale MMA8653FC accelerometer */ + .board_info = { + .type = "mma8653", + .addr = 0x1d, + .dev_name = "mma8653", + .swnode = &nextbook_ares8a_accel_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C3", + }, { + /* FT5416DQ9 touchscreen controller */ + .board_info = { + .type = "edt-ft5x06", + .addr = 0x38, + .dev_name = "ft5416", + .swnode = &nextbook_ares8a_ft5416_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FF:01", + .index = 17, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_LOW, + .con_id = "ft5416_irq", + }, + }, +}; + +const struct x86_dev_info nextbook_ares8a_info __initconst = { + .i2c_client_info = nextbook_ares8a_i2c_clients, + .i2c_client_count = ARRAY_SIZE(nextbook_ares8a_i2c_clients), + .gpiochip_type = X86_GPIOCHIP_CHERRYVIEW, +}; + +/* + * Peaq C1010 + * This is a standard Windows tablet, but it has a special Dolby button. + * This button has a WMI interface, but that is broken. Instead of trying to + * use the broken WMI interface, instantiate a gpio-keys device for this. + */ +static const struct software_node peaq_c1010_gpio_keys_node = { + .name = "gpio_keys", +}; + +static const struct property_entry peaq_c1010_dolby_key_props[] = { + PROPERTY_ENTRY_U32("linux,code", KEY_SOUND), + PROPERTY_ENTRY_STRING("label", "dolby_key"), + PROPERTY_ENTRY_GPIO("gpios", &baytrail_gpiochip_nodes[0], 3, GPIO_ACTIVE_LOW), + PROPERTY_ENTRY_U32("debounce-interval", 50), + { } +}; + +static const struct software_node peaq_c1010_dolby_key_node = { + .parent = &peaq_c1010_gpio_keys_node, + .properties = peaq_c1010_dolby_key_props, +}; + +static const struct software_node *peaq_c1010_button_swnodes[] = { + &peaq_c1010_gpio_keys_node, + &peaq_c1010_dolby_key_node, + NULL +}; + +const struct x86_dev_info peaq_c1010_info __initconst = { + .gpio_button_swnodes = peaq_c1010_button_swnodes, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* + * Whitelabel (sold as various brands) TM800A550L tablets. + * These tablet's DSDT contains a whole bunch of bogus ACPI I2C devices + * (removed through acpi_quirk_skip_i2c_client_enumeration()) and + * the touchscreen firmware node has the wrong GPIOs. + */ +static const char * const whitelabel_tm800a550l_accel_mount_matrix[] = { + "-1", "0", "0", + "0", "1", "0", + "0", "0", "1" +}; + +static const struct property_entry whitelabel_tm800a550l_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", whitelabel_tm800a550l_accel_mount_matrix), + { } +}; + +static const struct software_node whitelabel_tm800a550l_accel_node = { + .properties = whitelabel_tm800a550l_accel_props, +}; + +static const struct property_entry whitelabel_tm800a550l_goodix_props[] = { + PROPERTY_ENTRY_STRING("firmware-name", "gt912-tm800a550l.fw"), + PROPERTY_ENTRY_STRING("goodix,config-name", "gt912-tm800a550l.cfg"), + PROPERTY_ENTRY_U32("goodix,main-clk", 54), + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[1], 26, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("irq-gpios", &baytrail_gpiochip_nodes[2], 3, GPIO_ACTIVE_HIGH), + { } +}; + +static const struct software_node whitelabel_tm800a550l_goodix_node = { + .properties = whitelabel_tm800a550l_goodix_props, +}; + +static const struct x86_i2c_client_info whitelabel_tm800a550l_i2c_clients[] __initconst = { + { + /* goodix touchscreen */ + .board_info = { + .type = "GDIX1001:00", + .addr = 0x14, + .dev_name = "goodix_ts", + .swnode = &whitelabel_tm800a550l_goodix_node, + }, + .adapter_path = "\\_SB_.I2C2", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x44, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + }, { + /* kxcj91008 accelerometer */ + .board_info = { + .type = "kxcj91008", + .addr = 0x0f, + .dev_name = "kxcj91008", + .swnode = &whitelabel_tm800a550l_accel_node, + }, + .adapter_path = "\\_SB_.I2C3", + }, +}; + +const struct x86_dev_info whitelabel_tm800a550l_info __initconst = { + .i2c_client_info = whitelabel_tm800a550l_i2c_clients, + .i2c_client_count = ARRAY_SIZE(whitelabel_tm800a550l_i2c_clients), + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* + * Vexia EDU ATLA 10 tablet 5V, Android 4.4 + Guadalinex Ubuntu tablet + * distributed to schools in the Spanish Andalucía region. + */ +static const struct property_entry vexia_edu_atla10_5v_touchscreen_props[] = { + PROPERTY_ENTRY_U32("hid-descr-addr", 0x0000), + PROPERTY_ENTRY_U32("post-reset-deassert-delay-ms", 120), + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[1], 26, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node vexia_edu_atla10_5v_touchscreen_node = { + .properties = vexia_edu_atla10_5v_touchscreen_props, +}; + +static const struct x86_i2c_client_info vexia_edu_atla10_5v_i2c_clients[] __initconst = { + { + /* kxcjk1013 accelerometer */ + .board_info = { + .type = "kxcjk1013", + .addr = 0x0f, + .dev_name = "kxcjk1013", + }, + .adapter_path = "\\_SB_.I2C3", + }, { + /* touchscreen controller */ + .board_info = { + .type = "hid-over-i2c", + .addr = 0x38, + .dev_name = "FTSC1000", + .swnode = &vexia_edu_atla10_5v_touchscreen_node, + }, + .adapter_path = "\\_SB_.I2C4", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x44, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + } +}; + +const struct x86_dev_info vexia_edu_atla10_5v_info __initconst = { + .i2c_client_info = vexia_edu_atla10_5v_i2c_clients, + .i2c_client_count = ARRAY_SIZE(vexia_edu_atla10_5v_i2c_clients), + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* + * Vexia EDU ATLA 10 tablet 9V, Android 4.2 + Guadalinex Ubuntu tablet + * distributed to schools in the Spanish Andalucía region. + */ +static const char * const crystal_cove_pwrsrc_psy[] = { "crystal_cove_pwrsrc" }; + +static const struct property_entry vexia_edu_atla10_9v_ulpmc_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", crystal_cove_pwrsrc_psy), + { } +}; + +static const struct software_node vexia_edu_atla10_9v_ulpmc_node = { + .properties = vexia_edu_atla10_9v_ulpmc_props, +}; + +static const char * const vexia_edu_atla10_9v_accel_mount_matrix[] = { + "0", "-1", "0", + "1", "0", "0", + "0", "0", "1" +}; + +static const struct property_entry vexia_edu_atla10_9v_accel_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", vexia_edu_atla10_9v_accel_mount_matrix), + { } +}; + +static const struct software_node vexia_edu_atla10_9v_accel_node = { + .properties = vexia_edu_atla10_9v_accel_props, +}; + +static const struct property_entry vexia_edu_atla10_9v_touchscreen_props[] = { + PROPERTY_ENTRY_U32("hid-descr-addr", 0x0000), + PROPERTY_ENTRY_U32("post-reset-deassert-delay-ms", 120), + PROPERTY_ENTRY_GPIO("reset-gpios", &baytrail_gpiochip_nodes[0], 60, GPIO_ACTIVE_LOW), + { } +}; + +static const struct software_node vexia_edu_atla10_9v_touchscreen_node = { + .properties = vexia_edu_atla10_9v_touchscreen_props, +}; + +static const struct property_entry vexia_edu_atla10_9v_pmic_props[] = { + PROPERTY_ENTRY_BOOL("linux,register-pwrsrc-power_supply"), + { } +}; + +static const struct software_node vexia_edu_atla10_9v_pmic_node = { + .properties = vexia_edu_atla10_9v_pmic_props, +}; + +static const struct x86_i2c_client_info vexia_edu_atla10_9v_i2c_clients[] __initconst = { + { + /* I2C attached embedded controller, used to access fuel-gauge */ + .board_info = { + .type = "vexia_atla10_ec", + .addr = 0x76, + .dev_name = "ulpmc", + .swnode = &vexia_edu_atla10_9v_ulpmc_node, + }, + .adapter_path = "0000:00:18.1", + }, { + /* RT5642 audio codec */ + .board_info = { + .type = "rt5640", + .addr = 0x1c, + .dev_name = "rt5640", + }, + .adapter_path = "0000:00:18.2", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_GPIOINT, + .chip = "INT33FC:02", + .index = 4, + .trigger = ACPI_EDGE_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + .con_id = "rt5640_irq", + }, + }, { + /* kxtj21009 accelerometer */ + .board_info = { + .type = "kxtj21009", + .addr = 0x0f, + .dev_name = "kxtj21009", + .swnode = &vexia_edu_atla10_9v_accel_node, + }, + .adapter_path = "0000:00:18.5", + }, { + /* FT5416DQ9 touchscreen controller */ + .board_info = { + .type = "hid-over-i2c", + .addr = 0x38, + .dev_name = "FTSC1000", + .swnode = &vexia_edu_atla10_9v_touchscreen_node, + }, + .adapter_path = "0000:00:18.6", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x45, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + }, { + /* Crystal Cove PMIC */ + .board_info = { + .type = "intel_soc_pmic_crc", + .addr = 0x6e, + .dev_name = "intel_soc_pmic_crc", + .swnode = &vexia_edu_atla10_9v_pmic_node, + }, + .adapter_path = "0000:00:18.7", + .irq_data = { + .type = X86_ACPI_IRQ_TYPE_APIC, + .index = 0x43, + .trigger = ACPI_LEVEL_SENSITIVE, + .polarity = ACPI_ACTIVE_HIGH, + }, + } +}; + +static const struct x86_serdev_info vexia_edu_atla10_9v_serdevs[] __initconst = { + { + .ctrl.pci.devfn = PCI_DEVFN(0x1e, 3), + .ctrl_devname = "serial0", + .serdev_hid = "OBDA8723", + }, +}; + +static int __init vexia_edu_atla10_9v_init(struct device *dev) +{ + struct pci_dev *pdev; + int ret; + + /* Enable the Wifi module by setting the wifi_enable pin to 1 */ + ret = x86_android_tablet_get_gpiod("INT33FC:02", 20, "wifi_enable", + false, GPIOD_OUT_HIGH, NULL); + if (ret) + return ret; + + /* Reprobe the SDIO controller to enumerate the now enabled Wifi module */ + pdev = pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(0x11, 0)); + if (!pdev) { + pr_warn("Could not get PCI SDIO at devfn 0x%02x\n", PCI_DEVFN(0x11, 0)); + return 0; + } + + ret = device_reprobe(&pdev->dev); + if (ret) + pci_warn(pdev, "Reprobing error: %d\n", ret); + + pci_dev_put(pdev); + return 0; +} + +const struct x86_dev_info vexia_edu_atla10_9v_info __initconst = { + .i2c_client_info = vexia_edu_atla10_9v_i2c_clients, + .i2c_client_count = ARRAY_SIZE(vexia_edu_atla10_9v_i2c_clients), + .serdev_info = vexia_edu_atla10_9v_serdevs, + .serdev_count = ARRAY_SIZE(vexia_edu_atla10_9v_serdevs), + .init = vexia_edu_atla10_9v_init, + .use_pci = true, + .gpiochip_type = X86_GPIOCHIP_BAYTRAIL, +}; + +/* + * The firmware node for ktd2026 on Xaomi pad2. It composed of a RGB LED node + * with three subnodes for each color (B/G/R). The RGB LED node is named + * "multi-led" to align with the name in the device tree. + */ + +/* Main firmware node for ktd2026 */ +static const struct software_node ktd2026_node = { + .name = "ktd2026", +}; + +static const struct property_entry ktd2026_rgb_led_props[] = { + PROPERTY_ENTRY_U32("reg", 0), + PROPERTY_ENTRY_U32("color", LED_COLOR_ID_RGB), + PROPERTY_ENTRY_STRING("label", "mipad2:rgb:indicator"), + PROPERTY_ENTRY_STRING("linux,default-trigger", "bq27520-0-charging-orange-full-green"), + { } +}; + +static const struct software_node ktd2026_rgb_led_node = { + .name = "multi-led", + .properties = ktd2026_rgb_led_props, + .parent = &ktd2026_node, +}; + +static const struct property_entry ktd2026_blue_led_props[] = { + PROPERTY_ENTRY_U32("reg", 0), + PROPERTY_ENTRY_U32("color", LED_COLOR_ID_BLUE), + { } +}; + +static const struct software_node ktd2026_blue_led_node = { + .properties = ktd2026_blue_led_props, + .parent = &ktd2026_rgb_led_node, +}; + +static const struct property_entry ktd2026_green_led_props[] = { + PROPERTY_ENTRY_U32("reg", 1), + PROPERTY_ENTRY_U32("color", LED_COLOR_ID_GREEN), + { } +}; + +static const struct software_node ktd2026_green_led_node = { + .properties = ktd2026_green_led_props, + .parent = &ktd2026_rgb_led_node, +}; + +static const struct property_entry ktd2026_red_led_props[] = { + PROPERTY_ENTRY_U32("reg", 2), + PROPERTY_ENTRY_U32("color", LED_COLOR_ID_RED), + { } +}; + +static const struct software_node ktd2026_red_led_node = { + .properties = ktd2026_red_led_props, + .parent = &ktd2026_rgb_led_node, +}; + +static const struct software_node *ktd2026_node_group[] = { + &ktd2026_node, + &ktd2026_rgb_led_node, + &ktd2026_red_led_node, + &ktd2026_green_led_node, + &ktd2026_blue_led_node, + NULL +}; + +/* + * For the LEDs which backlight the Menu / Home / Back capacitive buttons on + * the bottom bezel. These are attached to a TPS61158 LED controller which + * is controlled by the "pwm_soc_lpss_2" PWM output. + */ +#define XIAOMI_MIPAD2_LED_PERIOD_NS 19200 +#define XIAOMI_MIPAD2_LED_MAX_DUTY_NS 6000 /* From Android kernel */ + +static struct pwm_device *xiaomi_mipad2_led_pwm; + +static int xiaomi_mipad2_brightness_set(struct led_classdev *led_cdev, + enum led_brightness val) +{ + struct pwm_state state = { + .period = XIAOMI_MIPAD2_LED_PERIOD_NS, + .duty_cycle = XIAOMI_MIPAD2_LED_MAX_DUTY_NS * val / LED_FULL, + /* Always set PWM enabled to avoid the pin floating */ + .enabled = true, + }; + + return pwm_apply_might_sleep(xiaomi_mipad2_led_pwm, &state); +} + +static int __init xiaomi_mipad2_init(struct device *dev) +{ + struct led_classdev *led_cdev; + + xiaomi_mipad2_led_pwm = devm_pwm_get(dev, "pwm_soc_lpss_2"); + if (IS_ERR(xiaomi_mipad2_led_pwm)) + return dev_err_probe(dev, PTR_ERR(xiaomi_mipad2_led_pwm), "getting pwm\n"); + + led_cdev = devm_kzalloc(dev, sizeof(*led_cdev), GFP_KERNEL); + if (!led_cdev) + return -ENOMEM; + + led_cdev->name = "mipad2:white:touch-buttons-backlight"; + led_cdev->max_brightness = LED_FULL; + led_cdev->default_trigger = "input-events"; + led_cdev->brightness_set_blocking = xiaomi_mipad2_brightness_set; + /* Turn LED off during suspend */ + led_cdev->flags = LED_CORE_SUSPENDRESUME; + + return devm_led_classdev_register(dev, led_cdev); +} + +/* + * If the EFI bootloader is not Xiaomi's own signed Android loader, then the + * Xiaomi Mi Pad 2 X86 tablet sets OSID in the DSDT to 1 (Windows), causing + * a bunch of devices to be hidden. + * + * This takes care of instantiating the hidden devices manually. + */ +static const struct x86_i2c_client_info xiaomi_mipad2_i2c_clients[] __initconst = { + { + /* BQ27520 fuel-gauge */ + .board_info = { + .type = "bq27520", + .addr = 0x55, + .dev_name = "bq27520", + .swnode = &fg_bq25890_supply_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C1", + }, { + /* KTD2026 RGB notification LED controller */ + .board_info = { + .type = "ktd2026", + .addr = 0x30, + .dev_name = "ktd2026", + .swnode = &ktd2026_node, + }, + .adapter_path = "\\_SB_.PCI0.I2C3", + }, +}; + +const struct x86_dev_info xiaomi_mipad2_info __initconst = { + .i2c_client_info = xiaomi_mipad2_i2c_clients, + .i2c_client_count = ARRAY_SIZE(xiaomi_mipad2_i2c_clients), + .swnode_group = ktd2026_node_group, + .init = xiaomi_mipad2_init, +}; diff --git a/drivers/platform/x86/x86-android-tablets/shared-psy-info.c b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c new file mode 100644 index 000000000..29fc466f7 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Shared psy info for X86 tablets which ship with Android as the factory image + * and which have broken DSDT tables. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include + +#include "shared-psy-info.h" +#include "x86-android-tablets.h" + +/* Generic / shared charger / battery settings */ +const char * const tusb1211_chg_det_psy[] = { "tusb1211-charger-detect" }; +const char * const bq24190_psy[] = { "bq24190-charger" }; +const char * const bq25890_psy[] = { "bq25890-charger-0" }; + +static const struct property_entry fg_bq24190_supply_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", bq24190_psy), + { } +}; + +const struct software_node fg_bq24190_supply_node = { + .properties = fg_bq24190_supply_props, +}; + +static const struct property_entry fg_bq25890_supply_props[] = { + PROPERTY_ENTRY_STRING_ARRAY("supplied-from", bq25890_psy), + { } +}; + +const struct software_node fg_bq25890_supply_node = { + .properties = fg_bq25890_supply_props, +}; + +static const u32 generic_lipo_battery_ovc_cap_celcius[] = { 25 }; + +static const u32 generic_lipo_4v2_battery_ovc_cap_table0[] = { + 4200000, 100, + 4150000, 95, + 4110000, 90, + 4075000, 85, + 4020000, 80, + 3982500, 75, + 3945000, 70, + 3907500, 65, + 3870000, 60, + 3853333, 55, + 3836667, 50, + 3820000, 45, + 3803333, 40, + 3786667, 35, + 3770000, 30, + 3750000, 25, + 3730000, 20, + 3710000, 15, + 3690000, 10, + 3610000, 5, + 3350000, 0 +}; + +static const u32 generic_lipo_hv_4v35_battery_ovc_cap_table0[] = { + 4300000, 100, + 4250000, 96, + 4200000, 91, + 4150000, 86, + 4110000, 82, + 4075000, 77, + 4020000, 73, + 3982500, 68, + 3945000, 64, + 3907500, 59, + 3870000, 55, + 3853333, 50, + 3836667, 45, + 3820000, 41, + 3803333, 36, + 3786667, 32, + 3770000, 27, + 3750000, 23, + 3730000, 18, + 3710000, 14, + 3690000, 9, + 3610000, 5, + 3350000, 0 +}; + +/* Standard LiPo (max 4.2V) settings used by most devs with a LiPo battery */ +static const struct property_entry generic_lipo_4v2_battery_props[] = { + PROPERTY_ENTRY_STRING("compatible", "simple-battery"), + PROPERTY_ENTRY_STRING("device-chemistry", "lithium-ion-polymer"), + PROPERTY_ENTRY_U32("precharge-current-microamp", 256000), + PROPERTY_ENTRY_U32("charge-term-current-microamp", 128000), + PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 2048000), + PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4208000), + PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-celsius", + generic_lipo_battery_ovc_cap_celcius), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-table-0", + generic_lipo_4v2_battery_ovc_cap_table0), + { } +}; + +const struct software_node generic_lipo_4v2_battery_node = { + .properties = generic_lipo_4v2_battery_props, +}; + +const struct software_node *generic_lipo_4v2_battery_swnodes[] = { + &generic_lipo_4v2_battery_node, + NULL +}; + +/* LiPo HighVoltage (max 4.35V) settings used by most devs with a HV battery */ +static const struct property_entry generic_lipo_hv_4v35_battery_props[] = { + PROPERTY_ENTRY_STRING("compatible", "simple-battery"), + PROPERTY_ENTRY_STRING("device-chemistry", "lithium-ion"), + PROPERTY_ENTRY_U32("precharge-current-microamp", 256000), + PROPERTY_ENTRY_U32("charge-term-current-microamp", 128000), + PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 1856000), + PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4352000), + PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-celsius", + generic_lipo_battery_ovc_cap_celcius), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-table-0", + generic_lipo_hv_4v35_battery_ovc_cap_table0), + { } +}; + +const struct software_node generic_lipo_hv_4v35_battery_node = { + .properties = generic_lipo_hv_4v35_battery_props, +}; + +const struct software_node *generic_lipo_hv_4v35_battery_swnodes[] = { + &generic_lipo_hv_4v35_battery_node, + NULL +}; + +/* For enabling the bq24190 5V boost based on id-pin */ +static struct regulator_consumer_supply intel_int3496_consumer = { + .supply = "vbus", + .dev_name = "intel-int3496", +}; + +static const struct regulator_init_data bq24190_vbus_init_data = { + .constraints = { + .name = "bq24190_vbus", + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, + .consumer_supplies = &intel_int3496_consumer, + .num_consumer_supplies = 1, +}; + +struct bq24190_platform_data bq24190_pdata = { + .regulator_init_data = &bq24190_vbus_init_data, +}; + +const char * const bq24190_modules[] __initconst = { + "intel_crystal_cove_charger", /* For the bq24190 IRQ */ + "bq24190_charger", /* For the Vbus regulator for intel-int3496 */ + NULL +}; + +static const struct property_entry int3496_reference_props[] __initconst = { + PROPERTY_ENTRY_GPIO("vbus-gpios", &baytrail_gpiochip_nodes[1], 15, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("mux-gpios", &baytrail_gpiochip_nodes[2], 1, GPIO_ACTIVE_HIGH), + PROPERTY_ENTRY_GPIO("id-gpios", &baytrail_gpiochip_nodes[2], 18, GPIO_ACTIVE_HIGH), + { } +}; + +/* Generic pdevs array and gpio-lookups for micro USB ID pin handling */ +const struct platform_device_info int3496_pdevs[] __initconst = { + { + /* For micro USB ID pin handling */ + .name = "intel-int3496", + .id = PLATFORM_DEVID_NONE, + .properties = int3496_reference_props, + }, +}; diff --git a/drivers/platform/x86/x86-android-tablets/shared-psy-info.h b/drivers/platform/x86/x86-android-tablets/shared-psy-info.h new file mode 100644 index 000000000..149befba3 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/shared-psy-info.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * + * Shared psy info for X86 tablets which ship with Android as the factory image + * and which have broken DSDT tables. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ +#ifndef __PDX86_SHARED_PSY_INFO_H +#define __PDX86_SHARED_PSY_INFO_H + +struct bq24190_platform_data; +struct platform_device_info; +struct software_node; + +extern const char * const tusb1211_chg_det_psy[]; +extern const char * const bq24190_psy[]; +extern const char * const bq25890_psy[]; + +extern const struct software_node fg_bq24190_supply_node; +extern const struct software_node fg_bq25890_supply_node; + +extern const struct software_node generic_lipo_4v2_battery_node; +extern const struct software_node *generic_lipo_4v2_battery_swnodes[]; + +extern const struct software_node generic_lipo_hv_4v35_battery_node; +extern const struct software_node *generic_lipo_hv_4v35_battery_swnodes[]; + +extern struct bq24190_platform_data bq24190_pdata; +extern const char * const bq24190_modules[]; + +extern const struct platform_device_info int3496_pdevs[]; + +#endif diff --git a/drivers/platform/x86/x86-android-tablets/vexia_atla10_ec.c b/drivers/platform/x86/x86-android-tablets/vexia_atla10_ec.c new file mode 100644 index 000000000..67dba9aa5 --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/vexia_atla10_ec.c @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * power_supply class (battery) driver for the I2C attached embedded controller + * found on Vexia EDU ATLA 10 (9V version) tablets. + * + * This is based on the ACPI Battery device in the DSDT which should work + * expect that it expects the I2C controller to be enumerated as an ACPI + * device and the tablet's BIOS enumerates all LPSS devices as PCI devices + * (and changing the LPSS BIOS settings from PCI -> ACPI does not work). + * + * Copyright (c) 2024 Hans de Goede + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* State field uses ACPI Battery spec status bits */ +#define ACPI_BATTERY_STATE_DISCHARGING BIT(0) +#define ACPI_BATTERY_STATE_CHARGING BIT(1) + +#define ATLA10_EC_BATTERY_STATE_COMMAND 0x87 +#define ATLA10_EC_BATTERY_INFO_COMMAND 0x88 + +/* From broken ACPI battery device in DSDT */ +#define ATLA10_EC_VOLTAGE_MIN_DESIGN_uV 3750000 + +/* Update data every 5 seconds */ +#define UPDATE_INTERVAL_JIFFIES (5 * HZ) + +struct atla10_ec_battery_state { + u8 status; /* Using ACPI Battery spec status bits */ + u8 capacity; /* Percent */ + __le16 charge_now_mAh; + __le16 voltage_now_mV; + __le16 current_now_mA; + __le16 charge_full_mAh; + __le16 temp; /* centi degrees Celsius */ +} __packed; + +struct atla10_ec_battery_info { + __le16 charge_full_design_mAh; + __le16 voltage_now_mV; /* Should be design voltage, but is not ? */ + __le16 charge_full_design2_mAh; +} __packed; + +struct atla10_ec_data { + struct i2c_client *client; + struct power_supply *psy; + struct delayed_work work; + struct mutex update_lock; + struct atla10_ec_battery_info info; + struct atla10_ec_battery_state state; + bool valid; /* true if state is valid */ + unsigned long last_update; /* In jiffies */ +}; + +static int atla10_ec_cmd(struct atla10_ec_data *data, u8 cmd, u8 len, u8 *values) +{ + struct device *dev = &data->client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + ret = i2c_smbus_read_block_data(data->client, cmd, buf); + if (ret != len) { + dev_err(dev, "I2C command 0x%02x error: %d\n", cmd, ret); + return -EIO; + } + + memcpy(values, buf, len); + return 0; +} + +static int atla10_ec_update(struct atla10_ec_data *data) +{ + int ret; + + if (data->valid && time_before(jiffies, data->last_update + UPDATE_INTERVAL_JIFFIES)) + return 0; + + ret = atla10_ec_cmd(data, ATLA10_EC_BATTERY_STATE_COMMAND, + sizeof(data->state), (u8 *)&data->state); + if (ret) + return ret; + + data->last_update = jiffies; + data->valid = true; + return 0; +} + +static int atla10_ec_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct atla10_ec_data *data = power_supply_get_drvdata(psy); + int charge_now_mAh, charge_full_mAh, ret; + + guard(mutex)(&data->update_lock); + + ret = atla10_ec_update(data); + if (ret) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (data->state.status & ACPI_BATTERY_STATE_DISCHARGING) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (data->state.status & ACPI_BATTERY_STATE_CHARGING) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (data->state.capacity == 100) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = data->state.capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + /* + * The EC has a bug where it reports charge-full-design as + * charge-now when the battery is full. Clamp charge-now to + * charge-full to workaround this. + */ + charge_now_mAh = le16_to_cpu(data->state.charge_now_mAh); + charge_full_mAh = le16_to_cpu(data->state.charge_full_mAh); + val->intval = min(charge_now_mAh, charge_full_mAh) * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = le16_to_cpu(data->state.voltage_now_mV) * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = le16_to_cpu(data->state.current_now_mA) * 1000; + /* + * Documentation/ABI/testing/sysfs-class-power specifies + * negative current for discharging. + */ + if (data->state.status & ACPI_BATTERY_STATE_DISCHARGING) + val->intval = -val->intval; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = le16_to_cpu(data->state.charge_full_mAh) * 1000; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = le16_to_cpu(data->state.temp) / 10; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = le16_to_cpu(data->info.charge_full_design_mAh) * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = ATLA10_EC_VOLTAGE_MIN_DESIGN_uV; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LIPO; + break; + default: + return -EINVAL; + } + + return 0; +} + +static void atla10_ec_external_power_changed_work(struct work_struct *work) +{ + struct atla10_ec_data *data = container_of(work, struct atla10_ec_data, work.work); + + dev_dbg(&data->client->dev, "External power changed\n"); + data->valid = false; + power_supply_changed(data->psy); +} + +static void atla10_ec_external_power_changed(struct power_supply *psy) +{ + struct atla10_ec_data *data = power_supply_get_drvdata(psy); + + /* After charger plug in/out wait 0.5s for things to stabilize */ + mod_delayed_work(system_percpu_wq, &data->work, HZ / 2); +} + +static const enum power_supply_property atla10_ec_psy_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, +}; + +static const struct power_supply_desc atla10_ec_psy_desc = { + .name = "atla10_ec_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = atla10_ec_psy_props, + .num_properties = ARRAY_SIZE(atla10_ec_psy_props), + .get_property = atla10_ec_psy_get_property, + .external_power_changed = atla10_ec_external_power_changed, +}; + +static int atla10_ec_probe(struct i2c_client *client) +{ + struct power_supply_config psy_cfg = { }; + struct device *dev = &client->dev; + struct atla10_ec_data *data; + int ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + psy_cfg.drv_data = data; + data->client = client; + + ret = devm_mutex_init(dev, &data->update_lock); + if (ret) + return ret; + + ret = devm_delayed_work_autocancel(dev, &data->work, + atla10_ec_external_power_changed_work); + if (ret) + return ret; + + ret = atla10_ec_cmd(data, ATLA10_EC_BATTERY_INFO_COMMAND, + sizeof(data->info), (u8 *)&data->info); + if (ret) + return ret; + + data->psy = devm_power_supply_register(dev, &atla10_ec_psy_desc, &psy_cfg); + return PTR_ERR_OR_ZERO(data->psy); +} + +static const struct i2c_device_id atla10_ec_id_table[] = { + { .name = "vexia_atla10_ec" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, atla10_ec_id_table); + +static struct i2c_driver atla10_ec_driver = { + .driver = { + .name = "vexia_atla10_ec", + }, + .probe = atla10_ec_probe, + .id_table = atla10_ec_id_table, +}; +module_i2c_driver(atla10_ec_driver); + +MODULE_AUTHOR("Hans de Goede "); +MODULE_DESCRIPTION("Battery driver for Vexia EDU ATLA 10 tablet EC"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/x86-android-tablets/x86-android-tablets.h b/drivers/platform/x86/x86-android-tablets/x86-android-tablets.h new file mode 100644 index 000000000..c756961ae --- /dev/null +++ b/drivers/platform/x86/x86-android-tablets/x86-android-tablets.h @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * + * DMI based code to deal with broken DSDTs on X86 tablets which ship with + * Android as (part of) the factory image. The factory kernels shipped on these + * devices typically have a bunch of things hardcoded, rather than specified + * in their DSDT. + * + * Copyright (C) 2021-2023 Hans de Goede + */ +#ifndef __PDX86_X86_ANDROID_TABLETS_H +#define __PDX86_X86_ANDROID_TABLETS_H + +#include +#include +#include +#include +#include + +struct gpio_desc; +struct platform_device_info; +struct software_node; + +/* + * Helpers to get Linux IRQ numbers given a description of the IRQ source + * (either IOAPIC index, or GPIO chip name + pin-number). + */ +enum x86_acpi_irq_type { + X86_ACPI_IRQ_TYPE_NONE, + X86_ACPI_IRQ_TYPE_APIC, + X86_ACPI_IRQ_TYPE_GPIOINT, + X86_ACPI_IRQ_TYPE_PMIC, +}; + +enum x86_gpiochip_type { + X86_GPIOCHIP_UNSPECIFIED = 0, + X86_GPIOCHIP_BAYTRAIL, + X86_GPIOCHIP_CHERRYVIEW, +}; + +struct x86_acpi_irq_data { + char *chip; /* GPIO chip label (GPIOINT) or PMIC ACPI path (PMIC) */ + enum x86_acpi_irq_type type; + enum irq_domain_bus_token domain; + int index; + int trigger; /* ACPI_EDGE_SENSITIVE / ACPI_LEVEL_SENSITIVE */ + int polarity; /* ACPI_ACTIVE_HIGH / ACPI_ACTIVE_LOW / ACPI_ACTIVE_BOTH */ + bool free_gpio; /* Release GPIO after getting IRQ (for TYPE_GPIOINT) */ + const char *con_id; +}; + +/* Structs to describe devices to instantiate */ +struct x86_i2c_client_info { + struct i2c_board_info board_info; + char *adapter_path; + struct x86_acpi_irq_data irq_data; +}; + +struct x86_spi_dev_info { + struct spi_board_info board_info; + char *ctrl_path; + struct x86_acpi_irq_data irq_data; +}; + +struct x86_serdev_info { + union { + struct { + const char *hid; + const char *uid; + } acpi; + struct { + unsigned int devfn; + } pci; + } ctrl; + const char *ctrl_devname; + /* + * ATM the serdev core only supports of or ACPI matching; and so far all + * Android x86 tablets DSDTs have usable serdev nodes, but sometimes + * under the wrong controller. So we just tie the existing serdev ACPI + * node to the right controller. + */ + const char *serdev_hid; +}; + +struct x86_dev_info { + const char * const *modules; + const struct software_node **swnode_group; + const struct x86_i2c_client_info *i2c_client_info; + const struct x86_spi_dev_info *spi_dev_info; + const struct platform_device_info *pdev_info; + const struct x86_serdev_info *serdev_info; + const struct software_node **gpio_button_swnodes; + int i2c_client_count; + int spi_dev_count; + int pdev_count; + int serdev_count; + int (*init)(struct device *dev); + void (*exit)(void); + bool use_pci; + enum x86_gpiochip_type gpiochip_type; +}; + +int x86_android_tablet_get_gpiod(const char *chip, int pin, const char *con_id, + bool active_low, enum gpiod_flags dflags, + struct gpio_desc **desc); +int x86_acpi_irq_helper_get(const struct x86_acpi_irq_data *data); + +/* Software nodes representing GPIO chips used by various tablets */ +extern const struct software_node baytrail_gpiochip_nodes[]; +extern const struct software_node cherryview_gpiochip_nodes[]; + +/* + * Extern declarations of x86_dev_info structs so there can be a single + * MODULE_DEVICE_TABLE(dmi, ...), while splitting the board descriptions. + */ +extern const struct x86_dev_info acer_a1_840_info; +extern const struct x86_dev_info acer_b1_750_info; +extern const struct x86_dev_info advantech_mica_071_info; +extern const struct x86_dev_info asus_me176c_info; +extern const struct x86_dev_info asus_tf103c_info; +extern const struct x86_dev_info chuwi_hi8_info; +extern const struct x86_dev_info cyberbook_t116_info; +extern const struct x86_dev_info czc_p10t; +extern const struct x86_dev_info lenovo_yogabook_x90_info; +extern const struct x86_dev_info lenovo_yogabook_x91_info; +extern const struct x86_dev_info lenovo_yoga_tab2_830_1050_info; +extern const struct x86_dev_info lenovo_yoga_tab2_1380_info; +extern const struct x86_dev_info lenovo_yt3_info; +extern const struct x86_dev_info medion_lifetab_s10346_info; +extern const struct x86_dev_info nextbook_ares8_info; +extern const struct x86_dev_info nextbook_ares8a_info; +extern const struct x86_dev_info peaq_c1010_info; +extern const struct x86_dev_info whitelabel_tm800a550l_info; +extern const struct x86_dev_info vexia_edu_atla10_5v_info; +extern const struct x86_dev_info vexia_edu_atla10_9v_info; +extern const struct x86_dev_info xiaomi_mipad2_info; +extern const struct dmi_system_id x86_android_tablet_ids[]; + +#endif diff --git a/drivers/platform/x86/xiaomi-wmi.c b/drivers/platform/x86/xiaomi-wmi.c new file mode 100644 index 000000000..3874f3336 --- /dev/null +++ b/drivers/platform/x86/xiaomi-wmi.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0 +/* WMI driver for Xiaomi Laptops */ + +#include +#include +#include +#include +#include + +#include + +#define XIAOMI_KEY_FN_ESC_0 "A2095CCE-0491-44E7-BA27-F8ED8F88AA86" +#define XIAOMI_KEY_FN_ESC_1 "7BBE8E39-B486-473D-BA13-66F75C5805CD" +#define XIAOMI_KEY_FN_FN "409B028D-F06B-4C7C-8BBB-EE133A6BD87E" +#define XIAOMI_KEY_CAPSLOCK "83FE7607-053A-4644-822A-21532C621FC7" +#define XIAOMI_KEY_FN_F7 "76E9027C-95D0-4180-8692-DA6747DD1C2D" + +#define XIAOMI_DEVICE(guid, key) \ + .guid_string = (guid), \ + .context = &(const unsigned int){key} + +struct xiaomi_wmi { + struct input_dev *input_dev; + struct mutex key_lock; /* Protects the key event sequence */ + unsigned int key_code; +}; + +static int xiaomi_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct xiaomi_wmi *data; + int ret; + + if (!context) + return -EINVAL; + + data = devm_kzalloc(&wdev->dev, sizeof(struct xiaomi_wmi), GFP_KERNEL); + if (data == NULL) + return -ENOMEM; + dev_set_drvdata(&wdev->dev, data); + + ret = devm_mutex_init(&wdev->dev, &data->key_lock); + if (ret < 0) + return ret; + + data->input_dev = devm_input_allocate_device(&wdev->dev); + if (data->input_dev == NULL) + return -ENOMEM; + data->input_dev->name = "Xiaomi WMI keys"; + data->input_dev->phys = "wmi/input0"; + + data->key_code = *((const unsigned int *)context); + set_bit(EV_KEY, data->input_dev->evbit); + set_bit(data->key_code, data->input_dev->keybit); + + return input_register_device(data->input_dev); +} + +static void xiaomi_wmi_notify(struct wmi_device *wdev, const struct wmi_buffer *dummy) +{ + struct xiaomi_wmi *data = dev_get_drvdata(&wdev->dev); + + mutex_lock(&data->key_lock); + input_report_key(data->input_dev, data->key_code, 1); + input_sync(data->input_dev); + input_report_key(data->input_dev, data->key_code, 0); + input_sync(data->input_dev); + mutex_unlock(&data->key_lock); +} + +static const struct wmi_device_id xiaomi_wmi_id_table[] = { + // { XIAOMI_DEVICE(XIAOMI_KEY_FN_ESC_0, KEY_FN_ESC) }, + // { XIAOMI_DEVICE(XIAOMI_KEY_FN_ESC_1, KEY_FN_ESC) }, + { XIAOMI_DEVICE(XIAOMI_KEY_FN_FN, KEY_PROG1) }, + // { XIAOMI_DEVICE(XIAOMI_KEY_CAPSLOCK, KEY_CAPSLOCK) }, + { XIAOMI_DEVICE(XIAOMI_KEY_FN_F7, KEY_CUT) }, + + /* Terminating entry */ + { } +}; + +static struct wmi_driver xiaomi_wmi_driver = { + .driver = { + .name = "xiaomi-wmi", + }, + .id_table = xiaomi_wmi_id_table, + .min_event_size = 0, + .probe = xiaomi_wmi_probe, + .notify_new = xiaomi_wmi_notify, + .no_singleton = true, +}; +module_wmi_driver(xiaomi_wmi_driver); + +MODULE_DEVICE_TABLE(wmi, xiaomi_wmi_id_table); +MODULE_AUTHOR("Mattias Jacobsson"); +MODULE_DESCRIPTION("Xiaomi WMI driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/platform/x86/xo1-rfkill.c b/drivers/platform/x86/xo1-rfkill.c new file mode 100644 index 000000000..5fedb99b9 --- /dev/null +++ b/drivers/platform/x86/xo1-rfkill.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Support for rfkill through the OLPC XO-1 laptop embedded controller + * + * Copyright (C) 2010 One Laptop per Child + */ + +#include +#include +#include +#include + +static bool card_blocked; + +static int rfkill_set_block(void *data, bool blocked) +{ + unsigned char cmd; + int r; + + if (blocked == card_blocked) + return 0; + + if (blocked) + cmd = EC_WLAN_ENTER_RESET; + else + cmd = EC_WLAN_LEAVE_RESET; + + r = olpc_ec_cmd(cmd, NULL, 0, NULL, 0); + if (r == 0) + card_blocked = blocked; + + return r; +} + +static const struct rfkill_ops rfkill_ops = { + .set_block = rfkill_set_block, +}; + +static int xo1_rfkill_probe(struct platform_device *pdev) +{ + struct rfkill *rfk; + int r; + + rfk = rfkill_alloc(pdev->name, &pdev->dev, RFKILL_TYPE_WLAN, + &rfkill_ops, NULL); + if (!rfk) + return -ENOMEM; + + r = rfkill_register(rfk); + if (r) { + rfkill_destroy(rfk); + return r; + } + + platform_set_drvdata(pdev, rfk); + return 0; +} + +static void xo1_rfkill_remove(struct platform_device *pdev) +{ + struct rfkill *rfk = platform_get_drvdata(pdev); + rfkill_unregister(rfk); + rfkill_destroy(rfk); +} + +static struct platform_driver xo1_rfkill_driver = { + .driver = { + .name = "xo1-rfkill", + }, + .probe = xo1_rfkill_probe, + .remove = xo1_rfkill_remove, +}; + +module_platform_driver(xo1_rfkill_driver); + +MODULE_AUTHOR("Daniel Drake "); +MODULE_DESCRIPTION("OLPC XO-1 software RF kill switch"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:xo1-rfkill"); diff --git a/drivers/platform/x86/xo15-ebook.c b/drivers/platform/x86/xo15-ebook.c new file mode 100644 index 000000000..e40e385c5 --- /dev/null +++ b/drivers/platform/x86/xo15-ebook.c @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OLPC XO-1.5 ebook switch driver + * (based on generic ACPI button driver) + * + * Copyright (C) 2009 Paul Fox + * Copyright (C) 2010 One Laptop per Child + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_NAME "xo15-ebook" + +#define XO15_EBOOK_CLASS MODULE_NAME +#define XO15_EBOOK_TYPE_UNKNOWN 0x00 +#define XO15_EBOOK_NOTIFY_STATUS 0x80 + +#define XO15_EBOOK_SUBCLASS "ebook" +#define XO15_EBOOK_HID "XO15EBK" +#define XO15_EBOOK_DEVICE_NAME "EBook Switch" + +MODULE_DESCRIPTION("OLPC XO-1.5 ebook switch driver"); +MODULE_LICENSE("GPL"); + +static const struct acpi_device_id ebook_device_ids[] = { + { XO15_EBOOK_HID, 0 }, + { "", 0 }, +}; +MODULE_DEVICE_TABLE(acpi, ebook_device_ids); + +struct ebook_switch { + struct input_dev *input; + char phys[32]; /* for input device */ + bool gpe_enabled; +}; + +static int ebook_send_state(struct device *dev) +{ + struct ebook_switch *button = dev_get_drvdata(dev); + unsigned long long state; + acpi_status status; + + status = acpi_evaluate_integer(ACPI_HANDLE(dev), "EBK", NULL, &state); + if (ACPI_FAILURE(status)) + return -EIO; + + /* input layer checks if event is redundant */ + input_report_switch(button->input, SW_TABLET_MODE, !state); + input_sync(button->input); + return 0; +} + +static void ebook_switch_notify(acpi_handle handle, u32 event, void *data) +{ + switch (event) { + case ACPI_FIXED_HARDWARE_EVENT: + case XO15_EBOOK_NOTIFY_STATUS: + ebook_send_state(data); + break; + default: + acpi_handle_debug(handle, "Unsupported event [0x%x]\n", event); + break; + } +} + +#ifdef CONFIG_PM_SLEEP +static int ebook_switch_resume(struct device *dev) +{ + return ebook_send_state(dev); +} +#endif + +static SIMPLE_DEV_PM_OPS(ebook_switch_pm, NULL, ebook_switch_resume); + +static int ebook_switch_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct acpi_device *device = ACPI_COMPANION(dev); + const struct acpi_device_id *id; + struct ebook_switch *button; + struct input_dev *input; + int error; + + button = devm_kzalloc(dev, sizeof(*button), GFP_KERNEL); + if (!button) + return -ENOMEM; + + platform_set_drvdata(pdev, button); + + input = devm_input_allocate_device(dev); + if (!input) + return -ENOMEM; + + button->input = input; + + id = acpi_match_acpi_device(ebook_device_ids, device); + if (!id) + return dev_err_probe(dev, -ENODEV, "Unsupported hid\n"); + + strscpy(acpi_device_name(device), XO15_EBOOK_DEVICE_NAME); + strscpy(acpi_device_class(device), XO15_EBOOK_CLASS "/" XO15_EBOOK_SUBCLASS); + + snprintf(button->phys, sizeof(button->phys), "%s/button/input0", id->id); + + input->name = acpi_device_name(device); + input->phys = button->phys; + input->id.bustype = BUS_HOST; + + input->evbit[0] = BIT_MASK(EV_SW); + set_bit(SW_TABLET_MODE, input->swbit); + + error = input_register_device(input); + if (error) + return error; + + error = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY, + ebook_switch_notify, dev); + if (error) + return error; + + ebook_send_state(dev); + + if (device->wakeup.flags.valid) { + /* Button's GPE is run-wake GPE */ + acpi_enable_gpe(device->wakeup.gpe_device, + device->wakeup.gpe_number); + button->gpe_enabled = true; + } + + return 0; +} + +static void ebook_switch_remove(struct platform_device *pdev) +{ + struct ebook_switch *button = platform_get_drvdata(pdev); + struct acpi_device *device = ACPI_COMPANION(&pdev->dev); + + if (button->gpe_enabled) + acpi_disable_gpe(device->wakeup.gpe_device, + device->wakeup.gpe_number); + + acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, + ebook_switch_notify); +} + +static struct platform_driver xo15_ebook_driver = { + .probe = ebook_switch_probe, + .remove = ebook_switch_remove, + .driver = { + .name = MODULE_NAME, + .acpi_match_table = ebook_device_ids, + .pm = &ebook_switch_pm, + }, +}; +module_platform_driver(xo15_ebook_driver); -- cgit v1.3.1