diff options
Diffstat (limited to 'drivers/soc/xilinx')
-rw-r--r-- | drivers/soc/xilinx/Kconfig | 44 | ||||
-rw-r--r-- | drivers/soc/xilinx/Makefile | 4 | ||||
-rw-r--r-- | drivers/soc/xilinx/xlnx_vcu.c | 630 | ||||
-rw-r--r-- | drivers/soc/xilinx/zynqmp_pm_domains.c | 311 | ||||
-rw-r--r-- | drivers/soc/xilinx/zynqmp_power.c | 263 |
5 files changed, 1252 insertions, 0 deletions
diff --git a/drivers/soc/xilinx/Kconfig b/drivers/soc/xilinx/Kconfig new file mode 100644 index 000000000..646512d72 --- /dev/null +++ b/drivers/soc/xilinx/Kconfig @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-2.0 +menu "Xilinx SoC drivers" + +config XILINX_VCU + tristate "Xilinx VCU logicoreIP Init" + depends on HAS_IOMEM + help + Provides the driver to enable and disable the isolation between the + processing system and programmable logic part by using the logicoreIP + register set. This driver also configures the frequency based on the + clock information from the logicoreIP register set. + + If you say yes here you get support for the logicoreIP. + + If unsure, say N. + + To compile this driver as a module, choose M here: the + module will be called xlnx_vcu. + +config ZYNQMP_POWER + bool "Enable Xilinx Zynq MPSoC Power Management driver" + depends on PM && ZYNQMP_FIRMWARE + default y + select MAILBOX + select ZYNQMP_IPI_MBOX + help + Say yes to enable power management support for ZyqnMP SoC. + This driver uses firmware driver as an interface for power + management request to firmware. It registers isr to handle + power management callbacks from firmware. It registers mailbox client + to handle power management callbacks from firmware. + + If in doubt, say N. + +config ZYNQMP_PM_DOMAINS + bool "Enable Zynq MPSoC generic PM domains" + default y + depends on PM && ZYNQMP_FIRMWARE + select PM_GENERIC_DOMAINS + help + Say yes to enable device power management through PM domains + If in doubt, say N. + +endmenu diff --git a/drivers/soc/xilinx/Makefile b/drivers/soc/xilinx/Makefile new file mode 100644 index 000000000..f66bfea5d --- /dev/null +++ b/drivers/soc/xilinx/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_XILINX_VCU) += xlnx_vcu.o +obj-$(CONFIG_ZYNQMP_POWER) += zynqmp_power.o +obj-$(CONFIG_ZYNQMP_PM_DOMAINS) += zynqmp_pm_domains.o diff --git a/drivers/soc/xilinx/xlnx_vcu.c b/drivers/soc/xilinx/xlnx_vcu.c new file mode 100644 index 000000000..a3aa40996 --- /dev/null +++ b/drivers/soc/xilinx/xlnx_vcu.c @@ -0,0 +1,630 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Xilinx VCU Init + * + * Copyright (C) 2016 - 2017 Xilinx, Inc. + * + * Contacts Dhaval Shah <dshah@xilinx.com> + */ +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/errno.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> + +/* Address map for different registers implemented in the VCU LogiCORE IP. */ +#define VCU_ECODER_ENABLE 0x00 +#define VCU_DECODER_ENABLE 0x04 +#define VCU_MEMORY_DEPTH 0x08 +#define VCU_ENC_COLOR_DEPTH 0x0c +#define VCU_ENC_VERTICAL_RANGE 0x10 +#define VCU_ENC_FRAME_SIZE_X 0x14 +#define VCU_ENC_FRAME_SIZE_Y 0x18 +#define VCU_ENC_COLOR_FORMAT 0x1c +#define VCU_ENC_FPS 0x20 +#define VCU_MCU_CLK 0x24 +#define VCU_CORE_CLK 0x28 +#define VCU_PLL_BYPASS 0x2c +#define VCU_ENC_CLK 0x30 +#define VCU_PLL_CLK 0x34 +#define VCU_ENC_VIDEO_STANDARD 0x38 +#define VCU_STATUS 0x3c +#define VCU_AXI_ENC_CLK 0x40 +#define VCU_AXI_DEC_CLK 0x44 +#define VCU_AXI_MCU_CLK 0x48 +#define VCU_DEC_VIDEO_STANDARD 0x4c +#define VCU_DEC_FRAME_SIZE_X 0x50 +#define VCU_DEC_FRAME_SIZE_Y 0x54 +#define VCU_DEC_FPS 0x58 +#define VCU_BUFFER_B_FRAME 0x5c +#define VCU_WPP_EN 0x60 +#define VCU_PLL_CLK_DEC 0x64 +#define VCU_GASKET_INIT 0x74 +#define VCU_GASKET_VALUE 0x03 + +/* vcu slcr registers, bitmask and shift */ +#define VCU_PLL_CTRL 0x24 +#define VCU_PLL_CTRL_RESET_MASK 0x01 +#define VCU_PLL_CTRL_RESET_SHIFT 0 +#define VCU_PLL_CTRL_BYPASS_MASK 0x01 +#define VCU_PLL_CTRL_BYPASS_SHIFT 3 +#define VCU_PLL_CTRL_FBDIV_MASK 0x7f +#define VCU_PLL_CTRL_FBDIV_SHIFT 8 +#define VCU_PLL_CTRL_POR_IN_MASK 0x01 +#define VCU_PLL_CTRL_POR_IN_SHIFT 1 +#define VCU_PLL_CTRL_PWR_POR_MASK 0x01 +#define VCU_PLL_CTRL_PWR_POR_SHIFT 2 +#define VCU_PLL_CTRL_CLKOUTDIV_MASK 0x03 +#define VCU_PLL_CTRL_CLKOUTDIV_SHIFT 16 +#define VCU_PLL_CTRL_DEFAULT 0 +#define VCU_PLL_DIV2 2 + +#define VCU_PLL_CFG 0x28 +#define VCU_PLL_CFG_RES_MASK 0x0f +#define VCU_PLL_CFG_RES_SHIFT 0 +#define VCU_PLL_CFG_CP_MASK 0x0f +#define VCU_PLL_CFG_CP_SHIFT 5 +#define VCU_PLL_CFG_LFHF_MASK 0x03 +#define VCU_PLL_CFG_LFHF_SHIFT 10 +#define VCU_PLL_CFG_LOCK_CNT_MASK 0x03ff +#define VCU_PLL_CFG_LOCK_CNT_SHIFT 13 +#define VCU_PLL_CFG_LOCK_DLY_MASK 0x7f +#define VCU_PLL_CFG_LOCK_DLY_SHIFT 25 +#define VCU_ENC_CORE_CTRL 0x30 +#define VCU_ENC_MCU_CTRL 0x34 +#define VCU_DEC_CORE_CTRL 0x38 +#define VCU_DEC_MCU_CTRL 0x3c +#define VCU_PLL_DIVISOR_MASK 0x3f +#define VCU_PLL_DIVISOR_SHIFT 4 +#define VCU_SRCSEL_MASK 0x01 +#define VCU_SRCSEL_SHIFT 0 +#define VCU_SRCSEL_PLL 1 + +#define VCU_PLL_STATUS 0x60 +#define VCU_PLL_STATUS_LOCK_STATUS_MASK 0x01 + +#define MHZ 1000000 +#define FVCO_MIN (1500U * MHZ) +#define FVCO_MAX (3000U * MHZ) +#define DIVISOR_MIN 0 +#define DIVISOR_MAX 63 +#define FRAC 100 +#define LIMIT (10 * MHZ) + +/** + * struct xvcu_device - Xilinx VCU init device structure + * @dev: Platform device + * @pll_ref: pll ref clock source + * @aclk: axi clock source + * @logicore_reg_ba: logicore reg base address + * @vcu_slcr_ba: vcu_slcr Register base address + * @coreclk: core clock frequency + */ +struct xvcu_device { + struct device *dev; + struct clk *pll_ref; + struct clk *aclk; + void __iomem *logicore_reg_ba; + void __iomem *vcu_slcr_ba; + u32 coreclk; +}; + +/** + * struct xvcu_pll_cfg - Helper data + * @fbdiv: The integer portion of the feedback divider to the PLL + * @cp: PLL charge pump control + * @res: PLL loop filter resistor control + * @lfhf: PLL loop filter high frequency capacitor control + * @lock_dly: Lock circuit configuration settings for lock windowsize + * @lock_cnt: Lock circuit counter setting + */ +struct xvcu_pll_cfg { + u32 fbdiv; + u32 cp; + u32 res; + u32 lfhf; + u32 lock_dly; + u32 lock_cnt; +}; + +static const struct xvcu_pll_cfg xvcu_pll_cfg[] = { + { 25, 3, 10, 3, 63, 1000 }, + { 26, 3, 10, 3, 63, 1000 }, + { 27, 4, 6, 3, 63, 1000 }, + { 28, 4, 6, 3, 63, 1000 }, + { 29, 4, 6, 3, 63, 1000 }, + { 30, 4, 6, 3, 63, 1000 }, + { 31, 6, 1, 3, 63, 1000 }, + { 32, 6, 1, 3, 63, 1000 }, + { 33, 4, 10, 3, 63, 1000 }, + { 34, 5, 6, 3, 63, 1000 }, + { 35, 5, 6, 3, 63, 1000 }, + { 36, 5, 6, 3, 63, 1000 }, + { 37, 5, 6, 3, 63, 1000 }, + { 38, 5, 6, 3, 63, 975 }, + { 39, 3, 12, 3, 63, 950 }, + { 40, 3, 12, 3, 63, 925 }, + { 41, 3, 12, 3, 63, 900 }, + { 42, 3, 12, 3, 63, 875 }, + { 43, 3, 12, 3, 63, 850 }, + { 44, 3, 12, 3, 63, 850 }, + { 45, 3, 12, 3, 63, 825 }, + { 46, 3, 12, 3, 63, 800 }, + { 47, 3, 12, 3, 63, 775 }, + { 48, 3, 12, 3, 63, 775 }, + { 49, 3, 12, 3, 63, 750 }, + { 50, 3, 12, 3, 63, 750 }, + { 51, 3, 2, 3, 63, 725 }, + { 52, 3, 2, 3, 63, 700 }, + { 53, 3, 2, 3, 63, 700 }, + { 54, 3, 2, 3, 63, 675 }, + { 55, 3, 2, 3, 63, 675 }, + { 56, 3, 2, 3, 63, 650 }, + { 57, 3, 2, 3, 63, 650 }, + { 58, 3, 2, 3, 63, 625 }, + { 59, 3, 2, 3, 63, 625 }, + { 60, 3, 2, 3, 63, 625 }, + { 61, 3, 2, 3, 63, 600 }, + { 62, 3, 2, 3, 63, 600 }, + { 63, 3, 2, 3, 63, 600 }, + { 64, 3, 2, 3, 63, 600 }, + { 65, 3, 2, 3, 63, 600 }, + { 66, 3, 2, 3, 63, 600 }, + { 67, 3, 2, 3, 63, 600 }, + { 68, 3, 2, 3, 63, 600 }, + { 69, 3, 2, 3, 63, 600 }, + { 70, 3, 2, 3, 63, 600 }, + { 71, 3, 2, 3, 63, 600 }, + { 72, 3, 2, 3, 63, 600 }, + { 73, 3, 2, 3, 63, 600 }, + { 74, 3, 2, 3, 63, 600 }, + { 75, 3, 2, 3, 63, 600 }, + { 76, 3, 2, 3, 63, 600 }, + { 77, 3, 2, 3, 63, 600 }, + { 78, 3, 2, 3, 63, 600 }, + { 79, 3, 2, 3, 63, 600 }, + { 80, 3, 2, 3, 63, 600 }, + { 81, 3, 2, 3, 63, 600 }, + { 82, 3, 2, 3, 63, 600 }, + { 83, 4, 2, 3, 63, 600 }, + { 84, 4, 2, 3, 63, 600 }, + { 85, 4, 2, 3, 63, 600 }, + { 86, 4, 2, 3, 63, 600 }, + { 87, 4, 2, 3, 63, 600 }, + { 88, 4, 2, 3, 63, 600 }, + { 89, 4, 2, 3, 63, 600 }, + { 90, 4, 2, 3, 63, 600 }, + { 91, 4, 2, 3, 63, 600 }, + { 92, 4, 2, 3, 63, 600 }, + { 93, 4, 2, 3, 63, 600 }, + { 94, 4, 2, 3, 63, 600 }, + { 95, 4, 2, 3, 63, 600 }, + { 96, 4, 2, 3, 63, 600 }, + { 97, 4, 2, 3, 63, 600 }, + { 98, 4, 2, 3, 63, 600 }, + { 99, 4, 2, 3, 63, 600 }, + { 100, 4, 2, 3, 63, 600 }, + { 101, 4, 2, 3, 63, 600 }, + { 102, 4, 2, 3, 63, 600 }, + { 103, 5, 2, 3, 63, 600 }, + { 104, 5, 2, 3, 63, 600 }, + { 105, 5, 2, 3, 63, 600 }, + { 106, 5, 2, 3, 63, 600 }, + { 107, 3, 4, 3, 63, 600 }, + { 108, 3, 4, 3, 63, 600 }, + { 109, 3, 4, 3, 63, 600 }, + { 110, 3, 4, 3, 63, 600 }, + { 111, 3, 4, 3, 63, 600 }, + { 112, 3, 4, 3, 63, 600 }, + { 113, 3, 4, 3, 63, 600 }, + { 114, 3, 4, 3, 63, 600 }, + { 115, 3, 4, 3, 63, 600 }, + { 116, 3, 4, 3, 63, 600 }, + { 117, 3, 4, 3, 63, 600 }, + { 118, 3, 4, 3, 63, 600 }, + { 119, 3, 4, 3, 63, 600 }, + { 120, 3, 4, 3, 63, 600 }, + { 121, 3, 4, 3, 63, 600 }, + { 122, 3, 4, 3, 63, 600 }, + { 123, 3, 4, 3, 63, 600 }, + { 124, 3, 4, 3, 63, 600 }, + { 125, 3, 4, 3, 63, 600 }, +}; + +/** + * xvcu_read - Read from the VCU register space + * @iomem: vcu reg space base address + * @offset: vcu reg offset from base + * + * Return: Returns 32bit value from VCU register specified + * + */ +static inline u32 xvcu_read(void __iomem *iomem, u32 offset) +{ + return ioread32(iomem + offset); +} + +/** + * xvcu_write - Write to the VCU register space + * @iomem: vcu reg space base address + * @offset: vcu reg offset from base + * @value: Value to write + */ +static inline void xvcu_write(void __iomem *iomem, u32 offset, u32 value) +{ + iowrite32(value, iomem + offset); +} + +/** + * xvcu_write_field_reg - Write to the vcu reg field + * @iomem: vcu reg space base address + * @offset: vcu reg offset from base + * @field: vcu reg field to write to + * @mask: vcu reg mask + * @shift: vcu reg number of bits to shift the bitfield + */ +static void xvcu_write_field_reg(void __iomem *iomem, int offset, + u32 field, u32 mask, int shift) +{ + u32 val = xvcu_read(iomem, offset); + + val &= ~(mask << shift); + val |= (field & mask) << shift; + + xvcu_write(iomem, offset, val); +} + +/** + * xvcu_set_vcu_pll_info - Set the VCU PLL info + * @xvcu: Pointer to the xvcu_device structure + * + * Programming the VCU PLL based on the user configuration + * (ref clock freq, core clock freq, mcu clock freq). + * Core clock frequency has higher priority than mcu clock frequency + * Errors in following cases + * - When mcu or clock clock get from logicoreIP is 0 + * - When VCU PLL DIV related bits value other than 1 + * - When proper data not found for given data + * - When sis570_1 clocksource related operation failed + * + * Return: Returns status, either success or error+reason + */ +static int xvcu_set_vcu_pll_info(struct xvcu_device *xvcu) +{ + u32 refclk, coreclk, mcuclk, inte, deci; + u32 divisor_mcu, divisor_core, fvco; + u32 clkoutdiv, vcu_pll_ctrl, pll_clk; + u32 cfg_val, mod, ctrl; + int ret, i; + const struct xvcu_pll_cfg *found = NULL; + + inte = xvcu_read(xvcu->logicore_reg_ba, VCU_PLL_CLK); + deci = xvcu_read(xvcu->logicore_reg_ba, VCU_PLL_CLK_DEC); + coreclk = xvcu_read(xvcu->logicore_reg_ba, VCU_CORE_CLK) * MHZ; + mcuclk = xvcu_read(xvcu->logicore_reg_ba, VCU_MCU_CLK) * MHZ; + if (!mcuclk || !coreclk) { + dev_err(xvcu->dev, "Invalid mcu and core clock data\n"); + return -EINVAL; + } + + refclk = (inte * MHZ) + (deci * (MHZ / FRAC)); + dev_dbg(xvcu->dev, "Ref clock from logicoreIP is %uHz\n", refclk); + dev_dbg(xvcu->dev, "Core clock from logicoreIP is %uHz\n", coreclk); + dev_dbg(xvcu->dev, "Mcu clock from logicoreIP is %uHz\n", mcuclk); + + clk_disable_unprepare(xvcu->pll_ref); + ret = clk_set_rate(xvcu->pll_ref, refclk); + if (ret) + dev_warn(xvcu->dev, "failed to set logicoreIP refclk rate\n"); + + ret = clk_prepare_enable(xvcu->pll_ref); + if (ret) { + dev_err(xvcu->dev, "failed to enable pll_ref clock source\n"); + return ret; + } + + refclk = clk_get_rate(xvcu->pll_ref); + + /* + * The divide-by-2 should be always enabled (==1) + * to meet the timing in the design. + * Otherwise, it's an error + */ + vcu_pll_ctrl = xvcu_read(xvcu->vcu_slcr_ba, VCU_PLL_CTRL); + clkoutdiv = vcu_pll_ctrl >> VCU_PLL_CTRL_CLKOUTDIV_SHIFT; + clkoutdiv = clkoutdiv & VCU_PLL_CTRL_CLKOUTDIV_MASK; + if (clkoutdiv != 1) { + dev_err(xvcu->dev, "clkoutdiv value is invalid\n"); + return -EINVAL; + } + + for (i = ARRAY_SIZE(xvcu_pll_cfg) - 1; i >= 0; i--) { + const struct xvcu_pll_cfg *cfg = &xvcu_pll_cfg[i]; + + fvco = cfg->fbdiv * refclk; + if (fvco >= FVCO_MIN && fvco <= FVCO_MAX) { + pll_clk = fvco / VCU_PLL_DIV2; + if (fvco % VCU_PLL_DIV2 != 0) + pll_clk++; + mod = pll_clk % coreclk; + if (mod < LIMIT) { + divisor_core = pll_clk / coreclk; + } else if (coreclk - mod < LIMIT) { + divisor_core = pll_clk / coreclk; + divisor_core++; + } else { + continue; + } + if (divisor_core >= DIVISOR_MIN && + divisor_core <= DIVISOR_MAX) { + found = cfg; + divisor_mcu = pll_clk / mcuclk; + mod = pll_clk % mcuclk; + if (mcuclk - mod < LIMIT) + divisor_mcu++; + break; + } + } + } + + if (!found) { + dev_err(xvcu->dev, "Invalid clock combination.\n"); + return -EINVAL; + } + + xvcu->coreclk = pll_clk / divisor_core; + mcuclk = pll_clk / divisor_mcu; + dev_dbg(xvcu->dev, "Actual Ref clock freq is %uHz\n", refclk); + dev_dbg(xvcu->dev, "Actual Core clock freq is %uHz\n", xvcu->coreclk); + dev_dbg(xvcu->dev, "Actual Mcu clock freq is %uHz\n", mcuclk); + + vcu_pll_ctrl &= ~(VCU_PLL_CTRL_FBDIV_MASK << VCU_PLL_CTRL_FBDIV_SHIFT); + vcu_pll_ctrl |= (found->fbdiv & VCU_PLL_CTRL_FBDIV_MASK) << + VCU_PLL_CTRL_FBDIV_SHIFT; + vcu_pll_ctrl &= ~(VCU_PLL_CTRL_POR_IN_MASK << + VCU_PLL_CTRL_POR_IN_SHIFT); + vcu_pll_ctrl |= (VCU_PLL_CTRL_DEFAULT & VCU_PLL_CTRL_POR_IN_MASK) << + VCU_PLL_CTRL_POR_IN_SHIFT; + vcu_pll_ctrl &= ~(VCU_PLL_CTRL_PWR_POR_MASK << + VCU_PLL_CTRL_PWR_POR_SHIFT); + vcu_pll_ctrl |= (VCU_PLL_CTRL_DEFAULT & VCU_PLL_CTRL_PWR_POR_MASK) << + VCU_PLL_CTRL_PWR_POR_SHIFT; + xvcu_write(xvcu->vcu_slcr_ba, VCU_PLL_CTRL, vcu_pll_ctrl); + + /* Set divisor for the core and mcu clock */ + ctrl = xvcu_read(xvcu->vcu_slcr_ba, VCU_ENC_CORE_CTRL); + ctrl &= ~(VCU_PLL_DIVISOR_MASK << VCU_PLL_DIVISOR_SHIFT); + ctrl |= (divisor_core & VCU_PLL_DIVISOR_MASK) << + VCU_PLL_DIVISOR_SHIFT; + ctrl &= ~(VCU_SRCSEL_MASK << VCU_SRCSEL_SHIFT); + ctrl |= (VCU_SRCSEL_PLL & VCU_SRCSEL_MASK) << VCU_SRCSEL_SHIFT; + xvcu_write(xvcu->vcu_slcr_ba, VCU_ENC_CORE_CTRL, ctrl); + + ctrl = xvcu_read(xvcu->vcu_slcr_ba, VCU_DEC_CORE_CTRL); + ctrl &= ~(VCU_PLL_DIVISOR_MASK << VCU_PLL_DIVISOR_SHIFT); + ctrl |= (divisor_core & VCU_PLL_DIVISOR_MASK) << + VCU_PLL_DIVISOR_SHIFT; + ctrl &= ~(VCU_SRCSEL_MASK << VCU_SRCSEL_SHIFT); + ctrl |= (VCU_SRCSEL_PLL & VCU_SRCSEL_MASK) << VCU_SRCSEL_SHIFT; + xvcu_write(xvcu->vcu_slcr_ba, VCU_DEC_CORE_CTRL, ctrl); + + ctrl = xvcu_read(xvcu->vcu_slcr_ba, VCU_ENC_MCU_CTRL); + ctrl &= ~(VCU_PLL_DIVISOR_MASK << VCU_PLL_DIVISOR_SHIFT); + ctrl |= (divisor_mcu & VCU_PLL_DIVISOR_MASK) << VCU_PLL_DIVISOR_SHIFT; + ctrl &= ~(VCU_SRCSEL_MASK << VCU_SRCSEL_SHIFT); + ctrl |= (VCU_SRCSEL_PLL & VCU_SRCSEL_MASK) << VCU_SRCSEL_SHIFT; + xvcu_write(xvcu->vcu_slcr_ba, VCU_ENC_MCU_CTRL, ctrl); + + ctrl = xvcu_read(xvcu->vcu_slcr_ba, VCU_DEC_MCU_CTRL); + ctrl &= ~(VCU_PLL_DIVISOR_MASK << VCU_PLL_DIVISOR_SHIFT); + ctrl |= (divisor_mcu & VCU_PLL_DIVISOR_MASK) << VCU_PLL_DIVISOR_SHIFT; + ctrl &= ~(VCU_SRCSEL_MASK << VCU_SRCSEL_SHIFT); + ctrl |= (VCU_SRCSEL_PLL & VCU_SRCSEL_MASK) << VCU_SRCSEL_SHIFT; + xvcu_write(xvcu->vcu_slcr_ba, VCU_DEC_MCU_CTRL, ctrl); + + /* Set RES, CP, LFHF, LOCK_CNT and LOCK_DLY cfg values */ + cfg_val = (found->res << VCU_PLL_CFG_RES_SHIFT) | + (found->cp << VCU_PLL_CFG_CP_SHIFT) | + (found->lfhf << VCU_PLL_CFG_LFHF_SHIFT) | + (found->lock_cnt << VCU_PLL_CFG_LOCK_CNT_SHIFT) | + (found->lock_dly << VCU_PLL_CFG_LOCK_DLY_SHIFT); + xvcu_write(xvcu->vcu_slcr_ba, VCU_PLL_CFG, cfg_val); + + return 0; +} + +/** + * xvcu_set_pll - PLL init sequence + * @xvcu: Pointer to the xvcu_device structure + * + * Call the api to set the PLL info and once that is done then + * init the PLL sequence to make the PLL stable. + * + * Return: Returns status, either success or error+reason + */ +static int xvcu_set_pll(struct xvcu_device *xvcu) +{ + u32 lock_status; + unsigned long timeout; + int ret; + + ret = xvcu_set_vcu_pll_info(xvcu); + if (ret) { + dev_err(xvcu->dev, "failed to set pll info\n"); + return ret; + } + + xvcu_write_field_reg(xvcu->vcu_slcr_ba, VCU_PLL_CTRL, + 1, VCU_PLL_CTRL_BYPASS_MASK, + VCU_PLL_CTRL_BYPASS_SHIFT); + xvcu_write_field_reg(xvcu->vcu_slcr_ba, VCU_PLL_CTRL, + 1, VCU_PLL_CTRL_RESET_MASK, + VCU_PLL_CTRL_RESET_SHIFT); + xvcu_write_field_reg(xvcu->vcu_slcr_ba, VCU_PLL_CTRL, + 0, VCU_PLL_CTRL_RESET_MASK, + VCU_PLL_CTRL_RESET_SHIFT); + /* + * Defined the timeout for the max time to wait the + * PLL_STATUS to be locked. + */ + timeout = jiffies + msecs_to_jiffies(2000); + do { + lock_status = xvcu_read(xvcu->vcu_slcr_ba, VCU_PLL_STATUS); + if (lock_status & VCU_PLL_STATUS_LOCK_STATUS_MASK) { + xvcu_write_field_reg(xvcu->vcu_slcr_ba, VCU_PLL_CTRL, + 0, VCU_PLL_CTRL_BYPASS_MASK, + VCU_PLL_CTRL_BYPASS_SHIFT); + return 0; + } + } while (!time_after(jiffies, timeout)); + + /* PLL is not locked even after the timeout of the 2sec */ + dev_err(xvcu->dev, "PLL is not locked\n"); + return -ETIMEDOUT; +} + +/** + * xvcu_probe - Probe existence of the logicoreIP + * and initialize PLL + * + * @pdev: Pointer to the platform_device structure + * + * Return: Returns 0 on success + * Negative error code otherwise + */ +static int xvcu_probe(struct platform_device *pdev) +{ + struct resource *res; + struct xvcu_device *xvcu; + int ret; + + xvcu = devm_kzalloc(&pdev->dev, sizeof(*xvcu), GFP_KERNEL); + if (!xvcu) + return -ENOMEM; + + xvcu->dev = &pdev->dev; + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "vcu_slcr"); + if (!res) { + dev_err(&pdev->dev, "get vcu_slcr memory resource failed.\n"); + return -ENODEV; + } + + xvcu->vcu_slcr_ba = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); + if (!xvcu->vcu_slcr_ba) { + dev_err(&pdev->dev, "vcu_slcr register mapping failed.\n"); + return -ENOMEM; + } + + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "logicore"); + if (!res) { + dev_err(&pdev->dev, "get logicore memory resource failed.\n"); + return -ENODEV; + } + + xvcu->logicore_reg_ba = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); + if (!xvcu->logicore_reg_ba) { + dev_err(&pdev->dev, "logicore register mapping failed.\n"); + return -ENOMEM; + } + + xvcu->aclk = devm_clk_get(&pdev->dev, "aclk"); + if (IS_ERR(xvcu->aclk)) { + dev_err(&pdev->dev, "Could not get aclk clock\n"); + return PTR_ERR(xvcu->aclk); + } + + xvcu->pll_ref = devm_clk_get(&pdev->dev, "pll_ref"); + if (IS_ERR(xvcu->pll_ref)) { + dev_err(&pdev->dev, "Could not get pll_ref clock\n"); + return PTR_ERR(xvcu->pll_ref); + } + + ret = clk_prepare_enable(xvcu->aclk); + if (ret) { + dev_err(&pdev->dev, "aclk clock enable failed\n"); + return ret; + } + + ret = clk_prepare_enable(xvcu->pll_ref); + if (ret) { + dev_err(&pdev->dev, "pll_ref clock enable failed\n"); + goto error_aclk; + } + + /* + * Do the Gasket isolation and put the VCU out of reset + * Bit 0 : Gasket isolation + * Bit 1 : put VCU out of reset + */ + xvcu_write(xvcu->logicore_reg_ba, VCU_GASKET_INIT, VCU_GASKET_VALUE); + + /* Do the PLL Settings based on the ref clk,core and mcu clk freq */ + ret = xvcu_set_pll(xvcu); + if (ret) { + dev_err(&pdev->dev, "Failed to set the pll\n"); + goto error_pll_ref; + } + + dev_set_drvdata(&pdev->dev, xvcu); + + dev_info(&pdev->dev, "%s: Probed successfully\n", __func__); + + return 0; + +error_pll_ref: + clk_disable_unprepare(xvcu->pll_ref); +error_aclk: + clk_disable_unprepare(xvcu->aclk); + return ret; +} + +/** + * xvcu_remove - Insert gasket isolation + * and disable the clock + * @pdev: Pointer to the platform_device structure + * + * Return: Returns 0 on success + * Negative error code otherwise + */ +static int xvcu_remove(struct platform_device *pdev) +{ + struct xvcu_device *xvcu; + + xvcu = platform_get_drvdata(pdev); + if (!xvcu) + return -ENODEV; + + /* Add the the Gasket isolation and put the VCU in reset. */ + xvcu_write(xvcu->logicore_reg_ba, VCU_GASKET_INIT, 0); + + clk_disable_unprepare(xvcu->pll_ref); + clk_disable_unprepare(xvcu->aclk); + + return 0; +} + +static const struct of_device_id xvcu_of_id_table[] = { + { .compatible = "xlnx,vcu" }, + { .compatible = "xlnx,vcu-logicoreip-1.0" }, + { } +}; +MODULE_DEVICE_TABLE(of, xvcu_of_id_table); + +static struct platform_driver xvcu_driver = { + .driver = { + .name = "xilinx-vcu", + .of_match_table = xvcu_of_id_table, + }, + .probe = xvcu_probe, + .remove = xvcu_remove, +}; + +module_platform_driver(xvcu_driver); + +MODULE_AUTHOR("Dhaval Shah <dshah@xilinx.com>"); +MODULE_DESCRIPTION("Xilinx VCU init Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/soc/xilinx/zynqmp_pm_domains.c b/drivers/soc/xilinx/zynqmp_pm_domains.c new file mode 100644 index 000000000..226d343f0 --- /dev/null +++ b/drivers/soc/xilinx/zynqmp_pm_domains.c @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ZynqMP Generic PM domain support + * + * Copyright (C) 2015-2019 Xilinx, Inc. + * + * Davorin Mista <davorin.mista@aggios.com> + * Jolly Shah <jollys@xilinx.com> + * Rajan Vaja <rajan.vaja@xilinx.com> + */ + +#include <linux/err.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/pm_domain.h> +#include <linux/slab.h> + +#include <linux/firmware/xlnx-zynqmp.h> + +#define ZYNQMP_NUM_DOMAINS (100) +/* Flag stating if PM nodes mapped to the PM domain has been requested */ +#define ZYNQMP_PM_DOMAIN_REQUESTED BIT(0) + +static int min_capability; + +/** + * struct zynqmp_pm_domain - Wrapper around struct generic_pm_domain + * @gpd: Generic power domain + * @node_id: PM node ID corresponding to device inside PM domain + * @flags: ZynqMP PM domain flags + */ +struct zynqmp_pm_domain { + struct generic_pm_domain gpd; + u32 node_id; + u8 flags; +}; + +/** + * zynqmp_gpd_is_active_wakeup_path() - Check if device is in wakeup source + * path + * @dev: Device to check for wakeup source path + * @not_used: Data member (not required) + * + * This function is checks device's child hierarchy and checks if any device is + * set as wakeup source. + * + * Return: 1 if device is in wakeup source path else 0 + */ +static int zynqmp_gpd_is_active_wakeup_path(struct device *dev, void *not_used) +{ + int may_wakeup; + + may_wakeup = device_may_wakeup(dev); + if (may_wakeup) + return may_wakeup; + + return device_for_each_child(dev, NULL, + zynqmp_gpd_is_active_wakeup_path); +} + +/** + * zynqmp_gpd_power_on() - Power on PM domain + * @domain: Generic PM domain + * + * This function is called before devices inside a PM domain are resumed, to + * power on PM domain. + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_power_on(struct generic_pm_domain *domain) +{ + int ret; + struct zynqmp_pm_domain *pd; + + pd = container_of(domain, struct zynqmp_pm_domain, gpd); + ret = zynqmp_pm_set_requirement(pd->node_id, + ZYNQMP_PM_CAPABILITY_ACCESS, + ZYNQMP_PM_MAX_QOS, + ZYNQMP_PM_REQUEST_ACK_BLOCKING); + if (ret) { + pr_err("%s() %s set requirement for node %d failed: %d\n", + __func__, domain->name, pd->node_id, ret); + return ret; + } + + pr_debug("%s() Powered on %s domain\n", __func__, domain->name); + return 0; +} + +/** + * zynqmp_gpd_power_off() - Power off PM domain + * @domain: Generic PM domain + * + * This function is called after devices inside a PM domain are suspended, to + * power off PM domain. + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_power_off(struct generic_pm_domain *domain) +{ + int ret; + struct pm_domain_data *pdd, *tmp; + struct zynqmp_pm_domain *pd; + u32 capabilities = min_capability; + bool may_wakeup; + + pd = container_of(domain, struct zynqmp_pm_domain, gpd); + + /* If domain is already released there is nothing to be done */ + if (!(pd->flags & ZYNQMP_PM_DOMAIN_REQUESTED)) { + pr_debug("%s() %s domain is already released\n", + __func__, domain->name); + return 0; + } + + list_for_each_entry_safe(pdd, tmp, &domain->dev_list, list_node) { + /* If device is in wakeup path, set capability to WAKEUP */ + may_wakeup = zynqmp_gpd_is_active_wakeup_path(pdd->dev, NULL); + if (may_wakeup) { + dev_dbg(pdd->dev, "device is in wakeup path in %s\n", + domain->name); + capabilities = ZYNQMP_PM_CAPABILITY_WAKEUP; + break; + } + } + + ret = zynqmp_pm_set_requirement(pd->node_id, capabilities, 0, + ZYNQMP_PM_REQUEST_ACK_NO); + /** + * If powering down of any node inside this domain fails, + * report and return the error + */ + if (ret) { + pr_err("%s() %s set requirement for node %d failed: %d\n", + __func__, domain->name, pd->node_id, ret); + return ret; + } + + pr_debug("%s() Powered off %s domain\n", __func__, domain->name); + return 0; +} + +/** + * zynqmp_gpd_attach_dev() - Attach device to the PM domain + * @domain: Generic PM domain + * @dev: Device to attach + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_gpd_attach_dev(struct generic_pm_domain *domain, + struct device *dev) +{ + int ret; + struct zynqmp_pm_domain *pd; + + pd = container_of(domain, struct zynqmp_pm_domain, gpd); + + /* If this is not the first device to attach there is nothing to do */ + if (domain->device_count) + return 0; + + ret = zynqmp_pm_request_node(pd->node_id, 0, 0, + ZYNQMP_PM_REQUEST_ACK_BLOCKING); + /* If requesting a node fails print and return the error */ + if (ret) { + pr_err("%s() %s request failed for node %d: %d\n", + __func__, domain->name, pd->node_id, ret); + return ret; + } + + pd->flags |= ZYNQMP_PM_DOMAIN_REQUESTED; + + pr_debug("%s() %s attached to %s domain\n", __func__, + dev_name(dev), domain->name); + return 0; +} + +/** + * zynqmp_gpd_detach_dev() - Detach device from the PM domain + * @domain: Generic PM domain + * @dev: Device to detach + */ +static void zynqmp_gpd_detach_dev(struct generic_pm_domain *domain, + struct device *dev) +{ + int ret; + struct zynqmp_pm_domain *pd; + + pd = container_of(domain, struct zynqmp_pm_domain, gpd); + + /* If this is not the last device to detach there is nothing to do */ + if (domain->device_count) + return; + + ret = zynqmp_pm_release_node(pd->node_id); + /* If releasing a node fails print the error and return */ + if (ret) { + pr_err("%s() %s release failed for node %d: %d\n", + __func__, domain->name, pd->node_id, ret); + return; + } + + pd->flags &= ~ZYNQMP_PM_DOMAIN_REQUESTED; + + pr_debug("%s() %s detached from %s domain\n", __func__, + dev_name(dev), domain->name); +} + +static struct generic_pm_domain *zynqmp_gpd_xlate + (struct of_phandle_args *genpdspec, void *data) +{ + struct genpd_onecell_data *genpd_data = data; + unsigned int i, idx = genpdspec->args[0]; + struct zynqmp_pm_domain *pd; + + pd = container_of(genpd_data->domains[0], struct zynqmp_pm_domain, gpd); + + if (genpdspec->args_count != 1) + return ERR_PTR(-EINVAL); + + /* Check for existing pm domains */ + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++) { + if (pd[i].node_id == idx) + goto done; + } + + /** + * Add index in empty node_id of power domain list as no existing + * power domain found for current index. + */ + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++) { + if (pd[i].node_id == 0) { + pd[i].node_id = idx; + break; + } + } + +done: + if (!genpd_data->domains[i] || i == ZYNQMP_NUM_DOMAINS) + return ERR_PTR(-ENOENT); + + return genpd_data->domains[i]; +} + +static int zynqmp_gpd_probe(struct platform_device *pdev) +{ + int i; + struct genpd_onecell_data *zynqmp_pd_data; + struct generic_pm_domain **domains; + struct zynqmp_pm_domain *pd; + struct device *dev = &pdev->dev; + + pd = devm_kcalloc(dev, ZYNQMP_NUM_DOMAINS, sizeof(*pd), GFP_KERNEL); + if (!pd) + return -ENOMEM; + + zynqmp_pd_data = devm_kzalloc(dev, sizeof(*zynqmp_pd_data), GFP_KERNEL); + if (!zynqmp_pd_data) + return -ENOMEM; + + zynqmp_pd_data->xlate = zynqmp_gpd_xlate; + + domains = devm_kcalloc(dev, ZYNQMP_NUM_DOMAINS, sizeof(*domains), + GFP_KERNEL); + if (!domains) + return -ENOMEM; + + if (!of_device_is_compatible(dev->parent->of_node, + "xlnx,zynqmp-firmware")) + min_capability = ZYNQMP_PM_CAPABILITY_UNUSABLE; + + for (i = 0; i < ZYNQMP_NUM_DOMAINS; i++, pd++) { + pd->node_id = 0; + pd->gpd.name = kasprintf(GFP_KERNEL, "domain%d", i); + pd->gpd.power_off = zynqmp_gpd_power_off; + pd->gpd.power_on = zynqmp_gpd_power_on; + pd->gpd.attach_dev = zynqmp_gpd_attach_dev; + pd->gpd.detach_dev = zynqmp_gpd_detach_dev; + + domains[i] = &pd->gpd; + + /* Mark all PM domains as initially powered off */ + pm_genpd_init(&pd->gpd, NULL, true); + } + + zynqmp_pd_data->domains = domains; + zynqmp_pd_data->num_domains = ZYNQMP_NUM_DOMAINS; + of_genpd_add_provider_onecell(dev->parent->of_node, zynqmp_pd_data); + + return 0; +} + +static int zynqmp_gpd_remove(struct platform_device *pdev) +{ + of_genpd_del_provider(pdev->dev.parent->of_node); + + return 0; +} + +static struct platform_driver zynqmp_power_domain_driver = { + .driver = { + .name = "zynqmp_power_controller", + }, + .probe = zynqmp_gpd_probe, + .remove = zynqmp_gpd_remove, +}; +module_platform_driver(zynqmp_power_domain_driver); + +MODULE_ALIAS("platform:zynqmp_power_controller"); diff --git a/drivers/soc/xilinx/zynqmp_power.c b/drivers/soc/xilinx/zynqmp_power.c new file mode 100644 index 000000000..c556623da --- /dev/null +++ b/drivers/soc/xilinx/zynqmp_power.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Xilinx Zynq MPSoC Power Management + * + * Copyright (C) 2014-2019 Xilinx, Inc. + * + * Davorin Mista <davorin.mista@aggios.com> + * Jolly Shah <jollys@xilinx.com> + * Rajan Vaja <rajan.vaja@xilinx.com> + */ + +#include <linux/mailbox_client.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/suspend.h> + +#include <linux/firmware/xlnx-zynqmp.h> +#include <linux/mailbox/zynqmp-ipi-message.h> + +/** + * struct zynqmp_pm_work_struct - Wrapper for struct work_struct + * @callback_work: Work structure + * @args: Callback arguments + */ +struct zynqmp_pm_work_struct { + struct work_struct callback_work; + u32 args[CB_ARG_CNT]; +}; + +static struct zynqmp_pm_work_struct *zynqmp_pm_init_suspend_work; +static struct mbox_chan *rx_chan; + +enum pm_suspend_mode { + PM_SUSPEND_MODE_FIRST = 0, + PM_SUSPEND_MODE_STD = PM_SUSPEND_MODE_FIRST, + PM_SUSPEND_MODE_POWER_OFF, +}; + +#define PM_SUSPEND_MODE_FIRST PM_SUSPEND_MODE_STD + +static const char *const suspend_modes[] = { + [PM_SUSPEND_MODE_STD] = "standard", + [PM_SUSPEND_MODE_POWER_OFF] = "power-off", +}; + +static enum pm_suspend_mode suspend_mode = PM_SUSPEND_MODE_STD; + +enum pm_api_cb_id { + PM_INIT_SUSPEND_CB = 30, + PM_ACKNOWLEDGE_CB, + PM_NOTIFY_CB, +}; + +static void zynqmp_pm_get_callback_data(u32 *buf) +{ + zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, 0, 0, 0, 0, buf); +} + +static irqreturn_t zynqmp_pm_isr(int irq, void *data) +{ + u32 payload[CB_PAYLOAD_SIZE]; + + zynqmp_pm_get_callback_data(payload); + + /* First element is callback API ID, others are callback arguments */ + if (payload[0] == PM_INIT_SUSPEND_CB) { + switch (payload[1]) { + case SUSPEND_SYSTEM_SHUTDOWN: + orderly_poweroff(true); + break; + case SUSPEND_POWER_REQUEST: + pm_suspend(PM_SUSPEND_MEM); + break; + default: + pr_err("%s Unsupported InitSuspendCb reason " + "code %d\n", __func__, payload[1]); + } + } + + return IRQ_HANDLED; +} + +static void ipi_receive_callback(struct mbox_client *cl, void *data) +{ + struct zynqmp_ipi_message *msg = (struct zynqmp_ipi_message *)data; + u32 payload[CB_PAYLOAD_SIZE]; + int ret; + + memcpy(payload, msg->data, sizeof(msg->len)); + /* First element is callback API ID, others are callback arguments */ + if (payload[0] == PM_INIT_SUSPEND_CB) { + if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) + return; + + /* Copy callback arguments into work's structure */ + memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], + sizeof(zynqmp_pm_init_suspend_work->args)); + + queue_work(system_unbound_wq, + &zynqmp_pm_init_suspend_work->callback_work); + + /* Send NULL message to mbox controller to ack the message */ + ret = mbox_send_message(rx_chan, NULL); + if (ret) + pr_err("IPI ack failed. Error %d\n", ret); + } +} + +/** + * zynqmp_pm_init_suspend_work_fn - Initialize suspend + * @work: Pointer to work_struct + * + * Bottom-half of PM callback IRQ handler. + */ +static void zynqmp_pm_init_suspend_work_fn(struct work_struct *work) +{ + struct zynqmp_pm_work_struct *pm_work = + container_of(work, struct zynqmp_pm_work_struct, callback_work); + + if (pm_work->args[0] == SUSPEND_SYSTEM_SHUTDOWN) { + orderly_poweroff(true); + } else if (pm_work->args[0] == SUSPEND_POWER_REQUEST) { + pm_suspend(PM_SUSPEND_MEM); + } else { + pr_err("%s Unsupported InitSuspendCb reason code %d.\n", + __func__, pm_work->args[0]); + } +} + +static ssize_t suspend_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *s = buf; + int md; + + for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) + if (suspend_modes[md]) { + if (md == suspend_mode) + s += sprintf(s, "[%s] ", suspend_modes[md]); + else + s += sprintf(s, "%s ", suspend_modes[md]); + } + + /* Convert last space to newline */ + if (s != buf) + *(s - 1) = '\n'; + return (s - buf); +} + +static ssize_t suspend_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int md, ret = -EINVAL; + + for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) + if (suspend_modes[md] && + sysfs_streq(suspend_modes[md], buf)) { + ret = 0; + break; + } + + if (!ret && md != suspend_mode) { + ret = zynqmp_pm_set_suspend_mode(md); + if (likely(!ret)) + suspend_mode = md; + } + + return ret ? ret : count; +} + +static DEVICE_ATTR_RW(suspend_mode); + +static int zynqmp_pm_probe(struct platform_device *pdev) +{ + int ret, irq; + u32 pm_api_version; + struct mbox_client *client; + + zynqmp_pm_init_finalize(); + zynqmp_pm_get_api_version(&pm_api_version); + + /* Check PM API version number */ + if (pm_api_version < ZYNQMP_PM_VERSION) + return -ENODEV; + + if (of_find_property(pdev->dev.of_node, "mboxes", NULL)) { + zynqmp_pm_init_suspend_work = + devm_kzalloc(&pdev->dev, + sizeof(struct zynqmp_pm_work_struct), + GFP_KERNEL); + if (!zynqmp_pm_init_suspend_work) + return -ENOMEM; + + INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, + zynqmp_pm_init_suspend_work_fn); + client = devm_kzalloc(&pdev->dev, sizeof(*client), GFP_KERNEL); + if (!client) + return -ENOMEM; + + client->dev = &pdev->dev; + client->rx_callback = ipi_receive_callback; + + rx_chan = mbox_request_channel_byname(client, "rx"); + if (IS_ERR(rx_chan)) { + dev_err(&pdev->dev, "Failed to request rx channel\n"); + return PTR_ERR(rx_chan); + } + } else if (of_find_property(pdev->dev.of_node, "interrupts", NULL)) { + irq = platform_get_irq(pdev, 0); + if (irq <= 0) + return -ENXIO; + + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + zynqmp_pm_isr, + IRQF_NO_SUSPEND | IRQF_ONESHOT, + dev_name(&pdev->dev), + &pdev->dev); + if (ret) { + dev_err(&pdev->dev, "devm_request_threaded_irq '%d' " + "failed with %d\n", irq, ret); + return ret; + } + } else { + dev_err(&pdev->dev, "Required property not found in DT node\n"); + return -ENOENT; + } + + ret = sysfs_create_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); + if (ret) { + dev_err(&pdev->dev, "unable to create sysfs interface\n"); + return ret; + } + + return 0; +} + +static int zynqmp_pm_remove(struct platform_device *pdev) +{ + sysfs_remove_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); + + if (!rx_chan) + mbox_free_channel(rx_chan); + + return 0; +} + +static const struct of_device_id pm_of_match[] = { + { .compatible = "xlnx,zynqmp-power", }, + { /* end of table */ }, +}; +MODULE_DEVICE_TABLE(of, pm_of_match); + +static struct platform_driver zynqmp_pm_platform_driver = { + .probe = zynqmp_pm_probe, + .remove = zynqmp_pm_remove, + .driver = { + .name = "zynqmp_power", + .of_match_table = pm_of_match, + }, +}; +module_platform_driver(zynqmp_pm_platform_driver); |