David Brazdil | 0f672f6 | 2019-12-10 10:32:29 +0000 | [diff] [blame] | 1 | // SPDX-License-Identifier: GPL-2.0+ |
| 2 | /* |
| 3 | * Copyright (C) 2016 Freescale Semiconductor, Inc. |
| 4 | * Copyright 2017-2018 NXP |
| 5 | * Dong Aisheng <aisheng.dong@nxp.com> |
| 6 | * |
| 7 | * Implementation of the SCU based Power Domains |
| 8 | * |
| 9 | * NOTE: a better implementation suggested by Ulf Hansson is using a |
| 10 | * single global power domain and implement the ->attach|detach_dev() |
| 11 | * callback for the genpd and use the regular of_genpd_add_provider_simple(). |
| 12 | * From within the ->attach_dev(), we could get the OF node for |
| 13 | * the device that is being attached and then parse the power-domain |
| 14 | * cell containing the "resource id" and store that in the per device |
| 15 | * struct generic_pm_domain_data (we have void pointer there for |
| 16 | * storing these kind of things). |
| 17 | * |
| 18 | * Additionally, we need to implement the ->stop() and ->start() |
| 19 | * callbacks of genpd, which is where you "power on/off" devices, |
| 20 | * rather than using the above ->power_on|off() callbacks. |
| 21 | * |
| 22 | * However, there're two known issues: |
| 23 | * 1. The ->attach_dev() of power domain infrastructure still does |
| 24 | * not support multi domains case as the struct device *dev passed |
| 25 | * in is a virtual PD device, it does not help for parsing the real |
| 26 | * device resource id from device tree, so it's unware of which |
| 27 | * real sub power domain of device should be attached. |
| 28 | * |
| 29 | * The framework needs some proper extension to support multi power |
| 30 | * domain cases. |
| 31 | * |
| 32 | * 2. It also breaks most of current drivers as the driver probe sequence |
| 33 | * behavior changed if removing ->power_on|off() callback and use |
| 34 | * ->start() and ->stop() instead. genpd_dev_pm_attach will only power |
| 35 | * up the domain and attach device, but will not call .start() which |
| 36 | * relies on device runtime pm. That means the device power is still |
| 37 | * not up before running driver probe function. For SCU enabled |
| 38 | * platforms, all device drivers accessing registers/clock without power |
| 39 | * domain enabled will trigger a HW access error. That means we need fix |
| 40 | * most drivers probe sequence with proper runtime pm. |
| 41 | * |
| 42 | * In summary, we need fix above two issue before being able to switch to |
| 43 | * the "single global power domain" way. |
| 44 | * |
| 45 | */ |
| 46 | |
| 47 | #include <dt-bindings/firmware/imx/rsrc.h> |
| 48 | #include <linux/firmware/imx/sci.h> |
| 49 | #include <linux/io.h> |
| 50 | #include <linux/module.h> |
| 51 | #include <linux/of.h> |
| 52 | #include <linux/of_address.h> |
| 53 | #include <linux/of_platform.h> |
| 54 | #include <linux/platform_device.h> |
| 55 | #include <linux/pm.h> |
| 56 | #include <linux/pm_domain.h> |
| 57 | #include <linux/slab.h> |
| 58 | |
| 59 | /* SCU Power Mode Protocol definition */ |
| 60 | struct imx_sc_msg_req_set_resource_power_mode { |
| 61 | struct imx_sc_rpc_msg hdr; |
| 62 | u16 resource; |
| 63 | u8 mode; |
| 64 | } __packed; |
| 65 | |
| 66 | #define IMX_SCU_PD_NAME_SIZE 20 |
| 67 | struct imx_sc_pm_domain { |
| 68 | struct generic_pm_domain pd; |
| 69 | char name[IMX_SCU_PD_NAME_SIZE]; |
| 70 | u32 rsrc; |
| 71 | }; |
| 72 | |
| 73 | struct imx_sc_pd_range { |
| 74 | char *name; |
| 75 | u32 rsrc; |
| 76 | u8 num; |
| 77 | |
| 78 | /* add domain index */ |
| 79 | bool postfix; |
| 80 | u8 start_from; |
| 81 | }; |
| 82 | |
| 83 | struct imx_sc_pd_soc { |
| 84 | const struct imx_sc_pd_range *pd_ranges; |
| 85 | u8 num_ranges; |
| 86 | }; |
| 87 | |
| 88 | static const struct imx_sc_pd_range imx8qxp_scu_pd_ranges[] = { |
| 89 | /* LSIO SS */ |
| 90 | { "pwm", IMX_SC_R_PWM_0, 8, true, 0 }, |
| 91 | { "gpio", IMX_SC_R_GPIO_0, 8, true, 0 }, |
| 92 | { "gpt", IMX_SC_R_GPT_0, 5, true, 0 }, |
| 93 | { "kpp", IMX_SC_R_KPP, 1, false, 0 }, |
| 94 | { "fspi", IMX_SC_R_FSPI_0, 2, true, 0 }, |
| 95 | { "mu_a", IMX_SC_R_MU_0A, 14, true, 0 }, |
| 96 | { "mu_b", IMX_SC_R_MU_13B, 1, true, 13 }, |
| 97 | |
| 98 | /* CONN SS */ |
| 99 | { "usb", IMX_SC_R_USB_0, 2, true, 0 }, |
| 100 | { "usb0phy", IMX_SC_R_USB_0_PHY, 1, false, 0 }, |
| 101 | { "usb2", IMX_SC_R_USB_2, 1, false, 0 }, |
| 102 | { "usb2phy", IMX_SC_R_USB_2_PHY, 1, false, 0 }, |
| 103 | { "sdhc", IMX_SC_R_SDHC_0, 3, true, 0 }, |
| 104 | { "enet", IMX_SC_R_ENET_0, 2, true, 0 }, |
| 105 | { "nand", IMX_SC_R_NAND, 1, false, 0 }, |
| 106 | { "mlb", IMX_SC_R_MLB_0, 1, true, 0 }, |
| 107 | |
| 108 | /* AUDIO SS */ |
| 109 | { "audio-pll0", IMX_SC_R_AUDIO_PLL_0, 1, false, 0 }, |
| 110 | { "audio-pll1", IMX_SC_R_AUDIO_PLL_1, 1, false, 0 }, |
| 111 | { "audio-clk-0", IMX_SC_R_AUDIO_CLK_0, 1, false, 0 }, |
| 112 | { "dma0-ch", IMX_SC_R_DMA_0_CH0, 16, true, 0 }, |
| 113 | { "dma1-ch", IMX_SC_R_DMA_1_CH0, 16, true, 0 }, |
| 114 | { "dma2-ch", IMX_SC_R_DMA_2_CH0, 5, true, 0 }, |
| 115 | { "asrc0", IMX_SC_R_ASRC_0, 1, false, 0 }, |
| 116 | { "asrc1", IMX_SC_R_ASRC_1, 1, false, 0 }, |
| 117 | { "esai0", IMX_SC_R_ESAI_0, 1, false, 0 }, |
| 118 | { "spdif0", IMX_SC_R_SPDIF_0, 1, false, 0 }, |
| 119 | { "sai", IMX_SC_R_SAI_0, 3, true, 0 }, |
| 120 | { "amix", IMX_SC_R_AMIX, 1, false, 0 }, |
| 121 | { "mqs0", IMX_SC_R_MQS_0, 1, false, 0 }, |
| 122 | { "dsp", IMX_SC_R_DSP, 1, false, 0 }, |
| 123 | { "dsp-ram", IMX_SC_R_DSP_RAM, 1, false, 0 }, |
| 124 | |
| 125 | /* DMA SS */ |
| 126 | { "can", IMX_SC_R_CAN_0, 3, true, 0 }, |
| 127 | { "ftm", IMX_SC_R_FTM_0, 2, true, 0 }, |
| 128 | { "lpi2c", IMX_SC_R_I2C_0, 4, true, 0 }, |
| 129 | { "adc", IMX_SC_R_ADC_0, 1, true, 0 }, |
| 130 | { "lcd", IMX_SC_R_LCD_0, 1, true, 0 }, |
| 131 | { "lcd0-pwm", IMX_SC_R_LCD_0_PWM_0, 1, true, 0 }, |
| 132 | { "lpuart", IMX_SC_R_UART_0, 4, true, 0 }, |
| 133 | { "lpspi", IMX_SC_R_SPI_0, 4, true, 0 }, |
| 134 | { "irqstr_dsp", IMX_SC_R_IRQSTR_DSP, 1, false, 0 }, |
| 135 | |
| 136 | /* VPU SS */ |
| 137 | { "vpu", IMX_SC_R_VPU, 1, false, 0 }, |
| 138 | { "vpu-pid", IMX_SC_R_VPU_PID0, 8, true, 0 }, |
| 139 | { "vpu-dec0", IMX_SC_R_VPU_DEC_0, 1, false, 0 }, |
| 140 | { "vpu-enc0", IMX_SC_R_VPU_ENC_0, 1, false, 0 }, |
| 141 | |
| 142 | /* GPU SS */ |
| 143 | { "gpu0-pid", IMX_SC_R_GPU_0_PID0, 4, true, 0 }, |
| 144 | |
| 145 | /* HSIO SS */ |
| 146 | { "pcie-b", IMX_SC_R_PCIE_B, 1, false, 0 }, |
| 147 | { "serdes-1", IMX_SC_R_SERDES_1, 1, false, 0 }, |
| 148 | { "hsio-gpio", IMX_SC_R_HSIO_GPIO, 1, false, 0 }, |
| 149 | |
| 150 | /* MIPI SS */ |
| 151 | { "mipi0", IMX_SC_R_MIPI_0, 1, false, 0 }, |
| 152 | { "mipi0-pwm0", IMX_SC_R_MIPI_0_PWM_0, 1, false, 0 }, |
| 153 | { "mipi0-i2c", IMX_SC_R_MIPI_0_I2C_0, 2, true, 0 }, |
| 154 | |
| 155 | /* LVDS SS */ |
| 156 | { "lvds0", IMX_SC_R_LVDS_0, 1, false, 0 }, |
| 157 | |
| 158 | /* DC SS */ |
| 159 | { "dc0", IMX_SC_R_DC_0, 1, false, 0 }, |
| 160 | { "dc0-pll", IMX_SC_R_DC_0_PLL_0, 2, true, 0 }, |
| 161 | }; |
| 162 | |
| 163 | static const struct imx_sc_pd_soc imx8qxp_scu_pd = { |
| 164 | .pd_ranges = imx8qxp_scu_pd_ranges, |
| 165 | .num_ranges = ARRAY_SIZE(imx8qxp_scu_pd_ranges), |
| 166 | }; |
| 167 | |
| 168 | static struct imx_sc_ipc *pm_ipc_handle; |
| 169 | |
| 170 | static inline struct imx_sc_pm_domain * |
| 171 | to_imx_sc_pd(struct generic_pm_domain *genpd) |
| 172 | { |
| 173 | return container_of(genpd, struct imx_sc_pm_domain, pd); |
| 174 | } |
| 175 | |
| 176 | static int imx_sc_pd_power(struct generic_pm_domain *domain, bool power_on) |
| 177 | { |
| 178 | struct imx_sc_msg_req_set_resource_power_mode msg; |
| 179 | struct imx_sc_rpc_msg *hdr = &msg.hdr; |
| 180 | struct imx_sc_pm_domain *pd; |
| 181 | int ret; |
| 182 | |
| 183 | pd = to_imx_sc_pd(domain); |
| 184 | |
| 185 | hdr->ver = IMX_SC_RPC_VERSION; |
| 186 | hdr->svc = IMX_SC_RPC_SVC_PM; |
| 187 | hdr->func = IMX_SC_PM_FUNC_SET_RESOURCE_POWER_MODE; |
| 188 | hdr->size = 2; |
| 189 | |
| 190 | msg.resource = pd->rsrc; |
| 191 | msg.mode = power_on ? IMX_SC_PM_PW_MODE_ON : IMX_SC_PM_PW_MODE_LP; |
| 192 | |
| 193 | ret = imx_scu_call_rpc(pm_ipc_handle, &msg, true); |
| 194 | if (ret) |
| 195 | dev_err(&domain->dev, "failed to power %s resource %d ret %d\n", |
| 196 | power_on ? "up" : "off", pd->rsrc, ret); |
| 197 | |
| 198 | return ret; |
| 199 | } |
| 200 | |
| 201 | static int imx_sc_pd_power_on(struct generic_pm_domain *domain) |
| 202 | { |
| 203 | return imx_sc_pd_power(domain, true); |
| 204 | } |
| 205 | |
| 206 | static int imx_sc_pd_power_off(struct generic_pm_domain *domain) |
| 207 | { |
| 208 | return imx_sc_pd_power(domain, false); |
| 209 | } |
| 210 | |
| 211 | static struct generic_pm_domain *imx_scu_pd_xlate(struct of_phandle_args *spec, |
| 212 | void *data) |
| 213 | { |
| 214 | struct generic_pm_domain *domain = ERR_PTR(-ENOENT); |
| 215 | struct genpd_onecell_data *pd_data = data; |
| 216 | unsigned int i; |
| 217 | |
| 218 | for (i = 0; i < pd_data->num_domains; i++) { |
| 219 | struct imx_sc_pm_domain *sc_pd; |
| 220 | |
| 221 | sc_pd = to_imx_sc_pd(pd_data->domains[i]); |
| 222 | if (sc_pd->rsrc == spec->args[0]) { |
| 223 | domain = &sc_pd->pd; |
| 224 | break; |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | return domain; |
| 229 | } |
| 230 | |
| 231 | static struct imx_sc_pm_domain * |
| 232 | imx_scu_add_pm_domain(struct device *dev, int idx, |
| 233 | const struct imx_sc_pd_range *pd_ranges) |
| 234 | { |
| 235 | struct imx_sc_pm_domain *sc_pd; |
| 236 | int ret; |
| 237 | |
| 238 | sc_pd = devm_kzalloc(dev, sizeof(*sc_pd), GFP_KERNEL); |
| 239 | if (!sc_pd) |
| 240 | return ERR_PTR(-ENOMEM); |
| 241 | |
| 242 | sc_pd->rsrc = pd_ranges->rsrc + idx; |
| 243 | sc_pd->pd.power_off = imx_sc_pd_power_off; |
| 244 | sc_pd->pd.power_on = imx_sc_pd_power_on; |
| 245 | |
| 246 | if (pd_ranges->postfix) |
| 247 | snprintf(sc_pd->name, sizeof(sc_pd->name), |
| 248 | "%s%i", pd_ranges->name, pd_ranges->start_from + idx); |
| 249 | else |
| 250 | snprintf(sc_pd->name, sizeof(sc_pd->name), |
| 251 | "%s", pd_ranges->name); |
| 252 | |
| 253 | sc_pd->pd.name = sc_pd->name; |
| 254 | |
| 255 | if (sc_pd->rsrc >= IMX_SC_R_LAST) { |
| 256 | dev_warn(dev, "invalid pd %s rsrc id %d found", |
| 257 | sc_pd->name, sc_pd->rsrc); |
| 258 | |
| 259 | devm_kfree(dev, sc_pd); |
| 260 | return NULL; |
| 261 | } |
| 262 | |
| 263 | ret = pm_genpd_init(&sc_pd->pd, NULL, true); |
| 264 | if (ret) { |
| 265 | dev_warn(dev, "failed to init pd %s rsrc id %d", |
| 266 | sc_pd->name, sc_pd->rsrc); |
| 267 | devm_kfree(dev, sc_pd); |
| 268 | return NULL; |
| 269 | } |
| 270 | |
| 271 | return sc_pd; |
| 272 | } |
| 273 | |
| 274 | static int imx_scu_init_pm_domains(struct device *dev, |
| 275 | const struct imx_sc_pd_soc *pd_soc) |
| 276 | { |
| 277 | const struct imx_sc_pd_range *pd_ranges = pd_soc->pd_ranges; |
| 278 | struct generic_pm_domain **domains; |
| 279 | struct genpd_onecell_data *pd_data; |
| 280 | struct imx_sc_pm_domain *sc_pd; |
| 281 | u32 count = 0; |
| 282 | int i, j; |
| 283 | |
| 284 | for (i = 0; i < pd_soc->num_ranges; i++) |
| 285 | count += pd_ranges[i].num; |
| 286 | |
| 287 | domains = devm_kcalloc(dev, count, sizeof(*domains), GFP_KERNEL); |
| 288 | if (!domains) |
| 289 | return -ENOMEM; |
| 290 | |
| 291 | pd_data = devm_kzalloc(dev, sizeof(*pd_data), GFP_KERNEL); |
| 292 | if (!pd_data) |
| 293 | return -ENOMEM; |
| 294 | |
| 295 | count = 0; |
| 296 | for (i = 0; i < pd_soc->num_ranges; i++) { |
| 297 | for (j = 0; j < pd_ranges[i].num; j++) { |
| 298 | sc_pd = imx_scu_add_pm_domain(dev, j, &pd_ranges[i]); |
| 299 | if (IS_ERR_OR_NULL(sc_pd)) |
| 300 | continue; |
| 301 | |
| 302 | domains[count++] = &sc_pd->pd; |
| 303 | dev_dbg(dev, "added power domain %s\n", sc_pd->pd.name); |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | pd_data->domains = domains; |
| 308 | pd_data->num_domains = count; |
| 309 | pd_data->xlate = imx_scu_pd_xlate; |
| 310 | |
| 311 | of_genpd_add_provider_onecell(dev->of_node, pd_data); |
| 312 | |
| 313 | return 0; |
| 314 | } |
| 315 | |
| 316 | static int imx_sc_pd_probe(struct platform_device *pdev) |
| 317 | { |
| 318 | const struct imx_sc_pd_soc *pd_soc; |
| 319 | int ret; |
| 320 | |
| 321 | ret = imx_scu_get_handle(&pm_ipc_handle); |
| 322 | if (ret) |
| 323 | return ret; |
| 324 | |
| 325 | pd_soc = of_device_get_match_data(&pdev->dev); |
| 326 | if (!pd_soc) |
| 327 | return -ENODEV; |
| 328 | |
| 329 | return imx_scu_init_pm_domains(&pdev->dev, pd_soc); |
| 330 | } |
| 331 | |
| 332 | static const struct of_device_id imx_sc_pd_match[] = { |
| 333 | { .compatible = "fsl,imx8qxp-scu-pd", &imx8qxp_scu_pd}, |
| 334 | { .compatible = "fsl,scu-pd", &imx8qxp_scu_pd}, |
| 335 | { /* sentinel */ } |
| 336 | }; |
| 337 | |
| 338 | static struct platform_driver imx_sc_pd_driver = { |
| 339 | .driver = { |
| 340 | .name = "imx-scu-pd", |
| 341 | .of_match_table = imx_sc_pd_match, |
| 342 | }, |
| 343 | .probe = imx_sc_pd_probe, |
| 344 | }; |
| 345 | builtin_platform_driver(imx_sc_pd_driver); |
| 346 | |
| 347 | MODULE_AUTHOR("Dong Aisheng <aisheng.dong@nxp.com>"); |
| 348 | MODULE_DESCRIPTION("IMX SCU Power Domain driver"); |
| 349 | MODULE_LICENSE("GPL v2"); |