UFS phy driver


UFS stands for Universal Flash Storage, and is a replacement (or enhancement) over SD protocol for storage. The hardware is present for 2-3 years in phones now but most likely lacks any actual storage attached, so it wasn’t really necessary to concern oneself with UFS for mainlining up until recently.

It contains a core driver + a phy driver, similar to how USB works. In the case of billie2, the phy is listed as compatible = "qcom,ufs-phy-qmp-v3-660"; in downstream, and mainline only has v3 (or sdm845, where it was first preset (possibly)). So we’ll have to add v3-660 support.


The UFS PHY drivers live in drivers/phy/qualcomm/phy-qcom-qmp.c in Mainline (if they are qmp, mine is). If you look at the implementation of sdm845 (closest cousin) you’ll see a bunch of properties, including:

  • 4 init tables: serdes, tx, rx, pcs that contain register + value
  • clocks, vregs (regulators/supplies)
  • regs – a small table with register remaps
  • nlanes — max number of children in DT
  • start_ctrl, pwddn_ctrl value to put in certain registers for start/stop
  • is_dual_lane_phy — this is somewhat completely orthogonal to nlanes, at least as far as code is concerned, and has to do with hardware initialization of one vs two lanes
  • no_pcs_sw_reset — whether reset happens via software (write to some register in driver), or “hardware” (using a reset property in DT).

Initialization Sequence

On downstream there is table_a and table_b, and on downstream there isn’t explicitly mentioned, but if you follow the code, you’ll see that serdes, tx, rx, pcs are used for initialization. The difference is that on downstream, all registers are specified with absolute offsets defined in the header drivers/phy/qualcomm/phy-qcom-ufs-qmp-v3-660.h:

/* QCOM UFS PHY control registers */
#define COM_BASE	0x000
#define COM_OFF(x)	(COM_BASE + x)
#define COM_SIZE	0x1C0

#define TX_BASE		0x400
#define TX_OFF(x)	(TX_BASE + x)
#define TX_SIZE		0x128

#define RX_BASE		0x600
#define RX_OFF(x)	(RX_BASE + x)
#define RX_SIZE		0x1FC

#define PHY_BASE	0xC00
#define PHY_OFF(x)	(PHY_BASE + x)
#define PHY_SIZE	0x1B4

On mainline however, these offsets are not set in the code. Instead they are specified as separate reg values:

/* example from qcom/msm8996.dtsi */
ufsphy: phy@627000 {
	/* ... */
	ufsphy_lane: lanes@627400 {
		reg = <0x627400 0x12c>,
		      <0x627600 0x200>,
		      <0x627c00 0x1b4>;
		#phy-cells = <0>;

All this means, that registers on Downstream are defined via absolute offset, and on mainline they are defined with relative offset (from the start of the block), but you have to know which block they belong to, so that is why they are split into 4 tables.

Note that the mainline approach, while a bit cleaner (no macros in the register declaration) is less flexible — i.e you can’t mix writes to registers from different blocks (at least not easily).

So to transform DS to ML, one has to take the A+B tables, (B table is just one register+value), sort them by type (register names helpfully have a meaningful prefix, like QSERDES_RX_ for rx registers, UFS_PHY_ is pcs), and then put them in the corresponding table in mainline. I did check the downstream v3 and mainline sdm845 and the values follow this rule with minuscule exceptions.

Clocks and vregs

These are easy. Clocks — just copied the sdm845 setup, after verifying that downstream 845 had same clocks as my downstream. Regulators (vregs) seem to be the same on all phys so just copied those as well.


Somewhat confusingly, there is nlanes and is_dual_lane_phy which seem redundant (based on name), but they actually describe different semantic lanes. The dual_lane_phy is related to additional rx/tx register banks (so more elements in the reg property shown above), where nlanes dictates the number of ufsphy_lane nodes. So is_dual_lane_phy is about lower-level lanes, and nlanes is higher-level lanes, (in multiples of 1 or 2 depending on num of lower level lanes).

In our specific case there is 1 lower-level lane (is_dual_lane_phy = false) and I can see only one higher level lane as well (so nlanes = 1). To be fair even the other nlanes = 2 platforms still have one specified in DT, so I can’t verify this theory. It could be carried over from other phys (like usb phys), and the phy nlanes are wrongly specified as two because somebody didn’t think it through… IDK.

Registers and misc values

The register table and start/power down values are mostly the same as 845 values.

The QPHY_PCS_READY_STATUS is set under pcs registers with BIT(0), same as ufs_qcom_phy_qmp_v3_660_start_serdes function in downstream.

QPHY_PCS_READY_STATUS is used with PCS_READY bit to poll until ready. Same as downstream ufs_qcom_phy_qmp_v3_660_is_pcs_ready.

QPHY_POWER_DOWN_CONTROL is used with .pwrdn_ctrl to shut things down. Downstream fn is ufs_qcom_phy_qmp_v3_660_power_control.


There wasn’t much to troubleshoot other than figuring out that init-tables need to be regrouped. Having 845 downstream code to compare to implemented mainline driver solved this mystery.

In general it’s a good idea to verify every single register value and find how it’s used. Never blindly copy code and hope it works.