1617 lines
42 KiB
C
1617 lines
42 KiB
C
/* Copyright (c) 2013-2015, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* UFS debugfs - add debugfs interface to the ufshcd.
|
|
* This is currently used for statistics collection and exporting from the
|
|
* UFS driver.
|
|
* This infrastructure can be used for debugging or direct tweaking
|
|
* of the driver from userspace.
|
|
*
|
|
*/
|
|
|
|
#include <linux/random.h>
|
|
#include "ufs-debugfs.h"
|
|
#include "unipro.h"
|
|
#include "ufshci.h"
|
|
|
|
enum field_width {
|
|
BYTE = 1,
|
|
WORD = 2,
|
|
};
|
|
|
|
struct desc_field_offset {
|
|
char *name;
|
|
int offset;
|
|
enum field_width width_byte;
|
|
};
|
|
|
|
#define UFS_ERR_STATS_PRINT(file, error_index, string, error_seen) \
|
|
do { \
|
|
if (err_stats[error_index]) { \
|
|
seq_printf(file, string, \
|
|
err_stats[error_index]); \
|
|
error_seen = true; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define DOORBELL_CLR_TOUT_US (1000 * 1000) /* 1 sec */
|
|
|
|
#ifdef CONFIG_UFS_FAULT_INJECTION
|
|
|
|
#define INJECT_COMMAND_HANG (0x0)
|
|
|
|
static DECLARE_FAULT_ATTR(fail_default_attr);
|
|
static char *fail_request;
|
|
module_param(fail_request, charp, 0);
|
|
|
|
/**
|
|
* struct ufsdbg_err_scenario - error scenario use case
|
|
* @name: the name of the error scenario
|
|
* @err_code_arr: error codes array for this error scenario
|
|
* @num_err_codes: number of error codes in err_code_arr
|
|
*/
|
|
struct ufsdbg_err_scenario {
|
|
const char *name;
|
|
const int *err_code_arr;
|
|
u32 num_err_codes;
|
|
u32 num_err_injected;
|
|
};
|
|
|
|
/*
|
|
* the following static arrays are aggregation of possible errors
|
|
* that might occur during the relevant error scenario
|
|
*/
|
|
static const int err_inject_intr_err_codes[] = {
|
|
CONTROLLER_FATAL_ERROR,
|
|
SYSTEM_BUS_FATAL_ERROR,
|
|
INJECT_COMMAND_HANG,
|
|
};
|
|
|
|
static const int err_inject_pwr_change_err_codes[] = {
|
|
-EIO,
|
|
-ETIMEDOUT,
|
|
-1,
|
|
PWR_REMOTE,
|
|
PWR_BUSY,
|
|
PWR_ERROR_CAP,
|
|
PWR_FATAL_ERROR,
|
|
};
|
|
|
|
static const int err_inject_uic_err_codes[] = {
|
|
-EIO,
|
|
-ETIMEDOUT,
|
|
};
|
|
|
|
static const int err_inject_dme_attr_err_codes[] = {
|
|
/* an invalid DME attribute for host and device */
|
|
0x1600,
|
|
};
|
|
|
|
static const int err_inject_query_err_codes[] = {
|
|
/* an invalid idn for flag/attribute/descriptor query request */
|
|
0xFF,
|
|
};
|
|
|
|
static struct ufsdbg_err_scenario err_scen_arr[] = {
|
|
{
|
|
"ERR_INJECT_INTR",
|
|
err_inject_intr_err_codes,
|
|
ARRAY_SIZE(err_inject_intr_err_codes),
|
|
},
|
|
{
|
|
"ERR_INJECT_PWR_CHANGE",
|
|
err_inject_pwr_change_err_codes,
|
|
ARRAY_SIZE(err_inject_pwr_change_err_codes),
|
|
},
|
|
{
|
|
"ERR_INJECT_UIC",
|
|
err_inject_uic_err_codes,
|
|
ARRAY_SIZE(err_inject_uic_err_codes),
|
|
},
|
|
{
|
|
"ERR_INJECT_DME_ATTR",
|
|
err_inject_dme_attr_err_codes,
|
|
ARRAY_SIZE(err_inject_dme_attr_err_codes),
|
|
},
|
|
{
|
|
"ERR_INJECT_QUERY",
|
|
err_inject_query_err_codes,
|
|
ARRAY_SIZE(err_inject_query_err_codes),
|
|
},
|
|
};
|
|
|
|
static bool inject_fatal_err_tr(struct ufs_hba *hba, u8 ocs_err)
|
|
{
|
|
int tag;
|
|
|
|
tag = find_first_bit(&hba->outstanding_reqs, hba->nutrs);
|
|
if (tag == hba->nutrs)
|
|
return 0;
|
|
|
|
ufshcd_writel(hba, ~(1 << tag), REG_UTP_TRANSFER_REQ_LIST_CLEAR);
|
|
(&hba->lrb[tag])->utr_descriptor_ptr->header.dword_2 =
|
|
cpu_to_be32(ocs_err);
|
|
|
|
/* fatal error injected */
|
|
return 1;
|
|
}
|
|
|
|
static bool inject_fatal_err_tm(struct ufs_hba *hba, u8 ocs_err)
|
|
{
|
|
int tag;
|
|
|
|
tag = find_first_bit(&hba->outstanding_tasks, hba->nutmrs);
|
|
if (tag == hba->nutmrs)
|
|
return 0;
|
|
|
|
ufshcd_writel(hba, ~(1 << tag), REG_UTP_TASK_REQ_LIST_CLEAR);
|
|
(&hba->utmrdl_base_addr[tag])->header.dword_2 =
|
|
cpu_to_be32(ocs_err);
|
|
|
|
/* fatal error injected */
|
|
return 1;
|
|
}
|
|
|
|
static bool inject_cmd_hang_tr(struct ufs_hba *hba)
|
|
{
|
|
int tag;
|
|
|
|
tag = find_first_bit(&hba->outstanding_reqs, hba->nutrs);
|
|
if (tag == hba->nutrs)
|
|
return 0;
|
|
|
|
__clear_bit(tag, &hba->outstanding_reqs);
|
|
hba->lrb[tag].cmd = NULL;
|
|
__clear_bit(tag, &hba->lrb_in_use);
|
|
|
|
/* command hang injected */
|
|
return 1;
|
|
}
|
|
|
|
static int inject_cmd_hang_tm(struct ufs_hba *hba)
|
|
{
|
|
int tag;
|
|
|
|
tag = find_first_bit(&hba->outstanding_tasks, hba->nutmrs);
|
|
if (tag == hba->nutmrs)
|
|
return 0;
|
|
|
|
__clear_bit(tag, &hba->outstanding_tasks);
|
|
__clear_bit(tag, &hba->tm_slots_in_use);
|
|
|
|
/* command hang injected */
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
ufsdbg_intr_fail_request(struct ufs_hba *hba, u32 *intr_status)
|
|
{
|
|
u8 ocs_err;
|
|
|
|
dev_info(hba->dev, "%s: fault-inject error: 0x%x\n",
|
|
__func__, *intr_status);
|
|
|
|
switch (*intr_status) {
|
|
case CONTROLLER_FATAL_ERROR: /* fall through */
|
|
ocs_err = OCS_FATAL_ERROR;
|
|
goto set_ocs;
|
|
case SYSTEM_BUS_FATAL_ERROR:
|
|
ocs_err = OCS_INVALID_CMD_TABLE_ATTR;
|
|
set_ocs:
|
|
if (!inject_fatal_err_tr(hba, ocs_err))
|
|
if (!inject_fatal_err_tm(hba, ocs_err))
|
|
goto out;
|
|
break;
|
|
case INJECT_COMMAND_HANG:
|
|
if (!inject_cmd_hang_tr(hba))
|
|
inject_cmd_hang_tm(hba);
|
|
break;
|
|
default:
|
|
BUG();
|
|
/* some configurations ignore panics caused by BUG() */
|
|
break;
|
|
}
|
|
out:
|
|
return;
|
|
}
|
|
|
|
static bool
|
|
ufsdbg_find_err_code(enum ufsdbg_err_inject_scenario usecase,
|
|
int *ret, u32 *index)
|
|
{
|
|
struct ufsdbg_err_scenario *err_scen = &err_scen_arr[usecase];
|
|
u32 err_code_index;
|
|
|
|
if (!err_scen->num_err_codes)
|
|
return false;
|
|
|
|
err_code_index = prandom_u32() % err_scen->num_err_codes;
|
|
|
|
*index = err_code_index;
|
|
*ret = err_scen->err_code_arr[err_code_index];
|
|
return true;
|
|
}
|
|
|
|
void ufsdbg_error_inject_dispatcher(struct ufs_hba *hba,
|
|
enum ufsdbg_err_inject_scenario usecase,
|
|
int success_value, int *ret_value)
|
|
{
|
|
int opt_ret = 0;
|
|
u32 err_code_index = 0;
|
|
|
|
/* sanity check and verify error scenario bit */
|
|
if ((unlikely(!hba || !ret_value)) ||
|
|
(likely(!(hba->debugfs_files.err_inj_scenario_mask &
|
|
BIT(usecase)))))
|
|
goto out;
|
|
|
|
if (usecase < 0 || usecase >= ERR_INJECT_MAX_ERR_SCENARIOS) {
|
|
dev_err(hba->dev, "%s: invalid usecase value (%d)\n",
|
|
__func__, usecase);
|
|
goto out;
|
|
}
|
|
|
|
if (!ufsdbg_find_err_code(usecase, &opt_ret, &err_code_index))
|
|
goto out;
|
|
|
|
if (!should_fail(&hba->debugfs_files.fail_attr, 1))
|
|
goto out;
|
|
|
|
/* if an error already occurred/injected */
|
|
if (*ret_value != success_value)
|
|
goto out;
|
|
|
|
switch (usecase) {
|
|
case ERR_INJECT_INTR:
|
|
/* an error already occurred */
|
|
if (*ret_value & UFSHCD_ERROR_MASK)
|
|
goto out;
|
|
|
|
ufsdbg_intr_fail_request(hba, (u32 *)&opt_ret);
|
|
/* fall through */
|
|
case ERR_INJECT_PWR_CHANGE:
|
|
case ERR_INJECT_UIC:
|
|
case ERR_INJECT_DME_ATTR:
|
|
case ERR_INJECT_QUERY:
|
|
goto should_fail;
|
|
default:
|
|
dev_err(hba->dev, "%s: unsupported error scenario\n",
|
|
__func__);
|
|
goto out;
|
|
}
|
|
|
|
should_fail:
|
|
*ret_value = opt_ret;
|
|
err_scen_arr[usecase].num_err_injected++;
|
|
pr_debug("%s: error code index [%d], error code %d (0x%x) is injected for scenario \"%s\"\n",
|
|
__func__, err_code_index, *ret_value, *ret_value,
|
|
err_scen_arr[usecase].name);
|
|
out:
|
|
/**
|
|
* here it's guaranteed that ret_value has the correct value,
|
|
* whether it was assigned with a new value, or kept its own
|
|
* original incoming value
|
|
*/
|
|
return;
|
|
}
|
|
|
|
static int ufsdbg_err_inj_scenario_read(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
enum ufsdbg_err_inject_scenario err_case;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
seq_printf(file, "%-40s %-17s %-15s\n",
|
|
"Error Scenario:", "Bit[#]", "STATUS");
|
|
|
|
for (err_case = ERR_INJECT_INTR;
|
|
err_case < ERR_INJECT_MAX_ERR_SCENARIOS; err_case++) {
|
|
seq_printf(file, "%-40s 0x%-15lx %-15s\n",
|
|
err_scen_arr[err_case].name,
|
|
UFS_BIT(err_case),
|
|
hba->debugfs_files.err_inj_scenario_mask &
|
|
UFS_BIT(err_case) ? "ENABLE" : "DISABLE");
|
|
}
|
|
|
|
seq_printf(file, "bitwise of error scenario is 0x%x\n\n",
|
|
hba->debugfs_files.err_inj_scenario_mask);
|
|
|
|
seq_puts(file, "usage example:\n");
|
|
seq_puts(file, "echo 0x4 > /sys/kernel/debug/.../err_inj_scenario\n");
|
|
seq_puts(file, "in order to enable ERR_INJECT_INTR\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static
|
|
int ufsdbg_err_inj_scenario_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file,
|
|
ufsdbg_err_inj_scenario_read, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_err_inj_scenario_write(struct file *file,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = file->f_mapping->host->i_private;
|
|
int ret;
|
|
int err_scen = 0;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtoint_from_user(ubuf, cnt, 0, &err_scen);
|
|
if (ret) {
|
|
dev_err(hba->dev, "%s: Invalid argument\n", __func__);
|
|
return ret;
|
|
}
|
|
|
|
hba->debugfs_files.err_inj_scenario_mask = err_scen;
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_err_inj_scenario_ops = {
|
|
.open = ufsdbg_err_inj_scenario_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_err_inj_scenario_write,
|
|
};
|
|
|
|
static int ufsdbg_err_inj_stats_read(struct seq_file *file, void *data)
|
|
{
|
|
enum ufsdbg_err_inject_scenario err;
|
|
|
|
seq_printf(file, "%-40s %-20s\n",
|
|
"Error Scenario:", "Num of Errors Injected");
|
|
|
|
for (err = 0; err < ERR_INJECT_MAX_ERR_SCENARIOS; err++) {
|
|
seq_printf(file, "%-40s %-20d\n",
|
|
err_scen_arr[err].name,
|
|
err_scen_arr[err].num_err_injected);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static
|
|
int ufsdbg_err_inj_stats_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file,
|
|
ufsdbg_err_inj_stats_read, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_err_inj_stats_write(struct file *file,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
enum ufsdbg_err_inject_scenario err;
|
|
|
|
for (err = 0; err < ERR_INJECT_MAX_ERR_SCENARIOS; err++)
|
|
err_scen_arr[err].num_err_injected = 0;
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_err_inj_stats_ops = {
|
|
.open = ufsdbg_err_inj_stats_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_err_inj_stats_write,
|
|
};
|
|
|
|
static void ufsdbg_setup_fault_injection(struct ufs_hba *hba)
|
|
{
|
|
struct dentry *fault_dir;
|
|
|
|
hba->debugfs_files.fail_attr = fail_default_attr;
|
|
|
|
if (fail_request)
|
|
setup_fault_attr(&hba->debugfs_files.fail_attr, fail_request);
|
|
|
|
/* suppress dump stack every time failure is injected */
|
|
hba->debugfs_files.fail_attr.verbose = 0;
|
|
|
|
fault_dir = fault_create_debugfs_attr("inject_fault",
|
|
hba->debugfs_files.debugfs_root,
|
|
&hba->debugfs_files.fail_attr);
|
|
|
|
if (IS_ERR(fault_dir)) {
|
|
dev_err(hba->dev, "%s: failed to create debugfs entry for fault injection\n",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
hba->debugfs_files.err_inj_scenario =
|
|
debugfs_create_file("err_inj_scenario",
|
|
S_IRUGO | S_IWUGO,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_err_inj_scenario_ops);
|
|
|
|
if (!hba->debugfs_files.err_inj_scenario) {
|
|
dev_err(hba->dev,
|
|
"%s: Could not create debugfs entry for err_scenario",
|
|
__func__);
|
|
goto fail_err_inj_scenario;
|
|
}
|
|
|
|
hba->debugfs_files.err_inj_stats =
|
|
debugfs_create_file("err_inj_stats", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_err_inj_stats_ops);
|
|
if (!hba->debugfs_files.err_inj_stats) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create err_inj_stats debugfs entry\n",
|
|
__func__);
|
|
goto fail_err_inj_stats;
|
|
}
|
|
|
|
return;
|
|
|
|
fail_err_inj_stats:
|
|
debugfs_remove(hba->debugfs_files.err_inj_scenario);
|
|
fail_err_inj_scenario:
|
|
debugfs_remove_recursive(fault_dir);
|
|
}
|
|
#else
|
|
static void ufsdbg_setup_fault_injection(struct ufs_hba *hba)
|
|
{
|
|
}
|
|
#endif /* CONFIG_UFS_FAULT_INJECTION */
|
|
|
|
#define BUFF_LINE_SIZE 16 /* Must be a multiplication of sizeof(u32) */
|
|
#define TAB_CHARS 8
|
|
|
|
static int ufsdbg_tag_stats_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
struct ufs_stats *ufs_stats;
|
|
int i, j;
|
|
int max_depth;
|
|
bool is_tag_empty = true;
|
|
unsigned long flags;
|
|
char *sep = " | * | ";
|
|
|
|
if (!hba)
|
|
goto exit;
|
|
|
|
ufs_stats = &hba->ufs_stats;
|
|
|
|
if (!ufs_stats->enabled) {
|
|
pr_debug("%s: ufs statistics are disabled\n", __func__);
|
|
seq_puts(file, "ufs statistics are disabled");
|
|
goto exit;
|
|
}
|
|
|
|
max_depth = hba->nutrs;
|
|
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
/* Header */
|
|
seq_printf(file, " Tag Stat\t\t%s Number of pending reqs upon issue (Q fullness)\n",
|
|
sep);
|
|
for (i = 0; i < TAB_CHARS * (TS_NUM_STATS + 4); i++) {
|
|
seq_puts(file, "-");
|
|
if (i == (TAB_CHARS * 3 - 1))
|
|
seq_puts(file, sep);
|
|
}
|
|
seq_printf(file,
|
|
"\n #\tnum uses\t%s\t #\tAll\tRead\tWrite\tUrg.R\tUrg.W\tFlush\n",
|
|
sep);
|
|
|
|
/* values */
|
|
for (i = 0; i < max_depth; i++) {
|
|
if (ufs_stats->tag_stats[i][TS_TAG] <= 0 &&
|
|
ufs_stats->tag_stats[i][TS_READ] <= 0 &&
|
|
ufs_stats->tag_stats[i][TS_WRITE] <= 0 &&
|
|
ufs_stats->tag_stats[i][TS_URGENT_READ] <= 0 &&
|
|
ufs_stats->tag_stats[i][TS_URGENT_WRITE] <= 0 &&
|
|
ufs_stats->tag_stats[i][TS_FLUSH] <= 0)
|
|
continue;
|
|
|
|
is_tag_empty = false;
|
|
seq_printf(file, " %d\t ", i);
|
|
for (j = 0; j < TS_NUM_STATS; j++) {
|
|
seq_printf(file, "%llu\t", ufs_stats->tag_stats[i][j]);
|
|
if (j != 0)
|
|
continue;
|
|
seq_printf(file, "\t%s\t %d\t%llu\t", sep, i,
|
|
ufs_stats->tag_stats[i][TS_READ] +
|
|
ufs_stats->tag_stats[i][TS_WRITE] +
|
|
ufs_stats->tag_stats[i][TS_URGENT_READ] +
|
|
ufs_stats->tag_stats[i][TS_URGENT_WRITE] +
|
|
ufs_stats->tag_stats[i][TS_FLUSH]);
|
|
}
|
|
seq_puts(file, "\n");
|
|
}
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
|
|
if (is_tag_empty)
|
|
pr_debug("%s: All tags statistics are empty", __func__);
|
|
|
|
exit:
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_tag_stats_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_tag_stats_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_tag_stats_write(struct file *filp,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = filp->f_mapping->host->i_private;
|
|
struct ufs_stats *ufs_stats;
|
|
int val = 0;
|
|
int ret, bit = 0;
|
|
unsigned long flags;
|
|
|
|
ret = kstrtoint_from_user(ubuf, cnt, 0, &val);
|
|
if (ret) {
|
|
dev_err(hba->dev, "%s: Invalid argument\n", __func__);
|
|
return ret;
|
|
}
|
|
|
|
ufs_stats = &hba->ufs_stats;
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
|
|
if (!val) {
|
|
ufs_stats->enabled = false;
|
|
pr_debug("%s: Disabling UFS tag statistics", __func__);
|
|
} else {
|
|
ufs_stats->enabled = true;
|
|
pr_debug("%s: Enabling & Resetting UFS tag statistics",
|
|
__func__);
|
|
memset(hba->ufs_stats.tag_stats[0], 0,
|
|
sizeof(**hba->ufs_stats.tag_stats) *
|
|
TS_NUM_STATS * hba->nutrs);
|
|
|
|
/* initialize current queue depth */
|
|
ufs_stats->q_depth = 0;
|
|
for_each_set_bit_from(bit, &hba->outstanding_reqs, hba->nutrs)
|
|
ufs_stats->q_depth++;
|
|
pr_debug("%s: Enabled UFS tag statistics", __func__);
|
|
}
|
|
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_tag_stats_fops = {
|
|
.open = ufsdbg_tag_stats_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_tag_stats_write,
|
|
};
|
|
|
|
static int ufsdbg_query_stats_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
struct ufs_stats *ufs_stats = &hba->ufs_stats;
|
|
int i, j;
|
|
static const char *opcode_name[UPIU_QUERY_OPCODE_MAX] = {
|
|
"QUERY_OPCODE_NOP:",
|
|
"QUERY_OPCODE_READ_DESC:",
|
|
"QUERY_OPCODE_WRITE_DESC:",
|
|
"QUERY_OPCODE_READ_ATTR:",
|
|
"QUERY_OPCODE_WRITE_ATTR:",
|
|
"QUERY_OPCODE_READ_FLAG:",
|
|
"QUERY_OPCODE_SET_FLAG:",
|
|
"QUERY_OPCODE_CLEAR_FLAG:",
|
|
"QUERY_OPCODE_TOGGLE_FLAG:",
|
|
};
|
|
|
|
seq_puts(file, "\n");
|
|
seq_puts(file, "The following table shows how many TIMES each IDN was sent to device for each QUERY OPCODE:\n");
|
|
seq_puts(file, "\n");
|
|
|
|
for (i = 0; i < UPIU_QUERY_OPCODE_MAX; i++) {
|
|
seq_printf(file, "%-30s", opcode_name[i]);
|
|
|
|
for (j = 0; j < MAX_QUERY_IDN; j++) {
|
|
/*
|
|
* we would like to print only the non-zero data,
|
|
* (non-zero number of times that IDN was sent
|
|
* to the device per opcode). There is no
|
|
* importance to the "table structure" of the output.
|
|
*/
|
|
if (ufs_stats->query_stats_arr[i][j])
|
|
seq_printf(file, "IDN 0x%02X: %d,\t", j,
|
|
ufs_stats->query_stats_arr[i][j]);
|
|
}
|
|
seq_puts(file, "\n");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_query_stats_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_query_stats_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_query_stats_write(struct file *filp,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = filp->f_mapping->host->i_private;
|
|
struct ufs_stats *ufs_stats = &hba->ufs_stats;
|
|
int i, j;
|
|
|
|
mutex_lock(&hba->dev_cmd.lock);
|
|
|
|
for (i = 0; i < UPIU_QUERY_OPCODE_MAX; i++)
|
|
for (j = 0; j < MAX_QUERY_IDN; j++)
|
|
ufs_stats->query_stats_arr[i][j] = 0;
|
|
|
|
mutex_unlock(&hba->dev_cmd.lock);
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_query_stats_fops = {
|
|
.open = ufsdbg_query_stats_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_query_stats_write,
|
|
};
|
|
|
|
static int ufsdbg_err_stats_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
int *err_stats;
|
|
unsigned long flags;
|
|
bool error_seen = false;
|
|
|
|
if (!hba)
|
|
goto exit;
|
|
|
|
err_stats = hba->ufs_stats.err_stats;
|
|
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
|
|
seq_puts(file, "\n==UFS errors that caused controller reset==\n");
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_HIBERN8_EXIT,
|
|
"controller reset due to hibern8 exit error:\t %d\n",
|
|
error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_VOPS_SUSPEND,
|
|
"controller reset due to vops suspend error:\t\t %d\n",
|
|
error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_EH,
|
|
"controller reset due to error handling:\t\t %d\n",
|
|
error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_CLEAR_PEND_XFER_TM,
|
|
"controller reset due to clear xfer/tm regs:\t\t %d\n",
|
|
error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_INT_FATAL_ERRORS,
|
|
"controller reset due to fatal interrupt:\t %d\n",
|
|
error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_INT_UIC_ERROR,
|
|
"controller reset due to uic interrupt error:\t %d\n",
|
|
error_seen);
|
|
|
|
if (error_seen)
|
|
error_seen = false;
|
|
else
|
|
seq_puts(file,
|
|
"so far, no errors that caused controller reset\n\n");
|
|
|
|
seq_puts(file, "\n\n==UFS other errors==\n");
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_HIBERN8_ENTER,
|
|
"hibern8 enter:\t\t %d\n", error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_RESUME,
|
|
"resume error:\t\t %d\n", error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_SUSPEND,
|
|
"suspend error:\t\t %d\n", error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_LINKSTARTUP,
|
|
"linkstartup error:\t\t %d\n", error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_POWER_MODE_CHANGE,
|
|
"power change error:\t %d\n", error_seen);
|
|
|
|
UFS_ERR_STATS_PRINT(file, UFS_ERR_TASK_ABORT,
|
|
"abort callback:\t\t %d\n\n", error_seen);
|
|
|
|
if (!error_seen)
|
|
seq_puts(file,
|
|
"so far, no other UFS related errors\n\n");
|
|
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
exit:
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_err_stats_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_err_stats_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_err_stats_write(struct file *filp,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = filp->f_mapping->host->i_private;
|
|
struct ufs_stats *ufs_stats;
|
|
unsigned long flags;
|
|
|
|
ufs_stats = &hba->ufs_stats;
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
|
|
pr_debug("%s: Resetting UFS error statistics", __func__);
|
|
memset(ufs_stats->err_stats, 0, sizeof(hba->ufs_stats.err_stats));
|
|
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_err_stats_fops = {
|
|
.open = ufsdbg_err_stats_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_err_stats_write,
|
|
};
|
|
|
|
static int ufshcd_init_statistics(struct ufs_hba *hba)
|
|
{
|
|
struct ufs_stats *stats = &hba->ufs_stats;
|
|
int ret = 0;
|
|
int i;
|
|
|
|
stats->enabled = false;
|
|
stats->tag_stats = kzalloc(sizeof(*stats->tag_stats) * hba->nutrs,
|
|
GFP_KERNEL);
|
|
if (!hba->ufs_stats.tag_stats)
|
|
goto no_mem;
|
|
|
|
stats->tag_stats[0] = kzalloc(sizeof(**stats->tag_stats) *
|
|
TS_NUM_STATS * hba->nutrs, GFP_KERNEL);
|
|
if (!stats->tag_stats[0])
|
|
goto no_mem;
|
|
|
|
for (i = 1; i < hba->nutrs; i++)
|
|
stats->tag_stats[i] = &stats->tag_stats[0][i * TS_NUM_STATS];
|
|
|
|
memset(stats->err_stats, 0, sizeof(hba->ufs_stats.err_stats));
|
|
|
|
goto exit;
|
|
|
|
no_mem:
|
|
dev_err(hba->dev, "%s: Unable to allocate UFS tag_stats", __func__);
|
|
ret = -ENOMEM;
|
|
exit:
|
|
return ret;
|
|
}
|
|
|
|
void ufsdbg_pr_buf_to_std(struct ufs_hba *hba, int offset, int num_regs,
|
|
char *str, void *priv)
|
|
{
|
|
int i;
|
|
char linebuf[38];
|
|
int size = num_regs * sizeof(u32);
|
|
int lines = size / BUFF_LINE_SIZE +
|
|
(size % BUFF_LINE_SIZE ? 1 : 0);
|
|
struct seq_file *file = priv;
|
|
|
|
if (!hba || !file) {
|
|
pr_err("%s called with NULL pointer\n", __func__);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < lines; i++) {
|
|
hex_dump_to_buffer(hba->mmio_base + offset + i * BUFF_LINE_SIZE,
|
|
min(BUFF_LINE_SIZE, size), BUFF_LINE_SIZE, 4,
|
|
linebuf, sizeof(linebuf), false);
|
|
seq_printf(file, "%s [%x]: %s\n", str, i * BUFF_LINE_SIZE,
|
|
linebuf);
|
|
size -= BUFF_LINE_SIZE/sizeof(u32);
|
|
}
|
|
}
|
|
|
|
static int ufsdbg_host_regs_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
|
|
pm_runtime_get_sync(hba->dev);
|
|
ufshcd_hold(hba, false);
|
|
ufsdbg_pr_buf_to_std(hba, 0, UFSHCI_REG_SPACE_SIZE / sizeof(u32),
|
|
"host regs", file);
|
|
ufshcd_release(hba, false);
|
|
pm_runtime_put_sync(hba->dev);
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_host_regs_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_host_regs_show, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_host_regs_fops = {
|
|
.open = ufsdbg_host_regs_open,
|
|
.read = seq_read,
|
|
};
|
|
|
|
static int ufsdbg_dump_device_desc_show(struct seq_file *file, void *data)
|
|
{
|
|
int err = 0;
|
|
int buff_len = QUERY_DESC_DEVICE_MAX_SIZE;
|
|
u8 desc_buf[QUERY_DESC_DEVICE_MAX_SIZE];
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
|
|
struct desc_field_offset device_desc_field_name[] = {
|
|
{"bLength", 0x00, BYTE},
|
|
{"bDescriptorType", 0x01, BYTE},
|
|
{"bDevice", 0x02, BYTE},
|
|
{"bDeviceClass", 0x03, BYTE},
|
|
{"bDeviceSubClass", 0x04, BYTE},
|
|
{"bProtocol", 0x05, BYTE},
|
|
{"bNumberLU", 0x06, BYTE},
|
|
{"bNumberWLU", 0x07, BYTE},
|
|
{"bBootEnable", 0x08, BYTE},
|
|
{"bDescrAccessEn", 0x09, BYTE},
|
|
{"bInitPowerMode", 0x0A, BYTE},
|
|
{"bHighPriorityLUN", 0x0B, BYTE},
|
|
{"bSecureRemovalType", 0x0C, BYTE},
|
|
{"bSecurityLU", 0x0D, BYTE},
|
|
{"Reserved", 0x0E, BYTE},
|
|
{"bInitActiveICCLevel", 0x0F, BYTE},
|
|
{"wSpecVersion", 0x10, WORD},
|
|
{"wManufactureDate", 0x12, WORD},
|
|
{"iManufactureName", 0x14, BYTE},
|
|
{"iProductName", 0x15, BYTE},
|
|
{"iSerialNumber", 0x16, BYTE},
|
|
{"iOemID", 0x17, BYTE},
|
|
{"wManufactureID", 0x18, WORD},
|
|
{"bUD0BaseOffset", 0x1A, BYTE},
|
|
{"bUDConfigPLength", 0x1B, BYTE},
|
|
{"bDeviceRTTCap", 0x1C, BYTE},
|
|
{"wPeriodicRTCUpdate", 0x1D, WORD}
|
|
};
|
|
|
|
pm_runtime_get_sync(hba->dev);
|
|
err = ufshcd_read_device_desc(hba, desc_buf, buff_len);
|
|
pm_runtime_put_sync(hba->dev);
|
|
|
|
if (!err) {
|
|
int i;
|
|
struct desc_field_offset *tmp;
|
|
for (i = 0; i < ARRAY_SIZE(device_desc_field_name); ++i) {
|
|
tmp = &device_desc_field_name[i];
|
|
|
|
if (tmp->width_byte == BYTE) {
|
|
seq_printf(file,
|
|
"Device Descriptor[Byte offset 0x%x]: %s = 0x%x\n",
|
|
tmp->offset,
|
|
tmp->name,
|
|
(u8)desc_buf[tmp->offset]);
|
|
} else if (tmp->width_byte == WORD) {
|
|
seq_printf(file,
|
|
"Device Descriptor[Byte offset 0x%x]: %s = 0x%x\n",
|
|
tmp->offset,
|
|
tmp->name,
|
|
*(u16 *)&desc_buf[tmp->offset]);
|
|
} else {
|
|
seq_printf(file,
|
|
"Device Descriptor[offset 0x%x]: %s. Wrong Width = %d",
|
|
tmp->offset, tmp->name, tmp->width_byte);
|
|
}
|
|
}
|
|
} else {
|
|
seq_printf(file, "Reading Device Descriptor failed. err = %d\n",
|
|
err);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int ufsdbg_show_hba_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
|
|
seq_printf(file, "hba->outstanding_tasks = 0x%x\n",
|
|
(u32)hba->outstanding_tasks);
|
|
seq_printf(file, "hba->outstanding_reqs = 0x%x\n",
|
|
(u32)hba->outstanding_reqs);
|
|
|
|
seq_printf(file, "hba->capabilities = 0x%x\n", hba->capabilities);
|
|
seq_printf(file, "hba->nutrs = %d\n", hba->nutrs);
|
|
seq_printf(file, "hba->nutmrs = %d\n", hba->nutmrs);
|
|
seq_printf(file, "hba->ufs_version = 0x%x\n", hba->ufs_version);
|
|
seq_printf(file, "hba->irq = 0x%x\n", hba->irq);
|
|
seq_printf(file, "hba->auto_bkops_enabled = %d\n",
|
|
hba->auto_bkops_enabled);
|
|
|
|
seq_printf(file, "hba->ufshcd_state = 0x%x\n", hba->ufshcd_state);
|
|
seq_printf(file, "hba->clk_gating.state = 0x%x\n",
|
|
hba->clk_gating.state);
|
|
seq_printf(file, "hba->eh_flags = 0x%x\n", hba->eh_flags);
|
|
seq_printf(file, "hba->intr_mask = 0x%x\n", hba->intr_mask);
|
|
seq_printf(file, "hba->ee_ctrl_mask = 0x%x\n", hba->ee_ctrl_mask);
|
|
|
|
/* HBA Errors */
|
|
seq_printf(file, "hba->errors = 0x%x\n", hba->errors);
|
|
seq_printf(file, "hba->uic_error = 0x%x\n", hba->uic_error);
|
|
seq_printf(file, "hba->saved_err = 0x%x\n", hba->saved_err);
|
|
seq_printf(file, "hba->saved_uic_err = 0x%x\n", hba->saved_uic_err);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_show_hba_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_show_hba_show, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_show_hba_fops = {
|
|
.open = ufsdbg_show_hba_open,
|
|
.read = seq_read,
|
|
};
|
|
|
|
static int ufsdbg_dump_device_desc_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file,
|
|
ufsdbg_dump_device_desc_show, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_dump_device_desc = {
|
|
.open = ufsdbg_dump_device_desc_open,
|
|
.read = seq_read,
|
|
};
|
|
|
|
static int ufsdbg_power_mode_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
char *names[] = {
|
|
"INVALID MODE",
|
|
"FAST MODE",
|
|
"SLOW MODE",
|
|
"INVALID MODE",
|
|
"FASTAUTO MODE",
|
|
"SLOWAUTO MODE",
|
|
"INVALID MODE",
|
|
};
|
|
|
|
/* Print current status */
|
|
seq_puts(file, "UFS current power mode [RX, TX]:");
|
|
seq_printf(file, "gear=[%d,%d], lane=[%d,%d], pwr=[%s,%s], rate = %c",
|
|
hba->pwr_info.gear_rx, hba->pwr_info.gear_tx,
|
|
hba->pwr_info.lane_rx, hba->pwr_info.lane_tx,
|
|
names[hba->pwr_info.pwr_rx],
|
|
names[hba->pwr_info.pwr_tx],
|
|
hba->pwr_info.hs_rate == PA_HS_MODE_B ? 'B' : 'A');
|
|
seq_puts(file, "\n\n");
|
|
|
|
/* Print usage */
|
|
seq_puts(file,
|
|
"To change power mode write 'GGLLMM' where:\n"
|
|
"G - selected gear\n"
|
|
"L - number of lanes\n"
|
|
"M - power mode:\n"
|
|
"\t1 = fast mode\n"
|
|
"\t2 = slow mode\n"
|
|
"\t4 = fast-auto mode\n"
|
|
"\t5 = slow-auto mode\n"
|
|
"first letter is for RX, second letter is for TX.\n\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool ufsdbg_power_mode_validate(struct ufs_pa_layer_attr *pwr_mode)
|
|
{
|
|
if (pwr_mode->gear_rx < UFS_PWM_G1 || pwr_mode->gear_rx > UFS_PWM_G7 ||
|
|
pwr_mode->gear_tx < UFS_PWM_G1 || pwr_mode->gear_tx > UFS_PWM_G7 ||
|
|
pwr_mode->lane_rx < 1 || pwr_mode->lane_rx > 2 ||
|
|
pwr_mode->lane_tx < 1 || pwr_mode->lane_tx > 2 ||
|
|
(pwr_mode->pwr_rx != FAST_MODE && pwr_mode->pwr_rx != SLOW_MODE &&
|
|
pwr_mode->pwr_rx != FASTAUTO_MODE &&
|
|
pwr_mode->pwr_rx != SLOWAUTO_MODE) ||
|
|
(pwr_mode->pwr_tx != FAST_MODE && pwr_mode->pwr_tx != SLOW_MODE &&
|
|
pwr_mode->pwr_tx != FASTAUTO_MODE &&
|
|
pwr_mode->pwr_tx != SLOWAUTO_MODE)) {
|
|
pr_err("%s: power parameters are not valid\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int ufsdbg_cfg_pwr_param(struct ufs_hba *hba,
|
|
struct ufs_pa_layer_attr *new_pwr,
|
|
struct ufs_pa_layer_attr *final_pwr)
|
|
{
|
|
int ret = 0;
|
|
bool is_dev_sup_hs = false;
|
|
bool is_new_pwr_hs = false;
|
|
int dev_pwm_max_rx_gear;
|
|
int dev_pwm_max_tx_gear;
|
|
|
|
if (!hba->max_pwr_info.is_valid) {
|
|
dev_err(hba->dev, "%s: device max power is not valid. can't configure power\n",
|
|
__func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (hba->max_pwr_info.info.pwr_rx == FAST_MODE)
|
|
is_dev_sup_hs = true;
|
|
|
|
if (new_pwr->pwr_rx == FAST_MODE || new_pwr->pwr_rx == FASTAUTO_MODE)
|
|
is_new_pwr_hs = true;
|
|
|
|
final_pwr->lane_rx = hba->max_pwr_info.info.lane_rx;
|
|
final_pwr->lane_tx = hba->max_pwr_info.info.lane_tx;
|
|
|
|
/* device doesn't support HS but requested power is HS */
|
|
if (!is_dev_sup_hs && is_new_pwr_hs) {
|
|
pr_err("%s: device doesn't support HS. requested power is HS\n",
|
|
__func__);
|
|
return -ENOTSUPP;
|
|
} else if ((is_dev_sup_hs && is_new_pwr_hs) ||
|
|
(!is_dev_sup_hs && !is_new_pwr_hs)) {
|
|
/*
|
|
* If device and requested power mode are both HS or both PWM
|
|
* then dev_max->gear_xx are the gears to be assign to
|
|
* final_pwr->gear_xx
|
|
*/
|
|
final_pwr->gear_rx = hba->max_pwr_info.info.gear_rx;
|
|
final_pwr->gear_tx = hba->max_pwr_info.info.gear_tx;
|
|
} else if (is_dev_sup_hs && !is_new_pwr_hs) {
|
|
/*
|
|
* If device supports HS but requested power is PWM, then we
|
|
* need to find out what is the max gear in PWM the device
|
|
* supports
|
|
*/
|
|
|
|
ufshcd_dme_get(hba, UIC_ARG_MIB(PA_MAXRXPWMGEAR),
|
|
&dev_pwm_max_rx_gear);
|
|
|
|
if (!dev_pwm_max_rx_gear) {
|
|
pr_err("%s: couldn't get device max pwm rx gear\n",
|
|
__func__);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
ufshcd_dme_peer_get(hba, UIC_ARG_MIB(PA_MAXRXPWMGEAR),
|
|
&dev_pwm_max_tx_gear);
|
|
|
|
if (!dev_pwm_max_tx_gear) {
|
|
pr_err("%s: couldn't get device max pwm tx gear\n",
|
|
__func__);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
final_pwr->gear_rx = dev_pwm_max_rx_gear;
|
|
final_pwr->gear_tx = dev_pwm_max_tx_gear;
|
|
}
|
|
|
|
if ((new_pwr->gear_rx > final_pwr->gear_rx) ||
|
|
(new_pwr->gear_tx > final_pwr->gear_tx) ||
|
|
(new_pwr->lane_rx > final_pwr->lane_rx) ||
|
|
(new_pwr->lane_tx > final_pwr->lane_tx)) {
|
|
pr_err("%s: (RX,TX) GG,LL: in PWM/HS new pwr [%d%d,%d%d] exceeds device limitation [%d%d,%d%d]\n",
|
|
__func__,
|
|
new_pwr->gear_rx, new_pwr->gear_tx,
|
|
new_pwr->lane_rx, new_pwr->lane_tx,
|
|
final_pwr->gear_rx, final_pwr->gear_tx,
|
|
final_pwr->lane_rx, final_pwr->lane_tx);
|
|
return -ENOTSUPP;
|
|
}
|
|
|
|
final_pwr->gear_rx = new_pwr->gear_rx;
|
|
final_pwr->gear_tx = new_pwr->gear_tx;
|
|
final_pwr->lane_rx = new_pwr->lane_rx;
|
|
final_pwr->lane_tx = new_pwr->lane_tx;
|
|
final_pwr->pwr_rx = new_pwr->pwr_rx;
|
|
final_pwr->pwr_tx = new_pwr->pwr_tx;
|
|
final_pwr->hs_rate = new_pwr->hs_rate;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static int ufsdbg_config_pwr_mode(struct ufs_hba *hba,
|
|
struct ufs_pa_layer_attr *desired_pwr_mode)
|
|
{
|
|
int ret;
|
|
|
|
pm_runtime_get_sync(hba->dev);
|
|
ufshcd_scsi_block_requests(hba);
|
|
ret = ufshcd_wait_for_doorbell_clr(hba, DOORBELL_CLR_TOUT_US);
|
|
if (!ret)
|
|
ret = ufshcd_change_power_mode(hba, desired_pwr_mode);
|
|
ufshcd_scsi_unblock_requests(hba);
|
|
pm_runtime_put_sync(hba->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t ufsdbg_power_mode_write(struct file *file,
|
|
const char __user *ubuf, size_t cnt,
|
|
loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = file->f_mapping->host->i_private;
|
|
struct ufs_pa_layer_attr pwr_mode;
|
|
struct ufs_pa_layer_attr final_pwr_mode;
|
|
char pwr_mode_str[BUFF_LINE_SIZE] = {0};
|
|
loff_t buff_pos = 0;
|
|
int ret;
|
|
int idx = 0;
|
|
|
|
ret = simple_write_to_buffer(pwr_mode_str, BUFF_LINE_SIZE,
|
|
&buff_pos, ubuf, cnt);
|
|
|
|
pwr_mode.gear_rx = pwr_mode_str[idx++] - '0';
|
|
pwr_mode.gear_tx = pwr_mode_str[idx++] - '0';
|
|
pwr_mode.lane_rx = pwr_mode_str[idx++] - '0';
|
|
pwr_mode.lane_tx = pwr_mode_str[idx++] - '0';
|
|
pwr_mode.pwr_rx = pwr_mode_str[idx++] - '0';
|
|
pwr_mode.pwr_tx = pwr_mode_str[idx++] - '0';
|
|
|
|
/*
|
|
* Switching between rates is not currently supported so use the
|
|
* current rate.
|
|
* TODO: add rate switching if and when it is supported in the future
|
|
*/
|
|
pwr_mode.hs_rate = hba->pwr_info.hs_rate;
|
|
|
|
/* Validate user input */
|
|
if (!ufsdbg_power_mode_validate(&pwr_mode))
|
|
return -EINVAL;
|
|
|
|
pr_debug("%s: new power mode requested [RX,TX]: Gear=[%d,%d], Lane=[%d,%d], Mode=[%d,%d]\n",
|
|
__func__,
|
|
pwr_mode.gear_rx, pwr_mode.gear_tx, pwr_mode.lane_rx,
|
|
pwr_mode.lane_tx, pwr_mode.pwr_rx, pwr_mode.pwr_tx);
|
|
|
|
ret = ufsdbg_cfg_pwr_param(hba, &pwr_mode, &final_pwr_mode);
|
|
if (ret) {
|
|
dev_err(hba->dev,
|
|
"%s: failed to configure new power parameters, ret = %d\n",
|
|
__func__, ret);
|
|
return cnt;
|
|
}
|
|
|
|
ret = ufsdbg_config_pwr_mode(hba, &final_pwr_mode);
|
|
if (ret == -EBUSY)
|
|
dev_err(hba->dev,
|
|
"%s: ufshcd_config_pwr_mode failed: system is busy, try again\n",
|
|
__func__);
|
|
else if (ret)
|
|
dev_err(hba->dev,
|
|
"%s: ufshcd_config_pwr_mode failed, ret=%d\n",
|
|
__func__, ret);
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static int ufsdbg_power_mode_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_power_mode_show, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_power_mode_desc = {
|
|
.open = ufsdbg_power_mode_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_power_mode_write,
|
|
};
|
|
|
|
static int ufsdbg_dme_read(void *data, u64 *attr_val, bool peer)
|
|
{
|
|
int ret;
|
|
struct ufs_hba *hba = data;
|
|
u32 attr_id, read_val = 0;
|
|
int (*read_func)(struct ufs_hba *, u32, u32 *);
|
|
u32 attr_sel;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
read_func = peer ? ufshcd_dme_peer_get : ufshcd_dme_get;
|
|
attr_id = peer ? hba->debugfs_files.dme_peer_attr_id :
|
|
hba->debugfs_files.dme_local_attr_id;
|
|
pm_runtime_get_sync(hba->dev);
|
|
ufshcd_scsi_block_requests(hba);
|
|
ret = ufshcd_wait_for_doorbell_clr(hba, DOORBELL_CLR_TOUT_US);
|
|
if (!ret) {
|
|
if ((attr_id >= MPHY_RX_ATTR_ADDR_START)
|
|
&& (attr_id <= MPHY_RX_ATTR_ADDR_END))
|
|
attr_sel = UIC_ARG_MIB_SEL(attr_id,
|
|
UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0));
|
|
else
|
|
attr_sel = UIC_ARG_MIB(attr_id);
|
|
|
|
ret = read_func(hba, attr_sel, &read_val);
|
|
}
|
|
ufshcd_scsi_unblock_requests(hba);
|
|
pm_runtime_put_sync(hba->dev);
|
|
|
|
if (!ret)
|
|
*attr_val = (u64)read_val;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int ufsdbg_dme_local_set_attr_id(void *data, u64 attr_id)
|
|
{
|
|
struct ufs_hba *hba = data;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
hba->debugfs_files.dme_local_attr_id = (u32)attr_id;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_dme_local_read(void *data, u64 *attr_val)
|
|
{
|
|
return ufsdbg_dme_read(data, attr_val, false);
|
|
}
|
|
|
|
DEFINE_SIMPLE_ATTRIBUTE(ufsdbg_dme_local_read_ops,
|
|
ufsdbg_dme_local_read,
|
|
ufsdbg_dme_local_set_attr_id,
|
|
"%llu\n");
|
|
|
|
static int ufsdbg_dme_peer_read(void *data, u64 *attr_val)
|
|
{
|
|
struct ufs_hba *hba = data;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
else
|
|
return ufsdbg_dme_read(data, attr_val, true);
|
|
}
|
|
|
|
static int ufsdbg_dme_peer_set_attr_id(void *data, u64 attr_id)
|
|
{
|
|
struct ufs_hba *hba = data;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
hba->debugfs_files.dme_peer_attr_id = (u32)attr_id;
|
|
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_SIMPLE_ATTRIBUTE(ufsdbg_dme_peer_read_ops,
|
|
ufsdbg_dme_peer_read,
|
|
ufsdbg_dme_peer_set_attr_id,
|
|
"%llu\n");
|
|
|
|
static int ufsdbg_dbg_print_en_read(void *data, u64 *attr_val)
|
|
{
|
|
struct ufs_hba *hba = data;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
*attr_val = (u64)hba->ufshcd_dbg_print;
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_dbg_print_en_set(void *data, u64 attr_id)
|
|
{
|
|
struct ufs_hba *hba = data;
|
|
|
|
if (!hba)
|
|
return -EINVAL;
|
|
|
|
if (attr_id & ~UFSHCD_DBG_PRINT_ALL)
|
|
return -EINVAL;
|
|
|
|
hba->ufshcd_dbg_print = (u32)attr_id;
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_SIMPLE_ATTRIBUTE(ufsdbg_dbg_print_en_ops,
|
|
ufsdbg_dbg_print_en_read,
|
|
ufsdbg_dbg_print_en_set,
|
|
"%llu\n");
|
|
|
|
static ssize_t ufsdbg_req_stats_write(struct file *filp,
|
|
const char __user *ubuf, size_t cnt, loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = filp->f_mapping->host->i_private;
|
|
int val;
|
|
int ret;
|
|
unsigned long flags;
|
|
|
|
ret = kstrtoint_from_user(ubuf, cnt, 0, &val);
|
|
if (ret) {
|
|
dev_err(hba->dev, "%s: Invalid argument\n", __func__);
|
|
return ret;
|
|
}
|
|
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
ufshcd_init_req_stats(hba);
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static int ufsdbg_req_stats_show(struct seq_file *file, void *data)
|
|
{
|
|
struct ufs_hba *hba = (struct ufs_hba *)file->private;
|
|
int i;
|
|
unsigned long flags;
|
|
|
|
/* Header */
|
|
seq_printf(file, "\t%-10s %-10s %-10s %-10s %-10s %-10s",
|
|
"All", "Write", "Read", "Read(urg)", "Write(urg)", "Flush");
|
|
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
|
|
seq_printf(file, "\n%s:\t", "Min");
|
|
for (i = 0; i < TS_NUM_STATS; i++)
|
|
seq_printf(file, "%-10llu ", hba->ufs_stats.req_stats[i].min);
|
|
seq_printf(file, "\n%s:\t", "Max");
|
|
for (i = 0; i < TS_NUM_STATS; i++)
|
|
seq_printf(file, "%-10llu ", hba->ufs_stats.req_stats[i].max);
|
|
seq_printf(file, "\n%s:\t", "Avg.");
|
|
for (i = 0; i < TS_NUM_STATS; i++)
|
|
seq_printf(file, "%-10llu ",
|
|
div64_u64(hba->ufs_stats.req_stats[i].sum,
|
|
hba->ufs_stats.req_stats[i].count));
|
|
seq_printf(file, "\n%s:\t", "Count");
|
|
for (i = 0; i < TS_NUM_STATS; i++)
|
|
seq_printf(file, "%-10llu ", hba->ufs_stats.req_stats[i].count);
|
|
seq_puts(file, "\n");
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_req_stats_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_req_stats_show, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_req_stats_desc = {
|
|
.open = ufsdbg_req_stats_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_req_stats_write,
|
|
};
|
|
|
|
|
|
static int ufsdbg_reset_controller_show(struct seq_file *file, void *data)
|
|
{
|
|
seq_puts(file, "echo 1 > /sys/kernel/debug/.../reset_controller\n");
|
|
seq_puts(file, "resets the UFS controller and restores its operational state\n\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ufsdbg_reset_controller_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ufsdbg_reset_controller_show,
|
|
inode->i_private);
|
|
}
|
|
|
|
static ssize_t ufsdbg_reset_controller_write(struct file *filp,
|
|
const char __user *ubuf, size_t cnt, loff_t *ppos)
|
|
{
|
|
struct ufs_hba *hba = filp->f_mapping->host->i_private;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(hba->host->host_lock, flags);
|
|
|
|
/*
|
|
* simulating a dummy error in order to "convince"
|
|
* eh_work to actually reset the controller
|
|
*/
|
|
hba->saved_err |= INT_FATAL_ERRORS;
|
|
hba->silence_err_logs = true;
|
|
schedule_work(&hba->eh_work);
|
|
|
|
spin_unlock_irqrestore(hba->host->host_lock, flags);
|
|
|
|
return cnt;
|
|
}
|
|
|
|
static const struct file_operations ufsdbg_reset_controller = {
|
|
.open = ufsdbg_reset_controller_open,
|
|
.read = seq_read,
|
|
.write = ufsdbg_reset_controller_write,
|
|
};
|
|
|
|
void ufsdbg_add_debugfs(struct ufs_hba *hba)
|
|
{
|
|
char root_name[sizeof("ufshcd00")];
|
|
|
|
if (!hba) {
|
|
dev_err(hba->dev, "%s: NULL hba, exiting", __func__);
|
|
goto err_no_root;
|
|
}
|
|
|
|
snprintf(root_name, ARRAY_SIZE(root_name), "%s%d", UFSHCD,
|
|
hba->host->host_no);
|
|
|
|
hba->debugfs_files.debugfs_root = debugfs_create_dir(root_name, NULL);
|
|
if (IS_ERR(hba->debugfs_files.debugfs_root))
|
|
/* Don't complain -- debugfs just isn't enabled */
|
|
goto err_no_root;
|
|
if (!hba->debugfs_files.debugfs_root) {
|
|
/*
|
|
* Complain -- debugfs is enabled, but it failed to
|
|
* create the directory
|
|
*/
|
|
dev_err(hba->dev,
|
|
"%s: NULL debugfs root directory, exiting", __func__);
|
|
goto err_no_root;
|
|
}
|
|
|
|
hba->debugfs_files.stats_folder = debugfs_create_dir("stats",
|
|
hba->debugfs_files.debugfs_root);
|
|
if (!hba->debugfs_files.stats_folder) {
|
|
dev_err(hba->dev,
|
|
"%s: NULL stats_folder, exiting", __func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.tag_stats =
|
|
debugfs_create_file("tag_stats", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.stats_folder, hba,
|
|
&ufsdbg_tag_stats_fops);
|
|
if (!hba->debugfs_files.tag_stats) {
|
|
dev_err(hba->dev, "%s: NULL tag_stats file, exiting",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.query_stats =
|
|
debugfs_create_file("query_stats", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.stats_folder, hba,
|
|
&ufsdbg_query_stats_fops);
|
|
if (!hba->debugfs_files.query_stats) {
|
|
dev_err(hba->dev, "%s: NULL query_stats file, exiting",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.err_stats =
|
|
debugfs_create_file("err_stats", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.stats_folder, hba,
|
|
&ufsdbg_err_stats_fops);
|
|
if (!hba->debugfs_files.err_stats) {
|
|
dev_err(hba->dev, "%s: NULL err_stats file, exiting",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
if (ufshcd_init_statistics(hba)) {
|
|
dev_err(hba->dev, "%s: Error initializing statistics",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.host_regs = debugfs_create_file("host_regs", S_IRUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_host_regs_fops);
|
|
if (!hba->debugfs_files.host_regs) {
|
|
dev_err(hba->dev, "%s: NULL hcd regs file, exiting", __func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.show_hba = debugfs_create_file("show_hba", S_IRUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_show_hba_fops);
|
|
if (!hba->debugfs_files.show_hba) {
|
|
dev_err(hba->dev, "%s: NULL hba file, exiting", __func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.dump_dev_desc =
|
|
debugfs_create_file("dump_device_desc", S_IRUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_dump_device_desc);
|
|
if (!hba->debugfs_files.dump_dev_desc) {
|
|
dev_err(hba->dev,
|
|
"%s: NULL dump_device_desc file, exiting", __func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.power_mode =
|
|
debugfs_create_file("power_mode", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_power_mode_desc);
|
|
if (!hba->debugfs_files.power_mode) {
|
|
dev_err(hba->dev,
|
|
"%s: NULL power_mode_desc file, exiting", __func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.dme_local_read =
|
|
debugfs_create_file("dme_local_read", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_dme_local_read_ops);
|
|
if (!hba->debugfs_files.dme_local_read) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create dme_local_read debugfs entry\n",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.dme_peer_read =
|
|
debugfs_create_file("dme_peer_read", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_dme_peer_read_ops);
|
|
if (!hba->debugfs_files.dme_peer_read) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create dme_peer_read debugfs entry\n",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.dbg_print_en =
|
|
debugfs_create_file("dbg_print_en", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_dbg_print_en_ops);
|
|
if (!hba->debugfs_files.dbg_print_en) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create dbg_print_en debugfs entry\n",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.req_stats =
|
|
debugfs_create_file("req_stats", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.stats_folder, hba,
|
|
&ufsdbg_req_stats_desc);
|
|
if (!hba->debugfs_files.req_stats) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create req_stats debugfs entry\n",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
hba->debugfs_files.reset_controller =
|
|
debugfs_create_file("reset_controller", S_IRUSR | S_IWUSR,
|
|
hba->debugfs_files.debugfs_root, hba,
|
|
&ufsdbg_reset_controller);
|
|
if (!hba->debugfs_files.reset_controller) {
|
|
dev_err(hba->dev,
|
|
"%s: failed create reset_controller debugfs entry",
|
|
__func__);
|
|
goto err;
|
|
}
|
|
|
|
ufsdbg_setup_fault_injection(hba);
|
|
|
|
ufshcd_vops_add_debugfs(hba, hba->debugfs_files.debugfs_root);
|
|
|
|
return;
|
|
|
|
err:
|
|
debugfs_remove_recursive(hba->debugfs_files.debugfs_root);
|
|
hba->debugfs_files.debugfs_root = NULL;
|
|
err_no_root:
|
|
dev_err(hba->dev, "%s: failed to initialize debugfs\n", __func__);
|
|
}
|
|
|
|
void ufsdbg_remove_debugfs(struct ufs_hba *hba)
|
|
{
|
|
ufshcd_vops_remove_debugfs(hba);
|
|
debugfs_remove_recursive(hba->debugfs_files.debugfs_root);
|
|
kfree(hba->ufs_stats.tag_stats);
|
|
|
|
}
|