Bluetooth: btintel_pcie: verify and serialize D-state transitions

The gp0_received flag only reports that the GP0 handler ran. It does not
contain proof that the handler completed the requested D-state transition:
a matched case can leave the state unchanged.

Refresh the boot-stage register before treating the transition as
successful. When the controller is in D0 but the handler did not re-arm the
interface, restore the alive context, reset the IA, restart RX, and
complete the mailbox/alive handshake. Propagate RX setup failures to the PM
caller. Likewise, when the controller is in D3 but the handler did not
record it, restore the alive context so the next transition dispatches
correctly.

Serialize the alive-context claim between the GP0 handler and the
set_dxstate() fallback with irq_lock. Only the path that claims the D0
transition resets the IA and rearms RX, avoiding concurrent updates to the
shared ring state.

Co-developed-by: Ravindra <ravindra@intel.com>
Signed-off-by: Ravindra <ravindra@intel.com>
Signed-off-by: Sergey Lebedev <lsa.uz@pm.me>
Fixes: 88c6216a52 ("Bluetooth: btintel_pcie: Suspend/Resume: Controller doorbell interrupt handling")
Tested-by: Sergey Lebedev <lsa.uz@pm.me>
Signed-off-by: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
This commit is contained in:
Sergey Lebedev 2026-09-28 22:40:02 +05:30 committed by Luiz Augusto von Dentz
parent 2926eb3967
commit 4f6ead67b9

View File

@ -1975,6 +1975,10 @@ static void btintel_pcie_msix_gp0_handler(struct btintel_pcie_data *data)
submit_rx = false;
signal_waitq = false;
/* Serialize the alive_intr_ctxt claim against set_dxstate()'s
* register-based fallback, which can run concurrently.
*/
spin_lock(&data->irq_lock);
switch (data->alive_intr_ctxt) {
case BTINTEL_PCIE_ROM:
data->alive_intr_ctxt = BTINTEL_PCIE_FW_DL;
@ -2030,6 +2034,7 @@ static void btintel_pcie_msix_gp0_handler(struct btintel_pcie_data *data)
data->alive_intr_ctxt);
break;
}
spin_unlock(&data->irq_lock);
if (submit_rx) {
btintel_pcie_reset_ia(data);
@ -4212,7 +4217,7 @@ static void btintel_pcie_coredump(struct device *dev)
static int btintel_pcie_set_dxstate(struct btintel_pcie_data *data, u32 dxstate)
{
int retry = 0;
int retry = 0, err;
long status;
u32 dx_intr_timeout_ms = 200;
@ -4224,30 +4229,73 @@ static int btintel_pcie_set_dxstate(struct btintel_pcie_data *data, u32 dxstate)
status = wait_event_timeout(data->gp0_wait_q, data->gp0_received,
msecs_to_jiffies(dx_intr_timeout_ms));
if (status)
return 0;
if (!status) {
bt_dev_warn(data->hdev,
"Timeout (%u ms) on alive interrupt for D%d entry, retry count %d",
dx_intr_timeout_ms, dxstate, retry);
bt_dev_warn(data->hdev,
"Timeout (%u ms) on alive interrupt for D%d entry, retry count %d",
dx_intr_timeout_ms, dxstate, retry);
/* clear gp0 cause */
btintel_pcie_clr_reg_bits(data,
BTINTEL_PCIE_CSR_MSIX_HW_INT_CAUSES,
BTINTEL_PCIE_MSIX_HW_INT_CAUSES_GP0);
}
/* clear gp0 cause */
btintel_pcie_clr_reg_bits(data,
BTINTEL_PCIE_CSR_MSIX_HW_INT_CAUSES,
BTINTEL_PCIE_MSIX_HW_INT_CAUSES_GP0);
/* A hardware bug may cause the alive interrupt to be missed. Refresh
* boot_stage_cache from hardware, since only the interrupt handler
* updates it. Finally retry only if the state check still fails.
/* gp0_received is set at the top of the handler, before the switch on
* alive_intr_ctxt. Error and lockdown are filtered out above it, but a
* matched case can still complete without doing anything - D3 breaks
* unchanged while the controller has not reached D0 - and a hardware
* bug may drop the interrupt outright. Either way the flag says a gp0
* was handled, not that the transition completed, and only the register
* knows. Refresh the cache here and retry only if the state check still
* fails.
*/
data->boot_stage_cache = btintel_pcie_rd_reg32(data,
BTINTEL_PCIE_CSR_BOOT_STAGE_REG);
if (dxstate == BTINTEL_PCIE_STATE_D0) {
if (btintel_pcie_in_d0(data))
if (btintel_pcie_in_d0(data)) {
bool armed;
/* Claim the transition under irq_lock so a
* concurrent gp0 handler run cannot also arm RX.
*/
spin_lock(&data->irq_lock);
armed = data->alive_intr_ctxt == BTINTEL_PCIE_D0;
if (!armed)
data->alive_intr_ctxt = BTINTEL_PCIE_D0;
spin_unlock(&data->irq_lock);
if (armed)
return 0;
/* Do what the handler's D3 -> D0 branch
* would have done, unless it already has.
*/
btintel_pcie_reset_ia(data);
err = btintel_pcie_start_rx(data);
if (err)
return err;
/* Complete the mbox<->alive handshake */
if (test_and_clear_bit(BTINTEL_PCIE_MBOX_PARSE_PENDING,
&data->flags)) {
set_bit(BTINTEL_PCIE_MBOX_PARSE_READY, &data->flags);
wake_up(&data->mbox_parse_wait_q);
}
return 0;
}
} else {
if (btintel_pcie_in_d3(data))
if (btintel_pcie_in_d3(data)) {
/* Do what the handler's D0 -> D3 branch
* would have done, unless it already has.
*/
spin_lock(&data->irq_lock);
if (data->alive_intr_ctxt != BTINTEL_PCIE_D3)
data->alive_intr_ctxt = BTINTEL_PCIE_D3;
spin_unlock(&data->irq_lock);
return 0;
}
}
} while (++retry < BTINTEL_PCIE_DX_TRANSITION_MAX_RETRIES);