commit 95f3f072da16549f69c7eaea398be37336c7922c Author: Nikkuss Date: Fri Apr 10 15:45:53 2026 +0400 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..33a641a --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# x1e-nixos + +NixOS flake providing kernel and hardware support for the **Microsoft Surface Laptop 7** (Snapdragon X Elite / x1e80100). + +## Usage + +```nix +# flake.nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + x1e-nixos.url = "git+https://git.scug.io/nikkuss/x1e-nixos.git"; + + # Required: provides pkgs.slbounce, pkgs.tcblaunch, + # pkgs.x1e80100-firmware, pkgs.x1e80100-linux-firmware + custom-pkgs = { + url = "git+https://git.scug.io/nikkuss/pkgs.git"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, x1e-nixos, custom-pkgs, ... }: { + nixosConfigurations.my-surface = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + { nixpkgs.overlays = [ custom-pkgs.overlays.default ]; } + x1e-nixos.nixosModules.default + ./configuration.nix + ]; + }; + }; +} +``` + +The `default` module imports everything (kernel, modules, boot, hardware). To pick individual pieces: + +```nix +x1e-nixos.nixosModules.kernel # boot.kernelPackages + initrd config +x1e-nixos.nixosModules.kernel-modules # out-of-tree .ko modules +x1e-nixos.nixosModules.boot # systemd-boot + slbounce + firmware paths +x1e-nixos.nixosModules.hardware # DTB + firmware packages +``` + +## Optional modules + +Some modules don't auto-load via device tree / PCI and are behind config flags: + +```nix +{ + x1e.model = "15"; # "13" (default) or "15" — selects the correct DTB + x1e.cpuParking = true; # loads cpu_parking at boot + x1e.ecReboot = true; # loads ec_reboot at boot, exposes /sys/kernel/ec_reboot/reboot +} +``` + +## Touchpad not working + +After a lid close or certain sleep/wake cycles, the EC may cut power to the touchpad sensor in a way that Linux cannot recover from on its own. If the touchpad stops responding: + +**Option A — sysfs (no reboot required on its own, but the EC reset will reboot the machine):** + +```sh +modprobe ec_reboot +echo 1 > /sys/kernel/ec_reboot/reboot +``` + +**Option B — hard power off:** + +Hold the power button for **10 seconds** until the machine fully shuts off, then power it back on. + +Both methods force a full EC power cycle, which resets the touchpad sensor state. + +## EL2 + +The EL2 boot specialisation (slbounce) is currently broken. The default boot entry runs in EL1. diff --git a/boot.nix b/boot.nix new file mode 100644 index 0000000..49ed0a4 --- /dev/null +++ b/boot.nix @@ -0,0 +1,93 @@ +{ lib, pkgs, ... }: +{ + specialisation.el2.configuration = { + hardware.deviceTree = { + overlays = lib.mkAfter [ + { + name = "x1e-el2"; + dtboFile = "${pkgs.slbounce}/share/slbounce/dtbo/x1e-el2.dtbo"; + } + ]; + }; + boot.kernelParams = [ "id_aa64mmfr0.ecv=1" ]; + systemd.services.el2-boot-indicator = { + description = "EL2 boot indicator (blink Caps Lock LED)"; + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + LED=$(find /sys/class/leds -name "*capslock*" 2>/dev/null | head -1) + if [ -z "$LED" ]; then + LED=$(find /sys/class/leds -name "*kbd*" 2>/dev/null | head -1) + fi + if [ -n "$LED" ]; then + for i in 1 2 3 4 5; do + echo 1 > "$LED/brightness" 2>/dev/null || true + sleep 0.3 + echo 0 > "$LED/brightness" 2>/dev/null || true + sleep 0.3 + done + echo 1 > "$LED/brightness" 2>/dev/null || true + fi + ''; + }; + }; + + boot = { + initrd.extraFirmwarePaths = [ + "ath12k/WCN7850/hw2.0/amss.bin" + "ath12k/WCN7850/hw2.0/board-2.bin" + "ath12k/QCN9274/hw2.0/board-2.bin" + "ath12k/QCN9274/hw2.0/firmware-2.bin" + "ath12k/WCN7850/hw2.0/m3.bin" + "qcom/x1e80100/microsoft/Romulus/adsp_dtbs.elf" + "qcom/x1e80100/microsoft/Romulus/adspr.jsn" + "qcom/x1e80100/microsoft/Romulus/adsps.jsn" + "qcom/x1e80100/microsoft/Romulus/adspua.jsn" + "qcom/x1e80100/microsoft/Romulus/battmgr.jsn" + "qcom/x1e80100/microsoft/Romulus/cdsp_dtbs.elf" + "qcom/x1e80100/microsoft/Romulus/cdspr.jsn" + "qcom/x1e80100/microsoft/Romulus/qcadsp8380.mbn" + "qcom/x1e80100/microsoft/Romulus/qccdsp8380.mbn" + "qcom/x1e80100/microsoft/qcdxkmsuc8380.mbn" + "qcom/gen70500_sqe.fw" + "qcom/gen70500_sqe.fw.zst" + "qcom/gen70500_gmu.bin" + ]; + + kernelParams = [ + "clk_ignore_unused" + "pd_ignore_unused" + "iomem=relaxed" + ]; + + supportedFilesystems = { + btrfs = true; + zfs = lib.mkForce false; + cifs = lib.mkForce false; + }; + + consoleLogLevel = 7; + + loader.systemd-boot = { + edk2-uefi-shell.enable = true; + extraFiles = { + "EFI/systemd/drivers/slbouncea64.efi" = "${pkgs.slbounce}/share/slbounce/slbounce.efi"; + "tcblaunch.exe" = "${pkgs.tcblaunch}/share/tcblaunch/tcblaunch.exe"; + "sltest.efi" = "${pkgs.slbounce}/share/slbounce/sltest.efi"; + "dtbhack.efi" = "${pkgs.slbounce}/share/slbounce/dtbhack.efi"; + "firmware/qcom/x1e80100/microsoft/Romulus/qcadsp8380.mbn" = + "${pkgs.x1e80100-firmware}/lib/firmware/qcom/x1e80100/microsoft/Romulus/qcadsp8380.mbn"; + "firmware/qcom/x1e80100/microsoft/Romulus/adsp_dtbs.elf" = + "${pkgs.x1e80100-firmware}/lib/firmware/qcom/x1e80100/microsoft/Romulus/adsp_dtbs.elf"; + "firmware/qcom/x1e80100/microsoft/Romulus/qccdsp8380.mbn" = + "${pkgs.x1e80100-firmware}/lib/firmware/qcom/x1e80100/microsoft/Romulus/qccdsp8380.mbn"; + "firmware/qcom/x1e80100/microsoft/Romulus/cdsp_dtbs.elf" = + "${pkgs.x1e80100-firmware}/lib/firmware/qcom/x1e80100/microsoft/Romulus/cdsp_dtbs.elf"; + }; + }; + }; +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3f46dd2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "Surface Laptop 7 (x1e80100 / Snapdragon X Elite) kernel and hardware support"; + + outputs = { self }: { + nixosModules = { + default = self.nixosModules.all; + + all = { ... }: { + imports = [ + self.nixosModules.kernel + self.nixosModules.kernel-modules + self.nixosModules.boot + self.nixosModules.hardware + ]; + }; + + kernel = import ./kernel.nix; + kernel-modules = import ./kernel-modules.nix; + boot = import ./boot.nix; + hardware = import ./hardware.nix; + }; + }; +} diff --git a/hardware.nix b/hardware.nix new file mode 100644 index 0000000..60a1208 --- /dev/null +++ b/hardware.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: +let + dtbName = { + "13" = "qcom/x1e80100-microsoft-romulus13.dtb"; + "15" = "qcom/x1e80100-microsoft-romulus15.dtb"; + }.${config.x1e.model}; +in +{ + options.x1e.model = lib.mkOption { + type = lib.types.enum [ "13" "15" ]; + default = "13"; + description = "Surface Laptop 7 display size (13.8\" or 15\")."; + }; + + config = { + nixpkgs.hostPlatform = "aarch64-linux"; + + hardware = { + enableRedistributableFirmware = lib.mkForce true; + enableAllFirmware = lib.mkForce true; + firmware = [ + pkgs.x1e80100-firmware + pkgs.x1e80100-linux-firmware + ]; + deviceTree = { + enable = true; + name = dtbName; + filter = "*romulus*"; + overlays = [ + { + name = "surface-laptop-7-sam"; + dtsFile = ./kernel/dtb-overlays/surface-laptop-7-sam.dts; + } + { + name = "surface-laptop-7-touchpad"; + dtsFile = ./kernel/dtb-overlays/surface-laptop-7-touchpad.dts; + } + { + name = "surface-laptop-7-thermal"; + dtsFile = ./kernel/dtb-overlays/surface-laptop-7-thermal.dts; + } + ]; + }; + }; + }; +} diff --git a/kernel-modules.nix b/kernel-modules.nix new file mode 100644 index 0000000..669afc0 --- /dev/null +++ b/kernel-modules.nix @@ -0,0 +1,271 @@ +{ config, lib, pkgs, ... }: +let + kernel = config.boot.kernelPackages.kernel; + + spi-hid = pkgs.stdenv.mkDerivation { + pname = "spi-hid"; + version = "0.3.1-${kernel.version}"; + src = ./kernel/modules/spi-hid; + hardeningDisable = [ + "pic" + "format" + ]; + nativeBuildInputs = kernel.moduleBuildDependencies ++ [ pkgs.kmod ]; + makeFlags = [ + "KERNELRELEASE=${kernel.modDirVersion}" + "KERNEL_DIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" + "INSTALL_MOD_PATH=$(out)" + ]; + buildPhase = '' + runHook preBuild + make -C ${kernel.dev}/lib/modules/${kernel.modDirVersion}/build \ + M=$(pwd) \ + ARCH=${pkgs.stdenv.hostPlatform.linuxArch} \ + modules + runHook postBuild + ''; + installPhase = '' + runHook preInstall + install -D -m 644 spi-hid.ko $out/lib/modules/${kernel.modDirVersion}/extra/spi-hid.ko + runHook postInstall + ''; + enableParallelBuilding = true; + meta = with lib; { + description = "HID over SPI (HIDSPI v3) QSPI transport driver"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + + + ath12k-norfkill = pkgs.stdenv.mkDerivation { + pname = "ath12k-norfkill"; + inherit (kernel) + src + version + postPatch + nativeBuildInputs + ; + kernel_dev = kernel.dev; + kernelVersion = kernel.modDirVersion; + patches = [ + ./kernel/modules/ath12k/disable-rfkill.patch + ]; + buildPhase = '' + BUILT_KERNEL=$kernel_dev/lib/modules/$kernelVersion/build + cp $BUILT_KERNEL/Module.symvers . + cp $BUILT_KERNEL/.config . + cp $kernel_dev/vmlinux . + make "-j$NIX_BUILD_CORES" modules_prepare + make "-j$NIX_BUILD_CORES" M=drivers/net/wireless/ath/ath12k modules + ''; + installPhase = '' + install -D -m 644 drivers/net/wireless/ath/ath12k/ath12k.ko \ + $out/lib/modules/${kernel.modDirVersion}/extra/ath12k.ko + if [ -f drivers/net/wireless/ath/ath12k/wifi7/ath12k_wifi7.ko ]; then + install -D -m 644 drivers/net/wireless/ath/ath12k/wifi7/ath12k_wifi7.ko \ + $out/lib/modules/${kernel.modDirVersion}/extra/ath12k_wifi7.ko + fi + ''; + meta = with lib; { + description = "ath12k with rfkill config early-return workaround"; + license = licenses.bsd3; + platforms = platforms.linux; + }; + }; + + gpi-qspi = pkgs.stdenv.mkDerivation { + pname = "gpi-qspi"; + inherit (kernel) + src + version + postPatch + nativeBuildInputs + ; + kernel_dev = kernel.dev; + kernelVersion = kernel.modDirVersion; + patches = [ + ./kernel/modules/qcom-qspi/gpi.patch + ]; + buildPhase = '' + BUILT_KERNEL=$kernel_dev/lib/modules/$kernelVersion/build + cp $BUILT_KERNEL/Module.symvers . + cp $BUILT_KERNEL/.config . + cp $kernel_dev/vmlinux . + make "-j$NIX_BUILD_CORES" modules_prepare + make "-j$NIX_BUILD_CORES" M=drivers/dma/qcom modules + ''; + installPhase = '' + make \ + INSTALL_MOD_PATH="$out" \ + XZ="xz -T$NIX_BUILD_CORES" \ + M="drivers/dma/qcom" \ + modules_install + ''; + meta = with lib; { + description = "Qualcomm GPI DMA with QSPI protocol 9 support"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + + spi-geni-qcom-qspi = pkgs.stdenv.mkDerivation { + pname = "spi-geni-qcom-qspi"; + inherit (kernel) + src + version + postPatch + nativeBuildInputs + ; + kernel_dev = kernel.dev; + kernelVersion = kernel.modDirVersion; + patches = [ + ./kernel/modules/qcom-qspi/gpi.patch + ./kernel/modules/qcom-qspi/spi-geni-qcom.patch + ]; + buildPhase = '' + BUILT_KERNEL=$kernel_dev/lib/modules/$kernelVersion/build + cp $BUILT_KERNEL/Module.symvers . + cp $BUILT_KERNEL/.config . + cp $kernel_dev/vmlinux . + make "-j$NIX_BUILD_CORES" modules_prepare + make "-j$NIX_BUILD_CORES" M=drivers/spi CONFIG_SPI_QCOM_GENI=m + ''; + installPhase = '' + install -D -m 644 drivers/spi/spi-geni-qcom.ko \ + $out/lib/modules/${kernel.modDirVersion}/extra/spi-geni-qcom.ko + ''; + meta = with lib; { + description = "Qualcomm GENI SPI with QSPI 1-4-4 support"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + + cpu-parking = pkgs.stdenv.mkDerivation { + pname = "cpu-parking"; + version = "0.1.0-${kernel.version}"; + src = ./kernel/modules/cpu-parking; + hardeningDisable = [ + "pic" + "format" + ]; + nativeBuildInputs = kernel.moduleBuildDependencies ++ [ pkgs.kmod ]; + makeFlags = [ + "KERNELRELEASE=${kernel.modDirVersion}" + "KERNEL_DIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" + "INSTALL_MOD_PATH=$(out)" + ]; + buildPhase = '' + runHook preBuild + make -C ${kernel.dev}/lib/modules/${kernel.modDirVersion}/build \ + M=$(pwd) \ + ARCH=${pkgs.stdenv.hostPlatform.linuxArch} \ + modules + runHook postBuild + ''; + installPhase = '' + runHook preInstall + install -D -m 644 cpu_parking.ko $out/lib/modules/${kernel.modDirVersion}/extra/cpu_parking.ko + runHook postInstall + ''; + enableParallelBuilding = true; + meta = with lib; { + description = "CPU core parking for Snapdragon X Elite (x1e80100)"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + + ec-reboot = pkgs.stdenv.mkDerivation { + pname = "ec-reboot"; + version = "0.1.0-${kernel.version}"; + src = ./kernel/modules/ec-reboot; + hardeningDisable = [ + "pic" + "format" + ]; + nativeBuildInputs = kernel.moduleBuildDependencies ++ [ pkgs.kmod ]; + makeFlags = [ + "KERNELRELEASE=${kernel.modDirVersion}" + "KERNEL_DIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" + "INSTALL_MOD_PATH=$(out)" + ]; + buildPhase = '' + runHook preBuild + make -C ${kernel.dev}/lib/modules/${kernel.modDirVersion}/build \ + M=$(pwd) \ + ARCH=${pkgs.stdenv.hostPlatform.linuxArch} \ + modules + runHook postBuild + ''; + installPhase = '' + runHook preInstall + install -D -m 644 ec-reboot.ko $out/lib/modules/${kernel.modDirVersion}/extra/ec-reboot.ko + runHook postInstall + ''; + enableParallelBuilding = true; + meta = with lib; { + description = "Trigger EC hard reset via SSAM for Surface Laptop 7"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + + platform-profile = pkgs.stdenv.mkDerivation { + pname = "platform-profile-no-acpi"; + inherit (kernel) + src + version + postPatch + nativeBuildInputs + ; + kernel_dev = kernel.dev; + kernelVersion = kernel.modDirVersion; + patches = [ + ./kernel/modules/platform-profile/platform-profile-no-acpi.patch + ]; + buildPhase = '' + BUILT_KERNEL=$kernel_dev/lib/modules/$kernelVersion/build + cp $BUILT_KERNEL/Module.symvers . + cp $BUILT_KERNEL/.config . + cp $kernel_dev/vmlinux . + make "-j$NIX_BUILD_CORES" modules_prepare + make "-j$NIX_BUILD_CORES" M=drivers/acpi modules + ''; + installPhase = '' + make \ + INSTALL_MOD_PATH="$out" \ + XZ="xz -T$NIX_BUILD_CORES" \ + M="drivers/acpi" \ + modules_install + ''; + meta = with lib; { + description = "Platform profile module patched for DT-based systems"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; + }; + in +{ + options.x1e = { + cpuParking = lib.mkEnableOption "CPU core parking for Snapdragon X Elite"; + ecReboot = lib.mkEnableOption "EC hard-reset sysfs trigger (/sys/kernel/ec_reboot/reboot)"; + }; + + config = { + boot.extraModulePackages = [ + spi-hid + gpi-qspi + spi-geni-qcom-qspi + ath12k-norfkill + platform-profile + ] + ++ lib.optional config.x1e.ecReboot ec-reboot + ++ lib.optional config.x1e.cpuParking cpu-parking; + + boot.kernelModules = + lib.optional config.x1e.ecReboot "ec_reboot" + ++ lib.optional config.x1e.cpuParking "cpu_parking"; + }; +} diff --git a/kernel.nix b/kernel.nix new file mode 100644 index 0000000..bf2a45d --- /dev/null +++ b/kernel.nix @@ -0,0 +1,166 @@ +{ pkgs, lib, ... }: +{ + boot = { + kernelPackages = + let + linux_x1e = pkgs.buildLinux rec { + version = "7.0.0-rc5"; + modDirVersion = "7.0.0-rc5"; + src = pkgs.fetchFromGitHub { + owner = "torvalds"; + repo = "linux"; + rev = "d1d81e9d1a4dd846aee9ae77ff9ecc2800d72148"; # v7.0-rc5 + hash = "sha256-UN1xOwSyn5YzdxQzEF6vTKev6vtN3iE2aiv7OT7TBAM="; + }; + ignoreConfigErrors = true; + structuredExtraConfig = with lib.kernel; { + SURFACE_PLATFORMS = yes; + SURFACE_AGGREGATOR = module; + SURFACE_AGGREGATOR_BUS = yes; + SURFACE_AGGREGATOR_REGISTRY = module; + SURFACE_AGGREGATOR_CDEV = module; + SURFACE_ACPI_NOTIFY = module; + SURFACE_PLATFORM_PROFILE = module; + SENSORS_SURFACE_FAN = module; + SENSORS_SURFACE_TEMP = module; + SERIAL_DEV_BUS = yes; + SERIAL_DEV_CTRL_TTYPORT = yes; + STRICT_DEVMEM = lib.kernel.no; + IO_STRICT_DEVMEM = lib.kernel.no; + }; + }; + in + lib.recurseIntoAttrs (pkgs.linuxPackagesFor linux_x1e); + + # No blacklist needed: the out-of-tree copies of spi-geni-qcom, + # ath12k and ath12k_wifi7 built by kernel-modules.nix land in + # lib/modules/.../extra/, which depmod prioritises over the in-tree + # kernel/ versions. Blacklisting by name would block auto-load via + # PCI/DT modalias. + + initrd = { + includeDefaultModules = lib.mkForce false; + systemd = { + enable = true; + emergencyAccess = true; + tpm2.enable = false; + }; + kernelModules = [ + "msm" + "qrtr" + "drm_exec" + "tcsrcc_x1e80100" + "gpucc_x1e80100" + "dispcc_x1e80100" + "phy_qcom_edp" + "panel_edp" + "pmic_glink_altmode" + "ps883x" + "gpu_sched" + "i2c_hid_of" + "i2c_qcom_geni" + ]; + availableKernelModules = [ + "btrfs" + "garp" + "mrp" + "stp" + "llc" + "sch_fq_codel" + "nfnetlink" + "dmi_sysfs" + "autofs4" + "isofs" + "cdc_ether" + "onboard_usb_dev" + "usbhid" + "uas" + "usb_storage" + "snd_soc_hdmi_codec" + "input_leds" + "hid_generic" + "hid" + "nvme" + "pm8941_pwrkey" + "nvme_core" + "crc_itu_t" + "libarc4" + "mhi" + "qcom_spmi_temp_alarm" + "industrialio" + "leds_qcom_lpg" + "qcom_pbs" + "led_class_multicolor" + "qcom_pon" + "nvmem_qcom_spmi_sdam" + "reboot_mode" + "snd_soc_lpass_rx_macro" + "snd_soc_lpass_wsa_macro" + "snd_soc_lpass_tx_macro" + "snd_soc_lpass_va_macro" + "snd_soc_lpass_macro_common" + "qcom_q6v5_pas" + "qcom_pil_info" + "qcom_q6v5" + "snd_soc_core" + "ac97_bus" + "qcom_sysmon" + "snd_pcm" + "pinctrl_sm8550_lpass_lpi" + "qcom_common" + "qcom_spmi_pmic" + "snd_timer" + "ghash_ce" + "pinctrl_lpass_lpi" + "qcom_stats" + "phy_qcom_edp" + "i2c_qcom_geni" + "qcom_geni_serial" + "snd" + "lpasscc_sc8280xp" + "dispcc_x1e80100" + "qcom_glink_smem" + "icc_bwmon" + "ucsi_glink" + "arm_smccc_trng" + "typec_ucsi" + "qcom_battmgr" + "fixed" + "socinfo" + "leds_gpio" + "uio" + "pwm_bl" + "efi_pstore" + "nls_iso8859_1" + "gpucc_x1e80100" + "tcsrcc_x1e80100" + "qrtr" + "msm" + "mdt_loader" + "ocmem" + "drm_exec" + "gpu_sched" + "overlay" + "phy_qcom_qmp_combo" + "phy_snps_eusb2" + "phy_qcom_eusb2_repeater" + "phy_qcom_qmp_pcie" + "tcsrcc_x1e80100" + "dispcc-x1e80100" + "gpucc-x1e80100" + "phy_qcom_edp" + "panel_edp" + "ps883x" + "pmic_glink_altmode" + "i2c_hid_of" + "surface_aggregator" + "surface_aggregator_registry" + "surface_aggregator_cdev" + "surface_acpi_notify" + "surface_platform_profile" + "surface_fan" + "surface_temp" + ]; + }; + }; +} diff --git a/kernel/dtb-overlays/surface-laptop-7-sam.dts b/kernel/dtb-overlays/surface-laptop-7-sam.dts new file mode 100644 index 0000000..2f6372f --- /dev/null +++ b/kernel/dtb-overlays/surface-laptop-7-sam.dts @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Surface Laptop 7 (Romulus) Surface Aggregator Module overlay. + * Connects the SAM EC to UART2 with hardware flow control. + */ + +/dts-v1/; +/plugin/; + +#define IRQ_TYPE_EDGE_RISING 1 + +/ { + compatible = "microsoft,romulus13", "microsoft,romulus15", "qcom,x1e80100"; +}; + +&tlmm { + qup_uart2_default: qup-uart2-default-state { + pins = "gpio11", "gpio10", "gpio8", "gpio9"; + function = "qup0_se2"; + drive-strength = <2>; + bias-disable; + }; + + sam_irq: sam-irq-state { + pins = "gpio91"; + function = "gpio"; + drive-strength = <2>; + bias-pull-up; + input-enable; + }; +}; + +&uart2 { + status = "okay"; + + pinctrl-0 = <&qup_uart2_default>; + pinctrl-names = "default"; + + embedded-controller { + compatible = "microsoft,surface-sam"; + interrupts-extended = <&tlmm 91 IRQ_TYPE_EDGE_RISING>; + current-speed = <4000000>; + }; +}; diff --git a/kernel/dtb-overlays/surface-laptop-7-thermal.dts b/kernel/dtb-overlays/surface-laptop-7-thermal.dts new file mode 100644 index 0000000..734093a --- /dev/null +++ b/kernel/dtb-overlays/surface-laptop-7-thermal.dts @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Surface Laptop 7 CPU thermal throttling overlay. Adds #cooling-cells + * to CPU nodes and passive trip points at 85 °C (5 °C hysteresis) so + * cpufreq cooling is registered and step_wise throttling kicks in. + */ + +/dts-v1/; +/plugin/; + +#define THERMAL_NO_LIMIT 0xffffffff + +/ { + compatible = "microsoft,romulus13", "microsoft,romulus15", "qcom,x1e80100"; +}; + +&cpu0 { #cooling-cells = <2>; }; +&cpu1 { #cooling-cells = <2>; }; +&cpu2 { #cooling-cells = <2>; }; +&cpu3 { #cooling-cells = <2>; }; +&cpu4 { #cooling-cells = <2>; }; +&cpu5 { #cooling-cells = <2>; }; +&cpu6 { #cooling-cells = <2>; }; +&cpu7 { #cooling-cells = <2>; }; +&cpu8 { #cooling-cells = <2>; }; +&cpu9 { #cooling-cells = <2>; }; +&cpu10 { #cooling-cells = <2>; }; +&cpu11 { #cooling-cells = <2>; }; + +&{/thermal-zones/cpu0-0-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu0_0_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu0_0_passive>; + cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu0-1-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu0_1_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu0_1_passive>; + cooling-device = <&cpu1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu0-2-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu0_2_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu0_2_passive>; + cooling-device = <&cpu2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu0-3-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu0_3_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu0_3_passive>; + cooling-device = <&cpu3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu1-0-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu1_0_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu1_0_passive>; + cooling-device = <&cpu4 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu1-1-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu1_1_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu1_1_passive>; + cooling-device = <&cpu5 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu1-2-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu1_2_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu1_2_passive>; + cooling-device = <&cpu6 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu1-3-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu1_3_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu1_3_passive>; + cooling-device = <&cpu7 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu2-0-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu2_0_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu2_0_passive>; + cooling-device = <&cpu8 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu2-1-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu2_1_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu2_1_passive>; + cooling-device = <&cpu9 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu2-2-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu2_2_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu2_2_passive>; + cooling-device = <&cpu10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; + +&{/thermal-zones/cpu2-3-top-thermal} { + polling-delay-passive = <250>; + polling-delay = <1000>; + trips { + cpu2_3_passive: trip-passive { + temperature = <85000>; + hysteresis = <5000>; + type = "passive"; + }; + }; + cooling-maps { + map0 { + trip = <&cpu2_3_passive>; + cooling-device = <&cpu11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; +}; diff --git a/kernel/dtb-overlays/surface-laptop-7-touchpad.dts b/kernel/dtb-overlays/surface-laptop-7-touchpad.dts new file mode 100644 index 0000000..d6fa7d5 --- /dev/null +++ b/kernel/dtb-overlays/surface-laptop-7-touchpad.dts @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Surface Laptop 7 (Romulus) MSHW0238 QSPI touchpad overlay. + */ + +/dts-v1/; +/plugin/; + +#define IRQ_TYPE_LEVEL_LOW 8 +#define QCOM_GPI_QSPI 4 + +/ { + compatible = "microsoft,romulus13", "microsoft,romulus15", "qcom,x1e80100"; +}; + +&gpi_dma2 { + status = "okay"; +}; + +&{/} { + vreg_ts_5p0: ts-5p0-regulator { + compatible = "regulator-fixed"; + regulator-name = "vreg_ts_5p0"; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + gpio = <&tlmm 65 0>; + enable-active-high; + startup-delay-us = <100000>; + }; +}; + +&tlmm { + qup_qspi19_data_clk: qup-qspi19-data-clk-state { + pins = "gpio76", "gpio77", "gpio78"; + function = "qup2_se3"; + drive-strength = <6>; + bias-disable; + }; + + qup_qspi19_cs: qup-qspi19-cs-state { + pins = "gpio79"; + function = "qup2_se3"; + drive-strength = <6>; + bias-disable; + }; + + qup_qspi19_data23: qup-qspi19-data23-state { + pins = "gpio66", "gpio67"; + function = "qup2_se3"; + drive-strength = <6>; + bias-disable; + }; + + spi19_hid0_reset_deassert: spi19-hid0-reset-deassert-state { + pins = "gpio65", "gpio120"; + function = "gpio"; + drive-strength = <16>; + bias-disable; + output-high; + }; + + spi19_hid0_reset_assert: spi19-hid0-reset-assert-state { + pins = "gpio65", "gpio120"; + function = "gpio"; + drive-strength = <16>; + bias-disable; + output-low; + }; + + spi19_hid0_int_bias: spi19-hid0-int-bias-state { + pins = "gpio3"; + function = "gpio"; + input-enable; + bias-pull-up; + }; +}; + +&spi19 { + status = "okay"; + compatible = "qcom,geni-spi-qspi"; + spi-max-frequency = <20000000>; + qcom,qspi-read-opcode = <0xEB>; + qcom,qspi-read-dummy-clocks = <8>; + qcom,qspi-read-cmd-bytes = <4>; + + pinctrl-0 = <&qup_qspi19_data_clk>, <&qup_qspi19_cs>, + <&qup_qspi19_data23>, + <&spi19_hid0_int_bias>; + pinctrl-names = "default"; + + dmas = <&gpi_dma2 0 3 QCOM_GPI_QSPI>, + <&gpi_dma2 1 3 QCOM_GPI_QSPI>; + dma-names = "tx", "rx"; + + touchpad: touchpad@0 { + compatible = "hid-over-spi"; + reg = <0>; + spi-max-frequency = <20000000>; + + interrupt-parent = <&tlmm>; + interrupts = <3 IRQ_TYPE_LEVEL_LOW>; + + vdd-supply = <&vreg_ts_5p0>; + + pinctrl-0 = <&spi19_hid0_reset_deassert>; + pinctrl-1 = <&spi19_hid0_reset_assert>; + pinctrl-names = "active", "reset"; + }; +}; diff --git a/kernel/modules/ath12k/disable-rfkill.patch b/kernel/modules/ath12k/disable-rfkill.patch new file mode 100644 index 0000000..654cd84 --- /dev/null +++ b/kernel/modules/ath12k/disable-rfkill.patch @@ -0,0 +1,11 @@ +--- a/drivers/net/wireless/ath/ath12k/core.c ++++ b/drivers/net/wireless/ath/ath12k/core.c +@@ -77,6 +77,8 @@ static int ath12k_core_rfkill_config(struct ath12k_base *ab) + if (!(ab->target_caps.sys_cap_info & WMI_SYS_CAP_INFO_RFKILL)) + return 0; + ++ return 0; ++ + if (ath12k_acpi_get_disable_rfkill(ab)) + return 0; + diff --git a/kernel/modules/cpu-parking/Makefile b/kernel/modules/cpu-parking/Makefile new file mode 100644 index 0000000..d629a1d --- /dev/null +++ b/kernel/modules/cpu-parking/Makefile @@ -0,0 +1,9 @@ +obj-m += cpu_parking.o + +KDIR ?= /lib/modules/$(shell uname -r)/build + +all: + $(MAKE) -C $(KDIR) M=$(PWD) modules + +clean: + $(MAKE) -C $(KDIR) M=$(PWD) clean diff --git a/kernel/modules/cpu-parking/cpu_parking.c b/kernel/modules/cpu-parking/cpu_parking.c new file mode 100644 index 0000000..8e6930d --- /dev/null +++ b/kernel/modules/cpu-parking/cpu_parking.c @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * CPU core parking for Snapdragon X Elite. + */ + +#include +#include +#include +#include +#include + +#define DRIVER_NAME "cpu_parking" +#define MAX_CPUS 16 + +static int set_enabled(const char *val, const struct kernel_param *kp); +static const struct kernel_param_ops enabled_ops = { + .set = set_enabled, + .get = param_get_bool, +}; + +static bool enabled = true; +module_param_cb(enabled, &enabled_ops, &enabled, 0644); +MODULE_PARM_DESC(enabled, "Enable/disable parking (default: 1)"); + +static unsigned int sample_interval_ms = 1000; +module_param(sample_interval_ms, uint, 0644); +MODULE_PARM_DESC(sample_interval_ms, "Polling interval in ms (default: 1000)"); + +static unsigned int busy_up_pct = 75; +module_param(busy_up_pct, uint, 0644); +MODULE_PARM_DESC(busy_up_pct, "Avg util%% above which to unpark a core (default: 75)"); + +static unsigned int busy_down_pct = 30; +module_param(busy_down_pct, uint, 0644); +MODULE_PARM_DESC(busy_down_pct, "Avg util%% below which to park a core (default: 30)"); + +static unsigned int min_online; +module_param(min_online, uint, 0644); +MODULE_PARM_DESC(min_online, "Minimum P-cores to keep online (default: 0)"); + +static unsigned int parkable_cpus = 0xFF0; +module_param(parkable_cpus, uint, 0644); +MODULE_PARM_DESC(parkable_cpus, "Bitmask of parkable CPUs (default: 0xFF0 = CPUs 4-11)"); + +static unsigned int parked_mask; +module_param_named(parked_cpus, parked_mask, uint, 0444); +MODULE_PARM_DESC(parked_cpus, "Current parked CPU bitmask (read-only)"); + +static unsigned int last_avg_util; +module_param(last_avg_util, uint, 0444); +MODULE_PARM_DESC(last_avg_util, "Last average utilization %% (read-only)"); + +static u64 prev_idle[MAX_CPUS]; +static u64 prev_wall[MAX_CPUS]; +static struct delayed_work parking_work; + +static void snapshot_cpu(unsigned int cpu) +{ + u64 wall; + + if (cpu >= MAX_CPUS) + return; + prev_idle[cpu] = get_cpu_idle_time_us(cpu, &wall); + prev_wall[cpu] = wall; +} + +static void snapshot_all_online(void) +{ + unsigned int cpu; + + for_each_online_cpu(cpu) + snapshot_cpu(cpu); +} + +static void park_cpu(unsigned int cpu) +{ + if (!cpu_online(cpu)) + return; + if (remove_cpu(cpu)) { + pr_warn(DRIVER_NAME ": failed to park cpu%u\n", cpu); + return; + } + parked_mask |= BIT(cpu); + pr_info(DRIVER_NAME ": parked cpu%u (parked=0x%03x)\n", cpu, parked_mask); +} + +static void unpark_cpu(unsigned int cpu) +{ + if (cpu_online(cpu)) + return; + if (add_cpu(cpu)) { + pr_warn(DRIVER_NAME ": failed to unpark cpu%u\n", cpu); + return; + } + parked_mask &= ~BIT(cpu); + snapshot_cpu(cpu); + pr_info(DRIVER_NAME ": unparked cpu%u (parked=0x%03x)\n", cpu, parked_mask); +} + +static void unpark_all(void) +{ + int cpu; + + for_each_possible_cpu(cpu) { + if (parked_mask & BIT(cpu)) + unpark_cpu(cpu); + } +} + +static void parking_work_fn(struct work_struct *work) +{ + unsigned int cpu, nr_sampled = 0, total_util = 0; + unsigned int online_parkable = 0, parked_parkable; + u64 idle, wall, d_idle, d_wall; + int target; + + if (!enabled) { + unpark_all(); + return; + } + + for_each_online_cpu(cpu) { + if (cpu >= MAX_CPUS) + continue; + + idle = get_cpu_idle_time_us(cpu, &wall); + if (idle == (u64)-1) + continue; + + d_idle = idle - prev_idle[cpu]; + d_wall = wall - prev_wall[cpu]; + prev_idle[cpu] = idle; + prev_wall[cpu] = wall; + + if (d_wall > 0 && d_idle <= d_wall) + total_util += (unsigned int)((d_wall - d_idle) * 100 / d_wall); + + nr_sampled++; + if (parkable_cpus & BIT(cpu)) + online_parkable++; + } + + if (nr_sampled == 0) + goto resched; + + last_avg_util = total_util / nr_sampled; + parked_parkable = hweight32(parked_mask & parkable_cpus); + + if (last_avg_util > busy_up_pct && parked_parkable > 0) { + for (cpu = 0; cpu < MAX_CPUS; cpu++) { + if ((parked_mask & BIT(cpu)) && + (parkable_cpus & BIT(cpu))) { + unpark_cpu(cpu); + break; + } + } + } else if (last_avg_util < busy_down_pct && + online_parkable > min_online) { + for (target = MAX_CPUS - 1; target >= 0; target--) { + if ((parkable_cpus & BIT(target)) && + cpu_online(target) && + !(parked_mask & BIT(target))) { + park_cpu(target); + break; + } + } + } + +resched: + schedule_delayed_work(&parking_work, + msecs_to_jiffies(sample_interval_ms)); +} + +static int set_enabled(const char *val, const struct kernel_param *kp) +{ + bool was_enabled = enabled; + int ret; + + ret = param_set_bool(val, kp); + if (ret) + return ret; + + if (!was_enabled && enabled) { + snapshot_all_online(); + schedule_delayed_work(&parking_work, + msecs_to_jiffies(sample_interval_ms)); + pr_info(DRIVER_NAME ": re-enabled\n"); + } + return 0; +} + +static int __init cpu_parking_init(void) +{ + snapshot_all_online(); + INIT_DELAYED_WORK(&parking_work, parking_work_fn); + schedule_delayed_work(&parking_work, + msecs_to_jiffies(sample_interval_ms)); + + pr_info(DRIVER_NAME ": loaded (parkable=0x%03x interval=%ums up=%u%% down=%u%%)\n", + parkable_cpus, sample_interval_ms, busy_up_pct, busy_down_pct); + return 0; +} + +static void __exit cpu_parking_exit(void) +{ + cancel_delayed_work_sync(&parking_work); + unpark_all(); + pr_info(DRIVER_NAME ": unloaded, all cores online\n"); +} + +module_init(cpu_parking_init); +module_exit(cpu_parking_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("CPU core parking for Snapdragon X Elite (x1e80100)"); diff --git a/kernel/modules/ec-reboot/Makefile b/kernel/modules/ec-reboot/Makefile new file mode 100644 index 0000000..235e4eb --- /dev/null +++ b/kernel/modules/ec-reboot/Makefile @@ -0,0 +1,13 @@ +ifneq ($(KERNELRELEASE),) +obj-m += ec-reboot.o +ec-reboot-objs := ec_reboot.o +else +KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build +PWD := $(shell pwd) + +all: + $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules + +clean: + $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean +endif diff --git a/kernel/modules/ec-reboot/ec_reboot.c b/kernel/modules/ec-reboot/ec_reboot.c new file mode 100644 index 0000000..699a0a7 --- /dev/null +++ b/kernel/modules/ec-reboot/ec_reboot.c @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include + +static ssize_t reboot_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct ssam_controller *ctrl; + struct ssam_request rqst = {}; + int ret; + + if (count < 1 || buf[0] != '1') + return -EINVAL; + + ctrl = ssam_get_controller(); + if (!ctrl) + return -ENODEV; + + rqst.target_category = 0x01; + rqst.target_id = 0x01; + rqst.command_id = 0x14; + rqst.instance_id = 0x00; + rqst.flags = 0; + rqst.length = 0; + rqst.payload = NULL; + + ret = ssam_request_do_sync(ctrl, &rqst, NULL); + if (ret) + return ret; + + return count; +} + +static struct kobj_attribute reboot_attr = __ATTR_WO(reboot); + +static struct attribute *ec_reboot_attrs[] = { + &reboot_attr.attr, + NULL, +}; + +static const struct attribute_group ec_reboot_group = { + .attrs = ec_reboot_attrs, +}; + +static struct kobject *ec_reboot_kobj; + +static int __init ec_reboot_init(void) +{ + int ret; + + ec_reboot_kobj = kobject_create_and_add("ec_reboot", kernel_kobj); + if (!ec_reboot_kobj) + return -ENOMEM; + + ret = sysfs_create_group(ec_reboot_kobj, &ec_reboot_group); + if (ret) + kobject_put(ec_reboot_kobj); + + return ret; +} + +static void __exit ec_reboot_exit(void) +{ + kobject_put(ec_reboot_kobj); +} + +module_init(ec_reboot_init); +module_exit(ec_reboot_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("EC hard reset via SSAM"); diff --git a/kernel/modules/platform-profile/platform-profile-no-acpi.patch b/kernel/modules/platform-profile/platform-profile-no-acpi.patch new file mode 100644 index 0000000..7424880 --- /dev/null +++ b/kernel/modules/platform-profile/platform-profile-no-acpi.patch @@ -0,0 +1,99 @@ +--- a/drivers/acpi/platform_profile.c ++++ b/drivers/acpi/platform_profile.c +@@ -176,7 +176,8 @@ static ssize_t profile_store(struct device *dev, + return ret; + } + +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ if (acpi_kobj) ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); + + return count; + } +@@ -341,7 +342,8 @@ static ssize_t platform_profile_store(struct kobject *kobj, + return ret; + } + +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ if (acpi_kobj) ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); + + return count; + } +@@ -377,7 +379,8 @@ void platform_profile_notify(struct device *dev) + scoped_cond_guard(mutex_intr, return, &profile_lock) { + _notify_class_profile(dev, NULL); + } +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ if (acpi_kobj) ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); + } + EXPORT_SYMBOL_GPL(platform_profile_notify); + +@@ -425,7 +428,8 @@ int platform_profile_cycle(void) + return err; + } + +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ if (acpi_kobj) ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); + + return 0; + } +@@ -487,9 +491,11 @@ struct device *platform_profile_register(struct device *dev, const char *name, + goto cleanup_ida; + } + +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ if (acpi_kobj) ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); + +- err = sysfs_update_group(acpi_kobj, &platform_profile_group); ++ err = acpi_kobj ? sysfs_update_group(acpi_kobj, &platform_profile_group) ++ : 0; + if (err) + goto cleanup_cur; + +@@ -519,8 +525,10 @@ void platform_profile_remove(struct device *dev) + ida_free(&platform_profile_ida, pprof->minor); + device_unregister(&pprof->dev); + +- sysfs_notify(acpi_kobj, NULL, "platform_profile"); +- sysfs_update_group(acpi_kobj, &platform_profile_group); ++ if (acpi_kobj) { ++ sysfs_notify(acpi_kobj, NULL, "platform_profile"); ++ sysfs_update_group(acpi_kobj, &platform_profile_group); ++ } + } + EXPORT_SYMBOL_GPL(platform_profile_remove); + +@@ -567,14 +575,16 @@ static int __init platform_profile_init(void) + { + int err; + +- if (acpi_disabled) +- return -EOPNOTSUPP; +- + err = class_register(&platform_profile_class); + if (err) + return err; + +- err = sysfs_create_group(acpi_kobj, &platform_profile_group); ++ /* ++ * Legacy sysfs interface under /sys/firmware/acpi/ is only available ++ * when ACPI is enabled. The class-based interface works regardless. ++ */ ++ err = acpi_kobj ? sysfs_create_group(acpi_kobj, &platform_profile_group) ++ : 0; + if (err) + class_unregister(&platform_profile_class); + +@@ -584,7 +594,8 @@ static int __init platform_profile_init(void) + static void __exit platform_profile_exit(void) + { +- sysfs_remove_group(acpi_kobj, &platform_profile_group); ++ if (acpi_kobj) ++ sysfs_remove_group(acpi_kobj, &platform_profile_group); + class_unregister(&platform_profile_class); + } + module_init(platform_profile_init); diff --git a/kernel/modules/qcom-qspi/gpi.patch b/kernel/modules/qcom-qspi/gpi.patch new file mode 100644 index 0000000..1826f50 --- /dev/null +++ b/kernel/modules/qcom-qspi/gpi.patch @@ -0,0 +1,197 @@ +diff -ruN a/drivers/dma/qcom/gpi.c b/drivers/dma/qcom/gpi.c +--- a/drivers/dma/qcom/gpi.c ++++ b/drivers/dma/qcom/gpi.c +@@ -48,6 +48,12 @@ + #define TRE_SPI_GO_CS GENMASK(10, 8) + #define TRE_SPI_GO_FRAG BIT(26) + ++/* QSPI GO WD0 - flags field is 12 bits at [31:20] instead of SPI's 8 bits */ ++#define TRE_QSPI_GO_FLAGS GENMASK(31, 20) ++ ++/* QSPI Config0 WD0 - dummy clock count */ ++#define TRE_SPI_C0_DUMMY_CLK GENMASK(21, 14) ++ + /* GO WD2 */ + #define TRE_RX_LEN GENMASK(23, 0) + +@@ -1275,8 +1281,17 @@ + upper_32_bits(ring->phys_addr)); + gpi_write_reg(gpii, chan->ch_cntxt_db_reg + CNTXT_5_RING_RP_MSB - CNTXT_4_RING_RP_LSB, + upper_32_bits(ring->phys_addr)); +- gpi_write_reg(gpii, gpii->regs + GPII_n_CH_k_SCRATCH_0_OFFS(id, chid), +- GPII_n_CH_k_SCRATCH_0(pair_chid, chan->protocol, chan->seid)); ++ /* ++ * For QSPI, use the SE's native protocol value (9) in SCRATCH_0. ++ * The DT binding uses QCOM_GPI_QSPI=4 for channel selection, but ++ * the GSI firmware expects the actual SE protocol ID. ++ */ ++ { ++ u32 hw_proto = chan->protocol == QCOM_GPI_QSPI ? ++ 9 : chan->protocol; ++ gpi_write_reg(gpii, gpii->regs + GPII_n_CH_k_SCRATCH_0_OFFS(id, chid), ++ GPII_n_CH_k_SCRATCH_0(pair_chid, hw_proto, chan->seid)); ++ } + gpi_write_reg(gpii, gpii->regs + GPII_n_CH_k_SCRATCH_1_OFFS(id, chid), 0); + gpi_write_reg(gpii, gpii->regs + GPII_n_CH_k_SCRATCH_2_OFFS(id, chid), 0); + gpi_write_reg(gpii, gpii->regs + GPII_n_CH_k_SCRATCH_3_OFFS(id, chid), 0); +@@ -1699,6 +1714,116 @@ + return tre_idx; + } + ++static void qspi_add_config_tre(struct gpi_spi_config *spi, ++ struct gpi_tre *tre) ++{ ++ tre->dword[0] = u32_encode_bits(spi->word_len, TRE_SPI_C0_WORD_SZ); ++ tre->dword[0] |= u32_encode_bits(spi->loopback_en, TRE_SPI_C0_LOOPBACK); ++ tre->dword[0] |= u32_encode_bits(spi->clock_pol_high, TRE_SPI_C0_CPOL); ++ tre->dword[0] |= u32_encode_bits(spi->data_pol_high, TRE_SPI_C0_CPHA); ++ tre->dword[0] |= u32_encode_bits(spi->pack_en, TRE_SPI_C0_TX_PACK); ++ tre->dword[0] |= u32_encode_bits(spi->pack_en, TRE_SPI_C0_RX_PACK); ++ tre->dword[0] |= u32_encode_bits(spi->dummy_clk_cnt, TRE_SPI_C0_DUMMY_CLK); ++ ++ tre->dword[1] = 0; ++ ++ tre->dword[2] = u32_encode_bits(spi->clk_div, TRE_C0_CLK_DIV); ++ tre->dword[2] |= u32_encode_bits(spi->clk_src, TRE_C0_CLK_SRC); ++ ++ tre->dword[3] = u32_encode_bits(TRE_TYPE_CONFIG0, TRE_FLAGS_TYPE); ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_CHAIN); ++} ++ ++static int gpi_create_qspi_tre(struct gchan *chan, struct gpi_desc *desc, ++ struct scatterlist *sgl, ++ enum dma_transfer_direction direction) ++{ ++ struct gpi_spi_config *spi = chan->config; ++ struct device *dev = chan->gpii->gpi_dev->dev; ++ unsigned int tre_idx = 0; ++ struct gpi_tre *tre; ++ u32 qspi_flags; ++ dma_addr_t address; ++ int len; ++ ++ /* Config TRE (TX channel only) */ ++ if (direction == DMA_MEM_TO_DEV && spi->set_config) { ++ qspi_add_config_tre(spi, &desc->tre[tre_idx]); ++ tre_idx++; ++ } ++ ++ /* Go TRE (TX channel only) */ ++ if (direction == DMA_MEM_TO_DEV) { ++ tre = &desc->tre[tre_idx]; ++ tre_idx++; ++ ++ qspi_flags = spi->qspi_lane_flags; ++ if (spi->fragmentation) ++ qspi_flags |= BIT(6); ++ ++ tre->dword[0] = u32_encode_bits(qspi_flags, TRE_QSPI_GO_FLAGS); ++ tre->dword[0] |= u32_encode_bits(spi->cs, TRE_SPI_GO_CS); ++ tre->dword[0] |= u32_encode_bits(spi->cmd, TRE_SPI_GO_CMD); ++ ++ tre->dword[1] = 0; ++ tre->dword[2] = u32_encode_bits(spi->rx_len, TRE_RX_LEN); ++ ++ tre->dword[3] = u32_encode_bits(TRE_TYPE_GO, TRE_FLAGS_TYPE); ++ if (spi->cmd == SPI_RX) { ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_IEOB); ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_LINK); ++ } else if (spi->cmd == SPI_TX) { ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_CHAIN); ++ } else { /* SPI_TX_RX or SPI_DUPLEX */ ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_CHAIN); ++ if (spi->rx_len > 0) ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_LINK); ++ } ++ } ++ ++ /* DMA TRE - skip for SPI_RX on TX channel */ ++ if (direction == DMA_MEM_TO_DEV && spi->cmd == SPI_RX) ++ goto skip_dma_tre; ++ ++ tre = &desc->tre[tre_idx]; ++ tre_idx++; ++ ++ address = sg_dma_address(sgl); ++ len = sg_dma_len(sgl); ++ ++ /* ++ * For QSPI TX_RX, limit TX DMA to command bytes only. ++ * The SE parses the TX data as opcode+address and uses rx_len ++ * from the Go TRE for the receive phase. Sending more TX bytes ++ * than the command length confuses the SE's QSPI state machine. ++ */ ++ if (direction == DMA_MEM_TO_DEV && spi->cmd == SPI_TX_RX && ++ spi->tx_cmd_len > 0 && spi->tx_cmd_len < len) ++ len = spi->tx_cmd_len; ++ ++ if (direction == DMA_MEM_TO_DEV && len <= 2 * sizeof(tre->dword[0])) { ++ tre->dword[0] = 0; ++ tre->dword[1] = 0; ++ memcpy(&tre->dword[0], sg_virt(sgl), len); ++ tre->dword[2] = u32_encode_bits(len, TRE_DMA_IMMEDIATE_LEN); ++ tre->dword[3] = u32_encode_bits(TRE_TYPE_IMMEDIATE_DMA, TRE_FLAGS_TYPE); ++ } else { ++ tre->dword[0] = lower_32_bits(address); ++ tre->dword[1] = upper_32_bits(address); ++ tre->dword[2] = u32_encode_bits(len, TRE_DMA_LEN); ++ tre->dword[3] = u32_encode_bits(TRE_TYPE_DMA, TRE_FLAGS_TYPE); ++ } ++ tre->dword[3] |= u32_encode_bits(1, TRE_FLAGS_IEOT); ++ ++skip_dma_tre: ++ for (len = 0; len < tre_idx; len++) ++ dev_dbg(dev, "QSPI TRE:%d %x:%x:%x:%x\n", len, ++ desc->tre[len].dword[0], desc->tre[len].dword[1], ++ desc->tre[len].dword[2], desc->tre[len].dword[3]); ++ ++ return tre_idx; ++} ++ + static int gpi_create_spi_tre(struct gchan *chan, struct gpi_desc *desc, + struct scatterlist *sgl, enum dma_transfer_direction direction) + { +@@ -1841,7 +1966,9 @@ + return NULL; + + /* create TREs for xfer */ +- if (gchan->protocol == QCOM_GPI_SPI) { ++ if (gchan->protocol == QCOM_GPI_QSPI) { ++ i = gpi_create_qspi_tre(gchan, gpi_desc, sgl, direction); ++ } else if (gchan->protocol == QCOM_GPI_SPI) { + i = gpi_create_spi_tre(gchan, gpi_desc, sgl, direction); + } else if (gchan->protocol == QCOM_GPI_I2C) { + i = gpi_create_i2c_tre(gchan, gpi_desc, sgl, direction, flags); +diff -ruN a/include/dt-bindings/dma/qcom-gpi.h b/include/dt-bindings/dma/qcom-gpi.h +--- a/include/dt-bindings/dma/qcom-gpi.h ++++ b/include/dt-bindings/dma/qcom-gpi.h +@@ -7,5 +7,6 @@ + #define QCOM_GPI_SPI 1 + #define QCOM_GPI_UART 2 + #define QCOM_GPI_I2C 3 ++#define QCOM_GPI_QSPI 4 + + #endif /* __DT_BINDINGS_DMA_QCOM_GPI_H__ */ +diff -ruN a/include/linux/dma/qcom-gpi-dma.h b/include/linux/dma/qcom-gpi-dma.h +--- a/include/linux/dma/qcom-gpi-dma.h ++++ b/include/linux/dma/qcom-gpi-dma.h +@@ -13,6 +13,7 @@ + SPI_TX = 1, + SPI_RX, + SPI_DUPLEX, ++ SPI_TX_RX = 7, /* QSPI full-duplex (TX opcode+addr, RX data) */ + }; + + /** +@@ -44,6 +45,11 @@ + u32 clk_src; + enum spi_transfer_cmd cmd; + u32 rx_len; ++ /* QSPI extensions */ ++ bool qspi_mode; ++ u16 qspi_lane_flags; ++ u8 dummy_clk_cnt; ++ u16 tx_cmd_len; + }; + + enum i2c_op { diff --git a/kernel/modules/qcom-qspi/spi-geni-qcom.patch b/kernel/modules/qcom-qspi/spi-geni-qcom.patch new file mode 100644 index 0000000..efa6a94 --- /dev/null +++ b/kernel/modules/qcom-qspi/spi-geni-qcom.patch @@ -0,0 +1,354 @@ +diff -ruN a/drivers/spi/spi-geni-qcom.c b/drivers/spi/spi-geni-qcom.c +--- a/drivers/spi/spi-geni-qcom.c ++++ b/drivers/spi/spi-geni-qcom.c +@@ -2,6 +2,7 @@ + // Copyright (c) 2017-2018, The Linux foundation. All rights reserved. + + #include ++#include + #include + #include + #include +@@ -75,9 +76,68 @@ + #define GSI_CPHA BIT(4) + #define GSI_CPOL BIT(5) + ++/* QSPI 1-4-4 support (added on top of the standard SPI controller). */ ++#define QSPI_SE_PROTO 9 ++ ++#define GENI_IO_MUX_1_EN BIT(1) ++#define GENI_IO_MUX_2_EN BIT(2) ++#define GENI_IO_MUX_3_EN BIT(3) ++#define GENI_QSPI_IO_MUX_EN (GENI_IO_MUX_0_EN | GENI_IO_MUX_1_EN | \ ++ GENI_IO_MUX_2_EN | GENI_IO_MUX_3_EN) ++ ++#define SE_GSI_EVENT_EN 0xe18 ++#define SE_IRQ_EN 0xe1c ++#define SE_DMA_TX_IRQ_CLR 0xc44 ++#define SE_DMA_TX_IRQ_EN_SET 0xc4c ++#define SE_DMA_TX_IRQ_EN_CLR 0xc50 ++#define SE_DMA_RX_IRQ_CLR 0xd44 ++#define SE_DMA_RX_IRQ_EN_SET 0xd4c ++#define SE_DMA_RX_IRQ_EN_CLR 0xd50 ++ ++#define DMA_RX_EVENT_EN BIT(0) ++#define DMA_TX_EVENT_EN BIT(1) ++#define GENI_M_EVENT_EN BIT(2) ++#define GENI_S_EVENT_EN BIT(3) ++ ++#define QSPI_M_IRQ_EN_GPI 0x33c00046 ++#define QSPI_S_IRQ_EN_GPI 0x03001e06 ++#define QSPI_DMA_TX_IRQ_EN 0x0d ++#define QSPI_DMA_RX_IRQ_EN 0x1d ++ ++#define QSPI_SINGLE_SDR 0x000 ++#define QSPI_QUAD_SDR BIT(9) ++ ++/* Defaults for a quad read with 1-byte opcode + 3-byte address. */ ++#define QSPI_DEFAULT_READ_OPCODE 0xEB ++#define QSPI_DEFAULT_DUMMY_CLK_CNT 8 ++#define QSPI_DEFAULT_TX_CMD_LEN 4 ++#define QSPI_DEFAULT_MAX_SPEED_HZ 20000000 ++ ++/** ++ * struct spi_geni_data - per-compatible behavioral flags ++ * @qspi_mode: controller runs in QSPI 1-4-4 mode with 4 data lanes ++ */ ++struct spi_geni_data { ++ bool qspi_mode; ++}; ++ ++/** ++ * struct spi_geni_qspi_params - per-SE QSPI tunables read from DT ++ * @read_opcode: first TX byte that identifies a read transfer (e.g. 0xEB) ++ * @dummy_clk_cnt: dummy clocks inserted between address and read data ++ * @tx_cmd_len: number of TX bytes forming the read command (opcode+address) ++ */ ++struct spi_geni_qspi_params { ++ u32 read_opcode; ++ u32 dummy_clk_cnt; ++ u32 tx_cmd_len; ++}; ++ + struct spi_geni_master { + struct geni_se se; + struct device *dev; ++ const struct spi_geni_data *data; ++ struct spi_geni_qspi_params qspi; + u32 tx_fifo_depth; + u32 fifo_width_bits; + u32 tx_wm; +@@ -104,6 +164,52 @@ + int cur_xfer_mode; + }; + ++static inline bool spi_geni_is_qspi(const struct spi_geni_master *mas) ++{ ++ return mas->data && mas->data->qspi_mode; ++} ++ ++/* Enable all 4 data lanes on the GENI output mux for QSPI. */ ++static void qspi_setup_io_mux(struct spi_geni_master *mas) ++{ ++ struct geni_se *se = &mas->se; ++ u32 out; ++ ++ out = readl(se->base + GENI_OUTPUT_CTRL); ++ out |= GENI_QSPI_IO_MUX_EN; ++ writel(out, se->base + GENI_OUTPUT_CTRL); ++} ++ ++/* ++ * Reprogram the SE IRQ / DMA / event registers for GPI DMA. ++ * ++ * geni_se_resources_on() (called from runtime_resume) writes a fixed set of ++ * values into SE_IRQ_EN that are correct for FIFO/SE_DMA mode but clobber the ++ * GPI configuration. The QSPI SE_PROTO (9) also needs different masks than ++ * the defaults. Call this from prepare_message (post resume, pre transfer) ++ * to restore the GPI-friendly values. ++ */ ++static void prep_se_for_gpi_dma(struct spi_geni_master *mas) ++{ ++ void __iomem *base = mas->se.base; ++ ++ writel(GENI_DMA_MODE_EN, base + SE_GENI_DMA_MODE_EN); ++ writel(0, base + SE_IRQ_EN); ++ writel(DMA_RX_EVENT_EN | DMA_TX_EVENT_EN | ++ GENI_M_EVENT_EN | GENI_S_EVENT_EN, ++ base + SE_GSI_EVENT_EN); ++ writel(QSPI_M_IRQ_EN_GPI, base + SE_GENI_M_IRQ_EN); ++ writel(QSPI_S_IRQ_EN_GPI, base + SE_GENI_S_IRQ_EN); ++ writel(0xf, base + SE_DMA_TX_IRQ_EN_CLR); ++ writel(QSPI_DMA_TX_IRQ_EN, base + SE_DMA_TX_IRQ_EN_SET); ++ writel(0xfff, base + SE_DMA_RX_IRQ_EN_CLR); ++ writel(QSPI_DMA_RX_IRQ_EN, base + SE_DMA_RX_IRQ_EN_SET); ++ writel(0xffc07fff, base + SE_GENI_M_IRQ_CLEAR); ++ writel(0x0fc07f3f, base + SE_GENI_S_IRQ_CLEAR); ++ writel(0xf, base + SE_DMA_TX_IRQ_CLR); ++ writel(0xfff, base + SE_DMA_RX_IRQ_CLR); ++} ++ + static void spi_slv_setup(struct spi_geni_master *mas) + { + struct geni_se *se = &mas->se; +@@ -411,7 +517,20 @@ + } + + if (xfer->tx_buf && xfer->rx_buf) { +- peripheral.cmd = SPI_DUPLEX; ++ /* ++ * QSPI uses SPI_TX_RX (7) for TX-opcode-then-RX-data transfers; ++ * standard SPI uses SPI_DUPLEX (3) for true full-duplex. ++ * ++ * For QSPI_TX_RX the GSI firmware needs an explicit rx_len so ++ * it knows how many bytes to clock in after the TX command ++ * phase. SPI_DUPLEX uses xfer->len implicitly. ++ */ ++ if (spi_geni_is_qspi(mas)) { ++ peripheral.cmd = SPI_TX_RX; ++ peripheral.rx_len = (xfer->len << 3) / xfer->bits_per_word; ++ } else { ++ peripheral.cmd = SPI_DUPLEX; ++ } + } else if (xfer->tx_buf) { + peripheral.cmd = SPI_TX; + peripheral.rx_len = 0; +@@ -445,6 +564,44 @@ + peripheral.fragmentation = FRAGMENTATION; + } + ++ if (spi_geni_is_qspi(mas)) { ++ bool multi = !list_is_singular(&spi->cur_msg->transfers); ++ ++ peripheral.qspi_mode = true; ++ ++ /* ++ * In a multi-transfer message the first write uses SINGLE_SDR ++ * (opcode+addr lane transition) while subsequent TX-only ++ * transfers stay in QUAD. ++ */ ++ if (multi && xfer->tx_buf && !xfer->rx_buf && ++ &xfer->transfer_list == spi->cur_msg->transfers.next) ++ peripheral.qspi_lane_flags = QSPI_SINGLE_SDR; ++ else ++ peripheral.qspi_lane_flags = QSPI_QUAD_SDR; ++ ++ if (peripheral.cmd == SPI_TX_RX && xfer->tx_buf) { ++ const u8 *tx = xfer->tx_buf; ++ ++ /* ++ * The configured read opcode is followed by an address ++ * then dummy clocks before the device drives the data ++ * lanes. Any other first byte means the host is issuing ++ * a write command, so downgrade to TX-only. ++ */ ++ if (tx[0] == mas->qspi.read_opcode) { ++ peripheral.dummy_clk_cnt = mas->qspi.dummy_clk_cnt; ++ peripheral.tx_cmd_len = mas->qspi.tx_cmd_len; ++ } else { ++ peripheral.cmd = SPI_TX; ++ peripheral.dummy_clk_cnt = 0; ++ peripheral.rx_len = 0; ++ } ++ } else if (peripheral.cmd == SPI_RX) { ++ peripheral.dummy_clk_cnt = mas->qspi.dummy_clk_cnt; ++ } ++ } ++ + if (peripheral.cmd & SPI_RX) { + dmaengine_slave_config(mas->rx, &config); + rx_desc = dmaengine_prep_slave_sg(mas->rx, xfer->rx_sg.sgl, xfer->rx_sg.nents, +@@ -467,8 +624,18 @@ + return -EIO; + } + +- tx_desc->callback_result = spi_gsi_callback_result; +- tx_desc->callback_param = spi; ++ /* ++ * In QSPI mode the Go TRE on the TX channel has no DMA TRE for SPI_RX ++ * (and for TX_RX completes TX-side early), so the real completion ++ * event is the IEOT on the RX channel. Attach the callback there. ++ */ ++ if (spi_geni_is_qspi(mas) && (peripheral.cmd & SPI_RX)) { ++ rx_desc->callback_result = spi_gsi_callback_result; ++ rx_desc->callback_param = spi; ++ } else { ++ tx_desc->callback_result = spi_gsi_callback_result; ++ tx_desc->callback_param = spi; ++ } + + if (peripheral.cmd & SPI_RX) + dmaengine_submit(rx_desc); +@@ -534,7 +701,13 @@ + return ret; + + case GENI_GPI_DMA: +- /* nothing to do for GPI DMA */ ++ /* ++ * In QSPI mode, runtime_resume's call to geni_se_resources_on() ++ * clobbers the GPI-specific IRQ/DMA register layout. Restore ++ * it before every message. ++ */ ++ if (spi_geni_is_qspi(mas)) ++ prep_se_for_gpi_dma(mas); + return 0; + } + +@@ -609,6 +782,18 @@ + goto out_pm; + } + spi_slv_setup(mas); ++ } else if (spi_geni_is_qspi(mas)) { ++ /* ++ * QSPI SEs report protocol 9 in hardware. The GENI_SE_SPI ++ * firmware loader cannot be used here (no firmware for proto 9 ++ * is shipped), so reject anything else. ++ */ ++ if (proto != QSPI_SE_PROTO) { ++ dev_err(mas->dev, "Expected QSPI proto %d, got %d\n", ++ QSPI_SE_PROTO, proto); ++ goto out_pm; ++ } ++ qspi_setup_io_mux(mas); + } else if (proto == GENI_SE_INVALID_PROTO) { + ret = geni_load_se_firmware(se, GENI_SE_SPI); + if (ret) { +@@ -640,9 +825,28 @@ + else + mas->oversampling = 1; + ++ /* ++ * QSPI SEs cannot use FIFO mode (the FIFO path would try to drive ++ * a single-lane word format on a 4-lane QSPI bus), so force GPI DMA ++ * regardless of what GENI_IF_DISABLE_RO reports. ++ */ + fifo_disable = readl(se->base + GENI_IF_DISABLE_RO) & FIFO_IF_DISABLE; ++ if (spi_geni_is_qspi(mas)) ++ fifo_disable = 1; ++ + switch (fifo_disable) { + case 1: ++ /* ++ * For QSPI, bring the SE up in SE_DMA first (which arms the ++ * DMA-related registers) and let it settle before switching ++ * to GPI_DMA and grabbing the GPII channels. Without this ++ * intermediate step the first GPI command after probe can ++ * hang on the CH STOP completion. ++ */ ++ if (spi_geni_is_qspi(mas)) { ++ geni_se_select_mode(se, GENI_SE_DMA); ++ msleep(10); ++ } + ret = spi_geni_grab_gpi_chan(mas); + if (!ret) { /* success case */ + mas->cur_xfer_mode = GENI_GPI_DMA; +@@ -653,6 +857,16 @@ + goto out_pm; + } + /* ++ * For QSPI there is no usable FIFO fallback: FIFO mode cannot ++ * drive a 4-lane QSPI bus. Fail the probe instead of silently ++ * producing garbage. ++ */ ++ if (spi_geni_is_qspi(mas)) { ++ dev_err(mas->dev, "Failed to grab GPI DMA channels for QSPI: %d\n", ++ ret); ++ goto out_pm; ++ } ++ /* + * in case of failure to get gpi dma channel, we can still do the + * FIFO mode, so fallthrough + */ +@@ -1052,11 +1266,24 @@ + mas = spi_controller_get_devdata(spi); + mas->irq = irq; + mas->dev = dev; ++ mas->data = device_get_match_data(dev); + mas->se.dev = dev; + mas->se.wrapper = dev_get_drvdata(dev->parent); + mas->se.base = base; + mas->se.clk = clk; + ++ if (spi_geni_is_qspi(mas)) { ++ mas->qspi.read_opcode = QSPI_DEFAULT_READ_OPCODE; ++ mas->qspi.dummy_clk_cnt = QSPI_DEFAULT_DUMMY_CLK_CNT; ++ mas->qspi.tx_cmd_len = QSPI_DEFAULT_TX_CMD_LEN; ++ device_property_read_u32(dev, "qcom,qspi-read-opcode", ++ &mas->qspi.read_opcode); ++ device_property_read_u32(dev, "qcom,qspi-read-dummy-clocks", ++ &mas->qspi.dummy_clk_cnt); ++ device_property_read_u32(dev, "qcom,qspi-read-cmd-bytes", ++ &mas->qspi.tx_cmd_len); ++ } ++ + ret = devm_pm_opp_set_clkname(&pdev->dev, "se"); + if (ret) + return ret; +@@ -1069,9 +1296,12 @@ + + spi->bus_num = -1; + spi->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_CS_HIGH; ++ if (spi_geni_is_qspi(mas)) ++ spi->mode_bits |= SPI_TX_QUAD | SPI_RX_QUAD; + spi->bits_per_word_mask = SPI_BPW_RANGE_MASK(4, 32); + spi->num_chipselect = 4; +- spi->max_speed_hz = 50000000; ++ spi->max_speed_hz = spi_geni_is_qspi(mas) ? QSPI_DEFAULT_MAX_SPEED_HZ ++ : 50000000; + spi->max_dma_len = 0xffff0; /* 24 bits for tx/rx dma length */ + spi->prepare_message = spi_geni_prepare_message; + spi->transfer_one = spi_geni_transfer_one; +@@ -1197,8 +1427,13 @@ + SET_SYSTEM_SLEEP_PM_OPS(spi_geni_suspend, spi_geni_resume) + }; + ++static const struct spi_geni_data spi_geni_qspi_data = { ++ .qspi_mode = true, ++}; ++ + static const struct of_device_id spi_geni_dt_match[] = { + { .compatible = "qcom,geni-spi" }, ++ { .compatible = "qcom,geni-spi-qspi", .data = &spi_geni_qspi_data }, + {} + }; + MODULE_DEVICE_TABLE(of, spi_geni_dt_match); diff --git a/kernel/modules/spi-hid/Kconfig b/kernel/modules/spi-hid/Kconfig new file mode 100644 index 0000000..acc4e2f --- /dev/null +++ b/kernel/modules/spi-hid/Kconfig @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020 Microsoft Corporation +# +# 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. +# + +config SPI_HID + tristate "HID over SPI transport layer" + default n + help + Say Y here if you use a keyboard, a touchpad, a touchscreen, or any + other HID based devices which is connected to your computer via SPI. + + If unsure, say N. + + This support is also available as a module. If so, the module + will be called spi-hid. diff --git a/kernel/modules/spi-hid/Makefile b/kernel/modules/spi-hid/Makefile new file mode 100644 index 0000000..2d97af0 --- /dev/null +++ b/kernel/modules/spi-hid/Makefile @@ -0,0 +1,16 @@ +ifneq ($(KERNELRELEASE),) +obj-m += spi-hid.o +spi-hid-objs := spi-hid-core.o +else +KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build +PWD := $(shell pwd) + +all: + $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules + +clean: + $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean + +install: + $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install +endif diff --git a/kernel/modules/spi-hid/hid/hid-ids.h b/kernel/modules/spi-hid/hid/hid-ids.h new file mode 100644 index 0000000..3c6777e --- /dev/null +++ b/kernel/modules/spi-hid/hid/hid-ids.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Stub hid-ids.h for out-of-tree spi-hid build + * The actual hid-ids.h is not needed by spi-hid driver + */ +#ifndef HID_IDS_H_FILE +#define HID_IDS_H_FILE + +/* Empty stub - spi-hid doesn't use any HID vendor/device IDs */ + +#endif diff --git a/kernel/modules/spi-hid/spi-hid-core.c b/kernel/modules/spi-hid/spi-hid-core.c new file mode 100644 index 0000000..218b040 --- /dev/null +++ b/kernel/modules/spi-hid/spi-hid-core.c @@ -0,0 +1,1079 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HID over SPI (HIDSPI v3) transport driver for QSPI touchpads. + * + * Based on Microsoft's spi-hid v2 driver. + * Copyright (c) 2020 Microsoft Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "spi-hid-core.h" + +static struct hid_ll_driver spi_hid_ll_driver; +static int spi_hid_process_input_report(struct spi_hid *shid, + const u8 *body, int body_len); + +/* QSPI transfer helpers */ + +static void qspi_fill_cmd(u8 *buf, u8 opcode, u32 addr) +{ + buf[0] = opcode; + buf[1] = (addr >> 16) & 0xFF; + buf[2] = (addr >> 8) & 0xFF; + buf[3] = addr & 0xFF; +} + +static int qspi_read_sync(struct spi_hid *shid, u32 addr, void *buf, int len) +{ + struct spi_transfer xfer = {}; + u8 *tx, *rx; + int ret; + + tx = kzalloc(len, GFP_KERNEL); + rx = kzalloc(len, GFP_KERNEL); + if (!tx || !rx) { + kfree(tx); + kfree(rx); + return -ENOMEM; + } + + qspi_fill_cmd(tx, SPI_HID_QSPI_READ_OPCODE, addr); + + xfer.tx_buf = tx; + xfer.rx_buf = rx; + xfer.len = len; + xfer.tx_nbits = 4; + xfer.rx_nbits = 4; + + ret = spi_sync_transfer(shid->spi, &xfer, 1); + if (ret == 0) + memcpy(buf, rx, len); + + kfree(tx); + kfree(rx); + return ret; +} + +static int qspi_write_sync(struct spi_hid *shid, u32 addr, + const void *data, int len) +{ + struct device *dev = &shid->spi->dev; + struct spi_transfer xfer = {}; + int total = SPI_HID_QSPI_CMD_LEN + len; + u8 *tx; + int ret; + + tx = kzalloc(total, GFP_KERNEL); + if (!tx) + return -ENOMEM; + + qspi_fill_cmd(tx, SPI_HID_QSPI_WRITE_OPCODE, addr); + if (data && len > 0) + memcpy(tx + SPI_HID_QSPI_CMD_LEN, data, len); + + xfer.tx_buf = tx; + xfer.rx_buf = NULL; + xfer.len = total; + xfer.tx_nbits = 4; + xfer.rx_nbits = 4; + + ret = spi_sync_transfer(shid->spi, &xfer, 1); + + kfree(tx); + return ret; +} + +static void spi_hid_parse_input_header(const u8 *buf, + struct spi_hid_input_header *hdr) +{ + u32 raw = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); + + hdr->sync_const = (raw >> 24) & 0xFF; + hdr->version = raw & 0x0F; + hdr->body_len = ((raw >> 8) & 0x3FFF) * 4; + hdr->last_frag = (raw >> 22) & 1; +} + +static void spi_hid_parse_body_header(const u8 *buf, + struct spi_hid_body_header *bhdr) +{ + bhdr->report_type = buf[0]; + bhdr->content_length = buf[1] | (buf[2] << 8); + bhdr->content_id = buf[3]; +} + +static int spi_hid_validate_header(struct spi_hid *shid, + struct spi_hid_input_header *hdr) +{ + struct device *dev = &shid->spi->dev; + + if (hdr->sync_const != SPI_HID_INPUT_HEADER_SYNC_BYTE) { + dev_err(dev, "Bad sync: 0x%02x\n", hdr->sync_const); + return -EINVAL; + } + + if (hdr->version != SPI_HID_INPUT_HEADER_VERSION) { + dev_err(dev, "Bad version: %d\n", hdr->version); + return -EINVAL; + } + + if (hdr->body_len == 0) + return 0; + + if (shid->desc.max_input_length != 0 && + hdr->body_len > shid->desc.max_input_length) { + dev_err(dev, "Body %u > max %u\n", + hdr->body_len, shid->desc.max_input_length); + return -EMSGSIZE; + } + + return 0; +} + +static void spi_hid_parse_dev_desc(struct spi_hid_device_desc_raw *raw, + struct spi_hid_device_descriptor *desc) +{ + desc->hid_version = le16_to_cpu(raw->bcdVersion); + desc->report_descriptor_length = le16_to_cpu(raw->wReportDescLength); + desc->max_input_length = le16_to_cpu(raw->wMaxInputLength); + desc->max_output_length = le16_to_cpu(raw->wMaxOutputLength); + desc->max_fragment_length = le16_to_cpu(raw->wMaxFragmentLength); + desc->vendor_id = le16_to_cpu(raw->wVendorID); + desc->product_id = le16_to_cpu(raw->wProductID); + desc->version_id = le16_to_cpu(raw->wVersionID); + desc->flags = le16_to_cpu(raw->wFlags); +} + +static int spi_hid_send_output(struct spi_hid *shid, + u8 report_type, u8 content_id, + const u8 *content, int content_len) +{ + u8 body[SPI_HID_BODY_HEADER_LEN + 512]; + int body_len = SPI_HID_BODY_HEADER_LEN + content_len; + + if (body_len > sizeof(body)) { + dev_err(&shid->spi->dev, "Output too large: %d\n", body_len); + return -E2BIG; + } + + body[0] = report_type; + body[1] = content_len & 0xFF; + body[2] = (content_len >> 8) & 0xFF; + body[3] = content_id; + if (content && content_len > 0) + memcpy(body + SPI_HID_BODY_HEADER_LEN, content, content_len); + + return qspi_write_sync(shid, SPI_HID_OUTPUT_ADDR, body, body_len); +} + +/* Caller must hold shid->lock. */ +static int spi_hid_sync_request_ms(struct spi_hid *shid, + u8 report_type, u8 content_id, + const u8 *content, int content_len, + unsigned int timeout_ms) +{ + struct device *dev = &shid->spi->dev; + unsigned long timeout; + int ret; + + reinit_completion(&shid->output_done); + + ret = spi_hid_send_output(shid, report_type, content_id, + content, content_len); + if (ret) { + dev_err(dev, "Failed to send output type %d: %d\n", + report_type, ret); + return ret; + } + + timeout = wait_for_completion_timeout(&shid->output_done, + msecs_to_jiffies(timeout_ms)); + if (timeout == 0) { + dev_err(dev, "Response timeout for type %d (%ums)\n", + report_type, timeout_ms); + return -ETIMEDOUT; + } + + return 0; +} + +static int spi_hid_sync_request(struct spi_hid *shid, + u8 report_type, u8 content_id, + const u8 *content, int content_len) +{ + return spi_hid_sync_request_ms(shid, report_type, content_id, + content, content_len, + SPI_HID_RESPONSE_TIMEOUT_MS); +} + +static int spi_hid_power_down(struct spi_hid *shid) +{ + int ret; + + if (!shid->powered) + return 0; + + if (shid->pinctrl_sleep) + pinctrl_select_state(shid->pinctrl, shid->pinctrl_sleep); + + if (shid->supply) { + ret = regulator_disable(shid->supply); + if (ret) { + dev_err(&shid->spi->dev, "regulator disable failed\n"); + return ret; + } + } + + shid->powered = false; + return 0; +} + +static int spi_hid_power_up(struct spi_hid *shid) +{ + int ret; + + if (shid->powered) + return 0; + + shid->powered = true; + + if (shid->supply) { + ret = regulator_enable(shid->supply); + if (ret) { + shid->powered = false; + return ret; + } + } + + usleep_range(5000, 6000); + return 0; +} + +static struct hid_device *spi_hid_disconnect_hid(struct spi_hid *shid) +{ + struct hid_device *hid = shid->hid; + + shid->hid = NULL; + return hid; +} + +static void spi_hid_stop_hid(struct spi_hid *shid) +{ + struct hid_device *hid; + + hid = spi_hid_disconnect_hid(shid); + if (hid) { + cancel_work_sync(&shid->create_device_work); + cancel_work_sync(&shid->refresh_device_work); + hid_destroy_device(hid); + } +} + +static int spi_hid_error_handler(struct spi_hid *shid) +{ + struct device *dev = &shid->spi->dev; + int ret; + + if (shid->power_state == SPI_HID_POWER_MODE_OFF) + return 0; + + dev_err(dev, "Error handler (attempt %d)\n", shid->attempts); + + if (shid->attempts++ >= SPI_HID_MAX_RESET_ATTEMPTS) { + dev_err(dev, "Unresponsive device, aborting\n"); + spi_hid_stop_hid(shid); + spi_hid_power_down(shid); + return -ESHUTDOWN; + } + + shid->ready = false; + + ret = pinctrl_select_state(shid->pinctrl, shid->pinctrl_reset); + if (ret) { + dev_err(dev, "Reset assert failed\n"); + return ret; + } + shid->power_state = SPI_HID_POWER_MODE_OFF; + + msleep(SPI_HID_RESET_ASSERT_MS); + + shid->power_state = SPI_HID_POWER_MODE_ACTIVE; + ret = pinctrl_select_state(shid->pinctrl, shid->pinctrl_active); + if (ret) { + dev_err(dev, "Reset deassert failed\n"); + return ret; + } + + msleep(SPI_HID_POST_DIR_DELAY_MS); + + return 0; +} + +static int spi_hid_input_report_handler(struct spi_hid *shid, + const u8 *body, int body_len) +{ + struct device *dev = &shid->spi->dev; + struct spi_hid_body_header bhdr; + int ret; + + if (!shid->ready || !shid->hid) + return 0; + + spi_hid_parse_body_header(body, &bhdr); + + ret = hid_input_report(shid->hid, HID_INPUT_REPORT, + (u8 *)(body + 3), + bhdr.content_length + 1, 1); + + if (ret == -ENODEV || ret == -EBUSY) + return 0; + + return ret; +} + +static int spi_hid_process_input_report(struct spi_hid *shid, + const u8 *body, int body_len) +{ + struct device *dev = &shid->spi->dev; + struct spi_hid_body_header bhdr; + struct spi_hid_device_desc_raw *raw; + + if (body_len < SPI_HID_BODY_HEADER_LEN) { + dev_err(dev, "Body too short: %d\n", body_len); + return -EINVAL; + } + + spi_hid_parse_body_header(body, &bhdr); + + switch (bhdr.report_type) { + case SPI_HID_REPORT_TYPE_DATA: + return spi_hid_input_report_handler(shid, body, body_len); + + case SPI_HID_REPORT_TYPE_RESET_RESP: + shid->resp_type = SPI_HID_REPORT_TYPE_RESET_RESP; + shid->resp_len = 0; + if (!completion_done(&shid->output_done)) { + complete(&shid->output_done); + } else { + if (!shid->ready) + schedule_work(&shid->reset_work); + else + schedule_work(&shid->refresh_device_work); + } + return 0; + + case SPI_HID_REPORT_TYPE_DEVICE_DESC: + shid->attempts = 0; + if (body_len >= SPI_HID_BODY_HEADER_LEN + + sizeof(struct spi_hid_device_desc_raw)) { + raw = (struct spi_hid_device_desc_raw *) + (body + SPI_HID_BODY_HEADER_LEN); + spi_hid_parse_dev_desc(raw, &shid->desc); + } + if (shid->resp_buf && body_len <= SPI_HID_MAX_INPUT_LEN) { + memcpy(shid->resp_buf, body, body_len); + shid->resp_len = body_len; + shid->resp_type = bhdr.report_type; + } + if (!completion_done(&shid->output_done)) + complete(&shid->output_done); + else if (!shid->hid) + schedule_work(&shid->create_device_work); + else + schedule_work(&shid->refresh_device_work); + return 0; + + case SPI_HID_REPORT_TYPE_COMMAND_RESP: + case SPI_HID_REPORT_TYPE_GET_FEATURE_RESP: + case SPI_HID_REPORT_TYPE_REPORT_DESC: + case SPI_HID_REPORT_TYPE_SET_FEATURE_RESP: + case SPI_HID_REPORT_TYPE_OUTPUT_REPORT_RESP: + case SPI_HID_REPORT_TYPE_GET_INPUT_RESP: + if (shid->resp_buf && body_len <= SPI_HID_MAX_INPUT_LEN) { + memcpy(shid->resp_buf, body, body_len); + shid->resp_len = body_len; + shid->resp_type = bhdr.report_type; + } + if (!completion_done(&shid->output_done)) + complete(&shid->output_done); + return 0; + + default: + dev_err(dev, "Unknown report type: 0x%x (body_len=%d raw: %*ph)\n", + bhdr.report_type, body_len, + min(body_len, 16), body); + return -EINVAL; + } +} + +static irqreturn_t spi_hid_irq_thread(int irq, void *_shid) +{ + struct spi_hid *shid = _shid; + struct device *dev = &shid->spi->dev; + struct spi_hid_input_header hdr; + struct spi_transfer xfer = {}; + int ret; + + if (!shid->powered) + return IRQ_HANDLED; + + xfer.tx_buf = shid->irq_hdr_tx; + xfer.rx_buf = shid->irq_hdr_rx; + xfer.len = SPI_HID_INPUT_HEADER_LEN; + xfer.tx_nbits = 4; + xfer.rx_nbits = 4; + + ret = spi_sync_transfer(shid->spi, &xfer, 1); + if (ret) { + shid->bus_error_count++; + shid->bus_last_error = ret; + if (shid->bus_error_count <= 5) + dev_err(dev, "Header read failed: %d\n", ret); + if (shid->bus_error_count == 100) { + dev_err(dev, "Too many read errors, disabling IRQ\n"); + disable_irq_nosync(shid->spi->irq); + } + return IRQ_HANDLED; + } + + spi_hid_parse_input_header(shid->irq_hdr_rx, &hdr); + + ret = spi_hid_validate_header(shid, &hdr); + if (ret) { + shid->bus_error_count++; + shid->bus_last_error = ret; + if (shid->bus_error_count <= 5 || + shid->bus_error_count % 1000 == 0) + dev_err(dev, "Invalid header (%u errors): %*ph\n", + shid->bus_error_count, + SPI_HID_INPUT_HEADER_LEN, shid->irq_hdr_rx); + if (shid->bus_error_count == 100) { + dev_err(dev, "Too many errors, disabling IRQ\n"); + disable_irq_nosync(shid->spi->irq); + } + return IRQ_HANDLED; + } + shid->bus_error_count = 0; + + if (hdr.body_len == 0) + return IRQ_HANDLED; + + if (hdr.body_len > SPI_HID_MAX_INPUT_LEN) { + dev_err(dev, "Body too large: %u\n", hdr.body_len); + return IRQ_HANDLED; + } + + memset(&xfer, 0, sizeof(xfer)); + xfer.tx_buf = shid->irq_bdy_tx; + xfer.rx_buf = shid->irq_bdy_rx; + xfer.len = hdr.body_len; + xfer.tx_nbits = 4; + xfer.rx_nbits = 4; + + ret = spi_sync_transfer(shid->spi, &xfer, 1); + if (ret) { + dev_err(dev, "Body read failed: %d\n", ret); + shid->bus_error_count++; + shid->bus_last_error = ret; + return IRQ_HANDLED; + } + + spi_hid_process_input_report(shid, shid->irq_bdy_rx, hdr.body_len); + + return IRQ_HANDLED; +} + +static int spi_hid_create_device(struct spi_hid *shid); + +static void spi_hid_reset_work(struct work_struct *work) +{ + struct spi_hid *shid = container_of(work, struct spi_hid, reset_work); + struct device *dev = &shid->spi->dev; + int ret, attempt; + + if (shid->ready) + shid->dir_count++; + + flush_work(&shid->create_device_work); + + if (shid->power_state == SPI_HID_POWER_MODE_OFF) + return; + + flush_work(&shid->refresh_device_work); + + mutex_lock(&shid->lock); + + for (attempt = 0; attempt < SPI_HID_MAX_RESET_ATTEMPTS; attempt++) { + int dd_try, rd_try, rd_len; + + if (attempt > 0) { + pinctrl_select_state(shid->pinctrl, shid->pinctrl_reset); + msleep(SPI_HID_RESET_ASSERT_MS); + pinctrl_select_state(shid->pinctrl, shid->pinctrl_active); + msleep(2000); + } + + for (dd_try = 0; dd_try < SPI_HID_MAX_INIT_RETRIES; dd_try++) { + ret = spi_hid_sync_request(shid, + SPI_HID_OUT_DEVICE_DESC, + 0, NULL, 0); + if (ret) { + dev_err(dev, "GET_DD failed: %d\n", ret); + break; + } + if (shid->resp_type == SPI_HID_REPORT_TYPE_DEVICE_DESC) + break; + if (shid->resp_type == SPI_HID_REPORT_TYPE_RESET_RESP) { + msleep(SPI_HID_POST_DIR_DELAY_MS); + continue; + } + dev_err(dev, "GET_DD: unexpected type %d\n", + shid->resp_type); + msleep(SPI_HID_POST_DIR_DELAY_MS); + } + if (dd_try >= SPI_HID_MAX_INIT_RETRIES || ret) { + dev_err(dev, "GET_DD failed, restarting\n"); + continue; + } + + for (rd_try = 0; rd_try < SPI_HID_MAX_INIT_RETRIES; rd_try++) { + ret = spi_hid_sync_request(shid, + SPI_HID_OUT_REPORT_DESC, + 0, NULL, 0); + if (ret) { + dev_err(dev, "GET_RD failed: %d\n", ret); + break; + } + if (shid->resp_type == SPI_HID_REPORT_TYPE_REPORT_DESC) + break; + if (shid->resp_type == SPI_HID_REPORT_TYPE_RESET_RESP) { + msleep(SPI_HID_POST_DIR_DELAY_MS); + continue; + } + dev_err(dev, "GET_RD: unexpected type %d\n", + shid->resp_type); + msleep(SPI_HID_POST_DIR_DELAY_MS); + } + if (rd_try >= SPI_HID_MAX_INIT_RETRIES || ret) { + dev_err(dev, "GET_RD failed, restarting\n"); + continue; + } + + rd_len = shid->resp_len - SPI_HID_BODY_HEADER_LEN; + if (rd_len <= 0 || rd_len > 2048) { + dev_err(dev, "GET_RD: bad length %d\n", rd_len); + continue; + } + memcpy(shid->rd_buf, + shid->resp_buf + SPI_HID_BODY_HEADER_LEN, rd_len); + shid->rd_len = rd_len; + + goto success; + } + + dev_err(dev, "Init failed after %d reset cycles\n", attempt); + mutex_unlock(&shid->lock); + spi_hid_error_handler(shid); + return; + +success: + mutex_unlock(&shid->lock); + + if (!shid->hid) + spi_hid_create_device(shid); +} + +static int spi_hid_create_device(struct spi_hid *shid) +{ + struct hid_device *hid; + struct device *dev = &shid->spi->dev; + int ret; + + hid = hid_allocate_device(); + if (IS_ERR(hid)) { + dev_err(dev, "Failed to allocate hid device: %ld\n", + PTR_ERR(hid)); + return PTR_ERR(hid); + } + + hid->driver_data = shid->spi; + hid->ll_driver = &spi_hid_ll_driver; + hid->dev.parent = &shid->spi->dev; + hid->bus = BUS_SPI; + hid->version = shid->desc.hid_version; + hid->vendor = shid->desc.vendor_id; + hid->product = shid->desc.product_id; + + snprintf(hid->name, sizeof(hid->name), "spi %04hX:%04hX", + hid->vendor, hid->product); + strscpy(hid->phys, dev_name(&shid->spi->dev), sizeof(hid->phys)); + + shid->hid = hid; + + ret = hid_add_device(hid); + if (ret) { + dev_err(dev, "Failed to add hid device: %d\n", ret); + hid = spi_hid_disconnect_hid(shid); + if (hid) + hid_destroy_device(hid); + return ret; + } + + return 0; +} + +static void spi_hid_create_device_work(struct work_struct *work) +{ + struct spi_hid *shid = + container_of(work, struct spi_hid, create_device_work); + struct device *dev = &shid->spi->dev; + int ret; + + if (shid->desc.hid_version != SPI_HID_SUPPORTED_VERSION) { + dev_err(dev, "Unsupported version 0x%04x (expected 0x%04x)\n", + shid->desc.hid_version, SPI_HID_SUPPORTED_VERSION); + spi_hid_error_handler(shid); + return; + } + + ret = spi_hid_create_device(shid); + if (ret) { + dev_err(dev, "Failed to create HID device: %d\n", ret); + return; + } + + shid->attempts = 0; +} + +static void spi_hid_refresh_device_work(struct work_struct *work) +{ + struct spi_hid *shid = + container_of(work, struct spi_hid, refresh_device_work); + + shid->ready = true; +} + +static int spi_hid_ll_start(struct hid_device *hid) +{ + struct spi_device *spi = hid->driver_data; + struct spi_hid *shid = spi_get_drvdata(spi); + + if (shid->desc.max_input_length < HID_MIN_BUFFER_SIZE) { + dev_err(&spi->dev, "max_input_length %d < HID_MIN_BUFFER_SIZE\n", + shid->desc.max_input_length); + return -EINVAL; + } + + return 0; +} + +static void spi_hid_ll_stop(struct hid_device *hid) +{ + hid->claimed = 0; +} + +static int spi_hid_ll_open(struct hid_device *hid) +{ + return 0; +} + +static void spi_hid_ll_close(struct hid_device *hid) +{ +} + +static int spi_hid_ll_power(struct hid_device *hid, int level) +{ + struct spi_device *spi = hid->driver_data; + struct spi_hid *shid = spi_get_drvdata(spi); + + if (!shid->hid) + return -ENODEV; + + return 0; +} + +static int spi_hid_ll_parse(struct hid_device *hid) +{ + struct spi_device *spi = hid->driver_data; + struct spi_hid *shid = spi_get_drvdata(spi); + struct device *dev = &spi->dev; + int ret; + + if (!shid->rd_buf || shid->rd_len <= 0) { + dev_err(dev, "No report descriptor available\n"); + return -ENODATA; + } + + ret = hid_parse_report(hid, shid->rd_buf, shid->rd_len); + if (ret) + dev_err(dev, "hid_parse_report failed: %d\n", ret); + else + shid->ready = true; + + return ret; +} + +static int spi_hid_ll_raw_request(struct hid_device *hid, + unsigned char reportnum, __u8 *buf, + size_t len, unsigned char rtype, int reqtype) +{ + struct spi_device *spi = hid->driver_data; + struct spi_hid *shid = spi_get_drvdata(spi); + struct device *dev = &spi->dev; + + int ret, copy_len; + + if (reqtype == HID_REQ_SET_REPORT && rtype == HID_FEATURE_REPORT) { + mutex_lock(&shid->lock); + ret = spi_hid_sync_request(shid, SPI_HID_OUT_SET_FEATURE, + buf[0], buf + 1, len - 1); + mutex_unlock(&shid->lock); + if (ret) + return ret; + if (shid->resp_type == SPI_HID_REPORT_TYPE_RESET_RESP) + return len; + if (shid->resp_type != SPI_HID_REPORT_TYPE_SET_FEATURE_RESP) { + dev_err(dev, "SET_FEATURE got resp type %d\n", + shid->resp_type); + return -EIO; + } + return len; + } + + if (reqtype == HID_REQ_GET_REPORT && rtype == HID_FEATURE_REPORT) { + mutex_lock(&shid->lock); + ret = spi_hid_sync_request(shid, SPI_HID_OUT_GET_FEATURE, + reportnum, NULL, 0); + mutex_unlock(&shid->lock); + if (ret) { + dev_err(dev, "GET_FEATURE 0x%02x failed: %d\n", + reportnum, ret); + return ret; + } + + if (shid->resp_type == SPI_HID_REPORT_TYPE_RESET_RESP) { + memset(buf, 0, len); + buf[0] = reportnum; + return len; + } + + if (shid->resp_type != SPI_HID_REPORT_TYPE_GET_FEATURE_RESP) { + dev_err(dev, "GET_FEATURE got resp type %d\n", + shid->resp_type); + return -EIO; + } + + copy_len = shid->resp_len - SPI_HID_BODY_HEADER_LEN; + if (copy_len <= 0) { + dev_err(dev, "GET_FEATURE 0x%02x: empty response\n", + reportnum); + return -EIO; + } + if (copy_len > (int)len - 1) + copy_len = (int)len - 1; + + buf[0] = reportnum; + memcpy(buf + 1, shid->resp_buf + SPI_HID_BODY_HEADER_LEN, + copy_len); + return 1 + copy_len; + } + + if (reqtype == HID_REQ_SET_REPORT) { + mutex_lock(&shid->lock); + ret = spi_hid_send_output(shid, SPI_HID_OUT_OUTPUT_REPORT, + buf[0], buf + 1, len - 1); + mutex_unlock(&shid->lock); + return ret ? ret : len; + } + + dev_err(dev, "Unsupported request: reqtype=%d rtype=%d\n", + reqtype, rtype); + return -EIO; +} + +static int spi_hid_ll_output_report(struct hid_device *hid, + __u8 *buf, size_t len) +{ + struct spi_device *spi = hid->driver_data; + struct spi_hid *shid = spi_get_drvdata(spi); + struct device *dev = &spi->dev; + int ret; + + mutex_lock(&shid->lock); + if (!shid->ready) { + dev_err(dev, "output_report called in unready state\n"); + ret = -ENODEV; + goto out; + } + + ret = spi_hid_send_output(shid, SPI_HID_OUT_OUTPUT_REPORT, + buf[0], &buf[1], len - 1); + if (ret == 0) + ret = len; + +out: + mutex_unlock(&shid->lock); + return ret; +} + +static struct hid_ll_driver spi_hid_ll_driver = { + .start = spi_hid_ll_start, + .stop = spi_hid_ll_stop, + .open = spi_hid_ll_open, + .close = spi_hid_ll_close, + .power = spi_hid_ll_power, + .parse = spi_hid_ll_parse, + .output_report = spi_hid_ll_output_report, + .raw_request = spi_hid_ll_raw_request, +}; + +static ssize_t ready_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct spi_hid *shid = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%s\n", shid->ready ? "ready" : "not ready"); +} +static DEVICE_ATTR_RO(ready); + +static ssize_t bus_error_count_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct spi_hid *shid = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d (%d)\n", + shid->bus_error_count, shid->bus_last_error); +} +static DEVICE_ATTR_RO(bus_error_count); + +static ssize_t device_initiated_reset_count_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct spi_hid *shid = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", shid->dir_count); +} +static DEVICE_ATTR_RO(device_initiated_reset_count); + +static const struct attribute *const spi_hid_attributes[] = { + &dev_attr_ready.attr, + &dev_attr_bus_error_count.attr, + &dev_attr_device_initiated_reset_count.attr, + NULL +}; + +static int spi_hid_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct spi_hid *shid; + unsigned long irqflags; + int ret; + + if (spi->irq <= 0) { + dev_err(dev, "Missing IRQ\n"); + return spi->irq ?: -EINVAL; + } + + shid = devm_kzalloc(dev, sizeof(*shid), GFP_KERNEL); + if (!shid) + return -ENOMEM; + + shid->spi = spi; + shid->power_state = SPI_HID_POWER_MODE_ACTIVE; + spi_set_drvdata(spi, shid); + + spi->mode = SPI_MODE_0 | SPI_TX_QUAD | SPI_RX_QUAD; + spi->max_speed_hz = 20000000; + spi->bits_per_word = 8; + ret = spi_setup(spi); + if (ret) { + dev_err(dev, "SPI setup failed: %d\n", ret); + return ret; + } + + shid->resp_buf = kzalloc(SPI_HID_MAX_INPUT_LEN, GFP_KERNEL); + shid->rd_buf = kzalloc(2048, GFP_KERNEL); + shid->irq_hdr_tx = kzalloc(SPI_HID_INPUT_HEADER_LEN, GFP_KERNEL); + shid->irq_hdr_rx = kzalloc(SPI_HID_INPUT_HEADER_LEN, GFP_KERNEL); + shid->irq_bdy_tx = kzalloc(SPI_HID_MAX_INPUT_LEN, GFP_KERNEL); + shid->irq_bdy_rx = kzalloc(SPI_HID_MAX_INPUT_LEN, GFP_KERNEL); + if (!shid->resp_buf || !shid->rd_buf || + !shid->irq_hdr_tx || !shid->irq_hdr_rx || + !shid->irq_bdy_tx || !shid->irq_bdy_rx) { + ret = -ENOMEM; + goto err_free; + } + + qspi_fill_cmd(shid->irq_hdr_tx, SPI_HID_QSPI_READ_OPCODE, + SPI_HID_INPUT_HDR_ADDR); + qspi_fill_cmd(shid->irq_bdy_tx, SPI_HID_QSPI_READ_OPCODE, + SPI_HID_INPUT_BDY_ADDR); + + shid->desc.max_input_length = SPI_HID_MAX_INPUT_LEN; + + ret = sysfs_create_files(&dev->kobj, spi_hid_attributes); + if (ret) { + dev_err(dev, "sysfs_create_files failed\n"); + goto err_free; + } + + mutex_init(&shid->lock); + init_completion(&shid->output_done); + complete(&shid->output_done); + + INIT_WORK(&shid->reset_work, spi_hid_reset_work); + INIT_WORK(&shid->create_device_work, spi_hid_create_device_work); + INIT_WORK(&shid->refresh_device_work, spi_hid_refresh_device_work); + + shid->supply = devm_regulator_get_optional(dev, "vdd"); + if (IS_ERR(shid->supply)) { + if (PTR_ERR(shid->supply) == -EPROBE_DEFER) { + ret = -EPROBE_DEFER; + goto err_sysfs; + } + shid->supply = NULL; + } + + shid->pinctrl = devm_pinctrl_get(dev); + if (IS_ERR(shid->pinctrl)) { + dev_err(dev, "pinctrl_get failed: %ld\n", + PTR_ERR(shid->pinctrl)); + ret = PTR_ERR(shid->pinctrl); + goto err_sysfs; + } + + shid->pinctrl_reset = pinctrl_lookup_state(shid->pinctrl, "reset"); + if (IS_ERR(shid->pinctrl_reset)) { + dev_err(dev, "pinctrl 'reset' not found: %ld\n", + PTR_ERR(shid->pinctrl_reset)); + ret = PTR_ERR(shid->pinctrl_reset); + goto err_sysfs; + } + + shid->pinctrl_active = pinctrl_lookup_state(shid->pinctrl, "active"); + if (IS_ERR(shid->pinctrl_active)) { + dev_err(dev, "pinctrl 'active' not found: %ld\n", + PTR_ERR(shid->pinctrl_active)); + ret = PTR_ERR(shid->pinctrl_active); + goto err_sysfs; + } + + shid->pinctrl_sleep = pinctrl_lookup_state(shid->pinctrl, "sleep"); + if (IS_ERR(shid->pinctrl_sleep)) + shid->pinctrl_sleep = shid->pinctrl_reset; + + irqflags = irq_get_trigger_type(spi->irq) | IRQF_ONESHOT; + ret = request_threaded_irq(spi->irq, NULL, spi_hid_irq_thread, + irqflags, dev_name(&spi->dev), shid); + if (ret) { + dev_err(dev, "request_threaded_irq failed: %d\n", ret); + goto err_sysfs; + } + disable_irq(spi->irq); + shid->irq_enabled = false; + + pm_runtime_enable(dev->parent); + ret = pm_runtime_get_sync(dev->parent); + if (ret < 0) { + dev_warn(dev, "SPI ctrl PM get failed: %d\n", ret); + pm_runtime_put_noidle(dev->parent); + } + + /* + * Bring-up: hold the device in reset (power off, reset asserted) + * for a few ms, then deassert reset and enable the regulator. The + * 2 s settle gives the touchpad firmware time to boot before we + * arm the IRQ and start talking to it. + */ + pinctrl_select_state(shid->pinctrl, shid->pinctrl_reset); + msleep(SPI_HID_RESET_ASSERT_MS); + pinctrl_select_state(shid->pinctrl, shid->pinctrl_active); + spi_hid_power_up(shid); + msleep(2000); + + pm_runtime_put(dev->parent); + + enable_irq(spi->irq); + shid->irq_enabled = true; + + return 0; + +err_sysfs: + sysfs_remove_files(&dev->kobj, spi_hid_attributes); +err_free: + kfree(shid->resp_buf); + kfree(shid->rd_buf); + kfree(shid->irq_hdr_tx); + kfree(shid->irq_hdr_rx); + kfree(shid->irq_bdy_tx); + kfree(shid->irq_bdy_rx); + return ret; +} + +static void spi_hid_remove(struct spi_device *spi) +{ + struct spi_hid *shid = spi_get_drvdata(spi); + struct device *dev = &spi->dev; + + spi_hid_power_down(shid); + free_irq(spi->irq, shid); + shid->irq_enabled = false; + sysfs_remove_files(&dev->kobj, spi_hid_attributes); + spi_hid_stop_hid(shid); + + kfree(shid->resp_buf); + kfree(shid->rd_buf); + kfree(shid->irq_hdr_tx); + kfree(shid->irq_hdr_rx); + kfree(shid->irq_bdy_tx); + kfree(shid->irq_bdy_rx); +} + +static const struct of_device_id spi_hid_of_match[] = { + { .compatible = "hid-over-spi" }, + {}, +}; +MODULE_DEVICE_TABLE(of, spi_hid_of_match); + +static const struct spi_device_id spi_hid_id_table[] = { + { "hid-over-spi", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(spi, spi_hid_id_table); + +static struct spi_driver spi_hid_driver = { + .driver = { + .name = "spi_hid", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(spi_hid_of_match), + }, + .probe = spi_hid_probe, + .remove = spi_hid_remove, + .id_table = spi_hid_id_table, +}; + +module_spi_driver(spi_hid_driver); + +MODULE_DESCRIPTION("HID over SPI (HIDSPI v3) QSPI transport driver"); +MODULE_LICENSE("GPL"); diff --git a/kernel/modules/spi-hid/spi-hid-core.h b/kernel/modules/spi-hid/spi-hid-core.h new file mode 100644 index 0000000..0c3cf3e --- /dev/null +++ b/kernel/modules/spi-hid/spi-hid-core.h @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * HID over SPI (HIDSPI v3) transport driver for QSPI touchpads. + * + * Based on Microsoft's spi-hid v2 driver. + * Copyright (c) 2020 Microsoft Corporation + */ + +#ifndef SPI_HID_CORE_H +#define SPI_HID_CORE_H + +#include +#include +#include +#include + +#define SPI_HID_INPUT_HEADER_SYNC_BYTE 0x5A +#define SPI_HID_INPUT_HEADER_VERSION 0x03 + +#define SPI_HID_QSPI_READ_OPCODE 0xEB +#define SPI_HID_QSPI_WRITE_OPCODE 0xE2 +#define SPI_HID_QSPI_CMD_LEN 4 + +#define SPI_HID_INPUT_HDR_ADDR 0x1000 +#define SPI_HID_INPUT_BDY_ADDR 0x1004 +#define SPI_HID_OUTPUT_ADDR 0x2000 + +#define SPI_HID_SUPPORTED_VERSION 0x0300 + +#define SPI_HID_BODY_HEADER_LEN 4 + +/* Input report types (device -> host) */ +#define SPI_HID_REPORT_TYPE_DATA 0x01 +#define SPI_HID_REPORT_TYPE_RESET_RESP 0x03 +#define SPI_HID_REPORT_TYPE_COMMAND_RESP 0x04 +#define SPI_HID_REPORT_TYPE_GET_FEATURE_RESP 0x05 +#define SPI_HID_REPORT_TYPE_DEVICE_DESC 0x07 +#define SPI_HID_REPORT_TYPE_REPORT_DESC 0x08 +#define SPI_HID_REPORT_TYPE_SET_FEATURE_RESP 0x09 +#define SPI_HID_REPORT_TYPE_OUTPUT_REPORT_RESP 0x0A +#define SPI_HID_REPORT_TYPE_GET_INPUT_RESP 0x0B + +/* Output report types (host -> device) */ +#define SPI_HID_OUT_DEVICE_DESC 0x01 +#define SPI_HID_OUT_REPORT_DESC 0x02 +#define SPI_HID_OUT_SET_FEATURE 0x03 +#define SPI_HID_OUT_GET_FEATURE 0x04 +#define SPI_HID_OUT_OUTPUT_REPORT 0x05 +#define SPI_HID_OUT_GET_INPUT_REPORT 0x06 +#define SPI_HID_OUT_COMMAND_CONTENT 0x07 + +#define SPI_HID_POWER_MODE_ACTIVE 0x01 +#define SPI_HID_POWER_MODE_SLEEP 0x02 +#define SPI_HID_POWER_MODE_OFF 0x03 + +#define SPI_HID_RESET_ASSERT_MS 300 +#define SPI_HID_POST_DIR_DELAY_MS 25 +#define SPI_HID_RESPONSE_TIMEOUT_MS 2000 +#define SPI_HID_MAX_RESET_ATTEMPTS 3 +#define SPI_HID_MAX_INIT_RETRIES 10 + +#define SPI_HID_INPUT_HEADER_LEN 4 +#define SPI_HID_MAX_INPUT_LEN SZ_8K + +struct spi_hid_device_desc_raw { + __le16 wDeviceDescLength; + __le16 bcdVersion; + __le16 wReportDescLength; + __le16 wMaxInputLength; + __le16 wMaxOutputLength; + __le16 wMaxFragmentLength; + __le16 wVendorID; + __le16 wProductID; + __le16 wVersionID; + __le16 wFlags; + __u8 reserved[4]; +} __packed; + +/* Parsed device descriptor */ +struct spi_hid_device_descriptor { + u16 hid_version; + u16 report_descriptor_length; + u16 max_input_length; + u16 max_output_length; + u16 max_fragment_length; + u16 vendor_id; + u16 product_id; + u16 version_id; + u16 flags; +}; + +struct spi_hid_input_header { + u8 version; + u16 body_len; + u8 last_frag; + u8 sync_const; +}; + +struct spi_hid_body_header { + u8 report_type; + u16 content_length; + u8 content_id; +}; + +struct spi_hid { + struct spi_device *spi; + struct hid_device *hid; + + struct spi_hid_device_descriptor desc; + + u8 *resp_buf; + int resp_len; + u8 resp_type; + + u8 power_state; + u8 attempts; + + bool ready; + bool irq_enabled; + + struct regulator *supply; + struct pinctrl *pinctrl; + struct pinctrl_state *pinctrl_reset; + struct pinctrl_state *pinctrl_active; + struct pinctrl_state *pinctrl_sleep; + struct work_struct reset_work; + struct work_struct create_device_work; + struct work_struct refresh_device_work; + + struct mutex lock; + struct completion output_done; + + u32 bus_error_count; + int bus_last_error; + + u32 dir_count; + u32 powered; + + u8 *rd_buf; + int rd_len; + + u8 *irq_hdr_tx; + u8 *irq_hdr_rx; + u8 *irq_bdy_tx; + u8 *irq_bdy_rx; +}; + +#endif diff --git a/kernel/modules/spi-hid/spi-hid_trace.h b/kernel/modules/spi-hid/spi-hid_trace.h new file mode 100644 index 0000000..7c96bf7 --- /dev/null +++ b/kernel/modules/spi-hid/spi-hid_trace.h @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * spi-hid_trace.h + * + * Copyright (c) 2020 Microsoft Corporation + * + * 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. + */ + +#undef TRACE_SYSTEM +#define TRACE_SYSTEM spi_hid + +#if !defined(_SPI_HID_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) +#define _SPI_HID_TRACE_H + +#include +#include +#include "spi-hid-core.h" + +DECLARE_EVENT_CLASS(spi_hid_transfer, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret), + + TP_STRUCT__entry( + __field(int, bus_num) + __field(int, chip_select) + __field(int, len) + __field(int, ret) + __dynamic_array(u8, rx_buf, rx_len) + __dynamic_array(u8, tx_buf, tx_len) + ), + + TP_fast_assign( + __entry->bus_num = shid->spi->controller->bus_num; + __entry->chip_select = shid->spi->chip_select; + __entry->len = rx_len + tx_len; + __entry->ret = ret; + + memcpy(__get_dynamic_array(tx_buf), tx_buf, tx_len); + memcpy(__get_dynamic_array(rx_buf), rx_buf, rx_len); + ), + + TP_printk("spi%d.%d: len=%d tx=[%*phD] rx=[%*phD] --> %d", + __entry->bus_num, __entry->chip_select, __entry->len, + __get_dynamic_array_len(tx_buf), __get_dynamic_array(tx_buf), + __get_dynamic_array_len(rx_buf), __get_dynamic_array(rx_buf), + __entry->ret) +); + +DEFINE_EVENT(spi_hid_transfer, spi_hid_input_async, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret) +); + +DEFINE_EVENT(spi_hid_transfer, spi_hid_input_header_complete, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret) +); + +DEFINE_EVENT(spi_hid_transfer, spi_hid_input_body_complete, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret) +); + +DEFINE_EVENT(spi_hid_transfer, spi_hid_output_begin, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret) +); + +DEFINE_EVENT(spi_hid_transfer, spi_hid_output_end, + TP_PROTO(struct spi_hid *shid, const void *tx_buf, int tx_len, + const void *rx_buf, u16 rx_len, int ret), + TP_ARGS(shid, tx_buf, tx_len, rx_buf, rx_len, ret) +); + +DECLARE_EVENT_CLASS(spi_hid_irq, + TP_PROTO(struct spi_hid *shid, int irq), + + TP_ARGS(shid, irq), + + TP_STRUCT__entry( + __field(int, bus_num) + __field(int, chip_select) + __field(int, irq) + ), + + TP_fast_assign( + __entry->bus_num = shid->spi->controller->bus_num; + __entry->chip_select = shid->spi->chip_select; + __entry->irq = irq; + ), + + TP_printk("spi%d.%d: IRQ %d", + __entry->bus_num, __entry->chip_select, __entry->irq) +); + +DEFINE_EVENT(spi_hid_irq, spi_hid_dev_irq, + TP_PROTO(struct spi_hid *shid, int irq), + TP_ARGS(shid, irq) +); + +DECLARE_EVENT_CLASS(spi_hid, + TP_PROTO(struct spi_hid *shid), + + TP_ARGS(shid), + + TP_STRUCT__entry( + __field(int, bus_num) + __field(int, chip_select) + __field(int, input_stage) + __field(int, power_state) + __field(u32, input_transfer_pending) + __field(bool, ready) + + __field(int, vendor_id) + __field(int, product_id) + __field(int, max_input_length) + __field(int, max_output_length) + __field(u16, hid_version) + __field(u16, report_descriptor_length) + __field(u16, version_id) + ), + + TP_fast_assign( + __entry->bus_num = shid->spi->controller->bus_num; + __entry->chip_select = shid->spi->chip_select; + __entry->input_stage = shid->input_stage; + __entry->power_state = shid->power_state; + __entry->input_transfer_pending = shid->input_transfer_pending; + __entry->ready = shid->ready; + + __entry->vendor_id = shid->desc.vendor_id; + __entry->product_id = shid->desc.product_id; + __entry->max_input_length = shid->desc.max_input_length; + __entry->max_output_length = shid->desc.max_output_length; + __entry->hid_version = shid->desc.hid_version; + __entry->report_descriptor_length = shid->desc.report_descriptor_length; + __entry->version_id = shid->desc.version_id; + ), + + TP_printk("spi%d.%d: (%04x:%04x v%d) HID v%d.%d state i:%d p:%d len i:%d o:%d r:%d flags %c:%d", + __entry->bus_num, __entry->chip_select, __entry->vendor_id, + __entry->product_id, __entry->version_id, + __entry->hid_version >> 8, __entry->hid_version & 0xff, + __entry->input_stage, __entry->power_state, + __entry->max_input_length, __entry->max_output_length, + __entry->report_descriptor_length, + __entry->ready ? 'R' : 'r', + __entry->input_transfer_pending) +); + +DEFINE_EVENT(spi_hid, spi_hid_bus_input_report, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_process_input_report, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_input_report_handler, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_reset_work, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_create_device_work, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_refresh_device_work, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +DEFINE_EVENT(spi_hid, spi_hid_response_handler, + TP_PROTO(struct spi_hid *shid), + TP_ARGS(shid) +); + +#endif /* _SPI_HID_TRACE_H */ + +#undef TRACE_INCLUDE_PATH +#define TRACE_INCLUDE_PATH . +#define TRACE_INCLUDE_FILE spi-hid_trace +#include diff --git a/kernel/modules/spi-hid/trace.c b/kernel/modules/spi-hid/trace.c new file mode 100644 index 0000000..7c8e423 --- /dev/null +++ b/kernel/modules/spi-hid/trace.c @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0 +/** + * trace.c - SPI HID Trace Support + * + * Copyright (C) 2020 Microsoft Corporation + * + * Author: Felipe Balbi + */ + +#define CREATE_TRACE_POINTS +#include "spi-hid_trace.h"