From f47fe85d66d8bbfeb8f35b07c9f6435a24e425f9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 13:48:07 +1100 Subject: [PATCH 01/11] phy: apple: keep failed PIPE setup retryable A fatal PIPE check or USB3 host lock failure returns before the mux is changed, but the wrapper still records the requested backend. Subsequent PHY set_mode calls then skip the failed setup. Parking has the same false state update when its initial check fails. Record the backend only after successful setup or parking. When a fixed hub's lane mode already matches the Type-C request, retry an outstanding PIPE setup without reconfiguring lanes or disturbing DisplayPort ownership. Keep the existing mode-dirty gate and fatal USB3 host lock ACK requirement. Add host fault injection of the production PIPE functions and PHY/Type-C callbacks. Cover all mode backends, both USB3 host BIST generations, repeated fixed-hub retries, parking, SAFE USB2 and capability guards. The baseline fails each regression group; the corrected functions pass with address and undefined-behavior sanitizers. The register sequence is unchanged. Signed-off-by: Ryan Murray Assisted-by: LLM --- MAINTAINERS | 1 + drivers/phy/apple/atc.c | 17 +- tools/diagnostics/test-apple-atc-pipe.py | 310 +++++++++++++++++++++++ 3 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 tools/diagnostics/test-apple-atc-pipe.py diff --git a/MAINTAINERS b/MAINTAINERS index f3d453a49eb90..2a85175e6c50d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2020,6 +2020,7 @@ F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/display-attributes-fixture.json F: tools/diagnostics/test-apple-afk-admission.py +F: tools/diagnostics/test-apple-atc-pipe.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py F: tools/diagnostics/test-apple-dptx-readiness.py diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index ea04dec0f0d05..4f29e720f0a0c 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1415,8 +1415,7 @@ static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) /* * Route the PIPE to the backend the current mode needs. The state is recorded - * whether or not the sequence succeeded: the PIPE is configured once per mode, - * and a failed attempt is not retried on the next set_mode call. + * only after the sequence succeeds, so a failed attempt remains retryable. */ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) { @@ -1436,7 +1435,8 @@ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) ret = atcphy_configure_pipehandler_dummy(atcphy); break; } - atcphy->pipe_state = state; + if (!ret) + atcphy->pipe_state = state; return ret; } @@ -1452,8 +1452,10 @@ static void atcphy_park_pipehandler(struct apple_atcphy *atcphy) return; ret = atcphy_configure_pipehandler_dummy(atcphy); - if (ret) + if (ret) { dev_warn(atcphy->dev, "Failed to switch PIPE to dummy: %d\n", ret); + return; + } atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; } @@ -3281,8 +3283,13 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta target_mode = APPLE_ATCPHY_MODE_USB2; dev_dbg(atcphy->dev, "mux target=%u\n", target_mode); - if (atcphy->mode == target_mode && !atcphy->mode_dirty) + if (atcphy->mode == target_mode && !atcphy->mode_dirty) { + /* Retry a failed PIPE setup without disturbing the configured lanes. */ + if (atcphy->fixed_usb2 && atcphy->host_active && + atcphy->pipe_state != atcphy_modes[target_mode].pipehandler_state) + return atcphy_configure_pipehandler(atcphy, true); return 0; + } switch (target_mode) { case APPLE_ATCPHY_MODE_TBT: diff --git a/tools/diagnostics/test-apple-atc-pipe.py b/tools/diagnostics/test-apple-atc-pipe.py new file mode 100644 index 0000000000000..fa7fbac85d842 --- /dev/null +++ b/tools/diagnostics/test-apple-atc-pipe.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Fault-inject production PIPE and mux callbacks against register mocks.""" +import hashlib +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / 'drivers/phy/apple/atc.c').read_text() + + +def block(marker): + start = source.index(marker) + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +def function(name): + return block(re.search(r'^static [^\n]+\b' + name + r'\(', source, re.M).group()) + + +parts = re.findall(r'^#define [^\n]*(?:\\\n[^\n]*)*', source, re.M) +for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode'): + parts.append(block('enum ' + name + ' {') + ';') +for name in ('atcphy_mode_configuration', 'atcphy_hw'): + parts.append(block('struct ' + name + ' {') + ';') +parts.append(r''' +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *pipehandler; } regs; + void *dev; + int lock; + bool fixed_usb2, host_active, ss_tunables, dp_tunables, mode_dirty; + unsigned int dp_owned_pmas; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +''') +start = source.index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') +parts.append(source[start:source.index('\n};', start) + 3]) +parts.append(r''' +static u32 pipe_regs[0x1000 / 4], core_regs[0x30000 / 4]; +static unsigned int polls, fail_poll, mux_writes, lane_calls; +static bool stuck_ack; +static u32 readl(void *address) { return *(u32 *)address; } +static void mask32(void *address, u32 mask, u32 value) +{ + *(u32 *)address = (*(u32 *)address & ~mask) | value; + if (address == (void *)&pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)address & PIPEHANDLER_LOCK_EN; + if (address == (void *)&pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) + mux_writes++; +} +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +static void writel(u32 v, void *p) { mask32(p, ~0U, v); } +static void core_mask32(struct apple_atcphy *p, u32 o, u32 m, u32 v) +{ assert(o < sizeof(core_regs) && o % 4 == 0); mask32(p->regs.core + o, m, v); } +static void core_set32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, 0, v); } +static void core_clear32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, v, 0); } +#define readl_poll_timeout(address, value, condition, interval, timeout) \ + ({ (value) = readl(address); polls++; \ + polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; }) +/* Lane configuration is outside this test: count calls and commit its result. */ +static int atcphy_configure(struct apple_atcphy *p, enum atcphy_mode mode) +{ lane_calls++; p->mode = mode; p->mode_dirty = false; return 0; } +''') +for name in ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', 'atcphy_pipehandler_check', + 'atcphy_pipehandler_set_mux', 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_usb3_set_mode', 'atcphy_mux_set'): + parts.append(function(name)) +production = '\n'.join(parts) +print('Production PIPE/mux SHA256:', hashlib.sha256(production.encode()).hexdigest(), flush=True) +prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint16_t u16; +#define BIT(n) (1U << (n)) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +#define FIELD_PREP(m, v) (((u32)(v) << __builtin_ctz(m)) & (m)) +#define FIELD_GET(m, v) (((v) & (m)) >> __builtin_ctz(m)) +#define field_prep FIELD_PREP +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +#define lockdep_assert_held(...) ((void)0) +#define WARN_ON_ONCE(v) assert(!(v)) +#define guard(kind) (void) +#define fallthrough __attribute__((fallthrough)) +#define udelay(n) ((void)(n)) +enum { TYPEC_STATE_SAFE, TYPEC_STATE_USB, TYPEC_MODE_USB4, + TYPEC_DP_STATE_C, TYPEC_DP_STATE_D, TYPEC_DP_STATE_E }; +enum { USB_TYPEC_TBT_SID = 0x8087, USB_TYPEC_DP_SID = 0xff01 }; +#define EUDO_USB_MODE_MASK 3U +enum { EUDO_USB_MODE_USB2, EUDO_USB_MODE_USB3, EUDO_USB_MODE_USB4 }; +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE, PHY_MODE_INVALID }; +struct enter_usb_data { u32 eudo; }; +struct typec_altmode { unsigned int svid; }; +struct typec_mux_state { unsigned long mode; struct typec_altmode *alt; void *data; }; +struct typec_mux_dev { struct apple_atcphy *data; }; +struct phy { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +#define phy_get_drvdata(phy) ((phy)->data) +''' +suffix = r''' +static struct atcphy_hw hw = { + .gen = ATCPHY_GENERATION_T8122, .has_t8140_dp = true, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, +}; +static void initial(struct apple_atcphy *p, enum atcphy_mode mode, + enum atcphy_pipehandler_state backend) +{ + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(core_regs, 0, sizeof(core_regs)); + core_regs[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + *p = (struct apple_atcphy) { + .hw = &hw, .regs.core = (unsigned char *)core_regs, + .regs.pipehandler = (unsigned char *)pipe_regs, + .mode = mode, .typec_mode = mode, .pipe_state = backend, + .ss_tunables = true, .dp_tunables = true, + }; + polls = fail_poll = mux_writes = lane_calls = 0; + stuck_ack = false; +} +static int request(struct apple_atcphy *p, enum atcphy_mode mode) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct enter_usb_data data = { .eudo = EUDO_USB_MODE_USB4 }; + struct typec_mux_state state = { .mode = TYPEC_STATE_SAFE }; + if (mode == APPLE_ATCPHY_MODE_USB3) + state.mode = TYPEC_STATE_USB; + else if (mode == APPLE_ATCPHY_MODE_USB3_DP || mode == APPLE_ATCPHY_MODE_DP) { + state.mode = mode == APPLE_ATCPHY_MODE_DP ? TYPEC_DP_STATE_C : TYPEC_DP_STATE_D; + state.alt = &dp; + } else if (mode == APPLE_ATCPHY_MODE_USB4) { + state.mode = TYPEC_MODE_USB4; + state.data = &data; + } + return atcphy_mux_set(&mux, &state); +} +static void backend_failure(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (enum atcphy_mode mode = APPLE_ATCPHY_MODE_OFF; mode <= APPLE_ATCPHY_MODE_DP; mode++) { + enum atcphy_pipehandler_state target = atcphy_modes[mode].pipehandler_state; + enum atcphy_pipehandler_state old = target == ATCPHY_PIPEHANDLER_STATE_DUMMY ? + ATCPHY_PIPEHANDLER_STATE_USB3 : ATCPHY_PIPEHANDLER_STATE_DUMMY; + for (unsigned int host = 0; host < 2; host++) { + initial(&p, mode, old); + /* A previous lock ACK cannot be cleared: fatal before mux writes. */ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_configure_pipehandler(&p, host) == -ETIMEDOUT); + assert(p.pipe_state == old && mux_writes == 0); + stuck_ack = false; + assert(atcphy_configure_pipehandler(&p, host) == 0); + assert(p.pipe_state == target && mux_writes == 3); + const unsigned int data[] = { PIPEHANDLER_MUX_CTRL_DATA_DUMMY, + PIPEHANDLER_MUX_CTRL_DATA_USB3, PIPEHANDLER_MUX_CTRL_DATA_USB4 }; + const unsigned int clk[] = { PIPEHANDLER_MUX_CTRL_CLK_DUMMY, + PIPEHANDLER_MUX_CTRL_CLK_USB3, PIPEHANDLER_MUX_CTRL_CLK_USB4 }; + u32 mux = pipe_regs[PIPEHANDLER_MUX_CTRL / 4]; + assert(FIELD_GET(hw.pipe_mux_data, mux) == data[target]); + assert(FIELD_GET(hw.pipe_mux_clk, mux) == clk[target]); + cases++; + } + } + printf("PASS: %u backend/role check-failure and retry cases\n", cases); +} +static void host_lock_failure(void) +{ + struct apple_atcphy p; + for (unsigned int gen = 0; gen < 2; gen++) { + hw.gen = gen ? ATCPHY_GENERATION_T8122 : ATCPHY_GENERATION_T8103; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + struct phy phy = { &p }; + stuck_ack = true; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == -ETIMEDOUT); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 0); + assert(!(pipe_regs[PIPEHANDLER_LOCK_REQ / 4] & PIPEHANDLER_LOCK_EN)); + stuck_ack = false; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 3); + unsigned int before = polls; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == 0); + assert(polls == before && mux_writes == 3); + } + puts("PASS: both USB3 host BIST generations retain fatal lock ACK and retry"); +} +static void mux_retry(void) +{ + struct apple_atcphy p; + const enum atcphy_mode modes[] = { APPLE_ATCPHY_MODE_USB3, APPLE_ATCPHY_MODE_USB3_DP }; + for (unsigned int m = 0; m < 2; m++) { + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + stuck_ack = true; + assert(request(&p, modes[m]) == -ETIMEDOUT); + assert(p.mode == modes[m] && !p.mode_dirty && lane_calls == 1); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 0); + p.dp_owned_pmas = 3; /* PIPE retry must not reconfigure an owned DP mode. */ + assert(request(&p, modes[m]) == -ETIMEDOUT); + assert(lane_calls == 1 && p.dp_owned_pmas == 3 && mux_writes == 0); + stuck_ack = false; + assert(request(&p, modes[m]) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && lane_calls == 1); + assert(p.dp_owned_pmas == 3 && mux_writes == 3); + unsigned int before = polls; + assert(request(&p, modes[m]) == 0 && polls == before && lane_calls == 1); + } + puts("PASS: fixed-hub same-mode retry preserves lanes and DP ownership"); +} +static void same_mode_pending(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + stuck_ack = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == -ETIMEDOUT); + assert(lane_calls == 0 && mux_writes == 0); + stuck_ack = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && lane_calls == 0); + puts("PASS: same-mode pending PIPE setup independently retries"); +} +static void parking_and_guards(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + atcphy_park_pipehandler(&p); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 0); + stuck_ack = false; + atcphy_park_pipehandler(&p); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 3); + atcphy_park_pipehandler(&p); + assert(mux_writes == 3); + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + assert(request(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && lane_calls == 0 && mux_writes == 0); + p.mode_dirty = true; + assert(request(&p, APPLE_ATCPHY_MODE_OFF) == 0 && lane_calls == 1 && !p.mode_dirty); + /* Inactive hosts and ordinary ports retain their deferred PIPE setup. */ + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0 && mux_writes == 0); + p.fixed_usb2 = false; + p.host_active = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0 && mux_writes == 0); + /* Capability rejection must still precede mode configuration. */ + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + p.ss_tunables = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == -EOPNOTSUPP && lane_calls == 0); + p.ss_tunables = true; + p.dp_tunables = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3_DP) == -EOPNOTSUPP && lane_calls == 0); + assert(request(&p, APPLE_ATCPHY_MODE_USB4) == -EOPNOTSUPP && lane_calls == 0); + puts("PASS: parking retry, SAFE fixed USB2, dirty-mode and capability guards"); +} +int main(int argc, char **argv) +{ + assert(argc == 2); + if (!strcmp(argv[1], "backend")) backend_failure(); + else if (!strcmp(argv[1], "host")) host_lock_failure(); + else if (!strcmp(argv[1], "mux")) mux_retry(); + else if (!strcmp(argv[1], "same")) same_mode_pending(); + else if (!strcmp(argv[1], "park")) parking_and_guards(); + else assert(false); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='apple-atc-pipe-') as directory: + path = Path(directory) + unit = path / 'test.c' + unit.write_text(prefix + production + suffix) + command = shlex.split(os.environ.get('CC', 'clang')) + command += ['-std=gnu11', '-Wall', '-Werror', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) + failed = False + for case in ('backend', 'host', 'mux', 'same', 'park'): + result = subprocess.run([str(path / 'test'), case]) + if result.returncode: + print('FAIL:', case, 'exit', result.returncode, flush=True) + failed = True + raise SystemExit(1 if failed else 0) From 4b2a0cf3a094bd60fdbcdeb579650edd72feaff9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 13:57:20 +1100 Subject: [PATCH 02/11] phy: apple: propagate PIPE parking failures A failed PIPE parking check leaves the previous backend selected. The fixed-hub mux and USB3 power-off callbacks nevertheless continue lane or power changes, and the reset callback also powers down USB2 and reports success. This drops ownership assumptions while the PIPE is still live. Return the parking error and stop each caller before its next transition. Park before asserting DWC3 reset so a fatal check cannot leave the controller reset or USB2 powered off. Keep host state, mode dirtiness and DP ownership intact on failure. The PIPE handshake policy is unchanged. Add sanitized host fault injection of the production mux, power-off and reset callbacks, including their actual PIPE, mode, AUX and PMA functions. Cover repeated failures and recovery across dirty states and orientation, and verify fixed-hub SAFE retains USB2. Adapt existing PIPE and unwind tests to the parking return value. All three caller groups fail on the parent and pass after propagation; the W=1 ARM64 ATC object also builds. Signed-off-by: Ryan Murray Assisted-by: LLM --- MAINTAINERS | 1 + drivers/phy/apple/atc.c | 26 +- tools/diagnostics/test-apple-atc-parking.py | 363 ++++++++++++++++++++ tools/diagnostics/test-apple-atc-pipe.py | 6 +- tools/diagnostics/test-apple-atc-unwind.py | 4 +- 5 files changed, 388 insertions(+), 12 deletions(-) create mode 100644 tools/diagnostics/test-apple-atc-parking.py diff --git a/MAINTAINERS b/MAINTAINERS index 2a85175e6c50d..57aa876a8e359 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2020,6 +2020,7 @@ F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/display-attributes-fixture.json F: tools/diagnostics/test-apple-afk-admission.py +F: tools/diagnostics/test-apple-atc-parking.py F: tools/diagnostics/test-apple-atc-pipe.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 4f29e720f0a0c..e97ce77fe5f31 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1442,21 +1442,22 @@ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) } /* Route the PIPE back to the dummy backend unless it is there already */ -static void atcphy_park_pipehandler(struct apple_atcphy *atcphy) +static int atcphy_park_pipehandler(struct apple_atcphy *atcphy) { int ret; lockdep_assert_held(&atcphy->lock); if (atcphy->pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY) - return; + return 0; ret = atcphy_configure_pipehandler_dummy(atcphy); if (ret) { dev_warn(atcphy->dev, "Failed to switch PIPE to dummy: %d\n", ret); - return; + return ret; } atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + return 0; } static void atcphy_setup_pipehandler(struct apple_atcphy *atcphy) @@ -2869,10 +2870,13 @@ static const struct phy_ops apple_atc_usb2_phy_ops = { static int atcphy_usb3_power_off(struct phy *phy) { struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; guard(mutex)(&atcphy->lock); - atcphy_park_pipehandler(atcphy); + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; atcphy->host_active = false; if (atcphy->mode != APPLE_ATCPHY_MODE_OFF) @@ -3124,11 +3128,16 @@ static void _atcphy_dwc3_reset_assert(struct apple_atcphy *atcphy) static int atcphy_dwc3_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct apple_atcphy *atcphy = container_of(rcdev, struct apple_atcphy, rcdev); + int ret; guard(mutex)(&atcphy->lock); + /* Do not reset the controller or USB2 PHY if PIPE parking fails. */ + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + _atcphy_dwc3_reset_assert(atcphy); - atcphy_park_pipehandler(atcphy); atcphy->host_active = false; atcphy_usb2_power_off(atcphy); @@ -3324,8 +3333,11 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta * mode without a USB3 backend, and route it to the new backend * afterwards while dwc3 is up in host mode. */ - if (pipe_state != ATCPHY_PIPEHANDLER_STATE_USB3) - atcphy_park_pipehandler(atcphy); + if (pipe_state != ATCPHY_PIPEHANDLER_STATE_USB3) { + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + } ret = atcphy_configure(atcphy, target_mode); if (ret) diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py new file mode 100644 index 0000000000000..e16841cdc73d4 --- /dev/null +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Fault-inject production PIPE parking, mux, power-off and reset callbacks.""" +import hashlib +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +atc = (root / 'drivers/phy/apple/atc.c').read_text() +dp = (root / 'drivers/phy/apple/atc-j700-dp.h').read_text() + + +def block(source, marker): + start = source.index(marker) + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +def function(source, name): + return block(source, re.search(r'^static [^\n]+\b' + name + r'\(', source, re.M).group()) + + +# Use the production mode table and wrappers too, so SAFE mapping and fast paths +# are tested rather than mirrored by a second implementation. +parts = re.findall(r'^#define [^\n]*(?:\\\n[^\n]*)*', atc, re.M) +for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode', + 'atcphy_dp_link_rate'): + parts.append(block(atc, 'enum ' + name + ' {') + ';') +for name in ('atcphy_mode_configuration', 'atcphy_hw'): + parts.append(block(atc, 'struct ' + name + ' {') + ';') +parts.append(r''' +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *lpdptx, *pipehandler, *usb2phy; } regs; + struct reset_controller_dev rcdev; + void *dev; + int lock, dp_link_rate; + bool fixed_usb2, host_active, swap_lanes, ss_tunables, dp_tunables; + bool aux_power_requested, mode_dirty, dp_spread_spectrum, dp_presets_valid[5]; + u32 dp_presets[5][16]; + unsigned int dp_idle_pmas, dp_owned_pmas; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +''') +start = atc.index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') +parts.append(atc[start:atc.index('\n};', start) + 3]) +parts.append(r''' +struct phy_configure_opts_dp { + unsigned int link_rate, lanes, voltage[4], pre[4]; + bool set_rate, set_lanes, set_voltages, ssc; +}; +static u32 registers[0x30000 / 4]; +static u32 pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; +static unsigned int polls, fail_poll, writes, usb2_on, usb2_writes, reset_writes, mux_writes; +static bool stuck_ack; +static u32 readl(void *address) { return *(u32 *)address; } +static void core_mask32(struct apple_atcphy *p, u32 offset, u32 mask, u32 value) +{ + (void)p; + assert(offset < sizeof(registers) && offset % 4 == 0); + registers[offset / 4] = (registers[offset / 4] & ~mask) | value; + writes++; + if (offset == ATCPHY_POWER_CTRL) + registers[ATCPHY_POWER_STAT / 4] = registers[offset / 4]; + if (offset == 0x2000) { + if (value == 0x10000001) + registers[offset / 4] |= BIT(1); + else { + registers[offset / 4] &= ~BIT(1); + registers[0x7034 / 4] &= ~BIT(3); + } + } +} +static void core_set32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, 0, v); } +static void core_clear32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, v, 0); } +static void mask32(void *p, u32 m, u32 v) +{ + *(u32 *)p = (*(u32 *)p & ~m) | v; + if (p == (void *)&pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)p & PIPEHANDLER_LOCK_EN; + if (p == (void *)&pipe_regs[PIPEHANDLER_AON_GEN / 4]) reset_writes++; + if (p == (void *)&pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) mux_writes++; + if ((uintptr_t)p >= (uintptr_t)usb2_regs && + (uintptr_t)p < (uintptr_t)usb2_regs + sizeof(usb2_regs)) usb2_writes++; +} +static void writel(u32 value, void *address) { mask32(address, ~0U, value); } +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +#define readl_poll_timeout_atomic(address, value, condition, interval, timeout) \ + ({ (value) = readl(address); polls++; \ + polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; }) +#define readl_poll_timeout readl_poll_timeout_atomic +static void atcphy_usb2_power_on(struct apple_atcphy *p) { (void)p; usb2_on++; } +static void atcphy_apply_tunables(struct apple_atcphy *p, enum atcphy_mode mode) +{ (void)p; (void)mode; } +static void atc_tunnel_stop_t8103(struct apple_atcphy *p) { (void)p; } +static void atc_tunnel_restore(struct apple_atcphy *p) { (void)p; } +''') +for name in ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', 'atcphy_pipehandler_check', + 'atcphy_pipehandler_set_mux', 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_usb2_power_off', '_atcphy_dwc3_reset_assert', 'atcphy_dwc3_reset_assert'): + parts.append(function(atc, name)) +for name in ('atcphy_get_mode_config', 'j700_field_update', 'j700_pma_idle'): + parts.append(function(atc, name)) +parts.append(dp) +for name in ('atcphy_configure_lanes', 'j700_aux_enable', 'j700_aux_disable', + 'atcphy_enable_dp_aux', 'atcphy_disable_dp_aux', 'atcphy_power_off', + 'atcphy_power_on', 'atcphy_configure', 'atcphy_mux_set', 'atcphy_usb3_power_off'): + parts.append(function(atc, name)) +production = '\n'.join(parts) +print('Production parking/caller SHA256:', hashlib.sha256(production.encode()).hexdigest(), flush=True) +prefix = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint16_t u16; +#define BIT(n) (1U << (n)) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +#define FIELD_PREP(m, v) (((u32)(v) << __builtin_ctz(m)) & (m)) +#define FIELD_GET(m, v) (((v) & (m)) >> __builtin_ctz(m)) +#define field_prep FIELD_PREP +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define hweight32(v) __builtin_popcount(v) +#define dev_info_once(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +static void report(void *dev, const char *fmt, ...) { (void)dev; (void)fmt; } +#define dev_err report +#define lockdep_assert_held(...) ((void)0) +#define WARN_ON_ONCE(v) assert(!(v)) +#define guard(kind) (void) +#define fallthrough __attribute__((fallthrough)) +#define ndelay(n) ((void)(n)) +#define udelay(n) ((void)(n)) +#define usleep_range(a, b) ((void)(a), (void)(b)) +enum { TYPEC_STATE_SAFE, TYPEC_STATE_USB, TYPEC_MODE_USB4, + TYPEC_DP_STATE_C, TYPEC_DP_STATE_D, TYPEC_DP_STATE_E }; +enum { USB_TYPEC_TBT_SID = 0x8087, USB_TYPEC_DP_SID = 0xff01 }; +#define EUDO_USB_MODE_MASK 3U +enum { EUDO_USB_MODE_USB2, EUDO_USB_MODE_USB3, EUDO_USB_MODE_USB4 }; +struct enter_usb_data { u32 eudo; }; +struct typec_altmode { unsigned int svid; }; +struct typec_mux_state { unsigned long mode; struct typec_altmode *alt; void *data; }; +struct typec_mux_dev { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +struct phy { struct apple_atcphy *data; }; +#define phy_get_drvdata(phy) ((phy)->data) +struct reset_controller_dev { int unused; }; +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +''' +suffix = r''' +static struct atcphy_hw hw = { + .has_t8140_dp = true, .gen = ATCPHY_GENERATION_T8122, + .aciophy_lane_mode = ACIOPHY_LANE_MODE_T8122, + .aciophy_crossbar = ACIOPHY_CROSSBAR_T8122, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, +}; +static void initial(struct apple_atcphy *p, bool fixed, bool dirty, bool swapped) +{ + memset(registers, 0, sizeof(registers)); + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(usb2_regs, 0, sizeof(usb2_regs)); + registers[AUS_COMMON_DIG_RCAL1 / 4] = AUS_COMMON_DIG_RCAL1_ALL_CODES_DONE; + pipe_regs[PIPEHANDLER_AON_GEN / 4] = PIPEHANDLER_AON_GEN_DWC3_RESET_N; + usb2_regs[USB2PHY_USBCTL / 4] = USB2PHY_USBCTL_RUN; + *p = (struct apple_atcphy) { + .hw = &hw, .regs.core = (unsigned char *)registers, + .regs.lpdptx = (unsigned char *)registers, + .regs.pipehandler = (unsigned char *)pipe_regs, + .regs.usb2phy = (unsigned char *)usb2_regs, + .fixed_usb2 = fixed, .host_active = true, .swap_lanes = swapped, + .ss_tunables = true, .dp_tunables = true, .dp_presets_valid = { true }, + .mode = APPLE_ATCPHY_MODE_USB3_DP, .typec_mode = APPLE_ATCPHY_MODE_USB3_DP, + .pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3, .mode_dirty = dirty, + .dp_owned_pmas = 3, .dp_idle_pmas = dirty ? 1 : 0, + .aux_power_requested = true, + }; + polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; + stuck_ack = false; +} +static void inject_parking(unsigned int fault) +{ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + if (fault) + fail_poll = 1; + else + stuck_ack = true; +} +static void recover(void) { stuck_ack = false; fail_poll = polls = 0; } +static int request(struct apple_atcphy *p, bool safe) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct typec_mux_state state = { + .mode = safe ? TYPEC_STATE_SAFE : TYPEC_DP_STATE_C, + .alt = safe ? NULL : &dp, + }; + return atcphy_mux_set(&mux, &state); +} +static void unchanged(struct apple_atcphy *p, bool dirty, bool host) +{ + assert(p->mode == APPLE_ATCPHY_MODE_USB3_DP && p->mode_dirty == dirty); + assert(p->pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p->host_active == host); + assert(p->dp_owned_pmas == 3 && p->dp_idle_pmas == (dirty ? 1U : 0U)); + assert(p->aux_power_requested); + assert(writes == 0 && usb2_on == 0 && usb2_writes == 0 && reset_writes == 0); + assert(mux_writes == 0); + assert(pipe_regs[PIPEHANDLER_AON_GEN / 4] == PIPEHANDLER_AON_GEN_DWC3_RESET_N); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + if (dirty) { + struct phy_configure_opts_dp training = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(p, &training) == -EBUSY && writes == 0); + } +} +static void observed(const char *caller, int result) +{ + printf("%s: result=%d core_writes=%u usb2_writes=%u reset_writes=%u\n", + caller, result, writes, usb2_writes, reset_writes); + fflush(stdout); +} +static void mux_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int swapped = 0; swapped < 2; swapped++) + for (unsigned int host = 0; host < 2; host++) + for (unsigned int safe = 0; safe < 2; safe++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, true, dirty, swapped); + p.host_active = host; + inject_parking(fault); + int ret = request(&p, safe); + observed("mux parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, host); + if (!fault) { + assert(request(&p, safe) == -ETIMEDOUT); + unchanged(&p, dirty, host); + } + recover(); + assert(request(&p, safe) == 0); + assert(p.mode == (safe ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_DP)); + assert(!p.mode_dirty && p.host_active == host); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !p.dp_owned_pmas); + assert(!p.dp_idle_pmas && p.aux_power_requested == !safe); + assert(usb2_on == 1 && usb2_writes == 0 && reset_writes == 0); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + unsigned int before = writes; + assert(request(&p, safe) == 0 && writes == before && usb2_on == 1); + cases++; + } + printf("PASS: %u actual mux parking/retry scenarios preserve fixed USB2 and ownership\n", cases); +} +static void power_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int swapped = 0; swapped < 2; swapped++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, fixed, dirty, swapped); + struct phy phy = { &p }; + inject_parking(fault); + int ret = atcphy_usb3_power_off(&phy); + observed("power-off parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, true); + if (!fault) { + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + unchanged(&p, dirty, true); + } + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + assert(usb2_writes == 0 && reset_writes == 0); + unsigned int before = writes; + assert(atcphy_usb3_power_off(&phy) == 0 && writes == before); + cases++; + } + printf("PASS: %u actual power-off parking/retry scenarios\n", cases); +} +static void reset_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, fixed, dirty, false); + inject_parking(fault); + int ret = atcphy_dwc3_reset_assert(&p.rcdev, 0); + observed("reset parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, true); + if (!fault) { + assert(atcphy_dwc3_reset_assert(&p.rcdev, 0) == -ETIMEDOUT); + unchanged(&p, dirty, true); + } + recover(); + assert(atcphy_dwc3_reset_assert(&p.rcdev, 0) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !p.host_active); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty == dirty); + assert(p.dp_owned_pmas == 3 && p.dp_idle_pmas == (dirty ? 1U : 0U)); + assert(p.aux_power_requested && writes == 0); + assert(reset_writes == 2 && usb2_writes > 0); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); + assert(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_FORCE_CLAMP_EN); + assert(!(usb2_regs[USB2PHY_USBCTL / 4] & USB2PHY_USBCTL_RUN)); + cases++; + } + printf("PASS: %u actual reset parking/retry scenarios retain DP ownership\n", cases); +} +int main(int argc, char **argv) +{ + assert(argc == 2); + if (!strcmp(argv[1], "mux")) mux_parking(); + else if (!strcmp(argv[1], "power")) power_parking(); + else if (!strcmp(argv[1], "reset")) reset_parking(); + else assert(false); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='apple-atc-parking-') as directory: + path = Path(directory) + unit = path / 'test.c' + unit.write_text(prefix + production + suffix) + command = shlex.split(os.environ.get('CC', 'clang')) + command += ['-std=gnu11', '-Wall', '-Werror', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) + failed = False + for case in ('mux', 'power', 'reset'): + result = subprocess.run([str(path / 'test'), case]) + if result.returncode: + print('FAIL:', case, 'exit', result.returncode, flush=True) + failed = True + raise SystemExit(1 if failed else 0) diff --git a/tools/diagnostics/test-apple-atc-pipe.py b/tools/diagnostics/test-apple-atc-pipe.py index fa7fbac85d842..7a6525d8588b6 100644 --- a/tools/diagnostics/test-apple-atc-pipe.py +++ b/tools/diagnostics/test-apple-atc-pipe.py @@ -250,12 +250,12 @@ def function(name): initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; stuck_ack = true; - atcphy_park_pipehandler(&p); + assert(atcphy_park_pipehandler(&p) == -ETIMEDOUT); assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 0); stuck_ack = false; - atcphy_park_pipehandler(&p); + assert(atcphy_park_pipehandler(&p) == 0); assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 3); - atcphy_park_pipehandler(&p); + assert(atcphy_park_pipehandler(&p) == 0); assert(mux_writes == 3); initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); p.fixed_usb2 = p.host_active = true; diff --git a/tools/diagnostics/test-apple-atc-unwind.py b/tools/diagnostics/test-apple-atc-unwind.py index 0494b16fb3bc7..c5807546b9a70 100644 --- a/tools/diagnostics/test-apple-atc-unwind.py +++ b/tools/diagnostics/test-apple-atc-unwind.py @@ -92,8 +92,8 @@ def function(source, name): { (void)p; (void)mode; } static void atc_tunnel_stop_t8103(struct apple_atcphy *p) { (void)p; } static void atc_tunnel_restore(struct apple_atcphy *p) { (void)p; } -static void atcphy_park_pipehandler(struct apple_atcphy *p) -{ p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; } +static int atcphy_park_pipehandler(struct apple_atcphy *p) +{ p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; return 0; } static int atcphy_configure_pipehandler(struct apple_atcphy *p, bool host) { (void)host; p->pipe_state = atcphy_modes[p->mode].pipehandler_state; return 0; } ''') From 7588266c274bd32b4d5838360d731b4099d9c63e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 13:59:49 +1100 Subject: [PATCH 03/11] phy: apple: retain failed USB3 power-off cleanup After parking succeeds, USB3 power-off still ignores mode shutdown errors and clears host state before cleanup completes. DP teardown or domain sleep can therefore fail while the callback reports success. An earlier failed transition can also leave OFF recorded with dirty resources, which the callback skips entirely. Propagate mode shutdown errors, clear host state only after success and retry cleanup when an OFF mode is dirty. Keep the cleanup ownership and training guards in the existing mode configuration functions. Extend the production-callback host diagnostic with DP stop, dirty OFF and both domain-sleep failures. Inject all 54 cleanup poll phases across ordinary and fixed-hub ports, checking retained obligations, blocked DP training and successful retry. The parent fails this regression group; the corrected callback and W=1 ARM64 ATC object pass. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 8 ++- tools/diagnostics/test-apple-atc-parking.py | 76 ++++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index e97ce77fe5f31..2b4662e234fb9 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -2877,11 +2877,13 @@ static int atcphy_usb3_power_off(struct phy *phy) ret = atcphy_park_pipehandler(atcphy); if (ret) return ret; + if (atcphy->mode != APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) { + ret = atcphy_configure(atcphy, APPLE_ATCPHY_MODE_OFF); + if (ret) + return ret; + } atcphy->host_active = false; - if (atcphy->mode != APPLE_ATCPHY_MODE_OFF) - atcphy_configure(atcphy, APPLE_ATCPHY_MODE_OFF); - return 0; } diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py index e16841cdc73d4..0af77ee5deee1 100644 --- a/tools/diagnostics/test-apple-atc-parking.py +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -336,12 +336,86 @@ def function(source, name): } printf("PASS: %u actual reset parking/retry scenarios retain DP ownership\n", cases); } +static void power_cleanup(void) +{ + struct apple_atcphy p; + unsigned int phases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + initial(&p, fixed, false, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + struct phy phy = { &p }; + fail_poll = 1; /* The first DP stop ACK cannot complete. */ + int ret = atcphy_usb3_power_off(&phy); + observed("power-off DP cleanup failure", ret); + assert(ret == -ETIMEDOUT && p.mode_dirty && p.host_active); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.dp_owned_pmas == 3); + assert(p.aux_power_requested && usb2_writes == 0 && reset_writes == 0); + struct phy_configure_opts_dp training = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + unsigned int before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(!p.mode_dirty && !p.host_active && !p.dp_owned_pmas && !p.aux_power_requested); + /* A failed initial transition can leave OFF recorded with dirty resources. */ + initial(&p, fixed, true, false); + p.mode = APPLE_ATCPHY_MODE_OFF; + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + fail_poll = 1; + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode_dirty && p.host_active && p.dp_owned_pmas == 3); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + for (unsigned int fault = 1; fault <= 2; fault++) { + initial(&p, fixed, false, false); + p.mode = APPLE_ATCPHY_MODE_USB2; + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + p.dp_owned_pmas = p.dp_idle_pmas = 0; + p.aux_power_requested = false; + fail_poll = fault; /* BIG/SMALL sleep ACK. */ + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && p.mode_dirty && p.host_active); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0 && !p.mode_dirty && !p.host_active); + } + /* Inject every poll in DP stop, pending PMA idle, AUX and domain sleep. */ + initial(&p, fixed, true, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + assert(atcphy_usb3_power_off(&phy) == 0); + unsigned int total = polls; + for (unsigned int fault = 1; fault <= total; fault++) { + initial(&p, fixed, true, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + fail_poll = fault; + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty && p.host_active); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(usb2_writes == 0 && reset_writes == 0); + unsigned int owned = p.dp_owned_pmas, idle = p.dp_idle_pmas; + bool aux = p.aux_power_requested; + unsigned int before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(p.dp_owned_pmas == owned && p.dp_idle_pmas == idle); + assert(p.aux_power_requested == aux); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + phases++; + } + } + printf("PASS: %u cleanup poll faults retain retry/ownership; dirty OFF retries cleanup\n", phases); +} int main(int argc, char **argv) { assert(argc == 2); if (!strcmp(argv[1], "mux")) mux_parking(); else if (!strcmp(argv[1], "power")) power_parking(); else if (!strcmp(argv[1], "reset")) reset_parking(); + else if (!strcmp(argv[1], "cleanup")) power_cleanup(); else assert(false); return 0; } @@ -355,7 +429,7 @@ def function(source, name): '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) failed = False - for case in ('mux', 'power', 'reset'): + for case in ('mux', 'power', 'reset', 'cleanup'): result = subprocess.run([str(path / 'test'), case]) if result.returncode: print('FAIL:', case, 'exit', result.returncode, flush=True) From 40e5a1e56f372592dc7c5562f7d2010752c7ce3f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 14:36:36 +1100 Subject: [PATCH 04/11] usb: apple: retry incomplete ATC teardown Retain each core-owned PHY power reference until its provider accepts shutdown. Apple glue records the remaining releases and reset, keeps the domain protected, and drains those obligations before another start. Host/gadget teardown, completed PHY releases and core exit run once. Expose small core teardown/removal finishers so Apple can own the power release sequence without a second generic decrement. Existing DWC3 callers retain their original sequence. Restore the saved Type-C mode from USB2 init when delayed shutdown completed after the last mux event. Mark legacy ATC transitions dirty before changing the domain and block PIPE/DP setup until the transition is restored, as on J700. Record uncompleted probe/removal cleanup rather than hiding it. Add independently authored production-function ASan/UBSan diagnostics. The power-reference, pending-reset and legacy-abort controls fail on 7044af61e9e9 and pass with this change. The 48 shutdown/restart and 36 pending-failure restart permutations, partial PHY releases, startup failures, root-hub references and normal generic teardown pass. Existing ATC diagnostics and ARM64 LLVM W=1 ATC/DWC3 objects pass. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 23 +- drivers/usb/dwc3/core.c | 31 +- drivers/usb/dwc3/dwc3-apple.c | 114 +++++-- drivers/usb/dwc3/glue.h | 20 ++ tools/diagnostics/apple-dwc3-teardown/cases.c | 304 ++++++++++++++++++ tools/diagnostics/apple-dwc3-teardown/mocks.c | 121 +++++++ .../diagnostics/apple-dwc3-teardown/prefix.c | 53 +++ tools/diagnostics/test-apple-dwc3-teardown.py | 102 ++++++ 8 files changed, 724 insertions(+), 44 deletions(-) create mode 100644 tools/diagnostics/apple-dwc3-teardown/cases.c create mode 100644 tools/diagnostics/apple-dwc3-teardown/mocks.c create mode 100644 tools/diagnostics/apple-dwc3-teardown/prefix.c create mode 100644 tools/diagnostics/test-apple-dwc3-teardown.py diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 2b4662e234fb9..80d8e26489b2d 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -654,7 +654,7 @@ struct atcphy_hw { * @ss_tunables: The complete SuperSpeed tunable set was supplied * @dp_tunables: The complete T8140 DisplayPort bundle was supplied * @aux_power_requested: AUX power-up began and needs matching shutdown - * @mode_dirty: J700 mode transition needs cleanup before it can be retried + * @mode_dirty: Mode transition needs cleanup before it can be retried * @dp_idle_pmas: J700 PMAs with an incomplete routing idle sequence * @dp_owned_pmas: J700 PMAs requiring main-link cleanup * @dp_spread_spectrum: J700 main-link spread-spectrum state @@ -1962,6 +1962,9 @@ static int atcphy_dp_configure(struct apple_atcphy *atcphy, enum atcphy_dp_link_ u32 reg; guard(mutex)(&atcphy->lock); + /* A failed transition may have asserted the PHY resets already. */ + if (atcphy->mode_dirty) + return -EBUSY; /* the AUSPLL is in use as the Thunderbolt DP tunnel pixel clock */ if (atcphy->tunnel_clock_on || atcphy->tunnel_saved) return -EBUSY; @@ -2714,8 +2717,7 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) return ret; } /* Keep failed transitions retryable even when the requested mode is unchanged. */ - if (atcphy->hw->has_t8140_dp) - atcphy->mode_dirty = true; + atcphy->mode_dirty = true; /* the mode change below may power the PHY down: stop the tunnel clock first */ atc_tunnel_stop_t8103(atcphy); atc_tunnel_restore(atcphy); @@ -2839,11 +2841,15 @@ static int atcphy_usb2_init(struct phy *phy) { struct apple_atcphy *atcphy = phy_get_drvdata(phy); - if (!atcphy->fixed_usb2) - return 0; - guard(mutex)(&atcphy->lock); + /* A delayed shutdown may have completed after the last Type-C event. */ + if (!atcphy->fixed_usb2) { + if (atcphy->mode == APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) + return atcphy_configure(atcphy, atcphy->typec_mode); + return 0; + } + /* * dwc3 initialises its PHYs after releasing its reset, and asserting that * reset powered the USB2 PHY off. A port behind a fixed hub gets no Type-C @@ -2853,7 +2859,7 @@ static int atcphy_usb2_init(struct phy *phy) * the Type-C mux last asked for, USB2 until a cable event says otherwise. */ set32(atcphy->regs.usb2phy + USB2PHY_SIG, USB2PHY_SIG_HOST); - if (atcphy->mode == APPLE_ATCPHY_MODE_OFF) + if (atcphy->mode == APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) return atcphy_configure(atcphy, atcphy->typec_mode); atcphy_usb2_power_on(atcphy); @@ -2910,6 +2916,9 @@ static int atcphy_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode * PIPE handler once. Nothing needs to be done either when the PIPE is already routed to * the backend the current mode uses, which includes the dummy backend of the USB2 modes. */ + /* A failed lane/domain transition must be restored before routing PIPE. */ + if (atcphy->mode_dirty) + return -EBUSY; if (atcphy->pipe_state == atcphy_modes[atcphy->mode].pipehandler_state) return 0; diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index 6f3c00b089371..c2bbcbcf3797a 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -995,14 +995,20 @@ static void dwc3_clk_disable(struct dwc3 *dwc) clk_disable_unprepare(dwc->bus_clk); } -void dwc3_core_exit(struct dwc3 *dwc) +void dwc3_core_exit_after_phy_power_off(struct dwc3 *dwc) { - dwc3_event_buffers_cleanup(dwc); - dwc3_phy_power_off(dwc); dwc3_phy_exit(dwc); dwc3_clk_disable(dwc); reset_control_assert(dwc->reset); } +EXPORT_SYMBOL_GPL(dwc3_core_exit_after_phy_power_off); + +void dwc3_core_exit(struct dwc3 *dwc) +{ + dwc3_event_buffers_cleanup(dwc); + dwc3_phy_power_off(dwc); + dwc3_core_exit_after_phy_power_off(dwc); +} EXPORT_SYMBOL_GPL(dwc3_core_exit); static bool dwc3_core_is_valid(struct dwc3 *dwc) @@ -2410,14 +2416,16 @@ static int dwc3_probe(struct platform_device *pdev) return dwc3_core_probe(&probe_data); } -void dwc3_core_remove(struct dwc3 *dwc) +static void dwc3_core_remove_common(struct dwc3 *dwc, bool core_exited) { pm_runtime_get_sync(dwc->dev); - dwc3_core_exit_mode(dwc); + if (!core_exited) + dwc3_core_exit_mode(dwc); dwc3_debugfs_exit(dwc); - dwc3_core_exit(dwc); + if (!core_exited) + dwc3_core_exit(dwc); dwc3_ulpi_exit(dwc); pm_runtime_allow(dwc->dev); @@ -2438,8 +2446,19 @@ void dwc3_core_remove(struct dwc3 *dwc) power_supply_put(dwc->usb_psy); } } + +void dwc3_core_remove(struct dwc3 *dwc) +{ + dwc3_core_remove_common(dwc, false); +} EXPORT_SYMBOL_GPL(dwc3_core_remove); +void dwc3_core_remove_no_shutdown(struct dwc3 *dwc) +{ + dwc3_core_remove_common(dwc, true); +} +EXPORT_SYMBOL_GPL(dwc3_core_remove_no_shutdown); + static void dwc3_remove(struct platform_device *pdev) { dwc3_core_remove(platform_get_drvdata(pdev)); diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c index 232a4de95bb75..e1d1104db98a3 100644 --- a/drivers/usb/dwc3/dwc3-apple.c +++ b/drivers/usb/dwc3/dwc3-apple.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "dwc3-apple-state.h" #include "glue.h" @@ -96,6 +97,11 @@ * @fixed_hub: A fixed hub sits on the controller's USB2 port, the controller stays in host mode * @lock: Mutex for synchronizing access * @state: Current state of the controller, see enum dwc3_apple_state and the comment above + * @core_active: Core still owns initialized PHYs + * @core_exit_pending: Event buffers have been cleaned for this shutdown + * @reset_pending: Controller reset must complete before another start + * @usb2_power_off_pending: Core-owned USB2 references still to release + * @usb3_power_off_pending: Core-owned USB3 references still to release */ struct dwc3_apple { struct dwc3 dwc; @@ -113,6 +119,11 @@ struct dwc3_apple { struct mutex lock; enum dwc3_apple_state state; + bool core_active; + bool core_exit_pending; + bool reset_pending; + unsigned long usb2_power_off_pending; + unsigned long usb3_power_off_pending; }; #define to_dwc3_apple(d) container_of((d), struct dwc3_apple, dwc) @@ -254,6 +265,52 @@ static int dwc3_apple_core_init(struct dwc3_apple *appledwc) return ret; } +/* Keep failed releases owned by the glue, without repeating completed teardown. */ +static int dwc3_apple_core_stop(struct dwc3_apple *appledwc) +{ + struct dwc3 *dwc = &appledwc->dwc; + int i, ret; + + lockdep_assert_held(&appledwc->lock); + if (appledwc->core_active) { + if (!appledwc->core_exit_pending) { + dwc3_enable_susphy(dwc, true); + dwc3_event_buffers_cleanup(dwc); + appledwc->core_exit_pending = true; + } + for (i = 0; i < dwc->num_usb3_ports; i++) { + if (!(appledwc->usb3_power_off_pending & BIT(i))) + continue; + ret = phy_power_off(dwc->usb3_generic_phy[i]); + if (ret) + return ret; + appledwc->usb3_power_off_pending &= ~BIT(i); + } + for (i = 0; i < dwc->num_usb2_ports; i++) { + if (!(appledwc->usb2_power_off_pending & BIT(i))) + continue; + ret = phy_power_off(dwc->usb2_generic_phy[i]); + if (ret) + return ret; + appledwc->usb2_power_off_pending &= ~BIT(i); + } + usb_phy_set_suspend(dwc->usb3_phy, 1); + usb_phy_set_suspend(dwc->usb2_phy, 1); + dwc3_core_exit_after_phy_power_off(dwc); + appledwc->core_active = false; + appledwc->core_exit_pending = false; + } + if (appledwc->reset_pending) { + ret = reset_control_assert(appledwc->reset); + if (ret) + return ret; + appledwc->reset_pending = false; + } + dev_pm_syscore_device(appledwc->dev, false); + + return 0; +} + /* * Bring the core up for the given role: configure the USB2 PHY, release the reset and probe * (on the first call) or initialise the core. @@ -265,6 +322,11 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st lockdep_assert_held(&appledwc->lock); + /* A failed exit must finish before any new PHY reference is acquired. */ + ret = dwc3_apple_core_stop(appledwc); + if (ret) + return ret; + /* * The USB2 PHY on this platform must be configured for device mode before dwc3 tries to * access it. Otherwise, the new configuration will sometimes only take affect after the @@ -297,11 +359,14 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st return ret; } + appledwc->reset_pending = true; + /* Keep register access powered until both PHY shutdown and reset finish. */ + dev_pm_syscore_device(appledwc->dev, true); ret = dwc3_apple_core_init(appledwc); if (ret) { - ret_reset = reset_control_assert(appledwc->reset); + ret_reset = dwc3_apple_core_stop(appledwc); if (ret_reset) - dev_warn(appledwc->dev, "Failed to assert reset, err=%d\n", ret_reset); + dev_warn(appledwc->dev, "Failed to finish core shutdown, err=%d\n", ret_reset); return ret; } @@ -310,6 +375,9 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st * configure some unknown Apple-specific settings and then bring up xhci or gadget mode. */ dwc3_apple_setup_cio(appledwc); + appledwc->core_active = true; + appledwc->usb2_power_off_pending = BIT(appledwc->dwc.num_usb2_ports) - 1; + appledwc->usb3_power_off_pending = BIT(appledwc->dwc.num_usb3_ports) - 1; return 0; } @@ -386,51 +454,32 @@ static int dwc3_apple_init(struct dwc3_apple *appledwc, enum dwc3_apple_state st return 0; core_exit: - dwc3_core_exit(&appledwc->dwc); - ret_reset = reset_control_assert(appledwc->reset); + ret_reset = dwc3_apple_core_stop(appledwc); if (ret_reset) - dev_warn(appledwc->dev, "Failed to assert reset, err=%d\n", ret_reset); + dev_warn(appledwc->dev, "Failed to finish core shutdown, err=%d\n", ret_reset); return ret; } static int dwc3_apple_exit(struct dwc3_apple *appledwc) { - int ret = 0; - lockdep_assert_held(&appledwc->lock); switch (appledwc->state) { case DWC3_APPLE_PROBE_PENDING: case DWC3_APPLE_NO_CABLE: - /* Nothing to do if we're already off */ - return 0; + break; case DWC3_APPLE_DEVICE: - dev_pm_syscore_device(appledwc->dev, false); dwc3_gadget_exit(&appledwc->dwc); + appledwc->state = DWC3_APPLE_NO_CABLE; break; case DWC3_APPLE_HOST: - dev_pm_syscore_device(appledwc->dev, false); dwc3_host_exit(&appledwc->dwc); + appledwc->state = DWC3_APPLE_NO_CABLE; break; } - /* - * This platform requires SUSPHY to be enabled in order to properly power down the PHY - * and switch dwc3's PIPE interface back to a dummy PHY (i.e. no USB3 support and USB2 via - * a different PHY connected through ULPI). - */ - dwc3_enable_susphy(&appledwc->dwc, true); - dwc3_core_exit(&appledwc->dwc); - appledwc->state = DWC3_APPLE_NO_CABLE; - - ret = reset_control_assert(appledwc->reset); - if (ret) { - dev_err(appledwc->dev, "Failed to assert reset, err=%d\n", ret); - return ret; - } - - return 0; + return dwc3_apple_core_stop(appledwc); } static int dwc3_usb_role_switch_set(struct usb_role_switch *sw, enum usb_role role) @@ -596,9 +645,11 @@ static int dwc3_apple_probe(struct platform_device *pdev) scoped_guard(mutex, &appledwc->lock) ret = dwc3_apple_init(appledwc, DWC3_APPLE_HOST); if (ret) { - /* dwc3_apple_init() has already undone the core initialisation */ + /* A failed cleanup remains visible even when probe cannot be retried. */ + if (appledwc->core_active || appledwc->reset_pending) + dev_err(dev, "Host startup left pending PHY/reset cleanup\n"); if (appledwc->state != DWC3_APPLE_PROBE_PENDING) - dwc3_core_remove(&appledwc->dwc); + dwc3_core_remove_no_shutdown(&appledwc->dwc); usb_role_switch_unregister(appledwc->role_sw); return dev_err_probe(dev, ret, "Failed to start host mode\n"); } @@ -620,9 +671,10 @@ static void dwc3_apple_remove(struct platform_device *pdev) * dwc3_core_probe was never called and there's hence no need to call dwc3_core_remove. * dwc3_apple_exit can be called unconditionally because it checks the state itself. */ - dwc3_apple_exit(appledwc); + if (dwc3_apple_exit(appledwc)) + dev_err(appledwc->dev, "Removal left pending PHY/reset cleanup\n"); if (appledwc->state != DWC3_APPLE_PROBE_PENDING) - dwc3_core_remove(&appledwc->dwc); + dwc3_core_remove_no_shutdown(&appledwc->dwc); } static const struct of_device_id dwc3_apple_of_match[] = { diff --git a/drivers/usb/dwc3/glue.h b/drivers/usb/dwc3/glue.h index d738e1739ae01..db5f32df712bf 100644 --- a/drivers/usb/dwc3/glue.h +++ b/drivers/usb/dwc3/glue.h @@ -108,6 +108,26 @@ int dwc3_core_init(struct dwc3 *dwc); */ void dwc3_core_exit(struct dwc3 *dwc); +/** + * dwc3_core_exit_after_phy_power_off - Finish a glue-owned core shutdown + * @dwc: Pointer to DWC3 controller context + * + * The glue must first stop host/gadget operation, clean up event buffers, + * release every core-owned generic PHY power reference and suspend legacy + * USB PHYs. This finishes PHY exit, clock disable and reset exactly once. + */ +void dwc3_core_exit_after_phy_power_off(struct dwc3 *dwc); + +/** + * dwc3_core_remove_no_shutdown - Release resources without a second shutdown + * @dwc: Pointer to DWC3 controller context + * + * The glue owns host/gadget and core shutdown, including reporting any + * failure to finish it. This releases software resources without repeating + * those operations or discarding another generic PHY reference. + */ +void dwc3_core_remove_no_shutdown(struct dwc3 *dwc); + /** * dwc3_host_init - Initialize host mode operation * @dwc: Pointer to DWC3 controller context diff --git a/tools/diagnostics/apple-dwc3-teardown/cases.c b/tools/diagnostics/apple-dwc3-teardown/cases.c new file mode 100644 index 0000000000000..db4f198307362 --- /dev/null +++ b/tools/diagnostics/apple-dwc3-teardown/cases.c @@ -0,0 +1,304 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +static struct atcphy_hw hardware; +static int counted_off(struct phy *p) { callback_calls++; return atcphy_usb3_power_off(p); } +static int other_off(struct phy *p) { (void)p; return other_off_error; } +static const struct phy_ops usb3_ops = { .power_off = counted_off }; +static const struct phy_ops usb2_ops = { .init = atcphy_usb2_init, .power_off = other_off }; +static const struct phy_ops plain_ops = { .power_off = other_off }; +static void dwc3_host_exit(struct dwc3 *d) +{ + host_exits++; + if (root_refs) { + assert(phy_power_off(d->usb3_generic_phy[0]) == 0); + assert(phy_exit(d->usb3_generic_phy[0]) == 0); + root_refs = 0; + } +} +/* Boundary mocks acquire references through the production generic PHY API. */ +static int dwc3_core_init(struct dwc3 *d) +{ + starts++; + if (core_init_error) return core_init_error; + for (int i = 0; i < d->num_usb2_ports; i++) assert(phy_init(d->usb2_generic_phy[i]) == 0); + for (int i = 0; i < d->num_usb3_ports; i++) assert(phy_init(d->usb3_generic_phy[i]) == 0); + return dwc3_phy_power_on(d); +} +static int dwc3_apple_core_probe(struct dwc3_apple *a) +{ + int ret = dwc3_core_init(&a->dwc); + if (!ret) a->state = DWC3_APPLE_NO_CABLE; + return ret; +} +static void dwc3_apple_set_role(struct dwc3_apple *a, enum dwc3_apple_state s) +{ assert(atcphy_usb3_set_mode(a->usb3_phy, s == DWC3_APPLE_HOST ? PHY_MODE_USB_HOST : PHY_MODE_USB_DEVICE, 0) == 0); } +struct fixture { struct apple_atcphy atc; struct phy p2, p3, extra; struct dwc3_apple apple; }; +static void initial(struct fixture *f, unsigned int gen, bool fixed, bool pm) +{ + memset(f, 0, sizeof(*f)); + memset(core_regs, 0, sizeof(core_regs)); + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(usb2_regs, 0, sizeof(usb2_regs)); + hardware = (struct atcphy_hw){ + .gen = gen == 0 ? ATCPHY_GENERATION_T8103 : ATCPHY_GENERATION_T8122, + .has_t8140_dp = gen == 2, .has_usb4 = gen != 2, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, + }; + f->atc = (struct apple_atcphy){ .hw = &hardware, + .regs.core = (unsigned char *)core_regs, .regs.lpdptx = (unsigned char *)core_regs, + .regs.pipehandler = (unsigned char *)pipe_regs, .regs.usb2phy = (unsigned char *)usb2_regs, + .mode = APPLE_ATCPHY_MODE_USB3, .typec_mode = APPLE_ATCPHY_MODE_USB3, + .pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3, + .host_active = true, .fixed_usb2 = fixed, .ss_tunables = true, .dp_tunables = true, + }; + f->p3 = (struct phy){ .data = &f->atc, .ops = &usb3_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->p2 = (struct phy){ .data = &f->atc, .ops = &usb2_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->extra = (struct phy){ .ops = &plain_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->apple = (struct dwc3_apple){ .state = DWC3_APPLE_HOST, .reset = &f->atc.rcdev, + .usb2_phy = &f->p2, .usb3_phy = &f->p3, .core_active = true, .reset_pending = true, + .usb3_power_off_pending = 1, .usb2_power_off_pending = 1, + .dwc = { .num_usb3_ports = 1, .num_usb2_ports = 1, + .usb3_generic_phy = { &f->p3 }, .usb2_generic_phy = { &f->p2 } }, + }; + core_regs[ATCPHY_POWER_CTRL / 4] = ATCPHY_POWER_PHY_RESET_N | ATCPHY_POWER_SLEEP_BIG | + ATCPHY_POWER_SLEEP_SMALL | ATCPHY_POWER_APB_RESET_N; + core_regs[ATCPHY_POWER_STAT / 4] = core_regs[ATCPHY_POWER_CTRL / 4]; + core_regs[AUS_COMMON_DIG_RCAL1 / 4] = AUS_COMMON_DIG_RCAL1_ALL_CODES_DONE; + core_regs[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + pipe_regs[PIPEHANDLER_AON_GEN / 4] = PIPEHANDLER_AON_GEN_DWC3_RESET_N; + polls = fail_poll = reset_calls = reset_writes = core_writes = warnings = 0; + generic_warnings = 0; + host_exits = clocks_disabled = callback_calls = gadget_exits = event_cleanups = 0; + legacy_suspends = core_mode_exits = free_buffers = starts = deasserts = setup_cio = root_refs = 0; + host_init_error = gadget_init_error = core_init_error = reset_error = other_off_error = 0; + init_cleanup_fault = false; + syscore = true; + stuck_ack = false; +} +static void recover(void) { fail_poll = 0; stuck_ack = false; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = 0; } +static int exit_apple(struct fixture *f) +{ mutex_lock(&f->apple.lock); int ret = dwc3_apple_exit(&f->apple); mutex_unlock(&f->apple.lock); return ret; } +static int role(struct fixture *f, enum usb_role r) +{ struct usb_role_switch sw = { &f->apple }; return dwc3_usb_role_switch_set(&sw, r); } +static void clean(struct fixture *f) +{ + assert(!f->p3.power_count && !f->p2.power_count && !f->p3.init_count && !f->p2.init_count); + assert(!f->p3.pm_refs && !f->p2.pm_refs && !f->apple.core_active && !f->apple.reset_pending); + assert(!f->apple.usb3_power_off_pending && !f->apple.usb2_power_off_pending); + assert(!f->atc.host_active && !f->atc.mode_dirty && f->atc.mode == APPLE_ATCPHY_MODE_OFF); + assert(!syscore); + assert(!f->atc.lock && !f->p3.mutex && !f->p2.mutex && !f->apple.lock); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); +} +static void cycles(void) +{ + unsigned int cases = 0; + for (unsigned int gen = 0; gen < 3; gen++) + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int pm = 0; pm < 2; pm++) + for (unsigned int fault = 0; fault < 4; fault++) { + struct fixture f; initial(&f, gen, fixed, pm); + if (fault == 1) { + stuck_ack = true; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + } else if (fault > 1) { + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = fault - 1; + } + int ret = exit_apple(&f); + printf("CYCLE gen=%u fixed=%u pm=%u fault=%u exit=%d refs=%d init=%d\n", + gen, fixed, pm, fault, ret, f.p3.power_count, f.p3.init_count); fflush(stdout); + if (fault) { + assert(ret == -ETIMEDOUT && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.apple.core_active && clocks_disabled == 0 && event_cleanups == 1 && host_exits == 1); + assert(syscore); + if (fault > 1) assert(f.atc.mode_dirty); + if (fault == 1) { + assert(role(&f, USB_ROLE_HOST) == -ETIMEDOUT && starts == 0 && deasserts == 0); + assert(host_exits == 1 && event_cleanups == 1 && f.p3.power_count == 1); + } + recover(); + assert(exit_apple(&f) == 0); + } else assert(ret == 0); + clean(&f); + assert(host_exits == 1 && clocks_disabled == 1 && event_cleanups == 1); + assert(exit_apple(&f) == 0 && host_exits == 1 && clocks_disabled == 1); + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.atc.mode == APPLE_ATCPHY_MODE_USB3 && !f.atc.mode_dirty); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(role(&f, USB_ROLE_NONE) == 0); + clean(&f); + assert(!generic_warnings); + if (!fault) assert(!warnings); + cases++; + } + printf("PASS: %u normal/parking/domain shutdown and restart scenarios\n", cases); +} +static void pending_reset(void) +{ + for (unsigned int gen = 0; gen < 3; gen++) { + struct fixture f; initial(&f, gen, false, true); + reset_error = -ETIMEDOUT; + assert(exit_apple(&f) == -ETIMEDOUT); + assert(!f.apple.core_active && f.apple.reset_pending && !f.p3.power_count && !f.p3.init_count); + assert(syscore); + assert(role(&f, USB_ROLE_HOST) == -ETIMEDOUT && !starts && !deasserts); + assert(host_exits == 1 && clocks_disabled == 1 && event_cleanups == 1 && callback_calls == 1); + reset_error = 0; + assert(exit_apple(&f) == 0); + clean(&f); + assert(host_exits == 1 && clocks_disabled == 1 && callback_calls == 1); + assert(reset_calls == 3); + } + puts("PASS: pending reset retry does not repeat host, PHY or core teardown"); +} +static void partial_release(void) +{ + struct fixture f; initial(&f, 1, false, true); + other_off_error = -EIO; + assert(exit_apple(&f) == -EIO); + assert(!f.p3.power_count && f.p3.init_count == 1 && f.p2.power_count == 1); + assert(!f.apple.usb3_power_off_pending && f.apple.usb2_power_off_pending == 1); + assert(callback_calls == 1 && !clocks_disabled); + other_off_error = 0; + assert(exit_apple(&f) == 0); clean(&f); + assert(callback_calls == 1 && event_cleanups == 1 && clocks_disabled == 1); + initial(&f, 1, false, false); + f.apple.dwc.num_usb3_ports = 2; f.apple.dwc.usb3_generic_phy[1] = &f.extra; + f.apple.usb3_power_off_pending = 3; + other_off_error = -EIO; + assert(exit_apple(&f) == -EIO && !f.p3.power_count && f.extra.power_count == 1); + other_off_error = 0; + assert(exit_apple(&f) == 0 && !f.extra.power_count && !f.extra.init_count); + clean(&f); assert(callback_calls == 1); + puts("PASS: successful USB3 references are not released twice after another PHY fails"); +} +static void init_failures(void) +{ + for (unsigned int device = 0; device < 2; device++) { + struct fixture f; initial(&f, 2, false, true); + assert(exit_apple(&f) == 0); + if (device) gadget_init_error = -EIO; else host_init_error = -EIO; + reset_error = -ETIMEDOUT; + assert(role(&f, device ? USB_ROLE_DEVICE : USB_ROLE_HOST) == -EIO); + assert(f.apple.reset_pending && !f.apple.core_active && !f.p3.power_count); + assert(syscore); + host_init_error = gadget_init_error = reset_error = 0; + assert(role(&f, device ? USB_ROLE_DEVICE : USB_ROLE_HOST) == 0); + assert(f.p3.power_count == 1 && f.p3.init_count == 1); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + } + struct fixture f; initial(&f, 1, false, false); assert(exit_apple(&f) == 0); + core_init_error = -EIO; reset_error = -ETIMEDOUT; + assert(role(&f, USB_ROLE_HOST) == -EIO && f.apple.reset_pending && !f.apple.core_active); + core_init_error = reset_error = 0; + assert(role(&f, USB_ROLE_HOST) == 0); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + initial(&f, 1, false, true); assert(exit_apple(&f) == 0); + host_init_error = -EIO; init_cleanup_fault = true; + assert(role(&f, USB_ROLE_HOST) == -EIO && f.apple.core_active); + assert(syscore); + assert(f.p3.power_count == 1 && f.p3.init_count == 1); + recover(); host_init_error = 0; init_cleanup_fault = false; + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + puts("PASS: host/gadget/core startup failure reset obligations drain before retry"); +} +static void pending_restart(void) +{ + unsigned int cases = 0; + for (unsigned int gen = 0; gen < 3; gen++) + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int pm = 0; pm < 2; pm++) + for (unsigned int fault = 1; fault < 4; fault++) { + struct fixture f; initial(&f, gen, fixed, pm); + if (fault == 1) { + stuck_ack = true; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + } else { f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = fault - 1; } + assert(exit_apple(&f) == -ETIMEDOUT); + recover(); + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.atc.mode == APPLE_ATCPHY_MODE_USB3 && !f.atc.mode_dirty); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(!generic_warnings); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); cases++; + } + struct fixture f; initial(&f, 2, true, true); f.apple.fixed_hub = true; + assert(role(&f, USB_ROLE_NONE) == 0 && f.apple.state == DWC3_APPLE_HOST && f.p3.power_count == 1); + assert(exit_apple(&f) == 0); + mutex_lock(&f.apple.lock); + assert(dwc3_apple_init(&f.apple, DWC3_APPLE_HOST) == 0); + mutex_unlock(&f.apple.lock); + assert(f.p3.power_count == 1 && exit_apple(&f) == 0); clean(&f); + printf("PASS: %u pending-failure to HOST transitions and actual fixed-hub role no-op\n", cases); +} +static void negative_power(void) +{ + struct fixture f; initial(&f, 0, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(exit_apple(&f) == -ETIMEDOUT && f.p3.power_count == 1 && f.p3.init_count == 1); + recover(); assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1); +} +static void negative_reset(void) +{ + struct fixture f; initial(&f, 0, false, false); reset_error = -ETIMEDOUT; + assert(exit_apple(&f) == -ETIMEDOUT && reset_calls == 1); + reset_error = 0; assert(exit_apple(&f) == 0 && reset_calls == 2); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); +} +static void negative_legacy(void) +{ + struct fixture f; initial(&f, 0, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(phy_power_off(&f.p3) == -ETIMEDOUT && f.atc.mode_dirty); + recover(); + struct typec_mux_dev mux = { &f.atc }; + struct typec_mux_state state = { .mode = TYPEC_STATE_USB }; + assert(atcphy_mux_set(&mux, &state) == 0); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); +} +static void legacy_restore(void) +{ + for (unsigned int gen = 0; gen < 3; gen++) { + struct fixture f; initial(&f, gen, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(phy_power_off(&f.p3) == -ETIMEDOUT && f.atc.mode_dirty); + unsigned int before = core_writes; + assert(atcphy_usb3_set_mode(&f.p3, PHY_MODE_USB_HOST, 0) == -EBUSY && core_writes == before); + if (gen < 2) { + f.atc.dp_link_rate = ATCPHY_DP_LINK_RATE_RBR; + assert(atcphy_dp_configure(&f.atc, ATCPHY_DP_LINK_RATE_RBR) == -EBUSY && core_writes == before); + } + recover(); + struct typec_mux_dev mux = { &f.atc }; + struct typec_mux_state state = { .mode = TYPEC_STATE_USB }; + assert(atcphy_mux_set(&mux, &state) == 0 && !f.atc.mode_dirty && core_writes > before); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(phy_power_off(&f.p3) == 0 && !f.p3.power_count); + } + puts("PASS: M1/M2/J700 aborted shutdown blocks PIPE and restores saved lane mode"); +} +static void removal_and_generic(void) +{ + struct fixture f; initial(&f, 0, false, false); + root_refs = 1; assert(phy_init(&f.p3) == 0 && phy_power_on(&f.p3) == 0); + assert(exit_apple(&f) == 0); clean(&f); + unsigned int callbacks = callback_calls, clocks = clocks_disabled, events = event_cleanups; + dwc3_core_remove_no_shutdown(&f.apple.dwc); + assert(callbacks == callback_calls && clocks == clocks_disabled && events == event_cleanups); + assert(!core_mode_exits && free_buffers == 1 && !f.p3.power_count && !f.p3.init_count); + initial(&f, 1, false, false); + dwc3_core_exit(&f.apple.dwc); + assert(!f.p3.power_count && !f.p3.init_count && !f.p2.power_count && !f.p2.init_count); + assert(event_cleanups == 1 && clocks_disabled == 1 && callback_calls == 1); + puts("PASS: root-hub references, already-exited removal and ordinary generic core exit"); +} +int main(int argc, char **argv) +{ + if (argc == 2 && !strcmp(argv[1], "power")) { negative_power(); return 0; } + if (argc == 2 && !strcmp(argv[1], "reset")) { negative_reset(); return 0; } + if (argc == 2 && !strcmp(argv[1], "legacy")) { negative_legacy(); return 0; } + cycles(); pending_restart(); pending_reset(); partial_release(); init_failures(); legacy_restore(); removal_and_generic(); + puts("PASS: Apple teardown actual-function qualification"); return 0; +} diff --git a/tools/diagnostics/apple-dwc3-teardown/mocks.c b/tools/diagnostics/apple-dwc3-teardown/mocks.c new file mode 100644 index 0000000000000..8edd8783c7f29 --- /dev/null +++ b/tools/diagnostics/apple-dwc3-teardown/mocks.c @@ -0,0 +1,121 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *lpdptx, *pipehandler, *usb2phy; } regs; + struct reset_controller_dev rcdev; + void *dev; + int lock, dp_link_rate; + bool swap_lanes, tunnel_clock_on, tunnel_saved; + bool fixed_usb2, host_active, mode_dirty, ss_tunables, dp_tunables, aux_power_requested; + unsigned int dp_owned_pmas; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +struct phy; +struct phy_ops { int (*init)(struct phy *); int (*exit)(struct phy *); + int (*power_on)(struct phy *); int (*power_off)(struct phy *); }; +struct phy { struct apple_atcphy *data; const struct phy_ops *ops; + int mutex, power_count, init_count, pm_refs, pwr, dev; bool pm_enabled; }; +#define phy_get_drvdata(p) ((p)->data) +static int phy_pm_runtime_get_sync(struct phy *p) +{ if (!p->pm_enabled) return -ENOTSUPP; p->pm_refs++; return 0; } +static void phy_pm_runtime_put(struct phy *p) { if (p->pm_enabled) p->pm_refs--; } +static void phy_pm_runtime_put_sync(struct phy *p) { phy_pm_runtime_put(p); } +static int regulator_enable(int p) { assert(!p); return 0; } +static void regulator_disable(int p) { assert(!p); } +struct dwc3 { struct phy *usb2_generic_phy[3], *usb3_generic_phy[3]; + int num_usb2_ports, num_usb3_ports, usb2_phy, usb3_phy; + struct reset_controller_dev *reset; + int dev, usb_psy, vbus_draw_work; }; +struct dwc3_apple { struct dwc3 dwc; enum dwc3_apple_state state; + struct reset_controller_dev *reset; int lock; void *dev; + struct phy *usb2_phy, *usb3_phy; + bool fixed_hub, core_active, core_exit_pending, reset_pending; + unsigned long usb2_power_off_pending, usb3_power_off_pending; }; +static unsigned int polls, fail_poll, reset_calls, reset_writes, core_writes; +static unsigned int host_exits, clocks_disabled, callback_calls; +static unsigned int gadget_exits, event_cleanups, legacy_suspends, core_mode_exits, free_buffers; +static unsigned int starts, deasserts, setup_cio, root_refs; +static int host_init_error, gadget_init_error, core_init_error, reset_error, other_off_error; +static bool init_cleanup_fault; +static bool syscore; +static bool stuck_ack; +static u32 core_regs[0x30000 / 4], pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; +static u32 readl(void *p) { return *(u32 *)p; } +static void mask32(void *p, u32 m, u32 v) +{ + *(u32 *)p = (*(u32 *)p & ~m) | v; + if (p == &pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)p & PIPEHANDLER_LOCK_EN; + if (p == &pipe_regs[PIPEHANDLER_AON_GEN / 4]) reset_writes++; + if ((uintptr_t)p >= (uintptr_t)core_regs && + (uintptr_t)p < (uintptr_t)core_regs + sizeof(core_regs)) core_writes++; +} +static void writel(u32 v, void *p) { mask32(p, ~0U, v); } +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +static void core_mask32(struct apple_atcphy *p, u32 o, u32 m, u32 v) +{ + assert(o < sizeof(core_regs)); + mask32(p->regs.core + o, m, v); + if (o == ATCPHY_POWER_CTRL) core_regs[ATCPHY_POWER_STAT / 4] = core_regs[o / 4]; +} +static void core_set32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, 0, v); } +static void core_clear32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, v, 0); } +#define readl_poll_timeout(a,v,c,i,t) \ + ({ (v) = readl(a); ++polls; polls == fail_poll || !(c) ? -ETIMEDOUT : 0; }) +#define readl_poll_timeout_atomic readl_poll_timeout +/* These dependencies are outside the cross-layer test; no DP ownership is seeded. */ +static int j700_dp_stop(struct apple_atcphy *p) { assert(!p->dp_owned_pmas); return 0; } +static int j700_dp_idle_pending(struct apple_atcphy *p) { (void)p; return 0; } +static void atc_tunnel_stop_t8103(struct apple_atcphy *p) { (void)p; } +static void atc_tunnel_restore(struct apple_atcphy *p) { (void)p; } +static void atcphy_usb2_power_on(struct apple_atcphy *p) { (void)p; } +static void atcphy_apply_tunables(struct apple_atcphy *p, enum atcphy_mode m) { (void)p; (void)m; } +static int atcphy_configure_lanes(struct apple_atcphy *p, enum atcphy_mode m) { (void)p; (void)m; return 0; } +static int atcphy_enable_dp_aux(struct apple_atcphy *p) { (void)p; return 0; } +static int atcphy_auspll_apb_command(struct apple_atcphy *p, int command) +{ (void)p; (void)command; return 0; } +static int atcphy_dp_configure_lane(struct apple_atcphy *p, int lane, + const struct atcphy_dp_link_rate_configuration *config) +{ (void)p; (void)lane; (void)config; return 0; } +static void usb_phy_set_suspend(int p, int s) { (void)p; (void)s; legacy_suspends++; } +static void usb_phy_shutdown(int p) { (void)p; } +static void dwc3_event_buffers_cleanup(struct dwc3 *d) { (void)d; event_cleanups++; } +static void dwc3_clk_disable(struct dwc3 *d) { (void)d; clocks_disabled++; } +static void dwc3_host_exit(struct dwc3 *d); +static void dwc3_gadget_exit(struct dwc3 *d) { (void)d; gadget_exits++; } +static void dev_pm_syscore_device(void *d, bool s) { (void)d; syscore = s; } +static void dwc3_enable_susphy(struct dwc3 *d, bool s) { (void)d; (void)s; } +static int atcphy_dwc3_reset_assert(struct reset_controller_dev *, unsigned long); +static int reset_control_assert(struct reset_controller_dev *r) +{ if (!r) return 0; reset_calls++; if (reset_error) return reset_error; + return atcphy_dwc3_reset_assert(r, 0); } +static int atcphy_dwc3_reset_deassert(struct reset_controller_dev *, unsigned long); +static int reset_control_deassert(struct reset_controller_dev *r) +{ deasserts++; return atcphy_dwc3_reset_deassert(r, 0); } +static int phy_set_mode(struct phy *p, int m) { (void)p; (void)m; return 0; } +static int dwc3_apple_core_probe(struct dwc3_apple *a); +static int dwc3_core_init(struct dwc3 *d); +static void dwc3_apple_setup_cio(struct dwc3_apple *a) { (void)a; setup_cio++; } +static int dwc3_apple_usb2_mode_before_reset(enum dwc3_apple_state s, enum phy_mode *m) +{ if (s != DWC3_APPLE_HOST && s != DWC3_APPLE_DEVICE) return -EINVAL; + *m = s == DWC3_APPLE_HOST ? PHY_MODE_INVALID : PHY_MODE_USB_DEVICE; return 0; } +static void dwc3_apple_set_role(struct dwc3_apple *a, enum dwc3_apple_state s); +static int dwc3_host_init(struct dwc3 *d) +{ (void)d; if (init_cleanup_fault) { stuck_ack = true; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; } return host_init_error; } +static int dwc3_gadget_init(struct dwc3 *d) { (void)d; return gadget_init_error; } +#define pm_runtime_get_sync(...) ((void)0) +#define pm_runtime_allow(...) ((void)0) +#define pm_runtime_disable(...) ((void)0) +#define pm_runtime_dont_use_autosuspend(...) ((void)0) +#define pm_runtime_put_noidle(...) ((void)0) +#define pm_runtime_set_suspended(...) ((void)0) +#define dev_set_drvdata(...) ((void)0) +#define cancel_work_sync(...) ((void)0) +#define power_supply_put(...) ((void)0) +static void dwc3_core_exit_mode(struct dwc3 *d) { (void)d; core_mode_exits++; } +static void dwc3_debugfs_exit(struct dwc3 *d) { (void)d; } +static void dwc3_ulpi_exit(struct dwc3 *d) { (void)d; } +static void dwc3_free_event_buffers(struct dwc3 *d) { (void)d; free_buffers++; } diff --git a/tools/diagnostics/apple-dwc3-teardown/prefix.c b/tools/diagnostics/apple-dwc3-teardown/prefix.c new file mode 100644 index 0000000000000..9a28d7ccb8964 --- /dev/null +++ b/tools/diagnostics/apple-dwc3-teardown/prefix.c @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint8_t u8; +#define ENOTSUPP 524 +#define BIT(n) (1U << (n)) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +#define FIELD_PREP(m, v) (((u32)(v) << __builtin_ctz(m)) & (m)) +#define FIELD_GET(m, v) (((v) & (m)) >> __builtin_ctz(m)) +#define field_prep FIELD_PREP +#define __iomem +#define udelay(n) ((void)(n)) +#define usleep_range(a,b) ((void)(a), (void)(b)) +#define fallthrough __attribute__((fallthrough)) +static unsigned int warnings, generic_warnings; +static void warning(const char *message) +{ warnings++; if (strstr(message, "phy_power_on was called before phy_init")) generic_warnings++; } +#define dev_warn(d, message, ...) warning(message) +#define dev_err(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define lockdep_assert_held(p) assert(*(p)) +#define WARN_ON_ONCE(v) assert(!(v)) +static void mutex_lock(int *m) { assert(!*m); *m = 1; } +static void mutex_unlock(int *m) { assert(*m); *m = 0; } +struct held { int *m; }; +static struct held hold(int *m) { mutex_lock(m); return (struct held){m}; } +static void release(struct held *h) { mutex_unlock(h->m); } +#define guard(kind) struct held held_guard __attribute__((cleanup(release))) = hold +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE }; +enum { APPLE_ATCPHY_LANE_0, APPLE_ATCPHY_LANE_1 }; +enum usb_role { USB_ROLE_NONE, USB_ROLE_HOST, USB_ROLE_DEVICE }; +struct usb_role_switch { struct dwc3_apple *data; }; +#define usb_role_switch_get_drvdata(s) ((s)->data) +#define PHY_MODE_INVALID (-1) +enum { TYPEC_STATE_SAFE, TYPEC_STATE_USB, TYPEC_MODE_USB4, + TYPEC_DP_STATE_C, TYPEC_DP_STATE_D, TYPEC_DP_STATE_E }; +enum { USB_TYPEC_TBT_SID = 0x8087, USB_TYPEC_DP_SID = 0xff01 }; +#define EUDO_USB_MODE_MASK 3U +enum { EUDO_USB_MODE_USB2, EUDO_USB_MODE_USB3, EUDO_USB_MODE_USB4 }; +struct enter_usb_data { u32 eudo; }; +struct typec_altmode { unsigned int svid; }; +struct typec_mux_state { unsigned long mode; struct typec_altmode *alt; void *data; }; +struct typec_mux_dev { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +struct reset_controller_dev { int unused; }; +#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) diff --git a/tools/diagnostics/test-apple-dwc3-teardown.py b/tools/diagnostics/test-apple-dwc3-teardown.py new file mode 100644 index 0000000000000..4a007d07d8ddf --- /dev/null +++ b/tools/diagnostics/test-apple-dwc3-teardown.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Fault-inject production Apple DWC3 teardown and ATC recovery; retain artifacts.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess + +p = argparse.ArgumentParser() +p.add_argument('--source', type=Path, default=Path(__file__).resolve().parents[2]) +p.add_argument('--out', type=Path, required=True) +p.add_argument('--case', choices=('all', 'power', 'reset', 'legacy'), default='all') +args = p.parse_args() +args.out.mkdir(parents=True, exist_ok=True) +files = {n: (args.source / f).read_text() for n, f in { + 'atc': 'drivers/phy/apple/atc.c', + 'phy': 'drivers/phy/phy-core.c', + 'core': 'drivers/usb/dwc3/core.c', + 'apple': 'drivers/usb/dwc3/dwc3-apple.c', + 'state': 'drivers/usb/dwc3/dwc3-apple-state.h', +}.items()} + +def block(s, start): + a = s.index('{', start) + depth, b = 1, a + 1 + while depth: + depth += (s[b] == '{') - (s[b] == '}') + b += 1 + return s[start:b] + +def fn(n, name): + s = files[n] + m = re.search(r'^(?:static [^\n]*\b|(?:int|void) )' + name + r'\(', s, re.M) + assert m, (n, name) + return block(s, m.start()) + +decls = [] +for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode', 'atcphy_dp_link_rate'): + decls.append(block(files['atc'], files['atc'].index('enum ' + name + ' {')) + ';') +for name in ('atcphy_mode_configuration', 'atcphy_hw', 'atcphy_dp_link_rate_configuration'): + decls.append(block(files['atc'], files['atc'].index('struct ' + name + ' {')) + ';') +decls.append(block(files['state'], files['state'].index('enum dwc3_apple_state {')) + ';') +start = files['atc'].index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') +end = files['atc'].index('\n};', start) + 3 +decls.append(files['atc'][start:end]) +start = files['atc'].index('static const struct atcphy_dp_link_rate_configuration dp_lr_config[]') +end = files['atc'].index('\n};', start) + 3 +decls.append(files['atc'][start:end]) + +names = { + 'atc': ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', + 'atcphy_pipehandler_check', 'atcphy_pipehandler_set_mux', + 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_get_mode_config', 'atcphy_dp_configure', + 'j700_field_update', 'j700_aux_disable', 'atcphy_disable_dp_aux', + 'atcphy_power_off', 'atcphy_power_on', 'atcphy_configure', 'atcphy_usb2_init', 'atcphy_usb3_power_off', + 'atcphy_usb3_set_mode', 'atcphy_usb2_power_off', + '_atcphy_dwc3_reset_assert', 'atcphy_dwc3_reset_assert', 'atcphy_mux_set'), + 'phy': ('phy_init', 'phy_exit', 'phy_power_on', 'phy_power_off'), + 'core': ('dwc3_phy_power_on', 'dwc3_phy_power_off', 'dwc3_phy_exit', 'dwc3_core_exit'), + 'apple': ('dwc3_apple_core_stop', 'dwc3_apple_core_init', 'dwc3_apple_core_start', + 'dwc3_apple_init', 'dwc3_apple_exit', 'dwc3_usb_role_switch_set'), +} +names['core'] = ('dwc3_phy_power_on', 'dwc3_phy_power_off', 'dwc3_phy_exit', + 'dwc3_core_exit_after_phy_power_off', 'dwc3_core_exit', + 'dwc3_core_remove_common', 'dwc3_core_remove', 'dwc3_core_remove_no_shutdown') +names['atc'] += ('atcphy_dwc3_reset_deassert',) +names = {n: tuple(name for name in group if re.search(r'^(?:static [^\n]*\b|(?:int|void) )' + name + r'\(', files[n], re.M)) + for n, group in names.items()} +functions = '\n\n'.join(fn(n, name) for n, group in names.items() for name in group) +definitions = [] +lines = iter(files['atc'].splitlines()) +for line in lines: + if not line.startswith('#define '): + continue + definition = [line] + while definition[-1].endswith('\\'): + definition.append(next(lines)) + definitions.append('\n'.join(definition)) +definitions = '\n'.join(definitions) +here = Path(__file__).resolve().parent +fallback = '' if 'dwc3_core_remove_no_shutdown' in names['core'] else '#define dwc3_core_remove_no_shutdown dwc3_core_remove\n' +unit = ((here / 'apple-dwc3-teardown/prefix.c').read_text() + '\n' + definitions + '\n' + + '\n'.join(decls) + '\n' + (here / 'apple-dwc3-teardown/mocks.c').read_text() + '\n' + + functions + '\n' + fallback + (here / 'apple-dwc3-teardown/cases.c').read_text()) +(args.out / 'extracted-production.c').write_text(functions) +(args.out / 'host.c').write_text(unit) +metadata = {'functions': names, 'extracted_sha256': hashlib.sha256(functions.encode()).hexdigest(), + 'sources': {n: hashlib.sha256(s.encode()).hexdigest() for n, s in files.items()}} +(args.out / 'sources.json').write_text(json.dumps(metadata, indent=2) + '\n') +print(json.dumps(metadata), flush=True) +subprocess.run(['clang', '-std=gnu11', '-Wall', '-Werror', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer', + str(args.out / 'host.c'), '-o', str(args.out / 'host')], check=True) +subprocess.run([str(args.out / 'host'), args.case], check=True) From e923a54dcb659d7ff204efce02382522b562eb63 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:03:02 +1100 Subject: [PATCH 05/11] phy: apple: invalidate ATC routes across cable flips A Type-C orientation change updates swap_lanes without invalidating the completed mode. A repeated request for the same mode then skips lane configuration, and DP training can use the new lane selection against the old physical route. Fixed-hub mode changes also leave a live USB3 PIPE attached when their destination still uses USB3, although the lane backend is being rebuilt. Mark orientation changes dirty so existing cleanup and training guards apply until the route is restored. Park the fixed-hub PIPE before every lane rebuild and reconnect it after successful configuration. Preserve parking errors, DP ownership, same-orientation fast paths, fixed USB2 host behavior and the established generation-specific routing masks. Add a dynamic-debug orientation/state record without reading calibration data. Extend the production-function ASan/UBSan diagnostic with 144 orientation routes across T8103/T8122/T8140/T8142 and fixed/ordinary ports, both initial orientations and flip roundtrips, 122 transition poll injections, ownership and training guards, pending flip cancellation and capability rejection. The parent fails all three new regression groups; the patch passes them. R41-config ARM64 LLVM W=1 ATC/DWC3 objects and strict checkpatch pass. Reversed J700 hardware qualification remains with the release owner. (cherry picked from commit 072e4922e3bad5ca2917d6339a2a7ed71708885e) Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 31 ++- tools/diagnostics/test-apple-atc-parking.py | 262 +++++++++++++++++++- 2 files changed, 277 insertions(+), 16 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 80d8e26489b2d..83aea6e38454b 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -3194,19 +3194,30 @@ static int atcphy_probe_rcdev(struct apple_atcphy *atcphy) static int atcphy_sw_set(struct typec_switch_dev *sw, enum typec_orientation orientation) { struct apple_atcphy *atcphy = typec_switch_get_drvdata(sw); + bool swap_lanes; guard(mutex)(&atcphy->lock); switch (orientation) { case TYPEC_ORIENTATION_NONE: - break; + return 0; case TYPEC_ORIENTATION_NORMAL: - atcphy->swap_lanes = false; + swap_lanes = false; break; case TYPEC_ORIENTATION_REVERSE: - atcphy->swap_lanes = true; + swap_lanes = true; break; + default: + return -EINVAL; } + /* The mode alone cannot describe a route after its orientation changes. */ + if (atcphy->swap_lanes != swap_lanes) { + atcphy->swap_lanes = swap_lanes; + atcphy->mode_dirty = true; + } + dev_dbg(atcphy->dev, "orientation=%u mode=%u dirty=%u pipe=%u host=%u\n", + orientation, atcphy->mode, atcphy->mode_dirty, + atcphy->pipe_state, atcphy->host_active); return 0; } @@ -3340,15 +3351,13 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta /* * The controller of a port behind a fixed hub stays up across mode * changes, so the PIPE has to follow the mode from here rather than - * from a later set_mode call: park it before the lanes change to a - * mode without a USB3 backend, and route it to the new backend - * afterwards while dwc3 is up in host mode. + * from a later set_mode call. Even another USB3 mode can move the + * backend to the other PMA when orientation changes. Park before + * rebuilding the lanes, then reconnect while dwc3 is in host mode. */ - if (pipe_state != ATCPHY_PIPEHANDLER_STATE_USB3) { - ret = atcphy_park_pipehandler(atcphy); - if (ret) - return ret; - } + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; ret = atcphy_configure(atcphy, target_mode); if (ret) diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py index 0af77ee5deee1..b6537215c9864 100644 --- a/tools/diagnostics/test-apple-atc-parking.py +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only -"""Fault-inject production PIPE parking, mux, power-off and reset callbacks.""" +"""Fault-inject production PIPE, orientation, mux, power-off and reset callbacks.""" import hashlib import os from pathlib import Path @@ -62,19 +62,28 @@ def function(source, name): static u32 pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; static unsigned int polls, fail_poll, writes, usb2_on, usb2_writes, reset_writes, mux_writes; static bool stuck_ack; +static bool require_parked; static u32 readl(void *address) { return *(u32 *)address; } static void core_mask32(struct apple_atcphy *p, u32 offset, u32 mask, u32 value) { (void)p; assert(offset < sizeof(registers) && offset % 4 == 0); + if (require_parked) { + assert(p->pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(FIELD_GET(p->hw->pipe_mux_data, pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) == + PIPEHANDLER_MUX_CTRL_DATA_DUMMY); + assert(FIELD_GET(p->hw->pipe_mux_clk, pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) == + PIPEHANDLER_MUX_CTRL_CLK_DUMMY); + } registers[offset / 4] = (registers[offset / 4] & ~mask) | value; writes++; if (offset == ATCPHY_POWER_CTRL) registers[ATCPHY_POWER_STAT / 4] = registers[offset / 4]; if (offset == 0x2000) { - if (value == 0x10000001) + if (value == 0x10000001) { registers[offset / 4] |= BIT(1); - else { + registers[0x7034 / 4] |= BIT(3); + } else { registers[offset / 4] &= ~BIT(1); registers[0x7034 / 4] &= ~BIT(3); } @@ -119,7 +128,8 @@ def function(source, name): parts.append(dp) for name in ('atcphy_configure_lanes', 'j700_aux_enable', 'j700_aux_disable', 'atcphy_enable_dp_aux', 'atcphy_disable_dp_aux', 'atcphy_power_off', - 'atcphy_power_on', 'atcphy_configure', 'atcphy_mux_set', 'atcphy_usb3_power_off'): + 'atcphy_power_on', 'atcphy_configure', 'atcphy_sw_set', 'atcphy_mux_set', + 'atcphy_usb3_set_mode', 'atcphy_usb3_power_off'): parts.append(function(atc, name)) production = '\n'.join(parts) print('Production parking/caller SHA256:', hashlib.sha256(production.encode()).hexdigest(), flush=True) @@ -157,11 +167,16 @@ def function(source, name): enum { USB_TYPEC_TBT_SID = 0x8087, USB_TYPEC_DP_SID = 0xff01 }; #define EUDO_USB_MODE_MASK 3U enum { EUDO_USB_MODE_USB2, EUDO_USB_MODE_USB3, EUDO_USB_MODE_USB4 }; +enum typec_orientation { TYPEC_ORIENTATION_NONE, TYPEC_ORIENTATION_NORMAL, + TYPEC_ORIENTATION_REVERSE }; +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE }; struct enter_usb_data { u32 eudo; }; struct typec_altmode { unsigned int svid; }; struct typec_mux_state { unsigned long mode; struct typec_altmode *alt; void *data; }; struct typec_mux_dev { struct apple_atcphy *data; }; +struct typec_switch_dev { struct apple_atcphy *data; }; #define typec_mux_get_drvdata(mux) ((mux)->data) +#define typec_switch_get_drvdata(sw) ((sw)->data) struct phy { struct apple_atcphy *data; }; #define phy_get_drvdata(phy) ((phy)->data) struct reset_controller_dev { int unused; }; @@ -197,6 +212,7 @@ def function(source, name): }; polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; stuck_ack = false; + require_parked = false; } static void inject_parking(unsigned int fault) { @@ -409,6 +425,238 @@ def function(source, name): } printf("PASS: %u cleanup poll faults retain retry/ownership; dirty OFF retries cleanup\n", phases); } +static int select_mode(struct apple_atcphy *p, enum atcphy_mode mode) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct typec_mux_state state = { .mode = TYPEC_STATE_SAFE }; + + if (mode == APPLE_ATCPHY_MODE_USB3) + state.mode = TYPEC_STATE_USB; + else if (mode == APPLE_ATCPHY_MODE_USB3_DP || mode == APPLE_ATCPHY_MODE_DP) { + state.alt = &dp; + state.mode = mode == APPLE_ATCPHY_MODE_DP ? TYPEC_DP_STATE_C : TYPEC_DP_STATE_D; + } + return atcphy_mux_set(&mux, &state); +} +static void orient(struct apple_atcphy *p, bool reverse) +{ + struct typec_switch_dev sw = { p }; + assert(atcphy_sw_set(&sw, reverse ? TYPEC_ORIENTATION_REVERSE : + TYPEC_ORIENTATION_NORMAL) == 0); +} +static void orientation_initial(struct apple_atcphy *p, bool fixed) +{ + initial(p, fixed, false, false); + p->mode = p->typec_mode = fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF; + p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + p->dp_owned_pmas = p->dp_idle_pmas = 0; + p->aux_power_requested = false; + p->dp_link_rate = -1; + p->host_active = fixed; + registers[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + registers[ATCPHY_MISC / 4] = BIT(7); /* Preserve unrelated boot state. */ + pipe_regs[PIPEHANDLER_MUX_CTRL / 4] = + FIELD_PREP(hw.pipe_mux_data, PIPEHANDLER_MUX_CTRL_DATA_DUMMY) | + FIELD_PREP(hw.pipe_mux_clk, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); +} +static void check_route(struct apple_atcphy *p, enum atcphy_mode mode, bool reverse) +{ + assert(p->mode == mode && p->swap_lanes == reverse && !p->mode_dirty); + assert(registers[ATCPHY_MISC / 4] & BIT(7)); + if (hw.has_t8140_dp) { + u32 mask = mode == APPLE_ATCPHY_MODE_DP ? 0x3ffff : 0x1ffff; + assert((registers[0x60 / 4] & 0xfff) == + (mode == APPLE_ATCPHY_MODE_DP ? 0x492 : (reverse ? 0x252 : 0x489))); + assert((registers[0x64 / 4] & mask) == (mode == APPLE_ATCPHY_MODE_DP ? + (reverse ? 0x114 : 0x22014) : (0x110 | reverse))); + /* J700's audited path preserves the miscellaneous swap control. */ + assert(!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP)); + } else { + const struct atcphy_mode_configuration *cfg = atcphy_get_mode_config(p, mode); + u32 lanes = FIELD_PREP(ACIOPHY_LANE_MODE_RX0, cfg->lane_mode[0]) | + FIELD_PREP(ACIOPHY_LANE_MODE_TX0, cfg->lane_mode[0]) | + FIELD_PREP(ACIOPHY_LANE_MODE_RX1, cfg->lane_mode[1]) | + FIELD_PREP(ACIOPHY_LANE_MODE_TX1, cfg->lane_mode[1]); + assert((registers[hw.aciophy_lane_mode / 4] & 0xfff) == lanes); + assert(!!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP) == cfg->set_swap); + assert(FIELD_GET(ACIOPHY_CROSSBAR_PROTOCOL, + registers[hw.aciophy_crossbar / 4]) == cfg->crossbar); + } + assert(p->pipe_state == (p->fixed_usb2 ? atcphy_modes[mode].pipehandler_state : + ATCPHY_PIPEHANDLER_STATE_DUMMY)); +} +static void orientation_roundtrip(void) +{ + struct apple_atcphy p; + const enum atcphy_mode modes[] = { + APPLE_ATCPHY_MODE_USB3, APPLE_ATCPHY_MODE_USB3_DP, APPLE_ATCPHY_MODE_DP, + }; + unsigned int cases = 0; + + for (unsigned int generation = 0; generation < 4; generation++) { + hw.gen = generation == 0 ? ATCPHY_GENERATION_T8103 : ATCPHY_GENERATION_T8122; + hw.has_t8140_dp = generation == 2; + hw.has_usb4 = !hw.has_t8140_dp; + hw.aciophy_lane_mode = generation == 0 ? ACIOPHY_LANE_MODE_T8103 : + ACIOPHY_LANE_MODE_T8122; + hw.aciophy_crossbar = generation == 0 ? ACIOPHY_CROSSBAR_T8103 : + ACIOPHY_CROSSBAR_T8122; + hw.pipe_mux_clk = generation == 3 ? PIPEHANDLER_MUX_CTRL_CLK_T8142 : + PIPEHANDLER_MUX_CTRL_CLK_T8103; + hw.pipe_mux_data = generation == 3 ? PIPEHANDLER_MUX_CTRL_DATA_T8142 : + PIPEHANDLER_MUX_CTRL_DATA_T8103; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int m = 0; m < ARRAY_SIZE(modes); m++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + for (unsigned int turn = 0; turn < 3; turn++) { + bool reverse = first ^ (turn & 1); + orient(&p, reverse); + if (turn) { + assert(p.mode_dirty); + struct phy phy = { &p }; + unsigned int before = writes; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == -EBUSY); + assert(writes == before); + } + require_parked = true; + assert(select_mode(&p, modes[m]) == 0); + require_parked = false; + check_route(&p, modes[m], reverse); + unsigned int before = writes, before_mux = mux_writes; + orient(&p, reverse); + assert(select_mode(&p, modes[m]) == 0); + assert(writes == before && mux_writes == before_mux); + struct typec_switch_dev sw = { &p }; + assert(atcphy_sw_set(&sw, TYPEC_ORIENTATION_NONE) == 0); + assert(!p.mode_dirty && p.swap_lanes == reverse); + cases++; + } + /* A USB3 protocol change also rebuilds the live backend. */ + if (m == 0) { + require_parked = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + require_parked = false; + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + } + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == (fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF)); + assert(!p.mode_dirty && !p.aux_power_requested && !p.dp_idle_pmas); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + } + } + } + printf("PASS: %u production orientation routes across T8103/T8122/T8140/T8142, fixed/ordinary ports, both flip roundtrips\n", cases); +} +static void orientation_faults(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + orient(&p, !first); + unsigned int before = writes; + struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 2 }; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + /* Derive all actual poll phases, then inject each in a fresh transition. */ + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int total = polls; + for (unsigned int fault = 1; fault <= total; fault++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + unsigned int owned = p.dp_owned_pmas; + orient(&p, !first); + polls = 0; + fail_poll = fault; + int ret = select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP); + /* The inherited BIST/park handshake has advisory poll phases. */ + if (!ret) { + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + cases++; + continue; + } + assert(ret == -ETIMEDOUT); + if (p.mode_dirty) { + before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + if (fault == 1 && !fixed) + assert(p.dp_owned_pmas == owned && p.aux_power_requested); + } else { + assert(fixed && p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(first)); + assert(j700_dp_configure(&p, &training) == 0); + cases++; + } + /* A fatal parking check preserves the old owner and all lane words. */ + if (fixed) { + orientation_initial(&p, true); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, true); + before = writes; + inject_parking(0); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(writes == before && p.dp_owned_pmas == 2 && p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p.host_active); + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, true); + } + /* Reversing a pending flip cannot erase cleanup obligations. */ + orientation_initial(&p, fixed); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, true); + orient(&p, false); + assert(p.mode_dirty && p.dp_owned_pmas == 2); + p.ss_tunables = false; + before = writes; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -EOPNOTSUPP); + assert(writes == before && p.mode_dirty && p.dp_owned_pmas == 2); + p.ss_tunables = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, false); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + } + } + printf("PASS: %u orientation transition poll injections, dirty training guard, ownership and retry controls\n", cases); +} +static void backend_change(void) +{ + struct apple_atcphy p; + + orientation_initial(&p, true); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); + require_parked = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + require_parked = false; + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, false); + puts("PASS: a live fixed-hub USB3 backend parks before same-orientation protocol changes"); +} int main(int argc, char **argv) { assert(argc == 2); @@ -416,6 +664,9 @@ def function(source, name): else if (!strcmp(argv[1], "power")) power_parking(); else if (!strcmp(argv[1], "reset")) reset_parking(); else if (!strcmp(argv[1], "cleanup")) power_cleanup(); + else if (!strcmp(argv[1], "orientation")) orientation_roundtrip(); + else if (!strcmp(argv[1], "orientation-faults")) orientation_faults(); + else if (!strcmp(argv[1], "backend-change")) backend_change(); else assert(false); return 0; } @@ -429,7 +680,8 @@ def function(source, name): '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) failed = False - for case in ('mux', 'power', 'reset', 'cleanup'): + for case in ('mux', 'power', 'reset', 'cleanup', 'orientation', 'orientation-faults', + 'backend-change'): result = subprocess.run([str(path / 'test'), case]) if result.returncode: print('FAIL:', case, 'exit', result.returncode, flush=True) From 9d489a355cb9f5f0de178c1b8b70d7a2eedadae0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:15:37 +1100 Subject: [PATCH 06/11] test: apple: require fatal ATC orientation errors The orientation poll-injection loop accepts every successful return as an advisory phase. Suppressing the fatal RCAL1 timeout consequently passes all of the original tests. Its generic route oracle also consults the same mode table as the implementation, allowing incorrect reversed routes to pass. Record the production poll callers and allow success only for the existing dummy/BIST/unlock advisory phases. Check explicit public lane and crossbar expectations and preservation of unrelated routing state. Add SAFE-first roundtrips, persistent cleanup failure after BIST and fatal host-ACK errors, and cancellation after a partially applied orientation change. Reviewed production candidate 072e4922e3bad5ca2917d6339a2a7ed71708885e passes the strengthened ASan/UBSan controls. Isolated RCAL-error suppression and incorrect generic reverse-route controls pass the original diagnostic and fail this diagnostic. The prerequisite parent still fails the candidate regressions. Production driver code is unchanged; electrical recovery still requires hardware qualification. (cherry picked from commit ecf9f745f122252cc285466720b3b4b8addd1967) Signed-off-by: Ryan Murray Assisted-by: LLM --- tools/diagnostics/test-apple-atc-parking.py | 210 +++++++++++++++++--- 1 file changed, 181 insertions(+), 29 deletions(-) diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py index b6537215c9864..2304b948b7a2b 100644 --- a/tools/diagnostics/test-apple-atc-parking.py +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -62,7 +62,10 @@ def function(source, name): static u32 pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; static unsigned int polls, fail_poll, writes, usb2_on, usb2_writes, reset_writes, mux_writes; static bool stuck_ack; -static bool require_parked; +static const char *poll_caller[256], *fail_caller; +static bool bist_status_stuck; +static unsigned int bist_faults; +static bool require_parked, expected_j700_swap; static u32 readl(void *address) { return *(u32 *)address; } static void core_mask32(struct apple_atcphy *p, u32 offset, u32 mask, u32 value) { @@ -105,8 +108,13 @@ def function(source, name): static void set32(void *p, u32 v) { mask32(p, 0, v); } static void clear32(void *p, u32 v) { mask32(p, v, 0); } #define readl_poll_timeout_atomic(address, value, condition, interval, timeout) \ - ({ (value) = readl(address); polls++; \ - polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; }) + ({ (value) = readl(address); assert(polls < ARRAY_SIZE(poll_caller)); \ + poll_caller[polls++] = __func__; \ + bool bist_fault = bist_status_stuck && (void *)(address) == \ + (void *)®isters[ACIOPHY_BIST_MAC_USBPHY_STATUS2 / 4]; \ + bist_faults += bist_fault; \ + polls == fail_poll || bist_fault || \ + (fail_caller && !strcmp(fail_caller, __func__)) || !(condition) ? -ETIMEDOUT : 0; }) #define readl_poll_timeout readl_poll_timeout_atomic static void atcphy_usb2_power_on(struct apple_atcphy *p) { (void)p; usb2_on++; } static void atcphy_apply_tunables(struct apple_atcphy *p, enum atcphy_mode mode) @@ -211,7 +219,9 @@ def function(source, name): .aux_power_requested = true, }; polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; - stuck_ack = false; + stuck_ack = bist_status_stuck = false; + fail_caller = NULL; + bist_faults = 0; require_parked = false; } static void inject_parking(unsigned int fault) @@ -222,7 +232,12 @@ def function(source, name): else stuck_ack = true; } -static void recover(void) { stuck_ack = false; fail_poll = polls = 0; } +static void recover(void) +{ + stuck_ack = bist_status_stuck = false; + fail_caller = NULL; + fail_poll = polls = 0; +} static int request(struct apple_atcphy *p, bool safe) { struct typec_mux_dev mux = { p }; @@ -455,7 +470,10 @@ def function(source, name): p->dp_link_rate = -1; p->host_active = fixed; registers[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; - registers[ATCPHY_MISC / 4] = BIT(7); /* Preserve unrelated boot state. */ + registers[ATCPHY_MISC / 4] = BIT(7) | ATCPHY_MISC_LANE_SWAP; + expected_j700_swap = true; + registers[hw.aciophy_lane_mode / 4] = 0xa5000000; + registers[hw.aciophy_crossbar / 4] = 0x80000000; pipe_regs[PIPEHANDLER_MUX_CTRL / 4] = FIELD_PREP(hw.pipe_mux_data, PIPEHANDLER_MUX_CTRL_DATA_DUMMY) | FIELD_PREP(hw.pipe_mux_clk, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); @@ -464,27 +482,24 @@ def function(source, name): { assert(p->mode == mode && p->swap_lanes == reverse && !p->mode_dirty); assert(registers[ATCPHY_MISC / 4] & BIT(7)); - if (hw.has_t8140_dp) { - u32 mask = mode == APPLE_ATCPHY_MODE_DP ? 0x3ffff : 0x1ffff; - assert((registers[0x60 / 4] & 0xfff) == - (mode == APPLE_ATCPHY_MODE_DP ? 0x492 : (reverse ? 0x252 : 0x489))); - assert((registers[0x64 / 4] & mask) == (mode == APPLE_ATCPHY_MODE_DP ? - (reverse ? 0x114 : 0x22014) : (0x110 | reverse))); - /* J700's audited path preserves the miscellaneous swap control. */ - assert(!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP)); - } else { - const struct atcphy_mode_configuration *cfg = atcphy_get_mode_config(p, mode); - u32 lanes = FIELD_PREP(ACIOPHY_LANE_MODE_RX0, cfg->lane_mode[0]) | - FIELD_PREP(ACIOPHY_LANE_MODE_TX0, cfg->lane_mode[0]) | - FIELD_PREP(ACIOPHY_LANE_MODE_RX1, cfg->lane_mode[1]) | - FIELD_PREP(ACIOPHY_LANE_MODE_TX1, cfg->lane_mode[1]); - assert((registers[hw.aciophy_lane_mode / 4] & 0xfff) == lanes); - assert(!!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP) == cfg->set_swap); - assert(FIELD_GET(ACIOPHY_CROSSBAR_PROTOCOL, - registers[hw.aciophy_crossbar / 4]) == cfg->crossbar); - } - assert(p->pipe_state == (p->fixed_usb2 ? atcphy_modes[mode].pipehandler_state : - ATCPHY_PIPEHANDLER_STATE_DUMMY)); + /* Fixed public route expectations, independent of the production mode table. */ + assert(mode == APPLE_ATCPHY_MODE_USB3 || mode == APPLE_ATCPHY_MODE_USB3_DP || + mode == APPLE_ATCPHY_MODE_DP); + u32 lane = registers[hw.aciophy_lane_mode / 4]; + u32 crossbar = registers[hw.aciophy_crossbar / 4]; + u32 mask = hw.has_t8140_dp && mode != APPLE_ATCPHY_MODE_DP ? 0x1ffff : 0x3ffff; + assert((lane & 0xfff) == (mode == APPLE_ATCPHY_MODE_DP ? 0x492 : + (reverse ? 0x252 : 0x489))); + assert((lane & 0xfffff000) == 0xa5000000); + assert((crossbar & mask) == (mode == APPLE_ATCPHY_MODE_DP ? + (reverse ? 0x114 : 0x22014) : (0x110 | reverse))); + assert(crossbar & 0x80000000); + /* J700 preserves the boot swap bit; generic routes apply their explicit swap. */ + assert(!!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP) == + (hw.has_t8140_dp ? expected_j700_swap : + (mode != APPLE_ATCPHY_MODE_DP && reverse))); + assert(p->pipe_state == (p->fixed_usb2 && mode != APPLE_ATCPHY_MODE_DP ? + ATCPHY_PIPEHANDLER_STATE_USB3 : ATCPHY_PIPEHANDLER_STATE_DUMMY)); } static void orientation_roundtrip(void) { @@ -574,6 +589,25 @@ def function(source, name): polls = 0; assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); unsigned int total = polls; + bool advisory[256] = { false }; + for (unsigned int i = 0; i < total; i++) { + const char *caller = poll_caller[i]; + /* Only the two park handshakes, BIST, and host unlock are advisory. */ + if (fixed && (i < 2 || i + 1 == total)) { + assert(!strcmp(caller, i == 0 ? "atcphy_pipehandler_lock" : + "atcphy_pipehandler_unlock")); + advisory[i] = true; + } else if (!strcmp(caller, "atcphy_configure_pipehandler_usb3_host_t8122")) { + assert(fixed); + advisory[i] = true; + } else { + assert(!strcmp(caller, "j700_dp_wait") || + !strcmp(caller, "j700_field_update") || + !strcmp(caller, "atcphy_power_on") || + !strcmp(caller, "atcphy_configure") || + (fixed && !strcmp(caller, "atcphy_pipehandler_lock"))); + } + } for (unsigned int fault = 1; fault <= total; fault++) { orientation_initial(&p, fixed); orient(&p, first); @@ -584,7 +618,8 @@ def function(source, name): polls = 0; fail_poll = fault; int ret = select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP); - /* The inherited BIST/park handshake has advisory poll phases. */ + /* A fatal phase must never be accepted as advisory success. */ + assert((ret == 0) == advisory[fault - 1]); if (!ret) { check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); assert(!p.dp_owned_pmas && !p.dp_idle_pmas); @@ -657,6 +692,121 @@ def function(source, name): check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, false); puts("PASS: a live fixed-hub USB3 backend parks before same-orientation protocol changes"); } + + +static void failed_flip_return(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, !first); + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int route_fault = 0; + for (unsigned int i = 0; i < polls; i++) + if (!strcmp(poll_caller[i], "j700_field_update")) route_fault = i + 1; + assert(route_fault); + + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, !first); + polls = 0; + fail_poll = route_fault; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + /* The route changed before PMA idle completed; completed mode is still old. */ + assert((registers[0x60 / 4] & 0xfff) == (first ? 0x489 : 0x252)); + assert(p.mode_dirty && p.dp_idle_pmas == BIT(first) && !p.dp_owned_pmas); + orient(&p, first); + assert(p.mode_dirty && p.dp_idle_pmas == BIT(first)); + unsigned int before = writes; + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + assert(!p.dp_idle_pmas && !p.dp_owned_pmas); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + cases++; + } + } + printf("PASS: %u partially applied flip cancellations preserve pending PMA ownership\n", cases); +} + +static void safe_first_recovery(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, true); + orient(&p, first); + bist_status_stuck = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + assert(bist_faults == 2); /* Existing BIST status timeouts remain advisory. */ + check_route(&p, APPLE_ATCPHY_MODE_USB3, first); + bist_status_stuck = false; + fail_caller = "atcphy_pipehandler_lock"; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && !p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && p.host_active); + assert(p.aux_power_requested); + + /* Persistent AUX cleanup failure blocks SAFE before orientation publication. */ + fail_caller = "j700_field_update"; + u32 route = registers[0x60 / 4], crossbar = registers[0x64 / 4]; + for (unsigned int attempt = 0; attempt < 3; attempt++) { + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.swap_lanes == first && p.mode_dirty && p.aux_power_requested); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.host_active); + assert(registers[0x60 / 4] == route && registers[0x64 / 4] == crossbar); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + } + /* Returning the plug cannot clear an unavailable cleanup handshake. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.swap_lanes == first && p.mode_dirty); + recover(); /* Model hardware readiness returning; production performs the retry. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && !p.mode_dirty && !p.aux_power_requested); + /* SAFE uses the existing generic USB2 route and clears its swap control. */ + assert(!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP)); + expected_j700_swap = false; + orient(&p, !first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + + /* Real reconnect order: SAFE must succeed before the new orientation is published. */ + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(first)); + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + unsigned int before = writes, before_mux = mux_writes; + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(writes == before && mux_writes == before_mux); + cases++; + } + printf("PASS: %u chained BIST/host-ACK failure, persistent SAFE failure and SAFE-first roundtrip controls\n", cases); +} + int main(int argc, char **argv) { assert(argc == 2); @@ -667,6 +817,8 @@ def function(source, name): else if (!strcmp(argv[1], "orientation")) orientation_roundtrip(); else if (!strcmp(argv[1], "orientation-faults")) orientation_faults(); else if (!strcmp(argv[1], "backend-change")) backend_change(); + else if (!strcmp(argv[1], "safe-first")) safe_first_recovery(); + else if (!strcmp(argv[1], "failed-flip-return")) failed_flip_return(); else assert(false); return 0; } @@ -681,7 +833,7 @@ def function(source, name): subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) failed = False for case in ('mux', 'power', 'reset', 'cleanup', 'orientation', 'orientation-faults', - 'backend-change'): + 'backend-change', 'safe-first', 'failed-flip-return'): result = subprocess.run([str(path / 'test'), case]) if result.returncode: print('FAIL:', case, 'exit', result.returncode, flush=True) From 1c4939352d75e25f88ee62bb0ed50bf7a6ecb22a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:43:51 +1100 Subject: [PATCH 07/11] phy: apple: require acknowledged live USB3 PIPE parking A fixed-hub T8140 host keeps its controller alive across connector changes. Parking currently continues after a failed lock or unlock, records the dummy backend and lets lane/reset teardown proceed despite an incomplete handoff. An already asserted LOCK request also reports success without checking ACK. Verify actual ACK for existing requests. Require completed parking handshakes for the live T8140 fixed-hub USB3 backend. On failure retain backend and resource ownership, mark the mode dirty and return the error before further native reset or lane changes. Existing retries restore the transition when readiness returns. Keep the historical inactive/legacy parking policy and the fatal USB3 host setup ACK requirement. Extend the strict production-function sanitizer diagnostics with live LOCK/UNLOCK failure ordering, ownership and both orientation roundtrips, eight policy scope controls and REQ-without-ACK rejection. Keep fatal and advisory phases explicitly distinguished in the corrected poll harness. The parent fails the new regressions; the patch passes existing 144 route, 122 poll, sticky SAFE, partial cancellation and ownership controls. This repairs the reported handoff contract, not a demonstrated clock or electrical root cause. Warm hotplug hardware qualification remains open. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 26 +++++-- tools/diagnostics/test-apple-atc-parking.py | 81 +++++++++++++++++++-- tools/diagnostics/test-apple-atc-pipe.py | 42 ++++++++++- 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 83aea6e38454b..fe63091c18e29 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1085,12 +1085,11 @@ static int atcphy_pipehandler_lock(struct apple_atcphy *atcphy) u32 reg; if (readl(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ) & PIPEHANDLER_LOCK_EN) { - dev_warn(atcphy->dev, "Pipehandler already locked\n"); - return 0; + dev_warn(atcphy->dev, "Pipehandler lock already requested\n"); + } else { + set32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); } - set32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); - ret = readl_poll_timeout(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_ACK, reg, reg & PIPEHANDLER_LOCK_EN, 10, PIPEHANDLER_LOCK_ACK_TIMEOUT_US); if (ret) { @@ -1379,6 +1378,9 @@ static void atcphy_enable_dummy_phy(struct apple_atcphy *atcphy) static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) { + bool live_usb3 = atcphy->hw->has_t8140_dp && atcphy->fixed_usb2 && + atcphy->host_active && + atcphy->pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3; int ret; ret = atcphy_pipehandler_check(atcphy); @@ -1392,16 +1394,28 @@ static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) set32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE, PIPEHANDLER_OVERRIDE_RXDETECT); ret = atcphy_pipehandler_lock(atcphy); - if (ret) + if (ret) { + /* A live controller must acknowledge before its backend changes. */ + if (live_usb3) { + atcphy->mode_dirty = true; + return ret; + } dev_warn(atcphy->dev, "Failed to lock pipehandler"); + } /* Switch to dummy PHY */ atcphy_pipehandler_set_mux(atcphy, PIPEHANDLER_MUX_CTRL_DATA_DUMMY, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); ret = atcphy_pipehandler_unlock(atcphy); - if (ret) + if (ret) { + /* The mux changed, but the handoff has not completed. Retain ownership. */ + if (live_usb3) { + atcphy->mode_dirty = true; + return ret; + } dev_warn(atcphy->dev, "Failed to unlock pipehandler"); + } mask32(atcphy->regs.pipehandler + PIPEHANDLER_NONSELECTED_OVERRIDE, PIPEHANDLER_NATIVE_POWER_DOWN, FIELD_PREP(PIPEHANDLER_NATIVE_POWER_DOWN, 2)); diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py index 2304b948b7a2b..a7957b6a9b6e9 100644 --- a/tools/diagnostics/test-apple-atc-parking.py +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -62,6 +62,7 @@ def function(source, name): static u32 pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; static unsigned int polls, fail_poll, writes, usb2_on, usb2_writes, reset_writes, mux_writes; static bool stuck_ack; +static bool hold_unlock_ack; static const char *poll_caller[256], *fail_caller; static bool bist_status_stuck; static unsigned int bist_faults; @@ -98,7 +99,9 @@ def function(source, name): { *(u32 *)p = (*(u32 *)p & ~m) | v; if (p == (void *)&pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) - pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)p & PIPEHANDLER_LOCK_EN; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = hold_unlock_ack && + !(*(u32 *)p & PIPEHANDLER_LOCK_EN) ? PIPEHANDLER_LOCK_EN : + *(u32 *)p & PIPEHANDLER_LOCK_EN; if (p == (void *)&pipe_regs[PIPEHANDLER_AON_GEN / 4]) reset_writes++; if (p == (void *)&pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) mux_writes++; if ((uintptr_t)p >= (uintptr_t)usb2_regs && @@ -220,6 +223,7 @@ def function(source, name): }; polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; stuck_ack = bist_status_stuck = false; + hold_unlock_ack = false; fail_caller = NULL; bist_faults = 0; require_parked = false; @@ -235,6 +239,7 @@ def function(source, name): static void recover(void) { stuck_ack = bist_status_stuck = false; + hold_unlock_ack = false; fail_caller = NULL; fail_poll = polls = 0; } @@ -592,10 +597,9 @@ def function(source, name): bool advisory[256] = { false }; for (unsigned int i = 0; i < total; i++) { const char *caller = poll_caller[i]; - /* Only the two park handshakes, BIST, and host unlock are advisory. */ - if (fixed && (i < 2 || i + 1 == total)) { - assert(!strcmp(caller, i == 0 ? "atcphy_pipehandler_lock" : - "atcphy_pipehandler_unlock")); + /* Only BIST and the subsequent USB3 host unlock are advisory. */ + if (fixed && i + 1 == total) { + assert(!strcmp(caller, "atcphy_pipehandler_unlock")); advisory[i] = true; } else if (!strcmp(caller, "atcphy_configure_pipehandler_usb3_host_t8122")) { assert(fixed); @@ -605,7 +609,8 @@ def function(source, name): !strcmp(caller, "j700_field_update") || !strcmp(caller, "atcphy_power_on") || !strcmp(caller, "atcphy_configure") || - (fixed && !strcmp(caller, "atcphy_pipehandler_lock"))); + (fixed && (!strcmp(caller, "atcphy_pipehandler_lock") || + !strcmp(caller, "atcphy_pipehandler_unlock")))); } } for (unsigned int fault = 1; fault <= total; fault++) { @@ -756,7 +761,17 @@ def function(source, name): assert(bist_faults == 2); /* Existing BIST status timeouts remain advisory. */ check_route(&p, APPLE_ATCPHY_MODE_USB3, first); bist_status_stuck = false; - fail_caller = "atcphy_pipehandler_lock"; + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int host_lock_poll = 0; + for (unsigned int i = 0; i < polls; i++) + if (!strcmp(poll_caller[i], "atcphy_pipehandler_lock")) host_lock_poll = i + 1; + assert(host_lock_poll > 1); /* Inject the host lock, after acknowledged parking. */ + orientation_initial(&p, true); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + polls = 0; + fail_poll = host_lock_poll; assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && !p.mode_dirty); assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && p.host_active); @@ -807,6 +822,55 @@ def function(source, name): printf("PASS: %u chained BIST/host-ACK failure, persistent SAFE failure and SAFE-first roundtrip controls\n", cases); } +static void live_parking_handshake(void) +{ + struct apple_atcphy p; + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 2 }; + unsigned int cases = 0; + for (unsigned int reverse = 0; reverse < 2; reverse++) { + for (unsigned int unlock = 0; unlock < 2; unlock++) { + orientation_initial(&p, true); + orient(&p, reverse); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + u32 lanes = registers[0x60 / 4], route = registers[0x64 / 4]; + u32 native = pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4]; + unsigned int before = writes, owned = p.dp_owned_pmas; + if (unlock) + hold_unlock_ack = true; + else + stuck_ack = true; + for (unsigned int retry = 0; retry < 3; retry++) { + unsigned int before_mux = mux_writes; + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p.host_active); + assert(p.dp_owned_pmas == owned && p.aux_power_requested); + assert(writes == before && registers[0x60 / 4] == lanes && registers[0x64 / 4] == route); + assert(pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4] == native); + if (!unlock) assert(mux_writes == before_mux); + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(!usb2_writes && !reset_writes); + } + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && !p.mode_dirty && p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + expected_j700_swap = false; + orient(&p, !reverse); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !reverse); + assert(j700_dp_configure(&p, &rate) == 0 && p.dp_owned_pmas == BIT(reverse)); + cases++; + } + } + printf("PASS: %u live parking LOCK/UNLOCK failure, ownership, write ordering and retry roundtrips\n", cases); +} + int main(int argc, char **argv) { assert(argc == 2); @@ -818,6 +882,7 @@ def function(source, name): else if (!strcmp(argv[1], "orientation-faults")) orientation_faults(); else if (!strcmp(argv[1], "backend-change")) backend_change(); else if (!strcmp(argv[1], "safe-first")) safe_first_recovery(); + else if (!strcmp(argv[1], "live-park")) live_parking_handshake(); else if (!strcmp(argv[1], "failed-flip-return")) failed_flip_return(); else assert(false); return 0; @@ -833,7 +898,7 @@ def function(source, name): subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) failed = False for case in ('mux', 'power', 'reset', 'cleanup', 'orientation', 'orientation-faults', - 'backend-change', 'safe-first', 'failed-flip-return'): + 'backend-change', 'safe-first', 'failed-flip-return', 'live-park'): result = subprocess.run([str(path / 'test'), case]) if result.returncode: print('FAIL:', case, 'exit', result.returncode, flush=True) diff --git a/tools/diagnostics/test-apple-atc-pipe.py b/tools/diagnostics/test-apple-atc-pipe.py index 7a6525d8588b6..5231c8240588e 100644 --- a/tools/diagnostics/test-apple-atc-pipe.py +++ b/tools/diagnostics/test-apple-atc-pipe.py @@ -281,6 +281,44 @@ def function(name): assert(request(&p, APPLE_ATCPHY_MODE_USB4) == -EOPNOTSUPP && lane_calls == 0); puts("PASS: parking retry, SAFE fixed USB2, dirty-mode and capability guards"); } +static void requested_lock(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + pipe_regs[PIPEHANDLER_LOCK_REQ / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_pipehandler_lock(&p) == -ETIMEDOUT && polls == 1); + assert(!(pipe_regs[PIPEHANDLER_LOCK_REQ / 4] & PIPEHANDLER_LOCK_EN)); + pipe_regs[PIPEHANDLER_LOCK_REQ / 4] = PIPEHANDLER_LOCK_EN; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + assert(atcphy_pipehandler_lock(&p) == 0 && polls == 2); + puts("PASS: an existing LOCK request must still have actual ACK before success"); +} +static void live_policy_scope(void) +{ + struct apple_atcphy p; + for (unsigned int t8140 = 0; t8140 < 2; t8140++) { + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int active = 0; active < 2; active++) { + hw.has_t8140_dp = t8140; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + p.fixed_usb2 = fixed; + p.host_active = active; + stuck_ack = true; /* Request exists, but ACK never asserts. */ + int ret = atcphy_park_pipehandler(&p); + if (t8140 && fixed && active) { + assert(ret == -ETIMEDOUT && p.mode_dirty && mux_writes == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); + assert(!pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4]); + } else { + assert(ret == 0 && !p.mode_dirty && mux_writes == 3); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + } + } + } + puts("PASS: eight live/inactive, fixed/ordinary and T8140/legacy parking policy controls"); +} int main(int argc, char **argv) { assert(argc == 2); @@ -289,6 +327,8 @@ def function(name): else if (!strcmp(argv[1], "mux")) mux_retry(); else if (!strcmp(argv[1], "same")) same_mode_pending(); else if (!strcmp(argv[1], "park")) parking_and_guards(); + else if (!strcmp(argv[1], "requested")) requested_lock(); + else if (!strcmp(argv[1], "scope")) live_policy_scope(); else assert(false); return 0; } @@ -302,7 +342,7 @@ def function(name): '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) failed = False - for case in ('backend', 'host', 'mux', 'same', 'park'): + for case in ('backend', 'host', 'mux', 'same', 'park', 'requested', 'scope'): result = subprocess.run([str(path / 'test'), case]) if result.returncode: print('FAIL:', case, 'exit', result.returncode, flush=True) From 1d28bb2b5bc56601b2c0f178ac0212c4d6ac3f72 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:54:20 +1100 Subject: [PATCH 08/11] phy: apple: retain dirty state on live parking entry ACK errors An advisory USB3 host unlock can leave ACK asserted. If the next live fixed-hub parking attempt cannot release it, the initial check returns without marking the mode dirty. A subsequent request for the cached mode then reports success and DP training remains permitted despite the failed handoff. Mark this entry check failure dirty under the same live T8140 predicate as the parking lock and unlock failures. Retain backend and ownership and require a successful retry before cached mode success or training. Extend the production-function sanitizer controls for entry ACK failures, cached mode requests, both orientations and unchanged legacy/inactive scope. Host controls reproduce the cached success and training writes on the reviewed commit and pass with this correction. This establishes software error handling only; physical hotplug qualification remains open. (cherry picked from commit 926975ec6a633f26897565a34e66284d4a9527f2) Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 5 ++++- tools/diagnostics/test-apple-atc-parking.py | 24 ++++++++++++++------- tools/diagnostics/test-apple-atc-pipe.py | 11 +++++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index fe63091c18e29..f24e3ce8c2e58 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1384,8 +1384,11 @@ static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) int ret; ret = atcphy_pipehandler_check(atcphy); - if (ret) + if (ret) { + if (live_usb3) + atcphy->mode_dirty = true; return ret; + } /* Force disable link detection */ clear32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE_VALUES, diff --git a/tools/diagnostics/test-apple-atc-parking.py b/tools/diagnostics/test-apple-atc-parking.py index a7957b6a9b6e9..2c232e6e90a40 100644 --- a/tools/diagnostics/test-apple-atc-parking.py +++ b/tools/diagnostics/test-apple-atc-parking.py @@ -255,7 +255,8 @@ def function(source, name): } static void unchanged(struct apple_atcphy *p, bool dirty, bool host) { - assert(p->mode == APPLE_ATCPHY_MODE_USB3_DP && p->mode_dirty == dirty); + bool expected_dirty = dirty || (p->hw->has_t8140_dp && p->fixed_usb2 && host); + assert(p->mode == APPLE_ATCPHY_MODE_USB3_DP && p->mode_dirty == expected_dirty); assert(p->pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p->host_active == host); assert(p->dp_owned_pmas == 3 && p->dp_idle_pmas == (dirty ? 1U : 0U)); assert(p->aux_power_requested); @@ -263,7 +264,7 @@ def function(source, name): assert(mux_writes == 0); assert(pipe_regs[PIPEHANDLER_AON_GEN / 4] == PIPEHANDLER_AON_GEN_DWC3_RESET_N); assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); - if (dirty) { + if (expected_dirty) { struct phy_configure_opts_dp training = { .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, }; @@ -361,7 +362,7 @@ def function(source, name): recover(); assert(atcphy_dwc3_reset_assert(&p.rcdev, 0) == 0); assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !p.host_active); - assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty == dirty); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty == (dirty || fixed)); assert(p.dp_owned_pmas == 3 && p.dp_idle_pmas == (dirty ? 1U : 0U)); assert(p.aux_power_requested && writes == 0); assert(reset_writes == 2 && usb2_writes > 0); @@ -831,7 +832,7 @@ def function(source, name): struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 2 }; unsigned int cases = 0; for (unsigned int reverse = 0; reverse < 2; reverse++) { - for (unsigned int unlock = 0; unlock < 2; unlock++) { + for (unsigned int fault = 0; fault < 3; fault++) { orientation_initial(&p, true); orient(&p, reverse); assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); @@ -839,10 +840,15 @@ def function(source, name): u32 lanes = registers[0x60 / 4], route = registers[0x64 / 4]; u32 native = pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4]; unsigned int before = writes, owned = p.dp_owned_pmas; - if (unlock) + if (fault == 2) { + /* A previous host unlock left ACK asserted before parking. */ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; hold_unlock_ack = true; - else + } else if (fault == 1) { + hold_unlock_ack = true; + } else { stuck_ack = true; + } for (unsigned int retry = 0; retry < 3; retry++) { unsigned int before_mux = mux_writes; assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); @@ -851,7 +857,9 @@ def function(source, name): assert(p.dp_owned_pmas == owned && p.aux_power_requested); assert(writes == before && registers[0x60 / 4] == lanes && registers[0x64 / 4] == route); assert(pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4] == native); - if (!unlock) assert(mux_writes == before_mux); + if (fault != 1) assert(mux_writes == before_mux); + /* A failed SAFE cannot be forgotten by returning to the cached mode. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); assert(!usb2_writes && !reset_writes); } @@ -868,7 +876,7 @@ def function(source, name): cases++; } } - printf("PASS: %u live parking LOCK/UNLOCK failure, ownership, write ordering and retry roundtrips\n", cases); + printf("PASS: %u live parking entry-ACK/LOCK/UNLOCK failure, ownership, write ordering and retry roundtrips\n", cases); } int main(int argc, char **argv) diff --git a/tools/diagnostics/test-apple-atc-pipe.py b/tools/diagnostics/test-apple-atc-pipe.py index 5231c8240588e..d3548976741ed 100644 --- a/tools/diagnostics/test-apple-atc-pipe.py +++ b/tools/diagnostics/test-apple-atc-pipe.py @@ -314,10 +314,19 @@ def function(name): assert(ret == 0 && !p.mode_dirty && mux_writes == 3); assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); } + /* Entry ACK release is fatal everywhere, but dirties only live T8140 parking. */ + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + p.fixed_usb2 = fixed; + p.host_active = active; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_park_pipehandler(&p) == -ETIMEDOUT); + assert(p.mode_dirty == (t8140 && fixed && active)); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 0); } } } - puts("PASS: eight live/inactive, fixed/ordinary and T8140/legacy parking policy controls"); + puts("PASS: eight live/inactive, fixed/ordinary and T8140/legacy LOCK and entry-ACK policy controls"); } int main(int argc, char **argv) { From 00a2d824322f833a69000752c289181dd2e1ce3a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 17:06:03 +1100 Subject: [PATCH 09/11] phy: apple: make an existing PIPE request notice debug-only An existing request with actual ACK is a valid idempotent lock operation. Use debug level for that ordinary notice. Preserve all ACK polling, timeout/release warnings, fatal errors and dirty-state ownership guards. (cherry picked from commit b705aed087d0966cfa8bb4a344c2476e4cfca0a3) (cherry picked from commit 4a23c249fa834fe34250441873e71998a8c76e0e) Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/phy/apple/atc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index f24e3ce8c2e58..f654bb519c0f9 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1085,7 +1085,7 @@ static int atcphy_pipehandler_lock(struct apple_atcphy *atcphy) u32 reg; if (readl(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ) & PIPEHANDLER_LOCK_EN) { - dev_warn(atcphy->dev, "Pipehandler lock already requested\n"); + dev_dbg(atcphy->dev, "Pipehandler lock already requested\n"); } else { set32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); } From bdb9402f1b14958189cc6c15370baf04857ad077 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 17:57:17 +1100 Subject: [PATCH 10/11] drm/apple: keep physical unplug authoritative on failed teardown TIPD can leave the shared external connector present when closing the TBT switch or dropping the USB role fails before its OOB disconnect. Send the existing owner-gated disconnect before these fallible transitions, while retaining failed route/PHY ownership and bounded cable revalidation. DCP already reports physical absence after a failed stop. Keep that report authoritative by discarding pending old-sink hotplug replay and consuming firmware hotplug callbacks while the external cable is absent. Preserve power, link, generation and DMA teardown guards. Add an actual-function host regression with injected transition/stop failures, late callbacks, stale stop generations and two shared ports. The fixed source passes 88 ASan/UBSan assertions; the unchanged parent fails 14. Both affected source files compile for arm64 with W=1. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/gpu/drm/apple/dcp.c | 4 +- drivers/usb/typec/tipd/core.c | 31 +- .../apple/check-physical-disconnect.py | 333 ++++++++++++++++++ 3 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 tools/testing/apple/check-physical-disconnect.py diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 1b1d1d18a78e2..915181bfc5373 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2493,6 +2493,8 @@ static void dcp_external_report_disconnected(struct apple_dcp *dcp) unsigned int action; guard(mutex)(&dcp->iomfb.lock); + /* A paused callback from the old sink must not be replayed on wake. */ + dcp->external_hotplug_pending = false; action = dcp_mode_hotplug(&dcp->mode_state, false, dcp->connector ? &dcp->connector->connected : NULL); if (dcp->connector) { @@ -2666,7 +2668,7 @@ bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) return false; /* No lifecycle or HPD mutex may be acquired from the RX callback. */ lockdep_assert_held(&dcp->iomfb.lock); - if (READ_ONCE(dcp->quiescing)) + if (READ_ONCE(dcp->quiescing) || !READ_ONCE(dcp->external_cable)) return true; if (dcp->iomfb.presentation_paused) { dcp->external_hotplug_pending = true; diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index 36a6a0abc349e..bb1174cf2289b 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -776,6 +776,23 @@ static void cd321x_update_work(struct work_struct *work) /* A different dock must release the old routing before reconfiguration. */ was_disconnected |= partner_changed; + /* + * Report sink absence before any fallible hardware transition. A failed + * shutdown retains the route/PHY owner, but cannot keep the sink present. + * Every Type-C port references the same external DCP connector. Only the + * port which successfully acquired the shared display route may report a + * disconnect. A second DP sink can negotiate Alt Mode even though its mux + * request is rejected with -EBUSY; treating that sink as the connector + * owner would tear down the display carried by the first port. + */ + if (IS_REACHABLE(CONFIG_DRM) && cd321x->connector_fwnode && + dp_route_was_active && + (!new_connected || was_disconnected || !dp_connected || !dp_hpd || + dp_hpd_changed || dp_mode_changed)) { + drm_connector_oob_hotplug_event(cd321x->connector_fwnode, + connector_status_disconnected); + } + /* * ACIO carries the tunneled PCIe reset handshake over the still-live * Type-C data path. Close a disappearing USB4/TBT session before dropping @@ -798,20 +815,6 @@ static void cd321x_update_work(struct work_struct *work) goto retry; } - /* - * Every Type-C port references the same external DCP connector. Only the - * port which successfully acquired the shared display route may report a - * disconnect. A second DP sink can negotiate Alt Mode even though its mux - * request is rejected with -EBUSY; treating that sink as the connector - * owner would tear down the display carried by the first port. - */ - if (IS_REACHABLE(CONFIG_DRM) && cd321x->connector_fwnode && - dp_route_was_active && - (!new_connected || was_disconnected || !dp_connected || !dp_hpd || - dp_hpd_changed || dp_mode_changed)) { - drm_connector_oob_hotplug_event(cd321x->connector_fwnode, connector_status_disconnected); - } - /* Process partner disconnection or change */ if (!new_connected || partner_changed) { if (!IS_ERR(tps->partner)) diff --git a/tools/testing/apple/check-physical-disconnect.py b/tools/testing/apple/check-physical-disconnect.py new file mode 100644 index 0000000000000..f9ee9dcb8deb8 --- /dev/null +++ b/tools/testing/apple/check-physical-disconnect.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Execute the driver functions with mocked hardware and work scheduling. + +Run from the kernel tree; --source may select a parent tree for negative control. +This tests logical notifications and retained ownership, not hardware completion. +""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess + +p = argparse.ArgumentParser(description=__doc__) +p.add_argument('--source', type=Path) +p.add_argument('--output', type=Path, required=True) +p.add_argument('--cc', default='clang') +a = p.parse_args() +r = a.source or Path(__file__).resolve().parents[3] +a.output.mkdir(parents=True, exist_ok=True) + +def function(file, name): + s = (r / file).read_text() + m = re.search(r'^[\w* \t]+\b' + name + r'\([^;{}]*\)\s*\{', s, re.M) + assert m, name + pos = m.end() + depth = 1 + while depth: + depth += (s[pos] == '{') - (s[pos] == '}') + pos += 1 + return s[m.start():pos] + +pm = (r / 'drivers/usb/typec/tipd/cd321x-pm.h').read_text() +pm = pm.replace('#include ', '') +core = 'drivers/usb/typec/tipd/core.c' +dcp = 'drivers/gpu/drm/apple/dcp.c' +funcs = '\n'.join(function(f, n) for f, n in ( + ('drivers/gpu/drm/apple/iomfb.c', 'iomfb_pause_presentation'), + (dcp, 'dcp_external_report_disconnected'), + (dcp, 'dcp_external_publish_generation'), + (dcp, 'dcp_external_stop_locked'), + (dcp, 'dcp_external_disconnect'), + (dcp, 'dcp_external_stop_work'), + (dcp, 'dcp_external_hotplug'), + (core, 'cd321x_retry_revalidation'), + (core, 'cd321x_update_work'))) +prefix = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; +#define guard(x) fake_guard +#define fake_guard(x) ((void)(x)) +#define scoped_guard(x,y) for (int once = ((void)(y), 1); once; once = 0) +#define lockdep_assert_held(x) ((void)(x)) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x) = (v)) +#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) +#define to_delayed_work(w) container_of(w,struct delayed_work,work) +#define msecs_to_jiffies(x) (x) +#define IS_REACHABLE(x) 1 +#define CONFIG_DRM 1 +#define IS_ERR(x) false +#define PTR_ERR(x) (-EINVAL) +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +#define CD321X_DEBOUNCE_DELAY_MS 500 +#define TPS_STATUS_PLUG_PRESENT (1U << 0) +#define TPS_STATUS_PLUG_UPSIDE_DOWN (1U << 1) +#define TPS_DATA_STATUS_USB2_CONNECTION (1U << 0) +#define TPS_DATA_STATUS_USB3_CONNECTION (1U << 1) +#define TPS_DATA_STATUS_TBT_CONNECTION (1U << 2) +#define TPS_DATA_STATUS_DP_CONNECTION (1U << 3) +#define TPS_DATA_STATUS_DP_PIN_ASSIGNMENT_MASK (3U << 4) +#define TPS_DATA_STATUS_USB_DATA_ROLE (1U << 6) +#define CD321X_DATA_STATUS_USB4_CONNECTION (1U << 7) +#define CD321X_DATA_STATUS_HPD_LEVEL (1U << 8) +#define TPS_POWER_STATUS_PWROPMODE(x) (x) +#define TPS_STATUS_TO_UPSIDE_DOWN(x) ((x)&TPS_STATUS_PLUG_UPSIDE_DOWN) +#define TPS_STATUS_TO_TYPEC_PORTROLE(x) 0 +#define TPS_STATUS_TO_TYPEC_VCONN(x) 0 +#define TPS_STATUS_TO_TYPEC_DATAROLE(x) 0 +#define TYPEC_STATE_SAFE 0 +#define TYPEC_STATE_USB 1 +#define TYPEC_THUNDERBOLT_SWITCH_OFF 0 +#define TYPEC_ACCESSORY_NONE 0 +#define DCP_HOTPLUG_NOTIFY 1 +struct work_struct { bool pending; }; +struct delayed_work { struct work_struct work; }; +static int system_freezable_wq; +static int schedule_work(struct work_struct *w) { w->pending = true; return 1; } +static void cancel_delayed_work_sync(struct delayed_work *w) { w->work.pending = false; } +static void mod_delayed_work(int q, struct delayed_work *w, int t) +{ (void)q; (void)t; w->work.pending = true; } +struct apple_connector { bool connected, edid_invalidation_pending; }; +struct apple_dcp { + int external_lock, hpd_mutex; + struct { int lock; bool presentation_paused; } iomfb; + struct { bool connected; void *service; } dptxport[1]; + struct apple_connector *connector; + struct delayed_work typec_reconnect_wq, placeholder_edid_wq; + struct work_struct external_stop_work; + bool external_running, external_stopped, external_power_attempted; + bool external_cable, typec_cable_connected, typec_crtc_off, pipe_enabled_h17p; + bool external_hotplug_pending, external_hotplug_connected, quiescing; + u64 external_generation, external_rx_generation, external_crtc_generation; + u64 external_stop_generation, typec_generation; + int external_error, mode_state; void *avep; +}; +static int settle_error, power_error, hpd_error, release_error; +static int settles, powers, releases, detaches, notices; +static bool dcp_is_external_h17p(struct apple_dcp *d) { (void)d; return true; } +static unsigned dcp_mode_hotplug(int *mode, bool connected, bool *sink) +{ (void)mode; if (sink) *sink = connected; return 1; } +static void dcp_handle_hotplug_actions(struct apple_dcp *d, unsigned act) +{ (void)d; notices += !!act; } +static int iomfb_queue_settle(struct apple_dcp *d, int t) +{ (void)d; (void)t; settles++; return settle_error; } +static int iomfb_external_power_h17p(struct apple_dcp *d, bool on) +{ (void)d; assert(!on); powers++; return power_error; } +static bool dptx_service_available(void *p) { (void)p; return true; } +static int dptxport_set_hpd(void *p, bool on) +{ (void)p; assert(!on); return hpd_error; } +static int dptxport_release_display(void *p) +{ (void)p; releases++; return release_error; } +static void av_service_disconnect(struct apple_dcp *d) { (void)d; } +static void dpavservep_detach(struct apple_dcp *d) { (void)d; detaches++; } +static void dcp_firmware_metadata_connection(struct apple_dcp *d, u64 g, bool c) +{ (void)d; (void)g; (void)c; } +static int dcp_external_disconnect(struct apple_dcp *, bool); +static struct apple_dcp display; +static void drm_connector_oob_hotplug_event(void *node, int status) +{ assert(node); if (!status) dcp_external_disconnect(&display, true); } +#define connector_status_disconnected 0 +#define connector_status_connected 1 +''' +shim = r''' +enum usb_role { USB_ROLE_NONE, USB_ROLE_HOST, USB_ROLE_DEVICE }; +enum typec_pwr_opmode { TYPEC_PWR_MODE_USB, TYPEC_PWR_MODE_PD }; +enum typec_orientation { TYPEC_ORIENTATION_NONE, TYPEC_ORIENTATION_NORMAL, TYPEC_ORIENTATION_REVERSE }; +struct usb_pd_identity { int id; }; +struct typec_mux_state { void *alt, *data; int mode; }; +struct typec_thunderbolt_switch_data { int state; }; +struct typec_partner_desc { bool usb_pd; int accessory; struct usb_pd_identity *identity; }; +struct cd321x_status { u32 status, data_status, status_changed, data_status_changed, pwr_status; + struct usb_pd_identity partner_identity; }; +struct tps6598x { void *partner, *dev, *port, *psy; int lock; u32 data_status; enum usb_role *role_sw; }; +struct cd321x { struct tps6598x tps; struct cd321x_status update_status; + struct cd321x_pm_state pm; struct delayed_work update_work, resume_work; + struct typec_mux_state state; bool state_valid, display_route_active, cur_partner_is_pd; + struct usb_pd_identity cur_partner_identity; void *connector_fwnode, *tbt_switch; }; +static int tbt_error, role_error, safe_error, mode_error; +static int tbts, roles, safes; +static enum usb_role usb_role_switch_get_role(enum usb_role *r) { return *r; } +static int usb_role_switch_set_role(enum usb_role *r, enum usb_role n) +{ roles++; if (role_error) return role_error; *r = n; return 0; } +static int typec_thunderbolt_switch_set(void *p, void *s) +{ (void)p; (void)s; tbts++; return tbt_error; } +static int typec_set_mode(void *p, int s) +{ (void)p; assert(s == TYPEC_STATE_SAFE); safes++; return safe_error; } +static int cd321x_typec_update_mode(struct tps6598x *t, struct cd321x_status *s, bool *busy) +{ (void)t; (void)s; *busy = mode_error == -EBUSY; return mode_error; } +static void typec_unregister_partner(void *p) { (void)p; } +static void *typec_register_partner(void *p, struct typec_partner_desc *d) +{ (void)p; (void)d; return (void *)1; } +static void typec_partner_set_identity(void *p) { (void)p; } +#define typec_set_pwr_opmode(...) ((void)0) +#define typec_set_pwr_role(...) ((void)0) +#define typec_set_vconn_role(...) ((void)0) +#define typec_set_orientation(...) ((void)0) +#define typec_set_data_role(...) ((void)0) +#define power_supply_changed(...) ((void)0) +''' +tests = r''' +static int failures, checks; +#define CHECK(x) do { checks++; if (!(x)) { printf("FAIL line %d: %s\n", __LINE__, #x); failures++; } } while (0) +static struct apple_connector sink; +static enum usb_role role; +static struct cd321x port; +static void init(void) +{ + memset(&display,0,sizeof(display)); memset(&sink,0,sizeof(sink)); memset(&port,0,sizeof(port)); + sink.connected = true; display.connector = &sink; + display.external_running = display.external_power_attempted = true; + display.external_cable = display.typec_cable_connected = true; + display.dptxport[0].connected = display.pipe_enabled_h17p = true; + display.external_generation = display.external_rx_generation = display.external_crtc_generation = 7; + display.typec_generation = 4; + port.tps.partner = (void *)1; port.tps.role_sw = &role; role = USB_ROLE_HOST; + port.pm.phase = CD321X_PM_RUNNING; port.state_valid = true; port.state.mode = 2; + port.display_route_active = true; port.connector_fwnode = (void *)1; + settle_error = power_error = hpd_error = release_error = 0; + tbt_error = role_error = safe_error = mode_error = 0; + settles = powers = releases = detaches = notices = tbts = roles = safes = 0; +} +static void callback(bool connected) +{ + if (!dcp_external_hotplug(&display,connected)) + dcp_mode_hotplug(&display.mode_state,connected,&sink.connected); +} +int main(void) +{ + /* Unplug at each fallible transition with failing DCP power teardown. */ + for (int which = 0; which < 3; which++) { + init(); power_error = -ETIMEDOUT; + if (which == 0) tbt_error = -ETIMEDOUT; + if (which == 1) role_error = -EIO; + if (which == 2) safe_error = -ETIMEDOUT; + cd321x_update_work(&port.update_work.work); + CHECK(!sink.connected); CHECK(!display.external_cable); + CHECK(display.external_error == -ETIMEDOUT); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + CHECK(!display.external_stopped && !display.typec_crtc_off); + CHECK(port.display_route_active && !port.state_valid); + CHECK(port.pm.phase == CD321X_PM_REVALIDATE && port.pm.attempts_left == CD321X_RESUME_ATTEMPTS); + CHECK(notices > 0 && releases == 0); + callback(true); CHECK(!sink.connected); + printf("case transition-%d complete\n",which); + } + /* A rejected/nonowner port cannot disconnect the shared owner's sink. */ + for (int which = 0; which < 3; which++) { + init(); port.display_route_active = false; + if (which == 0) tbt_error = -EIO; + if (which == 1) role_error = -EIO; + if (which == 2) safe_error = -EIO; + cd321x_update_work(&port.update_work.work); + CHECK(sink.connected && display.external_cable && !notices); + } + init(); port.display_route_active = false; role = USB_ROLE_NONE; + port.update_status.status = TPS_STATUS_PLUG_PRESENT; + port.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&port.update_work.work); + CHECK(sink.connected && !notices && !port.display_route_active); + /* Two ports share the connector: busy-port unplug leaves its owner live. */ + init(); + struct cd321x second = port; + enum usb_role second_role = USB_ROLE_NONE; + second.tps.role_sw = &second_role; second.display_route_active = false; + second.update_status.status = TPS_STATUS_PLUG_PRESENT; + second.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&second.update_work.work); + CHECK(!second.display_route_active && sink.connected && !notices); + second.update_status.status = second.update_status.data_status = 0; + tbt_error = -EIO; cd321x_update_work(&second.update_work.work); + CHECK(port.display_route_active && sink.connected && !notices); + power_error = -EIO; cd321x_update_work(&port.update_work.work); + CHECK(!sink.connected && port.display_route_active); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + /* Revalidation is bounded even though the failed hardware remains owned. */ + init(); tbt_error = -EIO; + cd321x_update_work(&port.update_work.work); + for (int i = 0; i < CD321X_RESUME_ATTEMPTS; i++) { + CHECK(cd321x_pm_begin_read(&port.pm)); cd321x_pm_snapshot_ready(&port.pm); + cd321x_update_work(&port.update_work.work); + } + CHECK(port.pm.phase == CD321X_PM_STALE && !port.pm.attempts_left); + CHECK(!sink.connected && port.display_route_active); + /* Stop failure stages preserve all state whose shutdown was not acked. */ + for (int which = 0; which < 4; which++) { + init(); + if (which == 0) settle_error = -EIO; + if (which == 1) power_error = -EIO; + if (which == 2) hpd_error = -EIO; + if (which == 3) release_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(!sink.connected && !display.typec_cable_connected); + CHECK(display.external_generation == 8 && display.external_rx_generation == 8); + CHECK(display.external_error == -EIO && display.dptxport[0].connected); + CHECK(!display.external_stopped && !display.typec_crtc_off); + CHECK(display.pipe_enabled_h17p == (which < 2)); + callback(true); CHECK(!sink.connected); + } + /* An existing error returns before pause: late RX still cannot resurrect. */ + init(); display.external_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(!display.iomfb.presentation_paused); + callback(true); CHECK(!sink.connected); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + /* A queued callback from the old sink cannot be replayed after unplug. */ + init(); display.external_error = -EIO; + display.iomfb.presentation_paused = true; callback(true); + CHECK(display.external_hotplug_pending); + CHECK(dcp_external_disconnect(&display,true) == -EIO); + iomfb_pause_presentation(&display,false); CHECK(!sink.connected); + /* An old stop generation cannot touch a later session. */ + init(); display.external_stop_generation = 6; + dcp_external_stop_work(&display.external_stop_work); + CHECK(sink.connected && settles == 0 && powers == 0 && releases == 0); + CHECK(dcp_external_stop_locked(&display,6) == 0 && sink.connected); + /* Same-cable CRTC off remains distinct from physical absence. */ + init(); CHECK(dcp_external_disconnect(&display,false) == 0); + CHECK(display.external_cable && display.typec_cable_connected); + CHECK(display.external_generation == 7); + /* Genuine discovery and the paused DPMS generation gate still work. */ + init(); sink.connected = false; callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_power_attempted = false; callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_stopped = true; display.external_rx_generation = 8; + callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_stopped = true; callback(true); CHECK(!sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_error = -EIO; callback(true); CHECK(!sink.connected); + /* Successful physical teardown may release ownership normally. */ + init(); CHECK(dcp_external_disconnect(&display,true) == 0); + CHECK(!sink.connected && !display.dptxport[0].connected); + CHECK(display.external_stopped && display.typec_crtc_off && !display.pipe_enabled_h17p); + printf("%d assertions, %d failures\n", checks, failures); return !!failures; +} +''' +source = prefix + pm + shim + funcs + tests +c = a.output / 'physical-disconnect.c' +c.write_text(source) +exe = a.output / 'physical-disconnect' +subprocess.run([a.cc, '-std=gnu11', '-Wall', '-Wextra', '-Wno-unused-variable', + '-Wno-unused-function', '-Wno-unused-but-set-variable', '-fsanitize=address,undefined', '-g', + str(c), '-o', str(exe)], check=True) +result = subprocess.run([str(exe)], capture_output=True, text=True) +(a.output / 'result.log').write_text(result.stdout + result.stderr) +(a.output / 'result.json').write_text(json.dumps({ + 'source': str(r), 'extracted_functions_sha256': hashlib.sha256(funcs.encode()).hexdigest(), + 'exit_code': result.returncode, 'output': result.stdout + result.stderr, + 'limitations': ['Kernel locks/work queues and hardware RPCs are mocked; no hardware qualification.'] +}, indent=2) + '\n') +print(result.stdout + result.stderr, end='') +raise SystemExit(result.returncode) From cae98602a58bf5c86eeb7491c4a572a4397b6bb6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 18:19:21 +1100 Subject: [PATCH 11/11] drm/apple: distinguish physical absence from initial cable discovery A fresh external session has no OOB cable authority yet. The external_cable callback guard added by 80642a29 also consumes firmware discovery before that first OOB event, leaving the connector absent if raw connect supplies no second RX notification. Use one zero-initialized physical-absence tombstone instead. Set it before physical disconnect teardown and clear it on a real OOB connect, with WRITE_ONCE/READ_ONCE for lifecycle/RX access. Preserve fresh pre-OOB publication and pending replay, while rejecting callbacks during known physical absence. Same-cable DPMS never sets the tombstone. Retain early owner-gated OOB ordering, clearing pending replay on logical disconnect, failed hardware ownership and all generation/stop guards. This does not invent a callback generation token across a genuine reconnect. Extend the actual-function regression through OOB connect/link admission with a raw-connect stub that emits no second RX. The final source passes 116 ASan/UBSan assertions; the reviewed parent fails six. DCP compiles for arm64 with W=1, and source-only checkpatch is clean. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/gpu/drm/apple/dcp-internal.h | 2 + drivers/gpu/drm/apple/dcp.c | 4 +- .../apple/check-physical-disconnect.py | 60 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 2007129a3ac12..3e969754e758e 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -229,6 +229,8 @@ struct apple_dcp { u64 external_rx_generation; u64 external_stop_generation; bool external_cable; + /* RX tombstone: initially unknown; only physical OOB events change it. */ + bool external_physically_absent; bool external_running; bool external_power_attempted; /* iomfb.lock: replay the latest callback after activation succeeds. */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 915181bfc5373..e482511ad1edf 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2624,6 +2624,7 @@ static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical) guard(mutex)(&dcp->external_lock); /* Invalidate physical EDID before waiting for firmware power/link teardown. */ if (physical) { + WRITE_ONCE(dcp->external_physically_absent, true); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; @@ -2668,7 +2669,7 @@ bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) return false; /* No lifecycle or HPD mutex may be acquired from the RX callback. */ lockdep_assert_held(&dcp->iomfb.lock); - if (READ_ONCE(dcp->quiescing) || !READ_ONCE(dcp->external_cable)) + if (READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->external_physically_absent)) return true; if (dcp->iomfb.presentation_paused) { dcp->external_hotplug_pending = true; @@ -2846,6 +2847,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) if (new_cable) dcp->external_generation++; dcp->external_cable = true; + WRITE_ONCE(dcp->external_physically_absent, false); dcp_external_publish_generation(dcp); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, true); diff --git a/tools/testing/apple/check-physical-disconnect.py b/tools/testing/apple/check-physical-disconnect.py index f9ee9dcb8deb8..77351802b465c 100644 --- a/tools/testing/apple/check-physical-disconnect.py +++ b/tools/testing/apple/check-physical-disconnect.py @@ -44,6 +44,8 @@ def function(file, name): (dcp, 'dcp_external_disconnect'), (dcp, 'dcp_external_stop_work'), (dcp, 'dcp_external_hotplug'), + (dcp, 'dcp_external_link_locked'), + (dcp, 'dcp_dptx_connect_oob'), (core, 'cd321x_retry_revalidation'), (core, 'cd321x_update_work'))) prefix = r''' @@ -95,7 +97,9 @@ def function(file, name): struct delayed_work { struct work_struct work; }; static int system_freezable_wq; static int schedule_work(struct work_struct *w) { w->pending = true; return 1; } +static void cancel_work_sync(struct work_struct *w) { w->pending = false; } static void cancel_delayed_work_sync(struct delayed_work *w) { w->work.pending = false; } +#define cancel_delayed_work cancel_delayed_work_sync static void mod_delayed_work(int q, struct delayed_work *w, int t) { (void)q; (void)t; w->work.pending = true; } struct apple_connector { bool connected, edid_invalidation_pending; }; @@ -108,11 +112,23 @@ def function(file, name): struct work_struct external_stop_work; bool external_running, external_stopped, external_power_attempted; bool external_cable, typec_cable_connected, typec_crtc_off, pipe_enabled_h17p; + bool external_physically_absent, external_prepared, placeholder_retried; + int typec_reconnect_tries; bool external_hotplug_pending, external_hotplug_connected, quiescing; u64 external_generation, external_rx_generation, external_crtc_generation; u64 external_stop_generation, typec_generation; int external_error, mode_state; void *avep; }; +struct platform_device { struct apple_dcp *data; }; +static struct apple_dcp *platform_get_drvdata(struct platform_device *p) { return p->data; } +static int raw_error, raw_links; +static int dcp_dptx_connect_raw(struct apple_dcp *d, u32 port, bool *waiting) +{ assert(port == 0); *waiting = false; raw_links++; + if (!raw_error) d->dptxport[0].connected = true; + return raw_error; } +static bool dcp_is_typec_output(struct apple_dcp *d) { (void)d; return true; } +static int dcp_dptx_connect(struct apple_dcp *d, u32 port) { (void)d; (void)port; return 0; } +#define DPTX_RECONNECT_DELAY 500 static int settle_error, power_error, hpd_error, release_error; static int settles, powers, releases, detaches, notices; static bool dcp_is_external_h17p(struct apple_dcp *d) { (void)d; return true; } @@ -186,7 +202,8 @@ def function(file, name): static void init(void) { memset(&display,0,sizeof(display)); memset(&sink,0,sizeof(sink)); memset(&port,0,sizeof(port)); - sink.connected = true; display.connector = &sink; + sink.connected = true; display.connector = &sink; display.external_prepared = true; + raw_error = raw_links = 0; display.external_running = display.external_power_attempted = true; display.external_cable = display.typec_cable_connected = true; display.dptxport[0].connected = display.pipe_enabled_h17p = true; @@ -308,6 +325,47 @@ def function(file, name): display.external_stopped = true; callback(true); CHECK(!sink.connected); init(); sink.connected = false; display.iomfb.presentation_paused = true; display.external_error = -EIO; callback(true); CHECK(!sink.connected); + /* Fresh firmware discovery precedes OOB: no second RX is required. */ + init(); sink.connected = false; + display.external_cable = display.typec_cable_connected = false; + display.external_generation = display.external_rx_generation = 37; + display.external_power_attempted = false; display.external_stopped = true; + display.dptxport[0].connected = false; + callback(true); CHECK(sink.connected); CHECK(!display.external_physically_absent); + struct platform_device device = { .data = &display }; + CHECK(dcp_dptx_connect_oob(&device,0) == 0); + CHECK(raw_links == 1 && sink.connected); + CHECK(display.external_generation == 38); + /* Initially paused discovery keeps the original pending-replay behavior. */ + init(); sink.connected = false; display.external_cable = false; + display.external_power_attempted = false; display.iomfb.presentation_paused = true; + callback(true); CHECK(display.external_hotplug_pending); CHECK(!sink.connected); + CHECK(dcp_dptx_connect_oob(&device,0) == 0 && raw_links == 1); + iomfb_pause_presentation(&display,false); CHECK(sink.connected); + /* Physical absence consumes late RX and clears an already queued replay. */ + init(); display.iomfb.presentation_paused = true; callback(true); + CHECK(display.external_hotplug_pending); + CHECK(dcp_external_disconnect(&display,true) == 0); + CHECK(display.external_physically_absent && !display.external_hotplug_pending); + callback(true); CHECK(!sink.connected && !display.external_hotplug_pending); + /* A real next OOB connect removes the tombstone and permits discovery. */ + CHECK(dcp_dptx_connect_oob(&device,0) == 0); + CHECK(!display.external_physically_absent && display.external_cable); + CHECK(display.external_generation == 9 && raw_links == 1); + callback(true); CHECK(sink.connected); + /* Same-cable DPMS does not create a physical-absence tombstone. */ + init(); CHECK(dcp_external_disconnect(&display,false) == 0); + CHECK(!display.external_physically_absent && display.external_cable); + sink.connected = false; callback(true); CHECK(!sink.connected); + CHECK(display.external_hotplug_pending); + iomfb_pause_presentation(&display,false); CHECK(sink.connected); + /* A failed hardware stop remains owned, even after a real OOB reconnect. */ + init(); display.external_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(display.external_physically_absent); callback(true); CHECK(!sink.connected); + CHECK(dcp_dptx_connect_oob(&device,0) == -EIO); + CHECK(!display.external_physically_absent && display.external_error == -EIO); + CHECK(raw_links == 0 && display.dptxport[0].connected && display.pipe_enabled_h17p); /* Successful physical teardown may release ownership normally. */ init(); CHECK(dcp_external_disconnect(&display,true) == 0); CHECK(!sink.connected && !display.dptxport[0].connected);