From: Subhash Jadavani <subhashj@xxxxxxxxxxxxxx> In order to save power we should put the UFS link into hibern8 as soon as UFS link is idle and power measurement of active usecases (like audio/video playback/recording) show that putting UFS link in hibern8 @ 10ms of idle (if not earlier) would save significant power. Our current available solution is to do hibern8 with clock gating @idle timeout of 150ms. As clock gating has huge latencies (7ms each in enter and exit), we cannot bring down the idle timeout to <=10ms without degrading UFS throughput. Hence this change has added support to enter into hibern8 with another idle timer. Signed-off-by: Subhash Jadavani <subhashj@xxxxxxxxxxxxxx> Signed-off-by: Can Guo <cang@xxxxxxxxxxxxxx> Signed-off-by: Asutosh Das <asutoshd@xxxxxxxxxxxxxx> --- drivers/scsi/ufs/ufshcd.c | 372 ++++++++++++++++++++++++++++++++++++++++----- drivers/scsi/ufs/ufshcd.h | 39 +++++ include/trace/events/ufs.h | 20 +++ 3 files changed, 397 insertions(+), 34 deletions(-) diff --git a/drivers/scsi/ufs/ufshcd.c b/drivers/scsi/ufs/ufshcd.c index e950204..8a56ef6 100644 --- a/drivers/scsi/ufs/ufshcd.c +++ b/drivers/scsi/ufs/ufshcd.c @@ -246,6 +246,8 @@ static int __ufshcd_setup_clocks(struct ufs_hba *hba, bool on, static irqreturn_t ufshcd_intr(int irq, void *__hba); static int ufshcd_change_power_mode(struct ufs_hba *hba, struct ufs_pa_layer_attr *pwr_mode); +static void ufshcd_hold_all(struct ufs_hba *hba); +static void ufshcd_release_all(struct ufs_hba *hba); static inline bool ufshcd_valid_tag(struct ufs_hba *hba, int tag) { return tag >= 0 && tag < hba->nutrs; @@ -853,6 +855,18 @@ static inline bool ufshcd_is_hba_active(struct ufs_hba *hba) ? false : true; } +static const char *ufshcd_hibern8_on_idle_state_to_string( + enum ufshcd_hibern8_on_idle_state state) +{ + switch (state) { + case HIBERN8_ENTERED: return "HIBERN8_ENTERED"; + case HIBERN8_EXITED: return "HIBERN8_EXITED"; + case REQ_HIBERN8_ENTER: return "REQ_HIBERN8_ENTER"; + case REQ_HIBERN8_EXIT: return "REQ_HIBERN8_EXIT"; + default: return "UNKNOWN_STATE"; + } +} + u32 ufshcd_get_local_unipro_ver(struct ufs_hba *hba) { /* HCI version 1.0 and 1.1 supports UniPro 1.41 */ @@ -993,7 +1007,7 @@ static int ufshcd_wait_for_doorbell_clr(struct ufs_hba *hba, bool timeout = false, do_last_check = false; ktime_t start; - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); spin_lock_irqsave(hba->host->host_lock, flags); /* * Wait for all the outstanding tasks/transfer requests. @@ -1038,7 +1052,7 @@ static int ufshcd_wait_for_doorbell_clr(struct ufs_hba *hba, } out: spin_unlock_irqrestore(hba->host->host_lock, flags); - ufshcd_release(hba); + ufshcd_release_all(hba); return ret; } @@ -1601,8 +1615,16 @@ static void ufshcd_gate_work(struct work_struct *work) spin_unlock_irqrestore(hba->host->host_lock, flags); + if (ufshcd_is_hibern8_on_idle_allowed(hba)) + /* + * Hibern8 enter work (on Idle) needs clocks to be ON hence + * make sure that it is flushed before turning off the clocks. + */ + flush_delayed_work(&hba->hibern8_on_idle.enter_work); + /* put the link into hibern8 mode before turning off clocks */ - if (ufshcd_can_hibern8_during_gating(hba)) { + if (ufshcd_can_hibern8_during_gating(hba) && + ufshcd_is_link_active(hba)) { if (ufshcd_uic_hibern8_enter(hba)) { hba->clk_gating.state = CLKS_ON; trace_ufshcd_clk_gating(dev_name(hba->dev), @@ -1732,6 +1754,8 @@ static void ufshcd_init_clk_gating(struct ufs_hba *hba) { char wq_name[sizeof("ufs_clk_gating_00")]; + hba->clk_gating.state = CLKS_ON; + if (!ufshcd_is_clkgating_allowed(hba)) return; @@ -1774,6 +1798,246 @@ static void ufshcd_exit_clk_gating(struct ufs_hba *hba) destroy_workqueue(hba->clk_gating.clk_gating_workq); } +/** + * ufshcd_hibern8_hold - Make sure that link is not in hibern8. + * + * @hba: per adapter instance + * @async: This indicates whether caller wants to exit hibern8 asynchronously. + * + * Exit from hibern8 mode and set the link as active. + * + * Return 0 on success, non-zero on failure. + */ +int ufshcd_hibern8_hold(struct ufs_hba *hba, bool async) +{ + int rc = 0; + unsigned long flags; + + if (!ufshcd_is_hibern8_on_idle_allowed(hba)) + goto out; + + spin_lock_irqsave(hba->host->host_lock, flags); + hba->hibern8_on_idle.active_reqs++; + +start: + switch (hba->hibern8_on_idle.state) { + case HIBERN8_EXITED: + break; + case REQ_HIBERN8_ENTER: + if (cancel_delayed_work(&hba->hibern8_on_idle.enter_work)) { + hba->hibern8_on_idle.state = HIBERN8_EXITED; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + break; + } + /* + * If we here, it means Hibern8 enter work is either done or + * currently running. Hence, fall through to cancel hibern8 + * work and exit hibern8. + */ + case HIBERN8_ENTERED: + scsi_block_requests(hba->host); + hba->hibern8_on_idle.state = REQ_HIBERN8_EXIT; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + schedule_work(&hba->hibern8_on_idle.exit_work); + /* + * fall through to check if we should wait for this + * work to be done or not. + */ + case REQ_HIBERN8_EXIT: + if (async) { + rc = -EAGAIN; + hba->hibern8_on_idle.active_reqs--; + break; + } + spin_unlock_irqrestore(hba->host->host_lock, flags); + flush_work(&hba->hibern8_on_idle.exit_work); + /* Make sure state is HIBERN8_EXITED before returning */ + spin_lock_irqsave(hba->host->host_lock, flags); + goto start; + + default: + dev_err(hba->dev, "%s: H8 is in invalid state %d\n", + __func__, hba->hibern8_on_idle.state); + break; + } + spin_unlock_irqrestore(hba->host->host_lock, flags); +out: + return rc; +} + +/* host lock must be held before calling this variant */ +static void __ufshcd_hibern8_release(struct ufs_hba *hba) +{ + unsigned long delay_in_jiffies; + + if (!ufshcd_is_hibern8_on_idle_allowed(hba)) + return; + + hba->hibern8_on_idle.active_reqs--; + WARN_ON(hba->hibern8_on_idle.active_reqs < 0); + + if (hba->hibern8_on_idle.active_reqs + || hba->hibern8_on_idle.is_suspended + || hba->ufshcd_state != UFSHCD_STATE_OPERATIONAL + || hba->lrb_in_use || hba->outstanding_tasks + || hba->active_uic_cmd || hba->uic_async_done) + return; + + hba->hibern8_on_idle.state = REQ_HIBERN8_ENTER; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + /* + * Scheduling the delayed work after 1 jiffies will make the work to + * get schedule any time from 0ms to 1000/HZ ms which is not desirable + * for hibern8 enter work as it may impact the performance if it gets + * scheduled almost immediately. Hence make sure that hibern8 enter + * work gets scheduled atleast after 2 jiffies (any time between + * 1000/HZ ms to 2000/HZ ms). + */ + delay_in_jiffies = msecs_to_jiffies(hba->hibern8_on_idle.delay_ms); + if (delay_in_jiffies == 1) + delay_in_jiffies++; + + schedule_delayed_work(&hba->hibern8_on_idle.enter_work, + delay_in_jiffies); +} + +void ufshcd_hibern8_release(struct ufs_hba *hba) +{ + unsigned long flags; + + spin_lock_irqsave(hba->host->host_lock, flags); + __ufshcd_hibern8_release(hba); + spin_unlock_irqrestore(hba->host->host_lock, flags); +} + +static void ufshcd_hibern8_enter_work(struct work_struct *work) +{ + struct ufs_hba *hba = container_of(work, struct ufs_hba, + hibern8_on_idle.enter_work.work); + unsigned long flags; + + spin_lock_irqsave(hba->host->host_lock, flags); + if (hba->hibern8_on_idle.is_suspended) { + hba->hibern8_on_idle.state = HIBERN8_EXITED; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + goto rel_lock; + } + + if (hba->hibern8_on_idle.active_reqs + || hba->ufshcd_state != UFSHCD_STATE_OPERATIONAL + || hba->lrb_in_use || hba->outstanding_tasks + || hba->active_uic_cmd || hba->uic_async_done) + goto rel_lock; + + spin_unlock_irqrestore(hba->host->host_lock, flags); + + if (ufshcd_is_link_active(hba) && ufshcd_uic_hibern8_enter(hba)) { + /* Enter failed */ + hba->hibern8_on_idle.state = HIBERN8_EXITED; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + goto out; + } + ufshcd_set_link_hibern8(hba); + + /* + * In case you are here to cancel this work the hibern8_on_idle.state + * would be marked as REQ_HIBERN8_EXIT. In this case keep the state + * as REQ_HIBERN8_EXIT which would anyway imply that we are in hibern8 + * and a request to exit from it is pending. By doing this way, + * we keep the state machine in tact and this would ultimately + * prevent from doing cancel work multiple times when there are + * new requests arriving before the current cancel work is done. + */ + spin_lock_irqsave(hba->host->host_lock, flags); + if (hba->hibern8_on_idle.state == REQ_HIBERN8_ENTER) { + hba->hibern8_on_idle.state = HIBERN8_ENTERED; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + } +rel_lock: + spin_unlock_irqrestore(hba->host->host_lock, flags); +out: + return; +} + +static void ufshcd_hibern8_exit_work(struct work_struct *work) +{ + int ret; + unsigned long flags; + struct ufs_hba *hba = container_of(work, struct ufs_hba, + hibern8_on_idle.exit_work); + + cancel_delayed_work_sync(&hba->hibern8_on_idle.enter_work); + + spin_lock_irqsave(hba->host->host_lock, flags); + if ((hba->hibern8_on_idle.state == HIBERN8_EXITED) + || ufshcd_is_link_active(hba)) { + hba->hibern8_on_idle.state = HIBERN8_EXITED; + spin_unlock_irqrestore(hba->host->host_lock, flags); + goto unblock_reqs; + } + spin_unlock_irqrestore(hba->host->host_lock, flags); + + /* Exit from hibern8 */ + if (ufshcd_is_link_hibern8(hba)) { + ret = ufshcd_uic_hibern8_exit(hba); + if (!ret) { + spin_lock_irqsave(hba->host->host_lock, flags); + ufshcd_set_link_active(hba); + hba->hibern8_on_idle.state = HIBERN8_EXITED; + trace_ufshcd_hibern8_on_idle(dev_name(hba->dev), + ufshcd_hibern8_on_idle_state_to_string( + hba->hibern8_on_idle.state)); + spin_unlock_irqrestore(hba->host->host_lock, flags); + } + } +unblock_reqs: + scsi_unblock_requests(hba->host); +} + +static void ufshcd_init_hibern8_on_idle(struct ufs_hba *hba) +{ + if (!ufshcd_is_hibern8_on_idle_allowed(hba)) + return; + + INIT_DELAYED_WORK(&hba->hibern8_on_idle.enter_work, + ufshcd_hibern8_enter_work); + INIT_WORK(&hba->hibern8_on_idle.exit_work, ufshcd_hibern8_exit_work); + + hba->hibern8_on_idle.delay_ms = 10; + hba->hibern8_on_idle.state = HIBERN8_EXITED; +} + +static void ufshcd_exit_hibern8_on_idle(struct ufs_hba *hba) +{ + if (!ufshcd_is_hibern8_on_idle_allowed(hba)) + return; + /* Don't have anything to do for now */ +} + +static void ufshcd_hold_all(struct ufs_hba *hba) +{ + ufshcd_hold(hba, false); + ufshcd_hibern8_hold(hba, false); +} + +static void ufshcd_release_all(struct ufs_hba *hba) +{ + ufshcd_hibern8_release(hba); + ufshcd_release(hba); +} + /* Must be called with host lock acquired */ static void ufshcd_clk_scaling_start_busy(struct ufs_hba *hba) { @@ -2026,7 +2290,7 @@ static inline u8 ufshcd_get_upmcrs(struct ufs_hba *hba) int ret; unsigned long flags; - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); mutex_lock(&hba->uic_cmd_mutex); ufshcd_add_delay_before_dme_cmd(hba); @@ -2038,7 +2302,7 @@ static inline u8 ufshcd_get_upmcrs(struct ufs_hba *hba) mutex_unlock(&hba->uic_cmd_mutex); - ufshcd_release(hba); + ufshcd_release_all(hba); return ret; } @@ -2410,7 +2674,18 @@ static int ufshcd_queuecommand(struct Scsi_Host *host, struct scsi_cmnd *cmd) clear_bit_unlock(tag, &hba->lrb_in_use); goto out; } - WARN_ON(hba->clk_gating.state != CLKS_ON); + if (ufshcd_is_clkgating_allowed(hba)) + WARN_ON(hba->clk_gating.state != CLKS_ON); + + err = ufshcd_hibern8_hold(hba, true); + if (err) { + clear_bit_unlock(tag, &hba->lrb_in_use); + err = SCSI_MLQUEUE_HOST_BUSY; + ufshcd_release(hba); + goto out; + } + if (ufshcd_is_hibern8_on_idle_allowed(hba)) + WARN_ON(hba->hibern8_on_idle.state != HIBERN8_EXITED); lrbp = &hba->lrb[tag]; @@ -2731,7 +3006,7 @@ int ufshcd_query_flag(struct ufs_hba *hba, enum query_opcode opcode, BUG_ON(!hba); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); mutex_lock(&hba->dev_cmd.lock); ufshcd_init_query(hba, &request, &response, opcode, idn, index, selector); @@ -2775,7 +3050,7 @@ int ufshcd_query_flag(struct ufs_hba *hba, enum query_opcode opcode, out_unlock: mutex_unlock(&hba->dev_cmd.lock); - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -2799,7 +3074,7 @@ int ufshcd_query_attr(struct ufs_hba *hba, enum query_opcode opcode, BUG_ON(!hba); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); if (!attr_val) { dev_err(hba->dev, "%s: attribute value required for opcode 0x%x\n", __func__, opcode); @@ -2839,7 +3114,7 @@ int ufshcd_query_attr(struct ufs_hba *hba, enum query_opcode opcode, out_unlock: mutex_unlock(&hba->dev_cmd.lock); out: - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -2890,7 +3165,7 @@ static int __ufshcd_query_descriptor(struct ufs_hba *hba, BUG_ON(!hba); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); if (!desc_buf) { dev_err(hba->dev, "%s: descriptor buffer required for opcode 0x%x\n", __func__, opcode); @@ -2940,7 +3215,7 @@ static int __ufshcd_query_descriptor(struct ufs_hba *hba, out_unlock: mutex_unlock(&hba->dev_cmd.lock); out: - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -3751,10 +4026,9 @@ static int ufshcd_uic_change_pwr_mode(struct ufs_hba *hba, u8 mode) uic_cmd.command = UIC_CMD_DME_SET; uic_cmd.argument1 = UIC_ARG_MIB(PA_PWRMODE); uic_cmd.argument3 = mode; - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); ret = ufshcd_uic_pwr_ctrl(hba, &uic_cmd); - ufshcd_release(hba); - + ufshcd_release_all(hba); out: return ret; } @@ -4368,7 +4642,7 @@ static int ufshcd_verify_dev_init(struct ufs_hba *hba) int err = 0; int retries; - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); mutex_lock(&hba->dev_cmd.lock); for (retries = NOP_OUT_RETRIES; retries > 0; retries--) { err = ufshcd_exec_dev_cmd(hba, DEV_CMD_TYPE_NOP, @@ -4380,7 +4654,7 @@ static int ufshcd_verify_dev_init(struct ufs_hba *hba) dev_dbg(hba->dev, "%s: error %d retrying\n", __func__, err); } mutex_unlock(&hba->dev_cmd.lock); - ufshcd_release(hba); + ufshcd_release_all(hba); if (err) dev_err(hba->dev, "%s: NOP OUT failed %d\n", __func__, err); @@ -4777,6 +5051,7 @@ static void __ufshcd_transfer_req_compl(struct ufs_hba *hba, /* Do not touch lrbp after scsi done */ cmd->scsi_done(cmd); __ufshcd_release(hba); + __ufshcd_hibern8_release(hba); } else if (lrbp->command_type == UTP_CMD_TYPE_DEV_MANAGE || lrbp->command_type == UTP_CMD_TYPE_UFS_STORAGE) { if (hba->dev_cmd.complete) { @@ -5224,7 +5499,7 @@ static void ufshcd_err_handler(struct work_struct *work) hba = container_of(work, struct ufs_hba, eh_work); pm_runtime_get_sync(hba->dev); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); spin_lock_irqsave(hba->host->host_lock, flags); if (hba->ufshcd_state == UFSHCD_STATE_RESET) @@ -5334,7 +5609,7 @@ static void ufshcd_err_handler(struct work_struct *work) out: spin_unlock_irqrestore(hba->host->host_lock, flags); ufshcd_scsi_unblock_requests(hba); - ufshcd_release(hba); + ufshcd_release_all(hba); pm_runtime_put_sync(hba->dev); } @@ -5590,7 +5865,7 @@ static int ufshcd_issue_tm_cmd(struct ufs_hba *hba, int lun_id, int task_id, * the maximum wait time is bounded by %TM_CMD_TIMEOUT. */ wait_event(hba->tm_tag_wq, ufshcd_get_tm_free_slot(hba, &free_slot)); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); spin_lock_irqsave(host->host_lock, flags); task_req_descp = hba->utmrdl_base_addr; @@ -5650,7 +5925,7 @@ static int ufshcd_issue_tm_cmd(struct ufs_hba *hba, int lun_id, int task_id, ufshcd_put_tm_slot(hba, free_slot); wake_up(&hba->tm_tag_wq); - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -5763,7 +6038,7 @@ static int ufshcd_abort(struct scsi_cmnd *cmd) if (lrbp->lun == UFS_UPIU_UFS_DEVICE_WLUN) return ufshcd_eh_host_reset_handler(cmd); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); reg = ufshcd_readl(hba, REG_UTP_TRANSFER_REQ_DOOR_BELL); /* If command is already aborted/completed, return SUCCESS */ if (!(test_bit(tag, &hba->outstanding_reqs))) { @@ -5884,10 +6159,10 @@ static int ufshcd_abort(struct scsi_cmnd *cmd) } /* - * This ufshcd_release() corresponds to the original scsi cmd that got - * aborted here (as we won't get any IRQ for it). + * This ufshcd_release_all() corresponds to the original scsi cmd that + * got aborted here (as we won't get any IRQ for it). */ - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -5975,7 +6250,7 @@ static int ufshcd_eh_host_reset_handler(struct scsi_cmnd *cmd) hba = shost_priv(cmd->device->host); - ufshcd_hold(hba, false); + ufshcd_hold_all(hba); /* * Check if there is any race with fatal error handling. * If so, wait for it to complete. Even though fatal error @@ -6010,7 +6285,7 @@ static int ufshcd_eh_host_reset_handler(struct scsi_cmnd *cmd) ufshcd_clear_eh_in_progress(hba); spin_unlock_irqrestore(hba->host->host_lock, flags); - ufshcd_release(hba); + ufshcd_release_all(hba); return err; } @@ -6644,7 +6919,13 @@ static void ufshcd_async_scan(void *data, async_cookie_t cookie) { struct ufs_hba *hba = (struct ufs_hba *)data; + /* + * Don't allow clock gating and hibern8 enter for faster device + * detection. + */ + ufshcd_hold_all(hba); ufshcd_probe_hba(hba); + ufshcd_release_all(hba); } static enum blk_eh_timer_return ufshcd_eh_timed_out(struct scsi_cmnd *scmd) @@ -7439,8 +7720,10 @@ static int ufshcd_suspend(struct ufs_hba *hba, enum ufs_pm_op pm_op) * If we can't transition into any of the low power modes * just gate the clocks. */ - ufshcd_hold(hba, false); + WARN_ON(hba->hibern8_on_idle.active_reqs); + ufshcd_hold_all(hba); hba->clk_gating.is_suspended = true; + hba->hibern8_on_idle.is_suspended = true; if (hba->clk_scaling.is_allowed) { cancel_work_sync(&hba->clk_scaling.suspend_work); @@ -7493,6 +7776,10 @@ static int ufshcd_suspend(struct ufs_hba *hba, enum ufs_pm_op pm_op) if (ret) goto set_dev_active; + if (ufshcd_is_link_hibern8(hba) && + ufshcd_is_hibern8_on_idle_allowed(hba)) + hba->hibern8_on_idle.state = HIBERN8_ENTERED; + ufshcd_vreg_set_lpm(hba); disable_clks: @@ -7511,8 +7798,11 @@ static int ufshcd_suspend(struct ufs_hba *hba, enum ufs_pm_op pm_op) /* If link is active, device ref_clk can't be switched off */ __ufshcd_setup_clocks(hba, false, true); - hba->clk_gating.state = CLKS_OFF; - trace_ufshcd_clk_gating(dev_name(hba->dev), hba->clk_gating.state); + if (ufshcd_is_clkgating_allowed(hba)) { + hba->clk_gating.state = CLKS_OFF; + trace_ufshcd_clk_gating(dev_name(hba->dev), + hba->clk_gating.state); + } /* * Disable the host irq as host controller as there won't be any * host controller transaction expected till resume. @@ -7534,10 +7824,11 @@ static int ufshcd_suspend(struct ufs_hba *hba, enum ufs_pm_op pm_op) if (!ufshcd_set_dev_pwr_mode(hba, UFS_ACTIVE_PWR_MODE)) ufshcd_disable_auto_bkops(hba); enable_gating: + hba->hibern8_on_idle.is_suspended = false; if (hba->clk_scaling.is_allowed) ufshcd_resume_clkscaling(hba); hba->clk_gating.is_suspended = false; - ufshcd_release(hba); + ufshcd_release_all(hba); out: hba->pm_op_in_progress = 0; return ret; @@ -7587,10 +7878,13 @@ static int ufshcd_resume(struct ufs_hba *hba, enum ufs_pm_op pm_op) if (ufshcd_is_link_hibern8(hba)) { ret = ufshcd_uic_hibern8_exit(hba); - if (!ret) + if (!ret) { ufshcd_set_link_active(hba); - else + if (ufshcd_is_hibern8_on_idle_allowed(hba)) + hba->hibern8_on_idle.state = HIBERN8_EXITED; + } else { goto vendor_suspend; + } } else if (ufshcd_is_link_off(hba)) { ret = ufshcd_host_reset_and_restore(hba); /* @@ -7599,6 +7893,8 @@ static int ufshcd_resume(struct ufs_hba *hba, enum ufs_pm_op pm_op) */ if (ret || !ufshcd_is_link_active(hba)) goto vendor_suspend; + if (ufshcd_is_hibern8_on_idle_allowed(hba)) + hba->hibern8_on_idle.state = HIBERN8_EXITED; } if (!ufshcd_is_ufs_dev_active(hba)) { @@ -7617,12 +7913,13 @@ static int ufshcd_resume(struct ufs_hba *hba, enum ufs_pm_op pm_op) ufshcd_urgent_bkops(hba); hba->clk_gating.is_suspended = false; + hba->hibern8_on_idle.is_suspended = false; if (hba->clk_scaling.is_allowed) ufshcd_resume_clkscaling(hba); /* Schedule clock gating in case of no access to UFS device yet */ - ufshcd_release(hba); + ufshcd_release_all(hba); /* Enable Auto-Hibernate if configured */ ufshcd_auto_hibern8_enable(hba); @@ -7631,6 +7928,9 @@ static int ufshcd_resume(struct ufs_hba *hba, enum ufs_pm_op pm_op) set_old_link_state: ufshcd_link_state_transition(hba, old_link_state, 0); + if (ufshcd_is_link_hibern8(hba) && + ufshcd_is_hibern8_on_idle_allowed(hba)) + hba->hibern8_on_idle.state = HIBERN8_ENTERED; vendor_suspend: ufshcd_vops_suspend(hba, pm_op); disable_vreg: @@ -7640,6 +7940,8 @@ static int ufshcd_resume(struct ufs_hba *hba, enum ufs_pm_op pm_op) if (hba->clk_scaling.is_allowed) ufshcd_suspend_clkscaling(hba); ufshcd_setup_clocks(hba, false); + if (ufshcd_is_clkgating_allowed(hba)) + hba->clk_gating.state = CLKS_OFF; out: hba->pm_op_in_progress = 0; return ret; @@ -7842,6 +8144,7 @@ void ufshcd_remove(struct ufs_hba *hba) ufshcd_hba_stop(hba, true); ufshcd_exit_clk_gating(hba); + ufshcd_exit_hibern8_on_idle(hba); if (ufshcd_is_clkscaling_supported(hba)) device_remove_file(hba->dev, &hba->clk_scaling.enable_attr); ufshcd_hba_exit(hba); @@ -8004,6 +8307,7 @@ int ufshcd_init(struct ufs_hba *hba, void __iomem *mmio_base, unsigned int irq) init_waitqueue_head(&hba->dev_cmd.tag_wq); ufshcd_init_clk_gating(hba); + ufshcd_init_hibern8_on_idle(hba); /* * In order to avoid any spurious interrupt immediately after diff --git a/drivers/scsi/ufs/ufshcd.h b/drivers/scsi/ufs/ufshcd.h index a2e1d5c..eaccc76 100644 --- a/drivers/scsi/ufs/ufshcd.h +++ b/drivers/scsi/ufs/ufshcd.h @@ -370,6 +370,35 @@ struct ufs_saved_pwr_info { bool is_valid; }; +/* Hibern8 state */ +enum ufshcd_hibern8_on_idle_state { + HIBERN8_ENTERED, + HIBERN8_EXITED, + REQ_HIBERN8_ENTER, + REQ_HIBERN8_EXIT, +}; + +/** + * struct ufs_hibern8_on_idle - UFS Hibern8 on idle related data + * @enter_work: worker to put UFS link in hibern8 after some delay as + * specified in delay_ms + * @exit_work: worker to bring UFS link out of hibern8 + * @state: the current hibern8 state + * @delay_ms: hibern8 enter delay in ms + * @is_suspended: hibern8 enter is suspended when set to 1 which can be used + * during suspend/resume + * @active_reqs: number of requests that are pending and should be waited for + * completion before scheduling delayed "enter_work". + */ +struct ufs_hibern8_on_idle { + struct delayed_work enter_work; + struct work_struct exit_work; + enum ufshcd_hibern8_on_idle_state state; + unsigned long delay_ms; + bool is_suspended; + int active_reqs; +}; + /** * struct ufs_clk_scaling - UFS clock scaling related data * @active_reqs: number of requests that are pending. If this is zero when @@ -496,6 +525,7 @@ struct ufs_stats { * @clk_list_head: UFS host controller clocks list node head * @pwr_info: holds current power mode * @max_pwr_info: keeps the device max valid pwm + * @hibern8_on_idle: UFS Hibern8 on idle related data * @desc_size: descriptor sizes reported by device * @urgent_bkops_lvl: keeps track of urgent bkops level for device * @is_urgent_bkops_lvl_checked: keeps track if the urgent bkops level for @@ -670,6 +700,8 @@ struct ufs_hba { struct ufs_pwr_mode_info max_pwr_info; struct ufs_clk_gating clk_gating; + struct ufs_hibern8_on_idle hibern8_on_idle; + /* Control to enable/disable host capabilities */ u32 caps; /* Allow dynamic clk gating */ @@ -686,6 +718,9 @@ struct ufs_hba { * CAUTION: Enabling this might reduce overall UFS throughput. */ #define UFSHCD_CAP_INTR_AGGR (1 << 4) + /* Allow standalone Hibern8 enter on idle */ +#define UFSHCD_CAP_HIBERN8_ENTER_ON_IDLE (1 << 5) + /* * This capability allows the device auto-bkops to be always enabled * except during suspend (both runtime and suspend). @@ -724,6 +759,10 @@ static inline bool ufshcd_can_autobkops_during_suspend(struct ufs_hba *hba) { return hba->caps & UFSHCD_CAP_AUTO_BKOPS_SUSPEND; } +static inline bool ufshcd_is_hibern8_on_idle_allowed(struct ufs_hba *hba) +{ + return hba->caps & UFSHCD_CAP_HIBERN8_ENTER_ON_IDLE; +} static inline bool ufshcd_is_intr_aggr_allowed(struct ufs_hba *hba) { diff --git a/include/trace/events/ufs.h b/include/trace/events/ufs.h index bf6f826..66ec728 100644 --- a/include/trace/events/ufs.h +++ b/include/trace/events/ufs.h @@ -75,6 +75,26 @@ __print_symbolic(__entry->state, UFSCHD_CLK_GATING_STATES)) ); +TRACE_EVENT(ufshcd_hibern8_on_idle, + + TP_PROTO(const char *dev_name, const char *state), + + TP_ARGS(dev_name, state), + + TP_STRUCT__entry( + __string(dev_name, dev_name) + __string(state, state) + ), + + TP_fast_assign( + __assign_str(dev_name, dev_name); + __assign_str(state, state); + ), + + TP_printk("%s: state changed to %s", + __get_str(dev_name), __get_str(state)) +); + TRACE_EVENT(ufshcd_clk_scaling, TP_PROTO(const char *dev_name, const char *state, const char *clk, -- Qualcomm India Private Limited, on behalf of Qualcomm Innovation Center, Inc. Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, a Linux Foundation Collaborative Project. -- To unsubscribe from this list: send the line "unsubscribe linux-arm-msm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html