| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * GXP local power management interface. |
| * |
| * Copyright (C) 2021 Google LLC |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/io.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/types.h> |
| |
| #include "gxp-bpm.h" |
| #include "gxp-config.h" |
| #include "gxp-doorbell.h" |
| #include "gxp-internal.h" |
| #include "gxp-lpm.h" |
| |
| #if IS_GXP_TEST |
| #define gxp_lpm_wait_until(ret, ...) ((ret) = true) |
| #else |
| #define gxp_lpm_wait_until(ret, lpm_state, condition) \ |
| do { \ |
| int i = 100000; \ |
| while (i) { \ |
| lpm_state = lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET) & \ |
| PSM_CURR_STATE_MASK; \ |
| if (condition) \ |
| break; \ |
| udelay(2 * GXP_TIME_DELAY_FACTOR); \ |
| i--; \ |
| } \ |
| ret = (i != 0); \ |
| } while (0) |
| #endif |
| |
| void gxp_lpm_enable_state(struct gxp_dev *gxp, enum gxp_lpm_psm psm, uint state) |
| { |
| /* PS0 should always be enabled */ |
| if (state == LPM_ACTIVE_STATE || state > LPM_PG_STATE) |
| return; |
| |
| /* Disable all low power states */ |
| lpm_write_32_psm(gxp, psm, PSM_REG_ENABLE_STATE1_OFFSET, 0x0); |
| lpm_write_32_psm(gxp, psm, PSM_REG_ENABLE_STATE2_OFFSET, 0x0); |
| lpm_write_32_psm(gxp, psm, PSM_REG_ENABLE_STATE3_OFFSET, 0x0); |
| |
| /* Enable the requested low power state */ |
| lpm_write_32_psm(gxp, psm, state, 0x1); |
| } |
| |
| bool gxp_lpm_is_initialized(struct gxp_dev *gxp, enum gxp_lpm_psm psm) |
| { |
| u32 status = lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET); |
| |
| /* |
| * state_valid bit goes active and stays high forever the first time you |
| * write the start register |
| */ |
| if (status & PSM_STATE_VALID_MASK) |
| return true; |
| |
| return false; |
| } |
| |
| bool gxp_lpm_is_powered(struct gxp_dev *gxp, enum gxp_lpm_psm psm) |
| { |
| u32 status = lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET); |
| u32 state; |
| |
| if (!(status & PSM_STATE_VALID_MASK)) |
| return false; |
| state = status & PSM_CURR_STATE_MASK; |
| return state == LPM_ACTIVE_STATE || state == LPM_CG_STATE; |
| } |
| |
| uint gxp_lpm_get_state(struct gxp_dev *gxp, enum gxp_lpm_psm psm) |
| { |
| u32 status = lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET); |
| |
| return status & PSM_CURR_STATE_MASK; |
| } |
| |
| static int set_state_internal(struct gxp_dev *gxp, enum gxp_lpm_psm psm, uint target_state) |
| { |
| u32 val; |
| int i = 10000; |
| |
| /* Set SW sequencing mode and PS target */ |
| val = LPM_SW_PSM_MODE; |
| val |= target_state << LPM_CFG_SW_PS_TARGET_OFFSET; |
| lpm_write_32_psm(gxp, psm, PSM_REG_CFG_OFFSET, val); |
| |
| /* Start the SW sequence */ |
| lpm_write_32_psm(gxp, psm, PSM_REG_START_OFFSET, 0x1); |
| |
| /* Wait for LPM init done (0x60041688) */ |
| while (i && !(lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET) |
| & PSM_INIT_DONE_MASK)) { |
| udelay(1 * GXP_TIME_DELAY_FACTOR); |
| i--; |
| } |
| |
| if (!i) { |
| dev_err(gxp->dev, "Failed to switch PSM%u to PS%u\n", psm, target_state); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| int gxp_lpm_set_state(struct gxp_dev *gxp, enum gxp_lpm_psm psm, uint target_state, |
| bool verbose) |
| { |
| uint curr_state = gxp_lpm_get_state(gxp, psm); |
| |
| if (curr_state == target_state) |
| return 0; |
| |
| if (verbose) |
| dev_warn(gxp->dev, |
| "Forcing a transition to PS%u on core%u, status: %x\n", |
| target_state, psm, |
| lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET)); |
| |
| gxp_lpm_enable_state(gxp, psm, target_state); |
| |
| if ((curr_state != LPM_ACTIVE_STATE) |
| && (target_state != LPM_ACTIVE_STATE)) { |
| /* Switch to PS0 before switching to a low power state. */ |
| set_state_internal(gxp, psm, LPM_ACTIVE_STATE); |
| } |
| |
| set_state_internal(gxp, psm, target_state); |
| |
| if (verbose) |
| dev_warn( |
| gxp->dev, |
| "Finished forced transition on core %u. target: PS%u, actual: PS%u, status: %x\n", |
| psm, target_state, gxp_lpm_get_state(gxp, psm), |
| lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET)); |
| |
| /* Set HW sequencing mode */ |
| lpm_write_32_psm(gxp, psm, PSM_REG_CFG_OFFSET, LPM_HW_MODE); |
| |
| return 0; |
| } |
| |
| static int psm_enable(struct gxp_dev *gxp, enum gxp_lpm_psm psm) |
| { |
| int i = 10000; |
| |
| /* Return early if LPM is already initialized */ |
| if (gxp_lpm_is_initialized(gxp, psm)) { |
| if (psm != LPM_PSM_TOP) { |
| /* Ensure core is in PS3 */ |
| return gxp_lpm_set_state(gxp, psm, LPM_PG_STATE, |
| /*verbose=*/true); |
| } |
| |
| return 0; |
| } |
| |
| /* Write PSM start bit */ |
| lpm_write_32_psm(gxp, psm, PSM_REG_START_OFFSET, PSM_START); |
| |
| /* Wait for LPM init done (0x60041688) */ |
| while (i && !(lpm_read_32_psm(gxp, psm, PSM_REG_STATUS_OFFSET) |
| & PSM_INIT_DONE_MASK)) { |
| udelay(1 * GXP_TIME_DELAY_FACTOR); |
| i--; |
| } |
| |
| if (!i) |
| return 1; |
| |
| /* Set PSM to HW mode (0x60041680) */ |
| lpm_write_32_psm(gxp, psm, PSM_REG_CFG_OFFSET, PSM_HW_MODE); |
| |
| return 0; |
| } |
| |
| void gxp_lpm_init(struct gxp_dev *gxp) |
| { |
| if (gxp->lpm_init) |
| gxp->lpm_init(gxp); |
| /* Enable Top PSM */ |
| if (psm_enable(gxp, LPM_PSM_TOP)) |
| dev_err(gxp->dev, "Timed out when enabling Top PSM!\n"); |
| } |
| |
| void gxp_lpm_destroy(struct gxp_dev *gxp) |
| { |
| /* (b/171063370) Put Top PSM in ACTIVE state before block shutdown */ |
| dev_dbg(gxp->dev, "Kicking Top PSM out of ACG\n"); |
| |
| /* Disable all low-power states for TOP */ |
| lpm_write_32_psm(gxp, LPM_PSM_TOP, PSM_REG_ENABLE_STATE1_OFFSET, 0x0); |
| lpm_write_32_psm(gxp, LPM_PSM_TOP, PSM_REG_ENABLE_STATE2_OFFSET, 0x0); |
| } |
| |
| int gxp_lpm_up(struct gxp_dev *gxp, uint core) |
| { |
| /* Clear wakeup doorbell */ |
| gxp_doorbell_clear(gxp, CORE_WAKEUP_DOORBELL(core)); |
| |
| /* Enable core PSM */ |
| if (psm_enable(gxp, CORE_TO_PSM(core))) { |
| dev_err(gxp->dev, "Timed out when enabling Core%u PSM!\n", |
| core); |
| return -ETIMEDOUT; |
| } |
| |
| /* Enable PS1 (Clk Gated). Only required for core PSMs. */ |
| if (core < GXP_NUM_CORES) |
| gxp_lpm_enable_state(gxp, CORE_TO_PSM(core), LPM_CG_STATE); |
| |
| gxp_bpm_start(gxp, core); |
| |
| return 0; |
| } |
| |
| void gxp_lpm_down(struct gxp_dev *gxp, uint core) |
| { |
| if (gxp_lpm_get_state(gxp, CORE_TO_PSM(core)) == LPM_PG_STATE) |
| return; |
| /* Enable PS3 (Pwr Gated) */ |
| gxp_lpm_enable_state(gxp, CORE_TO_PSM(core), LPM_PG_STATE); |
| |
| /* Set wakeup doorbell to trigger an automatic transition to PS3 */ |
| gxp_doorbell_enable_for_core(gxp, CORE_WAKEUP_DOORBELL(core), core); |
| gxp_doorbell_set(gxp, CORE_WAKEUP_DOORBELL(core)); |
| msleep(25 * GXP_TIME_DELAY_FACTOR); |
| |
| /* |
| * Clear the core's interrupt mask and the wakeup doorbell to ensure |
| * the core will not wake unexpectedly. |
| */ |
| gxp_write_32(gxp, GXP_CORE_REG_COMMON_INT_MASK_0(core), 0); |
| gxp_doorbell_clear(gxp, CORE_WAKEUP_DOORBELL(core)); |
| |
| /* Ensure core is in PS3 */ |
| gxp_lpm_set_state(gxp, CORE_TO_PSM(core), LPM_PG_STATE, /*verbose=*/true); |
| } |
| |
| bool gxp_lpm_wait_state_ne(struct gxp_dev *gxp, enum gxp_lpm_psm psm, uint state) |
| { |
| __maybe_unused uint lpm_state; |
| bool ret; |
| |
| gxp_lpm_wait_until(ret, lpm_state, lpm_state != state); |
| |
| return ret; |
| } |
| |
| bool gxp_lpm_wait_state_eq(struct gxp_dev *gxp, enum gxp_lpm_psm psm, uint state) |
| { |
| __maybe_unused uint lpm_state; |
| bool ret; |
| |
| gxp_lpm_wait_until(ret, lpm_state, lpm_state == state); |
| |
| return ret; |
| } |