]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
scsi: ufs: core: Add support to retrain TX Equalization via debugfs
authorCan Guo <can.guo@oss.qualcomm.com>
Wed, 25 Mar 2026 15:21:49 +0000 (08:21 -0700)
committerMartin K. Petersen <martin.petersen@oracle.com>
Fri, 27 Mar 2026 21:20:30 +0000 (17:20 -0400)
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 <beanhuo@micron.com>
Reviewed-by: Bart Van Assche <bvanassche@acm.org>
Signed-off-by: Can Guo <can.guo@oss.qualcomm.com>
Reviewed-by: Peter Wang <peter.wang@mediatek.com>
Link: https://patch.msgid.link/20260325152154.1604082-8-can.guo@oss.qualcomm.com
Signed-off-by: Martin K. Petersen <martin.petersen@oracle.com>
drivers/ufs/core/ufs-debugfs.c
drivers/ufs/core/ufs-txeq.c
drivers/ufs/core/ufshcd-priv.h
drivers/ufs/core/ufshcd.c
include/ufs/ufshcd.h

index 831758b45163b862c8b779d12c06862c8b95a4b1..e3dd81d6fe828ecb36eab03c4a639115aa35a29b 100644 (file)
@@ -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 },
        { }
 };
 
index b68a7af782908d14c46d824bafec72fe910e08f6..3a879c644faa50894f960ac307391dc3e610c3cd 100644 (file)
@@ -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;
+}
index 45904e5746b2d61c2fb4a6c3f1bcf50f3fbc22e7..d296f00c099ddb253813d5d05fa40ea31c9e086b 100644 (file)
@@ -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)
index 39e6d12e347a1eb5a6475082a04b18389214012e..2e8255b3d883c3ffcb5f03ba832671c13a1722bd 100644 (file)
@@ -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,
index 35b1288327d0a29e2f7f58a8c93e602c4e39060e..bc9e48e89db433d151aa6000ba1ad1c5aade4abe 100644 (file)
@@ -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;
 };
 
 /**