From: Can Guo Date: Wed, 25 Mar 2026 15:21:49 +0000 (-0700) Subject: scsi: ufs: core: Add support to retrain TX Equalization via debugfs X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=adbabdcf0db0f929e642f95d7528dce0f6bd3a11;p=thirdparty%2Fkernel%2Fstable.git scsi: ufs: core: Add support to retrain TX Equalization via debugfs Drastic environmental changes, such as significant temperature shifts, can impact link signal integrity. In such cases, retraining TX Equalization is necessary to compensate for these environmental changes. Add a debugfs entry, 'tx_eq_ctrl', to allow userspace to manually trigger the TX Equalization training (EQTR) procedure and apply the identified optimal settings on the fly. These entries are created on a per-gear basis for High Speed Gear 4 (HS-G4) and above, as TX EQTR is not supported for lower gears. The 'tx_eq_ctrl' entry currently accepts the 'retrain' command to initiate the procedure. The interface is designed to be scalable to support additional commands in the future. Reading the 'tx_eq_ctrl' entry provides a usage hint to the user, ensuring the interface is self-documenting. The ufshcd's debugfs folder structure will look like below: /sys/kernel/debug/ufshcd/*ufs*/ |--tx_eq_hs_gear1/ | |--device_tx_eq_params | |--host_tx_eq_params |--tx_eq_hs_gear2/ |--tx_eq_hs_gear3/ |--tx_eq_hs_gear4/ |--tx_eq_hs_gear5/ |--tx_eq_hs_gear6/ |--device_tx_eq_params |--device_tx_eqtr_record |--host_tx_eq_params |--host_tx_eqtr_record |--tx_eq_ctrl Reviewed-by: Bean Huo Reviewed-by: Bart Van Assche Signed-off-by: Can Guo Reviewed-by: Peter Wang Link: https://patch.msgid.link/20260325152154.1604082-8-can.guo@oss.qualcomm.com Signed-off-by: Martin K. Petersen --- diff --git a/drivers/ufs/core/ufs-debugfs.c b/drivers/ufs/core/ufs-debugfs.c index 831758b45163..e3dd81d6fe82 100644 --- a/drivers/ufs/core/ufs-debugfs.c +++ b/drivers/ufs/core/ufs-debugfs.c @@ -401,9 +401,70 @@ static const struct file_operations ufs_tx_eqtr_record_fops = { .release = single_release, }; +static ssize_t ufs_tx_eq_ctrl_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + u32 gear = (u32)(uintptr_t)file->f_inode->i_private; + struct ufs_hba *hba = hba_from_file(file); + char kbuf[32]; + int ret; + + if (count >= sizeof(kbuf)) + return -EINVAL; + + if (copy_from_user(kbuf, buf, count)) + return -EFAULT; + + if (!ufshcd_is_tx_eq_supported(hba)) + return -EOPNOTSUPP; + + if (hba->ufshcd_state != UFSHCD_STATE_OPERATIONAL || + !hba->max_pwr_info.is_valid) + return -EBUSY; + + if (!hba->ufs_device_wlun) + return -ENODEV; + + kbuf[count] = '\0'; + + if (sysfs_streq(kbuf, "retrain")) { + ret = ufs_debugfs_get_user_access(hba); + if (ret) + return ret; + ret = ufshcd_retrain_tx_eq(hba, gear); + ufs_debugfs_put_user_access(hba); + } else { + /* Unknown operation */ + return -EINVAL; + } + + return ret ? ret : count; +} + +static int ufs_tx_eq_ctrl_show(struct seq_file *s, void *data) +{ + seq_puts(s, "write 'retrain' to retrain TX Equalization settings\n"); + return 0; +} + +static int ufs_tx_eq_ctrl_open(struct inode *inode, struct file *file) +{ + return single_open(file, ufs_tx_eq_ctrl_show, inode->i_private); +} + +static const struct file_operations ufs_tx_eq_ctrl_fops = { + .owner = THIS_MODULE, + .open = ufs_tx_eq_ctrl_open, + .read = seq_read, + .llseek = seq_lseek, + .write = ufs_tx_eq_ctrl_write, + .release = single_release, +}; + static const struct ufs_debugfs_attr ufs_tx_eqtr_attrs[] = { { "host_tx_eqtr_record", 0400, &ufs_tx_eqtr_record_fops }, { "device_tx_eqtr_record", 0400, &ufs_tx_eqtr_record_fops }, + { "tx_eq_ctrl", 0600, &ufs_tx_eq_ctrl_fops }, { } }; diff --git a/drivers/ufs/core/ufs-txeq.c b/drivers/ufs/core/ufs-txeq.c index b68a7af78290..3a879c644faa 100644 --- a/drivers/ufs/core/ufs-txeq.c +++ b/drivers/ufs/core/ufs-txeq.c @@ -628,9 +628,15 @@ static int ufshcd_setup_tx_eqtr_adapt_length(struct ufs_hba *hba, struct ufshcd_tx_eq_params *params, u32 gear) { + struct ufshcd_tx_eqtr_record *rec = params->eqtr_record; u32 adapt_eqtr; int ret; + if (rec && rec->saved_adapt_eqtr) { + adapt_eqtr = rec->saved_adapt_eqtr; + goto set_adapt_eqtr; + } + if (gear == UFS_HS_G4 || gear == UFS_HS_G5) { u64 t_adapt, t_adapt_local, t_adapt_peer; u32 adapt_cap_local, adapt_cap_peer, adapt_length; @@ -782,6 +788,10 @@ static int ufshcd_setup_tx_eqtr_adapt_length(struct ufs_hba *hba, return -EINVAL; } + if (rec) + rec->saved_adapt_eqtr = (u16)adapt_eqtr; + +set_adapt_eqtr: ret = ufshcd_dme_set(hba, UIC_ARG_MIB(PA_TXADAPTLENGTH_EQTR), adapt_eqtr); if (ret) dev_err(hba->dev, "Failed to set adapt length for TX EQTR: %d\n", ret); @@ -847,16 +857,33 @@ static int ufshcd_apply_tx_eqtr_settings(struct ufs_hba *hba, /** * ufshcd_update_tx_eq_params - Update TX Equalization params * @params: TX EQ parameters data structure + * @pwr_mode: target power mode containing gear and rate * @eqtr_data: TX EQTR data structure * - * Update TX Equalization params using results from TX EQTR data. + * Update TX Equalization params using results from TX EQTR data. Check also + * the TX EQTR FOM value for each TX lane in the TX EQTR data. If a TX lane got + * a FOM value of 0, restore the TX Equalization settings from the last known + * valid TX Equalization params for that specific TX lane. */ static inline void ufshcd_update_tx_eq_params(struct ufshcd_tx_eq_params *params, + struct ufs_pa_layer_attr *pwr_mode, struct ufshcd_tx_eqtr_data *eqtr_data) { struct ufshcd_tx_eqtr_record *rec = params->eqtr_record; + if (params->is_valid) { + int lane; + + for (lane = 0; lane < pwr_mode->lane_tx; lane++) + if (eqtr_data->host[lane].fom_val == 0) + eqtr_data->host[lane] = params->host[lane]; + + for (lane = 0; lane < pwr_mode->lane_rx; lane++) + if (eqtr_data->device[lane].fom_val == 0) + eqtr_data->device[lane] = params->device[lane]; + } + memcpy(params->host, eqtr_data->host, sizeof(params->host)); memcpy(params->device, eqtr_data->device, sizeof(params->device)); @@ -955,7 +982,7 @@ static int __ufshcd_tx_eqtr(struct ufs_hba *hba, dev_info(hba->dev, "TX EQTR procedure completed! Time elapsed: %llu ms\n", ktime_to_ms(ktime_sub(ktime_get(), start))); - ufshcd_update_tx_eq_params(params, eqtr_data); + ufshcd_update_tx_eq_params(params, pwr_mode, eqtr_data); return ret; } @@ -1079,6 +1106,7 @@ out: * ufshcd_config_tx_eq_settings - Configure TX Equalization settings * @hba: per adapter instance * @pwr_mode: target power mode containing gear and rate information + * @force_tx_eqtr: execute the TX EQTR procedure * * This function finds and sets the TX Equalization settings for the given * target power mode. @@ -1086,7 +1114,8 @@ out: * Returns 0 on success, error code otherwise */ int ufshcd_config_tx_eq_settings(struct ufs_hba *hba, - struct ufs_pa_layer_attr *pwr_mode) + struct ufs_pa_layer_attr *pwr_mode, + bool force_tx_eqtr) { struct ufshcd_tx_eq_params *params; u32 gear, rate; @@ -1123,7 +1152,7 @@ int ufshcd_config_tx_eq_settings(struct ufs_hba *hba, } params = &hba->tx_eq_params[gear - 1]; - if (!params->is_valid) { + if (!params->is_valid || force_tx_eqtr) { int ret; ret = ufshcd_tx_eqtr(hba, params, pwr_mode); @@ -1189,3 +1218,76 @@ void ufshcd_apply_valid_tx_eq_settings(struct ufs_hba *hba) } } } + +/** + * ufshcd_retrain_tx_eq - Retrain TX Equalization and apply new settings + * @hba: per-adapter instance + * @gear: target High-Speed (HS) gear for retraining + * + * This function initiates a refresh of the TX Equalization settings for a + * specific HS gear. It scales the clocks to maximum frequency, negotiates the + * power mode with the device, retrains TX EQ and applies new TX EQ settings + * by conducting a Power Mode change. + * + * Returns 0 on success, non-zero error code otherwise + */ +int ufshcd_retrain_tx_eq(struct ufs_hba *hba, u32 gear) +{ + struct ufs_pa_layer_attr new_pwr_info, final_params = {}; + int ret; + + if (!ufshcd_is_tx_eq_supported(hba) || !use_adaptive_txeq) + return -EOPNOTSUPP; + + if (gear < adaptive_txeq_gear) + return -ERANGE; + + ufshcd_hold(hba); + + ret = ufshcd_pause_command_processing(hba, 1 * USEC_PER_SEC); + if (ret) { + ufshcd_release(hba); + return ret; + } + + /* scale up clocks to max frequency before TX EQTR */ + if (ufshcd_is_clkscaling_supported(hba)) + ufshcd_scale_clks(hba, ULONG_MAX, true); + + new_pwr_info = hba->pwr_info; + new_pwr_info.gear_tx = gear; + new_pwr_info.gear_rx = gear; + + ret = ufshcd_vops_negotiate_pwr_mode(hba, &new_pwr_info, &final_params); + if (ret) + memcpy(&final_params, &new_pwr_info, sizeof(final_params)); + + if (final_params.gear_tx != gear) { + dev_err(hba->dev, "Negotiated Gear (%u) does not match target Gear (%u)\n", + final_params.gear_tx, gear); + ret = -EINVAL; + goto out; + } + + ret = ufshcd_config_tx_eq_settings(hba, &final_params, true); + if (ret) { + dev_err(hba->dev, "Failed to config TX Equalization for HS-G%u, Rate-%s: %d\n", + final_params.gear_tx, + ufs_hs_rate_to_str(final_params.hs_rate), ret); + goto out; + } + + /* Change Power Mode to apply the new TX EQ settings */ + ret = ufshcd_change_power_mode(hba, &final_params, + UFSHCD_PMC_POLICY_FORCE); + if (ret) + dev_err(hba->dev, "%s: Failed to change Power Mode to HS-G%u, Rate-%s: %d\n", + __func__, final_params.gear_tx, + ufs_hs_rate_to_str(final_params.hs_rate), ret); + +out: + ufshcd_resume_command_processing(hba); + ufshcd_release(hba); + + return ret; +} diff --git a/drivers/ufs/core/ufshcd-priv.h b/drivers/ufs/core/ufshcd-priv.h index 45904e5746b2..d296f00c099d 100644 --- a/drivers/ufs/core/ufshcd-priv.h +++ b/drivers/ufs/core/ufshcd-priv.h @@ -80,6 +80,7 @@ int ufshcd_try_to_abort_task(struct ufs_hba *hba, int tag); void ufshcd_release_scsi_cmd(struct ufs_hba *hba, struct scsi_cmnd *cmd); int ufshcd_pause_command_processing(struct ufs_hba *hba, u64 timeout_us); void ufshcd_resume_command_processing(struct ufs_hba *hba); +int ufshcd_scale_clks(struct ufs_hba *hba, unsigned long freq, bool scale_up); /** * enum ufs_descr_fmt - UFS string descriptor format @@ -108,10 +109,12 @@ int ufshcd_read_device_lvl_exception_id(struct ufs_hba *hba, u64 *exception_id); int ufshcd_uic_tx_eqtr(struct ufs_hba *hba, int gear); void ufshcd_apply_valid_tx_eq_settings(struct ufs_hba *hba); int ufshcd_config_tx_eq_settings(struct ufs_hba *hba, - struct ufs_pa_layer_attr *pwr_mode); + struct ufs_pa_layer_attr *pwr_mode, + bool force_tx_eqtr); void ufshcd_print_tx_eq_params(struct ufs_hba *hba); bool ufshcd_is_txeq_presets_used(struct ufs_hba *hba); bool ufshcd_is_txeq_preset_selected(u8 preshoot, u8 deemphasis); +int ufshcd_retrain_tx_eq(struct ufs_hba *hba, u32 gear); /* Wrapper functions for safely calling variant operations */ static inline const char *ufshcd_get_var_name(struct ufs_hba *hba) diff --git a/drivers/ufs/core/ufshcd.c b/drivers/ufs/core/ufshcd.c index 39e6d12e347a..2e8255b3d883 100644 --- a/drivers/ufs/core/ufshcd.c +++ b/drivers/ufs/core/ufshcd.c @@ -333,8 +333,6 @@ static inline void ufshcd_add_delay_before_dme_cmd(struct ufs_hba *hba); static int ufshcd_host_reset_and_restore(struct ufs_hba *hba); static void ufshcd_resume_clkscaling(struct ufs_hba *hba); static void ufshcd_suspend_clkscaling(struct ufs_hba *hba); -static int ufshcd_scale_clks(struct ufs_hba *hba, unsigned long freq, - bool scale_up); static irqreturn_t ufshcd_intr(int irq, void *__hba); static int ufshcd_setup_hba_vreg(struct ufs_hba *hba, bool on); static int ufshcd_setup_vreg(struct ufs_hba *hba, bool on); @@ -1209,8 +1207,7 @@ static int ufshcd_opp_set_rate(struct ufs_hba *hba, unsigned long freq) * * Return: 0 if successful; < 0 upon failure. */ -static int ufshcd_scale_clks(struct ufs_hba *hba, unsigned long freq, - bool scale_up) +int ufshcd_scale_clks(struct ufs_hba *hba, unsigned long freq, bool scale_up) { int ret = 0; ktime_t start = ktime_get(); @@ -4893,7 +4890,7 @@ int ufshcd_config_pwr_mode(struct ufs_hba *hba, memcpy(&final_params, desired_pwr_mode, sizeof(final_params)); } - ret = ufshcd_config_tx_eq_settings(hba, &final_params); + ret = ufshcd_config_tx_eq_settings(hba, &final_params, false); if (ret) dev_warn(hba->dev, "Failed to configure TX Equalization for HS-G%u, Rate-%s: %d\n", final_params.gear_tx, diff --git a/include/ufs/ufshcd.h b/include/ufs/ufshcd.h index 35b1288327d0..bc9e48e89db4 100644 --- a/include/ufs/ufshcd.h +++ b/include/ufs/ufshcd.h @@ -341,12 +341,14 @@ struct ufshcd_tx_eqtr_data { * @device_fom: Device TX EQTR FOM record * @last_record_ts: Timestamp of the most recent TX EQTR record * @last_record_index: Index of the most recent TX EQTR record + * @saved_adapt_eqtr: Saved Adaptation length setting for TX EQTR */ struct ufshcd_tx_eqtr_record { u8 host_fom[UFS_MAX_LANES][TX_HS_NUM_PRESHOOT][TX_HS_NUM_DEEMPHASIS]; u8 device_fom[UFS_MAX_LANES][TX_HS_NUM_PRESHOOT][TX_HS_NUM_DEEMPHASIS]; ktime_t last_record_ts; u16 last_record_index; + u16 saved_adapt_eqtr; }; /**