From 9eb9bc20c3a52015a800c39d54160d6aafdc6ac2 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 8 Nov 2026 20:05:40 -0500 Subject: [PATCH] thunderbolt: apple: Enable stable T6050 DisplayPort tunnel handoff Map all four T6050 DP IN adapters, forward their HPD interrupts to the display driver, and apply the host link, TMU and DP OUT policies needed for USB4 display tunneling. Preserve the existing M4 handshake policy. Wait 100 ms for a T6050 request generation to settle before handing it to DCP. Retired transient requests must not occupy the second display processor during dock discovery. Block delayed IRQ handlers and polling from reading registers after powered retirement. Developed with Claude Code. Signed-off-by: Erik Bourget --- .../thunderbolt/apple,t8103-usb4-acio.yaml | 34 +- drivers/thunderbolt/apple-dpin-state.c | 21 ++ drivers/thunderbolt/apple-dpin-state.h | 2 + drivers/thunderbolt/apple-dpin-test.c | 2 + drivers/thunderbolt/apple.c | 300 ++++++++++++++++-- drivers/thunderbolt/nhi.h | 9 + drivers/thunderbolt/switch.c | 45 ++- drivers/thunderbolt/tb.c | 19 +- drivers/thunderbolt/tb.h | 1 + drivers/thunderbolt/tb_regs.h | 1 + drivers/thunderbolt/tmu.c | 108 ++++++- drivers/thunderbolt/tunnel.c | 45 ++- 12 files changed, 530 insertions(+), 57 deletions(-) diff --git a/Documentation/devicetree/bindings/thunderbolt/apple,t8103-usb4-acio.yaml b/Documentation/devicetree/bindings/thunderbolt/apple,t8103-usb4-acio.yaml index fe61df65c5c27..9f6f992410fe1 100644 --- a/Documentation/devicetree/bindings/thunderbolt/apple,t8103-usb4-acio.yaml +++ b/Documentation/devicetree/bindings/thunderbolt/apple,t8103-usb4-acio.yaml @@ -70,18 +70,30 @@ properties: - const: sram - const: cpu - interrupts: - maxItems: 1 - description: Type 7 SRAM parity interrupt - - interrupt-names: - items: - - const: sram-parity - mboxes: maxItems: 1 description: Mailbox used for RTKit communication with the co-processor + interrupts: + minItems: 1 + maxItems: 4 + description: | + Type 7 SRAM parity interrupt, or interrupts of the router's DP IN + adapter blocks, one per DP IN adapter: + a change of the HPD level the adapter receives over the tunnel, and + IRQ_HPD from the sink. + + interrupt-names: + oneOf: + - items: + - const: sram-parity + - minItems: 1 + items: + - const: dpin0 + - const: dpin1 + - const: dpin2 + - const: dpin3 + resets: maxItems: 1 description: Reset used to start the ACIO block before booting the @@ -252,6 +264,12 @@ allOf: contains: const: apple,t6040-usb4-acio then: + properties: + interrupts: + maxItems: 1 + interrupt-names: + items: + - const: sram-parity required: - iommu@1a80000 - nhi@1f00000 diff --git a/drivers/thunderbolt/apple-dpin-state.c b/drivers/thunderbolt/apple-dpin-state.c index 1a2721d6e84a4..afaa64e3f6ba6 100644 --- a/drivers/thunderbolt/apple-dpin-state.c +++ b/drivers/thunderbolt/apple-dpin-state.c @@ -49,6 +49,27 @@ const struct apple_dpin_policy apple_dpin_m4 = { .lazy_idle = true, }; +/* + * T6050: the M3 flow (tunnel changed notifications, with the 10 ms HPD + * Output Set pulse and HPD wait before them; capacity retry, deferred + * bringup). Once a tunnel uses a DP IN block, its pending events are + * acknowledged (+0x04 written back, and +0x00 after a plug event) and its + * two events enabled (+0x08), as the block is prepared for a display. The + * DP IN handshake writes only DPTX_INACTIVE (+0x0c bit 0) and polls its ACK + * (+0x10 bit 0), gated on HPD. A tunnel whose DP IN block DCP never + * activated is torn down without the inactive handshake. A DPRX that is not + * done in time keeps the tunnel: the display may still train. + */ +const struct apple_dpin_policy apple_dpin_m5 = { + .flow = APPLE_DPIN_CHANGED, + .settle_ms = 100, + .capacity_retry = true, + .setup_irqs = true, + .defer_new_bringup = true, + .host_policy = TB_HOST_DP_NOTIFY | TB_HOST_DP_KEEP_DPRX_TIMEOUT, + .lazy_idle = true, +}; + enum apple_dpin_flow apple_dpin_hooks(const struct apple_dpin_policy *policy, bool queue_present, bool display_enabled) { diff --git a/drivers/thunderbolt/apple-dpin-state.h b/drivers/thunderbolt/apple-dpin-state.h index dcf3e53c0a1d6..38b8ec7eb439e 100644 --- a/drivers/thunderbolt/apple-dpin-state.h +++ b/drivers/thunderbolt/apple-dpin-state.h @@ -25,6 +25,7 @@ struct apple_dpin_policy { unsigned long host_policy; /* teardown touches the DP IN block only after DCP's Activate/Deactivate did */ bool lazy_idle; + unsigned int settle_ms; }; extern const struct apple_dpin_policy apple_dpin_disabled; @@ -32,6 +33,7 @@ extern const struct apple_dpin_policy apple_dpin_m1; extern const struct apple_dpin_policy apple_dpin_m2; extern const struct apple_dpin_policy apple_dpin_m3; extern const struct apple_dpin_policy apple_dpin_m4; +extern const struct apple_dpin_policy apple_dpin_m5; enum apple_dpin_phase { APPLE_DPIN_IDLE, diff --git a/drivers/thunderbolt/apple-dpin-test.c b/drivers/thunderbolt/apple-dpin-test.c index 0d4891ad74e14..dd532a2a051b5 100644 --- a/drivers/thunderbolt/apple-dpin-test.c +++ b/drivers/thunderbolt/apple-dpin-test.c @@ -360,6 +360,8 @@ static void apple_dpin_qualification_profiles(struct kunit *test) { "apple,t6031", &apple_dpin_m3, true, &apple_dpin_disabled }, { "apple,t8132", &apple_dpin_m4, true, &apple_dpin_m4 }, { "apple,t8132", &apple_dpin_m4, false, &apple_dpin_m4 }, + { "apple,t6050", &apple_dpin_m5, true, &apple_dpin_m5 }, + { "apple,t6050", &apple_dpin_m5, false, &apple_dpin_m5 }, { "apple,unknown", &apple_dpin_m1, true, &apple_dpin_disabled }, }; unsigned int i; diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 9cfd12a33dc06..e0cd146997d25 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -131,7 +131,12 @@ #define APPLE_MXWRAP_CPU_CTRL_RUN BIT(4) #define APPLE_MXWRAP_CPU_STATUS 0x44 +/* DP IN adapter blocks a host router may have (T6050 has four) */ +#define APPLE_DPIN_MAX 4 + struct apple_cio_hw { + /* DP IN adapter blocks of the router; 0 means the two of t8103 */ + unsigned int dpin_count; bool mxwrap; bool lstx_explicit; /* Leave MxWrap RUN alone: the CIO reconfig restarts the CPU. */ @@ -141,6 +146,8 @@ struct apple_cio_hw { bool strict_fw_ready; /* Keep the links of the host router out of CL states. */ bool no_host_clx; + /* Keep the domain's links symmetric. */ + bool no_asym; /* Type 7 router: Enter_USB cable words, bootloader RC tuning, Type 7 NHI. */ bool type7; /* @@ -152,6 +159,10 @@ struct apple_cio_hw { bool lstx_hold_from_probe; /* The router trains Gen4 only on a cable word that says Gen4. */ bool gen4_cable; + /* Run Gen4 links with link recovery enabled at both ends. */ + bool gen4_link_recovery; + /* Leave the host router's TMU to its firmware; host TMU constants. */ + bool host_tmu; const struct apple_dpin_policy *dp; }; @@ -197,9 +208,13 @@ static const struct apple_cio_hw apple_cio_t8142_hw = { * T6050 is Type 7 with an MxWrap CPU wrapper that is only clocked once the * CIO reset (the reconfig request) has run. Its CPU is released with a plain * store of RUN; as on T8132 it takes LSTX over only once its RTKit is up, and - * the AP holds the pin while the router is off. DP tunnels stay off for now. + * the AP holds the pin while the router is off. Its four DP IN adapters take + * the M5 policy, see apple_dpin_m5. A Gen4 link times out CL training once CL + * states are enabled, then drops; it also dropped in the middle of a + * transition to an asymmetric width. Its links stay in CL0 and symmetric. */ static const struct apple_cio_hw apple_cio_t6050_hw = { + .dpin_count = 4, .mxwrap = true, .type7 = true, .run_store = true, @@ -207,7 +222,11 @@ static const struct apple_cio_hw apple_cio_t6050_hw = { .lstx_hold_from_probe = true, .strict_fw_ready = true, .gen4_cable = true, - .dp = &apple_dpin_disabled, + .no_host_clx = true, + .no_asym = true, + .gen4_link_recovery = true, + .host_tmu = true, + .dp = &apple_dpin_m5, }; static const struct apple_cio_hw apple_cio_t6040_hw = { @@ -296,6 +315,9 @@ module_param(dpin_mode_value, uint, 0644); MODULE_PARM_DESC(dpin_mode_value, "DPIN0 MODE_A/MODE_B lab value (0-" __stringify(APPLE_DPIN_MODE_VALUE_MAX) "); read at each activate"); +/* DP IN adapters without an interrupt are polled this often (ms). */ +#define APPLE_DPIN_POLL_MS 20 + struct apple_dpin_binding; /** @@ -395,6 +417,7 @@ struct apple_cio { /* Type-C connector this router is wired to, for DP tunnel routing */ struct device_node *connector_np; bool dp_dual_stream; + unsigned int dpin_count; struct workqueue_struct *dp_wq; struct apple_dpin_ctx { struct apple_cio *acio; @@ -405,9 +428,19 @@ struct apple_cio { unsigned int idx; struct apple_dpin_state state; struct apple_dpin_tokens tokens; + unsigned long settle_until; /* new tunnel's admission deadline */ struct apple_dpin_binding *binding; /* worker-owned, handed to DCP */ bool dcp_touched; /* DCP ran the handshake on this mapping */ - } dpin[2]; + bool dcp_active; /* DCP activated the link (DPTX_INACTIVE clear) */ + /* DP IN interrupts: the line, or a poll where there is none */ + int irq; + bool irq_enabled; + struct delayed_work poll; + struct work_struct event_work; /* passes events on, on dp_wq */ + unsigned int events; /* APPLE_DP_TUNNEL_*, under lock */ + bool hpd; /* HPD level last seen, under lock */ + unsigned int irq_count; + } dpin[APPLE_DPIN_MAX]; }; /* Immutable identity for one callback handoff; freed only after provider drain. */ @@ -418,21 +451,27 @@ struct apple_dpin_binding { }; /* - * The ATC's DP IN adapter blocks ("atcN-dpin0/1" in the Apple device tree). + * The ATC's DP IN adapter blocks ("atcN-dpin0..3" in the Apple device tree). * Registers on t8103: * 0x00 status, bit 2 = HPD level * 0x04 interrupt status (write 1 to clear) * 0x08 interrupt enable [1:0] * 0x0c bit 0 DPTX_INACTIVE * 0x10 bit 0 DPTX_INACTIVE_ACK - * The block sits 0x390000 after the ACIO "rc" region; dpin1 is 0x8000 after dpin0. + * The blocks sit 0x390000 after the ACIO "rc" region: dpin1 0x8000 after + * dpin0, and on routers with four, dpin2 and dpin3 0x18000 and 0x20000 after. */ #define APPLE_DPIN_OFFSET 0x390000 -#define APPLE_DPIN_STRIDE 0x8000 #define APPLE_DPIN_SIZE 0x4000 + +static const u32 apple_dpin_offsets[APPLE_DPIN_MAX] = { + 0x0, 0x8000, 0x18000, 0x20000, +}; #define APPLE_DPIN_STATUS 0x00 #define APPLE_DPIN_STATUS_HPD BIT(2) #define APPLE_DPIN_IRQ_STATUS 0x04 +#define APPLE_DPIN_IRQ_HPD_CHANGE BIT(0) +#define APPLE_DPIN_IRQ_SINK BIT(1) #define APPLE_DPIN_IRQ_ENABLE 0x08 #define APPLE_DPIN_CTRL 0x0c #define APPLE_DPIN_CTRL_INACTIVE BIT(0) @@ -456,6 +495,8 @@ static const struct of_device_id apple_dpin_qualified_soc[] = { { .compatible = "apple,t6021" }, { .compatible = "apple,t6030" }, { .compatible = "apple,t8132" }, + /* T6050: t8103 DP IN handshake, M3 flow, four DP IN adapters */ + { .compatible = "apple,t6050" }, {}, }; @@ -583,6 +624,146 @@ static void apple_dpin_request_inactive(void *data) apple_dpin_set_active(c->acio, c->regs, c->idx, false); } +/* + * DP IN interrupts. +0x4 holds the events, write-1-to-clear: bit 0 the HPD + * level in +0x0 changed (+0x0 latches status bits that are cleared by + * writing it back), bit 1 the sink raised IRQ_HPD. Read +0x4 until it is + * stable, clear it, and hand what happened to appledrm: the display's DPTX + * port has no HPD input of its own on a tunnel. c->lock held. + */ +static unsigned int apple_dpin_irq_collect(struct apple_dpin_ctx *c, bool *seen) +{ + unsigned int events = 0, tries; + u32 irq, again, st; + bool hpd; + + lockdep_assert_held(&c->lock); + *seen = false; + /* A handler or poll may have waited on the lock while teardown stopped it. */ + if (!c->irq_enabled || !c->state.alive || !c->regs) + return 0; + irq = readl(c->regs + APPLE_DPIN_IRQ_STATUS); + for (tries = 0; tries < 8; tries++) { + again = readl(c->regs + APPLE_DPIN_IRQ_STATUS); + if (again == irq) + break; + irq = again; + } + if (!irq) + return 0; + *seen = true; + writel(irq, c->regs + APPLE_DPIN_IRQ_STATUS); + st = readl(c->regs + APPLE_DPIN_STATUS); + if (irq & APPLE_DPIN_IRQ_HPD_CHANGE) + writel(st, c->regs + APPLE_DPIN_STATUS); + hpd = st & APPLE_DPIN_STATUS_HPD; + if (hpd != c->hpd) + events |= APPLE_DP_TUNNEL_HPD_CHANGED; + c->hpd = hpd; + if (irq & APPLE_DPIN_IRQ_SINK) + events |= APPLE_DP_TUNNEL_IRQ_HPD; + c->irq_count++; + dev_dbg(c->acio->dev, "dpin%u: interrupt %#x (#%u), status %#x: HPD %d%s%s\n", + c->idx, irq, c->irq_count, st, hpd, + events & APPLE_DP_TUNNEL_IRQ_HPD ? ", IRQ_HPD" : "", + events & APPLE_DP_TUNNEL_HPD_CHANGED ? ", HPD changed" : ""); + events &= APPLE_DP_TUNNEL_IRQ_HPD | APPLE_DP_TUNNEL_HPD_CHANGED; + c->events |= events; + return events; +} + +/* Returns false if the adapter had nothing pending. */ +static bool apple_dpin_irq_dispatch(struct apple_dpin_ctx *c) +{ + unsigned int events; + bool seen; + + scoped_guard(mutex, &c->lock) + events = apple_dpin_irq_collect(c, &seen); + if (events) + queue_work(c->acio->dp_wq, &c->event_work); + return seen; +} + +static irqreturn_t apple_dpin_irq_thread(int irq, void *data) +{ + return apple_dpin_irq_dispatch(data) ? IRQ_HANDLED : IRQ_NONE; +} + +static void apple_dpin_poll_fn(struct work_struct *work) +{ + struct apple_dpin_ctx *c = container_of(to_delayed_work(work), + struct apple_dpin_ctx, poll); + bool mapped; + + apple_dpin_irq_dispatch(c); + scoped_guard(mutex, &c->lock) + mapped = c->irq_enabled && c->regs; + if (mapped) + queue_delayed_work(system_dfl_wq, &c->poll, + msecs_to_jiffies(APPLE_DPIN_POLL_MS)); +} + +/* Start taking the interrupts of a mapped adapter. c->lock held. */ +static void apple_dpin_irq_start(struct apple_dpin_ctx *c) +{ + lockdep_assert_held(&c->lock); + if (c->irq_enabled || !c->regs) + return; + c->hpd = readl(c->regs + APPLE_DPIN_STATUS) & APPLE_DPIN_STATUS_HPD; + c->events = 0; + c->irq_enabled = true; + if (c->irq > 0) + enable_irq(c->irq); + else + queue_delayed_work(system_dfl_wq, &c->poll, + msecs_to_jiffies(APPLE_DPIN_POLL_MS)); +} + +/* The handler takes c->lock: never wait for it here. c->lock held. */ +static void apple_dpin_irq_stop(struct apple_dpin_ctx *c) +{ + lockdep_assert_held(&c->lock); + if (!c->irq_enabled) + return; + c->irq_enabled = false; + c->events = 0; + if (c->irq > 0) + disable_irq_nosync(c->irq); + else + cancel_delayed_work(&c->poll); +} + +/* On dp_wq, ordered with bring-up and teardown: the binding is stable. */ +static void apple_dpin_event_fn(struct work_struct *work) +{ + struct apple_dpin_ctx *c = container_of(work, struct apple_dpin_ctx, event_work); + typeof(apple_dcp_tb_dp_event) *event; + unsigned int events; + bool hpd, active; + int ret; + + scoped_guard(mutex, &c->lock) { + events = c->events; + c->events = 0; + hpd = c->hpd; + active = c->dcp_active && c->state.alive; + } + /* The sink's requests matter once the display has activated the link. */ + if (!active) + events &= ~APPLE_DP_TUNNEL_IRQ_HPD; + if (!events || !c->binding) + return; + event = symbol_get(apple_dcp_tb_dp_event); + if (!event) + return; + ret = event(c->acio->connector_np, c->idx, c->binding->generation, events, hpd); + symbol_put(apple_dcp_tb_dp_event); + if (ret) + dev_info(c->acio->dev, "dpin%u: display did not take events %#x: %d\n", + c->idx, events, ret); +} + static void apple_dpin_request_mask_irqs(void *data) { struct apple_dpin_ctx *c = data; @@ -593,6 +774,7 @@ static void apple_dpin_request_mask_irqs(void *data) writel(readl(c->regs + APPLE_DPIN_IRQ_ENABLE) & ~3, c->regs + APPLE_DPIN_IRQ_ENABLE); + apple_dpin_irq_stop(c); } static bool apple_dpin_request(struct apple_dpin_ctx *c, u64 generation, bool active) @@ -602,10 +784,15 @@ static bool apple_dpin_request(struct apple_dpin_ctx *c, u64 generation, bool ac .mask_irqs = apple_dpin_request_mask_irqs, .ctx = c, }; + bool replacement = active && c->tokens.requested != generation; lockdep_assert_held(&c->lock); - return apple_dpin_powered_request(&c->state, &c->tokens, c->acio->dp, - generation, active, !!c->regs, &ops); + if (!apple_dpin_powered_request(&c->state, &c->tokens, c->acio->dp, + generation, active, !!c->regs, &ops)) + return false; + if (replacement) + c->settle_until = jiffies + msecs_to_jiffies(c->acio->dp->settle_ms); + return true; } /* Called by appledrm from DCP's Activate/Deactivate calls. */ @@ -613,6 +800,7 @@ static int apple_dpin_dcp_set_active(void *data, bool active) { const struct apple_dpin_binding *binding = data; struct apple_dpin_ctx *c = binding->ctx; + int ret; guard(mutex)(&c->lock); if (!apple_dpin_token_access(&c->tokens, binding->generation)) @@ -621,7 +809,10 @@ static int apple_dpin_dcp_set_active(void *data, bool active) if (!c->state.alive || !c->regs) return -ENODEV; c->dcp_touched = true; - return apple_dpin_set_active(c->acio, c->regs, c->idx, active); + ret = apple_dpin_set_active(c->acio, c->regs, c->idx, active); + if (!ret) + c->dcp_active = active; + return ret; } static bool apple_dpin_provider_get(void *data) @@ -721,14 +912,45 @@ static int apple_dpin_connect(struct apple_dpin_ctx *c, } } +/* + * A USB4 dock can withdraw and re-advertise its DP OUT adapters while the + * initial capability exchange completes. On T6050, starting DCP on the + * temporary tunnel can leave its AUX request stuck after that tunnel goes + * away. Let the request settle before admitting any DCP callback or touching + * the adapter. Teardown still idles immediately in apple_dpin_request(). + * A replacement owns a new deadline; this worker never admits the old token. + */ +static int apple_dpin_wait_settled(struct apple_dpin_ctx *c, u64 generation) +{ + unsigned long delay, now; + + if (!c->acio->dp->settle_ms) + return 0; + for (;;) { + scoped_guard(mutex, &c->lock) { + if (!c->state.alive || c->tokens.requested != generation) + return -ESTALE; + now = jiffies; + if (time_after_eq(now, c->settle_until)) + return 0; + delay = c->settle_until - now; + } + msleep(jiffies_to_msecs(delay) + 1); + } +} + static int apple_dpin_up(struct apple_dpin_ctx *c, struct apple_dpin_binding *binding, bool *deferred) { struct apple_cio *acio = c->acio; void __iomem *regs = c->regs; + int ret; u32 irq; *deferred = false; + ret = apple_dpin_wait_settled(c, binding->generation); + if (ret) + return ret; scoped_guard(mutex, &c->lock) { if (!c->state.alive || c->tokens.requested != binding->generation) return -ESTALE; @@ -739,7 +961,7 @@ static int apple_dpin_up(struct apple_dpin_ctx *c, /* This admission also owns the first DCP call after dropping the lock. */ if (!regs) { regs = ioremap_np(acio->rc_res->start + APPLE_DPIN_OFFSET + - c->idx * APPLE_DPIN_STRIDE, APPLE_DPIN_SIZE); + apple_dpin_offsets[c->idx], APPLE_DPIN_SIZE); if (!regs) return -ENOMEM; c->regs = regs; @@ -748,8 +970,7 @@ static int apple_dpin_up(struct apple_dpin_ctx *c, * Acknowledge what is pending and enable the DP IN interrupts: * +0x4 is write-1-to-clear, and a plug event (bit 0) also * latches status bits in +0x0 that are cleared by writing it - * back. Nothing handles these interrupts yet; they are enabled - * as part of bringing the adapter up. + * back. See apple_dpin_irq_collect() for what they report. */ if (acio->dp->setup_irqs) { irq = readl(regs + APPLE_DPIN_IRQ_STATUS); @@ -757,6 +978,7 @@ static int apple_dpin_up(struct apple_dpin_ctx *c, if (irq & BIT(0)) writel(readl(regs + APPLE_DPIN_STATUS), regs + APPLE_DPIN_STATUS); writel(readl(regs + APPLE_DPIN_IRQ_ENABLE) | 3, regs + APPLE_DPIN_IRQ_ENABLE); + apple_dpin_irq_start(c); } /* Activate may run before attach returns, so admit its immutable cookie now. */ apple_dpin_token_admit(&c->tokens, binding->generation); @@ -791,9 +1013,11 @@ static void apple_dpin_down(struct apple_dpin_ctx *c) } /* The powered request hook idled the adapter before queued teardown. */ scoped_guard(mutex, &c->lock) { + apple_dpin_irq_stop(c); regs = c->regs; c->regs = NULL; c->dcp_touched = false; + c->dcp_active = false; apple_dpin_step(&c->state, c->acio->dp, APPLE_DPIN_DROPPED, false, 0); } if (regs) @@ -921,8 +1145,10 @@ static void apple_cio_destroy_wq(void *data) struct apple_cio *acio = data; /* a pending retry would queue onto the destroyed queue */ - for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) + for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) { cancel_delayed_work_sync(&acio->dpin[i].retry); + cancel_delayed_work_sync(&acio->dpin[i].poll); + } destroy_workqueue(acio->dp_wq); } @@ -1910,7 +2136,7 @@ static void apple_dp_aux_work(struct work_struct *work) } /* - * Which DP IN adapter (0/1) on the host router this port is -- same + * Which DP IN adapter (0..3) on the host router this port is -- same * counting order as apple_dp_in_analog_base() above and the port-to-dpin * mapping in apple_nhi_dp_tunnel_changed(). */ @@ -1932,6 +2158,12 @@ static int apple_dpin_index_for_port(struct apple_nhi *anhi, struct tb_port *in) return -1; } +/* A DP IN adapter index with a block of its own on this router */ +static bool apple_dpin_valid(const struct apple_cio *acio, int idx) +{ + return acio && idx >= 0 && idx < acio->dpin_count; +} + static int apple_nhi_dp_tunnel_pre_activate(struct tb_nhi *nhi, struct tb_port *in, struct tb_port *out) @@ -1951,7 +2183,7 @@ static int apple_nhi_dp_tunnel_post_activate(struct tb_nhi *nhi, /* Validate the token before changing analog/AUX or display state. */ idx = apple_dpin_index_for_port(anhi, in); - if (idx >= 0 && idx <= 1 && anhi->acio && anhi->acio->connector_np && + if (apple_dpin_valid(anhi->acio, idx) && anhi->acio->connector_np && anhi->acio->dp_wq) { c = &anhi->acio->dpin[idx]; scoped_guard(mutex, &c->lock) { @@ -1978,7 +2210,7 @@ static int apple_nhi_dp_tunnel_post_activate(struct tb_nhi *nhi, apple_dp_dump_vse(in->sw); /* Route the display pipeline through the selected host DP IN adapter. */ - if (idx < 0 || idx > 1 || !anhi->acio) { + if (!apple_dpin_valid(anhi->acio, idx)) { dev_warn(anhi->dev, "DP IN tunnel: could not map port %u to a dpin index\n", in->port); @@ -2018,7 +2250,7 @@ static void apple_nhi_dp_tunnel_deactivate(struct tb_nhi *nhi, /* Sleep the adapter before tunnel teardown can remove its power. */ idx = apple_dpin_index_for_port(anhi, in); - if (idx >= 0 && idx <= 1 && anhi->acio && anhi->acio->dp_wq) { + if (apple_dpin_valid(anhi->acio, idx) && anhi->acio->dp_wq) { struct apple_dpin_ctx *c = &anhi->acio->dpin[idx]; scoped_guard(mutex, &c->lock) { @@ -2041,7 +2273,7 @@ static void apple_nhi_dp_tunnel_changed(struct tb_nhi *nhi, u8 in_port, unsigned int dpin = 0; bool found = false; - /* The crossbar's dpin0/dpin1 follow the host router's DP IN adapters in order. */ + /* The crossbar's dpin0..3 follow the host router's DP IN adapters in order. */ tb_switch_for_each_port(anhi->tb->root_switch, port) { if (!tb_port_is_dpin(port)) continue; @@ -2051,7 +2283,7 @@ static void apple_nhi_dp_tunnel_changed(struct tb_nhi *nhi, u8 in_port, } dpin++; } - if (!found || dpin > 1) { + if (!found || dpin >= acio->dpin_count) { dev_warn(acio->dev, "DP tunnel from unexpected adapter %u\n", in_port); return; } @@ -2079,7 +2311,7 @@ static bool apple_nhi_dp_tunnel_awaits_display(struct tb_nhi *nhi, int idx; idx = apple_dpin_index_for_port(anhi, in); - if (idx < 0 || idx > 1 || !anhi->acio) + if (!apple_dpin_valid(anhi->acio, idx)) return false; c = &anhi->acio->dpin[idx]; guard(mutex)(&c->lock); @@ -2220,6 +2452,12 @@ static int apple_nhi_probe(struct platform_device *pdev) anhi->nhi.quirks |= QUIRK_HOST_DP_NFC_CREDITS; if (acio->hw->no_host_clx) anhi->nhi.quirks |= QUIRK_NO_HOST_CLX; + if (acio->hw->no_asym) + anhi->nhi.quirks |= QUIRK_NO_ASYM; + if (acio->hw->gen4_link_recovery) + anhi->nhi.quirks |= QUIRK_GEN4_LINK_RECOVERY; + if (acio->hw->host_tmu) + anhi->nhi.quirks |= QUIRK_HOST_TMU; anhi->nhi.hop_count = readl(anhi->nhi_base + APPLE_CIO_NHI_HOP_COUNT) & APPLE_CIO_NHI_HOP_COUNT_MASK; @@ -3592,6 +3830,7 @@ static int apple_cio_probe(struct platform_device *pdev) else if (ret < 3) return dev_err_probe(dev, -EINVAL, "Not enough PM domains\n"); + acio->dpin_count = acio->hw->dpin_count ?: 2; acio->connector_np = of_graph_get_remote_node(dev->of_node, 1, -1); acio->dp_dual_stream = apple_dp_tunnel_dual_stream(acio->connector_np); /* Register layout, root qualification and connector wiring are separate. */ @@ -3606,6 +3845,8 @@ static int apple_cio_probe(struct platform_device *pdev) acio->dpin[i].idx = i; INIT_WORK(&acio->dpin[i].work, apple_dpin_work_fn); INIT_DELAYED_WORK(&acio->dpin[i].retry, apple_dpin_retry_fn); + INIT_DELAYED_WORK(&acio->dpin[i].poll, apple_dpin_poll_fn); + INIT_WORK(&acio->dpin[i].event_work, apple_dpin_event_fn); ret = devm_mutex_init(dev, &acio->dpin[i].lock); if (ret) return ret; @@ -3617,6 +3858,25 @@ static int apple_cio_probe(struct platform_device *pdev) ret = devm_add_action_or_reset(dev, apple_cio_destroy_wq, acio); if (ret) return ret; + /* Freed before the queue: started and stopped with the mapping. */ + for (unsigned int i = 0; i < acio->dpin_count && i < APPLE_DPIN_MAX; i++) { + struct apple_dpin_ctx *c = &acio->dpin[i]; + char name[8]; + + snprintf(name, sizeof(name), "dpin%u", i); + c->irq = platform_get_irq_byname_optional(to_platform_device(dev), name); + if (c->irq <= 0) + continue; + ret = devm_request_threaded_irq(dev, c->irq, NULL, + apple_dpin_irq_thread, + IRQF_ONESHOT | IRQF_NO_AUTOEN, + devm_kasprintf(dev, GFP_KERNEL, "%s-%s", + dev_name(dev), name), + c); + if (ret) + return dev_err_probe(dev, ret, + "cannot request %s interrupt\n", name); + } } /* And finally register the OOB notification for Thunderbolt/USB4 cables */ diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index 75795fbf865a1..6ec6d61f02759 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -222,6 +222,15 @@ struct tb_nhi_ops { #define QUIRK_KEEP_TUNNELS BIT(7) /* The links of the host router must not enter CL states. */ #define QUIRK_NO_HOST_CLX BIT(8) +/* The links of the domain must stay symmetric. */ +#define QUIRK_NO_ASYM BIT(9) +/* Gen4 links of the domain run with link recovery enabled on both ends. */ +#define QUIRK_GEN4_LINK_RECOVERY BIT(10) +/* + * The host router's firmware runs its TMU, and enhanced uni-directional + * mode takes the host's own constants, see tmu.c. + */ +#define QUIRK_HOST_TMU BIT(11) /* Host display policies are independent of which optional hooks are installed. */ #define TB_HOST_DP_NOTIFY BIT(0) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index a83bbcbca0194..1bc7165678e9d 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -3206,6 +3206,40 @@ int tb_switch_set_link_width(struct tb_switch *sw, enum tb_link_width width) return ret; } +/* + * Some hosts (Apple silicon) run their Gen4 links with link recovery + * enabled at both ends. Without it, the router firmware has no way back + * from a Gen4 link error but to disconnect the link. + */ +static void tb_switch_gen4_link_recovery(struct tb_switch *sw, + struct tb_port *up, struct tb_port *down) +{ + struct tb_port *ports[] = { down, up }; + unsigned int i; + u32 val; + + if (!(sw->tb->nhi->quirks & QUIRK_GEN4_LINK_RECOVERY) || + tb_port_get_link_generation(up) < 4) + return; + + for (i = 0; i < ARRAY_SIZE(ports); i++) { + struct tb_port *port = ports[i]; + + if (!port->cap_usb4 || + tb_port_read(port, &val, TB_CFG_PORT, + port->cap_usb4 + PORT_CS_19, 1)) + continue; + if (val & PORT_CS_19_GEN4_LINK_RECOVERY) + continue; + val |= PORT_CS_19_GEN4_LINK_RECOVERY; + if (tb_port_write(port, &val, TB_CFG_PORT, + port->cap_usb4 + PORT_CS_19, 1)) + tb_port_warn(port, "cannot enable Gen4 link recovery\n"); + else + tb_port_dbg(port, "Gen4 link recovery enabled\n"); + } +} + /** * tb_switch_configure_link() - Set link configured * @sw: Switch whose link is configured @@ -3236,8 +3270,15 @@ int tb_switch_configure_link(struct tb_switch *sw) down = up->remote; if (tb_switch_is_usb4(down->sw)) - return usb4_port_configure(down); - return tb_lc_configure_port(down); + ret = usb4_port_configure(down); + else + ret = tb_lc_configure_port(down); + if (ret) + return ret; + + if (tb_switch_is_usb4(up->sw) && tb_switch_is_usb4(down->sw)) + tb_switch_gen4_link_recovery(sw, up, down); + return 0; } /** diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index a4d439f2394df..f6e750743206b 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -355,6 +355,10 @@ static int tb_enable_tmu(struct tb_switch *sw) { int ret; + /* The host router's firmware runs its TMU on these hosts. */ + if (!tb_route(sw) && tb_switch_tmu_host_managed(sw)) + return 0; + /* * If both routers at the end of the link are v2 we simply * enable the enhanced uni-directional mode. That covers all @@ -746,9 +750,10 @@ static int tb_maximum_bandwidth(struct tb *tb, struct tb_port *src_port, /* * Can include asymmetric, only if it is actually supported by - * the lane adapter. + * the lane adapter and allowed on the host. */ - if (!tb_asym_supported(src_port, dst_port, port)) + if (!tb_asym_supported(src_port, dst_port, port) || + (tb->nhi->quirks & QUIRK_NO_ASYM)) include_asym = false; if (tb_is_upstream_port(port)) { @@ -1078,7 +1083,7 @@ static int tb_configure_asym(struct tb *tb, struct tb_port *src_port, struct tb_port *up; int ret = 0; - if (!asym_threshold) + if (!asym_threshold || (tb->nhi->quirks & QUIRK_NO_ASYM)) return 0; downstream = tb_port_path_direction_downstream(src_port, dst_port); @@ -3943,9 +3948,11 @@ static int tb_start(struct tb *tb, bool reset) * To support highest CLx state, we set host router's TMU to * Normal mode. */ - tb_switch_tmu_configure(tb->root_switch, TB_SWITCH_TMU_MODE_LOWRES); - /* Enable TMU if it is off */ - tb_switch_tmu_enable(tb->root_switch); + if (!tb_switch_tmu_host_managed(tb->root_switch)) { + tb_switch_tmu_configure(tb->root_switch, TB_SWITCH_TMU_MODE_LOWRES); + /* Enable TMU if it is off */ + tb_switch_tmu_enable(tb->root_switch); + } /* * Boot firmware might have created tunnels of its own. Since we diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 841d1ccb27042..a10d8d12f3d5d 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1061,6 +1061,7 @@ int tb_switch_tmu_post_time(struct tb_switch *sw); int tb_switch_tmu_disable(struct tb_switch *sw); int tb_switch_tmu_enable(struct tb_switch *sw); int tb_switch_tmu_configure(struct tb_switch *sw, enum tb_switch_tmu_mode mode); +bool tb_switch_tmu_host_managed(const struct tb_switch *sw); /** * tb_switch_tmu_is_configured() - Is given TMU mode configured diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h index 54057a331a596..827cbb739fb7e 100644 --- a/drivers/thunderbolt/tb_regs.h +++ b/drivers/thunderbolt/tb_regs.h @@ -398,6 +398,7 @@ struct tb_regs_port_header { #define PORT_CS_19_WOD BIT(17) #define PORT_CS_19_WOU4 BIT(18) #define PORT_CS_19_START_ASYM BIT(24) +#define PORT_CS_19_GEN4_LINK_RECOVERY BIT(31) /* Display Port adapter registers */ #define ADP_DP_CS_0 0x00 diff --git a/drivers/thunderbolt/tmu.c b/drivers/thunderbolt/tmu.c index cf779874c6756..ce494a044e4f0 100644 --- a/drivers/thunderbolt/tmu.c +++ b/drivers/thunderbolt/tmu.c @@ -19,7 +19,7 @@ static const unsigned int tmu_rates[] = { [TB_SWITCH_TMU_MODE_MEDRES_ENHANCED_UNI] = 16, }; -static const struct { +static const struct tmu_mode_params { unsigned int freq_meas_window; unsigned int avg_const; unsigned int delta_avg_const; @@ -37,6 +37,36 @@ static const struct { }, }; +/* + * Enhanced uni-directional mode as Apple silicon hosts run it: averaging + * and delta averaging constants 8, and replenish timeout 1875, threshold 0 + * and count 222, written to the parent side of the link only. + */ +static const struct tmu_mode_params tmu_params_host_enhanced = { + 800, 8, 8, 1875, 0, 222, 255, +}; + +/** + * tb_switch_tmu_host_managed() - Is the TMU set up the host's way + * @sw: Any router of the domain + * + * Return: %true if the host router's TMU is left to its firmware and + * enhanced uni-directional mode uses the host's constants. + */ +bool tb_switch_tmu_host_managed(const struct tb_switch *sw) +{ + return sw->tb->nhi->quirks & QUIRK_HOST_TMU; +} + +static const struct tmu_mode_params * +tb_tmu_params(const struct tb_switch *sw, enum tb_switch_tmu_mode mode) +{ + if (mode == TB_SWITCH_TMU_MODE_MEDRES_ENHANCED_UNI && + tb_switch_tmu_host_managed(sw)) + return &tmu_params_host_enhanced; + return &tmu_params[mode]; +} + static const char *tmu_mode_name(enum tb_switch_tmu_mode mode) { switch (mode) { @@ -66,8 +96,8 @@ static int tb_switch_set_tmu_mode_params(struct tb_switch *sw, u32 freq, avg, val; int ret; - freq = tmu_params[mode].freq_meas_window; - avg = tmu_params[mode].avg_const; + freq = tb_tmu_params(sw, mode)->freq_meas_window; + avg = tb_tmu_params(sw, mode)->avg_const; ret = tb_sw_read(sw, &val, TB_CFG_SWITCH, sw->tmu.cap + TMU_RTR_CS_0, 1); @@ -102,7 +132,7 @@ static int tb_switch_set_tmu_mode_params(struct tb_switch *sw, return ret; if (tb_switch_tmu_enhanced_is_supported(sw)) { - u32 delta_avg = tmu_params[mode].delta_avg_const; + u32 delta_avg = tb_tmu_params(sw, mode)->delta_avg_const; ret = tb_sw_read(sw, &val, TB_CFG_SWITCH, sw->tmu.cap + TMU_RTR_CS_18, 1); @@ -257,10 +287,10 @@ static int tb_port_set_tmu_mode_params(struct tb_port *port, u32 repl_timeout, repl_threshold, repl_n, dirswitch_n, val; int ret; - repl_timeout = tmu_params[mode].repl_timeout; - repl_threshold = tmu_params[mode].repl_threshold; - repl_n = tmu_params[mode].repl_n; - dirswitch_n = tmu_params[mode].dirswitch_n; + repl_timeout = tb_tmu_params(port->sw, mode)->repl_timeout; + repl_threshold = tb_tmu_params(port->sw, mode)->repl_threshold; + repl_n = tb_tmu_params(port->sw, mode)->repl_n; + dirswitch_n = tb_tmu_params(port->sw, mode)->dirswitch_n; ret = tb_port_read(port, &val, TB_CFG_PORT, port->cap_tmu + TMU_ADP_CS_8, 1); @@ -291,6 +321,23 @@ static int tb_port_set_tmu_mode_params(struct tb_port *port, port->cap_tmu + TMU_ADP_CS_9, 1); } +static int tb_port_tmu_dirswitch_write(struct tb_port *port, u32 dirswitch_n) +{ + int ret; + u32 val; + + ret = tb_port_read(port, &val, TB_CFG_PORT, + port->cap_tmu + TMU_ADP_CS_9, 1); + if (ret) + return ret; + + val &= ~TMU_ADP_CS_9_DIRSWITCH_N_MASK; + val |= FIELD_PREP(TMU_ADP_CS_9_DIRSWITCH_N_MASK, dirswitch_n); + + return tb_port_write(port, &val, TB_CFG_PORT, + port->cap_tmu + TMU_ADP_CS_9, 1); +} + /* Can be called to non-v2 lane adapters too */ static int tb_port_tmu_rate_write(struct tb_port *port, int rate) { @@ -786,7 +833,29 @@ static int tb_switch_tmu_enable_enhanced(struct tb_switch *sw) up = tb_upstream_port(sw); down = tb_switch_downstream_port(sw); - ret = tb_port_set_tmu_mode_params(up, sw->tmu.mode_request); + /* + * The host's way: time sync off on both lane adapters first, then + * on again once the mode is programmed (below). The host router + * applies its TMU configuration only when its lane adapter's time + * sync goes from off to on, and it comes out of reset on. + */ + if (tb_switch_tmu_host_managed(sw)) { + ret = tb_port_tmu_time_sync_disable(up); + if (!ret) + ret = tb_port_tmu_time_sync_disable(down); + if (ret) + goto out; + } + + /* + * The host's way: replenish parameters on the parent side only, + * but the direction switch count on both. + */ + if (tb_switch_tmu_host_managed(sw)) + ret = tb_port_tmu_dirswitch_write(up, + tb_tmu_params(sw, sw->tmu.mode_request)->dirswitch_n); + else + ret = tb_port_set_tmu_mode_params(up, sw->tmu.mode_request); if (ret) goto out; @@ -810,6 +879,16 @@ static int tb_switch_tmu_enable_enhanced(struct tb_switch *sw) if (ret) goto out; + /* Time sync on again: device side first, then the host router's. */ + if (tb_switch_tmu_host_managed(sw)) { + ret = tb_port_tmu_time_sync_enable(up); + if (ret) + goto out; + ret = tb_port_tmu_time_sync_enable(down); + if (ret) + goto out; + } + return 0; out: @@ -962,9 +1041,12 @@ int tb_switch_tmu_enable(struct tb_switch *sw) return ret; } - ret = tb_switch_tmu_set_time_disruption(sw, true); - if (ret) - return ret; + /* The host's way leaves the router's time disruption bit alone. */ + if (!tb_switch_tmu_host_managed(sw)) { + ret = tb_switch_tmu_set_time_disruption(sw, true); + if (ret) + return ret; + } if (tb_route(sw)) { /* @@ -1014,6 +1096,8 @@ int tb_switch_tmu_enable(struct tb_switch *sw) tb_sw_dbg(sw, "TMU: mode set to: %s\n", tmu_mode_name(sw->tmu.mode)); } + if (tb_switch_tmu_host_managed(sw)) + return ret; return tb_switch_tmu_set_time_disruption(sw, false); } diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index 947a297f4d09e..94b1cac077e97 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -1319,6 +1319,37 @@ static int tb_dp_bandwidth_alloc_mode_enable(struct tb_tunnel *tunnel) return 0; } +/* + * An Apple silicon host sets the SSC offset that the DP OUT adapter's + * stream clock recovery allows, the low half of TMU capability word 6, to + * 1500 before it touches the adapter otherwise. Docks come up with their + * own value (2500 on a Barlow Ridge dock). + */ +#define TB_DP_APPLE_SSC_OFFSET 1500 + +static void tb_dp_apple_ssc_offset(struct tb_tunnel *tunnel) +{ + struct tb_port *out = tunnel->dst_port; + u32 v, old; + + if (!tb_port_is_apple_host_dpin(tunnel->src_port) || + !tb_port_is_dpout(out) || !out->cap_tmu) + return; + /* Only inside the TMU capability: before its next-capability pointer. */ + if (tb_port_read(out, &v, TB_CFG_PORT, out->cap_tmu, 1) || + ((v & 0xff) && (v & 0xff) <= out->cap_tmu + 6) || + tb_port_read(out, &v, TB_CFG_PORT, out->cap_tmu + 6, 1)) + return; + old = v; + v = (v & ~GENMASK(15, 0)) | TB_DP_APPLE_SSC_OFFSET; + if (v != old && + tb_port_write(out, &v, TB_CFG_PORT, out->cap_tmu + 6, 1)) { + tb_port_warn(out, "cannot set the DP SSC offset\n"); + return; + } + tb_port_dbg(out, "DP SSC offset %#x (was %#x)\n", v & 0xffff, old & 0xffff); +} + static int tb_dp_pre_activate(struct tb_tunnel *tunnel) { struct tb_port *in = tunnel->src_port; @@ -1326,6 +1357,8 @@ static int tb_dp_pre_activate(struct tb_tunnel *tunnel) struct tb *tb = in->sw->tb; int ret; + tb_dp_apple_ssc_offset(tunnel); + ret = tb_dp_xchg_caps(tunnel); if (ret) return ret; @@ -1609,8 +1642,9 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) /* * Apple silicon host: the sink link is trained by the * host's DPTX through the tunnel; keep this DP OUT adapter - * from starting link training on its own, and hand it back - * once the tunnel is gone. + * from starting link training on its own. The bit is left + * as it is when the tunnel goes: the next tunnel to this + * adapter sets it again before enabling it. */ if (active && apple_dpin && !tb_port_read(out, &v, TB_CFG_PORT, out->cap_adap + ADP_DP_CS_3, 1)) { @@ -1619,13 +1653,6 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) tb_port_warn(out, "Apple: cannot hold off DP link training\n"); } ret = tb_dp_port_enable(out, active); - /* hand link training back even if the disable failed */ - if (!active && apple_dpin && - !tb_port_read(out, &v, TB_CFG_PORT, out->cap_adap + ADP_DP_CS_3, 1)) { - v &= ~ADP_DP_CS_3_NO_AUTO_LT; - if (tb_port_write(out, &v, TB_CFG_PORT, out->cap_adap + ADP_DP_CS_3, 1)) - tb_port_warn(out, "Apple: cannot restore DP link training\n"); - } if (ret && active) return ret; }