diff --git a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml new file mode 100644 index 0000000000000..f1396f44990db --- /dev/null +++ b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml @@ -0,0 +1,249 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/display/apple,t8152-dcp-iboot.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T8152 DCP on the iBoot display interface + +maintainers: + - Thomas Glanzmann + +description: | + The Display Coprocessor (DCP) of the Apple T8152 SoC runs RTKit firmware + that offers a small iBoot display interface (disp0-service) next to the + full IOMobileFramebuffer protocol. This binding describes the DCP driven + through that interface, together with the DisplayPort PHY its DPTX port + is routed to. The DCP fetches through its own DART, the display pipe + scans out through another one, which sees each scanout buffer at the same + address. + +properties: + compatible: + const: apple,t8152-dcp-iboot + + apple,afk-active-endpoints: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 65535 + description: + Runtime handoff from the bootloader. Bit N records that AFK endpoint + 0x20 + N is running and must acknowledge shutdown before a new session + starts. A zero bit means stopped. The bootloader inserts this only + for controllers whose complete endpoint state it owns, and reserves + the old session's buffers. The driver refuses takeover if absent. + This property must not be hard-coded in a board device tree. + + reg: + minItems: 2 + maxItems: 7 + + reg-names: + oneOf: + - items: + - const: coproc + - const: atc + - const: dp-clock + - const: atc-ps + - const: atc-psd + - const: aux + - const: aux-ps + minItems: 5 + description: + DCP coprocessor, ATC DisplayPort PHY (USB-C), the PMGR clock + control of its DisplayPort link, the PMGR power states the PHY + depends on, and optionally the AP DisplayPort AUX controller that + reads the sink's EDID with its PMGR power state + - items: + - const: coproc + - const: phy-core + - const: phy-apb + - const: aux + - const: aux-ps + - const: phy-ps + - const: coproc-ps + description: + DCP coprocessor, the HDMI dptx-phy blocks, the AP + DisplayPort AUX controller that reads the sink's EDID with its + PMGR power state, and the read-only PMGR power states for the + HDMI PHY and DCPEXT CPU. The driver only adopts the bootloader's + powered link and must check these states before accessing the PHY. + - items: + - const: coproc + - const: dp-clock + - const: aux + - const: aux-ps + minItems: 2 + description: + DCP coprocessor, the PMGR clock control of the ATC DisplayPort + link (shared by the ports), and optionally the AP DisplayPort AUX + controller with its PMGR power state; the USB-C ports follow as + usb-c child nodes + + apple,tunable-pre: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + items: + items: + - description: register offset in the ATC PHY block + - description: mask + - description: value + description: + ATC PHY tunables (ADT ACIOPHY_CMN and CIOPLL_CORE defaults), applied + before the lane crossbar setup when the PHY starts from cold. + + apple,tunable-post: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + items: + items: + - description: register offset in the ATC PHY block + - description: mask + - description: value + description: + ATC PHY tunables (ADT ATC_FABRIC, DPPLL and AUX defaults), applied + after the lane crossbar setup. + + apple,hdmi: + type: boolean + description: + The DCP drives the HDMI port through the dedicated dptx-phy and an + on-board DisplayPort-to-HDMI converter, instead of an ATC USB-C PHY. + + apple,usb-pd: + $ref: /schemas/types.yaml#/definitions/phandle + description: + The USB-C port's PD controller (SPMI), polled for the DisplayPort alt + mode connection and the plug orientation. + + hpd-gpios: + maxItems: 1 + description: + Hot plug detect line of the connector, if the AP can read it (J873g + HDMI); without it the sink is assumed to stay connected. + + mboxes: + maxItems: 1 + + iommus: + maxItems: 1 + description: DCP DART stream + + scanout: + type: object + additionalProperties: false + description: + The display pipe's scanout stream. Its DART is locked with its own + root, so it is a separate device that sees each scanout buffer at the + DCP's address. + properties: + iommus: + maxItems: 1 + dma-coherent: true + required: + - iommus + + dma-coherent: true + + apple,die: + $ref: /schemas/types.yaml#/definitions/uint32 + description: Die of the DPTX connection target, 0 if absent. + + "#address-cells": + const: 2 + + "#size-cells": + const: 2 + + ranges: true + +patternProperties: + "^usb-c@[0-9a-f]+$": + type: object + additionalProperties: false + description: + A USB-C port whose ATC PHY the DCP can drive. The DCP drives one port + at a time, the one a DisplayPort sink is plugged into. + properties: + reg: + maxItems: 3 + reg-names: + items: + - const: atc + - const: atc-ps + - const: atc-psd + description: + ATC DisplayPort PHY, the PMGR power states it depends on (DP_0, + DP_1 and USB_AON) and its common power domain (PSD) + apple,dfp-endpoint: + $ref: /schemas/types.yaml#/definitions/uint32 + description: The ATC's display-crossbar0 DFP endpoint (ATC instance). + apple,usb-pd: + $ref: /schemas/types.yaml#/definitions/phandle + description: The port's PD controller (SPMI). + apple,tunable-pre: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + description: As the node's apple,tunable-pre, for this port's PHY. + apple,tunable-post: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + description: As the node's apple,tunable-post, for this port's PHY. + required: + - reg + - reg-names + - apple,dfp-endpoint + - apple,usb-pd + +required: + - compatible + - reg + - reg-names + - mboxes + - iommus + - scanout + +additionalProperties: false + +examples: + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + display@286e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x86e00000 0x0 0x74000>, + <0x4 0x13000000 0x0 0x84000>, + <0x3 0x0002005c 0x0 0x4>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "coproc", "atc", "dp-clock", "atc-ps", "atc-psd"; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + + scanout { + iommus = <&disp0_dart 0>; + }; + }; + + display@296e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x96e00000 0x0 0x74000>, + <0x3 0x0002005c 0x0 0x4>; + reg-names = "coproc", "dp-clock"; + #address-cells = <2>; + #size-cells = <2>; + ranges; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + + scanout { + iommus = <&disp0_dart 0>; + }; + + usb-c@413000000 { + reg = <0x4 0x13000000 0x0 0x84000>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <3>; + apple,usb-pd = <&hpm3>; + }; + }; + }; diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml index 401b41f8ff18b..d438b95118085 100644 --- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml +++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml @@ -28,6 +28,7 @@ properties: - apple,t8103-usb4-dart - apple,t8110-dart - apple,t6000-dart + - apple,t8152-dart-gen3 - items: - const: apple,t8140-ane-dart - const: apple,t8140-dart @@ -66,7 +67,19 @@ properties: error-mask policy rather than changing it for these streams. reg: - maxItems: 1 + minItems: 1 + items: + - description: DART registers + - description: + Gen3 only. The SoC's TLB invalidation aperture window for this + DART (ADT dart-tlbi-aperture plus tlbi-aperture-offset), shared + by all Gen3 DARTs. A stream is invalidated by writing a payload + there and broadcasting a TLBI. + + reg-names: + items: + - const: dart + - const: tlbi interrupts: maxItems: 1 @@ -158,6 +171,28 @@ required: - '#iommu-cells' - interrupts +allOf: + - if: + properties: + compatible: + contains: + const: apple,t8152-dart-gen3 + then: + description: + T8152 Gen3 DARTs are owned by the SPTM. The bootloader hands them + over locked; the operating system only adds page table entries in + root slots that are empty at handoff. + properties: + reg: + minItems: 2 + required: + - reg-names + else: + properties: + reg: + maxItems: 1 + reg-names: false + additionalProperties: false examples: diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index 2f281baae5dca..a5d2322c28486 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -23,9 +23,11 @@ properties: compatible: oneOf: - description: - T8152 SMC post-office mailbox with counted FIFOs and interrupt + T8152 SMC and DCP post-office mailboxes with counted FIFOs and interrupt masking provided by the interrupt controller. - const: apple,t8152-smc-mailbox + enum: + - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox - description: ASC mailboxes are the most common variant found on the M1 used @@ -135,7 +137,9 @@ allOf: properties: compatible: contains: - const: apple,t8152-smc-mailbox + enum: + - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox then: properties: interrupts: @@ -195,6 +199,7 @@ allOf: - apple,t8112-ane-mailbox - apple,t8140-aop-setup-mailbox - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox then: properties: interrupts: diff --git a/Documentation/devicetree/bindings/power/apple,pmc.yaml b/Documentation/devicetree/bindings/power/apple,pmc.yaml index 477fc330d253f..4f85d4fc40b9b 100644 --- a/Documentation/devicetree/bindings/power/apple,pmc.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmc.yaml @@ -29,6 +29,7 @@ properties: - apple,t8132-pmc - apple,t8140-pmc - apple,t8142-pmc + - apple,t8152-pmc reg: minItems: 2 @@ -191,6 +192,7 @@ patternProperties: - apple,t8132-clpc - apple,t8140-clpc - apple,t8142-clpc + - apple,t8152-clpc reg: maxItems: 1 diff --git a/Documentation/devicetree/bindings/spi/apple,qspi.yaml b/Documentation/devicetree/bindings/spi/apple,qspi.yaml index 0b98af1ad68d2..40d08a8273712 100644 --- a/Documentation/devicetree/bindings/spi/apple,qspi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,qspi.yaml @@ -19,11 +19,15 @@ allOf: properties: compatible: - enum: - - apple,t8122-qspi - - apple,t8132-qspi - - apple,t6041-qspi - - apple,t8140-qspi + oneOf: + - enum: + - apple,t8122-qspi + - apple,t8132-qspi + - apple,t6041-qspi + - apple,t8140-qspi + - items: + - const: apple,t8152-qspi + - const: apple,t8140-qspi reg: maxItems: 1 diff --git a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml index bb4940558fbdb..c96ab3adeec3e 100644 --- a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml +++ b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml @@ -36,7 +36,9 @@ properties: - apple,t6000-spmi - apple,t8112-spmi - const: apple,spmi - - const: apple,t8142-spmi + - enum: + - apple,t8142-spmi + - apple,t8152-spmi reg: maxItems: 1 diff --git a/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi new file mode 100644 index 0000000000000..7d46e3666d4b9 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873/T8152 DCP display driver on the iBoot interface (apple_dcpib) for the + * rear USB-C and HDMI displays, included by the public J873g board tree. + * + * Resources are from the 26A434 ADT: dcp (iop,ascwrap-v8, post-office + * mailbox 5 with alias: 0x316020000 + 2 * 5 * 0x80), dart-dcp (SID 23), + * dart-disp0 (scanout SID 0), dart-tlbi-aperture (0x200000000) with + * tlbi-aperture-offset 0x400, and atc-phy3's PHY window. Both DARTs are + * Gen3 and stay locked by iBoot, each with its own live root, so the scanout + * stream is a child device with its own domain; Linux tables are published into vacant + * root slots, so the DMA window is the upper half of the 64 GiB vm-base + * window, above the firmware's and the bootloader's mappings. + */ +#include + +/ { + soc { + /* + * ADT /arm-io/nub-spmi-a0 (gen 4): the USB-C PD controllers of + * the three rear ports, each of which can carry the USB-C display. + */ + nub_spmi_a0: spmi@308908000 { + compatible = "apple,t8152-spmi"; + reg = <0x3 0x08908000 0x0 0x4000>; + #address-cells = <2>; + #size-cells = <0>; + + /* back-middle */ + hpm0: usb-pd@c { + compatible = "apple,sn201202x-spmi"; + reg = <0xc SPMI_USID>; + }; + + /* back-left */ + hpm1: usb-pd@a { + compatible = "apple,sn201202x-spmi"; + reg = <0xa SPMI_USID>; + }; + + /* back-right */ + hpm3: usb-pd@8 { + compatible = "apple,sn201202x-spmi"; + reg = <0x8 SPMI_USID>; + }; + }; + + /* + * ADT aop-gpio. Its interrupts are the AOP's, so it has no + * interrupt controller here; the HDMI HPD line is polled. + */ + pinctrl_aop: pinctrl@310824000 { + compatible = "apple,t8152-pinctrl", "apple,t8103-pinctrl"; + reg = <0x3 0x10824000 0x0 0x4000>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 77>; + apple,npins = <77>; + }; + + dcp_dart: iommu@286340000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x2 0x86340000 0x0 0x20000>, + <0x2 0x00000400 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + disp0_dart: iommu@286300000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x2 0x86300000 0x0 0xc000>, + <0x2 0x00000400 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dcp_mbox: mailbox@316020500 { + compatible = "apple,t8152-dcp-mailbox"; + reg = <0x3 0x16020500 0x0 0x80>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; + #mbox-cells = <0>; + }; + + dcp_iboot: display@286e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x86e00000 0x0 0x74000>, + <0x3 0x0002005c 0x0 0x4>, + <0x3 0x1a760000 0x0 0x4000>, + <0x3 0x00700460 0x0 0x4>; + /* aux: ADT dptx0-aux, aux-ps: PMGR GLOBAL_AUX_CTRL0 */ + reg-names = "coproc", "dp-clock", "aux", "aux-ps"; + #address-cells = <2>; + #size-cells = <2>; + ranges; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + /* + * Cacheable DMA, as iBoot maps DCP buffers: the firmware + * faults on entries with the DART's no-cache bit. + */ + dma-coherent; + + /* + * The display pipe's own stream: a separate device, so + * its domain imports dart-disp0's root, not dart-dcp's. + */ + scanout { + iommus = <&disp0_dart 0>; + dma-coherent; + }; + + /* + * USB-C back-middle: ADT atc-phy0. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@403000000 { + reg = <0x4 0x03000000 0x0 0x84000>, + <0x3 0x08290000 0x0 0x104>, + <0x3 0x03a13700 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <0>; + apple,usb-pd = <&hpm0>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bca4>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + + /* + * USB-C back-left: ADT atc-phy1. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@40b000000 { + reg = <0x4 0x0b000000 0x0 0x84000>, + <0x3 0x08290040 0x0 0x104>, + <0x3 0x03a13800 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <1>; + apple,usb-pd = <&hpm1>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bc26>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + + /* + * USB-C back-right: ADT atc-phy3. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@413000000 { + reg = <0x4 0x13000000 0x0 0x84000>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <3>; + apple,usb-pd = <&hpm3>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bc26>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + }; + + /* + * HDMI: DCPEXT0 (post-office mailbox 6) drives the dedicated + * dptx-phy through the PS190 converter. ADT dcpext0, + * dart-dcpext0 and dart-dispext0 (tlbi-aperture-offset 0x480). + */ + dcpext_dart: iommu@32a340000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x3 0x2a340000 0x0 0x20000>, + <0x2 0x00000480 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dispext0_dart: iommu@32a300000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x3 0x2a300000 0x0 0xc000>, + <0x2 0x00000480 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dcpext_mbox: mailbox@316020600 { + compatible = "apple,t8152-dcp-mailbox"; + reg = <0x3 0x16020600 0x0 0x80>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; + #mbox-cells = <0>; + }; + + dcpext_iboot: display@32ae00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x3 0x2ae00000 0x0 0x74000>, + <0x3 0x1a740000 0x0 0x10000>, + <0x3 0x1a700000 0x0 0x4000>, + <0x3 0x1a764000 0x0 0x4000>, + <0x3 0x00700480 0x0 0x4>, + <0x3 0x007016c0 0x0 0x4>, + <0x3 0x00700e00 0x0 0x4>; + /* aux: ADT dptx1-aux, aux-ps: PMGR GLOBAL_AUX_CTRL1 */ + /* Read-only PMGR DPTX_PHY and DISPEXT0_CPU admission checks. */ + reg-names = "coproc", "phy-core", "phy-apb", "aux", "aux-ps", + "phy-ps", "coproc-ps"; + mboxes = <&dcpext_mbox>; + iommus = <&dcpext_dart 23>; + dma-coherent; + apple,hdmi; + /* ADT dp2hdmi-gpio0 function-hdmi_hpd */ + hpd-gpios = <&pinctrl_aop 41 GPIO_ACTIVE_HIGH>; + + scanout { + iommus = <&dispext0_dart 0>; + dma-coherent; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi new file mode 100644 index 0000000000000..3b809b363b84e --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT + +/ { + soc { + /* + * Perf-state floors of voter agents 0-3 (PMP, CLPC, GFX, AP) and, + * at 0x1c6000, the honoured states and per-rail voter enables + * (rails DCS, SOC, DISP) in the PMC window, pmgr reg 46. + */ + pmc: power-management@302800000 { + compatible = "apple,t8152-pmc"; + reg = <0x3 0x02800000 0x0 0x10000>, + <0x3 0x029c6000 0x0 0x3000>; + reg-names = "voters", "rails"; + #address-cells = <1>; + #size-cells = <0>; + #cooling-cells = <2>; + /* PMP, CLPC and AP agents */ + apple,static-voters = <0 1 3>; + + clpc: performance-controller@1 { + compatible = "apple,t8152-clpc"; + reg = <1>; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi new file mode 100644 index 0000000000000..c03fbf04b24e4 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT + +/* + * The boot NOR behind the QSPIMC controller (ADT /arm-io/qspi/spinor: firmware, + * anvram, syscfg), for asahi-nvram and asahi-bless. The controller's power + * states (PMGR devices SPI_P and QSPI) are off once iBoot is done with it; + * reading it without them takes an SError. + */ +/ { + aliases { + nvram = &nvram; + }; + + /* QSPIMC input clock, as on T8140 (ADT clock-ids 412) */ + clk_200m: clock-200m { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <200000000>; + clock-output-names = "clk_200m"; + }; + + soc { + pmgr: power-management@300700000 { + compatible = "apple,t8152-pmgr", "apple,t8103-pmgr", + "syscon", "simple-mfd"; + #address-cells = <1>; + #size-cells = <1>; + reg = <0x3 0x700000 0x0 0x14000>; + }; + + qspi: spi@331118000 { + compatible = "apple,t8152-qspi", "apple,t8140-qspi"; + /* One aperture; the command block starts at offset 0x800. */ + reg = <0x3 0x31118000 0x0 0x4800>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clk_200m>; + power-domains = <&ps_qspi>; + #address-cells = <1>; + #size-cells = <0>; + + spinor: flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <10000000>; + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* + * 64 KiB NVRAM v3 banks ("3VVN") from 0x24000 + * to 0x1f4000, as on T8140 J700. + */ + nvram: partition@24000 { + label = "nvram"; + reg = <0x24000 0x1d0000>; + }; + }; + }; + }; + }; +}; + +&pmgr { + ps_spi_p: power-controller@d40 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0xd40 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + }; + + ps_qspi: power-controller@1140 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x1140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "qspi"; + power-domains = <&ps_spi_p>; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index f4ccb05ba0aab..382f581656009 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -283,3 +283,7 @@ #include "t8152-j873-gpu.dtsi" #include "t8152-j873-n1.dtsi" + +#include "t8152-j873-pmc.dtsi" +#include "t8152-j873-qspi.dtsi" +#include "t8152-j873-dcp.dtsi" diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig index 89c899e836797..d80f7832f6f27 100644 --- a/drivers/gpu/drm/Kconfig +++ b/drivers/gpu/drm/Kconfig @@ -302,6 +302,7 @@ config DRM_PRIVACY_SCREEN source "drivers/gpu/drm/adp/Kconfig" source "drivers/gpu/drm/amd/amdgpu/Kconfig" source "drivers/gpu/drm/apple/Kconfig" +source "drivers/gpu/drm/apple_dcpib/Kconfig" source "drivers/gpu/drm/arm/Kconfig" source "drivers/gpu/drm/armada/Kconfig" source "drivers/gpu/drm/asahi/Kconfig" diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile index cd966b1b5b47e..ccc1d8ef193c8 100644 --- a/drivers/gpu/drm/Makefile +++ b/drivers/gpu/drm/Makefile @@ -231,6 +231,7 @@ obj-$(CONFIG_DRM_APPLE_FABRIC_CORE) += apple/dcp-fabric-core.o obj-$(CONFIG_DRM_APPLE_FABRIC_KUNIT_TEST) += apple/dcp-fabric-test.o obj-$(CONFIG_DRM_APPLE) += apple/ obj-$(CONFIG_DRM_APPLE_KUNIT_TEST) += apple/iomfb-state-test.o +obj-$(CONFIG_DRM_APPLE_DCPIB) += apple_dcpib/ obj-$(CONFIG_DRM_ASPEED_GFX) += aspeed/ obj-$(CONFIG_DRM_MCDE) += mcde/ obj-$(CONFIG_DRM_TIDSS) += tidss/ diff --git a/drivers/gpu/drm/apple_dcpib/Kconfig b/drivers/gpu/drm/apple_dcpib/Kconfig new file mode 100644 index 0000000000000..9e3ba21b4b821 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +config DRM_APPLE_DCPIB + tristate "Apple T8152 DCP display (iBoot interface)" + depends on DRM && OF && ARM64 + depends on ARCH_APPLE || COMPILE_TEST + depends on APPLE_RTKIT + depends on SPMI + select APERTURE_HELPERS + select DRM_CLIENT_SELECTION + select DRM_GEM_DMA_HELPER + select DRM_KMS_HELPER + help + Display driver for the DCP of the Apple T8152 (Mac mini J873g) on + the firmware's small iBoot interface. It takes over the display the + bootloader brought up on the USB-C port. diff --git a/drivers/gpu/drm/apple_dcpib/Makefile b/drivers/gpu/drm/apple_dcpib/Makefile new file mode 100644 index 0000000000000..5a288e7df1a29 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +apple_dcpib-y := drv.o afk.o iboot.o dptx.o atc.o hdmi.o kms.o pd.o aux.o +obj-$(CONFIG_DRM_APPLE_DCPIB) += apple_dcpib.o diff --git a/drivers/gpu/drm/apple_dcpib/afk.c b/drivers/gpu/drm/apple_dcpib/afk.c new file mode 100644 index 0000000000000..036272171079a --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/afk.c @@ -0,0 +1,763 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * AFKEPV2 ring-buffer endpoints of the T8152 DCP (port of aura src/afk.c). + * + * Each endpoint shares one DMA buffer with the firmware, which carves a TX + * and an RX ring out of it. Messages carry an afkv2 header (interface, seq) + * and a message header (token, type, category). The firmware publishes + * services (REPORT/PUBLISH), which the AP opens; AP commands use out-of-band + * TX/RX buffers, firmware callbacks are inline standard service calls. + */ + +#include +#include +#include +#include + +#include "dcpib.h" + +#define RBEP_TYPE GENMASK_ULL(63, 48) +#define RBEP_INIT 0x80 +#define RBEP_INIT_ACK 0xa0 +#define RBEP_GETBUF 0x89 +#define RBEP_GETBUF_ACK 0xa1 +#define RBEP_INIT_TX 0x8a +#define RBEP_INIT_RX 0x8b +#define RBEP_START 0xa3 +#define RBEP_START_ACK 0x86 +#define RBEP_SEND 0xa2 +#define RBEP_RECV 0x85 +#define RBEP_SHUTDOWN 0xc0 +#define RBEP_SHUTDOWN_ACK 0xc1 + +#define BLOCK_SHIFT 6 +#define GETBUF_SIZE GENMASK_ULL(31, 16) +#define GETBUF_TAG GENMASK_ULL(15, 0) +#define GETBUF_ACK_DVA GENMASK_ULL(47, 0) +#define INITRB_OFFSET GENMASK_ULL(47, 32) +#define INITRB_SIZE GENMASK_ULL(31, 16) +#define INITRB_TAG GENMASK_ULL(15, 0) +#define SEND_WPTR GENMASK_ULL(31, 0) + +#define QE_MAGIC 0x20504f49 /* ' POI' */ +#define SERVICE_CALL_MAGIC 0x69706378 + +#define AFKV2_REPORT 0 +#define AFKV2_COMMAND 1 +#define AFKV2_RESPONSE 2 +#define AFKV2_PUBLISH 0x11 +#define AFKV2_OPEN 0x12 +#define AFKV2_STD_SERVICE 0xc0 + +#define DCPIB_BUF_SIZE 0x4000 +#define DCPIB_TIMEOUT msecs_to_jiffies(3000) + +struct afk_qe { + __le32 magic; + __le32 size; + __le32 channel; + __le32 type; + u8 data[]; +} __packed; + +struct afkv2_hdr { + u8 seq; + u8 reserved; + __le16 interface; + __le32 length; +} __packed; + +struct afkv2_msg { + __le64 token; + u8 type; + u8 category; + u8 flags; + u8 reserved[5]; +} __packed; + +struct afkv2_cmd { + u8 flags; + u8 id; + __le16 reserved; + __le32 value; /* command: response capacity; response: IOReturn */ +} __packed; + +struct afkv2_oob { + __le64 rxbuf; + __le64 txbuf; + __le32 rxlen; + __le32 txlen; +} __packed; + +struct afkv2_service_call { + __le16 reserved; + __le16 group; + __le32 command; + __le32 length; + __le32 magic; + u8 padding[48]; +} __packed; + +struct afk_announce { + char name[32]; + u8 props[]; +} __packed; + +int dcpib_dma_alloc(struct dcpib *dcp, struct dcpib_dma *dma, size_t size) +{ + dma->size = PAGE_ALIGN(size); + dma->cpu = dma_alloc_coherent(dcp->dev, dma->size, &dma->iova, + GFP_KERNEL); + return dma->cpu ? 0 : -ENOMEM; +} + +void dcpib_dma_free(struct dcpib *dcp, struct dcpib_dma *dma) +{ + if (dma->cpu) + dma_free_coherent(dcp->dev, dma->size, dma->cpu, dma->iova); + dma->cpu = NULL; +} + +static int dcpib_send(struct dcpib_ep *ep, u64 msg) +{ + return apple_rtkit_send_message(ep->dcp->rtk, ep->ep, msg, NULL, false); +} + +static int dcpib_ring_init(struct dcpib_ep *ep, struct dcpib_ring *rb, u64 base, + u64 size) +{ + u32 bufsz, block; + void *hdr; + + if (!ep->ring.cpu || base > ep->ring.size || + size > ep->ring.size - base || size < 4 * 0x40) + return -EINVAL; + + hdr = ep->ring.cpu + base; + bufsz = le32_to_cpu(READ_ONCE(*(__le32 *)hdr)); + /* Three blocks hold size, read pointer and write pointer. */ + if (!bufsz || bufsz >= size || (size - bufsz) % 3) + return -EINVAL; + block = (size - bufsz) / 3; + if (block < 0x40 || !is_power_of_2(block) || bufsz % block) + return -EINVAL; + + rb->rptr = hdr + block; + rb->wptr = hdr + 2 * block; + rb->buf = hdr + 3 * block; + rb->bufsz = bufsz; + rb->block = block; + if (READ_ONCE(*rb->rptr) >= bufsz || READ_ONCE(*rb->wptr) >= bufsz) + return -EINVAL; + rb->ready = true; + return 0; +} + +/* Queue one message on the TX ring and tell the firmware. */ +static int dcpib_tx(struct dcpib_ep *ep, const void *data, size_t size) +{ + struct dcpib_ring *rb = &ep->tx; + struct afk_qe *qe; + u32 rptr, wptr, advance; + int ret; + + mutex_lock(&ep->tx_lock); + if (!rb->ready || size > rb->bufsz - sizeof(*qe)) { + ret = -EINVAL; + goto out; + } + + rptr = le32_to_cpu(READ_ONCE(*rb->rptr)); + wptr = le32_to_cpu(READ_ONCE(*rb->wptr)); + advance = ALIGN(sizeof(*qe) + size, rb->block); + + if (wptr < rptr && advance >= rptr - wptr) + goto full; + if (wptr >= rptr) { + bool fits = advance < rb->bufsz - wptr || + (advance == rb->bufsz - wptr && rptr); + + if (!fits && advance >= rptr) + goto full; + } + + qe = rb->buf + wptr; + qe->magic = cpu_to_le32(QE_MAGIC); + qe->size = cpu_to_le32(size); + qe->channel = 0; + qe->type = 0; + wptr += sizeof(*qe); + /* A payload that does not fit before the end starts at offset 0. */ + if (size > rb->bufsz - wptr) { + memcpy(rb->buf, qe, sizeof(*qe)); + qe = rb->buf; + wptr = sizeof(*qe); + } + memcpy(qe->data, data, size); + wptr = ALIGN(wptr + size, rb->block); + if (wptr >= rb->bufsz) + wptr = 0; + + dma_mb(); + WRITE_ONCE(*rb->wptr, cpu_to_le32(wptr)); + dma_wmb(); + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SEND) | + FIELD_PREP(SEND_WPTR, wptr)); + goto out; +full: + dev_err(ep->dcp->dev, "AFK[%02x]: TX ring full\n", ep->ep); + ret = -ENOSPC; +out: + mutex_unlock(&ep->tx_lock); + return ret; +} + +static int dcpib_tx_v2(struct dcpib_ep *ep, u16 interface, void *packet, + size_t size) +{ + struct afkv2_hdr *hdr = packet; + + mutex_lock(&ep->tx_lock); + hdr->seq = ep->seq++; + mutex_unlock(&ep->tx_lock); + hdr->interface = cpu_to_le16(interface); + hdr->length = cpu_to_le32(size - sizeof(*hdr)); + return dcpib_tx(ep, packet, size); +} + +static struct dcpib_service *dcpib_find_service(struct dcpib_ep *ep, + u16 interface) +{ + for (unsigned int i = 0; i < ep->nr_services; i++) + if (ep->services[i].interface == interface) + return &ep->services[i]; + return NULL; +} + +static void dcpib_announce(struct dcpib_ep *ep, u16 interface, + const void *payload, size_t size) +{ + const struct afk_announce *ann = payload; + struct dcpib_service *service; + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + } __packed open = { + .msg = { .type = AFKV2_OPEN, .category = AFKV2_REPORT, .flags = 1 }, + }; + + if (!interface || size < sizeof(*ann) || + strnlen(ann->name, sizeof(ann->name)) == sizeof(ann->name)) + return; + if (dcpib_find_service(ep, interface)) + return; + if (ep->nr_services >= DCPIB_MAX_SERVICES) { + dev_warn(ep->dcp->dev, "AFK[%02x]: no room for service %s\n", + ep->ep, ann->name); + return; + } + + service = &ep->services[ep->nr_services]; + service->ep = ep; + service->ops = ep->ops; + service->interface = interface; + service->unit = ep->nr_services; + strscpy(service->name, ann->name, sizeof(service->name)); + ep->nr_services++; + + dev_info(ep->dcp->dev, "AFK[%02x]: opening %s on interface %u\n", + ep->ep, service->name, interface); + if (dcpib_tx_v2(ep, interface, &open, sizeof(open))) { + dev_err(ep->dcp->dev, "AFK[%02x]: failed to open %s\n", ep->ep, + service->name); + } else if (strstr(service->name, "dcpav-video-interface")) { + /* + * The AV service powers the display on with this interface and + * starts its idle power off timer: SET_MODE again once it is up. + */ + dcpib_av_started(ep->dcp); + } + if (ep->nr_services == ep->expected) + complete_all(&ep->services_done); +} + +/* Answer a firmware callback we cannot handle: empty, kIOReturnUnsupported. */ +static void dcpib_reject(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, + const struct afkv2_cmd *cmd, bool oob) +{ + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + struct afkv2_cmd cmd; + struct afkv2_oob oob; + } __packed reply = { + .msg = { .token = msg->token, .type = msg->type, + .category = AFKV2_RESPONSE }, + .cmd = { .flags = cmd->flags, .id = cmd->id, + .value = cpu_to_le32(KIO_RETURN_UNSUPPORTED) }, + }; + size_t len = sizeof(reply) - sizeof(reply.oob); + + if (oob) { + const struct afkv2_oob *o = (const void *)(cmd + 1); + + /* Keep the descriptors, return no data; never touch them. */ + reply.oob.rxbuf = o->rxbuf; + reply.oob.txbuf = o->txbuf; + len += sizeof(reply.oob); + } + dev_dbg(ep->dcp->dev, "AFK[%02x]: rejecting callback type %#x\n", + ep->ep, msg->type); + dcpib_tx_v2(ep, interface, &reply, len); +} + +static void dcpib_callback(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, const void *payload, + size_t size) +{ + const struct afkv2_cmd *cmd = payload; + const struct afkv2_service_call *call; + struct dcpib_service *service; + struct afkv2_hdr *rhdr; + struct afkv2_msg *rmsg; + struct afkv2_cmd *rcmd; + struct afkv2_service_call *rcall; + size_t body, len; + __le32 status; + u8 *reply; + + if (size < sizeof(*cmd) || cmd->flags & ~1) + return; + if ((cmd->flags & 1) && size < sizeof(*cmd) + sizeof(struct afkv2_oob)) + return; + + service = dcpib_find_service(ep, interface); + body = size - sizeof(*cmd); + call = (const void *)(cmd + 1); + /* Only the inline standard service call form is supported. */ + if (msg->type != AFKV2_STD_SERVICE || cmd->flags || !service || + !service->ops || !service->ops->call || + body < sizeof(*call) + sizeof(u32) || + le32_to_cpu(cmd->value) < body || + le32_to_cpu(call->magic) != SERVICE_CALL_MAGIC || + le32_to_cpu(call->length) != body - sizeof(*call)) { + dcpib_reject(ep, interface, msg, cmd, cmd->flags & 1); + return; + } + + len = sizeof(*rhdr) + sizeof(*rmsg) + size; + reply = kzalloc(len, GFP_KERNEL); + if (!reply) + return; + rhdr = (void *)reply; + rmsg = (void *)(rhdr + 1); + rcmd = (void *)(rmsg + 1); + rcall = (void *)(rcmd + 1); + rmsg->token = msg->token; + rmsg->type = msg->type; + rmsg->category = AFKV2_RESPONSE; + rcmd->id = cmd->id; + memcpy(rcall, call, body); + status = cpu_to_le32(service->ops->call(service, + le16_to_cpu(call->group), + le32_to_cpu(call->command), call + 1, + le32_to_cpu(call->length), rcall + 1, + le32_to_cpu(call->length))); + memcpy(rcall + 1, &status, sizeof(status)); + if (status) + dev_dbg(ep->dcp->dev, + "AFK[%02x]: callback %s group %u command %u -> %#x\n", + ep->ep, service->name, le16_to_cpu(call->group), + le32_to_cpu(call->command), le32_to_cpu(status)); + dcpib_tx_v2(ep, interface, reply, len); + kfree(reply); +} + +static void dcpib_response(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, const void *payload, + size_t size) +{ + const struct afkv2_cmd *cmd = payload; + const void *data; + size_t len; + int ret = 0; + + if (!ep->cmd_waiting || interface != ep->cmd_interface || + msg->type != ep->cmd_type || size < sizeof(*cmd) || + cmd->id != ep->cmd_id) { + dev_warn(ep->dcp->dev, + "AFK[%02x]: unexpected response on interface %u\n", + ep->ep, interface); + return; + } + + data = cmd + 1; + len = size - sizeof(*cmd); + if (cmd->flags & ~1) { + ret = -EPROTO; + } else if (le32_to_cpu(cmd->value)) { + dev_dbg(ep->dcp->dev, "AFK[%02x]: command %#x returned %#x\n", + ep->ep, msg->type, le32_to_cpu(cmd->value)); + ret = -EIO; + } else if (cmd->flags & 1) { + const struct afkv2_oob *oob = data; + + if (len < sizeof(*oob)) { + ret = -EPROTO; + } else { + data = oob + 1; + len -= sizeof(*oob); + if (le32_to_cpu(oob->rxlen)) { + if (le64_to_cpu(oob->rxbuf) != ep->rxbuf.iova || + le32_to_cpu(oob->rxlen) > ep->rxbuf.size) { + ret = -EPROTO; + } else { + dma_rmb(); + data = ep->rxbuf.cpu; + len = le32_to_cpu(oob->rxlen); + } + } + } + } + if (!ret && len > ep->cmd_out_len) + ret = -EOVERFLOW; + if (!ret && len) + memcpy(ep->cmd_out, data, len); + ep->cmd_out_len = ret ? 0 : len; + ep->cmd_ret = ret; + ep->cmd_waiting = false; + complete(&ep->cmd_done); +} + +static void dcpib_rx_work(struct work_struct *work) +{ + struct dcpib_ep *ep = container_of(work, struct dcpib_ep, rx_work); + struct dcpib_ring *rb = &ep->rx; + + if (!rb->ready) + return; + + for (;;) { + const struct afkv2_hdr *hdr; + const struct afkv2_msg *msg; + struct afk_qe *qe; + u32 rptr, wptr, qsize; + + dma_rmb(); + rptr = le32_to_cpu(READ_ONCE(*rb->rptr)); + wptr = le32_to_cpu(READ_ONCE(*rb->wptr)); + if (rptr == wptr) + break; + if (rptr >= rb->bufsz || rptr % rb->block) + goto corrupt; + + qe = rb->buf + rptr; + qsize = le32_to_cpu(qe->size); + if (le32_to_cpu(qe->magic) != QE_MAGIC || + qsize > rb->bufsz - sizeof(*qe)) + goto corrupt; + /* An entry that does not fit before the end is at offset 0. */ + if (sizeof(*qe) + qsize > rb->bufsz - rptr) { + rptr = 0; + qe = rb->buf; + qsize = le32_to_cpu(qe->size); + if (le32_to_cpu(qe->magic) != QE_MAGIC || + qsize > rb->bufsz - sizeof(*qe)) + goto corrupt; + } + + hdr = (const void *)qe->data; + msg = (const void *)(hdr + 1); + if (!qe->channel && !qe->type && + qsize >= sizeof(*hdr) + sizeof(*msg) && + le32_to_cpu(hdr->length) == qsize - sizeof(*hdr)) { + u16 interface = le16_to_cpu(hdr->interface); + const void *payload = msg + 1; + size_t size = qsize - sizeof(*hdr) - sizeof(*msg); + + if (msg->category == AFKV2_REPORT && + msg->type == AFKV2_PUBLISH) + dcpib_announce(ep, interface, payload, size); + else if (msg->category == AFKV2_RESPONSE) + dcpib_response(ep, interface, msg, payload, + size); + else if (msg->category == AFKV2_COMMAND) + dcpib_callback(ep, interface, msg, payload, + size); + else + dev_dbg(ep->dcp->dev, "AFK[%02x]: if %u cat %u type %#x: %*ph\n", + ep->ep, interface, msg->category, msg->type, + (int)min_t(size_t, size, 96), payload); + } else { + dev_dbg(ep->dcp->dev, "AFK[%02x]: ignoring message\n", + ep->ep); + } + + dma_mb(); + rptr = ALIGN(rptr + sizeof(*qe) + qsize, rb->block); + if (rptr >= rb->bufsz) + rptr = 0; + WRITE_ONCE(*rb->rptr, cpu_to_le32(rptr)); + } + return; + +corrupt: + dev_err(ep->dcp->dev, "AFK[%02x]: corrupt RX ring, stopping it\n", + ep->ep); + rb->ready = false; +} + +void dcpib_ep_recv(struct dcpib_ep *ep, u64 message) +{ + u32 type = FIELD_GET(RBEP_TYPE, message); + u64 base, size; + int ret; + + switch (type) { + case RBEP_INIT_ACK: + break; + case RBEP_GETBUF: + size = FIELD_GET(GETBUF_SIZE, message) << BLOCK_SHIFT; + ep->tag = FIELD_GET(GETBUF_TAG, message); + if (ep->ring.cpu || dcpib_dma_alloc(ep->dcp, &ep->ring, size)) { + dev_err(ep->dcp->dev, "AFK[%02x]: no ring buffer\n", + ep->ep); + break; + } + dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_GETBUF_ACK) | + FIELD_PREP(GETBUF_ACK_DVA, ep->ring.iova)); + break; + case RBEP_INIT_TX: + case RBEP_INIT_RX: + base = FIELD_GET(INITRB_OFFSET, message) << BLOCK_SHIFT; + size = FIELD_GET(INITRB_SIZE, message) << BLOCK_SHIFT; + if (FIELD_GET(INITRB_TAG, message) != ep->tag) { + dev_err(ep->dcp->dev, "AFK[%02x]: wrong ring tag\n", + ep->ep); + break; + } + ret = dcpib_ring_init(ep, type == RBEP_INIT_RX ? &ep->rx : &ep->tx, + base, size); + if (ret) { + dev_err(ep->dcp->dev, "AFK[%02x]: bad ring %#llx+%#llx\n", + ep->ep, base, size); + break; + } + if (ep->rx.ready && ep->tx.ready) + dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_START)); + break; + case RBEP_START_ACK: + complete_all(&ep->started); + break; + case RBEP_RECV: + queue_work(ep->dcp->wq, &ep->rx_work); + break; + case RBEP_SHUTDOWN_ACK: + complete_all(&ep->stopped); + break; + default: + dev_warn(ep->dcp->dev, "AFK[%02x]: unknown message %#llx\n", + ep->ep, message); + break; + } +} + +struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 epnum, + const struct dcpib_service_ops *ops, + unsigned int expected) +{ + struct dcpib_ep *ep; + int ret; + + if (epnum < DCPIB_FIRST_EP || epnum >= DCPIB_FIRST_EP + DCPIB_NR_EPS) + return ERR_PTR(-EINVAL); + + ep = devm_kzalloc(dcp->dev, sizeof(*ep), GFP_KERNEL); + if (!ep) + return ERR_PTR(-ENOMEM); + ep->dcp = dcp; + ep->ep = epnum; + ep->ops = ops; + ep->expected = expected; + init_completion(&ep->started); + init_completion(&ep->stopped); + init_completion(&ep->services_done); + init_completion(&ep->cmd_done); + mutex_init(&ep->tx_lock); + mutex_init(&ep->cmd_lock); + INIT_WORK(&ep->rx_work, dcpib_rx_work); + + ret = dcpib_dma_alloc(dcp, &ep->txbuf, DCPIB_BUF_SIZE); + if (!ret) + ret = dcpib_dma_alloc(dcp, &ep->rxbuf, DCPIB_BUF_SIZE); + if (ret) + goto err; + + dcp->eps[epnum - DCPIB_FIRST_EP] = ep; + apple_rtkit_adopt_ep(dcp->rtk, epnum); + /* Aura leaves these sessions running with its scanout. INIT while the + * messenger is active asserts in firmware. Stop only an explicitly + * advertised live endpoint and require its acknowledgment before INIT. + * Its old buffers remain reserved by the bootloader throughout. */ + if (dcp->inherited_afk_endpoints & BIT(epnum - DCPIB_FIRST_EP)) { + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SHUTDOWN)); + if (ret) + goto err_unregister; + if (!wait_for_completion_timeout(&ep->stopped, DCPIB_TIMEOUT)) { + dev_err(dcp->dev, "inherited endpoint %#x did not stop\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + dcp->inherited_afk_endpoints &= ~BIT(epnum - DCPIB_FIRST_EP); + dev_info(dcp->dev, "inherited endpoint %#x stopped\n", epnum); + } + /* + * START_EP for an application endpoint of the adopted session; the + * bootloader consumed the endpoint map, so apple_rtkit_start_ep() + * does not know it. + */ + ret = apple_rtkit_send_message(dcp->rtk, 0, + FIELD_PREP(GENMASK_ULL(59, 52), 5) | + FIELD_PREP(GENMASK_ULL(39, 32), epnum) | + BIT_ULL(1), NULL, false); + if (ret) { + dev_err(dcp->dev, "failed to start endpoint %#x: %d\n", epnum, ret); + goto err_unregister; + } + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_INIT)); + if (ret) + goto err_unregister; + if (!wait_for_completion_timeout(&ep->started, DCPIB_TIMEOUT)) { + dev_err(dcp->dev, "endpoint %#x did not start\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + if (!wait_for_completion_timeout(&ep->services_done, DCPIB_TIMEOUT) && + !ep->nr_services) { + dev_err(dcp->dev, "endpoint %#x announced no service\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + if (ep->nr_services < expected) + dev_warn(dcp->dev, "endpoint %#x: %u of %u services\n", epnum, + ep->nr_services, expected); + return ep; + +err_unregister: + /* The firmware may still own the rings; keep them allocated. */ + return ERR_PTR(ret); +err: + dcpib_dma_free(dcp, &ep->txbuf); + dcpib_dma_free(dcp, &ep->rxbuf); + return ERR_PTR(ret); +} + +void dcpib_ep_stop(struct dcpib_ep *ep) +{ + reinit_completion(&ep->stopped); + if (dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SHUTDOWN)) || + !wait_for_completion_timeout(&ep->stopped, DCPIB_TIMEOUT)) { + dev_err(ep->dcp->dev, "endpoint %#x did not stop\n", ep->ep); + return; + } + ep->rx.ready = ep->tx.ready = false; + flush_work(&ep->rx_work); +} + +int dcpib_command(struct dcpib_service *service, u8 type, const void *tx, + size_t txlen, void *rx, size_t *rxlen) +{ + struct dcpib_ep *ep = service->ep; + size_t cap = rxlen ? *rxlen : 0; + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + struct afkv2_cmd cmd; + struct afkv2_oob oob; + } __packed packet = { + .msg = { .type = type, .category = AFKV2_COMMAND }, + }; + int ret; + + if (txlen > ep->txbuf.size || cap > ep->rxbuf.size) + return -EINVAL; + + mutex_lock(&ep->cmd_lock); + memcpy(ep->txbuf.cpu, tx, txlen); + packet.cmd.flags = 1; + packet.cmd.id = ep->next_cmd_id++; + packet.cmd.value = cpu_to_le32(cap); + packet.oob.rxbuf = cpu_to_le64(cap ? ep->rxbuf.iova : 0); + packet.oob.txbuf = cpu_to_le64(ep->txbuf.iova); + packet.oob.rxlen = cpu_to_le32(cap); + packet.oob.txlen = cpu_to_le32(txlen); + + reinit_completion(&ep->cmd_done); + ep->cmd_interface = service->interface; + ep->cmd_type = type; + ep->cmd_id = packet.cmd.id; + ep->cmd_out = rx; + ep->cmd_out_len = cap; + ep->cmd_ret = -ETIMEDOUT; + WRITE_ONCE(ep->cmd_waiting, true); + + ret = dcpib_tx_v2(ep, service->interface, &packet, sizeof(packet)); + if (!ret && !wait_for_completion_timeout(&ep->cmd_done, DCPIB_TIMEOUT)) + ret = -ETIMEDOUT; + if (!ret) + ret = ep->cmd_ret; + if (!ret && rxlen) + *rxlen = ep->cmd_out_len; + WRITE_ONCE(ep->cmd_waiting, false); + /* A late response must not land in the caller's buffer. */ + flush_work(&ep->rx_work); + mutex_unlock(&ep->cmd_lock); + + if (ret) + dev_dbg(ep->dcp->dev, "AFK[%02x]: command %#x on %s: %d\n", + ep->ep, type, service->name, ret); + return ret; +} + +/* An AP-initiated standard service call (aura afk_service_call). */ +int dcpib_service_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_len, size_t data_pad, + void *output, size_t output_len, size_t output_pad) +{ + struct afkv2_service_call *call; + size_t len = max(data_len + data_pad, output_len + output_pad) + + sizeof(*call); + size_t rxlen = len; + u8 *buf; + int ret; + + buf = kzalloc(len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + call = (void *)buf; + call->group = cpu_to_le16(group); + call->command = cpu_to_le32(command); + call->length = cpu_to_le32(data_len + data_pad); + call->magic = cpu_to_le32(SERVICE_CALL_MAGIC); + if (data_len) + memcpy(call + 1, data, data_len); + + ret = dcpib_command(service, AFKV2_STD_SERVICE, buf, len, buf, &rxlen); + if (ret) + goto out; + if (rxlen < sizeof(*call) || + le32_to_cpu(call->magic) != SERVICE_CALL_MAGIC || + le16_to_cpu(call->group) != group || + le32_to_cpu(call->command) != command || + le32_to_cpu(call->length) > rxlen - sizeof(*call) || + le32_to_cpu(call->length) < output_len) { + ret = -EPROTO; + goto out; + } + if (output && output_len) + memcpy(output, call + 1, output_len); +out: + kfree(buf); + return ret; +} diff --git a/drivers/gpu/drm/apple_dcpib/atc.c b/drivers/gpu/drm/apple_dcpib/atc.c new file mode 100644 index 0000000000000..a284a4ca195f9 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/atc.c @@ -0,0 +1,789 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * T8152/J873g ATC DisplayPort PHYs for the DCP's DPTX port (from aura + * src/dcp/atc_phy.c, which drives ATC3). + * + * The bootloader trains the USB-C display before Linux starts; this driver + * takes that link over and then follows the firmware: link rate changes + * (stop and set up the lanes again), drive settings, the DCP0 -> ATCn/DFP0 + * display crossbar route, and with the display off or unplugged a full + * power down and a cold start. The DCP drives one USB-C port at a time: + * the one a sink is plugged into (each port's PD controller is polled). + */ + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define ATC_AON 0x1e00 +#define ATC_CFG 0x60000 +#define ATC_XBAR 0x74000 +#define ATC_TIMEOUT_US 10000 + +struct dcpib_atc_port { + void __iomem *base; + unsigned int index; /* ATC instance */ + /* display-crossbar0 DFP endpoint of this ATC */ + u32 dfp; + struct spmi_device *pd; + /* AP DisplayPort AUX controller aux0 (shared), for the EDID */ + struct dcpib_aux *aux; + /* PMGR power states of the four ATC dependencies (aura gate_addr) */ + void __iomem *ps; + void __iomem *psd; + /* the dependencies this driver powered up, and may power down */ + u8 owned; + /* {offset, mask, value} tunables from the ADT, before/after xbar config */ + const __be32 *tunable[2]; + int tunable_len[2]; + /* PMGR DP clock control (shared): the divider selects the link rate */ + void __iomem *dp_clock; + bool swap; + bool active; + bool route_started; + u32 link_rate; +}; + +struct dcpib_atc { + struct dcpib_aux aux; + struct dcpib_atc_port port[4]; + unsigned int nports; + /* the port the DCP drives, and the one a sink was found on */ + struct dcpib_atc_port *cur, *next; +}; + +static struct dcpib_atc_port *atc_cur(struct dcpib *dcp) +{ + return ((struct dcpib_atc *)dcp->phy_priv)->cur; +} + +static const u32 atc_rates[] = { + DCPIB_LINK_RATE_RBR, DCPIB_LINK_RATE_HBR, DCPIB_LINK_RATE_HBR2, + DCPIB_LINK_RATE_HBR3, +}; + +static void atc_update(struct dcpib_atc_port *atc, u32 offset, u32 mask, u32 value) +{ + u32 reg = readl(atc->base + offset); + + writel((reg & ~mask) | value, atc->base + offset); +} + +static int atc_poll(struct dcpib_atc_port *atc, u32 offset, u32 mask, u32 value) +{ + u32 reg; + + return readl_poll_timeout(atc->base + offset, reg, + (reg & mask) == value, 1, ATC_TIMEOUT_US); +} + +static int atc_route_stop(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + if (!atc->route_started) + return 0; + atc_update(atc, ATC_XBAR + 0x3c, 1, 0); + atc_update(atc, ATC_XBAR, 1, 0); + atc_update(atc, ATC_XBAR + 0xc, 1, 0); + atc_update(atc, ATC_XBAR + 0x24, 0x100, 0); + udelay(1); + /* + * With the pipe powered off (the bootloader hands the display over + * that way) nothing acknowledges the stop; the route is idle then, so + * finish it anyway. + */ + if (atc_poll(atc, ATC_XBAR + 0x800, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x808, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x81c, 0x100, 0)) + dev_info(dcp->dev, "ATC%u: no crossbar stop acknowledgement\n", atc->index); + atc_update(atc, ATC_XBAR + 8, 1, 0); + atc_update(atc, ATC_XBAR + 0x20, 7, 0); + atc_update(atc, ATC_XBAR + 0x38, 0x700, 0); + atc_update(atc, ATC_XBAR + 4, 1, 1); + atc_update(atc, ATC_XBAR + 0x1c, 1, 1); + atc_update(atc, ATC_XBAR + 0x34, 0x100, 0x100); + atc->route_started = false; + dev_dbg(dcp->dev, "ATC%u: display route disabled\n", atc->index); + return 0; +} + +/* + * A source -> ATCn/DFP0 route, PCLK1: both DFP0 mux fields select the + * source (AppleT8142ATCDPXBAR::connect): DCP0 or an AP AUX controller. + */ +#define ATC_SEL_DCP0 0 +#define ATC_SEL_AUX0 9 /* UFP endpoint 5 + 4 (getMuxSelector) */ + +static int atc_route_start_sel(struct dcpib *dcp, struct dcpib_atc_port *atc, + u32 sel) +{ + if (atc->route_started) + return 0; + atc_update(atc, ATC_XBAR + 0x44, 0x00f00000, sel << 20); + atc_update(atc, ATC_XBAR + 0x44, 0x00000f00, sel << 8); + atc_update(atc, ATC_XBAR + 4, 1, 0); + atc_update(atc, ATC_XBAR + 0x1c, 1, 0); + atc_update(atc, ATC_XBAR + 0x34, 0x100, 0); + udelay(1); + if (atc_poll(atc, ATC_XBAR + 0x804, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x818, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x82c, 0x100, 0)) { + dev_err(dcp->dev, "ATC%u: crossbar reset acknowledgement timeout\n", atc->index); + return -ETIMEDOUT; + } + atc_update(atc, ATC_XBAR + 8, 1, 1); + atc_update(atc, ATC_XBAR + 0x20, 7, 1); + atc_update(atc, ATC_XBAR + 0x38, 0x700, 0x100); + atc_update(atc, ATC_XBAR + 0x48, 1, 0); + atc_update(atc, ATC_XBAR, 1, 1); + atc_update(atc, ATC_XBAR + 0xc, 1, 1); + atc_update(atc, ATC_XBAR + 0x24, 0x100, 0x100); + atc_update(atc, ATC_XBAR + 0x4c, 0x100, 0x100); + atc_update(atc, ATC_XBAR + 0x3c, 1, 1); + atc->route_started = true; + dev_dbg(dcp->dev, "ATC%u: source %u to local DFP0 route configured\n", atc->index, sel); + return 0; +} + +static int atc_route_start(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + return atc_route_start_sel(dcp, atc, ATC_SEL_DCP0); +} + +/* Common four-lane setup; the rate-dependent fields follow in atc_dp_start. */ +static const struct { + u32 offset, mask, value; +} atc_dp_setup[] = { + { 0x2044, 0xffff, 0x4b0 }, + { 0x4c, 0x3c0000, 0x3c0000 }, + { 0x4c80, 0x40, 0x40 }, + { 0xdc80, 0x40, 0x40 }, + { 0x4fd4, 0x2000000, 0x2000000 }, + { 0x4fd4, 0x1800000, 0x800000 }, + { 0xdfd4, 0x2000000, 0x2000000 }, + { 0xdfd4, 0x1800000, 0x800000 }, + { 0x4d84, 0x1c0000, 0 }, + { 0x4fd4, 1, 0 }, + { 0x4fd4, 8, 0 }, + { 0x4fd4, 2, 2 }, + { 0x4fd4, 0x10, 0x10 }, + { 0x8904, 0x1c0000, 0 }, + { 0x8800, 0x10000000, 0 }, + { 0x8800, 0x80000000, 0 }, + { 0x8800, 0x20000000, 0x20000000 }, + { 0x8804, 1, 1 }, + { 0xdd84, 0x1c0000, 0 }, + { 0xdfd4, 1, 0 }, + { 0xdfd4, 8, 0 }, + { 0xdfd4, 2, 2 }, + { 0xdfd4, 0x10, 0x10 }, + { 0x11904, 0x1c0000, 0 }, + { 0x11800, 0x10000000, 0 }, + { 0x11800, 0x80000000, 0 }, + { 0x11800, 0x20000000, 0x20000000 }, + { 0x11804, 1, 1 }, + { 0x3c04, 2, 2 }, + { 0x3c04, 1, 1 }, + { 0xcc04, 2, 2 }, + { 0xcc04, 1, 1 }, +}; + +/* Program the PMGR DP clock divider and wait for the change to settle. */ +static int atc_clock_write(struct dcpib_atc_port *atc, u32 divider) +{ + u32 reg = readl(atc->dp_clock); + + writel((reg & ~0x0f000000) | (divider << 24), atc->dp_clock); + return readl_poll_timeout(atc->dp_clock, reg, !(reg & BIT(30)), 1, + ATC_TIMEOUT_US); +} + +/* Stop the DP lanes: route, shim clock, then both PMAs (aura atc_dp_stop). */ +static int atc_dp_stop(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + u32 reg; + + atc_route_stop(dcp, atc); + atc_update(atc, ATC_CFG + 0x14, 7, 7); + atc_update(atc, ATC_CFG + 0x18, 1, 0); + if (readl_poll_timeout(atc->base + ATC_CFG + 0x1c, reg, !(reg & 1), 1, + ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: DPTX clock stop failed\n", atc->index); + return -ETIMEDOUT; + } + atc_update(atc, 0x2040, BIT(16), 0); + udelay(5); + atc_update(atc, 0x2000, BIT(15), 0); + atc_update(atc, 0x2000, BIT(13), 0); + atc_update(atc, 0x2000, BIT(14), 0); + atc_update(atc, 0x2000, BIT(2), 0); + atc_update(atc, 0x2000, BIT(3), BIT(3)); + atc_update(atc, 0xe10, BIT(2), BIT(2)); + atc_update(atc, 0xe08, BIT(29), BIT(29)); + /* Remove both PMA clock requests, then clamp and power down both lanes. */ + atc_update(atc, ATC_AON + 0x24, BIT(12) | BIT(16) | BIT(14) | BIT(18), 0); + udelay(1); + for (u32 offset = 0x1e18; offset <= 0x1e1c; offset += 4) { + atc_update(atc, offset, BIT(5), 0); + udelay(1); + atc_update(atc, offset, BIT(0), BIT(0)); + udelay(1); + atc_update(atc, offset, BIT(2), 0); + udelay(1); + atc_update(atc, offset, BIT(1), 0); + udelay(1); + } + atc->link_rate = 0; + return 0; +} + +/* Four-lane DP setup at rate index r (aura atc_phy_set_link_rate). */ +static int atc_dp_start(struct dcpib *dcp, struct dcpib_atc_port *atc, u32 r) +{ + static const u32 tx_ctrl[] = { 0x4eb0, 0x8b10, 0xdeb0, 0x11b10 }; + static const u32 wake_count[] = { 0xbb5, 0xbb7, 0xbb8, 0xbb9 }; + int ret; + + if (readl(atc->dp_clock) & BIT(30)) + return -EBUSY; + atc_update(atc, ATC_CFG + 0x14, 7, 0); + atc_update(atc, ATC_CFG + 0x18, 1, 1); + if (!r) { + atc_update(atc, 0xe10, BIT(2), BIT(2)); + atc_update(atc, 0xe08, BIT(29), BIT(29)); + } + for (u32 i = 0; i < 2; i++) { + u32 offset = 0x1e18 + 4 * i; + + atc_update(atc, ATC_AON + 0x24, BIT(12 + 2 * i), BIT(12 + 2 * i)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(16 + 2 * i), BIT(16 + 2 * i)); + udelay(1); + atc_update(atc, offset, BIT(1), BIT(1)); + udelay(1); + atc_update(atc, offset, BIT(2), BIT(2)); + udelay(1); + atc_update(atc, offset, BIT(0), 0); + udelay(1); + atc_update(atc, offset, BIT(5), BIT(5)); + udelay(2); + } + atc_update(atc, 0x2000, 0x380000, r << 19); + for (unsigned int i = 0; i < ARRAY_SIZE(atc_dp_setup); i++) + atc_update(atc, atc_dp_setup[i].offset, atc_dp_setup[i].mask, + atc_dp_setup[i].value); + for (u32 i = 0; i < 2; i++) { + atc_update(atc, 0x8684 + i * 0x9000, 3, r ? 0 : 1); + atc_update(atc, 0x4c80 + i * 0x9000, 0x30, r ? 0 : 0x10); + } + for (unsigned int i = 0; i < ARRAY_SIZE(tx_ctrl); i++) { + atc_update(atc, tx_ctrl[i], 0x7e07ff, 1); + atc_update(atc, tx_ctrl[i] - 0x14, 0x7f, r < 2 ? 0x65 : 0x40); + } + atc_update(atc, 0x2000, 0x70, 0x10); + atc_update(atc, 0x2000, BIT(13), BIT(13)); + atc_update(atc, 0x2044, 0xffff0000, wake_count[r] << 16); + atc_update(atc, atc->swap ? 0x89f0 : 0x119f0, BIT(14), BIT(14)); + atc_update(atc, 0x2000, BIT(2), BIT(2)); + atc_update(atc, 0x2000, BIT(3), BIT(3)); + atc_update(atc, 0x2040, BIT(16), BIT(16)); + + ret = atc_clock_write(atc, 10 - r); + if (ret) { + dev_err(dcp->dev, "ATC%u: DP clock change timeout\n", atc->index); + return ret; + } + ret = atc_route_start(dcp, atc); + if (ret) + return ret; + atc->link_rate = atc_rates[r]; + dev_info(dcp->dev, "ATC%u: four-lane DPTX setup at rate %#x\n", atc->index, + atc->link_rate); + return 0; +} + +/* + * The power dependencies, in order: the ATC's USB AON (PS +0x100), its common + * PSD, then its two DP PS registers (+0, +0x20), with the values they idle at + * (ATC3: PS 0x1c0, PSD 58, PS 0xc0, 0xe0). + */ +#define ATC_PSD_TARGET 0x90 +#define ATC_PSD_STATE 0x80 +static const u32 atc_ps_offset[] = { 0x100, 0, 0x0, 0x20 }; +static const u32 atc_ps_idle[] = { 0x300, 6, 0x244, 0x244 }; + +static int atc_psd_wait(struct dcpib_atc_port *atc, u32 state) +{ + u32 reg; + + /* + * Only the state: the acknowledge bit aura also waits for stays clear + * when Linux switches the PSD, in either direction. + */ + return readl_poll_timeout(atc->psd + ATC_PSD_STATE, reg, + (reg & 7) == state, 1, ATC_TIMEOUT_US); +} + +static int atc_gate(struct dcpib_atc_port *atc, unsigned int i, bool on) +{ + u32 value = on ? (atc_ps_idle[i] & 0xfffffcf0) | 15 : atc_ps_idle[i]; + u32 reg; + + /* Already up (the bootloader's or an earlier activation's). */ + if (on && (i == 1 ? !(readl(atc->psd + ATC_PSD_STATE) & 7) : + (readl(atc->ps + atc_ps_offset[i]) & 0xf0) == 0xf0)) + return 0; + if (on) + atc->owned |= BIT(i); + else + atc->owned &= ~BIT(i); + if (i == 1) { + writel(on ? 0 : atc_ps_idle[i], atc->psd + ATC_PSD_TARGET); + return atc_psd_wait(atc, on ? 0 : atc_ps_idle[i]); + } + writel(value, atc->ps + atc_ps_offset[i]); + return readl_poll_timeout(atc->ps + atc_ps_offset[i], reg, + (reg & 0xf0) == ((value & 15) << 4), 1, + ATC_TIMEOUT_US); +} + +static void atc_apply_tunables(struct dcpib_atc_port *atc, unsigned int which) +{ + const __be32 *t = atc->tunable[which]; + + for (int i = 0; i + 3 <= atc->tunable_len[which]; i += 3) + atc_update(atc, be32_to_cpu(t[i]), be32_to_cpu(t[i + 1]), + be32_to_cpu(t[i + 2])); +} + +/* + * Cold start: the power dependencies, the common block and AUX (aura + * atc_gates_enable and atc_phy_activate), then the DP lanes at HBR. + */ +static int atc_activate(struct dcpib *dcp) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + void __iomem *cfg = atc->base + ATC_CFG; + unsigned int gates = 0; + u32 reg; + + if (atc->active) + return 0; + if (!atc->tunable[0] || !atc->tunable[1]) + return -EOPNOTSUPP; + for (; gates < ARRAY_SIZE(atc_ps_idle); gates++) + if (atc_gate(atc, gates, true)) + goto fail; + if (readl(cfg) != 4 || (readl(cfg + 4) & 3) || readl(cfg + 0x30)) + goto fail; + writel(readl(cfg + 0x30) | BIT(0), cfg + 0x30); + udelay(1); + writel(readl(cfg) | BIT(0), cfg); + if (readl_poll_timeout(cfg + 4, reg, reg & BIT(0), 1, ATC_TIMEOUT_US)) + goto fail; + writel(readl(cfg) | BIT(1), cfg); + if (readl_poll_timeout(cfg + 4, reg, reg & BIT(1), 1, ATC_TIMEOUT_US)) + goto fail; + writel(readl(cfg) & ~BIT(2), cfg); + udelay(1); + writel(readl(cfg) | BIT(3), cfg); + + atc_apply_tunables(atc, 0); + /* aciophy_xbar_config(mode=3): both PMAs carry DisplayPort. */ + atc_update(atc, 0x4c, 0x1f, 0x14); + atc_update(atc, 0x4c, 0x3ffe0, atc->swap ? 0x100 : 0x22000); + atc_apply_tunables(atc, 1); + atc_update(atc, ATC_CFG + 8, BIT(1), atc->swap ? BIT(1) : 0); + writel(readl(cfg) | BIT(4), cfg); + if ((readl(cfg) & 0x1f) != 0x1b) + goto fail; + /* aciophy_aux_init: release small/big sleep, then the AUX clamp. */ + atc_update(atc, ATC_AON + 0x24, BIT(8), BIT(8)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(6), BIT(6)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(10), 0); + udelay(1); + atc->active = true; + dev_info(dcp->dev, "ATC%u: common PHY and AUX up from cold\n", atc->index); + /* + * As aura's activation: the lanes start at HBR right away. From cold, + * AUX only works with the DP PLL and PMAs running (without them the + * firmware reports DEVICE_NOT_RESPONDING); the firmware switches to + * its rate afterwards. + */ + return atc_dp_start(dcp, atc, 1); +fail: + dev_err(dcp->dev, "ATC%u: cold start failed (gates %u, cfg %#x/%#x)\n", atc->index, + gates, gates == ARRAY_SIZE(atc_ps_idle) ? readl(cfg) : 0, + gates == ARRAY_SIZE(atc_ps_idle) ? readl(cfg + 4) : 0); + return -EIO; +} + +/* Power the PHY down (aura atc_phy_deactivate), dependencies included. */ +static int atc_deactivate(struct dcpib *dcp) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + void __iomem *cfg = atc->base + ATC_CFG; + int ret = 0; + u32 reg; + + if (!atc->active) + return 0; + if (atc->link_rate && atc_dp_stop(dcp, atc)) { + dev_err(dcp->dev, "ATC%u: DP stop failed\n", atc->index); + return -EIO; + } + atc_route_stop(dcp, atc); + atc_update(atc, ATC_AON + 0x24, BIT(10), BIT(10)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(6), 0); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(8), 0); + udelay(1); + writel(readl(cfg) & ~BIT(4), cfg); + udelay(1); + writel(readl(cfg) & ~BIT(3), cfg); + writel(readl(cfg) | BIT(2), cfg); + writel(readl(cfg) & ~BIT(1), cfg); + if (readl_poll_timeout(cfg + 4, reg, !(reg & BIT(1)), 1, ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: big-sleep entry timeout\n", atc->index); + ret = -ETIMEDOUT; + } + writel(readl(cfg) & ~BIT(0), cfg); + if (readl_poll_timeout(cfg + 4, reg, !(reg & BIT(0)), 1, ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: small-sleep entry timeout\n", atc->index); + ret = -ETIMEDOUT; + } + writel(readl(cfg + 0x30) & ~BIT(0), cfg + 0x30); + atc->active = false; + /* + * Then the power dependencies it powered up: the next activation starts + * it cold. The USB controller of a port keeps the ones it uses. + */ + for (int i = ARRAY_SIZE(atc_ps_idle) - 1; i >= 0; i--) + if ((atc->owned & BIT(i)) && atc_gate(atc, i, false)) + ret = -ETIMEDOUT; + dev_info(dcp->dev, "ATC%u: powered down: %d\n", atc->index, ret); + return ret; +} + +static int atc_set_link_rate(struct dcpib *dcp, u32 rate) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + u32 r; + int ret; + + /* The firmware drops the link (rate 0) before it retrains. */ + if (!rate || rate == atc->link_rate) + return 0; + for (r = 0; r < ARRAY_SIZE(atc_rates); r++) + if (atc_rates[r] == rate) + break; + if (r == ARRAY_SIZE(atc_rates) || !atc->active) + return -EINVAL; + dev_info(dcp->dev, "ATC%u: link rate %#x, was %#x\n", atc->index, rate, atc->link_rate); + if (atc->link_rate) { + ret = atc_dp_stop(dcp, atc); + if (ret) + return ret; + } + return atc_dp_start(dcp, atc, r); +} + +static int atc_set_route(struct dcpib *dcp, bool enable) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + if (!enable) + return atc_route_stop(dcp, atc); + if (!atc->active) + return -ENODEV; + if (atc_route_start(dcp, atc)) + return -EIO; + mdelay(5); + return 0; +} + +/* + * Matching T8152 fallback table, indexed by voltage * 4 + pre-emphasis; the + * ADT has no rate-specific dp-training-table, so it serves every rate. + */ +static const u32 atc_drive_presets[16] = { + 0x025, 0x420, 0x717, 0x90f, 0x019, 0x397, 0x78f, 0x025, + 0x011, 0x38f, 0x025, 0x025, 0x000, 0x025, 0x025, 0x025, +}; + +static int atc_set_drive(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + u32 presets[4]; + + if (!count) + return 0; + if (count > 4 || !atc->active || !atc->link_rate) + return -EINVAL; + /* Validate the entire request before changing any lane. */ + for (u32 i = 0; i < count; i++) { + u32 v = le32_to_cpu(settings[i].voltage); + u32 e = le32_to_cpu(settings[i].pre_emphasis); + + if (settings[i].format || v > 3 || e > 3 || v + e > 3) + return -EINVAL; + presets[i] = atc_drive_presets[v * 4 + e]; + } + for (u32 i = 0; i < count; i++) { + u32 pma = (i / 2) ^ !atc->swap; + /* Even DP lanes use the PMA RX pair, odd lanes its TX pair. */ + u32 offset = ((i & 1) ? 0x4eb0 : 0x8b10) + pma * 0x9000; + u32 preset = presets[i]; + u32 control = 1 | (((preset >> 13) & 0x3f) << 17) | + (((preset >> 7) & 0x3f) << 5); + + atc_update(atc, offset, 0x7e07ff, control); + atc_update(atc, offset - 0x14, 0x7f, (preset & 0x3f) | 0x40); + } + return 0; +} + +static void atc_set_orientation(struct dcpib *dcp, bool flipped) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + /* Applied by the next cold start; a running PHY keeps its own. */ + if (atc->active && atc->swap != flipped) + dev_warn(dcp->dev, "ATC%u: orientation changed while active\n", atc->index); + atc->swap = flipped; +} + +/* + * The sink's EDID through the AP AUX controller aux0, which reaches the ATC + * through crossbar UFP aux0 like the DCP: the DCP's route comes down for + * the read, so this runs while the firmware does no AUX itself (display + * off, HPD not asserted yet). + */ +static int atc_read_edid(struct dcpib *dcp, u8 *buf, size_t size) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + bool was_started = atc->route_started; + int ret; + + if (!atc->active || !atc->aux->regs) + return -ENODEV; + ret = dcpib_aux_init(dcp, atc->aux); + if (ret) + return ret; + atc_route_stop(dcp, atc); + ret = atc_route_start_sel(dcp, atc, ATC_SEL_AUX0); + if (!ret) + ret = dcpib_aux_read_edid(dcp, atc->aux, buf, size); + atc_route_stop(dcp, atc); + if (was_started) + atc_route_start(dcp, atc); + return ret; +} + +/* + * 1 with a sink on one of the ports: the current one first, then the + * others; select() switches to a port found that way. + */ +static int atc_detect(struct dcpib *dcp, bool *flipped) +{ + struct dcpib_atc *atc = dcp->phy_priv; + int ret = dcpib_pd_sink(atc->cur->pd, flipped); + + atc->next = atc->cur; + if (ret) + return ret; + for (unsigned int i = 0; i < atc->nports; i++) { + struct dcpib_atc_port *port = &atc->port[i]; + + if (port == atc->cur || dcpib_pd_sink(port->pd, flipped) <= 0) + continue; + atc->next = port; + /* A plug the other port's HPD state does not show yet. */ + dcp->hpd = 0; + return 1; + } + return 0; +} + +/* + * Drive the port a sink was found on: power the old one down and connect + * the DPTX port to the new one's display crossbar endpoint. + */ +static void atc_select(struct dcpib *dcp) +{ + struct dcpib_atc *atc = dcp->phy_priv; + struct dcpib_atc_port *port = atc->next; + + if (!port || port == atc->cur) + return; + if (atc->cur->active) + atc_deactivate(dcp); + dev_info(dcp->dev, "USB-C display on ATC%u now, was ATC%u\n", + port->index, atc->cur->index); + atc->cur = port; + dcp->pd = port->pd; + dcp->dcp_output = port->dfp; + if (dcpib_dptx_connect(dcp, 0)) + dev_err(dcp->dev, "DPTX connect to ATC%u failed\n", port->index); +} + +static const struct dcpib_phy_ops atc_ops = { + .activate = atc_activate, + .deactivate = atc_deactivate, + .set_link_rate = atc_set_link_rate, + .set_drive = atc_set_drive, + .set_route = atc_set_route, + .set_orientation = atc_set_orientation, + .read_edid = atc_read_edid, + .detect = atc_detect, + .select = atc_select, + .max_link_rate = DCPIB_LINK_RATE_HBR2, + /* + * Modes up to 1920x1080@60 and 1600x1200@60 (and 2560x1440@30) show, + * 1920x1200@60 and anything faster stay black at HBR2 with no error: + * the DCP's pixel clock on the ATC path tops out between 162 and + * about 190 MHz with the firmware on its iBoot interface. + */ + .max_pixel_rate = 130000000, + /* + * 720x480@60 and 720x576@50 (27 MHz) show, 640x480@60 (25.2 MHz) + * stays black here while HDMI shows it. + */ + .min_pixel_rate = 20000000, +}; + +/* PMGR registers, inside blocks their power domain driver has claimed. */ +static void __iomem *atc_map(struct dcpib *dcp, struct device_node *np, + const char *name) +{ + int i = of_property_match_string(np, "reg-names", name); + struct resource res; + + if (i < 0 || of_address_to_resource(np, i, &res)) + return NULL; + return devm_ioremap(dcp->dev, res.start, resource_size(&res)); +} + +static int atc_port_init(struct dcpib *dcp, struct dcpib_atc *atc, + struct device_node *np, void __iomem *dp_clock) +{ + struct dcpib_atc_port *port = &atc->port[atc->nports]; + u32 cfg, r; + + port->base = atc_map(dcp, np, "atc"); + port->ps = atc_map(dcp, np, "atc-ps"); + port->psd = atc_map(dcp, np, "atc-psd"); + if (!port->base || !port->ps || !port->psd) + return -ENOMEM; + port->pd = dcpib_pd_get(dcp, np); + if (IS_ERR_OR_NULL(port->pd)) + return port->pd ? PTR_ERR(port->pd) : -ENODEV; + /* The node without ports is ATC3's (J873g back-right). */ + port->dfp = 3; + of_property_read_u32(np, "apple,dfp-endpoint", &port->dfp); + port->index = port->dfp; + port->dp_clock = dp_clock; + port->aux = &atc->aux; + port->tunable[0] = of_get_property(np, "apple,tunable-pre", + &port->tunable_len[0]); + port->tunable[1] = of_get_property(np, "apple,tunable-post", + &port->tunable_len[1]); + port->tunable_len[0] /= sizeof(__be32); + port->tunable_len[1] /= sizeof(__be32); + atc->nports++; + + /* + * The PHY block hangs the bus while it is power-gated: look at it only + * with its DP dependencies and the common PSD up. + */ + if ((readl(port->ps) & 0xf0) != 0xf0 || + (readl(port->psd + ATC_PSD_STATE) & 7)) { + dev_info(dcp->dev, "ATC%u: powered down\n", port->index); + return 0; + } + /* Common block out of reset and AUX up, as aura's activation leaves it. */ + cfg = readl(port->base + ATC_CFG); + port->active = (cfg & 0x1f) == 0x1b; + port->swap = (readl(port->base + 0x4c) & 0x3ffe0) == 0x100; + port->route_started = (readl(port->base + ATC_XBAR) & 1) && + (readl(port->base + ATC_XBAR + 0x3c) & 1); + r = (readl(port->base + 0x2000) >> 19) & 7; + if (port->active && (readl(port->base + 0x2040) & BIT(16)) && + r < ARRAY_SIZE(atc_rates)) + port->link_rate = atc_rates[r]; + /* The bootloader's display: its dependencies are the display's. */ + if (port->active) + port->owned = GENMASK(ARRAY_SIZE(atc_ps_idle) - 1, 0); + dev_info(dcp->dev, + "ATC%u: inherited state: active %d, rate %#x, route %d, reversed %d (cfg %#x)\n", + port->index, port->active, port->link_rate, port->route_started, + port->swap, cfg); + return 0; +} + +int dcpib_atc_init(struct dcpib *dcp) +{ + struct device_node *np = dcp->dev->of_node; + void __iomem *dp_clock; + struct dcpib_atc *atc; + bool flipped; + int ret; + + atc = devm_kzalloc(dcp->dev, sizeof(*atc), GFP_KERNEL); + if (!atc) + return -ENOMEM; + dp_clock = atc_map(dcp, np, "dp-clock"); + if (!dp_clock) + return -ENOMEM; + /* AUX output select 9: the crossbar's UFP aux0 */ + dcpib_aux_probe(dcp, &atc->aux, 9); + + /* One child per USB-C port, or the node itself for ATC3 alone. */ + for_each_child_of_node_scoped(np, child) { + if (!of_property_present(child, "apple,dfp-endpoint")) + continue; + if (atc->nports == ARRAY_SIZE(atc->port)) + break; + ret = atc_port_init(dcp, atc, child, dp_clock); + if (ret) + return ret; + } + if (!atc->nports) { + ret = atc_port_init(dcp, atc, np, dp_clock); + if (ret) + return ret; + } + + /* The bootloader's display port, else one with a sink, else the first. */ + atc->cur = &atc->port[0]; + for (unsigned int i = 0; i < atc->nports; i++) { + if (atc->port[i].active) { + atc->cur = &atc->port[i]; + goto found; + } + } + for (unsigned int i = 0; i < atc->nports; i++) { + if (dcpib_pd_sink(atc->port[i].pd, &flipped) > 0) { + atc->cur = &atc->port[i]; + break; + } + } +found: + atc->next = atc->cur; + dcp->phy = &atc_ops; + dcp->phy_priv = atc; + dcp->pd = atc->cur->pd; + dcp->dcp_output = atc->cur->dfp; + dev_info(dcp->dev, "USB-C display on ATC%u (%u ports)\n", atc->cur->index, + atc->nports); + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/aux.c b/drivers/gpu/drm/apple_dcpib/aux.c new file mode 100644 index 0000000000000..c8c6bf9ce3137 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/aux.c @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The AP's DisplayPort AUX controllers aux0/aux1 (ADT dptx0-aux, dptx1-aux; + * macOS AppleAUXDPTX): Analogix-derived DPTX cores with Apple's request + * framing (AppleDPTXController::setupAuxTransaction, readDPCDGated). They + * attach to the display crossbar as UFP endpoints 5 and 6; the phy code + * routes one to its port for the time of a read. Used for the sink's EDID, + * which the DCP firmware does not hand out on its iBoot interface. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "dcpib.h" + +#define AUX_TIMEOUT_US 10000 +#define AUX_SEL 0x000 /* output: 9 crossbar UFP aux0, 0x10 lpdptx phy */ +#define AUX_FUNC_EN_2 0x01c +#define AUX_FUNC_EN_N BIT(2) +#define AUX_HW_RETRY 0x390 +#define AUX_INT_STA 0x3dc +#define AUX_REPLY BIT(1) +#define AUX_ERROR BIT(0) +#define AUX_CH_STA 0x780 +#define AUX_DEFER_CTL 0x788 +#define AUX_RX_COMM 0x78c +#define AUX_BUF_CTL 0x790 +#define AUX_BUF_CLR BIT(9) +#define AUX_BUF_COUNT GENMASK(8, 0) +#define AUX_LEN 0x794 +#define AUX_ADDR_7_0 0x798 +#define AUX_ADDR_15_8 0x79c +#define AUX_CMD_ADDR 0x7a0 /* request in [7:4], address [19:16] */ +#define AUX_CTL 0x7a4 +#define AUX_GO BIT(0) +#define AUX_ADDR_ONLY BIT(1) +#define AUX_DATA 0xc00 /* one byte per word */ +#define AUX_MAX 16 + +#define AUX_NATIVE_WRITE 0x80 +#define AUX_I2C_WRITE 0x00 +#define AUX_I2C_READ 0x10 +#define AUX_I2C_MOT 0x40 +#define DDC_ADDR 0x50 +#define DPCD_SET_POWER 0x600 +#define DP_SET_POWER_D0 1 + +static void aux_update(void __iomem *reg, u32 mask, u32 value) +{ + writel((readl(reg) & ~mask) | value, reg); +} + +int dcpib_aux_init(struct dcpib *dcp, struct dcpib_aux *aux) +{ + u32 reg; + + /* PMGR GLOBAL_AUX_CTRLn */ + aux_update(aux->ps, 0xf, 0xf); + if (readl_poll_timeout(aux->ps, reg, ((reg >> 4) & 0xf) == 0xf, 1, + AUX_TIMEOUT_US)) { + dev_err(dcp->dev, "AUX: controller does not power up\n"); + return -ETIMEDOUT; + } + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + aux_update(aux->regs + AUX_FUNC_EN_2, AUX_FUNC_EN_N, AUX_FUNC_EN_N); + udelay(10); + aux_update(aux->regs + AUX_FUNC_EN_2, AUX_FUNC_EN_N, 0); + writel(3 << 8, aux->regs + AUX_HW_RETRY); + writel(BIT(7) | 1, aux->regs + AUX_DEFER_CTL); + aux_update(aux->regs + AUX_SEL, 0x3f, aux->sel); + return 0; +} + +/* One AUX request of up to 16 bytes; returns the bytes transferred. */ +static int aux_xfer(struct dcpib_aux *aux, u32 cmd, u32 addr, u8 *buf, + u32 len, bool write) +{ + u32 sta, n; + int i; + + for (int tries = 0; tries < 7; tries++) { + writel(AUX_BUF_CLR, aux->regs + AUX_BUF_CTL); + writel(len ? len - 1 : 0, aux->regs + AUX_LEN); + writel(addr & 0xff, aux->regs + AUX_ADDR_7_0); + writel((addr >> 8) & 0xff, aux->regs + AUX_ADDR_15_8); + writel((cmd & 0xf0) | ((addr >> 16) & 0xf), aux->regs + AUX_CMD_ADDR); + aux_update(aux->regs + AUX_CTL, AUX_ADDR_ONLY, len ? 0 : AUX_ADDR_ONLY); + for (i = 0; write && i < len; i++) + writel(buf[i], aux->regs + AUX_DATA + 4 * i); + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + aux_update(aux->regs + AUX_CTL, AUX_GO, AUX_GO); + if (readl_poll_timeout(aux->regs + AUX_INT_STA, sta, + sta & (AUX_REPLY | AUX_ERROR), 10, 20000)) + return -ETIMEDOUT; + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + if ((sta & AUX_ERROR) || (readl(aux->regs + AUX_CH_STA) & 0xf)) + return -EIO; + /* [3:2] I2C and [1:0] AUX reply: 0 ACK, 1 NACK, 2 DEFER */ + sta = readl(aux->regs + AUX_RX_COMM); + if (sta & 0x5) + return -ENXIO; + if (sta & 0xa) { + usleep_range(500, 1000); + continue; + } + if (write) + return len; + n = min(readl(aux->regs + AUX_BUF_CTL) & AUX_BUF_COUNT, len); + for (i = 0; i < n; i++) + buf[i] = readl(aux->regs + AUX_DATA + 4 * i); + return n; + } + return -EBUSY; +} + +/* DDC over I2C-over-AUX: offset 0, then 16-byte reads up to @size. */ +static int aux_ddc_read(struct dcpib_aux *aux, u8 *buf, size_t size) +{ + u8 offset = 0; + size_t done = 0; + int ret; + + ret = aux_xfer(aux, AUX_I2C_WRITE | AUX_I2C_MOT, DDC_ADDR, + &offset, 1, true); + while (ret >= 0 && done < size) { + ret = aux_xfer(aux, AUX_I2C_READ | AUX_I2C_MOT, DDC_ADDR, + buf + done, min_t(size_t, size - done, AUX_MAX), + false); + if (ret > 0) + done += ret; + else if (!ret) + ret = -EIO; + } + /* End the I2C transaction with an address-only read without MOT. */ + aux_xfer(aux, AUX_I2C_READ, DDC_ADDR, NULL, 0, false); + return ret < 0 ? ret : done; +} + +/* + * The EDID over DDC, the base block and its extensions; the sink answers + * AUX only some time after the path is up, and a sink just plugged in a + * little later still. + */ +int dcpib_aux_read_edid(struct dcpib *dcp, struct dcpib_aux *aux, u8 *buf, + size_t size) +{ + int ret = -EIO, ext; + + u8 d0 = DP_SET_POWER_D0; + + for (int i = 0; i < 30 && (ret == -EIO || ret == -ETIMEDOUT); i++) { + msleep(i ? 100 : 20); + /* + * A sink left in D3 (after an unplug) NACKs DDC: wake it + * first, as the DP spec asks before AUX use. + */ + ret = aux_xfer(aux, AUX_NATIVE_WRITE, DPCD_SET_POWER, &d0, 1, true); + if (ret < 0) + continue; + usleep_range(1000, 2000); + ret = aux_ddc_read(aux, buf, min_t(size_t, size, EDID_LENGTH)); + } + if (ret != EDID_LENGTH) + return ret < 0 ? ret : -EIO; + ext = min_t(size_t, buf[0x7e], size / EDID_LENGTH - 1); + return ext ? aux_ddc_read(aux, buf, (1 + ext) * EDID_LENGTH) : ret; +} + +/* The optional "aux" and "aux-ps" (PMGR, mapped without claiming) regs. */ +/* The controller named by np's "aux" and "aux-ps" regs (np need not be dcp's). */ +void dcpib_aux_probe_node(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel, + struct device_node *np) +{ + int ps = of_property_match_string(np, "reg-names", "aux-ps"); + int regs = of_property_match_string(np, "reg-names", "aux"); + struct resource res; + + aux->sel = sel; + if (ps < 0 || regs < 0 || of_address_to_resource(np, ps, &res)) + return; + aux->ps = devm_ioremap(dcp->dev, res.start, resource_size(&res)); + aux->regs = devm_of_iomap(dcp->dev, np, regs, NULL); + if (IS_ERR(aux->regs) || !aux->ps) + aux->regs = NULL; +} + +void dcpib_aux_probe(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel) +{ + dcpib_aux_probe_node(dcp, aux, sel, dcp->dev->of_node); +} diff --git a/drivers/gpu/drm/apple_dcpib/dcpib.h b/drivers/gpu/drm/apple_dcpib/dcpib.h new file mode 100644 index 0000000000000..1ee609e4233d0 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/dcpib.h @@ -0,0 +1,347 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Apple DCP display driver on the iBoot interface (T8152). + * + * The T8152 DCP firmware (AppleDCP-1317) offers a small "disp0-service", the + * interface iBoot and aura use, next to the full IOMobileFramebuffer protocol. + * This driver uses only that service, the DPTX port service the firmware + * calls back into for the PHY and the DPAV controller services it expects to + * be opened, all over AFKEPV2. It is a port of aura's src/afk.c, + * src/dcp_iboot.c, src/dcp/dptx_port_ep.c and src/dcp/atc_phy.c. + */ +#ifndef __APPLE_DCPIB_H__ +#define __APPLE_DCPIB_H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct dcpib; +struct dcpib_ep; +struct dcpib_service; +struct spmi_device; +struct drm_edid; + +#define DCPIB_EP_IBOOT 0x23 +#define DCPIB_EP_DPAV 0x24 +#define DCPIB_EP_DPTX 0x2a +#define DCPIB_FIRST_EP 0x20 +#define DCPIB_NR_EPS 0x10 + +#define DCPIB_MAX_SERVICES 8 + +/* IOReturn values the firmware understands. */ +#define KIO_RETURN_SUCCESS 0 +#define KIO_RETURN_ERROR 0xe00002bcU +#define KIO_RETURN_BAD_ARG 0xe00002c2U +#define KIO_RETURN_UNSUPPORTED 0xe00002c7U +#define KIO_RETURN_NOT_READY 0xe00002d8U + +/* DPCD link rates in units of 0.27 Gbps */ +#define DCPIB_LINK_RATE_RBR 0x06 +#define DCPIB_LINK_RATE_HBR 0x0a +#define DCPIB_LINK_RATE_HBR2 0x14 +#define DCPIB_LINK_RATE_HBR3 0x1e + +struct dcpib_dma { + void *cpu; + dma_addr_t iova; + size_t size; +}; + +struct dcpib_service_ops { + /* Firmware-initiated standard service call; returns an IOReturn. */ + u32 (*call)(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_size, void *reply, + size_t reply_size); +}; + +struct dcpib_service { + struct dcpib_ep *ep; + const struct dcpib_service_ops *ops; + char name[32]; + u16 interface; + unsigned int unit; + void *cookie; +}; + +struct dcpib_ring { + bool ready; + u32 *rptr; + u32 *wptr; + void *buf; + u32 bufsz; + u32 block; +}; + +struct dcpib_ep { + struct dcpib *dcp; + u8 ep; + const struct dcpib_service_ops *ops; + + struct dcpib_dma ring; + u16 tag; + struct dcpib_ring tx, rx; + struct dcpib_dma txbuf, rxbuf; + + struct completion started; + struct completion stopped; + struct work_struct rx_work; + + /* Serialises TX ring writes and the sequence number. */ + struct mutex tx_lock; + u8 seq; + + struct dcpib_service services[DCPIB_MAX_SERVICES]; + unsigned int nr_services; + unsigned int expected; + struct completion services_done; + + /* One outstanding AP command at a time. */ + struct mutex cmd_lock; + bool cmd_waiting; + u16 cmd_interface; + u8 cmd_type; + u8 cmd_id; + u8 next_cmd_id; + void *cmd_out; + size_t cmd_out_len; + int cmd_ret; + struct completion cmd_done; +}; + +struct dcpib_timing_mode { + __le32 valid; + __le32 width; + __le32 height; + __le32 fps; /* 16.16 */ + u8 pad[8]; +} __packed; + +struct dcpib_color_mode { + __le32 valid; + __le32 colorimetry; + __le32 eotf; + __le32 encoding; + __le32 bpp; + u8 pad[4]; +} __packed; + +struct dcpib_drive_settings { + __le32 format; + __le32 voltage; + __le32 pre_emphasis; +} __packed; + +/* The phy behind the DCP's DPTX port. */ +struct dcpib_phy_ops { + int (*activate)(struct dcpib *dcp); + int (*deactivate)(struct dcpib *dcp); + int (*set_link_rate)(struct dcpib *dcp, u32 rate); /* DPCD units */ + int (*set_lane_count)(struct dcpib *dcp, u32 lanes); /* optional */ + int (*set_drive)(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count); + int (*set_route)(struct dcpib *dcp, bool enable); + /* optional: plug orientation for the next activation */ + void (*set_orientation)(struct dcpib *dcp, bool flipped); + /* optional: the sink's EDID, returns its length */ + int (*read_edid)(struct dcpib *dcp, u8 *buf, size_t size); + /* + * optional: 1 with a sink on one of the phy's ports, 0 without; the + * port it found is taken by select() in the plug path + */ + int (*detect)(struct dcpib *dcp, bool *flipped); + void (*select)(struct dcpib *dcp); + u32 max_link_rate; /* DPCD units */ + /* active pixels per second the path scans out, 0 if not limited */ + u64 min_pixel_rate, max_pixel_rate; +}; + +struct dcpib_port { + struct dcpib_service *service; + u32 link_rate; /* DPCD units */ + u32 lanes; + /* lanes of the last training, kept while the link is down */ + u32 trained_lanes; + /* last drive settings the firmware set, for GET_DRIVE_SETTINGS */ + struct dcpib_drive_settings drive[4]; + u32 drive_count; +}; + +struct dcpib { + /* Stop new endpoint work before detaching a failed live session. */ + struct mutex rx_gate; + bool stopping; + bool ready; + bool kms_work_ready; + struct device *dev; + struct drm_device drm; + struct apple_rtkit *rtk; + struct workqueue_struct *wq; + u32 inherited_afk_endpoints; + + struct dcpib_ep *eps[DCPIB_NR_EPS]; + struct dcpib_ep *iboot_ep; + struct dcpib_ep *dpav_ep; + /* the sink's EDID, read by the phy (NULL if unavailable) */ + const struct drm_edid *edid; + struct dcpib_ep *dptx_ep; + struct dcpib_service *iboot; + struct dcpib_port port[2]; + + /* The scanout DART's own device and domain (see dcpib_scanout_init). */ + struct platform_device *scanout; + struct iommu_domain *scanout_dom; + + /* + * Scanout buffers (dcpib_boot_surface), each for the largest mode: a + * frame is copied into one not on screen, which SET_SURFACE then shows. + * Copying into the buffer being scanned out tore every frame. Three, so + * the one written next left the screen a frame ago. + */ +#define DCPIB_SCANOUT_BUFS 3 + struct dcpib_dma scan[DCPIB_SCANOUT_BUFS]; + /* their pages write-combined: the pipe does not snoop the CPU caches */ + void *scan_wc[DCPIB_SCANOUT_BUFS]; + /* their addresses in the display pipe's DART (dcpib_scan_free_dva) */ + dma_addr_t scan_dva[DCPIB_SCANOUT_BUFS]; + /* scan[front] is on screen (or set as the surface next power on) */ + unsigned int front; + /* the part of the current picture scan[i] does not hold yet */ + struct drm_rect stale[DCPIB_SCANOUT_BUFS]; + /* when scan[i] stopped being the requested surface */ + ktime_t retired[DCPIB_SCANOUT_BUFS]; + /* the last SET_SURFACE of a new buffer */ + ktime_t last_swap; + /* + * A frame written within one refresh of the last swap waits in + * scan[pending] (-1: none) for swap_timer, at most one swap per + * refresh; a newer frame overwrites it. + */ + int pending; + u32 pending_w, pending_h, pending_pitch; + struct hrtimer swap_timer; + struct work_struct swap_work; + u32 boot_w, boot_h, boot_stride; + + const struct dcpib_phy_ops *phy; + void *phy_priv; + /* display-crossbar DFP endpoint the DCP drives */ + u32 dcp_output; + /* optional sink HPD line (J873g HDMI), polled; last level seen */ + struct gpio_desc *hpd_gpio; + int hpd; + /* USB-C: the PD controller of the port in use, polled the same way */ + struct spmi_device *pd; + /* HDMI phy: DCPEXT0, or the main DCP on the bootloader's HDMI-only route */ + bool hdmi; + u32 die; + + /* iBoot state below, under lock */ + struct mutex lock; + bool powered; + struct dcpib_timing_mode *tmodes; + unsigned int nr_tmodes; + struct dcpib_color_mode *cmodes; + unsigned int nr_cmodes; + struct dcpib_timing_mode cur_mode; + bool mode_set; + /* SET_MODE of cur_mode failed; the next plug sets it again */ + bool mode_failed; + /* USB-C: re-enter DP alt mode when the sink stopped answering */ + struct work_struct pd_reset_work; + unsigned long pd_reset_time; + /* The DCPAV video interface started: SET_MODE again, or the + * firmware's idle power off timer takes the link down 30 s later. */ + struct delayed_work av_work; + + struct drm_plane plane; + struct drm_crtc crtc; + struct drm_encoder encoder; + struct drm_connector connector; +}; + +static inline struct dcpib *to_dcpib(struct drm_device *drm) +{ + return container_of(drm, struct dcpib, drm); +} + +/* afk.c */ +struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 ep, + const struct dcpib_service_ops *ops, + unsigned int expected); +void dcpib_ep_stop(struct dcpib_ep *ep); +void dcpib_ep_recv(struct dcpib_ep *ep, u64 message); +int dcpib_command(struct dcpib_service *service, u8 type, const void *tx, + size_t txlen, void *rx, size_t *rxlen); +int dcpib_service_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_len, size_t data_pad, + void *output, size_t output_len, size_t output_pad); +int dcpib_dma_alloc(struct dcpib *dcp, struct dcpib_dma *dma, size_t size); +void dcpib_dma_free(struct dcpib *dcp, struct dcpib_dma *dma); + +/* iboot.c */ +int dcpib_iboot_init(struct dcpib *dcp); + +void dcpib_kms_stop(struct dcpib *dcp); +int dcpib_ib_set_power(struct dcpib *dcp, bool on); +int dcpib_ib_get_hpd(struct dcpib *dcp, u32 *timings, u32 *colors); +int dcpib_ib_get_modes(struct dcpib *dcp); +int dcpib_ib_set_mode(struct dcpib *dcp, const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c); +int dcpib_ib_set_surface(struct dcpib *dcp, u64 iova, u32 width, u32 height, + u32 stride); + +/* drv.c */ +int dcpib_wait_hpd(struct dcpib *dcp); + +/* dptx.c */ +int dcpib_dptx_init(struct dcpib *dcp); +int dcpib_dpav_init(struct dcpib *dcp); +int dcpib_dptx_connect(struct dcpib *dcp, unsigned int port); +int dcpib_dptx_set_hpd(struct dcpib *dcp, unsigned int port, bool hpd); + +/* aux.c */ +struct dcpib_aux { + void __iomem *regs; + void __iomem *ps; + u32 sel; /* output select of the core */ +}; + +void dcpib_aux_probe(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel); +void dcpib_aux_probe_node(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel, + struct device_node *np); +int dcpib_aux_init(struct dcpib *dcp, struct dcpib_aux *aux); +int dcpib_aux_read_edid(struct dcpib *dcp, struct dcpib_aux *aux, u8 *buf, + size_t size); + +/* atc.c, hdmi.c */ +int dcpib_atc_init(struct dcpib *dcp); +struct device_node *dcpib_hdmi_node(void); +int dcpib_hdmi_source(struct device_node *np); +int dcpib_hdmi_init(struct dcpib *dcp, struct device_node *np); + +/* pd.c */ +struct spmi_device *dcpib_pd_get(struct dcpib *dcp, struct device_node *np); +int dcpib_pd_sink(struct spmi_device *pd, bool *flipped); +int dcpib_pd_reconnect(struct dcpib *dcp); + +/* kms.c */ +int dcpib_kms_init(struct dcpib *dcp); +int dcpib_set_boot_mode(struct dcpib *dcp); +void dcpib_update_edid(struct dcpib *dcp); +void dcpib_reset_mode(struct dcpib *dcp); +void dcpib_av_started(struct dcpib *dcp); + +#endif diff --git a/drivers/gpu/drm/apple_dcpib/dptx.c b/drivers/gpu/drm/apple_dcpib/dptx.c new file mode 100644 index 0000000000000..55752bd74e056 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/dptx.c @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * DCP DPTX port service and DPAV controllers (port of aura + * src/dcp/dptx_port_ep.c and src/dcp/dpav_ep.c, AFKEPV2 form only). + */ + +#include +#include +#include + +#include "dcpib.h" + +enum dptx_apcall { + DPTX_APCALL_ACTIVATE = 0, + DPTX_APCALL_DEACTIVATE = 1, + DPTX_APCALL_GET_MAX_DRIVE_SETTINGS = 2, + DPTX_APCALL_SET_DRIVE_SETTINGS = 3, + DPTX_APCALL_GET_DRIVE_SETTINGS = 4, + DPTX_APCALL_WILL_CHANGE_LINK_CONFIG = 5, + DPTX_APCALL_DID_CHANGE_LINK_CONFIG = 6, + DPTX_APCALL_GET_MAX_LINK_RATE = 7, + DPTX_APCALL_GET_LINK_RATE = 8, + DPTX_APCALL_SET_LINK_RATE = 9, + DPTX_APCALL_GET_MAX_LANE_COUNT = 10, + DPTX_APCALL_GET_ACTIVE_LANE_COUNT = 11, + DPTX_APCALL_SET_ACTIVE_LANE_COUNT = 12, + DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD = 13, + DPTX_APCALL_GET_DOWN_SPREAD = 14, + DPTX_APCALL_SET_DOWN_SPREAD = 15, + DPTX_APCALL_GET_SUPPORTS_LANE_MAPPING = 16, + DPTX_APCALL_SET_LANE_MAP = 17, + DPTX_APCALL_GET_SUPPORTS_HPD = 18, + DPTX_APCALL_FORCE_HOTPLUG_DETECT = 19, + DPTX_APCALL_INACTIVE_SINK_DETECTED = 20, + DPTX_APCALL_SET_TILED_DISPLAY_HINTS = 21, + DPTX_APCALL_DEVICE_NOT_RESPONDING = 22, + DPTX_APCALL_DEVICE_BUSY_TIMEOUT = 23, + DPTX_APCALL_DEVICE_NOT_STARTED = 24, + DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE = 26, +}; + +#define DPTX_REMOTE_PORT_CORE GENMASK(3, 0) +#define DPTX_REMOTE_PORT_DFP GENMASK(7, 4) +#define DPTX_REMOTE_PORT_DIE GENMASK(11, 8) +#define DPTX_REMOTE_PORT_CONNECTED BIT(15) + +struct dptx_connection { + __le32 unk; + __le32 target; +} __packed; + +struct dptx_connection_reply { + __le32 unk; + __le32 target; + __le64 reserved; + __le32 status; + u8 padding[12]; +} __packed; + +struct dptx_hotplug { + u8 pad[16]; + __le32 unk; +} __packed; + +static bool dptx_needs_port(u32 command) +{ + switch (command) { + case DPTX_APCALL_ACTIVATE: + case DPTX_APCALL_DEACTIVATE: + case DPTX_APCALL_WILL_CHANGE_LINK_CONFIG: + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + case DPTX_APCALL_SET_LINK_RATE: + case DPTX_APCALL_SET_DOWN_SPREAD: + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: + case DPTX_APCALL_SET_DRIVE_SETTINGS: + case DPTX_APCALL_GET_LINK_RATE: + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + case DPTX_APCALL_GET_DRIVE_SETTINGS: + return true; + default: + return false; + } +} + +/* Requests carry their argument, replies their value, at offset 16. */ +static u32 dptx_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_size, void *reply, + size_t reply_size) +{ + struct dcpib *dcp = service->ep->dcp; + struct dcpib_port *port = service->cookie; + const struct dcpib_phy_ops *phy = dcp->phy; + u64 value; + u32 arg; + + if (group && !(group == 1 && (command == DPTX_APCALL_SET_DRIVE_SETTINGS || + command == DPTX_APCALL_GET_DRIVE_SETTINGS))) + return KIO_RETURN_UNSUPPORTED; + + dev_dbg(dcp->dev, "DPTX: port %u group %u command %u size %zu\n", + service->unit, group, command, data_size); + if ((!port || !phy) && dptx_needs_port(command)) + return KIO_RETURN_NOT_READY; + + switch (command) { + case DPTX_APCALL_ACTIVATE: + if (phy->activate(dcp)) + return KIO_RETURN_ERROR; + port->link_rate = DCPIB_LINK_RATE_HBR; + return 0; + case DPTX_APCALL_DEACTIVATE: + if (phy->deactivate(dcp)) + return KIO_RETURN_ERROR; + port->link_rate = 0; + return 0; + case DPTX_APCALL_WILL_CHANGE_LINK_CONFIG: + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + return phy->set_route(dcp, command == + DPTX_APCALL_DID_CHANGE_LINK_CONFIG) ? + KIO_RETURN_ERROR : 0; + case DPTX_APCALL_GET_MAX_LINK_RATE: + value = phy ? phy->max_link_rate : DCPIB_LINK_RATE_HBR; + break; + case DPTX_APCALL_GET_LINK_RATE: + value = port->link_rate; + break; + case DPTX_APCALL_SET_LINK_RATE: + if (data_size < 17) + return KIO_RETURN_BAD_ARG; + arg = ((const u8 *)data)[16]; + if (arg > phy->max_link_rate) + return KIO_RETURN_UNSUPPORTED; + if (phy->set_link_rate(dcp, arg)) + return KIO_RETURN_ERROR; + port->link_rate = arg; + return 0; + case DPTX_APCALL_GET_MAX_LANE_COUNT: + value = 4; + break; + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + value = port->lanes; + break; + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: + if (data_size < 20) + return KIO_RETURN_BAD_ARG; + arg = get_unaligned_le32(data + 16); + if (arg == 3 || arg > 4) + return KIO_RETURN_BAD_ARG; + dev_info(dcp->dev, "DPTX: %u active lanes\n", arg); + if (phy->set_lane_count && phy->set_lane_count(dcp, arg)) + return KIO_RETURN_ERROR; + port->lanes = arg; + if (arg) + port->trained_lanes = arg; + return 0; + case DPTX_APCALL_GET_MAX_DRIVE_SETTINGS: { + const struct dcpib_drive_settings maximum = { + 0, cpu_to_le32(3), cpu_to_le32(3) + }; + + if (reply_size < 16 + sizeof(maximum)) + return KIO_RETURN_BAD_ARG; + memcpy(reply + 16, &maximum, sizeof(maximum)); + return 0; + } + case DPTX_APCALL_SET_DRIVE_SETTINGS: { + struct dcpib_drive_settings settings[4]; + + if (group != 1) + return KIO_RETURN_UNSUPPORTED; + if (data_size < 32) + return KIO_RETURN_BAD_ARG; + arg = get_unaligned_le32(data + 16); + if (arg > 4 || data_size < 32 + arg * sizeof(settings[0])) + return KIO_RETURN_BAD_ARG; + memcpy(settings, data + 32, arg * sizeof(settings[0])); + if (phy->set_drive(dcp, settings, arg)) + return KIO_RETURN_ERROR; + memcpy(port->drive, settings, arg * sizeof(settings[0])); + port->drive_count = arg; + return 0; + } + case DPTX_APCALL_GET_DRIVE_SETTINGS: + /* + * The firmware reads back what it set during link training; an + * unsupported reply here was followed by a firmware crash. + * Same layout as SET: count at 16, settings from 32. + */ + if (group != 1) + return KIO_RETURN_UNSUPPORTED; + if (reply_size < 32 + port->drive_count * sizeof(port->drive[0])) + return KIO_RETURN_BAD_ARG; + memcpy(reply, data, min(reply_size, data_size)); + put_unaligned_le32(port->drive_count, reply + 16); + memcpy(reply + 32, port->drive, + port->drive_count * sizeof(port->drive[0])); + return 0; + case DPTX_APCALL_SET_DOWN_SPREAD: + if (data_size < 17) + return KIO_RETURN_BAD_ARG; + /* The qualified link setup runs with downspread disabled. */ + return ((const u8 *)data)[16] ? KIO_RETURN_UNSUPPORTED : 0; + case DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD: + case DPTX_APCALL_GET_DOWN_SPREAD: + case DPTX_APCALL_GET_SUPPORTS_LANE_MAPPING: + case DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE: + value = 0; + break; + case DPTX_APCALL_GET_SUPPORTS_HPD: + value = 1; + break; + case DPTX_APCALL_INACTIVE_SINK_DETECTED: + case DPTX_APCALL_DEVICE_NOT_RESPONDING: + case DPTX_APCALL_DEVICE_BUSY_TIMEOUT: + case DPTX_APCALL_DEVICE_NOT_STARTED: + /* Link fault reports: acknowledged with a zeroed retcode. */ + dev_info(dcp->dev, "DPTX: firmware reports link fault %u\n", + command); + memcpy(reply, data, min(reply_size, data_size)); + if (reply_size > 4) + memset(reply, 0, 4); + return 0; + case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: + memcpy(reply, data, min(reply_size, data_size)); + if (reply_size >= 4) + *(__le32 *)reply = cpu_to_le32(1); + return 0; + default: + dev_info(dcp->dev, "DPTX: unsupported call %u\n", command); + return KIO_RETURN_UNSUPPORTED; + } + + if (reply_size < 16 + sizeof(value)) + return KIO_RETURN_BAD_ARG; + put_unaligned_le64(value, reply + 16); + return 0; +} + +static const struct dcpib_service_ops dptx_ops = { + .call = dptx_call, +}; + +int dcpib_dptx_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_DPTX, &dptx_ops, 2); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dptx_ep = ep; + /* Ports in the order the firmware announced them. */ + for (unsigned int i = 0; i < ep->nr_services && i < 2; i++) { + dcp->port[i].service = &ep->services[i]; + ep->services[i].cookie = &dcp->port[i]; + } + return 0; +} + +int dcpib_dpav_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_DPAV, NULL, 4); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dpav_ep = ep; + return 0; +} + +/* connectTo, then requestDisplay, as aura dcp_dptx_connect. */ +int dcpib_dptx_connect(struct dcpib *dcp, unsigned int port) +{ + struct dcpib_service *service = dcp->port[port].service; + u32 target = FIELD_PREP(DPTX_REMOTE_PORT_CORE, 0) | + FIELD_PREP(DPTX_REMOTE_PORT_DFP, dcp->dcp_output) | + FIELD_PREP(DPTX_REMOTE_PORT_DIE, dcp->die) | + DPTX_REMOTE_PORT_CONNECTED; + struct dptx_connection cmd = { + /* IODPTXPortAttributes supportsHPD */ + .unk = cpu_to_le32(0x100), + .target = cpu_to_le32(target), + }; + struct dptx_connection_reply resp = {}; + __le32 status = 0; + int ret; + + if (!service) + return -ENODEV; + ret = dcpib_service_call(service, 0, 11, &cmd, sizeof(cmd), 24, &resp, + sizeof(resp), 0); + if (ret) + return ret; + if (resp.status) { + dev_err(dcp->dev, "DPTX: connect returned %#x\n", + le32_to_cpu(resp.status)); + return -EIO; + } + if (le32_to_cpu(resp.target) != target || + le32_to_cpu(resp.unk) != 0x100) + return -EPROTO; + + ret = dcpib_service_call(service, 0, 6, NULL, 0, 16, &status, + sizeof(status), 12); + if (!ret && status) { + dev_err(dcp->dev, "DPTX: requestDisplay returned %#x\n", + le32_to_cpu(status)); + ret = -EIO; + } + return ret; +} + +int dcpib_dptx_set_hpd(struct dcpib *dcp, unsigned int port, bool hpd) +{ + struct dcpib_service *service = dcp->port[port].service; + struct dptx_hotplug cmd = { .unk = cpu_to_le32(hpd) }, resp = {}; + int ret; + + if (!service) + return -ENODEV; + ret = dcpib_service_call(service, 8, 8, &cmd, sizeof(cmd), 12, &resp, + sizeof(resp), 12); + if (ret) + return ret; + /* The status is the first word of the reply. */ + return get_unaligned_le32(resp.pad) ? -EIO : 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c new file mode 100644 index 0000000000000..e80f54816a062 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Apple T8152 DCP on the iBoot interface: probe and bring-up. + * + * The bootloader (aura) trains the display and hands the DCP over with its + * RTKit session running. This driver adopts that session, starts the iBoot, + * DPAV and DPTX port endpoints, reconnects the DPTX port and asserts HPD, + * then registers a DRM device whose flips are iBoot SET_SURFACE calls, in + * the order aura's display_init_t8152 uses. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define DCPIB_HPD_TIMEOUT_MS 10000 + +static void dcpib_rtk_recv(void *cookie, u8 ep, u64 message) +{ + struct dcpib *dcp = cookie; + struct dcpib_ep *e = NULL; + + mutex_lock(&dcp->rx_gate); + if (dcp->stopping) + goto out; + if (ep >= DCPIB_FIRST_EP && ep < DCPIB_FIRST_EP + DCPIB_NR_EPS) + e = dcp->eps[ep - DCPIB_FIRST_EP]; + if (e) + dcpib_ep_recv(e, message); + else + dev_dbg(dcp->dev, "message %#llx for idle endpoint %#x\n", + message, ep); +out: + mutex_unlock(&dcp->rx_gate); +} + +static void dcpib_rtk_crashed(void *cookie, const void *crashlog, size_t size) +{ + struct dcpib *dcp = cookie; + + dev_err(dcp->dev, "DCP firmware crashed\n"); +} + +/* Buffers the firmware pre-allocated in its own DART mappings, or new ones. */ +static int dcpib_rtk_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dcpib *dcp = cookie; + + if (bfr->iova) { + struct iommu_domain *domain = iommu_get_domain_for_dev(dcp->dev); + phys_addr_t phys; + + if (!domain) + return -ENOMEM; + phys = iommu_iova_to_phys(domain, bfr->iova); + if (!phys) { + dev_warn(dcp->dev, "no mapping for firmware buffer %pad\n", + &bfr->iova); + return -ENOMEM; + } + /* The firmware writes these behind the CPU's back. */ + bfr->buffer = memremap(phys, bfr->size, MEMREMAP_WC); + if (!bfr->buffer) + return -ENOMEM; + bfr->is_mapped = true; + return 0; + } + + bfr->buffer = dma_alloc_coherent(dcp->dev, bfr->size, &bfr->iova, + GFP_KERNEL); + return bfr->buffer ? 0 : -ENOMEM; +} + +static void dcpib_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dcpib *dcp = cookie; + + if (bfr->is_mapped) + memunmap(bfr->buffer); + else + dma_free_coherent(dcp->dev, bfr->size, bfr->buffer, bfr->iova); +} + +static const struct apple_rtkit_ops dcpib_rtkit_ops = { + .crashed = dcpib_rtk_crashed, + .recv_message = dcpib_rtk_recv, + .shmem_setup = dcpib_rtk_shmem_setup, + .shmem_destroy = dcpib_rtk_shmem_destroy, +}; + +DEFINE_DRM_GEM_DMA_FOPS(dcpib_fops); + +static const struct drm_driver dcpib_drm_driver = { + /* + * Frames are copied by the CPU, imported ones too: a compositor + * driving both displays imports one device's buffers into the other. + */ + DRM_GEM_DMA_DRIVER_OPS_VMAP, + DRM_FBDEV_DMA_DRIVER_OPS, + .name = "apple-dcpib", + .desc = "Apple DCP (iBoot interface)", + .major = 1, + .minor = 0, + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, + .fops = &dcpib_fops, +}; + +/* + * Adopt the bootloader's RTKit session. The T8152 DCP does not acknowledge + * AP QUIESCED, so a quiesced handoff cannot be negotiated, and an IOP INIT + * while its AP power is still ON panics the firmware ("Invalid AP power: + * 32"). Keep the running session instead and start the application + * endpoints explicitly (dcpib_ep_start), as drm/apple does for live M5 + * sessions. + */ +static int dcpib_boot(struct dcpib *dcp) +{ + dcp->rtk = apple_rtkit_init_adopted(dcp->dev, dcp, NULL, 0, + &dcpib_rtkit_ops); + if (IS_ERR(dcp->rtk)) + return PTR_ERR(dcp->rtk); + return apple_rtkit_adopt_running(dcp->rtk); +} + +/* + * The display pipe scans out through dart-disp0, the DCP fetches through + * dart-dcp. Both are locked with their own live root, so each needs its own + * device and domain: one domain spanning both would publish dart-dcp's root + * into dart-disp0 and drop the scanout mappings the bootloader left there + * (the pipe then faults, IOMFB axi_rd_err). Scanout buffers are allocated on + * the DCP device and mapped into the scanout domain at the same address. + */ +static int dcpib_scanout_init(struct dcpib *dcp) +{ + struct device_node *np __free(device_node) = + of_get_child_by_name(dcp->dev->of_node, "scanout"); + int ret; + + if (!np) + return -ENODEV; + dcp->scanout = of_platform_device_create(np, NULL, dcp->dev); + if (!dcp->scanout) + dcp->scanout = of_find_device_by_node(np); + if (!dcp->scanout) + return -ENODEV; + ret = dma_set_mask_and_coherent(&dcp->scanout->dev, DMA_BIT_MASK(64)); + if (ret) + return ret; + ret = of_dma_configure(&dcp->scanout->dev, np, true); + if (ret) + return ret; + dcp->scanout_dom = iommu_get_domain_for_dev(&dcp->scanout->dev); + return dcp->scanout_dom ? 0 : -ENODEV; +} + +static void *dcpib_vmap_wc(struct sg_table *sgt) +{ + unsigned int n = 0, count = 0; + struct sg_page_iter iter; + struct page **pages; + void *vaddr; + + for_each_sgtable_page(sgt, &iter, 0) + count++; + pages = kvmalloc_array(count, sizeof(*pages), GFP_KERNEL); + if (!pages) + return NULL; + for_each_sgtable_page(sgt, &iter, 0) + pages[n++] = sg_page_iter_page(&iter); + vaddr = vmap(pages, count, VM_MAP, pgprot_writecombine(PAGE_KERNEL)); + kvfree(pages); + return vaddr; +} + +/* + * The display pipe's DART is locked and comes with the firmware's own + * mappings. The address the coprocessor's allocator handed out for a scanout + * buffer can be one of them, and mapping over it corrupts what the pipe + * fetches every frame (the picture flickers). Start at @dva and go down until + * a range with no mapping is found. + */ +static dma_addr_t dcpib_scan_free_dva(struct dcpib *dcp, dma_addr_t dva, + size_t size) +{ + const size_t step = SZ_16K; + + size = ALIGN(size, step); + for (int tries = 0; tries < 64 && dva >= size; tries++) { + size_t off; + + for (off = 0; off < size; off += step) + if (iommu_iova_to_phys(dcp->scanout_dom, dva + off)) + break; + if (off == size) + return dva; + dev_info(dcp->dev, "scanout DART: %pad+%#zx is in use, moving down\n", + &dva, off); + dva -= size; + } + return 0; +} + +/* One scanout buffer: allocated, cleared, mapped for the CPU and the pipe. */ +static int dcpib_scan_alloc(struct dcpib *dcp, int i, size_t size) +{ + struct dcpib_dma *fb = &dcp->scan[i]; + struct sg_table sgt; + ssize_t mapped; + dma_addr_t dva; + int ret; + + ret = dcpib_dma_alloc(dcp, fb, size); + if (ret) + return ret; + ret = dma_get_sgtable(dcp->dev, &sgt, fb->cpu, fb->iova, fb->size); + if (ret) + return ret; + /* + * The DCP is coherent, the display pipe is not: it reads memory past + * the CPU caches, and frames written through the cacheable mapping + * show only once evicted. Write through a write-combined mapping of + * the same pages, with the zeroing of the allocation cleaned out first. + */ + arch_wb_cache_pmem(fb->cpu, fb->size); + dcp->scan_wc[i] = dcpib_vmap_wc(&sgt); + if (!dcp->scan_wc[i]) { + sg_free_table(&sgt); + return -ENOMEM; + } + memset(dcp->scan_wc[i], 0, fb->size); + /* + * Cacheable, as iBoot maps scanout buffers: the pipe faults on + * entries with the DART's no-cache bit (IOMFB axi_rd_err). + */ + dva = dcpib_scan_free_dva(dcp, fb->iova, fb->size); + if (!dva) { + sg_free_table(&sgt); + return -ENOSPC; + } + mapped = iommu_map_sgtable(dcp->scanout_dom, dva, &sgt, + IOMMU_READ | IOMMU_WRITE | IOMMU_CACHE); + sg_free_table(&sgt); + if (mapped < (ssize_t)fb->size) + return mapped < 0 ? mapped : -ENOMEM; + dcp->scan_dva[i] = dva; + dev_info(dcp->dev, "scanout buffer %d: %zu bytes at %pad\n", i, fb->size, &dva); + return 0; +} + +/* + * A fresh iBoot session has no surface. Whenever the pipe runs without one + * (the link retrains when the DPTX services restart, SET_MODE turns it on), + * it fetches from a null base and the firmware takes an NMI (IOMFB + * axi_rd_err). Hand it a black buffer of the boot mode at once, sized from + * the bootloader's simple-framebuffer. + */ +static int dcpib_boot_surface(struct dcpib *dcp) +{ + /* + * The boot display's framebuffer is /chosen's; the bootloader adds the + * second display's (HDMI) at the root. + */ + struct device_node *chosen __free(device_node) = + of_find_node_by_path("/chosen"); + struct device_node *parent __free(device_node) = + of_property_read_bool(dcp->dev->of_node, "apple,hdmi") ? + of_find_node_by_path("/") : of_node_get(chosen); + u32 width = 1280, height = 720, stride = 1280 * 4; + int ret; + + for_each_child_of_node_scoped(parent, np) { + struct resource res; + + if (!of_device_is_compatible(np, "simple-framebuffer")) + continue; + of_property_read_u32(np, "width", &width); + of_property_read_u32(np, "height", &height); + of_property_read_u32(np, "stride", &stride); + /* + * Take the display over from simpledrm on the bootloader's + * framebuffer, so it does not shadow this driver's output + * (fbcon on a framebuffer nothing scans out). + */ + if (!of_address_to_resource(np, 0, &res)) { + ret = aperture_remove_conflicting_devices(res.start, + resource_size(&res), dcpib_drm_driver.name); + if (ret) + dev_warn(dcp->dev, "simpledrm stays: %d\n", ret); + } + break; + } + /* + * Large enough for the largest mode: while a SET_MODE switches, the + * pipe briefly scans the current surface with the new mode's size. + */ + size_t size = (size_t)stride * height; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) + size = max_t(size_t, size, (size_t)le32_to_cpu(dcp->tmodes[i].width) * + le32_to_cpu(dcp->tmodes[i].height) * 4); + dcp->boot_w = width; + dcp->boot_h = height; + dcp->boot_stride = stride; + for (int i = 0; i < ARRAY_SIZE(dcp->scan); i++) { + ret = dcpib_scan_alloc(dcp, i, size); + if (ret) + return ret; + } + dcp->front = 0; + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[0], width, height, + stride); + dev_info(dcp->dev, "boot surface %ux%u at %pad: %d\n", width, height, + &dcp->scan_dva[0], ret); + return ret; +} + +/* Wait until the firmware reports a sink with modes, as aura does. */ +int dcpib_wait_hpd(struct dcpib *dcp) +{ + unsigned long timeout = jiffies + msecs_to_jiffies(DCPIB_HPD_TIMEOUT_MS); + u32 timings = 0, colors = 0; + int hpd; + + do { + hpd = dcpib_ib_get_hpd(dcp, &timings, &colors); + if (hpd > 0 && timings && colors) { + dev_info(dcp->dev, "sink: %u timing, %u color modes\n", + timings, colors); + return 0; + } + msleep(100); + } while (time_before(jiffies, timeout)); + + dev_err(dcp->dev, "no sink (hpd %d, %u timing, %u color modes)\n", hpd, + timings, colors); + return hpd < 0 ? hpd : -ENODEV; +} + +static void dcpib_av_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(to_delayed_work(work), struct dcpib, + av_work); + + if (READ_ONCE(dcp->ready)) + dcpib_reset_mode(dcp); +} + +static int dcpib_probe_inner(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *hdmi_np __free(device_node) = dcpib_hdmi_node(); + int source = hdmi_np ? dcpib_hdmi_source(hdmi_np) : -ENODEV; + struct dcpib *dcp; + u32 inherited_afk_endpoints; + int ret; + + /* + * Without a USB-C display the bootloader drives HDMI from the main DCP + * (source 0) and never starts DCPEXT0. + */ + if (dev->of_node == hdmi_np && source != 1) { + dev_info(dev, "%s\n", source == 0 ? "HDMI is on the main DCP" : + "no bootloader HDMI link to adopt"); + return -ENODEV; + } + + ret = of_property_read_u32(dev->of_node, "apple,afk-active-endpoints", + &inherited_afk_endpoints); + if (ret || inherited_afk_endpoints & ~GENMASK(15, 0)) + return dev_err_probe(dev, -EINVAL, + "bootloader AFK endpoint handoff required\n"); + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); + if (ret) + return ret; + + dcp = devm_drm_dev_alloc(dev, &dcpib_drm_driver, struct dcpib, drm); + if (IS_ERR(dcp)) + return PTR_ERR(dcp); + dcp->dev = dev; + dcp->inherited_afk_endpoints = inherited_afk_endpoints; + platform_set_drvdata(pdev, dcp); + mutex_init(&dcp->lock); + mutex_init(&dcp->rx_gate); + INIT_DELAYED_WORK(&dcp->av_work, dcpib_av_work); + of_property_read_u32(dev->of_node, "apple,die", &dcp->die); + + dcp->wq = devm_alloc_ordered_workqueue(dev, "dcpib", WQ_MEM_RECLAIM); + if (!dcp->wq) + return -ENOMEM; + + ret = dcpib_scanout_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no scanout DART stream\n"); + + dcp->hdmi = dev->of_node == hdmi_np || source == 0; + if (dcp->hdmi && dev->of_node != hdmi_np) + dev_info(dev, "HDMI from the main DCP (no USB-C display at boot)\n"); + /* Read only: the pin belongs to a controller the AOP runs. */ + dcp->hpd_gpio = devm_fwnode_gpiod_get(dev, + of_fwnode_handle(dcp->hdmi ? hdmi_np : dev->of_node), "hpd", + GPIOD_ASIS, "hpd"); + if (PTR_ERR(dcp->hpd_gpio) == -ENOENT) + dcp->hpd_gpio = NULL; + if (IS_ERR(dcp->hpd_gpio)) + return dev_err_probe(dev, PTR_ERR(dcp->hpd_gpio), "no HPD GPIO\n"); + dcp->hpd = 1; + ret = dcp->hdmi ? dcpib_hdmi_init(dcp, hdmi_np) : dcpib_atc_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DisplayPort phy\n"); + + ret = dcpib_boot(dcp); + if (ret) + return dev_err_probe(dev, ret, "cannot adopt the DCP\n"); + + ret = dcpib_iboot_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no disp0-service\n"); + /* + * The new iBoot session has dropped the surface while the pipe still + * runs: it fetches from a null base (IOMFB axi_rd_err, firmware NMI) + * unless it is powered off at once. It comes back on below, after a + * surface is set. + */ + ret = dcpib_ib_set_power(dcp, false); + if (ret) + return dev_err_probe(dev, ret, "display power off failed\n"); + ret = dcpib_dpav_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DPAV controllers\n"); + ret = dcpib_dptx_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DPTX port service\n"); + + ret = dcpib_dptx_connect(dcp, 0); + if (ret) + return dev_err_probe(dev, ret, "DPTX connect failed\n"); + ret = dcpib_dptx_set_hpd(dcp, 0, true); + if (ret) + return dev_err_probe(dev, ret, "DPTX hotplug failed\n"); + + ret = dcpib_wait_hpd(dcp); + if (ret) + return ret; + ret = dcpib_ib_get_modes(dcp); + if (ret) + return dev_err_probe(dev, ret, "no modes\n"); + ret = dcpib_boot_surface(dcp); + if (ret) + return dev_err_probe(dev, ret, "no boot surface\n"); + /* + * Without the bootloader's link (no display on the port at boot) the + * phy starts cold, the right way round for the plug. + */ + if (dcp->phy->detect && dcp->phy->set_orientation) { + bool flipped; + + if (dcp->phy->detect(dcp, &flipped) > 0) { + if (dcp->phy->select) + dcp->phy->select(dcp); + dcp->phy->set_orientation(dcp, flipped); + } + } + ret = dcp->phy->activate(dcp); + if (ret) + return dev_err_probe(dev, ret, "phy activation failed\n"); + /* While the display is still off and the firmware does no AUX. */ + dcpib_update_edid(dcp); + ret = dcpib_ib_set_power(dcp, true); + if (ret) + return dev_err_probe(dev, ret, "set_power failed\n"); + dcp->powered = true; + dev_info(dev, "%u timing modes, %u color modes\n", dcp->nr_tmodes, + dcp->nr_cmodes); + + ret = dcpib_kms_init(dcp); + if (ret) + return ret; + ret = dcpib_set_boot_mode(dcp); + if (ret) + dev_warn(dev, "boot mode not set again: %d\n", ret); + ret = drm_dev_register(&dcp->drm, 0); + if (ret) + return ret; + WRITE_ONCE(dcp->ready, true); + if (dcp->hpd_gpio || dcp->pd) + drmm_kms_helper_poll_init(&dcp->drm); + drm_client_setup(&dcp->drm, NULL); + return 0; +} + +static int dcpib_probe(struct platform_device *pdev) +{ + struct dcpib *dcp; + int ret; + + platform_set_drvdata(pdev, NULL); + ret = dcpib_probe_inner(pdev); + dcp = platform_get_drvdata(pdev); + if (!ret || !dcp || IS_ERR_OR_NULL(dcp->rtk)) + return ret; + + /* + * Once adopted, the DCP may still DMA into our buffers on any error. + * Gate RX before draining endpoint work; keep RTKit available until + * every sender has stopped. Do not reclaim firmware-visible storage + * or let driver-core retry a partially started session. + */ + WRITE_ONCE(dcp->ready, false); + mutex_lock(&dcp->rx_gate); + dcp->stopping = true; + mutex_unlock(&dcp->rx_gate); + flush_workqueue(dcp->wq); + cancel_delayed_work_sync(&dcp->av_work); + dcpib_kms_stop(dcp); + apple_rtkit_free_retaining_buffers(dcp->rtk); + dcp->rtk = NULL; + dev_err(dcp->dev, + "initialization failed (%d); live DMA resources retained until reboot\n", + ret); + return 0; +} + +static const struct of_device_id dcpib_of_match[] = { + { .compatible = "apple,t8152-dcp-iboot" }, + {} +}; +MODULE_DEVICE_TABLE(of, dcpib_of_match); + +static struct platform_driver dcpib_platform_driver = { + .driver = { + .name = "apple-dcpib", + .of_match_table = dcpib_of_match, + /* The DCP and the link cannot be handed back yet. */ + .suppress_bind_attrs = true, + }, + .probe = dcpib_probe, +}; +/* There is no safe firmware shutdown/handoff yet; never unload this module. */ +static int __init dcpib_init(void) +{ + return platform_driver_register(&dcpib_platform_driver); +} +module_init(dcpib_init); + +MODULE_DESCRIPTION("Apple T8152 DCP display driver (iBoot interface)"); +MODULE_LICENSE("Dual MIT/GPL"); diff --git a/drivers/gpu/drm/apple_dcpib/hdmi.c b/drivers/gpu/drm/apple_dcpib/hdmi.c new file mode 100644 index 0000000000000..1a154b2c3c162 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/hdmi.c @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The J873g HDMI port: the dedicated T8152 dptx-phy in display-crossbar0's + * lpdptx slot, driven by DCPEXT0 and converted by a PS190 (aura + * src/dcp/dptx_phy.c, DPTX_PHY_T8152_HDMI). + * + * The bootloader powers the phy up, applies its tunables, selects the DCP + * and trains the link. This keeps that setup and reprograms what a link + * training changes: the PLL for a new link rate and the active lanes. Drive + * settings are only recorded, as on macOS, and there is no per-link route. + */ + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define HDMI_LANES 4 +#define HDMI_LANE0 0x4500 +#define HDMI_LANE_STRIDE 0x2800 + +struct dcpib_hdmi { + void __iomem *core; + void __iomem *apb; + /* AP DisplayPort AUX controller aux1, for the EDID */ + struct dcpib_aux aux; +}; + +/* AppleT8112DPTXPort::setDPTXSourceSelect: crossbar UFP feeding the phy */ +#define HDMI_SOURCE_SELECT 0x10 +#define HDMI_SOURCE_AUX1 6 /* UFP endpoint of dptx1-aux */ + +/* AppleT8152DPTXPort::setBitsInReg */ +static void hdmi_set_bits(struct dcpib_hdmi *hdmi, u32 offset, u32 shift, + u32 mask, u32 value) +{ + u32 reg = readl(hdmi->core + offset); + + writel((reg & ~mask) | (value << shift), hdmi->core + offset); +} + +static void hdmi_apb_update(struct dcpib_hdmi *hdmi, u32 offset, u32 mask, + u32 value) +{ + u32 reg = readl(hdmi->apb + offset); + + writel((reg & ~mask) | value, hdmi->apb + offset); +} + +/* + * AppleT8152DPTXPort::getTunableConfig: the DPPLLConfig words for each link + * rate (constant tables in AppleDisplayCrossbar, 26A434). + */ +static const struct { + u32 rate; /* DPCD units */ + u32 w[22]; +} hdmi_pll_configs[] = { + { DCPIB_LINK_RATE_RBR, + { 1, 0x21c, 0, 0, 1, 0x13, 1, 0, 0, 0x1a, 1, 0x3c, 2, 1, 0, 0, 1, 0, + 0, 0, 1, 3 } }, + { DCPIB_LINK_RATE_HBR, + { 1, 0x1c2, 0x1fff, 0x3ffe, 1, 0x13, 1, 0, 0, 0x1a, 1, 0x32, 2, 1, 0, + 0, 1, 0, 0, 0, 1, 3 } }, + { DCPIB_LINK_RATE_HBR2, + { 0, 0x1c2, 0x1fff, 0x3ffe, 1, 9, 1, 0, 0, 0x1a, 1, 0x32, 1, 0, 0, 1, + 0, 0, 0, 1, 1, 3 } }, + { DCPIB_LINK_RATE_HBR3, + { 0, 0x2a3, 0x2ffd, 0x3ffc, 1, 9, 1, 0, 0, 0x1a, 1, 0x5a, 1, 0, 0, 1, + 0, 0, 0, 1, 1, 3 } }, +}; + +/* + * AppleT8152DPTXPort::pllPowerUp/pllPowerDown: command in 0x408[2:0], + * request bit 3, acknowledge bit 19. + */ +static int hdmi_pll_command(struct dcpib *dcp, struct dcpib_hdmi *hdmi, + u32 command) +{ + int i; + + hdmi_set_bits(hdmi, 0x408, 6, 0x40, 1); + hdmi_set_bits(hdmi, 0x408, 0, 0x7, command); + hdmi_set_bits(hdmi, 0x408, 3, 0x8, 1); + for (i = 0; i < 10; i++) { + udelay(16); + if (readl(hdmi->core + 0x408) & BIT(19)) + break; + } + hdmi_set_bits(hdmi, 0x408, 3, 0x8, 0); + hdmi_set_bits(hdmi, 0x408, 6, 0x40, 0); + if (i == 10) { + dev_err(dcp->dev, "HDMI: no ACK to PLL command %u\n", command); + return -ETIMEDOUT; + } + return 0; +} + +/* + * AppleT8152DPTXPort::phySetActiveLaneCount: lane-disable bits, 0x2420 holds + * the mask of unused lanes. + */ +static void hdmi_set_lanes(struct dcpib_hdmi *hdmi, u32 lanes) +{ + static const u32 offsets[] = { 0x64, 0x24, 0x24, 0x48, 0x48, 0x5c, 0x5c, 0x48 }; + static const u32 shifts[] = { 4, 4, 1, 0, 1, 2, 3, 4 }; + static const u32 masks[] = { 0x10, 0x10, 0x2, 0x1, 0x2, 0x4, 0x8, 0x10 }; + u32 lane, i; + + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 0); + for (lane = 0; lane < HDMI_LANES; lane++) { + u32 base = HDMI_LANE0 + lane * HDMI_LANE_STRIDE; + + for (i = 0; i < ARRAY_SIZE(offsets); i++) + hdmi_set_bits(hdmi, base + offsets[i], shifts[i], + masks[i], lane >= lanes); + } + hdmi_set_bits(hdmi, 0x2420, 0, 0xf, 0xf & ~((1 << lanes) - 1)); + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 1); +} + +static int hdmi_activate(struct dcpib *dcp) +{ + return 0; +} + +static int hdmi_deactivate(struct dcpib *dcp) +{ + dev_info(dcp->dev, "HDMI: keeping the link up on deactivate\n"); + return 0; +} + +/* + * AppleDPTXPort::setLinkRate with the T8152 hooks: phyPowerDown, + * pllPowerDown, pllConfigureFrequency, phy/pllSetHalfLinkRate, pllPowerUp. + */ +static int hdmi_set_link_rate(struct dcpib *dcp, u32 rate) +{ + struct dcpib_hdmi *hdmi = dcp->phy_priv; + const u32 *w = NULL; + bool half; + size_t i; + int ret; + + for (i = 0; i < ARRAY_SIZE(hdmi_pll_configs); i++) + if (hdmi_pll_configs[i].rate == rate) + w = hdmi_pll_configs[i].w; + if (!w) + return -EINVAL; + + dev_info(dcp->dev, "HDMI: link rate %#x\n", rate); + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 0); + ret = hdmi_pll_command(dcp, hdmi, 0); + if (ret) + return ret; + + /* pllConfigureFrequency, downspread off */ + hdmi_set_bits(hdmi, 0x6c0, 0, 0x7ff, w[1]); + hdmi_set_bits(hdmi, 0x6c0, 11, 0x800, w[6]); + hdmi_set_bits(hdmi, 0x6c0, 13, 0x2000, w[10]); + hdmi_set_bits(hdmi, 0x6c0, 18, 0x3c0000, w[21]); + hdmi_set_bits(hdmi, 0x6c4, 14, 0xfffc000, w[2]); + hdmi_set_bits(hdmi, 0x6c4, 0, 0x3fff, + (!(w[7] & 1) && w[2] < 2) ? 0 : w[3]); + hdmi_set_bits(hdmi, 0x6c8, 14, 0xc000, w[0]); + hdmi_set_bits(hdmi, 0x6c8, 8, 0x100, 0); + hdmi_set_bits(hdmi, 0x6c8, 0, 0xff, w[8]); + hdmi_set_bits(hdmi, 0x6c8, 9, 0x3e00, w[5]); + hdmi_set_bits(hdmi, 0x6c8, 16, 0x10000, w[17]); + hdmi_set_bits(hdmi, 0x6c8, 26, 0xfc000000, w[9]); + hdmi_set_bits(hdmi, 0x6cc, 0, 0x7f, w[11]); + hdmi_set_bits(hdmi, 0x6cc, 9, 0x600, w[13]); + hdmi_set_bits(hdmi, 0x6cc, 11, 0x7800, w[14]); + hdmi_set_bits(hdmi, 0x6cc, 15, 0x38000, w[15]); + hdmi_set_bits(hdmi, 0x6cc, 20, 0x100000, w[16]); + hdmi_set_bits(hdmi, 0x6cc, 21, 0x200000, w[18]); + hdmi_set_bits(hdmi, 0x6cc, 22, 0xc00000, w[19]); + hdmi_set_bits(hdmi, 0x6cc, 24, 0x3000000, w[4]); + hdmi_set_bits(hdmi, 0x6cc, 26, 0xc000000, w[20]); + + /* AppleT8112DPTXPort::updateLinkRateDependentShadowRegisters */ + hdmi_apb_update(hdmi, 0xa0, BIT(3), BIT(3)); + hdmi_apb_update(hdmi, 0xa0, BIT(2), BIT(2)); + hdmi_apb_update(hdmi, 0xa0, BIT(18), BIT(18)); + hdmi_apb_update(hdmi, 0xa0, BIT(18), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(19), BIT(19)); + hdmi_apb_update(hdmi, 0xa0, BIT(19), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(2), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(3), 0); + + /* No UHBR rates; half link rate from HBR up. */ + half = rate >= DCPIB_LINK_RATE_HBR; + hdmi_set_bits(hdmi, 0x2400, 2, 0x4, 0); + hdmi_set_bits(hdmi, 0x2400, 3, 0x8, !half); + hdmi_set_bits(hdmi, 0x0000, 17, 0x20000, !half); + + ret = hdmi_pll_command(dcp, hdmi, 7); + if (ret) + return ret; + if (!(readl(hdmi->core + 0x438) & BIT(30))) { + dev_err(dcp->dev, "HDMI: PLL failed to lock at rate %#x\n", rate); + return -EIO; + } + return 0; +} + +static int hdmi_set_lane_count(struct dcpib *dcp, u32 lanes) +{ + /* T8152 takes no zero lane count. */ + if (lanes) + hdmi_set_lanes(dcp->phy_priv, lanes); + return 0; +} + +static int hdmi_set_drive(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count) +{ + return 0; +} + +static int hdmi_set_route(struct dcpib *dcp, bool enable) +{ + return 0; +} + +/* + * The sink's EDID through the AP AUX controller aux1: the phy takes its + * source from aux1 for the read (its AUX reaches the PS190, which reads the + * monitor's DDC), then from the DCP again. The display is off meanwhile. + */ +static int hdmi_read_edid(struct dcpib *dcp, u8 *buf, size_t size) +{ + struct dcpib_hdmi *hdmi = dcp->phy_priv; + u32 src; + int ret; + + if (!hdmi->aux.regs) + return -ENODEV; + ret = dcpib_aux_init(dcp, &hdmi->aux); + if (ret) + return ret; + src = readl(hdmi->apb + HDMI_SOURCE_SELECT); + writel((src & ~0xf) | HDMI_SOURCE_AUX1, hdmi->apb + HDMI_SOURCE_SELECT); + ret = dcpib_aux_read_edid(dcp, &hdmi->aux, buf, size); + writel(src, hdmi->apb + HDMI_SOURCE_SELECT); + return ret; +} + +static const struct dcpib_phy_ops hdmi_ops = { + .activate = hdmi_activate, + .deactivate = hdmi_deactivate, + .set_link_rate = hdmi_set_link_rate, + .set_lane_count = hdmi_set_lane_count, + .set_drive = hdmi_set_drive, + .set_route = hdmi_set_route, + .read_edid = hdmi_read_edid, + .max_link_rate = DCPIB_LINK_RATE_HBR3, + /* display-crossbar0 DFP endpoint of the lpdptx slot */ +}; + +/* The DCP node that describes the HDMI phy (DCPEXT0's). */ +struct device_node *dcpib_hdmi_node(void) +{ + struct device_node *np; + + for_each_compatible_node(np, NULL, "apple,t8152-dcp-iboot") + if (of_property_read_bool(np, "apple,hdmi")) + return np; + return NULL; +} + +static void __iomem *hdmi_map(struct device *dev, struct device_node *np, + const char *name) +{ + int index = of_property_match_string(np, "reg-names", name); + + if (index < 0) + return IOMEM_ERR_PTR(index); + return devm_of_iomap(dev, np, index, NULL); +} + +static bool hdmi_power_active(struct device_node *np, const char *name) +{ + int index = of_property_match_string(np, "reg-names", name); + void __iomem *regs; + u32 state; + + if (index < 0) + return false; + regs = of_iomap(np, index); + if (!regs) + return false; + state = readl(regs); + iounmap(regs); + /* Both the requested and actual PMGR states must be active. */ + return (state & GENMASK(7, 0)) == GENMASK(7, 0); +} + +/* + * The DCP the bootloader wired to the phy (aura writes its DCP index: 0 for + * the main DCP on the HDMI-only route, 1 for DCPEXT0 next to a USB-C + * display), or -ENODEV if it left the PHY off or the PLL unlocked. + */ +int dcpib_hdmi_source(struct device_node *np) +{ + int core = of_property_match_string(np, "reg-names", "phy-core"); + int apb = of_property_match_string(np, "reg-names", "phy-apb"); + void __iomem *core_regs, *apb_regs; + int ret = -ENODEV; + + /* The PHY aperture aborts while gated, including on a USB-C-only boot. */ + if (core < 0 || apb < 0 || !hdmi_power_active(np, "phy-ps")) + return -ENODEV; + core_regs = of_iomap(np, core); + apb_regs = of_iomap(np, apb); + if (core_regs && apb_regs && readl(core_regs + 0x438) & BIT(30)) + ret = readl(apb_regs + HDMI_SOURCE_SELECT) & 0xf; + if (apb_regs) + iounmap(apb_regs); + if (core_regs) + iounmap(core_regs); + if (ret == 1 && !hdmi_power_active(np, "coproc-ps")) + return -ENODEV; + if (ret != 0 && ret != 1) + return -ENODEV; + return ret; +} + +/* The phy, its AUX controller and its HPD line come from np. */ +int dcpib_hdmi_init(struct dcpib *dcp, struct device_node *np) +{ + struct dcpib_hdmi *hdmi; + + hdmi = devm_kzalloc(dcp->dev, sizeof(*hdmi), GFP_KERNEL); + if (!hdmi) + return -ENOMEM; + hdmi->core = hdmi_map(dcp->dev, np, "phy-core"); + if (IS_ERR(hdmi->core)) + return PTR_ERR(hdmi->core); + hdmi->apb = hdmi_map(dcp->dev, np, "phy-apb"); + if (IS_ERR(hdmi->apb)) + return PTR_ERR(hdmi->apb); + + /* AUX output select 0x10: the lpdptx phy */ + dcpib_aux_probe_node(dcp, &hdmi->aux, 0x10, np); + + dev_info(dcp->dev, "HDMI: inherited source select %u, PLL %slocked\n", + readl(hdmi->apb + HDMI_SOURCE_SELECT) & 0xf, + readl(hdmi->core + 0x438) & BIT(30) ? "" : "not "); + + dcp->phy = &hdmi_ops; + dcp->phy_priv = hdmi; + /* display-crossbar0 DFP endpoint lpdptx */ + dcp->dcp_output = 4; + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/iboot.c b/drivers/gpu/drm/apple_dcpib/iboot.c new file mode 100644 index 0000000000000..b6a0b0bf32a03 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/iboot.c @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* The DCP "disp0-service" iBoot interface (port of aura src/dcp_iboot.c). */ + +#include +#include + +#include "dcpib.h" + +#define IBOOT_SET_SURFACE 1 +#define IBOOT_SET_POWER 2 +#define IBOOT_GET_HPD 3 +#define IBOOT_GET_TIMING_MODES 4 +#define IBOOT_GET_COLOR_MODES 5 +#define IBOOT_SET_MODE 6 + +#define IBOOT_BUF_SIZE 0x4000 + +#define FMT_BGRA 1 +#define ADDR_PLANAR 1 +#define EOTF_GAMMA_SDR 1 + +struct iboot_txcmd { + __le32 op; + __le32 len; + __le32 unk1; + __le32 unk2; +} __packed; + +struct iboot_rxcmd { + __le32 op; + __le32 len; +} __packed; + +struct iboot_hpd { + u8 hpd; + u8 pad[3]; + __le32 timing_cnt; + __le32 color_cnt; +} __packed; + +struct iboot_plane { + __le32 unk1; + __le64 addr; + __le32 tile_size; + __le32 stride; + __le32 unk2[4]; + __le32 addr_format; + __le32 unk3; +} __packed; + +struct iboot_layer { + struct iboot_plane planes[3]; + __le32 unk; + __le32 plane_cnt; + __le32 width; + __le32 height; + __le32 surface_fmt; + __le32 colorspace; + __le32 eotf; + u8 transform; + u8 padding[3]; +} __packed; + +static int dcpib_ib_disp0_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcp->iboot_ep; + + for (unsigned int i = 0; i < ep->nr_services; i++) { + if (!strcmp(ep->services[i].name, "disp0-service") || + !strcmp(ep->services[i].name, "dispext-service")) { + dcp->iboot = &ep->services[i]; + return 0; + } + } + return -ENODEV; +} + +int dcpib_iboot_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_IBOOT, NULL, 1); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->iboot_ep = ep; + return dcpib_ib_disp0_init(dcp); +} + +/* + * The AFKEPV2 setters answer with transport status alone: supplying a reply + * buffer makes the firmware try to copy an empty message and assert. + */ +static int dcpib_ib_cmd(struct dcpib *dcp, u32 op, const void *in, + size_t in_size, void *out, size_t out_cap, + size_t min_size) +{ + struct iboot_txcmd *tx; + struct iboot_rxcmd *rx = NULL; + bool setter = op == IBOOT_SET_POWER || op == IBOOT_SET_MODE || + op == IBOOT_SET_SURFACE; + size_t rxlen = IBOOT_BUF_SIZE; + int ret; + + if (!dcp->iboot) + return -ENODEV; + tx = kzalloc(sizeof(*tx) + in_size, GFP_KERNEL); + if (!tx) + return -ENOMEM; + tx->op = cpu_to_le32(op); + tx->len = cpu_to_le32(sizeof(*tx) + in_size); + if (in_size) + memcpy(tx + 1, in, in_size); + + if (!setter) { + rx = kzalloc(rxlen, GFP_KERNEL); + if (!rx) { + ret = -ENOMEM; + goto out; + } + } + ret = dcpib_command(dcp->iboot, 0xc0, tx, sizeof(*tx) + in_size, rx, + setter ? NULL : &rxlen); + if (ret || setter) + goto out; + + if (rxlen < sizeof(*rx) || le32_to_cpu(rx->len) < sizeof(*rx) || + le32_to_cpu(rx->len) > rxlen) { + dev_err(dcp->dev, "iboot: bad response length for op %u\n", op); + ret = -EPROTO; + goto out; + } + rxlen = le32_to_cpu(rx->len) - sizeof(*rx); + if (rxlen < min_size) { + ret = -EPROTO; + goto out; + } + memcpy(out, rx + 1, min(rxlen, out_cap)); +out: + kfree(rx); + kfree(tx); + return ret; +} + +int dcpib_ib_set_power(struct dcpib *dcp, bool on) +{ + __le32 power = cpu_to_le32(on); + + /* The command carries one byte of payload. */ + return dcpib_ib_cmd(dcp, IBOOT_SET_POWER, &power, 1, NULL, 0, 0); +} + +int dcpib_ib_get_hpd(struct dcpib *dcp, u32 *timings, u32 *colors) +{ + struct iboot_hpd hpd; + int ret = dcpib_ib_cmd(dcp, IBOOT_GET_HPD, NULL, 0, &hpd, sizeof(hpd), + sizeof(hpd)); + + if (ret) + return ret; + if (timings) + *timings = le32_to_cpu(hpd.timing_cnt); + if (colors) + *colors = le32_to_cpu(hpd.color_cnt); + return !!hpd.hpd; +} + +static int dcpib_ib_get_list(struct dcpib *dcp, u32 op, size_t entry, + void **list, unsigned int *count) +{ + u8 *buf; + u32 n; + int ret; + + buf = kzalloc(IBOOT_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + ret = dcpib_ib_cmd(dcp, op, NULL, 0, buf, IBOOT_BUF_SIZE, sizeof(__le32)); + if (ret) + goto out; + /* The response holds a count followed by the entries. */ + n = le32_to_cpu(*(__le32 *)buf); + if (n > (IBOOT_BUF_SIZE - sizeof(__le32)) / entry) { + ret = -EPROTO; + goto out; + } + kfree(*list); + *list = kmemdup(buf + sizeof(__le32), n * entry, GFP_KERNEL); + *count = *list ? n : 0; + ret = *list ? 0 : -ENOMEM; +out: + kfree(buf); + return ret; +} + +int dcpib_ib_get_modes(struct dcpib *dcp) +{ + int ret; + + ret = dcpib_ib_get_list(dcp, IBOOT_GET_TIMING_MODES, + sizeof(struct dcpib_timing_mode), + (void **)&dcp->tmodes, &dcp->nr_tmodes); + if (ret) + return ret; + return dcpib_ib_get_list(dcp, IBOOT_GET_COLOR_MODES, + sizeof(struct dcpib_color_mode), + (void **)&dcp->cmodes, &dcp->nr_cmodes); +} + +int dcpib_ib_set_mode(struct dcpib *dcp, const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c) +{ + dev_dbg(dcp->dev, "SET_MODE %ux%u fps %#x, bpp %u\n", + le32_to_cpu(t->width), le32_to_cpu(t->height), + le32_to_cpu(t->fps), le32_to_cpu(c->bpp)); + struct { + struct dcpib_timing_mode t; + struct dcpib_color_mode c; + } __packed cmd = { *t, *c }; + + return dcpib_ib_cmd(dcp, IBOOT_SET_MODE, &cmd, sizeof(cmd), NULL, 0, 0); +} + +int dcpib_ib_set_surface(struct dcpib *dcp, u64 iova, u32 width, u32 height, + u32 stride) +{ + dev_dbg(dcp->dev, "SET_SURFACE %#llx %ux%u stride %u\n", iova, width, + height, stride); + struct iboot_layer layer = { + .planes = { { + .addr = cpu_to_le64(iova), + .stride = cpu_to_le32(stride), + .addr_format = cpu_to_le32(ADDR_PLANAR), + } }, + .plane_cnt = cpu_to_le32(1), + .width = cpu_to_le32(width), + .height = cpu_to_le32(height), + .surface_fmt = cpu_to_le32(FMT_BGRA), + .colorspace = cpu_to_le32(2), + .eotf = cpu_to_le32(EOTF_GAMMA_SDR), + }; + + return dcpib_ib_cmd(dcp, IBOOT_SET_SURFACE, &layer, sizeof(layer), + NULL, 0, 0); +} diff --git a/drivers/gpu/drm/apple_dcpib/kms.c b/drivers/gpu/drm/apple_dcpib/kms.c new file mode 100644 index 0000000000000..e5a6746aba8f7 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/kms.c @@ -0,0 +1,864 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * KMS on the iBoot interface: one primary plane, CRTC, encoder and + * connector. The firmware only takes width, height and refresh rate for a + * mode and picks the timing itself, so the modes are built with CVT for + * userspace. + * + * The pipe always scans out one buffer, the boot surface, which holds the + * largest mode; framebuffers are shadow buffers copied into it, and a + * modeset only changes its geometry, as aura does. Changing the surface + * address between Linux buffers made later modesets crash the firmware + * (IOMFB axi_rd_err). There is no vblank interrupt. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +/* + * Swap between the scanout buffers per frame. Off: frames are copied into + * the buffer on screen, which tears during motion. + */ +static bool buffered = true; +module_param(buffered, bool, 0644); +MODULE_PARM_DESC(buffered, "Copy each frame into a scanout buffer that is not on screen (default: on)"); + +static const u32 dcpib_formats[] = { + DRM_FORMAT_XRGB8888, + DRM_FORMAT_ARGB8888, +}; + +static int dcpib_refresh(const struct dcpib_timing_mode *t) +{ + return (le32_to_cpu(t->fps) + 0x8000) >> 16; +} + +/* + * Whether @t works: the link at the PHY's highest rate must carry it (24 bpp + * plus about 12% blanking against rate x lanes x 0.8 for 8b/10b), and the + * pixel rate must stay within what the PHY's path scans out; a mode beyond + * either only blanks the display. + */ +static bool dcpib_mode_fits(struct dcpib *dcp, const struct dcpib_timing_mode *t) +{ + /* The link's lanes even while the display is off and it is down. */ + u32 lanes = dcp->port[0].trained_lanes ?: 2; + u64 link = (u64)dcp->phy->max_link_rate * 270000000ULL * lanes * 8 / 10; + u64 pixels = (u64)le32_to_cpu(t->width) * le32_to_cpu(t->height) * + dcpib_refresh(t); + + if (pixels < dcp->phy->min_pixel_rate || + (dcp->phy->max_pixel_rate && pixels > dcp->phy->max_pixel_rate)) + return false; + return pixels * 24 * 112 / 100 <= link; +} + +/* + * The firmware mode for @mode: same size, refresh rate within 1 Hz (EDID's + * 59.94 against the firmware's 60.00), the nearest one. + */ +static const struct dcpib_timing_mode * +dcpib_find_mode(struct dcpib *dcp, const struct drm_display_mode *mode) +{ + const struct dcpib_timing_mode *best = NULL; + int refresh = drm_mode_vrefresh(mode); + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + if (!t->valid || le32_to_cpu(t->width) != mode->hdisplay || + le32_to_cpu(t->height) != mode->vdisplay || + abs(dcpib_refresh(t) - refresh) > 1 || + !dcpib_mode_fits(dcp, t)) + continue; + if (!best || abs(dcpib_refresh(t) - refresh) < + abs(dcpib_refresh(best) - refresh)) + best = t; + } + return best; +} + +/* + * aura's display_choose_color_mode: valid, at most 32 bpp, the highest + * depth, then the lowest colorimetry, encoding and EOTF. Deeper modes the + * HDMI converter offers are refused by SET_MODE. + */ +static bool dcpib_color_better(const struct dcpib_color_mode *a, + const struct dcpib_color_mode *b) +{ + u32 abpp = le32_to_cpu(a->bpp), bbpp = le32_to_cpu(b->bpp); + + if (!!a->valid != !!b->valid) + return a->valid; + if ((abpp <= 32) != (bbpp <= 32)) + return abpp <= 32; + if (abpp != bbpp) + return abpp > bbpp; + if (a->colorimetry != b->colorimetry) + return le32_to_cpu(a->colorimetry) < le32_to_cpu(b->colorimetry); + if (a->encoding != b->encoding) + return le32_to_cpu(a->encoding) < le32_to_cpu(b->encoding); + return le32_to_cpu(a->eotf) < le32_to_cpu(b->eotf); +} + +static const struct dcpib_color_mode *dcpib_find_color(struct dcpib *dcp) +{ + const struct dcpib_color_mode *best = NULL; + + for (unsigned int i = 0; i < dcp->nr_cmodes; i++) + if (!best || dcpib_color_better(&dcp->cmodes[i], best)) + best = &dcp->cmodes[i]; + return best && best->valid ? best : NULL; +} + +/* + * The largest mode from 50 to 60 Hz for the console, up to 1920x1080 on + * USB-C: on the 2-lane HBR link of the J873g USB-C monitor 1920x1080@60 + * works, while SET_MODE 1920x1200@60 crashes the firmware. Low refresh + * modes (2560x1440@30) are left to userspace. + */ +static const struct dcpib_timing_mode *dcpib_preferred(struct dcpib *dcp) +{ + const struct dcpib_timing_mode *best = NULL; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + /* + * Byte 0 bit 6 marks standard modes a converter adds on its own + * (the J873g PS190 lists 3840x2160 for a 2560x1440 monitor), as + * aura's "max" mode skips them. + */ + if (!t->valid || dcpib_refresh(t) > 60 || dcpib_refresh(t) < 50 || + (t->pad[0] & 0x40) || + (!dcp->hdmi && (le32_to_cpu(t->width) > 1920 || + le32_to_cpu(t->height) > 1080)) || + !dcpib_mode_fits(dcp, t)) + continue; + if (!best || (u64)le32_to_cpu(t->width) * le32_to_cpu(t->height) > + (u64)le32_to_cpu(best->width) * le32_to_cpu(best->height) || + (t->width == best->width && t->height == best->height && + le32_to_cpu(t->fps) > le32_to_cpu(best->fps))) + best = t; + } + return best; +} + +/* + * The sink's EDID modes the firmware also has (mode_valid drops the rest), + * the largest of them preferred: firmware modes the sink does not list, + * such as 1400x1050 on the J873g EV2780, stay black. + */ +static int dcpib_edid_get_modes(struct drm_connector *connector) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + struct drm_display_mode *mode, *best = NULL; + int count = drm_edid_connector_add_modes(connector); + + mutex_lock(&dcp->lock); + list_for_each_entry(mode, &connector->probed_modes, head) { + mode->type &= ~DRM_MODE_TYPE_PREFERRED; + if (!dcpib_find_mode(dcp, mode)) + continue; + if (!best || mode->hdisplay * mode->vdisplay > + best->hdisplay * best->vdisplay || + (mode->hdisplay * mode->vdisplay == + best->hdisplay * best->vdisplay && + drm_mode_vrefresh(mode) > drm_mode_vrefresh(best))) + best = mode; + } + if (best) + best->type |= DRM_MODE_TYPE_PREFERRED; + mutex_unlock(&dcp->lock); + return count; +} + +static int dcpib_connector_get_modes(struct drm_connector *connector) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + const struct dcpib_timing_mode *preferred; + int count = 0; + + drm_edid_connector_update(connector, dcp->edid); + if (dcp->edid) + return dcpib_edid_get_modes(connector); + + /* Without an EDID, the firmware's list. */ + mutex_lock(&dcp->lock); + preferred = dcpib_preferred(dcp); + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + struct drm_display_mode *mode; + + if (!t->valid || !dcpib_mode_fits(dcp, t)) + continue; + mode = drm_cvt_mode(connector->dev, le32_to_cpu(t->width), + le32_to_cpu(t->height), dcpib_refresh(t), + false, false, false); + if (!mode) + continue; + if (t == preferred) + mode->type |= DRM_MODE_TYPE_PREFERRED; + drm_mode_probed_add(connector, mode); + count++; + } + mutex_unlock(&dcp->lock); + return count; +} + +static enum drm_mode_status +dcpib_connector_mode_valid(struct drm_connector *connector, + const struct drm_display_mode *mode) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + + return dcpib_find_mode(dcp, mode) ? MODE_OK : MODE_NOMODE; +} + +static const struct drm_connector_helper_funcs dcpib_connector_helper = { + .get_modes = dcpib_connector_get_modes, + .mode_valid = dcpib_connector_mode_valid, +}; + +static int dcpib_set_mode_locked(struct dcpib *dcp, + const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c); + +/* Read the sink's EDID through the phy, if it can, and keep it. */ +void dcpib_update_edid(struct dcpib *dcp) +{ + const struct drm_edid *edid = NULL; + u8 *buf; + int len; + + if (!dcp->phy->read_edid) + return; + buf = kzalloc(4 * EDID_LENGTH, GFP_KERNEL); + if (!buf) + return; + len = dcp->phy->read_edid(dcp, buf, 4 * EDID_LENGTH); + if (len >= EDID_LENGTH) { + edid = drm_edid_alloc(buf, len); + if (edid && !drm_edid_valid(edid)) { + drm_edid_free(edid); + edid = NULL; + } + } + if (!edid) + dev_warn(dcp->dev, "no EDID: %d\n", len); + else + dev_info(dcp->dev, "EDID: %d bytes\n", len); + drm_edid_free(dcp->edid); + dcp->edid = edid; + kfree(buf); +} + +static enum drm_connector_status +dcpib_connector_detect(struct drm_connector *connector, bool force) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + bool flipped = false; + int hpd; + + if (dcp->hpd_gpio) + hpd = gpiod_get_value_cansleep(dcp->hpd_gpio); + else if (dcp->phy->detect) + hpd = dcp->phy->detect(dcp, &flipped); + else + return dcp->nr_tmodes ? connector_status_connected : + connector_status_disconnected; + if (hpd < 0) + return connector_status_unknown; + if (hpd != dcp->hpd) { + dev_info(dcp->dev, "sink %s\n", hpd ? "plugged" : "unplugged"); + dcp->hpd = hpd; + mutex_lock(&dcp->lock); + /* + * USB-C: the DPTX port's HPD follows the PD controller, and a + * new plug may come the other way round. + */ + if (dcp->pd) { + /* + * A plug while the display stayed on (unplug and plug + * between two polls): start the PHY cold again, the + * new plug may come the other way round. + */ + if (hpd && dcp->powered) + dcp->phy->deactivate(dcp); + /* The sink may be on another port now. */ + if (hpd && dcp->phy->select) + dcp->phy->select(dcp); + if (hpd && dcp->phy->set_orientation) + dcp->phy->set_orientation(dcp, flipped); + /* The firmware talks AUX as soon as it sees HPD. */ + if (hpd && dcp->phy->activate(dcp)) + dev_err(dcp->dev, "phy activation failed\n"); + if (dcpib_dptx_set_hpd(dcp, 0, hpd)) + dev_err(dcp->dev, "DPTX HPD update failed\n"); + } + /* A new sink: the firmware reads its EDID, take the new lists. */ + if (hpd && (dcpib_wait_hpd(dcp) || dcpib_ib_get_modes(dcp))) + dev_err(dcp->dev, "no modes from the new sink\n"); + /* + * The EDID once the firmware has read it, so the sink's DDC + * answers, and before SET_MODE: reading it borrows the AUX path + * while the firmware does no AUX. + */ + if (hpd) + dcpib_update_edid(dcp); + /* The unplug dropped the link; SET_MODE trains it again. */ + if (hpd && dcp->powered && (dcp->mode_set || dcp->mode_failed)) { + struct dcpib_timing_mode t = dcp->cur_mode; + const struct dcpib_color_mode *c = dcpib_find_color(dcp); + + if (c) + dcpib_set_mode_locked(dcp, &t, c); + } + mutex_unlock(&dcp->lock); + } + return hpd && dcp->nr_tmodes ? connector_status_connected : + connector_status_disconnected; +} + +static const struct drm_connector_funcs dcpib_connector_funcs = { + .detect = dcpib_connector_detect, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = drm_connector_cleanup, + .reset = drm_atomic_helper_connector_reset, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, +}; + +static int dcpib_plane_atomic_check(struct drm_plane *plane, + struct drm_atomic_commit *state) +{ + struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); + struct drm_crtc_state *crtc_state = NULL; + + if (new->crtc) + crtc_state = drm_atomic_get_new_crtc_state(state, new->crtc); + /* The surface fills the whole mode; no scaling or positioning. */ + return drm_atomic_helper_check_plane_state(new, crtc_state, + DRM_PLANE_NO_SCALING, + DRM_PLANE_NO_SCALING, + false, false); +} + +static void dcpib_rect_grow(struct drm_rect *r, const struct drm_rect *add) +{ + if (!drm_rect_visible(r)) { + *r = *add; + return; + } + r->x1 = min(r->x1, add->x1); + r->y1 = min(r->y1, add->y1); + r->x2 = max(r->x2, add->x2); + r->y2 = max(r->y2, add->y2); +} + +/* The current mode's refresh period, from its exact 16.16 rate. */ +static s64 dcpib_period_ns(struct dcpib *dcp) +{ + u32 fps = le32_to_cpu(dcp->cur_mode.fps); + + if (fps < (24 << 16)) + fps = 60 << 16; + return div64_u64((u64)NSEC_PER_SEC << 16, fps); +} + +/* Show scan[buf]. Called with dcp->lock held. */ +static void dcpib_swap_locked(struct dcpib *dcp, unsigned int buf, u32 w, + u32 h, u32 pitch) +{ + int ret; + + /* The pipe must see the whole frame before it is swapped in. */ + wmb(); + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[buf], w, h, pitch); + if (ret) { + /* Still on the old buffer, which misses this frame. */ + dev_err_ratelimited(dcp->dev, "swap failed: %d\n", ret); + dcp->stale[dcp->front] = DRM_RECT_INIT(0, 0, INT_MAX / 2, INT_MAX / 2); + return; + } + dcp->last_swap = ktime_get(); + dcp->retired[dcp->front] = dcp->last_swap; + dcp->front = buf; +} + +static void dcpib_swap_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(work, struct dcpib, swap_work); + + mutex_lock(&dcp->lock); + if (dcp->pending >= 0) { + dcpib_swap_locked(dcp, dcp->pending, dcp->pending_w, + dcp->pending_h, dcp->pending_pitch); + dcp->pending = -1; + } + mutex_unlock(&dcp->lock); +} + +static enum hrtimer_restart dcpib_swap_timer(struct hrtimer *t) +{ + struct dcpib *dcp = container_of(t, struct dcpib, swap_timer); + + queue_work(system_highpri_wq, &dcp->swap_work); + return HRTIMER_NORESTART; +} + +/* + * Copy the frame into a scanout buffer that is not on screen and show it. + * That buffer already holds an older frame: copy this frame's damage and + * what changed since that buffer was last written. + * + * SET_SURFACE returns before the pipe takes the buffer at its next frame + * start, and there is no vblank interrupt. Several swaps within one refresh + * let an older frame stay on screen (a cursor that jumps back), so swaps are + * spaced one refresh apart: a frame that comes sooner waits in a pending + * buffer, which a newer frame overwrites, and a timer swaps it in. The + * commit itself never waits, so a client's next frame is not held up. + */ +static void dcpib_plane_atomic_update(struct drm_plane *plane, + struct drm_atomic_commit *state) +{ + struct drm_plane_state *old = drm_atomic_get_old_plane_state(state, plane); + struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); + struct drm_shadow_plane_state *shadow = to_drm_shadow_plane_state(new); + struct dcpib *dcp = to_dcpib(plane->dev); + struct drm_framebuffer *fb = new->fb; + struct drm_atomic_helper_damage_iter iter; + struct drm_rect clip, damage = { 0, 0, 0, 0 }; + struct drm_rect full = DRM_RECT_INIT(0, 0, fb ? fb->width : 0, + fb ? fb->height : 0); + unsigned int pitch, back; + struct iosys_map dst; + s64 period, since; + + if (!fb || !new->visible) + return; + pitch = (new->src_w >> 16) * 4; + drm_atomic_helper_damage_iter_init(&iter, old, new); + drm_atomic_for_each_plane_damage(&iter, &clip) + dcpib_rect_grow(&damage, &clip); + if (!drm_rect_visible(&damage)) + return; + + mutex_lock(&dcp->lock); + period = dcpib_period_ns(dcp); + if (!READ_ONCE(buffered)) { + /* Into the buffer on screen (tears during motion). */ + back = dcp->front; + } else if (dcp->pending >= 0) { + back = dcp->pending; + } else { + /* The other buffer that left the screen first. */ + unsigned int a = (dcp->front + 1) % DCPIB_SCANOUT_BUFS; + unsigned int b = (dcp->front + 2) % DCPIB_SCANOUT_BUFS; + s64 wait; + + back = ktime_before(dcp->retired[a], dcp->retired[b]) ? a : b; + /* Off screen for a full refresh: only after a mode change. */ + wait = period - ktime_to_ns(ktime_sub(ktime_get(), dcp->retired[back])); + if (dcp->mode_set && wait > 0) + usleep_range(div_s64(wait, NSEC_PER_USEC), + div_s64(wait, NSEC_PER_USEC) + 100); + } + clip = dcp->stale[back]; + dcpib_rect_grow(&clip, &damage); + if (drm_rect_intersect(&clip, &full)) { + iosys_map_set_vaddr(&dst, dcp->scan_wc[back]); + iosys_map_incr(&dst, drm_fb_clip_offset(pitch, fb->format, &clip)); + drm_fb_memcpy(&dst, &pitch, shadow->data, fb, &clip); + } + dcp->stale[back] = (struct drm_rect){ 0, 0, 0, 0 }; + for (unsigned int i = 0; i < DCPIB_SCANOUT_BUFS; i++) + if (i != back) + dcpib_rect_grow(&dcp->stale[i], &damage); + if (back == dcp->front) + goto out; + + since = ktime_to_ns(ktime_sub(ktime_get(), dcp->last_swap)); + if (dcp->pending < 0 && (!dcp->mode_set || since >= period)) { + dcpib_swap_locked(dcp, back, new->src_w >> 16, new->src_h >> 16, + pitch); + goto out; + } + dcp->pending_w = new->src_w >> 16; + dcp->pending_h = new->src_h >> 16; + dcp->pending_pitch = pitch; + if (dcp->pending < 0) { + dcp->pending = back; + hrtimer_start(&dcp->swap_timer, + ktime_add_ns(dcp->last_swap, period), + HRTIMER_MODE_ABS); + } +out: + mutex_unlock(&dcp->lock); +} + +static const struct drm_plane_helper_funcs dcpib_plane_helper = { + DRM_GEM_SHADOW_PLANE_HELPER_FUNCS, + .atomic_check = dcpib_plane_atomic_check, + .atomic_update = dcpib_plane_atomic_update, +}; + +static const struct drm_plane_funcs dcpib_plane_funcs = { + .update_plane = drm_atomic_helper_update_plane, + .disable_plane = drm_atomic_helper_disable_plane, + .destroy = drm_plane_cleanup, + DRM_GEM_SHADOW_PLANE_FUNCS, +}; + +static int dcpib_crtc_atomic_check(struct drm_crtc *crtc, + struct drm_atomic_commit *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + + /* No vblank interrupt: the helpers complete events after the flush. */ + new->no_vblank = true; + if (new->enable && !dcpib_find_mode(dcp, &new->mode)) + return -EINVAL; + return 0; +} + +/* SET_MODE, then the scanout buffer with the new mode's geometry. */ +static int dcpib_set_mode_locked(struct dcpib *dcp, + const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c) +{ + int ret; + + /* + * Right after power on the firmware still trains the link and fails + * SET_MODE with kIOReturnError; it takes the mode once it is up. + */ + for (int i = 0; i < 5; i++) { + ret = dcpib_ib_set_mode(dcp, t, c); + if (ret != -EIO) + break; + msleep(200); + } + if (ret) { + dev_err(dcp->dev, "set_mode failed: %d\n", ret); + /* + * USB-C: a sink that no longer answers AUX (seen after the + * monitor's deep sleep) comes back with a fresh DP alt mode + * entry; the plug that follows sets the mode again. At most + * once a minute, so a dead sink does not loop. + */ + if (dcp->pd && time_after(jiffies, dcp->pd_reset_time + 60 * HZ)) { + dcp->cur_mode = *t; + dcp->mode_failed = true; + dcp->pd_reset_time = jiffies; + schedule_work(&dcp->pd_reset_work); + } + return ret; + } + dcp->cur_mode = *t; + dcp->mode_set = true; + dcp->mode_failed = false; + /* Both buffers hold the old geometry: the next frame is copied whole. */ + for (unsigned int i = 0; i < DCPIB_SCANOUT_BUFS; i++) + dcp->stale[i] = DRM_RECT_INIT(0, 0, INT_MAX / 2, INT_MAX / 2); + /* A pending frame has the old geometry. */ + dcp->pending = -1; + /* As aura swaps after a modeset. */ + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[dcp->front], le32_to_cpu(t->width), + le32_to_cpu(t->height), + le32_to_cpu(t->width) * 4); + if (ret) + dev_err(dcp->dev, "set_surface failed: %d\n", ret); + return ret; +} + +/* + * The boot mode again: a display powered on without SET_MODE counts as idle + * to the firmware's AV stack, which powers the link off after 30 s. + */ +int dcpib_set_boot_mode(struct dcpib *dcp) +{ + const struct dcpib_timing_mode *best = NULL; + const struct dcpib_color_mode *c; + int ret = -ENOENT; + + mutex_lock(&dcp->lock); + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + if (!t->valid || le32_to_cpu(t->width) != dcp->boot_w || + le32_to_cpu(t->height) != dcp->boot_h || + dcpib_refresh(t) > 60) + continue; + if (!best || le32_to_cpu(t->fps) > le32_to_cpu(best->fps)) + best = t; + } + c = dcpib_find_color(dcp); + if (best && c) + ret = dcpib_set_mode_locked(dcp, best, c); + mutex_unlock(&dcp->lock); + return ret; +} + +/* + * The DCPAV service starts the firmware's idle power off timer when its video + * interface comes up: until a SET_MODE follows, it counts the powered-on + * display as idle and takes the link down 30 s later. The probe's SET_MODE + * comes before the interface starts, so one follows it here. + */ +void dcpib_reset_mode(struct dcpib *dcp) +{ + const struct dcpib_color_mode *c; + + mutex_lock(&dcp->lock); + c = dcpib_find_color(dcp); + if (c && dcp->powered && dcp->mode_set) + dcpib_set_mode_locked(dcp, &dcp->cur_mode, c); + mutex_unlock(&dcp->lock); +} + +void dcpib_av_started(struct dcpib *dcp) +{ + /* Let the firmware finish opening the interface before the SET_MODE. */ + queue_delayed_work(system_wq, &dcp->av_work, msecs_to_jiffies(1000)); +} + +static void dcpib_crtc_atomic_enable(struct drm_crtc *crtc, + struct drm_atomic_commit *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + const struct dcpib_timing_mode *t; + const struct dcpib_color_mode *c; + int ret; + + mutex_lock(&dcp->lock); + t = dcpib_find_mode(dcp, &new->mode); + c = dcpib_find_color(dcp); + if (!t || !c) + goto out; + if (!dcp->powered) { + /* The phy starts cold first; the firmware then trains. */ + ret = dcp->phy->activate(dcp); + if (ret) + dev_err(dcp->dev, "phy activation failed: %d\n", ret); + /* A pipe powered on without a surface fetches from a null base. */ + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[dcp->front], dcp->boot_w, + dcp->boot_h, dcp->boot_stride); + if (ret) + dev_err(dcp->dev, "set_surface failed: %d\n", ret); + ret = dcpib_ib_set_power(dcp, true); + if (ret) + dev_err(dcp->dev, "set_power failed: %d\n", ret); + dcp->powered = !ret; + } + if (dcp->mode_set && !memcmp(t, &dcp->cur_mode, sizeof(*t))) + goto out; + dev_info(dcp->dev, "modeset %ux%u@%d\n", le32_to_cpu(t->width), + le32_to_cpu(t->height), dcpib_refresh(t)); + /* + * As aura's display_configure: query the sink and both mode lists + * again before SET_MODE, and look the mode up in the fresh list. The + * DPTX port stays connected; reconnecting it here drops the link and + * the HDMI firmware then fails SET_MODE. + */ + ret = dcpib_ib_set_power(dcp, true); + if (!ret) + ret = dcpib_ib_get_hpd(dcp, NULL, NULL) < 0; + if (!ret) + ret = dcpib_ib_get_modes(dcp); + if (ret) + dev_err(dcp->dev, "refreshing the modes failed: %d\n", ret); + t = dcpib_find_mode(dcp, &new->mode); + c = dcpib_find_color(dcp); + if (!t || !c) + goto out; + dcpib_set_mode_locked(dcp, t, c); +out: + mutex_unlock(&dcp->lock); +} + +static void dcpib_crtc_atomic_disable(struct drm_crtc *crtc, + struct drm_atomic_commit *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + int ret; + + /* A mode change: SET_MODE alone switches, as before. */ + if (new->active) + return; + /* + * Display off (DPMS, blanking): the firmware stops the pipe and the + * monitor goes to sleep. The next enable sets the mode again. + */ + mutex_lock(&dcp->lock); + if (dcp->powered) { + ret = dcpib_ib_set_power(dcp, false); + if (ret) { + dev_err(dcp->dev, "set_power off failed: %d\n", ret); + } else { + dev_info(dcp->dev, "display off\n"); + /* Nothing scans out: power the phy down as well. */ + if (!dcp->phy->deactivate(dcp)) + dcp->port[0].link_rate = 0; + } + dcp->powered = !!ret; + dcp->mode_set = false; + } + mutex_unlock(&dcp->lock); +} + +static const struct drm_crtc_helper_funcs dcpib_crtc_helper = { + .atomic_check = dcpib_crtc_atomic_check, + .atomic_enable = dcpib_crtc_atomic_enable, + .atomic_disable = dcpib_crtc_atomic_disable, +}; + +static const struct drm_crtc_funcs dcpib_crtc_funcs = { + .reset = drm_atomic_helper_crtc_reset, + .destroy = drm_crtc_cleanup, + .set_config = drm_atomic_helper_set_config, + .page_flip = drm_atomic_helper_page_flip, + .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state, +}; + +static const struct drm_encoder_funcs dcpib_encoder_funcs = { + .destroy = drm_encoder_cleanup, +}; + +/* Modeset before the plane update, which copies at the new geometry. */ +static const struct drm_mode_config_helper_funcs dcpib_mode_config_helpers = { + .atomic_commit_tail = drm_atomic_helper_commit_tail_rpm, +}; + +static const struct drm_mode_config_funcs dcpib_mode_config_funcs = { + /* fbdev writes reach the scanout buffer through the dirty damage */ + .fb_create = drm_gem_fb_create_with_dirty, + .atomic_check = drm_atomic_helper_check, + .atomic_commit = drm_atomic_helper_commit, +}; + +static void dcpib_pd_reset_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(work, struct dcpib, pd_reset_work); + int ret; + + dev_info(dcp->dev, "USB-C sink does not answer, entering DP alt mode again\n"); + ret = dcpib_pd_reconnect(dcp); + if (ret) { + dev_err(dcp->dev, "DP alt mode entry failed: %d\n", ret); + return; + } + /* The controller unplugged and plugged: run the plug path. */ + mutex_lock(&dcp->lock); + dcp->hpd = 0; + mutex_unlock(&dcp->lock); + drm_kms_helper_hotplug_event(&dcp->drm); +} + +void dcpib_kms_stop(struct dcpib *dcp) +{ + if (!dcp->kms_work_ready) + return; + + hrtimer_cancel(&dcp->swap_timer); + cancel_work_sync(&dcp->swap_work); + cancel_work_sync(&dcp->pd_reset_work); +} + +static void dcpib_swap_stop(void *data) +{ + dcpib_kms_stop(data); +} + +int dcpib_kms_init(struct dcpib *dcp) +{ + struct drm_device *drm = &dcp->drm; + u32 max_w = 1920, max_h = 1080; + int ret; + + dcp->pending = -1; + INIT_WORK(&dcp->swap_work, dcpib_swap_work); + INIT_WORK(&dcp->pd_reset_work, dcpib_pd_reset_work); + hrtimer_setup(&dcp->swap_timer, dcpib_swap_timer, CLOCK_MONOTONIC, + HRTIMER_MODE_ABS); + dcp->kms_work_ready = true; + ret = devm_add_action_or_reset(dcp->dev, dcpib_swap_stop, dcp); + if (ret) + return ret; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + max_w = max(max_w, le32_to_cpu(dcp->tmodes[i].width)); + max_h = max(max_h, le32_to_cpu(dcp->tmodes[i].height)); + } + + ret = drmm_mode_config_init(drm); + if (ret) + return ret; + dcp->pd_reset_time = jiffies - 61 * HZ; + + drm->mode_config.min_width = 320; + drm->mode_config.min_height = 240; + drm->mode_config.max_width = max_w; + drm->mode_config.max_height = max_h; + drm->mode_config.preferred_depth = 24; + drm->mode_config.funcs = &dcpib_mode_config_funcs; + drm->mode_config.helper_private = &dcpib_mode_config_helpers; + + ret = drm_universal_plane_init(drm, &dcp->plane, 0, &dcpib_plane_funcs, + dcpib_formats, ARRAY_SIZE(dcpib_formats), + NULL, DRM_PLANE_TYPE_PRIMARY, NULL); + if (ret) + return ret; + drm_plane_helper_add(&dcp->plane, &dcpib_plane_helper); + drm_plane_enable_fb_damage_clips(&dcp->plane); + + ret = drm_crtc_init_with_planes(drm, &dcp->crtc, &dcp->plane, NULL, + &dcpib_crtc_funcs, NULL); + if (ret) + return ret; + drm_crtc_helper_add(&dcp->crtc, &dcpib_crtc_helper); + + ret = drm_encoder_init(drm, &dcp->encoder, &dcpib_encoder_funcs, + DRM_MODE_ENCODER_TMDS, NULL); + if (ret) + return ret; + dcp->encoder.possible_crtcs = drm_crtc_mask(&dcp->crtc); + + ret = drm_connector_init(drm, &dcp->connector, &dcpib_connector_funcs, + dcp->hdmi ? DRM_MODE_CONNECTOR_HDMIA : + DRM_MODE_CONNECTOR_DisplayPort); + if (ret) + return ret; + drm_connector_helper_add(&dcp->connector, &dcpib_connector_helper); + if (dcp->hpd_gpio || dcp->pd) + dcp->connector.polled = DRM_CONNECTOR_POLL_CONNECT | + DRM_CONNECTOR_POLL_DISCONNECT; + ret = drm_connector_attach_encoder(&dcp->connector, &dcp->encoder); + if (ret) + return ret; + + drm_mode_config_reset(drm); + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/pd.c b/drivers/gpu/drm/apple_dcpib/pd.c new file mode 100644 index 0000000000000..84ea367feb1cb --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/pd.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * USB-C plug state from the port's ACE3 PD controller (J873g hpm3) over + * SPMI, for the USB-C display's hotplug: the DP alt mode connection and the + * plug orientation. Logical registers are read through a window (aura + * src/ace3.c): select with a register-zero write, wait for the busy bit to + * clear, read the size at 0x1f and the data from 0x20. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define ACE3_SELECT 0x00 +#define ACE3_SELECT_BUSY BIT(7) +#define ACE3_SIZE 0x1f +#define ACE3_DATA 0x20 +#define ACE3_BURST 16 + +#define ACE3_REG_CMD1 0x08 +#define ACE3_REG_DATA1 0x09 +#define ACE3_REG_STATUS 0x1a +#define ACE3_REG_DATA_STATUS 0x5f + +#define STATUS_PLUG_PRESENT BIT(0) +#define STATUS_UPSIDE_DOWN BIT(4) +#define DATA_DP_CONNECTION BIT(8) +#define DATA_HPD_LEVEL BIT(15) /* CD321x, as tipd */ + +static int ace3_read(struct spmi_device *sdev, u8 reg, u8 *buf, size_t len) +{ + u8 sel, size; + int ret, err = 0; + + ret = spmi_register_zero_write(sdev, reg); + if (ret) + return ret; + ret = read_poll_timeout(spmi_ext_register_read, err, + err || !(sel & ACE3_SELECT_BUSY), 50, 20000, + false, sdev, ACE3_SELECT, &sel, 1); + if (ret || err) + return ret ?: err; + ret = spmi_ext_register_read(sdev, ACE3_SIZE, &size, 1); + if (ret) + return ret; + if (size < len) + return -EIO; + for (size_t done = 0; done < len; done += ACE3_BURST) { + ret = spmi_ext_register_read(sdev, ACE3_DATA + done, buf + done, + min_t(size_t, len - done, ACE3_BURST)); + if (ret) + return ret; + } + return 0; +} + +static int ace3_select(struct spmi_device *sdev, u8 reg) +{ + u8 sel; + int ret, err = 0; + + ret = spmi_register_zero_write(sdev, reg); + if (ret) + return ret; + ret = read_poll_timeout(spmi_ext_register_read, err, + err || !(sel & ACE3_SELECT_BUSY), 50, 20000, + false, sdev, ACE3_SELECT, &sel, 1); + return ret ?: err; +} + +static int ace3_write(struct spmi_device *sdev, u8 reg, const u8 *buf, + size_t len) +{ + int ret = ace3_select(sdev, reg); + + return ret ?: spmi_ext_register_write(sdev, ACE3_DATA, (u8 *)buf, len); +} + +/* A 4CC command with an optional data byte; 0 once the controller took it. */ +static int ace3_cmd(struct spmi_device *sdev, const char *cmd, int data) +{ + u8 val[4]; + int ret; + + ret = spmi_command_wakeup(sdev); + if (ret) + return ret; + if (data >= 0) { + u8 d = data; + + ret = ace3_write(sdev, ACE3_REG_DATA1, &d, 1); + if (ret) + return ret; + } + ret = ace3_write(sdev, ACE3_REG_CMD1, cmd, 4); + if (ret) + return ret; + for (int i = 0; i < 1000; i++) { + ret = ace3_read(sdev, ACE3_REG_CMD1, val, sizeof(val)); + if (ret) + return ret; + if (!memcmp(val, "!CMD", 4)) + return -EIO; + if (!memcmp(val, "\0\0\0\0", 4)) + return 0; + usleep_range(1000, 2000); + } + return -ETIMEDOUT; +} + +/* + * Leave and enter DP alt mode again, as unplugging and plugging the cable + * does: reset the controller (Gaid), then power the port up again once it + * takes commands (SSPS 0, as aura). A sink that stopped answering AUX after + * a monitor's deep sleep comes back this way. + */ +int dcpib_pd_reconnect(struct dcpib *dcp) +{ + int ret; + + /* Gaid resets the controller before it can report completion. */ + ret = spmi_command_wakeup(dcp->pd); + if (!ret) + ret = ace3_write(dcp->pd, ACE3_REG_CMD1, "Gaid", 4); + if (ret) + return ret; + for (int i = 0; i < 60; i++) { + msleep(500); + ret = ace3_cmd(dcp->pd, "SSPS", 0); + if (!ret) + return 0; + } + return ret; +} + +/* 1 with a DisplayPort sink in DP alt mode and HPD high, 0 without. */ +int dcpib_pd_sink(struct spmi_device *pd, bool *flipped) +{ + u8 status[4], data[4]; + int ret; + + /* Without a plug the controller sleeps and ignores selects. */ + ret = spmi_command_wakeup(pd); + if (ret) + return ret; + ret = ace3_read(pd, ACE3_REG_STATUS, status, sizeof(status)); + if (!ret) + ret = ace3_read(pd, ACE3_REG_DATA_STATUS, data, sizeof(data)); + if (ret) + return ret; + *flipped = status[0] & STATUS_UPSIDE_DOWN; + /* The sink answers AUX once it raised HPD, not at alt mode entry. */ + return (status[0] & STATUS_PLUG_PRESENT) && + (get_unaligned_le32(data) & DATA_DP_CONNECTION) && + (get_unaligned_le32(data) & DATA_HPD_LEVEL); +} + +/* The PD controller @np's apple,usb-pd points at, NULL without one. */ +struct spmi_device *dcpib_pd_get(struct dcpib *dcp, struct device_node *np) +{ + struct device_node *pd_np = of_parse_phandle(np, "apple,usb-pd", 0); + struct spmi_device *pd; + + if (!pd_np) + return NULL; + /* Holds a reference for the lifetime of the (unbindable) device. */ + pd = spmi_find_device_by_of_node(pd_np); + of_node_put(pd_np); + return pd ?: ERR_PTR(-EPROBE_DEFER); +} diff --git a/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs b/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs index 71a82541685e0..5e60e8bf37b84 100644 --- a/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs +++ b/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs @@ -78,6 +78,13 @@ const WITNESS_RANGES: [(u64, usize, u32); 25] = [ (0x100_002d_8000, 0xc0000, 4), ]; +/// Every colour-a page (address, captured usage) in file order, for the +/// stage-10 layout that is mapped without a capture file. +pub(crate) fn color_a_pages() -> impl Iterator { + RANGES.iter().flat_map(|&(base, length, usage)| + (0..length).step_by(PAGE_BYTES).map(move |offset| (base + offset as u64, usage))) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Capture { ColorA = 1, ColorB = 2, GearsG1 = 3, FragmentWitness = 4 } diff --git a/drivers/gpu/drm/asahi/g18/initdata/mod.rs b/drivers/gpu/drm/asahi/g18/initdata/mod.rs index 05c0d1c17633e..50b8901e252a4 100644 --- a/drivers/gpu/drm/asahi/g18/initdata/mod.rs +++ b/drivers/gpu/drm/asahi/g18/initdata/mod.rs @@ -424,7 +424,8 @@ impl G18FirmwareMemory { let pool = self.uma_pool.prepare_binding()?; self.uma_pool.load_hardware_descriptor(dev, &mut self.uma, pool, j873_sma_device_words(2, 6)?)?; job.parameter_buffer().load_row(&mut self.pbdesc)?; - job.encode_h1(dev)?; + // Without a capture (stage 10 only) the first application frame encodes. + if !job.layout_only_payload() { job.encode_h1(dev)?; } Ok(pool) } diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs index de7e2cd51f5d2..01952223a0593 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs @@ -136,6 +136,7 @@ impl G18GraphicsJob { } pub(crate) fn vm(&self) -> &mmu::Vm { self.program.vm() } + pub(crate) fn layout_only_payload(&self) -> bool { self.program.layout_only_payload() } pub(crate) fn context_id(&self) -> u8 { self.program.context_id() } pub(crate) fn scheduler_record(&self) -> Result { Ok(self.scheduler.binding(0)?.record) } pub(crate) fn queues(&mut self) -> &mut G18GraphicsQueues { &mut self.queues } @@ -288,7 +289,7 @@ impl G18GraphicsJob { /// Caller has allocated/retained PB and pool owners and filled all native /// fields. In particular a default resource word is not an admitted policy. #[inline(never)] - pub(crate) fn encode(&mut self, dev: &AsahiDrmDevice, inputs: &cmd::G18RenderCommandInputs) -> Result { + pub(crate) fn encode(&mut self, _dev: &AsahiDrmDevice, inputs: &cmd::G18RenderCommandInputs) -> Result { if self.encoded.is_some() || self.controls.ta.resource_word == 0 || self.controls.fragment.resource_word == 0 || inputs.context_id != u32::from(self.context_id()) diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs index 5a43ad3816e62..3eb58cce499b9 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs @@ -93,10 +93,10 @@ impl ApplicationJob { self.guard=Some(guard);self.generation=frame.generation;self.retired=false; self.client_frames=if new_client {1} else {self.client_frames.saturating_add(1)}; if remap { - dev_info!(dev.as_ref(),"G18_APP_VM frame={} vm={} context={} target={}x{} aliases={}\n", + crate::g18_kick_info!(dev.as_ref(),"G18_APP_VM frame={} vm={} context={} target={}x{} aliases={}\n", frame.generation,vm.id(),self.job.context_id(),render.target.width,render.target.height,self.user_aliases.len()); for (target,name) in self.job.targets.iter().zip(assembly::TARGET_NAMES) { - dev_info!(dev.as_ref(),"G18_APP_TARGET name={} va={:#x} bytes={:#x} context={}\n", + crate::g18_kick_info!(dev.as_ref(),"G18_APP_TARGET name={} va={:#x} bytes={:#x} context={}\n", name,target.iova(),target.logical_size,self.job.context_id()); } } diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs index 0a9a9504925b2..3175072b950e8 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs @@ -30,49 +30,81 @@ impl G18GraphicsMemory { _ => return Err(EINVAL), }; if !uat.is_t8152_geometry() || mmu::UAT_PGSZ != payload::PAGE_BYTES { return Err(ENODEV); } - let firmware = Firmware::request(name, dev.as_ref().as_ref())?; + let firmware = match Firmware::request(name, dev.as_ref().as_ref()) { + Ok(firmware) => firmware, + // Stage 10 executes no capture bytes: every application command + // is rebuilt from Mesa's parameters. It needs only the colour-a + // page layout, which is mapped here with zeroed contents. + Err(e) if e == ENOENT && profile == 1 + && crate::g18::params::g18_initdata_test.value() == 10 => + return Self::layout_only(dev, uat), + Err(e) => return Err(e), + }; let input = payload::GraphicsPayload::parse(firmware.data()).map_err(|_| EINVAL)?; if input.capture() as u32 != profile { return Err(EINVAL); } + let (context, aliases, pages) = Self::map_pages(dev, uat, + (0..input.page_count()).map(|index| input.page(index).map(|page| (page.address, Some(page.bytes)))))?; + let mut command = KVec::new(); + command.extend_from_slice(input.command(), GFP_KERNEL)?; + let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, capture: input.capture(), command, + body_digest: input.body_digest(), command_digest: payload::fnv64(input.command()) }; + fence(Ordering::SeqCst); + dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD profile={} pages={} low_aliases={} body_fnv={:#018x} command_fnv={:#018x} context={} roots={:x?}; unpublished\n", + profile, owner.pages.len(), owner.aliases.len(), owner.body_digest, owner.command_digest, + owner.context_id(), owner.roots()); + Ok(owner) + } + + /// The colour-a VM layout without its capture: zeroed pages, no command. + fn layout_only(dev: &AsahiDrmDevice, uat: &mmu::Uat) -> Result { + let (context, aliases, pages) = Self::map_pages(dev, uat, + payload::color_a_pages().map(|(address, _)| Some((address, None))))?; + let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, + capture: payload::Capture::ColorA, command: KVec::new(), body_digest: 0, command_digest: 0 }; + fence(Ordering::SeqCst); + dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD layout-only colour-a pages={} low_aliases={} context={} roots={:x?}; no capture, stage 10 encodes from Mesa only\n", + owner.pages.len(), owner.aliases.len(), owner.context_id(), owner.roots()); + Ok(owner) + } + + /// True when no capture was loaded: there is no H1 command to encode. + pub(crate) fn layout_only_payload(&self) -> bool { self.command.is_empty() } + + fn map_pages<'a>(dev: &AsahiDrmDevice, uat: &mmu::Uat, + sources: impl Iterator)>>) + -> Result<(mmu::G18ExecutionContext, KVec, KVec)> { let vm = uat.new_t8152_compute_fixture_vm()?; let context = uat.prepare_t8152_execution_context(&vm)?; let mut pages = KVec::new(); let mut aliases = KVec::new(); - for index in 0..input.page_count() { - let source = input.page(index).ok_or(EINVAL)?; + for source in sources { + let (address, bytes) = source.ok_or(EINVAL)?; // Captured usage flags are not PTE permissions. These private // fixture mappings have an explicit diagnostic RW policy. - let mut page = MappedObject::new_wc_at(dev, &vm, source.address, + let mut page = MappedObject::new_wc_at(dev, &vm, address, payload::PAGE_BYTES, mmu::PROT_GPU_SHARED_RW)?; page.with_bytes_mut(|out| { - out.copy_from_slice(source.bytes); - if (payload::OUTPUT..payload::OUTPUT + payload::OUTPUT_BYTES).contains(&source.address) { + match bytes { Some(bytes) => out.copy_from_slice(bytes), None => out.fill(0) } + if (payload::OUTPUT..payload::OUTPUT + payload::OUTPUT_BYTES).contains(&address) { out.fill(0xa5); } Ok(()) })?; - if source.address < 0x78000 { - aliases.push(page.map_alias_at(&vm, va::COMPACT_BIAS + source.address, + if address < 0x78000 { + aliases.push(page.map_alias_at(&vm, va::COMPACT_BIAS + address, mmu::PROT_GPU_SHARED_RW)?, GFP_KERNEL)?; for delta in [0, va::PAGE - 1] { - if vm.translate_iova(source.address + delta)? - != vm.translate_iova(va::COMPACT_BIAS + source.address + delta)? { + if vm.translate_iova(address + delta)? + != vm.translate_iova(va::COMPACT_BIAS + address + delta)? { return Err(EFAULT); } } } - vm.translate_iova(source.address)?; - vm.translate_iova(source.address + va::PAGE - 1)?; + vm.translate_iova(address)?; + vm.translate_iova(address + va::PAGE - 1)?; pages.push(page, GFP_KERNEL)?; } - let mut command = KVec::new(); - command.extend_from_slice(input.command(), GFP_KERNEL)?; - let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, capture: input.capture(), command, - body_digest: input.body_digest(), command_digest: payload::fnv64(input.command()) }; - fence(Ordering::SeqCst); - dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD profile={} pages={} low_aliases={} body_fnv={:#018x} command_fnv={:#018x} context={} roots={:x?}; unpublished\n", - profile, owner.pages.len(), owner.aliases.len(), owner.body_digest, owner.command_digest, - owner.context_id(), owner.roots()); - Ok(owner) + Ok((context, aliases, pages)) } pub(crate) fn vm(&self) -> &mmu::Vm { self.context.vm() } diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 1234b4a771e7a..2cfe618e4c279 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -46,6 +46,9 @@ #include #include +#include +#include + #include "dma-iommu.h" #if IS_ENABLED(CONFIG_APPLE_DART_KUNIT_TEST) @@ -221,6 +224,8 @@ struct apple_dart; struct apple_dart_hw { enum dart_type type; + /* T8110 register layout, SPTM-owned TLB invalidation (T8152) */ + bool gen3; irqreturn_t (*irq_handler)(int irq, void *dev); int (*invalidate_tlb)(struct apple_dart_stream_map *stream_map); void (*enable_remap)(struct apple_dart *dart, int sid, int remap_to); @@ -346,6 +351,10 @@ struct apple_dart { /* Serialize owned TCR publication against IRQ quarantine. */ spinlock_t managed_lock; + /* Gen3: shared TLBI aperture and this DART's route in it */ + void __iomem *tlbi; + u8 tlbi_route; + DECLARE_BITMAP(remapped, DART_MAX_STREAMS); u8 remap_target[DART_MAX_STREAMS]; @@ -1503,6 +1512,60 @@ static int apple_dart_fw_invalidate(struct apple_dart_stream_map *stream_map, return 0; } +#define DART_GEN3_TLBI_ROUTE 0x4604 +#define DART_GEN3_TLBI_VALID BIT_ULL(60) +#define DART_GEN3_TLBI_ROUTE_FIELD GENMASK_ULL(59, 56) +#define DART_GEN3_TLBI_SID GENMASK_ULL(55, 48) + +/* + * Gen3 DARTs (T8152) are owned by the SPTM, and their TLB command register at + * 0x80 answers translation queries instead of flushing. A stream is + * invalidated the way iBoot does it: write a 128-bit payload (valid, route, + * SID) to the shared TLBI aperture, then broadcast a TLBI, which the DART + * snoops and applies to that stream. + */ +static int +apple_dart_gen3_hw_invalidate_tlb(struct apple_dart_stream_map *stream_map) +{ + struct apple_dart *dart = stream_map->dart; + unsigned long flags; + int sid; + + u64 hcr = 0; + + spin_lock_irqsave(&dart->lock, flags); + /* + * The DART snoops an EL1&0 TLBI, as iBoot and aura (EL2 without VHE) + * issue it. A VHE kernel's TLBI VALE1OS targets EL2&0 while + * HCR_EL2.TGE is set, so clear TGE around it, as KVM does to reach + * the EL1&0 regime. + */ + if (is_kernel_in_hyp_mode()) { + hcr = read_sysreg(hcr_el2); + write_sysreg(hcr & ~HCR_TGE, hcr_el2); + isb(); + } + for_each_set_bit(sid, stream_map->sidmap, dart->num_streams) { + u64 lo = DART_GEN3_TLBI_VALID | + FIELD_PREP(DART_GEN3_TLBI_ROUTE_FIELD, dart->tlbi_route) | + FIELD_PREP(DART_GEN3_TLBI_SID, sid); + + dma_wmb(); + asm volatile("stp %0, xzr, [%1]\n" + "dsb ish\n" + ".inst 0xd50881bf\n" /* tlbi vale1os, xzr */ + "dsb osh\n" + : : "r"(lo), "r"(dart->tlbi) : "memory"); + } + if (is_kernel_in_hyp_mode()) { + write_sysreg(hcr, hcr_el2); + isb(); + } + spin_unlock_irqrestore(&dart->lock, flags); + + return 0; +} + static int apple_dart_t8020_hw_invalidate_tlb(struct apple_dart_stream_map *stream_map) { @@ -2110,7 +2173,10 @@ static int apple_dart_domain_flush_tlb_range(struct apple_dart_domain *domain, /* The range registers take the DVA the device issues. */ start = first + domain->dma_offset; end = last + domain->dma_offset; - if (range && (stream.dart->fw_mirror || stream.dart->fw_handoff) && + /* Gen3 has no range command; invalidate the complete stream. */ + if (stream.dart->hw->gen3) + ret = stream.dart->hw->invalidate_tlb(&stream); + else if (range && (stream.dart->fw_mirror || stream.dart->fw_handoff) && stream.dart->version >= 0x0202) ret = apple_dart_fw_invalidate(&stream, start, end); else if (range && stream.dart->locked && stream.dart->version >= 0x0202) @@ -4289,6 +4355,13 @@ static int apple_dart_probe(struct platform_device *pdev) * not admitted: never enable that level IRQ for one block alone. */ dart->irq = -1; + } else if (dart->hw->gen3) { + /* + * The T8110 fault handler acknowledges through the TLB command + * register, which a Gen3 DART uses for translation queries. + * Leave its faults to the firmware for now. + */ + dart->irq = -1; } else if (dart->preserve_display || j713_radio) { dart->irq = platform_get_irq_optional(pdev, 0); if (dart->irq == -ENXIO) @@ -4600,8 +4673,34 @@ static int apple_dart_probe(struct platform_device *pdev) goto err_clk_disable; } + if (dart->hw->gen3) { + struct resource *tlbi; + + /* Only an iBoot-provisioned (locked) Gen3 stream is supported. */ + if (!dart->locked) { + dev_err(dev, "Gen3 DART is not locked; cold setup is not supported\n"); + ret = -ENODEV; + goto err_clk_disable; + } + /* The aperture is shared by every Gen3 DART: map, do not claim. */ + tlbi = platform_get_resource_byname(pdev, IORESOURCE_MEM, "tlbi"); + if (!tlbi || resource_size(tlbi) < 16) { + dev_err(dev, "Gen3 DART needs its TLBI aperture\n"); + ret = -EINVAL; + goto err_clk_disable; + } + dart->tlbi = devm_ioremap(dev, tlbi->start, resource_size(tlbi)); + if (!dart->tlbi) { + ret = -ENOMEM; + goto err_clk_disable; + } + dart->tlbi_route = apple_dart_readl(dart, DART_GEN3_TLBI_ROUTE) & 0xf; + dev_info(dev, "Gen3 DART, version %#x, TLBI route %u\n", + dart->version, dart->tlbi_route); + } + /* Borrowed display state: never acknowledge someone else's fault. */ - if (dart->locked && !dart->preserve_display) + if (dart->locked && !dart->preserve_display && !dart->hw->gen3) apple_dart_drain_stale_exceptions(dart); /* Refuse an already pending inherited fault before joining the shared IRQ. */ @@ -4939,6 +5038,33 @@ static const struct apple_dart_hw apple_dart_hw_t6000 = { .ttbr_count = 4, }; +static const struct apple_dart_hw apple_dart_hw_gen3 = { + .type = DART_T8110, + .gen3 = true, + .irq_handler = apple_dart_t8110_irq, + .invalidate_tlb = apple_dart_gen3_hw_invalidate_tlb, + .fmt = APPLE_DART2, + .max_sid_count = 256, + + .enable_streams = DART_T8110_ENABLE_STREAMS, + .lock = DART_T8110_PROTECT, + .lock_bit = DART_T8110_PROTECT_TTBR_TCR, + + .error = DART_T8110_ERROR, + + .tcr = DART_T8110_TCR, + .tcr_enabled = DART_T8110_TCR_TRANSLATE_ENABLE, + .tcr_disabled = 0, + .tcr_bypass = DART_T8110_TCR_BYPASS_DAPF | DART_T8110_TCR_BYPASS_DART, + .tcr_4level = DART_T8110_TCR_FOUR_LEVEL, + + .ttbr = DART_T8110_TTBR, + .ttbr_valid = DART_T8110_TTBR_VALID, + .ttbr_addr_field_shift = DART_T8110_TTBR_ADDR_FIELD_SHIFT, + .ttbr_shift = DART_T8110_TTBR_SHIFT, + .ttbr_count = 1, +}; + static const struct apple_dart_hw apple_dart_hw_t8110 = { .type = DART_T8110, .irq_handler = apple_dart_t8110_irq, @@ -5202,6 +5328,7 @@ static __maybe_unused int apple_dart_resume(struct device *dev) static DEFINE_RUNTIME_DEV_PM_OPS(apple_dart_pm_ops, apple_dart_suspend, apple_dart_resume, NULL); static const struct of_device_id apple_dart_of_match[] = { + { .compatible = "apple,t8152-dart-gen3", .data = &apple_dart_hw_gen3 }, { .compatible = "apple,t8103-dart", .data = &apple_dart_hw_t8103 }, { .compatible = "apple,t8103-usb4-dart", .data = &apple_dart_hw_t8103_usb4 }, { .compatible = "apple,t8110-dart", .data = &apple_dart_hw_t8110 }, diff --git a/drivers/pmdomain/apple/pmc.c b/drivers/pmdomain/apple/pmc.c index 00bebde94a82c..0627ab148fc0c 100644 --- a/drivers/pmdomain/apple/pmc.c +++ b/drivers/pmdomain/apple/pmc.c @@ -1160,10 +1160,23 @@ static const struct apple_pmc_hw apple_pmc_hw_t6040 = { .ap_agent = 3, }; +/* T8152 uses the T8142 voter interface with its rails at 0x3029c6000. */ +static const struct apple_pmc_hw apple_pmc_hw_t8152 = { + .agent_stride = 0x4000, + .agents = 4, + .rails = 3, + .agent_state = 0x0, + .rail_enable = 0x2000, + .dcs_rail = 0, + .dcs_max = 7, + .ap_agent = 3, +}; + static const struct of_device_id apple_pmc_of_match[] = { { .compatible = "apple,t8132-pmc", .data = &apple_pmc_hw_t8132 }, { .compatible = "apple,t8140-pmc", .data = &apple_pmc_hw_t8140 }, { .compatible = "apple,t8142-pmc", .data = &apple_pmc_hw_t8142 }, + { .compatible = "apple,t8152-pmc", .data = &apple_pmc_hw_t8152 }, { .compatible = "apple,t6041-pmc", .data = &apple_pmc_hw_t6040 }, {} }; diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index b31a46b050358..c7b29f0e6a49b 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -1141,10 +1141,20 @@ static struct kunit_suite apple_clpc_suite = { kunit_test_suite(apple_clpc_suite); #endif +/* T8152: DCS states 0-7 and fabric states 0-4. */ +static const struct apple_clpc_hw apple_clpc_hw_t8152 = { + .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, + .map = { + [APPLE_CLPC_DCS] = { 2, 4, 6, 7 }, + [APPLE_CLPC_SOC] = { 1, 2, 3, 4 }, + }, +}; + static const struct of_device_id apple_clpc_of_match[] = { { .compatible = "apple,t8132-clpc", .data = &apple_clpc_hw_t8132 }, { .compatible = "apple,t8140-clpc", .data = &apple_clpc_hw_t8140 }, { .compatible = "apple,t8142-clpc", .data = &apple_clpc_hw_t8142 }, + { .compatible = "apple,t8152-clpc", .data = &apple_clpc_hw_t8152 }, { .compatible = "apple,t6041-clpc", .data = &apple_clpc_hw_t6040 }, {} }; diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index c13c03f9d92ab..3839a62d55e68 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -1708,6 +1708,7 @@ static const struct apple_mbox_hw apple_mbox_t8152_smc_hw = { static const struct of_device_id apple_mbox_of_match[] = { { .compatible = "apple,t8152-smc-mailbox", .data = &apple_mbox_t8152_smc_hw }, + { .compatible = "apple,t8152-dcp-mailbox", .data = &apple_mbox_t8152_smc_hw }, { .compatible = "apple,t6031-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8122-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8152-gfx-ascwrap-v8", .data = &apple_mbox_t8152_gfx_ascwrap_v8_hw }, diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 536ba515f826f..c968e4847e996 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -1222,6 +1222,19 @@ bool apple_rtkit_has_endpoint(struct apple_rtkit *rtk, u8 ep) } EXPORT_SYMBOL_GPL(apple_rtkit_has_endpoint); +/* + * A co-processor adopted from the bootloader has already sent its endpoint + * map. A client that starts one of its application endpoints by hand + * registers it here, so its messages are not reported as undiscovered. + */ +void apple_rtkit_adopt_ep(struct apple_rtkit *rtk, u8 ep) +{ + if (ep >= APPLE_RTKIT_APP_ENDPOINT_START && + ep < APPLE_RTKIT_MAX_ENDPOINTS) + set_bit(ep, rtk->endpoints); +} +EXPORT_SYMBOL_GPL(apple_rtkit_adopt_ep); + int apple_rtkit_start_ep(struct apple_rtkit *rtk, u8 endpoint) { u64 msg; diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index fbdb9ba0e7f2a..64b3cf9e8fc0d 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -518,6 +518,7 @@ static int apple_spmi_probe(struct platform_device *pdev) static const struct of_device_id apple_spmi_match_table[] = { { .compatible = "apple,t8142-spmi", .data = &apple_spmi_gen4 }, + { .compatible = "apple,t8152-spmi", .data = &apple_spmi_gen4 }, { .compatible = "apple,t8103-spmi", .data = &apple_spmi_gen1 }, { .compatible = "apple,spmi", .data = &apple_spmi_gen1 }, {} diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 35508001fbea4..ac08b80c30042 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -311,4 +311,14 @@ void apple_rtkit_flush_rx(struct apple_rtkit *rtk); */ bool apple_rtkit_has_endpoint(struct apple_rtkit *rtk, u8 ep); +/* + * Registers an application endpoint the client started itself on a + * co-processor whose endpoint map the bootloader consumed, so that its + * messages are not reported as undiscovered + * + * @rtk: RTKit reference + * @ep: endpoint to register + */ +void apple_rtkit_adopt_ep(struct apple_rtkit *rtk, u8 ep); + #endif /* _LINUX_APPLE_RTKIT_H_ */ diff --git a/tools/asahi/apple-dart-range-tests.py b/tools/asahi/apple-dart-range-tests.py index 5d311fef0813d..7ffa652e204ae 100644 --- a/tools/asahi/apple-dart-range-tests.py +++ b/tools/asahi/apple-dart-range-tests.py @@ -42,7 +42,7 @@ def main(): #define io_pgtable_ops_to_pgtable(x) ((struct io_pgtable *)(x)) struct apple_dart; struct apple_dart_stream_map { struct apple_dart *dart; unsigned long sidmap[1]; }; -struct apple_dart_hw { int (*invalidate_tlb)(struct apple_dart_stream_map *); }; +struct apple_dart_hw { int (*invalidate_tlb)(struct apple_dart_stream_map *); bool gen3; }; struct apple_dart { int num_streams, locked, version; bool fw_mirror, fw_handoff, managed_sids; void *dev; struct apple_dart_hw *hw; }; struct apple_dart_atomic_stream_map { struct apple_dart *dart; unsigned long sidmap[1]; }; struct apple_dart_domain {