[PATCH v7 2/2] scsi: ufs: spacemit: k3: Add UFS Host Controller driver

Trevor Gamblin tgamblin at baylibre.com
Mon Nov 2 06:22:31 PST 2026


On Thu Oct 1, 2026 at 11:10 PM EDT, Yixun Lan wrote:
> SpacemiT K3 SoC consist of UFS (Universal Flash Storage) Host Controller
> which has features compatible with JEDEC UFS 2.2, MIPI UniPro v1.61 and
> M-PHY v3.0 standard.

Hi,

I've been testing this using a v7.3 kernel in Yocto's meta-riscv layer. It's
been working well on a K3 Pico-ITX with fastboot flash in our nightly CI. An
example:

https://forgejo.baylibre.com/tgamblin/boardgarden/actions/runs/827/jobs/9/attempt/1

Tested-by: Trevor Gamblin <tgamblin at baylibre.com>

> Tested-by: Aurelien Jarno <aurelien at aurel32.net>
> Tested-by: Anirudh Srinivasan <asrinivasan at oss.tenstorrent.com>
> Signed-off-by: Yixun Lan <dlan at kernel.org>
> ---
>  drivers/ufs/host/Kconfig        |  12 +
>  drivers/ufs/host/Makefile       |   1 +
>  drivers/ufs/host/ufs-spacemit.c | 652 ++++++++++++++++++++++++++++++++++++++++
>  drivers/ufs/host/ufs-spacemit.h |  77 +++++
>  4 files changed, 742 insertions(+)
>
> diff --git a/drivers/ufs/host/Kconfig b/drivers/ufs/host/Kconfig
> index ff170c0b6da0..6146872fcecc 100644
> --- a/drivers/ufs/host/Kconfig
> +++ b/drivers/ufs/host/Kconfig
> @@ -132,6 +132,18 @@ config SCSI_UFS_VARIABLE_SG_ENTRY_SIZE
>  	bool
>  	default y if SCSI_UFS_EXYNOS && SCSI_UFS_CRYPTO
>  
> +config SCSI_UFS_SPACEMIT
> +	tristate "SpacemiT UFS controller driver"
> +	depends on SCSI_UFSHCD_PLATFORM && (ARCH_SPACEMIT || COMPILE_TEST)
> +	help
> +	  This selects the SpacemiT specific additions to UFSHCD platform driver.
> +	  UFS host on SpacemiT needs some vendor specific configuration before
> +	  accessing the hardware which includes PHY configuration and vendor
> +	  specific registers.
> +
> +	  Select this if you have UFS controller on SpacemiT chipset.
> +	  If unsure, say N.
> +
>  config SCSI_UFS_SPRD
>  	tristate "Unisoc specific hooks to UFS controller platform driver"
>  	depends on SCSI_UFSHCD_PLATFORM && (ARCH_SPRD || COMPILE_TEST)
> diff --git a/drivers/ufs/host/Makefile b/drivers/ufs/host/Makefile
> index 7d8db67eb23c..a3c447b8fc21 100644
> --- a/drivers/ufs/host/Makefile
> +++ b/drivers/ufs/host/Makefile
> @@ -13,6 +13,7 @@ obj-$(CONFIG_SCSI_UFS_HISI) += ufs-hisi.o
>  obj-$(CONFIG_SCSI_UFS_MEDIATEK) += ufs-mediatek.o
>  obj-$(CONFIG_SCSI_UFS_RENESAS) += ufs-renesas.o
>  obj-$(CONFIG_SCSI_UFS_ROCKCHIP) += ufs-rockchip.o
> +obj-$(CONFIG_SCSI_UFS_SPACEMIT) += ufs-spacemit.o
>  obj-$(CONFIG_SCSI_UFS_SPRD) += ufs-sprd.o
>  obj-$(CONFIG_SCSI_UFS_TI_J721E) += ti-j721e-ufs.o
>  obj-$(CONFIG_SCSI_UFS_AMD_VERSAL2) += ufs-amd-versal2.o ufshcd-dwc.o
> diff --git a/drivers/ufs/host/ufs-spacemit.c b/drivers/ufs/host/ufs-spacemit.c
> new file mode 100644
> index 000000000000..65aeba393b0f
> --- /dev/null
> +++ b/drivers/ufs/host/ufs-spacemit.c
> @@ -0,0 +1,652 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) 2026 SpacemiT (Hangzhou) Technology Co. Ltd
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/delay.h>
> +#include <linux/io.h>
> +#include <linux/iopoll.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/reset.h>
> +#include <linux/slab.h>
> +
> +#include <ufs/ufshcd.h>
> +#include <ufs/ufshci.h>
> +#include <ufs/ufs_quirks.h>
> +#include <ufs/unipro.h>
> +
> +#include "ufshcd-pltfrm.h"
> +#include "ufs-spacemit.h"
> +
> +/* Special analog register */
> +#define ANA_EQ_CTRL_REG_ATTR		0x00CD
> +#define ANA_HSGEAR_CTRL_ATTR		0x00C1
> +
> +#define UFS_AXI_CLK_LOW_FREQ_HZ		409600000UL
> +
> +/* PHY register magic values */
> +#define MPHY_PU_ALL			0x87f
> +#define MPHY_PU_WITH_HB8_RESET		0xb7f
> +#define MPHY_DEVICE_REFCLK_OE		BIT(8)
> +#define MPHY_DEVICE_HW_RESET		BIT(0)
> +
> +#define MPHY_PLL_LOCK_BIT		BIT(31)
> +#define MPHY_PLL_LOCK_TIMEOUT_US	10000
> +
> +/* M-PHY FSM states */
> +#define MPHY_RX_FSM_STATE	0xC1
> +#define MPHY_TX_FSM_STATE	0x41
> +
> +static int ufs_spacemit_dme_set(struct ufs_hba *hba,
> +				const struct ufshcd_dme_attr_val *v, int n)
> +{
> +	int ret = 0;
> +	int attr_node = 0;
> +
> +	for (attr_node = 0; attr_node < n; attr_node++) {
> +		ret = ufshcd_dme_set(hba, v[attr_node].attr_sel,
> +				     v[attr_node].mib_val);
> +
> +		if (ret)
> +			return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int ufs_spacemit_check_hibern8(struct ufs_hba *hba)
> +{
> +	u32 tx_fsm_val_0 = 0;
> +	u32 tx_fsm_val_1 = 0;
> +	int retries = DIV_ROUND_UP(HBRN8_POLL_TOUT_MS * 1000, 100);
> +	int err = 0;
> +
> +	do {
> +		err = ufshcd_dme_get(hba,
> +				     UIC_ARG_MIB_SEL(MPHY_TX_FSM_STATE,
> +						     UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)),
> +				     &tx_fsm_val_0);
> +		if (err)
> +			break;
> +
> +		err = ufshcd_dme_get(hba,
> +				     UIC_ARG_MIB_SEL(MPHY_TX_FSM_STATE,
> +						     UIC_ARG_MPHY_TX_GEN_SEL_INDEX(1)),
> +				     &tx_fsm_val_1);
> +		if (err || (tx_fsm_val_0 == TX_FSM_HIBERN8 &&
> +			    tx_fsm_val_1 == TX_FSM_HIBERN8))
> +			break;
> +
> +		usleep_range(100, 200);
> +	} while (--retries > 0);
> +
> +	if (err) {
> +		dev_err(hba->dev, "%s: unable to get TX_FSM_STATE, err %d\n",
> +			__func__, err);
> +	} else if (tx_fsm_val_0 != TX_FSM_HIBERN8 ||
> +		   tx_fsm_val_1 != TX_FSM_HIBERN8) {
> +		err = -ETIMEDOUT;
> +		dev_err(hba->dev,
> +			"%s: invalid TX_FSM_STATE, lane0 = %u, lane1 = %u\n",
> +			__func__, tx_fsm_val_0, tx_fsm_val_1);
> +	}
> +
> +	return err;
> +}
> +
> +static u32 ufs_spacemit_get_sys1clk_1us(struct ufs_hba *hba)
> +{
> +	struct ufs_clk_info *clki, *clka = NULL;
> +	struct list_head *head = &hba->clk_list_head;
> +	unsigned long rate_hz = 0;
> +
> +	if (!list_empty(head)) {
> +		list_for_each_entry(clki, head, list) {
> +			if (clki->name && !strcmp(clki->name, "axi") && clki->clk) {
> +				clka = clki;
> +				break;
> +			}
> +		}
> +	}
> +
> +	if (clka && clka->clk)
> +		rate_hz = clk_get_rate(clka->clk);
> +
> +	if (!rate_hz)
> +		return 0;
> +
> +	return DIV_ROUND_CLOSEST(rate_hz, 1000000);
> +}
> +
> +static int ufs_spacemit_wait_mphy_pll_lock(struct ufs_hba *hba)
> +{
> +	u32 val;
> +	int err;
> +
> +	err = read_poll_timeout(ufshcd_readl, val, val & MPHY_PLL_LOCK_BIT,
> +				10, MPHY_PLL_LOCK_TIMEOUT_US, false, hba,
> +				UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +	if (err)
> +		dev_err(hba->dev, "M-PHY PLL lock timeout\n");
> +
> +	return err;
> +}
> +
> +/**
> + * ufs_spacemit_mphy_init
> + * @hba: host controller instance
> + */
> +static int ufs_spacemit_mphy_init(struct ufs_hba *hba)
> +{
> +	int ret;
> +
> +	/* reset all mphy logical */
> +	ufshcd_writel(hba, 0x003, UFS_PHY_MNG_BASE + UFS_MPHY_RST_CTRL);
> +
> +	/* power up all */
> +	ufshcd_writel(hba, MPHY_PU_ALL, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +	/* asserted ana_rx_hb8_reset */
> +	ufshcd_writel(hba, 0xb7f, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +	fsleep(500);
> +
> +	/* deasserted ana_rx_hb8_reset */
> +	ufshcd_writel(hba, MPHY_PU_ALL, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +	/* Enable reference clock & deassert ufs device reset */
> +	ufshcd_writel(hba, MPHY_DEVICE_REFCLK_OE | MPHY_DEVICE_HW_RESET,
> +		      UFS_PHY_MNG_BASE + UFS_DEVICE_IO_CTRL);
> +
> +	fsleep(1000);
> +
> +	ret = ufs_spacemit_wait_mphy_pll_lock(hba);
> +	if (ret < 0)
> +		return ret;
> +
> +	dev_dbg(hba->dev, "M-PHY PLL locked successfully\n");
> +
> +	ufshcd_writel(hba, 0x1, UFS_PHY_MNG_BASE + UFS_MPHY_BKDR_CTRL);
> +
> +	ufshcd_writel(hba, 0x40, UFS_ATOP_BASE + (0xC2 << 2));
> +
> +	ufshcd_writel(hba, 0x0, UFS_PHY_MNG_BASE + UFS_MPHY_BKDR_CTRL);
> +
> +	fsleep(2000);
> +
> +	dev_dbg(hba->dev, "M-PHY init completed\n");
> +
> +	return 0;
> +}
> +
> +static int ufs_spacemit_uniprov1p6_init(struct ufs_hba *hba)
> +{
> +	static const struct ufshcd_dme_attr_val setup_attrs[] = {
> +		{ UIC_ARG_MIB(PA_TXHSG1SYNCLENGTH), 0x4f },
> +		{ UIC_ARG_MIB(0x1553), 0xf }, /* PA_TXHSG1PREPARELENGTH */
> +		{ UIC_ARG_MIB(PA_TXHSG2SYNCLENGTH), 0x4f },
> +		{ UIC_ARG_MIB(0x1555), 0xf }, /* PA_TXHSG2PREPARELENGTH */
> +		{ UIC_ARG_MIB(PA_TXHSG3SYNCLENGTH), 0x4f },
> +		{ UIC_ARG_MIB(0x1557), 0xf }, /* PA_TXHSG3PREPARELENGTH */
> +		{ UIC_ARG_MIB(0x155A), 0x0 }, /* PA_TXMK2EXTENSION */
> +		{ UIC_ARG_MIB(0x155B), 0x1}, /* PA_PEERSCRAMBLING */
> +		{ UIC_ARG_MIB(0x155C), 0x1 }, /* PA_TXSKIP */
> +		{ UIC_ARG_MIB(0x155D), 250 }, /* PA_TXSKIPPERIOD */
> +		{ UIC_ARG_MIB(PA_LOCAL_TX_LCC_ENABLE), 0x0 },
> +		{ UIC_ARG_MIB(0x155F), 0x0 }, /* PA_PEER_TX_LCC_ENABLE */
> +		{ UIC_ARG_MIB(0x1585), 0x1 }, /* PA_SCRAMBLING */
> +		{ UIC_ARG_MIB(PA_GRANULARITY), 0x1 },
> +		{ UIC_ARG_MIB(0x15AB), 0x0 }, /* PA_MK2EXTENSIONGUARDBAND */
> +		{ UIC_ARG_MIB(PA_STALLNOCONFIGTIME), 15 },
> +		{ UIC_ARG_MIB(0x15A8), 0x64 }, /* PA_TACTIVATE */
> +		{ UIC_ARG_MIB(0x1564), 0x64 },	/* PA_TXTRAILINGCLOCKS */
> +		{ UIC_ARG_MIB_SEL(0x008D, 4), 0x0B }, /* RX_LS_PREPARELEN_TIME RX0 */
> +		{ UIC_ARG_MIB_SEL(0x008D, 5), 0x0B }, /* RX_LS_PREPARELEN_TIME RX1 */
> +
> +		/* RX_HIBERNATE_BKEN RX0 */
> +		{ UIC_ARG_MIB_SEL(0x00F4, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x9F },
> +		/* RX_HIBERNATE_BKEN RX1 */
> +		{ UIC_ARG_MIB_SEL(0x00F4, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x9F },
> +		/* PWM_BURST_closure_length */
> +		{ UIC_ARG_MIB_SEL(0x008E, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)),   15 },
> +		{ UIC_ARG_MIB_SEL(0x008E, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)),   15 },
> +		/* min_stall_not_config_time */
> +		{ UIC_ARG_MIB_SEL(0x0088, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0xFF },
> +		{ UIC_ARG_MIB_SEL(0x0088, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0xFF },
> +		/* TX HB8_TIME CAP */
> +		{ UIC_ARG_MIB_SEL(0x000F, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0x64 },
> +		{ UIC_ARG_MIB_SEL(0x000F, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(1)), 0x64 },
> +		/* RX HB8_TIME CAP */
> +		{ UIC_ARG_MIB_SEL(0x0092, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x64 },
> +		{ UIC_ARG_MIB_SEL(0x0092, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x64 },
> +		/* TX EQ 3DB */
> +		{ UIC_ARG_MIB_SEL(0x00CD, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)),  0x5 },
> +		/* RX garbage cnt = 32 SI */
> +		{ UIC_ARG_MIB_SEL(0x00F2, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x9F },
> +		{ UIC_ARG_MIB_SEL(0x00F2, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x9F },
> +	};
> +
> +	return ufs_spacemit_dme_set(hba, setup_attrs, ARRAY_SIZE(setup_attrs));
> +}
> +
> +static void ufs_spacemit_set_dev_cap(struct ufs_host_params *host_params, u32 pwr_hs)
> +{
> +	if (!host_params)
> +		return;
> +
> +	memset(host_params, 0, sizeof(struct ufs_host_params));
> +	host_params->tx_lanes = UFS_SPACEMIT_K3_LIMIT_NUM_LANES_TX;
> +	host_params->rx_lanes = UFS_SPACEMIT_K3_LIMIT_NUM_LANES_RX;
> +	host_params->hs_rx_gear = UFS_SPACEMIT_K3_LIMIT_HSGEAR_RX;
> +	host_params->hs_tx_gear = UFS_SPACEMIT_K3_LIMIT_HSGEAR_TX;
> +	host_params->pwm_rx_gear = UFS_SPACEMIT_K3_LIMIT_PWMGEAR_RX;
> +	host_params->pwm_tx_gear = UFS_SPACEMIT_K3_LIMIT_PWMGEAR_TX;
> +	host_params->rx_pwr_pwm = UFS_SPACEMIT_K3_LIMIT_RX_PWR_PWM;
> +	host_params->tx_pwr_pwm = UFS_SPACEMIT_K3_LIMIT_TX_PWR_PWM;
> +	host_params->rx_pwr_hs = pwr_hs;
> +	host_params->tx_pwr_hs = pwr_hs;
> +	host_params->hs_rate = UFS_SPACEMIT_K3_LIMIT_HS_RATE;
> +	host_params->desired_working_mode = UFS_HS_MODE;
> +}
> +
> +static int ufs_spacemit_link_startup_pre_change(struct ufs_hba *hba)
> +{
> +	u32 value, sys1clk_1us;
> +	int err;
> +
> +	err = ufs_spacemit_mphy_init(hba);
> +	if (err < 0)
> +		return err;
> +
> +	err = ufs_spacemit_uniprov1p6_init(hba);
> +	if (err < 0)
> +		return err;
> +
> +	/* config sysclk and tx symbol clk before link startup */
> +	value = UFS_MAX_LINKSTARTUP_TIMER;
> +
> +	/* clear bit0~bit3, select b0 design */
> +	value &= ~0xf;
> +
> +	ufshcd_writel(hba, value, UFS_PA_LINK_STARTUP_TIMER);
> +
> +	sys1clk_1us = ufs_spacemit_get_sys1clk_1us(hba);
> +	if (!sys1clk_1us)
> +		sys1clk_1us = DIV_ROUND_CLOSEST(UFS_AXI_CLK_LOW_FREQ_HZ, 1000000);
> +	ufshcd_writel(hba, sys1clk_1us, UFS_SYS1CLK_1US);
> +	ufshcd_writel(hba, UFS_TX_SYMBO_CLK, UFS_TX_SYMBOL_CLK_NS_US);
> +
> +	dev_dbg(hba->dev, "REG_UFS_SYS1CLK_1US: 0x%x\n",
> +		ufshcd_readl(hba, UFS_SYS1CLK_1US));
> +	dev_dbg(hba->dev, "REG_UFS_TX_SYMBOL_CLK_NS_US: 0x%x\n",
> +		ufshcd_readl(hba, UFS_TX_SYMBOL_CLK_NS_US));
> +
> +	return 0;
> +}
> +
> +static int ufs_spacemit_link_startup_post_change(struct ufs_hba *hba)
> +{
> +	/* Add 0xe8 make UFS2.1 run GEAR3 + 2Lane at 409M */
> +	static const struct ufshcd_dme_attr_val setup_attrs[] = {
> +		{ UIC_ARG_MIB_SEL(0xe8, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0x97 },
> +		{ UIC_ARG_MIB_SEL(0xe8, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0xd7 },
> +		{ UIC_ARG_MIB_SEL(0xe8, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0x17 },
> +		{ UIC_ARG_MIB(DL_AFC0REQTIMEOUTVAL), UFS_DL_AFC0REQTIMEOUTVAL_MAX },
> +	};
> +
> +	return ufs_spacemit_dme_set(hba, setup_attrs, ARRAY_SIZE(setup_attrs));
> +}
> +
> +static int ufs_spacemit_link_startup_notify(struct ufs_hba *hba,
> +					    enum ufs_notify_change_status status)
> +{
> +	int err = 0;
> +
> +	switch (status) {
> +	case PRE_CHANGE:
> +		err = ufs_spacemit_link_startup_pre_change(hba);
> +		break;
> +	case POST_CHANGE:
> +		err = ufs_spacemit_link_startup_post_change(hba);
> +		break;
> +	default:
> +		break;
> +	}
> +
> +	return err;
> +}
> +
> +static int ufs_spacemit_negotiate_pwr_mode(struct ufs_hba *hba,
> +					   const struct ufs_pa_layer_attr *dev_max_params,
> +					   struct ufs_pa_layer_attr *dev_req_params)
> +{
> +	struct ufs_host_params host_params;
> +	int ret;
> +
> +	ufs_spacemit_set_dev_cap(&host_params, FAST_MODE);
> +	ret = ufshcd_negotiate_pwr_params(&host_params, dev_max_params,
> +					  dev_req_params);
> +	if (ret) {
> +		dev_err(hba->dev, "Failed to negotiate power params: %d\n", ret);
> +		return ret;
> +	}
> +
> +	dev_dbg(hba->dev,
> +		"Power mode config - gear_rx:%d, gear_tx:%d, lane_rx:%d, lane_tx:%d, pwr_rx:%d, pwr_tx:%d, hs_rate:%d\n",
> +		dev_req_params->gear_rx, dev_req_params->gear_tx, dev_req_params->lane_rx,
> +		dev_req_params->lane_tx, dev_req_params->pwr_rx, dev_req_params->pwr_tx,
> +		dev_req_params->hs_rate);
> +
> +	return ret;
> +}
> +
> +static int ufs_spacemit_pwr_change_notify(struct ufs_hba *hba,
> +					  enum ufs_notify_change_status status,
> +					  struct ufs_pa_layer_attr *dev_req_params)
> +{
> +	int ret = 0;
> +
> +	if (!dev_req_params) {
> +		dev_err(hba->dev, "Invalid Parameters\n");
> +		return -EINVAL;
> +	}
> +
> +	switch (status) {
> +	case PRE_CHANGE:
> +		break;
> +	case POST_CHANGE:
> +		ret = ufs_spacemit_wait_mphy_pll_lock(hba);
> +		if (ret < 0)
> +			return ret;
> +
> +		dev_dbg(hba->dev, "M-PHY PLL locked after power mode change\n");
> +		/* Set ANA_HSGEAR_CTRL_ATTR back to default value */
> +		ufshcd_dme_set(hba, UIC_ARG_MIB(ANA_HSGEAR_CTRL_ATTR), 0x00);
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
> +
> +	return ret;
> +}
> +
> +static int ufs_spacemit_quirk_host_pa_saveconfigtime(struct ufs_hba *hba)
> +{
> +	int err;
> +	u32 pa_vs_config_reg1;
> +
> +	err = ufshcd_dme_get(hba, UIC_ARG_MIB(UFS_PA_VS_CONFIG_REG1), &pa_vs_config_reg1);
> +	if (err)
> +		return err;
> +
> +	/* Allow extension of MSB bits of PA_SaveConfigTime attribute */
> +	return ufshcd_dme_set(hba, UIC_ARG_MIB(UFS_PA_VS_CONFIG_REG1),
> +			      (pa_vs_config_reg1 | (1 << 12)));
> +}
> +
> +static int ufs_spacemit_apply_dev_quirks(struct ufs_hba *hba)
> +{
> +	static const struct ufshcd_dme_attr_val setup_attrs[] = {
> +		/* LCC_DISABLE */
> +		{ UIC_ARG_MIB_SEL(TX_LCC_ENABLE, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0 },
> +		{ UIC_ARG_MIB_SEL(TX_LCC_ENABLE, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(1)), 0 },
> +		/* TX_Min_ActivateTime */
> +		{ UIC_ARG_MIB_SEL(TX_MIN_ACTIVATETIME, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(0)), 0x0 },
> +		{ UIC_ARG_MIB_SEL(TX_MIN_ACTIVATETIME, UIC_ARG_MPHY_TX_GEN_SEL_INDEX(1)), 0x0 },
> +		{ UIC_ARG_MIB(ANA_HSGEAR_CTRL_ATTR), 0x25 },
> +	};
> +	int err;
> +
> +	if (hba->dev_quirks & UFS_DEVICE_QUIRK_HOST_PA_SAVECONFIGTIME) {
> +		err = ufs_spacemit_quirk_host_pa_saveconfigtime(hba);
> +		if (err < 0)
> +			return err;
> +	}
> +
> +	if (hba->dev_info.wmanufacturerid == UFS_VENDOR_WDC)
> +		hba->dev_quirks |= UFS_DEVICE_QUIRK_HOST_PA_TACTIVATE;
> +
> +	err = ufs_spacemit_dme_set(hba, setup_attrs, ARRAY_SIZE(setup_attrs));
> +	if (err < 0)
> +		return err;
> +
> +	return ufs_spacemit_wait_mphy_pll_lock(hba);
> +}
> +
> +/**
> + * ufs_spacemit_advertise_quirks - advertise the known Spacemit UFS controller quirks
> + * @hba: host controller instance
> + *
> + * Spacemit UFS host controller might have some non standard behaviours (quirks)
> + * than what is specified by UFSHCI specification. Advertise all such
> + * quirks to standard UFS host controller driver so standard takes them into
> + * account.
> + */
> +static void ufs_spacemit_advertise_quirks(struct ufs_hba *hba)
> +{
> +	hba->quirks |= UFSHCD_QUIRK_BROKEN_AUTO_HIBERN8;
> +}
> +
> +/**
> + * ufs_spacemit_init - init phy and prepare clk
> + * @hba: host controller instance
> + */
> +static int ufs_spacemit_init(struct ufs_hba *hba)
> +{
> +	struct device *dev = hba->dev;
> +	struct ufs_spacemit_host *host;
> +	struct reset_control *rst;
> +
> +	host = devm_kzalloc(dev, sizeof(*host), GFP_KERNEL);
> +	if (!host)
> +		return -ENOMEM;
> +
> +	rst = devm_reset_control_get_exclusive_deasserted(dev, NULL);
> +	if (IS_ERR(rst))
> +		return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset control\n");
> +
> +	host->hba = hba;
> +	ufshcd_set_variant(hba, host);
> +	ufs_spacemit_advertise_quirks(hba);
> +
> +	hba->caps |= UFSHCD_CAP_RPM_AUTOSUSPEND;
> +
> +	return 0;
> +}
> +
> +/**
> + * ufs_spacemit_device_reset - Toggle device reset line
> + * @hba: per-adapter instance
> + *
> + * Toggles the reset line to reset the attached UFS device.
> + *
> + * Returns: 0 on success
> + */
> +static int ufs_spacemit_device_reset(struct ufs_hba *hba)
> +{
> +	/* Disable reference clock & asserted ufs device reset */
> +	ufshcd_writel(hba, 0x000, UFS_PHY_MNG_BASE + UFS_DEVICE_IO_CTRL);
> +	usleep_range(10, 15);
> +
> +	/* Enable reference clock & deassert ufs device reset */
> +	ufshcd_writel(hba, MPHY_DEVICE_REFCLK_OE | MPHY_DEVICE_HW_RESET,
> +		      UFS_PHY_MNG_BASE + UFS_DEVICE_IO_CTRL);
> +	usleep_range(10, 15);
> +
> +	return 0;
> +}
> +
> +/**
> + * ufs_spacemit_event_notify - Handle UFS error events
> + * @hba: host controller instance
> + * @evt: event type
> + * @data: event-specific data
> + *
> + * Handles error events from UFS core, print warning messages
> + */
> +static void ufs_spacemit_event_notify(struct ufs_hba *hba, enum ufs_event_type evt, void *data)
> +{
> +	switch (evt) {
> +	case UFS_EVT_PA_ERR:
> +		if (data)
> +			dev_warn(hba->dev, "PA error event, INT errors:0x%x, PA_ERR_CODE:0x%x\n",
> +				 hba->errors, *(u32 *)data);
> +
> +		break;
> +	case UFS_EVT_DL_ERR:
> +		if (data)
> +			dev_warn(hba->dev, "DL error event, INT errors:0x%x, DL_ERR:0x%x\n",
> +				 hba->errors, *(u32 *)data);
> +
> +		break;
> +	case UFS_EVT_ABORT:
> +		dev_warn(hba->dev, "Abort event, INT errors:0x%x\n", hba->errors);
> +		break;
> +	default:
> +		break;
> +	}
> +}
> +
> +static void ufs_spacemit_pre_hibern8(struct ufs_hba *hba, enum uic_cmd_dme cmd)
> +{
> +	static const struct ufshcd_dme_attr_val setup_attrs[] = {
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x84 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x84 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x85 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x85 },
> +	};
> +	int ret;
> +
> +	if (cmd == UIC_CMD_DME_HIBER_EXIT) {
> +		/* Enable reference clock & deassert ufs device reset */
> +		ufshcd_writel(hba, MPHY_DEVICE_REFCLK_OE | MPHY_DEVICE_HW_RESET,
> +			      UFS_PHY_MNG_BASE + UFS_DEVICE_IO_CTRL);
> +
> +		/* Power up all */
> +		ufshcd_writel(hba, MPHY_PU_ALL, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +		/* Assert ana_rx_hb8_reset */
> +		ufshcd_writel(hba, MPHY_PU_WITH_HB8_RESET,
> +			      UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +		fsleep(500);
> +
> +		/* Deassert ana_rx_hb8_reset */
> +		ufshcd_writel(hba, MPHY_PU_ALL, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +		ret = ufs_spacemit_wait_mphy_pll_lock(hba);
> +		if (ret < 0)
> +			return;
> +
> +		ufshcd_dme_set(hba, UIC_ARG_MIB(0xdd), 0x57);
> +		ufshcd_dme_set(hba, UIC_ARG_MIB(0xe8), 0x57);
> +	}
> +
> +	if (cmd == UIC_CMD_DME_HIBER_ENTER)
> +		ufs_spacemit_dme_set(hba, setup_attrs, ARRAY_SIZE(setup_attrs));
> +}
> +
> +static void ufs_spacemit_post_hibern8(struct ufs_hba *hba, enum uic_cmd_dme cmd)
> +{
> +	static const struct ufshcd_dme_attr_val setup_attrs[] = {
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x84 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x84 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(0)), 0x80 },
> +		{ UIC_ARG_MIB_SEL(0xf1, UIC_ARG_MPHY_RX_GEN_SEL_INDEX(1)), 0x80 },
> +		{ UIC_ARG_MIB(0xdd), 0x57 },
> +		{ UIC_ARG_MIB(0xdd), 0xd7 },
> +		{ UIC_ARG_MIB(0xe8), 0x57 },
> +		{ UIC_ARG_MIB(0xe8), 0xd7 },
> +	};
> +	int err;
> +
> +	if (cmd == UIC_CMD_DME_HIBER_ENTER) {
> +		err = ufs_spacemit_check_hibern8(hba);
> +		if (err)
> +			dev_err(hba->dev, "ufs_spacemit_check_hibern8 error\n");
> +
> +		ufs_spacemit_dme_set(hba, setup_attrs, ARRAY_SIZE(setup_attrs));
> +
> +		/* Power down M-PHY */
> +		ufshcd_writel(hba, 0x0, UFS_PHY_MNG_BASE + UFS_MPHY_PU_CTRL);
> +
> +		/* Disable reference clock, keep device reset deasserted */
> +		ufshcd_writel(hba, MPHY_DEVICE_HW_RESET,
> +			      UFS_PHY_MNG_BASE + UFS_DEVICE_IO_CTRL);
> +	}
> +}
> +
> +/**
> + * ufs_spacemit_hibern8_notify - Handle hibernate enter/exit
> + * @hba: host controller instance
> + * @cmd: UIC command (HIBER_ENTER or HIBER_EXIT)
> + * @status: notification status
> + *
> + * Manages M-PHY power state during hibernate transitions.
> + */
> +static void ufs_spacemit_hibern8_notify(struct ufs_hba *hba, enum uic_cmd_dme cmd,
> +					enum ufs_notify_change_status status)
> +{
> +	if (status == PRE_CHANGE)
> +		ufs_spacemit_pre_hibern8(hba, cmd);
> +
> +	if (status == POST_CHANGE)
> +		ufs_spacemit_post_hibern8(hba, cmd);
> +}
> +
> +/**
> + * struct ufs_hba_spacemit_vops - UFS Spacemit specific variant operations
> + *
> + * The variant operations configure the necessary controller and PHY
> + * handshake during initialization.
> + */
> +static const struct ufs_hba_variant_ops ufs_hba_spacemit_vops = {
> +	.name = "ufshcd-spacemit",
> +	.init = ufs_spacemit_init,
> +	.link_startup_notify = ufs_spacemit_link_startup_notify,
> +	.negotiate_pwr_mode = ufs_spacemit_negotiate_pwr_mode,
> +	.pwr_change_notify = ufs_spacemit_pwr_change_notify,
> +	.device_reset = ufs_spacemit_device_reset,
> +	.event_notify = ufs_spacemit_event_notify,
> +	.apply_dev_quirks = ufs_spacemit_apply_dev_quirks,
> +	.hibern8_notify = ufs_spacemit_hibern8_notify,
> +};
> +
> +static const struct of_device_id __maybe_unused ufs_spacemit_of_match[] = {
> +	{ .compatible = "spacemit,k3-ufshc" },
> +	{ /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(of, ufs_spacemit_of_match);
> +
> +static int ufs_spacemit_probe(struct platform_device *pdev)
> +{
> +	return ufshcd_pltfrm_init(pdev, &ufs_hba_spacemit_vops);
> +}
> +
> +static void ufs_spacemit_remove(struct platform_device *pdev)
> +{
> +	ufshcd_pltfrm_remove(pdev);
> +}
> +
> +static const struct dev_pm_ops ufs_spacemit_pm_ops = {
> +	SET_SYSTEM_SLEEP_PM_OPS(ufshcd_system_suspend, ufshcd_system_resume)
> +	SET_RUNTIME_PM_OPS(ufshcd_runtime_suspend, ufshcd_runtime_resume, NULL)
> +	.prepare	 = ufshcd_suspend_prepare,
> +	.complete	 = ufshcd_resume_complete,
> +};
> +
> +static struct platform_driver ufs_spacemit_pltform = {
> +	.probe	= ufs_spacemit_probe,
> +	.remove	= ufs_spacemit_remove,
> +	.driver	= {
> +		.name	= "ufshcd-spacemit",
> +		.of_match_table = of_match_ptr(ufs_spacemit_of_match),
> +		.pm	= &ufs_spacemit_pm_ops,
> +	},
> +};
> +module_platform_driver(ufs_spacemit_pltform);
> +
> +MODULE_DESCRIPTION("SpacemiT UFS Host Driver");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/ufs/host/ufs-spacemit.h b/drivers/ufs/host/ufs-spacemit.h
> new file mode 100644
> index 000000000000..1415db85edd5
> --- /dev/null
> +++ b/drivers/ufs/host/ufs-spacemit.h
> @@ -0,0 +1,77 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * SpacemiT UFS Host Controller driver
> + *
> + * Copyright (c) 2026 SpacemiT (Hangzhou) Technology Co. Ltd
> + */
> +
> +#ifndef _UFS_SPACEMIT_H_
> +#define _UFS_SPACEMIT_H_
> +
> +#include <linux/regulator/consumer.h>
> +#include <linux/pm_qos.h>
> +#include <linux/workqueue.h>
> +
> +/* Spacemit K3 UFS host controller vendor specific registers */
> +#define UFS_SYS1CLK_1US			0xC0
> +#define UFS_TX_SYMBOL_CLK_NS_US		0xC4
> +#define UFS_LOCAL_PORT_ID_REG		0xC8
> +#define UFS_PA_ERR_CODE			0xCC
> +#define UFS_RETRY_TIMER_REG		0xD0
> +#define UFS_PA_LINK_STARTUP_TIMER	0xD8
> +#define UFS_CFG1			0xDC
> +
> +/* MPHY control registers */
> +#define UFS_PHY_MNG_BASE		0x1B00
> +#define UFS_MPHY_RST_CTRL		0x0
> +#define UFS_MPHY_PU_CTRL		0x4
> +#define UFS_MPHY_BKDR_CTRL		0x8
> +#define UFS_DEVICE_IO_CTRL		0xC
> +
> +/* ATOP base*/
> +#define UFS_ATOP_BASE			0x1C00
> +
> +#define UFS_TX_SYMBO_CLK		0x800
> +#define UFS_MAX_LINKSTARTUP_TIMER	0xFFFFFFFF
> +#define UFS_DL_AFC0REQTIMEOUTVAL_MAX	0xFFFF
> +
> +#define MPHY_TX_FSM_STATE		0x41
> +#define TX_FSM_HIBERN8			0x1
> +#define HBRN8_POLL_TOUT_MS		100
> +#define DEFAULT_CLK_RATE_HZ		1000000
> +#define BUS_VECTOR_NAME_LEN		32
> +
> +#define UFS_HW_VER_MAJOR_SHFT		28
> +#define UFS_HW_VER_MAJOR_MASK		(0x000F << UFS_HW_VER_MAJOR_SHFT)
> +#define UFS_HW_VER_MINOR_SHFT		16
> +#define UFS_HW_VER_MINOR_MASK		(0x0FFF << UFS_HW_VER_MINOR_SHFT)
> +#define UFS_HW_VER_STEP_SHFT		0
> +#define UFS_HW_VER_STEP_MASK		(0xFFFF << UFS_HW_VER_STEP_SHFT)
> +
> +#define UFS_SPACEMIT_K3_LIMIT_NUM_LANES_RX	2
> +#define UFS_SPACEMIT_K3_LIMIT_NUM_LANES_TX	2
> +#define UFS_SPACEMIT_K3_LIMIT_HSGEAR_RX		UFS_HS_G3
> +#define UFS_SPACEMIT_K3_LIMIT_HSGEAR_TX		UFS_HS_G3
> +#define UFS_SPACEMIT_K3_LIMIT_PWMGEAR_RX	UFS_PWM_G4
> +#define UFS_SPACEMIT_K3_LIMIT_PWMGEAR_TX	UFS_PWM_G4
> +#define UFS_SPACEMIT_K3_LIMIT_RX_PWR_PWM	SLOW_MODE
> +#define UFS_SPACEMIT_K3_LIMIT_TX_PWR_PWM	SLOW_MODE
> +#define UFS_SPACEMIT_K3_LIMIT_RX_PWR_HS		FAST_MODE
> +#define UFS_SPACEMIT_K3_LIMIT_TX_PWR_HS		FAST_MODE
> +#define UFS_SPACEMIT_K3_LIMIT_HS_RATE		PA_HS_MODE_B
> +#define UFS_SPACEMIT_K3_LIMIT_DESIRED_MODE	2
> +
> +#define UFS_PA_VS_CONFIG_REG1			0x9000
> +#define UFS_DME_VS_CORE_CLK_CTRL		0xD002
> +
> +#define UFS_HCLKDIV_REG 0xFC
> +
> +struct ufs_spacemit_host {
> +	struct ufs_hba *hba;
> +};
> +
> +struct ufshcd_dme_attr_val {
> +	u32 attr_sel;
> +	u32 mib_val;
> +};
> +#endif /* _UFS_SPACEMIT_H_ */





More information about the linux-riscv mailing list