| // SPDX-License-Identifier: GPL-2.0 | 
 | /* | 
 |  * NHI specific operations | 
 |  * | 
 |  * Copyright (C) 2019, Intel Corporation | 
 |  * Author: Mika Westerberg <mika.westerberg@linux.intel.com> | 
 |  */ | 
 |  | 
 | #include <linux/delay.h> | 
 | #include <linux/suspend.h> | 
 |  | 
 | #include "nhi.h" | 
 | #include "nhi_regs.h" | 
 | #include "tb.h" | 
 |  | 
 | /* Ice Lake specific NHI operations */ | 
 |  | 
 | #define ICL_LC_MAILBOX_TIMEOUT	500 /* ms */ | 
 |  | 
 | static int check_for_device(struct device *dev, void *data) | 
 | { | 
 | 	return tb_is_switch(dev); | 
 | } | 
 |  | 
 | static bool icl_nhi_is_device_connected(struct tb_nhi *nhi) | 
 | { | 
 | 	struct tb *tb = pci_get_drvdata(nhi->pdev); | 
 | 	int ret; | 
 |  | 
 | 	ret = device_for_each_child(&tb->root_switch->dev, NULL, | 
 | 				    check_for_device); | 
 | 	return ret > 0; | 
 | } | 
 |  | 
 | static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) | 
 | { | 
 | 	u32 vs_cap; | 
 |  | 
 | 	/* | 
 | 	 * The Thunderbolt host controller is present always in Ice Lake | 
 | 	 * but the firmware may not be loaded and running (depending | 
 | 	 * whether there is device connected and so on). Each time the | 
 | 	 * controller is used we need to "Force Power" it first and wait | 
 | 	 * for the firmware to indicate it is up and running. This "Force | 
 | 	 * Power" is really not about actually powering on/off the | 
 | 	 * controller so it is accessible even if "Force Power" is off. | 
 | 	 * | 
 | 	 * The actual power management happens inside shared ACPI power | 
 | 	 * resources using standard ACPI methods. | 
 | 	 */ | 
 | 	pci_read_config_dword(nhi->pdev, VS_CAP_22, &vs_cap); | 
 | 	if (power) { | 
 | 		vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK; | 
 | 		vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT; | 
 | 		vs_cap |= VS_CAP_22_FORCE_POWER; | 
 | 	} else { | 
 | 		vs_cap &= ~VS_CAP_22_FORCE_POWER; | 
 | 	} | 
 | 	pci_write_config_dword(nhi->pdev, VS_CAP_22, vs_cap); | 
 |  | 
 | 	if (power) { | 
 | 		unsigned int retries = 350; | 
 | 		u32 val; | 
 |  | 
 | 		/* Wait until the firmware tells it is up and running */ | 
 | 		do { | 
 | 			pci_read_config_dword(nhi->pdev, VS_CAP_9, &val); | 
 | 			if (val & VS_CAP_9_FW_READY) | 
 | 				return 0; | 
 | 			usleep_range(3000, 3100); | 
 | 		} while (--retries); | 
 |  | 
 | 		return -ETIMEDOUT; | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd) | 
 | { | 
 | 	u32 data; | 
 |  | 
 | 	data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK; | 
 | 	pci_write_config_dword(nhi->pdev, VS_CAP_19, data | VS_CAP_19_VALID); | 
 | } | 
 |  | 
 | static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) | 
 | { | 
 | 	unsigned long end; | 
 | 	u32 data; | 
 |  | 
 | 	if (!timeout) | 
 | 		goto clear; | 
 |  | 
 | 	end = jiffies + msecs_to_jiffies(timeout); | 
 | 	do { | 
 | 		pci_read_config_dword(nhi->pdev, VS_CAP_18, &data); | 
 | 		if (data & VS_CAP_18_DONE) | 
 | 			goto clear; | 
 | 		usleep_range(1000, 1100); | 
 | 	} while (time_before(jiffies, end)); | 
 |  | 
 | 	return -ETIMEDOUT; | 
 |  | 
 | clear: | 
 | 	/* Clear the valid bit */ | 
 | 	pci_write_config_dword(nhi->pdev, VS_CAP_19, 0); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void icl_nhi_set_ltr(struct tb_nhi *nhi) | 
 | { | 
 | 	u32 max_ltr, ltr; | 
 |  | 
 | 	pci_read_config_dword(nhi->pdev, VS_CAP_16, &max_ltr); | 
 | 	max_ltr &= 0xffff; | 
 | 	/* Program the same value for both snoop and no-snoop */ | 
 | 	ltr = max_ltr << 16 | max_ltr; | 
 | 	pci_write_config_dword(nhi->pdev, VS_CAP_15, ltr); | 
 | } | 
 |  | 
 | static int icl_nhi_suspend(struct tb_nhi *nhi) | 
 | { | 
 | 	struct tb *tb = pci_get_drvdata(nhi->pdev); | 
 | 	int ret; | 
 |  | 
 | 	if (icl_nhi_is_device_connected(nhi)) | 
 | 		return 0; | 
 |  | 
 | 	if (tb_switch_is_icm(tb->root_switch)) { | 
 | 		/* | 
 | 		 * If there is no device connected we need to perform | 
 | 		 * both: a handshake through LC mailbox and force power | 
 | 		 * down before entering D3. | 
 | 		 */ | 
 | 		icl_nhi_lc_mailbox_cmd(nhi, ICL_LC_PREPARE_FOR_RESET); | 
 | 		ret = icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); | 
 | 		if (ret) | 
 | 			return ret; | 
 | 	} | 
 |  | 
 | 	return icl_nhi_force_power(nhi, false); | 
 | } | 
 |  | 
 | static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup) | 
 | { | 
 | 	struct tb *tb = pci_get_drvdata(nhi->pdev); | 
 | 	enum icl_lc_mailbox_cmd cmd; | 
 |  | 
 | 	if (!pm_suspend_via_firmware()) | 
 | 		return icl_nhi_suspend(nhi); | 
 |  | 
 | 	if (!tb_switch_is_icm(tb->root_switch)) | 
 | 		return 0; | 
 |  | 
 | 	cmd = wakeup ? ICL_LC_GO2SX : ICL_LC_GO2SX_NO_WAKE; | 
 | 	icl_nhi_lc_mailbox_cmd(nhi, cmd); | 
 | 	return icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); | 
 | } | 
 |  | 
 | static int icl_nhi_resume(struct tb_nhi *nhi) | 
 | { | 
 | 	int ret; | 
 |  | 
 | 	ret = icl_nhi_force_power(nhi, true); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	icl_nhi_set_ltr(nhi); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void icl_nhi_shutdown(struct tb_nhi *nhi) | 
 | { | 
 | 	icl_nhi_force_power(nhi, false); | 
 | } | 
 |  | 
 | const struct tb_nhi_ops icl_nhi_ops = { | 
 | 	.init = icl_nhi_resume, | 
 | 	.suspend_noirq = icl_nhi_suspend_noirq, | 
 | 	.resume_noirq = icl_nhi_resume, | 
 | 	.runtime_suspend = icl_nhi_suspend, | 
 | 	.runtime_resume = icl_nhi_resume, | 
 | 	.shutdown = icl_nhi_shutdown, | 
 | }; |